diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4893ea4..6420200 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,6 +1,6 @@ name: build -on: push +on: pull_request jobs: codestyle: @@ -37,10 +37,10 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - rust: [stable, 1.56.1] + rust: [stable, 1.63.0] exclude: - os: macOS-latest - rust: 1.56.1 + rust: 1.63.0 runs-on: ${{ matrix.os }} steps: @@ -50,5 +50,9 @@ jobs: rust-version: ${{ matrix.rust }} - name: Checkout uses: actions/checkout@v2 + - name: Fixes for MSRV + if: matrix.rust == '1.63.0' + run: | + cargo update - name: Test run: cargo test diff --git a/README.md b/README.md index 631dbdf..65bbca4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The crate is `no_std` but does require `alloc`. ## MSRV -1.56.1 +1.63.0 ## License diff --git a/src/de.rs b/src/de.rs index fd7e84e..c5f3c48 100644 --- a/src/de.rs +++ b/src/de.rs @@ -25,7 +25,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T return Err(Error::Uri(UriError(UriErrorInner::TooShort))); } - if !string[..SCHEME.len()].eq_ignore_ascii_case(SCHEME) { + if !string.get(..SCHEME.len()).map_or(false, |s| s.eq_ignore_ascii_case(SCHEME)) { return Err(Error::Uri(UriError(UriErrorInner::InvalidScheme))); } @@ -42,6 +42,16 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T let mut label = None; let mut message = None; if let Some(params) = params { + // [RFC 3986 § 3.4](https://www.rfc-editor.org/rfc/rfc3986#section-3.4): + // + // > The query component is indicated by the first question + // > mark ("?") character and terminated by a number sign ("#") character + // > or by the end of the URI. + let params = match params.find('#') { + Some(pos) => ¶ms[..pos], + None => params, + }; + for param in params.split('&') { let pos = param .find('=') @@ -83,7 +93,7 @@ impl<'a, T: DeserializeParams<'a>> Uri<'a, bitcoin::address::NetworkUnchecked, T } } -impl<'a, NetVal: NetworkValidation, T> Uri<'a, NetVal, T> { +impl Uri<'_, NetVal, T> { /// Makes the lifetime `'static` by converting all fields to owned. /// /// Note that this does **not** affect `extras`! @@ -224,6 +234,7 @@ impl std::error::Error for Error< pub struct UriError(UriErrorInner); #[derive(Debug, Clone)] +#[cfg_attr(not(feature = "std"), allow(dead_code))] enum UriErrorInner { TooShort, InvalidScheme, @@ -283,7 +294,7 @@ impl std::error::Error for UriError { } /// **Warning**: this implementation may needlessly allocate, consider using `TryFrom<&str>` instead. -impl<'a, T: for<'de> DeserializeParams<'de>> core::str::FromStr for Uri<'a, bitcoin::address::NetworkUnchecked, T> { +impl DeserializeParams<'de>> core::str::FromStr for Uri<'_, bitcoin::address::NetworkUnchecked, T> { type Err = Error; fn from_str(s: &str) -> Result { @@ -300,7 +311,7 @@ impl<'a, T: DeserializeParams<'a>> TryFrom<&'a str> for Uri<'a, bitcoin::address } /// **Warning**: this implementation may needlessly allocate, consider using `TryFrom<&str>` instead. -impl<'a, T: for<'de> DeserializeParams<'de>> TryFrom for Uri<'a, bitcoin::address::NetworkUnchecked, T> { +impl DeserializeParams<'de>> TryFrom for Uri<'_, bitcoin::address::NetworkUnchecked, T> { type Error = Error; fn try_from(s: String) -> Result { diff --git a/src/lib.rs b/src/lib.rs index b149115..25af98c 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ where pub extras: Extras, } -impl<'a, NetVal: NetworkValidation, T: Default> Uri<'a, NetVal, T> { +impl Uri<'_, NetVal, T> { /// Creates an URI with defaults. /// /// This sets all fields except `address` to default values. @@ -118,7 +118,7 @@ impl<'a, NetVal: NetworkValidation, T: Default> Uri<'a, NetVal, T> { } } -impl<'a, NetVal: NetworkValidation, T> Uri<'a, NetVal, T> { +impl Uri<'_, NetVal, T> { /// Creates an URI with defaults. /// /// This sets all fields except `address` and `extras` to default values. @@ -194,7 +194,7 @@ impl<'a> From<&'a str> for Param<'a> { } /// Cheap conversion -impl<'a> From for Param<'a> { +impl From for Param<'_> { fn from(value: String) -> Self { Param(ParamInner::UnencodedString(Cow::Owned(value))) } @@ -212,7 +212,7 @@ impl<'a> From<&'a [u8]> for Param<'a> { /// Cheap conversion #[cfg(feature = "non-compliant-bytes")] #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))] -impl<'a> From> for Param<'a> { +impl From> for Param<'_> { fn from(value: Vec) -> Self { Param(ParamInner::UnencodedBytes(Cow::Owned(value))) } @@ -284,6 +284,7 @@ enum ParamInner<'a> { /// The lifetime of this may be shorter than that of [`Param<'a>`]. #[cfg(feature = "non-compliant-bytes")] #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))] +#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))] pub struct ParamBytes<'a>(ParamIterInner<'a, core::iter::Cloned>>); /// Iterator over decoded bytes inside paramter. @@ -291,6 +292,7 @@ pub struct ParamBytes<'a>(ParamIterInner<'a, core::iter::Cloned`]. #[cfg(feature = "non-compliant-bytes")] #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))] +#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))] pub struct ParamBytesOwned<'a>(ParamIterInner<'a, Either>, alloc::vec::IntoIter>>); #[cfg(feature = "non-compliant-bytes")] @@ -315,7 +317,7 @@ impl DeserializationError for NoExtras { type Error = core::convert::Infallible; } -impl<'de> DeserializationState<'de> for EmptyState { +impl DeserializationState<'_> for EmptyState { type Value = NoExtras; fn is_param_known(&self, _key: &str) -> bool { @@ -331,7 +333,7 @@ impl<'de> DeserializationState<'de> for EmptyState { } } -impl<'a> SerializeParams for &'a NoExtras { +impl SerializeParams for &NoExtras { type Key = core::convert::Infallible; type Value = core::convert::Infallible; type Iterator = core::iter::Empty<(Self::Key, Self::Value)>; @@ -426,5 +428,64 @@ mod tests { assert!(uri.amount.is_none()); assert!(uri.label.is_none()); assert!(uri.message.is_none()); + + assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd"); + } + + #[test] + fn label_with_rfc3986_param_separator() { + let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%26bar%20%3D%20baz/blah?;:@"; + let uri = input.parse::>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap(); + let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap(); + assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd"); + assert_eq!(label, "foo&bar = baz/blah?;:@"); + assert!(uri.amount.is_none()); + assert!(uri.message.is_none()); + + assert_eq!(uri.to_string(), input); + } + + #[test] + fn label_with_rfc3986_fragment_separator() { + let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%23bar"; + let uri = input.parse::>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap(); + let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap(); + assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd"); + assert_eq!(label, "foo#bar"); + assert!(uri.amount.is_none()); + assert!(uri.message.is_none()); + + assert_eq!(uri.to_string(), input); + } + + #[test] + fn rfc3986_empty_fragment_not_defined_in_bip21() { + let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#"; + let uri = input.parse::>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap(); + let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap(); + assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd"); + assert_eq!(label, "foo"); + assert!(uri.amount.is_none()); + assert!(uri.message.is_none()); + assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo"); + } + + #[test] + fn rfc3986_non_empty_fragment_not_defined_in_bip21() { + let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#&message=not%20part%20of%20a%20message"; + let uri = input.parse::>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap(); + let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap(); + assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd"); + assert_eq!(label, "foo"); + assert!(uri.amount.is_none()); + assert!(uri.message.is_none()); + assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo"); + } + + #[test] + fn bad_unicode_scheme() { + let input = "bitcoinö:1andreas3batLhQa2FawWjeyjCqyBzypd"; + let uri = input.parse::>(); + assert!(uri.is_err()); } } diff --git a/src/ser.rs b/src/ser.rs index 997a511..9451097 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -30,7 +30,7 @@ pub trait SerializeParams { /// Checks if the display implementation outputs `=` character. struct EqSignChecker<'a, W: fmt::Write>(W, &'a dyn fmt::Display); -impl<'a, W: fmt::Write> fmt::Write for EqSignChecker<'a, W> { +impl fmt::Write for EqSignChecker<'_, W> { fn write_str(&mut self, s: &str) -> fmt::Result { if s.contains('=') { panic!("key '{}' contains equal sign", self.1); @@ -47,7 +47,61 @@ impl<'a, W: fmt::Write> fmt::Write for EqSignChecker<'a, W> { } /// Set of characters that will be percent-encoded -const ASCII_SET: percent_encoding_rfc3986::AsciiSet = percent_encoding_rfc3986::CONTROLS.add(b'&').add(b'?').add(b' ').add(b'='); +/// +/// This contains anything not in `query` (i.e. ``gen-delim` from the quoted +/// definitions`) as per RFC 3986, as well as '&' and '=' as per BIP 21. +/// +/// [BIP 21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki#abnf-grammar): +/// +/// > ```text +/// > labelparam = "label=" *qchar +/// > messageparam = "message=" *qchar +/// > otherparam = qchar *qchar [ "=" *qchar ] +/// > ``` +/// ... +/// > Here, "qchar" corresponds to valid characters of an RFC 3986 URI query +/// > component, excluding the "=" and "&" characters, which this BIP takes as +/// > separators. +/// +/// [RFC 3986 Appendix A](https://www.rfc-editor.org/rfc/rfc3986#appendix-A): +/// +/// > ```text +/// > pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +/// > query = *( pchar / "/" / "?" ) +/// > ``` +/// ... +/// > ```text +/// > pct-encoded = "%" HEXDIG HEXDIG +/// > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +/// > ``` +/// ... +/// > ```text +/// > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +/// > / "*" / "+" / "," / ";" / "=" +/// > ``` +const ASCII_SET: percent_encoding_rfc3986::AsciiSet = percent_encoding_rfc3986::NON_ALPHANUMERIC + // allow non-alphanumeric characters from `unreserved` + .remove(b'-') + .remove(b'.') + .remove(b'_') + .remove(b'~') + // allow non-alphanumeric characters from `sub-delims` excluding bip-21 + // separators ("&", and "=") + .remove(b'!') + .remove(b'$') + .remove(b'\'') + .remove(b'(') + .remove(b')') + .remove(b'*') + .remove(b'+') + .remove(b',') + .remove(b';') + // allow pchar extra chars + .remove(b':') + .remove(b'@') + // allow query extra chars + .remove(b'/') + .remove(b'?'); /// Percent-encodes writes. struct WriterEncoder(W); @@ -74,7 +128,7 @@ impl fmt::Display for DisplayEncoder { /// This is private because people should generally only display values as decoded struct DisplayParam<'a>(&'a Param<'a>); -impl<'a> fmt::Display for DisplayParam<'a> { +impl fmt::Display for DisplayParam<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &(self.0).0 { // TODO: improve percent_encoding_rfc_3986 so that allocation can be avoided @@ -121,7 +175,7 @@ fn maybe_display_param(writer: &mut impl fmt::Write, key: impl fmt::Display, val /// Formats QR-code-optimized URI if alternate form (`{:#}`) is used. #[rustfmt::skip] -impl<'a, T> fmt::Display for Uri<'a, bitcoin::address::NetworkChecked, T> where for<'b> &'b T: SerializeParams { +impl fmt::Display for Uri<'_, bitcoin::address::NetworkChecked, T> where for<'a> &'a T: SerializeParams { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if f.alternate() { write!(f, "bitcoin:{:#}", self.address)?;