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, + } + } +}