From e00dfa980615d57446215eae58e63689f48f0c7d Mon Sep 17 00:00:00 2001 From: connormullett Date: Thu, 17 Nov 2022 22:06:57 -0500 Subject: [PATCH] impl FromHexStr for structs with single u32 member Adds new module `string` to be later converted to its own crate. The module currently contains the FromHexStr trait and an error type to be used for implementing hex parsing on types. This change also adds implementations of FromHexStr for types with a single u32 member such as `Sequence(pub u32)`. All structs that match the following regex have been given this implementation `\(u32\)` and `\(pub u32\)`. All implementations have associated unit tests matching all possible cases. NonStandardSighashType has been ommitted from this change as it is an error and should not be constructed using the methods added in this change. Adds parse::hex_u32 for future use to be made generic to allow different sizes of integers to be parsed from hex strings. The error type FromHexError implements required traits such as Display and std::error::Error --- bitcoin/src/amount.rs | 1 + bitcoin/src/blockdata/locktime/absolute.rs | 89 ++++++++++++++++++++++ bitcoin/src/blockdata/transaction.rs | 29 +++++++ bitcoin/src/lib.rs | 1 + bitcoin/src/parse.rs | 9 +++ bitcoin/src/pow.rs | 32 ++++++++ bitcoin/src/string.rs | 76 ++++++++++++++++++ 7 files changed, 237 insertions(+) create mode 100644 bitcoin/src/string.rs diff --git a/bitcoin/src/amount.rs b/bitcoin/src/amount.rs index 5e32ad94..14cb9c92 100644 --- a/bitcoin/src/amount.rs +++ b/bitcoin/src/amount.rs @@ -1403,6 +1403,7 @@ pub mod serde { mod verification { use std::cmp; use std::convert::TryInto; + use super::*; // Note regarding the `unwind` parameter: this defines how many iterations diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs index 1af540e4..f02c29a6 100644 --- a/bitcoin/src/blockdata/locktime/absolute.rs +++ b/bitcoin/src/blockdata/locktime/absolute.rs @@ -19,6 +19,7 @@ use crate::error::ParseIntError; use crate::io::{self, Read, Write}; use crate::prelude::*; use crate::parse::{self, impl_parse_str_through_int}; +use crate::string::FromHexStr; #[cfg(docsrs)] use crate::absolute; @@ -86,6 +87,15 @@ impl fmt::Display for PackedLockTime { } } +impl FromHexStr for PackedLockTime { + type Error = Error; + + fn from_hex_str_no_prefix + Into>(s: S) -> Result { + let packed_lock_time = crate::parse::hex_u32(s)?; + Ok(Self(packed_lock_time)) + } +} + impl Encodable for PackedLockTime { #[inline] fn consensus_encode(&self, w: &mut W) -> Result { @@ -482,6 +492,16 @@ impl fmt::Display for Height { } } +impl FromHexStr for Height { + type Error = Error; + + fn from_hex_str_no_prefix + Into>(s: S) -> Result { + let height = crate::parse::hex_u32(s)?; + Self::from_consensus(height) + } +} + + impl FromStr for Height { type Err = Error; @@ -565,6 +585,15 @@ impl fmt::Display for Time { } } +impl FromHexStr for Time { + type Error = Error; + + fn from_hex_str_no_prefix + Into>(s: S) -> Result { + let time = crate::parse::hex_u32(s)?; + Time::from_consensus(time) + } +} + impl FromStr for Time { type Err = Error; @@ -750,4 +779,64 @@ mod tests { let got = format!("{:#}", n); assert_eq!(got, "block-height 100"); } + + #[test] + fn time_from_str_hex_happy_path() { + let actual = Time::from_hex_str("0x6289C350").unwrap(); + let expected = Time::from_consensus(0x6289C350).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn time_from_str_hex_no_prefix_happy_path() { + let time = Time::from_hex_str_no_prefix("6289C350").unwrap(); + assert_eq!(time, Time(0x6289C350)); + } + + #[test] + fn time_from_str_hex_invalid_hex_should_err() { + let hex = "0xzb93"; + let result = Time::from_hex_str(hex); + assert!(result.is_err()); + } + + #[test] + fn packed_lock_time_from_str_hex_happy_path() { + let actual = PackedLockTime::from_hex_str("0xBA70D").unwrap(); + let expected = PackedLockTime(0xBA70D); + assert_eq!(actual, expected); + } + + #[test] + fn packed_lock_time_from_str_hex_no_prefix_happy_path() { + let lock_time = PackedLockTime::from_hex_str_no_prefix("BA70D").unwrap(); + assert_eq!(lock_time, PackedLockTime(0xBA70D)); + } + + #[test] + fn packed_lock_time_from_str_hex_invalid_hex_should_ergr() { + let hex = "0xzb93"; + let result = PackedLockTime::from_hex_str(hex); + assert!(result.is_err()); + } + + #[test] + fn height_from_str_hex_happy_path() { + let actual = Height::from_hex_str("0xBA70D").unwrap(); + let expected = Height(0xBA70D); + assert_eq!(actual, expected); + } + + #[test] + fn height_from_str_hex_no_prefix_happy_path() { + let height = Height::from_hex_str_no_prefix("BA70D").unwrap(); + assert_eq!(height, Height(0xBA70D)); + } + + #[test] + fn height_from_str_hex_invalid_hex_should_err() { + let hex = "0xzb93"; + let result = Height::from_hex_str(hex); + assert!(result.is_err()); + } } diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index df3e2493..72d3ff04 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -15,6 +15,7 @@ use crate::prelude::*; use crate::io; +use crate::string::FromHexStr; use core::{fmt, str, default::Default}; use core::convert::TryFrom; @@ -404,6 +405,15 @@ impl Sequence { } } +impl FromHexStr for Sequence { + type Error = crate::parse::ParseIntError; + + fn from_hex_str_no_prefix + Into>(s: S) -> Result { + let sequence = crate::parse::hex_u32(s)?; + Ok(Self::from_consensus(sequence)) + } +} + impl Default for Sequence { /// The default value of sequence is 0xffffffff. fn default() -> Self { @@ -1446,6 +1456,25 @@ mod tests { assert!(unit_time_lock.is_rbf()); assert!(!lock_time_disabled.is_relative_lock_time()); } + + #[test] + fn sequence_from_str_hex_happy_path() { + let sequence = Sequence::from_hex_str("0xFFFFFFFF").unwrap(); + assert_eq!(sequence, Sequence::MAX); + } + + #[test] + fn sequence_from_str_hex_no_prefix_happy_path() { + let sequence = Sequence::from_hex_str_no_prefix("FFFFFFFF").unwrap(); + assert_eq!(sequence, Sequence::MAX); + } + + #[test] + fn sequence_from_str_hex_invalid_hex_should_err() { + let hex = "0xzb93"; + let result = Sequence::from_hex_str(hex); + assert!(result.is_err()); + } } #[cfg(bench)] diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index c7910da3..e5a4dbd8 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -107,6 +107,7 @@ pub mod pow; pub mod psbt; pub mod sighash; pub mod sign_message; +pub mod string; pub mod taproot; pub mod util; diff --git a/bitcoin/src/parse.rs b/bitcoin/src/parse.rs index fd69cbf2..67f3dfd2 100644 --- a/bitcoin/src/parse.rs +++ b/bitcoin/src/parse.rs @@ -84,6 +84,15 @@ pub(crate) fn int + Into>(s: S) -> Result + Into>(s: S) -> Result { + u32::from_str_radix(s.as_ref(), 16).map_err(|error| ParseIntError { + input: s.into(), + bits: u8::try_from(core::mem::size_of::() * 8).expect("max is 32 bits for u32"), + is_signed: u32::try_from(-1i8).is_ok(), + source: error, + }) +} + impl_std_error!(ParseIntError, source); /// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using `fn` diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 7f253b24..b59699ad 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -15,6 +15,8 @@ use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; use crate::hash_types::BlockHash; use crate::io::{self, Read, Write}; +use crate::prelude::String; +use crate::string::FromHexStr; /// Implements $int * $ty. Requires `u64::from($int)`. macro_rules! impl_int_mul { @@ -283,6 +285,15 @@ impl From for Target { fn from(c: CompactTarget) -> Self { Target::from_compact(c) } } +impl FromHexStr for CompactTarget { + type Error = crate::parse::ParseIntError; + + fn from_hex_str_no_prefix + Into>(s: S) -> Result { + let compact_target = crate::parse::hex_u32(s)?; + Ok(Self::from_consensus(compact_target)) + } +} + impl Encodable for CompactTarget { #[inline] fn consensus_encode(&self, w: &mut W) -> Result { @@ -1403,6 +1414,27 @@ mod tests { .is_err()); // invalid length } + #[test] + fn compact_target_from_hex_str_happy_path() { + let actual = CompactTarget::from_hex_str("0x01003456").unwrap(); + let expected = CompactTarget(0x01003456); + assert_eq!(actual, expected); + } + + #[test] + fn compact_target_from_hex_str_no_prefix_happy_path() { + let actual = CompactTarget::from_hex_str_no_prefix("01003456").unwrap(); + let expected = CompactTarget(0x01003456); + assert_eq!(actual, expected); + } + + #[test] + fn compact_target_from_hex_invalid_hex_should_err() { + let hex = "0xzbf9"; + let result = CompactTarget::from_hex_str(hex); + assert!(result.is_err()); + } + #[test] fn target_from_compact() { // (nBits, target) diff --git a/bitcoin/src/string.rs b/bitcoin/src/string.rs new file mode 100644 index 00000000..8f96e159 --- /dev/null +++ b/bitcoin/src/string.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Bitcoin string parsing utilities. +//! +//! This module provides utility types and traits +//! to support handling and parsing strings within `rust-bitcoin`. + +use core::fmt; + +use bitcoin_internals::write_err; + +use crate::prelude::String; + +/// Trait that allows types to be initialized from hex strings +pub trait FromHexStr: Sized { + /// An error occurred while parsing the hex string. + type Error; + + /// Parses provided string as hex requiring 0x prefix. + /// + /// This is intended for user-supplied inputs or already-existing protocols in which 0x prefix is used. + fn from_hex_str + Into>(s: S) -> Result> { + if !s.as_ref().starts_with("0x") { + Err(FromHexError::MissingPrefix(s.into())) + } else { + Ok(Self::from_hex_str_no_prefix(s.as_ref().trim_start_matches("0x"))?) + } + } + + /// Parses provided string as hex without requiring 0x prefix. + /// + /// This is **not** recommended for user-supplied inputs because of possible confusion with decimals. + /// It should be only used for existing protocols which always encode values as hex without 0x prefix. + fn from_hex_str_no_prefix + Into>(s: S) -> Result; +} + +/// Hex parsing error +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum FromHexError { + /// The input was not a valid hex string, contains the error that occurred while parsing. + ParseHex(E), + /// The input is missing `0x` prefix, contains the invalid input. + MissingPrefix(String), +} + +impl From for FromHexError { + fn from(e: E) -> Self { FromHexError::ParseHex(e) } +} + +impl fmt::Display for FromHexError +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::FromHexError::*; + + match *self { + ParseHex(ref e) => write_err!(f, "failed to parse hex string"; e), + MissingPrefix(ref value) => write_err!(f, "the input value `{}` is missing the `0x` prefix", value; self), + } + } +} + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl std::error::Error for FromHexError +where + E: std::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use self::FromHexError::*; + + match *self { + ParseHex(ref e) => Some(e), + MissingPrefix(_) => None, + } + } +}