diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index a272c4f..1a6371d 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -19,6 +19,7 @@ name = "hex-conservative" version = "0.3.0" dependencies = [ "arrayvec", + "if_rust_version", "serde", "serde_json", ] @@ -42,6 +43,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "itoa" version = "0.3.0" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 08aa332..636f2f8 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -13,6 +13,7 @@ name = "hex-conservative" version = "0.3.0" dependencies = [ "arrayvec", + "if_rust_version", "serde", "serde_json", ] @@ -36,6 +37,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "itoa" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index f64d0d6..2274b84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,15 @@ members = ["fuzz"] default = ["std"] std = ["alloc"] alloc = [] +# Enables detection of newer rust versions to provide additional features +# Turning it on may pull in dependencies that run build scripts and prolong compile time. +# This feature will do nothing once our MSRV supports version detection natively and will +# be removed using semver trick +newer-rust-version = ["dep:if_rust_version"] [dependencies] arrayvec = { version = "0.7.2", default-features = false } +if_rust_version = { version = "1.0.0", optional = true } serde = { version = "1.0", default-features = false, optional = true } diff --git a/README.md b/README.md index e1cd8d1..f65a2e2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,47 @@ -# Bitcoin Hexadecimal Library +# A Rust hexadecimal decoding library General purpose hex encoding/decoding library with a conservative MSRV and dependency policy. +## Stabilization strategy + +Because downstream crates may need to return hex errors in their APIs and they need to be +stabilized soon, this crate only exposes the errors and two basic decoding functions. This +should already help with the vast majority of the cases and we're sufficiently confident that +these errors won't have a breaking change any time soon (possibly never). + +If you're writing a binary you don't need to worry about any of this and just use the unstable +version for now. If you're writing a library you should use these stable errors in the API but +you may internally depend on the unstable crate version to get the advanced features that won't +affect your API. This way your API can stabilize before all features in this crate are fully +stable and you still can use all of them. + +## Crate feature flags + +* `std` - enables the standard library, on by default. +* `alloc` - enables features that require allocation such as decoding into `Vec`, implied +by `std`. +* `newer-rust-version` - enables Rust version detection and thus newer features, may add + dependency on a feature detection crate to reduce compile times. This + feature is expected to do nothing once the native detection is in Rust + and our MSRV is at least that version. We may also remove the feature + gate in 2.0 with semver trick once that happens. + ## Minimum Supported Rust Version (MSRV) This library should compile with almost any combination of features on **Rust 1.63.0**, however we reserve the right to use features to guard compiler specific code so `--all-features` may not work using the MSRV toolchain. -### Githooks +### Policy + +We don't intend to bump MSRV until the newer Rust version is at least two years old and also +included in Debian stable (1.63 is in Debian 12 at the moment). + +Note though that the dependencies may have looser policy. This is not considered +breaking/wrong - you would just need to pin them in `Cargo.lock` (not `.toml`). + + +## Githooks To assist devs in catching errors _before_ running CI we provide some githooks. If you do not already have locally configured githooks you can use the ones in this repository by running, in the diff --git a/contrib/test_vars.sh b/contrib/test_vars.sh index 78a7c7b..72f332f 100644 --- a/contrib/test_vars.sh +++ b/contrib/test_vars.sh @@ -5,10 +5,10 @@ # shellcheck disable=SC2034 # Test all these features with "std" enabled. -FEATURES_WITH_STD="serde" +FEATURES_WITH_STD="serde newer-rust-version" # Test all these features without "std" or "alloc" enabled. -FEATURES_WITHOUT_STD="alloc serde" +FEATURES_WITHOUT_STD="alloc serde newer-rust-version" # Run these examples. EXAMPLES="hexy:std wrap_array:std serde:std,serde" diff --git a/examples/hexy.rs b/examples/hexy.rs index e883109..bc70961 100644 --- a/examples/hexy.rs +++ b/examples/hexy.rs @@ -8,7 +8,9 @@ use std::fmt; use std::str::FromStr; -use hex_conservative::{fmt_hex_exact, Case, DisplayHex as _, FromHex as _, HexToArrayError}; +use hex_conservative::{ + fmt_hex_exact, Case, DecodeFixedSizedBytesError, DisplayHex as _, FromHex as _, +}; fn main() { let s = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe"; @@ -46,7 +48,7 @@ impl fmt::Display for Hexy { } impl FromStr for Hexy { - type Err = HexToArrayError; + type Err = DecodeFixedSizedBytesError; fn from_str(s: &str) -> Result { // Errors if the input is invalid diff --git a/examples/wrap_array.rs b/examples/wrap_array.rs index bb1fd2c..6f17cd5 100644 --- a/examples/wrap_array.rs +++ b/examples/wrap_array.rs @@ -7,7 +7,7 @@ use core::fmt; use core::str::FromStr; -use hex_conservative::{DisplayHex as _, FromHex as _, HexToArrayError}; +use hex_conservative::{DecodeFixedSizedBytesError, DisplayHex as _, FromHex as _}; fn main() { let hex = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe"; @@ -72,6 +72,6 @@ impl fmt::UpperHex for Wrap { } impl FromStr for Wrap { - type Err = HexToArrayError; + type Err = DecodeFixedSizedBytesError; fn from_str(s: &str) -> Result { Ok(Self(<[u8; 32]>::from_hex(s)?)) } } diff --git a/src/display.rs b/src/display.rs index ba58b4e..5ec21d8 100644 --- a/src/display.rs +++ b/src/display.rs @@ -25,13 +25,13 @@ //! assert_eq!(format!("{:0>8}", v.as_hex()), "0000abab"); //!``` -#[cfg(all(feature = "alloc", not(feature = "std")))] +#[cfg(feature = "alloc")] use alloc::string::String; use core::borrow::Borrow; use core::fmt; use super::Case; -#[cfg(any(test, feature = "std"))] +#[cfg(feature = "std")] use super::Table; use crate::buf_encoder::BufEncoder; @@ -607,16 +607,14 @@ where /// Given a `T:` [`fmt::Write`], `HexWriter` implements [`std::io::Write`] /// and writes the source bytes to its inner `T` as hex characters. -#[cfg(any(test, feature = "std"))] -#[cfg_attr(docsrs, doc(cfg(any(test, feature = "std"))))] +#[cfg(feature = "std")] #[derive(Debug)] pub struct HexWriter { writer: T, table: &'static Table, } -#[cfg(any(test, feature = "std"))] -#[cfg_attr(docsrs, doc(cfg(any(test, feature = "std"))))] +#[cfg(feature = "std")] impl HexWriter { /// Creates a `HexWriter` that writes the source bytes to `dest` as hex characters /// in the given `case`. @@ -627,8 +625,7 @@ impl HexWriter { pub fn into_inner(self) -> T { self.writer } } -#[cfg(any(test, feature = "std"))] -#[cfg_attr(docsrs, doc(cfg(any(test, feature = "std"))))] +#[cfg(feature = "std")] impl std::io::Write for HexWriter where T: core::fmt::Write, @@ -674,6 +671,7 @@ mod tests { use core::marker::PhantomData; use super::*; + use crate::alloc::vec::Vec; fn check_encoding(bytes: &[u8]) { use core::fmt::Write; @@ -1002,6 +1000,8 @@ mod tests { #[cfg(feature = "std")] mod std { + use alloc::string::String; + use alloc::vec::Vec; use std::io::Write as _; use arrayvec::ArrayString; diff --git a/src/error.rs b/src/error.rs index 99c83ff..7fe696b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,41 @@ use core::convert::Infallible; use core::fmt; +#[cfg(feature = "std")] +use std::error::Error as StdError; +#[cfg(all(not(feature = "std"), feature = "newer-rust-version"))] +if_rust_version::if_rust_version! { + >= 1.81 { + use core::error::Error as StdError; + } +} + +#[cfg(feature = "std")] +macro_rules! if_std_error { + ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => { + #[cfg_attr(docsrs, doc(cfg(any(feature = "std", all(feature = "newer-rust-version", rust_version = ">= 1.81.0")))))] + $($if_yes)* + } +} + +#[cfg(all(not(feature = "std"), feature = "newer-rust-version"))] +macro_rules! if_std_error { + ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => { + if_rust_version::if_rust_version! { + >= 1.81 { + #[cfg_attr(docsrs, doc(cfg(any(feature = "std", all(feature = "newer-rust-version", rust_version = ">= 1.81.0")))))] + $($if_yes)* + } $(else { $($if_not)* })? + } + } +} + +#[cfg(all(not(feature = "std"), not(feature = "newer-rust-version")))] +macro_rules! if_std_error { + ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => { + $($($if_not)*)? + } +} /// Formats error. /// @@ -13,101 +48,101 @@ use core::fmt; macro_rules! write_err { ($writer:expr, $string:literal $(, $args:expr)*; $source:expr) => { { - #[cfg(feature = "std")] - { - let _ = &$source; // Prevents clippy warnings. - write!($writer, $string $(, $args)*) - } - #[cfg(not(feature = "std"))] - { - write!($writer, concat!($string, ": {}") $(, $args)*, $source) + if_std_error! { + { + { + let _ = &$source; // Prevents clippy warnings. + write!($writer, $string $(, $args)*) + } + } else { + { + write!($writer, concat!($string, ": {}") $(, $args)*, $source) + } + } } } } } +pub(crate) use write_err; -/// Hex decoding error. +/// Error returned when hex decoding a hex string with variable length. +/// +/// This represents the first error encountered during decoding, however we may add other remaining +/// ones in the future. +/// +/// This error differs from [`DecodeFixedSizedBytesError`] in that the number of bytes is only known +/// at run time - e.g. when decoding `Vec`. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct HexToBytesError(pub(crate) ToBytesError); - -impl From for HexToBytesError { - #[inline] - fn from(never: Infallible) -> Self { match never {} } -} - -impl HexToBytesError { - /// Returns a [`ToBytesError`] from this [`HexToBytesError`]. - // Use clone instead of reference to give use maximum forward flexibility. - #[inline] - pub fn parse_error(&self) -> ToBytesError { self.0.clone() } -} - -impl fmt::Display for HexToBytesError { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } -} - -#[cfg(feature = "std")] -impl std::error::Error for HexToBytesError { - #[inline] - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) } -} - -impl From for HexToBytesError { - #[inline] - fn from(e: InvalidCharError) -> Self { Self(e.into()) } +pub enum DecodeDynSizedBytesError { + /// Non-hexadecimal character. + InvalidChar(InvalidCharError), + /// Purported hex string had odd (not even) length. + OddLengthString(OddLengthStringError), } -impl From for HexToBytesError { +impl DecodeDynSizedBytesError { + /// Adds `by_bytes` to all character positions stored inside. + /// + /// If you're parsing a larger string that consists of multiple hex sub-strings and want to + /// return `InvalidCharError` you may need to use this function so that the callers of your + /// parsing function can tell the exact position where decoding failed relative to the start of + /// the string passed into your parsing function. + /// + /// Note that this function has the standard Rust overflow behavior because you should only + /// ever pass in the position of the parsed hex string relative to the start of the entire + /// input. In that case overflow is impossible. + /// + /// This method consumes and returns `self` so that calling it inside a closure passed into + /// [`Result::map_err`] is convenient. + #[must_use] #[inline] - fn from(e: OddLengthStringError) -> Self { Self(e.into()) } -} + pub fn offset(self, by_bytes: usize) -> Self { + use DecodeDynSizedBytesError as E; -/// Hex decoding error while parsing to a vector of bytes. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ToBytesError { - /// Non-hexadecimal character. - InvalidChar(InvalidCharError), - /// Purported hex string had odd length. - OddLengthString(OddLengthStringError), + match self { + E::InvalidChar(e) => E::InvalidChar(e.offset(by_bytes)), + E::OddLengthString(e) => E::OddLengthString(e), + } + } } -impl From for ToBytesError { +impl From for DecodeDynSizedBytesError { #[inline] fn from(never: Infallible) -> Self { match never {} } } -impl fmt::Display for ToBytesError { +impl fmt::Display for DecodeDynSizedBytesError { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ToBytesError as E; + use DecodeDynSizedBytesError as E; match *self { - E::InvalidChar(ref e) => - write_err!(f, "invalid char, failed to create bytes from hex"; e), - E::OddLengthString(ref e) => - write_err!(f, "odd length, failed to create bytes from hex"; e), + E::InvalidChar(ref e) => write_err!(f, "failed to decode hex"; e), + E::OddLengthString(ref e) => write_err!(f, "failed to decode hex"; e), } } } -#[cfg(feature = "std")] -impl std::error::Error for ToBytesError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use ToBytesError as E; +if_std_error! {{ + impl StdError for DecodeDynSizedBytesError { + #[inline] + fn source(&self) -> Option<&(dyn StdError + 'static)> { + use DecodeDynSizedBytesError as E; - match *self { - E::InvalidChar(ref e) => Some(e), - E::OddLengthString(ref e) => Some(e), + match *self { + E::InvalidChar(ref e) => Some(e), + E::OddLengthString(ref e) => Some(e), + } } } -} +}} -impl From for ToBytesError { +impl From for DecodeDynSizedBytesError { #[inline] fn from(e: InvalidCharError) -> Self { Self::InvalidChar(e) } } -impl From for ToBytesError { +impl From for DecodeDynSizedBytesError { #[inline] fn from(e: OddLengthStringError) -> Self { Self::OddLengthString(e) } } @@ -127,21 +162,89 @@ impl From for InvalidCharError { impl InvalidCharError { /// Returns the invalid character byte. #[inline] - pub fn invalid_char(&self) -> u8 { self.invalid } + pub(crate) fn invalid_char(&self) -> u8 { self.invalid } /// Returns the position of the invalid character byte. #[inline] pub fn pos(&self) -> usize { self.pos } + + /// Adds `by_bytes` to all character positions stored inside. + /// + /// **Important**: if you have `DecodeDynSizedBytesError` or `DecodeFixedSizedBytesError` you + /// should call the method *on them* - do not match them and manually call this method. Doing + /// so may lead to broken behavior in the future. + /// + /// If you're parsing a larger string that consists of multiple hex sub-strings and want to + /// return `InvalidCharError` you may need to use this function so that the callers of your + /// parsing function can tell the exact position where decoding failed relative to the start of + /// the string passed into your parsing function. + /// + /// Note that this function has the standard Rust overflow behavior because you should only + /// ever pass in the position of the parsed hex string relative to the start of the entire + /// input. In that case overflow is impossible. + /// + /// This method consumes and returns `self` so that calling it inside a closure passed into + /// [`Result::map_err`] is convenient. + #[must_use] + #[inline] + pub fn offset(mut self, by_bytes: usize) -> Self { + self.pos += by_bytes; + self + } } +/// Note that the implementation displays position as 1-based instead of 0-based to be more +/// suitable to end users who might be non-programmers. impl fmt::Display for InvalidCharError { #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "invalid hex char {} at pos {}", self.invalid_char(), self.pos()) + // We're displaying this for general audience, not programmers, so we want to do 1-based + // position but that might confuse programmers who might think it's 0-based. Hopefully + // using more wordy approach will avoid the confusion. + + // format_args! would be simpler but we can't use it because of Rust issue #92698. + struct Format) -> fmt::Result>(F); + impl) -> fmt::Result> fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0(f) } + } + + // The lifetime is not extended in MSRV, so we need this. + let which; + let which: &dyn fmt::Display = match self.pos() { + 0 => &"1st", + 1 => &"2nd", + 2 => &"3rd", + pos => { + which = Format(move |f| write!(f, "{}th", pos + 1)); + &which + } + }; + + // The lifetime is not extended in MSRV, so we need these. + let chr_ascii; + let chr_non_ascii; + + let invalid_char = self.invalid_char(); + // We're currently not storing the entire character, so we need to make sure values >= + // 128 don't get misinterpreted as ISO-8859-1. + let chr: &dyn fmt::Display = if self.invalid_char().is_ascii() { + // Yes, the Debug output is correct here. Display would print the characters + // directly which would be confusing in case of control characters and it would + // also mess up the formatting. The `Debug` implementation of `char` properly + // escapes such characters. + chr_ascii = Format(move |f| write!(f, "{:?}", invalid_char as char)); + &chr_ascii + } else { + chr_non_ascii = Format(move |f| write!(f, "{:#02x}", invalid_char)); + &chr_non_ascii + }; + + write!(f, "the {} character, {}, is not a valid hex digit", which, chr) } } -#[cfg(feature = "std")] -impl std::error::Error for InvalidCharError {} +if_std_error! {{ + impl StdError for InvalidCharError {} +}} /// Purported hex string had odd length. #[derive(Debug, Clone, PartialEq, Eq)] @@ -163,93 +266,93 @@ impl OddLengthStringError { impl fmt::Display for OddLengthStringError { #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "odd hex string length {}", self.length()) + if self.length() == 1 { + write!(f, "the hex string is 1 byte long which is not an even number") + } else { + write!(f, "the hex string is {} bytes long which is not an even number", self.length()) + } } } -#[cfg(feature = "std")] -impl std::error::Error for OddLengthStringError {} - -/// Hex decoding error. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HexToArrayError(pub(crate) ToArrayError); +if_std_error! {{ + impl StdError for OddLengthStringError {} +}} -impl From for HexToArrayError { - #[inline] - fn from(never: Infallible) -> Self { match never {} } -} - -impl HexToArrayError { - /// Returns a [`ToArrayError`] from this [`HexToArrayError`]. - // Use clone instead of reference to give use maximum forward flexibility. - #[inline] - pub fn parse_error(&self) -> ToArrayError { self.0.clone() } -} - -impl fmt::Display for HexToArrayError { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } -} - -#[cfg(feature = "std")] -impl std::error::Error for HexToArrayError { - #[inline] - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) } -} - -impl From for HexToArrayError { - #[inline] - fn from(e: InvalidCharError) -> Self { Self(e.into()) } -} - -impl From for HexToArrayError { - #[inline] - fn from(e: InvalidLengthError) -> Self { Self(e.into()) } -} - -/// Hex decoding error while parsing a byte array. +/// Error returned when hex decoding bytes whose length is known at compile time. +/// +/// This error differs from [`DecodeDynSizedBytesError`] in that the number of bytes is known at +/// compile time - e.g. when decoding to an array of bytes. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ToArrayError { +pub enum DecodeFixedSizedBytesError { /// Non-hexadecimal character. InvalidChar(InvalidCharError), /// Tried to parse fixed-length hash from a string with the wrong length. InvalidLength(InvalidLengthError), } -impl From for ToArrayError { +impl DecodeFixedSizedBytesError { + /// Adds `by_bytes` to all character positions stored inside. + /// + /// If you're parsing a larger string that consists of multiple hex sub-strings and want to + /// return `InvalidCharError` you may need to use this function so that the callers of your + /// parsing function can tell the exact position where decoding failed relative to the start of + /// the string passed into your parsing function. + /// + /// Note that this function has the standard Rust overflow behavior because you should only + /// ever pass in the position of the parsed hex string relative to the start of the entire + /// input. In that case overflow is impossible. + /// + /// This method consumes and returns `self` so that calling it inside a closure passed into + /// [`Result::map_err`] is convenient. + #[must_use] + #[inline] + pub fn offset(self, by_bytes: usize) -> Self { + use DecodeFixedSizedBytesError as E; + + match self { + E::InvalidChar(e) => E::InvalidChar(e.offset(by_bytes)), + E::InvalidLength(e) => E::InvalidLength(e), + } + } +} + +impl From for DecodeFixedSizedBytesError { #[inline] fn from(never: Infallible) -> Self { match never {} } } -impl fmt::Display for ToArrayError { +impl fmt::Display for DecodeFixedSizedBytesError { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ToArrayError as E; + use DecodeFixedSizedBytesError as E; match *self { - E::InvalidChar(ref e) => write_err!(f, "failed to parse hex digit"; e), + E::InvalidChar(ref e) => write_err!(f, "failed to parse hex"; e), E::InvalidLength(ref e) => write_err!(f, "failed to parse hex"; e), } } } -#[cfg(feature = "std")] -impl std::error::Error for ToArrayError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use ToArrayError as E; +if_std_error! {{ + impl StdError for DecodeFixedSizedBytesError { + #[inline] + fn source(&self) -> Option<&(dyn StdError + 'static)> { + use DecodeFixedSizedBytesError as E; - match *self { - E::InvalidChar(ref e) => Some(e), - E::InvalidLength(ref e) => Some(e), + match *self { + E::InvalidChar(ref e) => Some(e), + E::InvalidLength(ref e) => Some(e), + } } } -} +}} -impl From for ToArrayError { +impl From for DecodeFixedSizedBytesError { #[inline] fn from(e: InvalidCharError) -> Self { Self::InvalidChar(e) } } -impl From for ToArrayError { +impl From for DecodeFixedSizedBytesError { #[inline] fn from(e: InvalidLengthError) -> Self { Self::InvalidLength(e) } } @@ -270,9 +373,16 @@ impl From for InvalidLengthError { impl InvalidLengthError { /// Returns the expected length. + /// + /// Note that this represents both the number of bytes and the number of characters that needs + /// to be passed into the decoder, since the hex digits are ASCII and thus always 1-byte long. #[inline] pub fn expected_length(&self) -> usize { self.expected } - /// Returns the position of the invalid character byte. + + /// Returns the number of *hex bytes* passed to the hex decoder. + /// + /// Note that this does not imply the number of characters nor hex digits since they may be + /// invalid (wide unicode chars). #[inline] pub fn invalid_length(&self) -> usize { self.invalid } } @@ -281,19 +391,24 @@ impl fmt::Display for InvalidLengthError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "invalid hex string length {} (expected {})", + // Note on singular vs plural: expected length is never odd, so it cannot be 1 + "the hex string is {} bytes long but exactly {} bytes were required", self.invalid_length(), self.expected_length() ) } } -#[cfg(feature = "std")] -impl std::error::Error for InvalidLengthError {} +if_std_error! {{ + impl StdError for InvalidLengthError {} +}} #[cfg(test)] #[cfg(feature = "std")] mod tests { + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + use super::*; use crate::FromHex; @@ -301,11 +416,12 @@ mod tests { assert!(error.source().is_some()); } + #[cfg(feature = "alloc")] #[test] fn invalid_char_error() { let result = as FromHex>::from_hex("12G4"); let error = result.unwrap_err(); - if let HexToBytesError(ToBytesError::InvalidChar(e)) = error { + if let DecodeDynSizedBytesError::InvalidChar(e) = error { assert!(!format!("{}", e).is_empty()); assert_eq!(e.invalid_char(), b'G'); assert_eq!(e.pos(), 2); @@ -314,13 +430,14 @@ mod tests { } } + #[cfg(feature = "alloc")] #[test] fn odd_length_string_error() { let result = as FromHex>::from_hex("123"); let error = result.unwrap_err(); assert!(!format!("{}", error).is_empty()); check_source(&error); - if let HexToBytesError(ToBytesError::OddLengthString(e)) = error { + if let DecodeDynSizedBytesError::OddLengthString(e) = error { assert!(!format!("{}", e).is_empty()); assert_eq!(e.length(), 3); } else { @@ -334,7 +451,7 @@ mod tests { let error = result.unwrap_err(); assert!(!format!("{}", error).is_empty()); check_source(&error); - if let HexToArrayError(ToArrayError::InvalidLength(e)) = error { + if let DecodeFixedSizedBytesError::InvalidLength(e) = error { assert!(!format!("{}", e).is_empty()); assert_eq!(e.expected_length(), 8); assert_eq!(e.invalid_length(), 3); @@ -345,14 +462,17 @@ mod tests { #[test] fn to_bytes_error() { - let error = ToBytesError::OddLengthString(OddLengthStringError { len: 7 }); + let error = DecodeDynSizedBytesError::OddLengthString(OddLengthStringError { len: 7 }); assert!(!format!("{}", error).is_empty()); check_source(&error); } #[test] fn to_array_error() { - let error = ToArrayError::InvalidLength(InvalidLengthError { expected: 8, invalid: 7 }); + let error = DecodeFixedSizedBytesError::InvalidLength(InvalidLengthError { + expected: 8, + invalid: 7, + }); assert!(!format!("{}", error).is_empty()); check_source(&error); } diff --git a/src/iter.rs b/src/iter.rs index 460c7ed..550b17b 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -9,7 +9,7 @@ use core::str; #[cfg(feature = "std")] use std::io; -#[cfg(all(feature = "alloc", not(feature = "std")))] +#[cfg(feature = "alloc")] use crate::alloc::vec::Vec; use crate::error::{InvalidCharError, OddLengthStringError}; use crate::{Case, Table}; @@ -72,7 +72,7 @@ impl<'a> HexToBytesIter> { /// /// This is equivalent to the combinator chain `iter().map().collect()` but was found by /// benchmarking to be faster. - #[cfg(any(test, feature = "std", feature = "alloc"))] + #[cfg(feature = "alloc")] pub(crate) fn drain_to_vec(self) -> Result, InvalidCharError> { let len = self.len(); let mut ret = Vec::with_capacity(len); @@ -350,6 +350,9 @@ where #[cfg(test)] mod tests { + #[cfg(feature = "alloc")] + use alloc::string::String; + use super::*; #[test] @@ -506,6 +509,7 @@ mod tests { ); } + #[cfg(feature = "alloc")] #[test] fn hex_to_bytes_vec_drain() { let hex = "deadbeef"; @@ -520,6 +524,7 @@ mod tests { assert!(got.is_empty()); } + #[cfg(feature = "alloc")] #[test] fn hex_to_bytes_vec_drain_first_char_error() { let hex = "geadbeef"; @@ -527,6 +532,7 @@ mod tests { assert_eq!(iter.drain_to_vec().unwrap_err(), InvalidCharError { invalid: b'g', pos: 0 }); } + #[cfg(feature = "alloc")] #[test] fn hex_to_bytes_vec_drain_middle_char_error() { let hex = "deadgeef"; @@ -534,6 +540,7 @@ mod tests { assert_eq!(iter.drain_to_vec().unwrap_err(), InvalidCharError { invalid: b'g', pos: 4 }); } + #[cfg(feature = "alloc")] #[test] fn hex_to_bytes_vec_drain_end_char_error() { let hex = "deadbeeg"; @@ -569,6 +576,7 @@ mod tests { } } + #[cfg(feature = "alloc")] #[test] fn roundtrip_forward() { let lower_want = "deadbeefcafebabe"; @@ -581,6 +589,7 @@ mod tests { assert_eq!(upper_got, upper_want); } + #[cfg(feature = "alloc")] #[test] fn roundtrip_backward() { let lower_want = "deadbeefcafebabe"; diff --git a/src/lib.rs b/src/lib.rs index bc6c9d7..2055bed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,15 +33,52 @@ //! ); //! # } //! ``` +//! +//! ## Stabilization strategy +//! +//! Because downstream crates may need to return hex errors in their APIs and they need to be +//! stabilized soon, this crate only exposes the errors and two basic decoding functions. This +//! should already help with the vast majority of the cases and we're sufficiently confident that +//! these errors won't have a breaking change any time soon (possibly never). +//! +//! If you're writing a binary you don't need to worry about any of this and just use the unstable +//! version for now. If you're writing a library you should use these stable errors in the API but +//! you may internally depend on the unstable crate version to get the advanced features that won't +//! affect your API. This way your API can stabilize before all features in this crate are fully +//! stable and you still can use all of them. +//! +//! ## Crate feature flags +//! +//! * `std` - enables the standard library, on by default. +//! * `alloc` - enables features that require allocation such as decoding into `Vec`, implied +//! by `std`. +//! * `newer-rust-version` - enables Rust version detection and thus newer features, may add +//! dependency on a feature detection crate to reduce compile times. This feature is expected to +//! do nothing once the native detection is in Rust and our MSRV is at least that version. We may +//! also remove the feature gate in 2.0 with semver trick once that happens. +//! +//! ## MSRV policy +//! +//! The MSRV of the crate is currently 1.63.0 and we don't intend to bump it until the newer Rust +//! version is at least two years old and also included in Debian stable (1.63 is in Debian 12 at +//! the moment). +//! +//! Note though that the dependencies may have looser policy. This is not considered breaking/wrong +//! - you would just need to pin them in `Cargo.lock` (not `.toml`). -#![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![no_std] // Experimental features we need. #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] // Coding conventions #![warn(missing_docs)] +#[cfg(feature = "std")] +extern crate std; + #[cfg(feature = "alloc")] +#[allow(unused_imports)] // false positive regarding macro +#[macro_use] extern crate alloc; #[doc(hidden)] @@ -76,28 +113,45 @@ pub(crate) use table::Table; pub use self::{ display::DisplayHex, error::{ - HexToArrayError, HexToBytesError, InvalidCharError, InvalidLengthError, - OddLengthStringError, ToArrayError, ToBytesError, + DecodeFixedSizedBytesError, DecodeDynSizedBytesError, InvalidCharError, InvalidLengthError, + OddLengthStringError, }, iter::{BytesToHexIter, HexToBytesIter, HexSliceToBytesIter}, parse::FromHex, }; -/// Decodes a hex string into a vector of bytes. +/// Decodes a hex string with variable length. +/// +/// The length of the returned `Vec` is determined by the length of the input, meaning all even +/// lengths of the input string are allowed. If you know the required length at compile time using +/// [`decode_to_array`] is most likely a better choice. /// /// # Errors /// -/// Errors if `s` is not a valid hex string. +/// Returns an error if `hex` contains invalid characters or doesn't have even length. #[cfg(feature = "alloc")] -pub fn decode_vec(s: &str) -> Result, HexToBytesError> { Vec::from_hex(s) } +pub fn decode_to_vec(hex: &str) -> Result, DecodeDynSizedBytesError> { + Ok(HexToBytesIter::new(hex)?.drain_to_vec()?) +} -/// Decodes a hex string into an array of bytes. +/// Decodes a hex string with an expected length known at compile time. +/// +/// If you don't know the required length at compile time you need to use [`decode_to_vec`] +/// instead. /// /// # Errors /// -/// Errors if `s` is not a valid hex string or the correct length. -pub fn decode_array(s: &str) -> Result<[u8; N], HexToArrayError> { - <[u8; N]>::from_hex(s) +/// Returns an error if `hex` contains invalid characters or has incorrect length. (Should be +/// `N * 2`.) +pub fn decode_to_array(hex: &str) -> Result<[u8; N], DecodeFixedSizedBytesError> { + if hex.len() == N * 2 { + let mut ret = [0u8; N]; + // checked above + HexToBytesIter::new_unchecked(hex).drain_to_slice(&mut ret)?; + Ok(ret) + } else { + Err(InvalidLengthError { invalid: hex.len(), expected: 2 * N }.into()) + } } /// Possible case of hex. @@ -185,6 +239,8 @@ macro_rules! test_hex_unwrap (($hex:expr) => ( as $crate::FromHex>::from #[cfg(test)] #[cfg(feature = "alloc")] mod tests { + use alloc::vec::Vec; + use crate::test_hex_unwrap as hex; #[test] diff --git a/src/parse.rs b/src/parse.rs index b3caaee..56753fd 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,13 +4,11 @@ use core::{fmt, str}; -#[cfg(all(feature = "alloc", not(feature = "std")))] +#[cfg(feature = "alloc")] use crate::alloc::vec::Vec; -use crate::error::InvalidLengthError; -use crate::iter::HexToBytesIter; #[rustfmt::skip] // Keep public re-exports separate. -pub use crate::error::{HexToBytesError, HexToArrayError}; +pub use crate::error::{DecodeDynSizedBytesError, DecodeFixedSizedBytesError}; /// Trait for objects that can be deserialized from hex strings. pub trait FromHex: Sized + sealed::Sealed { @@ -27,27 +25,16 @@ pub trait FromHex: Sized + sealed::Sealed { #[cfg(feature = "alloc")] impl FromHex for Vec { - type Error = HexToBytesError; + type Error = DecodeDynSizedBytesError; #[inline] - fn from_hex(s: &str) -> Result { - Ok(HexToBytesIter::new(s)?.drain_to_vec()?) - } + fn from_hex(s: &str) -> Result { crate::decode_to_vec(s) } } impl FromHex for [u8; LEN] { - type Error = HexToArrayError; - - fn from_hex(s: &str) -> Result { - if s.len() == LEN * 2 { - let mut ret = [0u8; LEN]; - // checked above - HexToBytesIter::new_unchecked(s).drain_to_slice(&mut ret)?; - Ok(ret) - } else { - Err(InvalidLengthError { invalid: s.len(), expected: 2 * LEN }.into()) - } - } + type Error = DecodeFixedSizedBytesError; + + fn from_hex(s: &str) -> Result { crate::decode_to_array(s) } } mod sealed { @@ -63,6 +50,7 @@ mod sealed { #[cfg(test)] mod tests { use super::*; + use crate::{HexToBytesIter, InvalidLengthError}; #[test] #[cfg(feature = "alloc")] diff --git a/tests/api.rs b/tests/api.rs index 2ed4622..701ec23 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -17,8 +17,9 @@ use core::{fmt, slice}; use hex_conservative::serde; // These imports test "typical" usage by user code. use hex_conservative::{ - buf_encoder, display, BytesToHexIter, Case, DisplayHex as _, HexToArrayError, HexToBytesError, - InvalidCharError, InvalidLengthError, OddLengthStringError, ToArrayError, ToBytesError, + buf_encoder, display, BytesToHexIter, Case, DecodeDynSizedBytesError, + DecodeFixedSizedBytesError, DisplayHex as _, InvalidCharError, InvalidLengthError, + OddLengthStringError, }; /// A struct that includes all public non-error enums. @@ -86,10 +87,8 @@ impl Structs<'_, slice::Iter<'_, u8>, String> { // These derives are the policy of `rust-bitcoin` not Rust API guidelines. #[derive(Debug, Clone, PartialEq, Eq)] // All public types implement Debug (C-DEBUG). struct Errors { - a: ToArrayError, - b: ToBytesError, - c: HexToArrayError, - d: HexToBytesError, + c: DecodeFixedSizedBytesError, + d: DecodeDynSizedBytesError, e: InvalidCharError, f: InvalidLengthError, g: OddLengthStringError,