Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,29 @@ use percent_encoding_rfc3986::PercentDecodeError;

impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T> {
/// Implements deserialization.
///
/// Fails immediately on an unclaimed `req-` parameter, per BIP21.
fn deserialize_raw(string: &'a str) -> Result<Self, Error<T::Error>> {
Self::deserialize_raw_impl(string, false)
}

/// Like [`Self::deserialize_raw`], but does not fail on an unclaimed
/// `req-` parameter. Instead its key is recorded and made available
/// afterward through [`Uri::unsatisfied_requirements`].
///
/// This is a deliberately separate, opt-in entry point: the ordinary
/// `FromStr`/`TryFrom<&str>` parse path (via [`Self::deserialize_raw`])
/// keeps rejecting an unclaimed `req-` parameter outright, unchanged.
fn deserialize_raw_permissive(string: &'a str) -> Result<Self, Error<T::Error>> {
Self::deserialize_raw_impl(string, true)
}

/// Shared implementation for [`Self::deserialize_raw`] and
/// [`Self::deserialize_raw_permissive`]. `permissive` selects what
/// happens to an unclaimed `req-` parameter: `false` fails immediately
/// (today's behavior, unchanged), `true` records its key into
/// `unsatisfied_requirements` and keeps parsing.
fn deserialize_raw_impl(string: &'a str, permissive: bool) -> Result<Self, Error<T::Error>> {
const SCHEME: &str = "bitcoin:";
if string.len() < SCHEME.len() {
return Err(Error::Uri(UriError(UriErrorInner::TooShort)));
Expand All @@ -41,6 +63,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T
let mut amount = None;
let mut label = None;
let mut message = None;
let mut unsatisfied_requirements = alloc::vec::Vec::new();
if let Some(params) = params {
// [RFC 3986 § 3.4](https://www.rfc-editor.org/rfc/rfc3986#section-3.4):
//
Expand Down Expand Up @@ -84,7 +107,11 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T
let decoder = Param::decode(value).map_err(Error::percent_decode(key))?;
let is_known = deserializer.deserialize_borrowed(extra_key, decoder).map_err(Error::Extras)?;
if is_known == ParamKind::Unknown && extra_key.starts_with("req-") {
return Err(Error::Uri(UriError(UriErrorInner::UnknownRequiredParameter(extra_key.to_owned()))));
if permissive {
unsatisfied_requirements.push(extra_key.to_owned());
} else {
return Err(Error::Uri(UriError(UriErrorInner::UnknownRequiredParameter(extra_key.to_owned()))));
}
}
},
}
Expand All @@ -98,6 +125,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T
label,
message,
extras,
unsatisfied_requirements,
})
}
}
Expand All @@ -113,6 +141,7 @@ impl<NetVal: NetworkValidation, T> Uri<'_, NetVal, T> {
label: self.label.map(|label| label.decode_into_owned()),
message: self.message.map(|message| message.decode_into_owned()),
extras: self.extras,
unsatisfied_requirements: self.unsatisfied_requirements,
}
}
}
Expand Down Expand Up @@ -322,6 +351,34 @@ impl<'a, T: DeserializeParams<'a>> TryFrom<&'a str> for Uri<'a, bitcoin::address
}
}

impl<T: for<'de> DeserializeParams<'de>> Uri<'_, bitcoin::address::NetworkUnchecked, T> {
/// Parses like `s.parse::<Uri<'static, _, T>>()`, except an unclaimed
/// `req-` parameter does not fail parsing.
///
/// Its key is recorded instead, and can be read back afterward with
/// [`Uri::unsatisfied_requirements`]. This lets a caller decide for
/// itself, after seeing what was actually unsatisfied, whether to
/// accept the URI anyway - for example when the caller understands a
/// requirement that `T`'s `Extras` implementation does not claim, or
/// wants to surface the specific unmet requirement(s) to a user instead
/// of a generic parse failure.
///
/// **Warning**: like `FromStr`, this may needlessly allocate; consider
/// [`Uri::try_parse_permissive`] for zero-copy parsing instead.
pub fn parse_permissive(s: &str) -> Result<Uri<'static, bitcoin::address::NetworkUnchecked, T>, Error<T::Error>> {
Uri::deserialize_raw_permissive(s).map(Uri::into_static)
}
}

impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T> {
/// Zero-copy counterpart of [`Uri::parse_permissive`].
///
/// See [`Uri::parse_permissive`] for what "permissive" means here.
pub fn try_parse_permissive(s: &'a str) -> Result<Self, Error<T::Error>> {
Self::deserialize_raw_permissive(s)
}
}

/// **Warning**: this implementation may needlessly allocate, consider using `TryFrom<&str>` instead.
impl<T: for<'de> DeserializeParams<'de>> TryFrom<String> for Uri<'_, bitcoin::address::NetworkUnchecked, T> {
type Error = Error<T::Error>;
Expand Down Expand Up @@ -355,6 +412,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T
label: self.label,
message: self.message,
extras: self.extras,
unsatisfied_requirements: self.unsatisfied_requirements,
})
}

Expand All @@ -366,6 +424,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T
label: self.label,
message: self.message,
extras: self.extras,
unsatisfied_requirements: self.unsatisfied_requirements,
}
}
}
114 changes: 114 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ where

/// Extra fields that can occur in a BIP21 URI.
pub extras: Extras,

/// `req-` parameter keys that were present in the URI but not claimed
/// by `extras`.
///
/// This is only ever non-empty on a `Uri` produced by
/// [`Uri::parse_permissive`]. The ordinary `FromStr`/`TryFrom<&str>`
/// parse path rejects an unclaimed `req-` parameter outright (per
/// BIP21), so a `Uri` obtained that way never has one left to report
/// here.
unsatisfied_requirements: alloc::vec::Vec<String>,
}

impl<NetVal: NetworkValidation, T: Default> Uri<'_, NetVal, T> {
Expand All @@ -114,6 +124,7 @@ impl<NetVal: NetworkValidation, T: Default> Uri<'_, NetVal, T> {
label: None,
message: None,
extras: Default::default(),
unsatisfied_requirements: alloc::vec::Vec::new(),
}
}
}
Expand All @@ -130,8 +141,20 @@ impl<NetVal: NetworkValidation, T> Uri<'_, NetVal, T> {
label: None,
message: None,
extras,
unsatisfied_requirements: alloc::vec::Vec::new(),
}
}

/// Returns the `req-` parameter keys that were present in the URI but
/// not claimed by `extras`.
///
/// Always empty unless this `Uri` was produced by
/// [`Uri::parse_permissive`]: parsing via `FromStr`/`TryFrom<&str>`
/// already fails the moment an unclaimed `req-` parameter is seen, so
/// there is never one left to report through this method.
pub fn unsatisfied_requirements(&self) -> &[String] {
&self.unsatisfied_requirements
}
}

/// Abstracted stringly parameter in the URI.
Expand Down Expand Up @@ -503,4 +526,95 @@ mod tests {
let uri = duplicate_message.parse::<Uri<'_, _>>();
assert!(uri.is_err());
}

#[test]
fn ordinary_parse_never_reports_unsatisfied_requirements() {
// `FromStr`/`TryFrom<&str>` still fail immediately on an unclaimed
// `req-`, so a `Uri` obtained that way never has one to report -
// `unsatisfied_requirements()` stays empty for every already-passing
// parse, e.g. one with no `req-` params at all.
let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=Luke-Jr";
let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
assert!(uri.unsatisfied_requirements().is_empty());
}

#[test]
fn parse_permissive_records_an_unclaimed_req_key_instead_of_failing() {
let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?req-somethingyoudontunderstand=50";
let uri = Uri::<'_, _>::parse_permissive(input)
.unwrap()
.require_network(bitcoin::Network::Bitcoin)
.unwrap();
assert_eq!(uri.unsatisfied_requirements(), ["req-somethingyoudontunderstand"]);
}

#[allow(clippy::inconsistent_digit_grouping)] // Use sats/bitcoin when grouping.
#[test]
fn parse_permissive_still_parses_normally_when_nothing_is_unsatisfied() {
let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=20.3&label=Luke-Jr";
let uri = Uri::<'_, _>::parse_permissive(input)
.unwrap()
.require_network(bitcoin::Network::Bitcoin)
.unwrap();
assert!(uri.unsatisfied_requirements().is_empty());
assert_eq!(uri.amount, Some(bitcoin::Amount::from_sat(20_30_000_000)));
}

#[test]
fn try_parse_permissive_is_zero_copy_and_records_unclaimed_req_keys() {
let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?req-somethingyoudontunderstand=50";
let uri = Uri::<'_, _>::try_parse_permissive(input).unwrap();
assert_eq!(uri.unsatisfied_requirements(), ["req-somethingyoudontunderstand"]);
}

/// A minimal `Extras` that claims exactly one `req-` key, to check that
/// claiming one `req-` key does not excuse an unrelated unclaimed one:
/// only the unclaimed one should show up in `unsatisfied_requirements`.
#[derive(Debug, Default, Clone)]
struct ReqFooExtras;

#[derive(Debug, Default, Clone)]
struct ReqFooState;

impl crate::de::DeserializationError for ReqFooExtras {
type Error = core::convert::Infallible;
}

impl crate::de::DeserializeParams<'_> for ReqFooExtras {
type DeserializationState = ReqFooState;
}

impl crate::de::DeserializationState<'_> for ReqFooState {
type Value = ReqFooExtras;

fn is_param_known(&self, key: &str) -> bool {
key == "req-foo"
}

fn deserialize_temp(&mut self, key: &str, _value: crate::Param<'_>) -> Result<crate::de::ParamKind, core::convert::Infallible> {
Ok(if key == "req-foo" {
crate::de::ParamKind::Known
} else {
crate::de::ParamKind::Unknown
})
}

fn finalize(self) -> Result<Self::Value, core::convert::Infallible> {
Ok(ReqFooExtras)
}
}

#[test]
fn parse_permissive_records_only_unclaimed_req_keys_not_claimed_ones() {
let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?req-foo=50&req-bar=1";
let uri = Uri::<'_, _, ReqFooExtras>::parse_permissive(input)
.unwrap()
.require_network(bitcoin::Network::Bitcoin)
.unwrap();
assert_eq!(uri.unsatisfied_requirements(), ["req-bar"]);

// The strict path still fails on this same input.
let strict = input.parse::<Uri<'_, _, ReqFooExtras>>();
assert!(strict.is_err());
}
}