Merge rust-bitcoin/rust-bitcoin#1400: Add trait `FromStrHex` for tuple structs with single `u32` member

e00dfa9806 impl FromHexStr for structs with single u32 member (connormullett)

Pull request description:

  Closes: #1112

   - Adds new trait `FromStrHex` with 2 methods: `from_hex_str` and `from_hex_str_no_prefix`
   -  Impl new trait on each tuple struct with single u32 member. eg `Time(u32)`

  As stated in the issue, grep through codebase with `\(u32\)` and `\(pub u32\)` to see all implementations and verify none were missed.

  NonStandardSighashType is an error type and should never be constructed from a hex string. Therefore, it has been omitted from this change.

  Tests are somewhat redundant, but cover 4 cases each. 2 happy paths, 1 for each function. 1 case for malformed/invalid hex input, and 1 for calling no_prefix without a prefix

ACKs for top commit:
  Kixunil:
    ACK e00dfa9806
  apoelstra:
    ACK e00dfa9806

Tree-SHA512: 221faef7fc1fa8fdb4cba79cfae317a0b63984937c345c6ca2287123a078f38911cdc07db7589a88b7bc6fbecf389e9bcff47952728410510ffcfc1857e0f91f
This commit is contained in:
Andrew Poelstra 2022-12-11 18:36:21 +00:00
commit 32afe5ae48
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
7 changed files with 237 additions and 0 deletions

View File

@ -1403,6 +1403,7 @@ pub mod serde {
mod verification { mod verification {
use std::cmp; use std::cmp;
use std::convert::TryInto; use std::convert::TryInto;
use super::*; use super::*;
// Note regarding the `unwind` parameter: this defines how many iterations // Note regarding the `unwind` parameter: this defines how many iterations

View File

@ -19,6 +19,7 @@ use crate::error::ParseIntError;
use crate::io::{self, Read, Write}; use crate::io::{self, Read, Write};
use crate::prelude::*; use crate::prelude::*;
use crate::parse::{self, impl_parse_str_through_int}; use crate::parse::{self, impl_parse_str_through_int};
use crate::string::FromHexStr;
#[cfg(docsrs)] #[cfg(docsrs)]
use crate::absolute; 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<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
let packed_lock_time = crate::parse::hex_u32(s)?;
Ok(Self(packed_lock_time))
}
}
impl Encodable for PackedLockTime { impl Encodable for PackedLockTime {
#[inline] #[inline]
fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> { fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
@ -482,6 +492,16 @@ impl fmt::Display for Height {
} }
} }
impl FromHexStr for Height {
type Error = Error;
fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
let height = crate::parse::hex_u32(s)?;
Self::from_consensus(height)
}
}
impl FromStr for Height { impl FromStr for Height {
type Err = Error; 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<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
let time = crate::parse::hex_u32(s)?;
Time::from_consensus(time)
}
}
impl FromStr for Time { impl FromStr for Time {
type Err = Error; type Err = Error;
@ -750,4 +779,64 @@ mod tests {
let got = format!("{:#}", n); let got = format!("{:#}", n);
assert_eq!(got, "block-height 100"); 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());
}
} }

View File

@ -15,6 +15,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::io; use crate::io;
use crate::string::FromHexStr;
use core::{fmt, str, default::Default}; use core::{fmt, str, default::Default};
use core::convert::TryFrom; use core::convert::TryFrom;
@ -405,6 +406,15 @@ impl Sequence {
} }
} }
impl FromHexStr for Sequence {
type Error = crate::parse::ParseIntError;
fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
let sequence = crate::parse::hex_u32(s)?;
Ok(Self::from_consensus(sequence))
}
}
impl Default for Sequence { impl Default for Sequence {
/// The default value of sequence is 0xffffffff. /// The default value of sequence is 0xffffffff.
fn default() -> Self { fn default() -> Self {
@ -1463,6 +1473,25 @@ mod tests {
assert!(unit_time_lock.is_rbf()); assert!(unit_time_lock.is_rbf());
assert!(!lock_time_disabled.is_relative_lock_time()); 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)] #[cfg(bench)]

View File

@ -107,6 +107,7 @@ pub mod pow;
pub mod psbt; pub mod psbt;
pub mod sighash; pub mod sighash;
pub mod sign_message; pub mod sign_message;
pub mod string;
pub mod taproot; pub mod taproot;
pub mod util; pub mod util;

View File

@ -84,6 +84,15 @@ pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, P
}) })
} }
pub(crate) fn hex_u32<S: AsRef<str> + Into<String>>(s: S) -> Result<u32, ParseIntError> {
u32::from_str_radix(s.as_ref(), 16).map_err(|error| ParseIntError {
input: s.into(),
bits: u8::try_from(core::mem::size_of::<u32>() * 8).expect("max is 32 bits for u32"),
is_signed: u32::try_from(-1i8).is_ok(),
source: error,
})
}
impl_std_error!(ParseIntError, source); impl_std_error!(ParseIntError, source);
/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using `fn` /// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using `fn`

View File

@ -15,6 +15,8 @@ use crate::consensus::encode::{self, Decodable, Encodable};
use crate::consensus::Params; use crate::consensus::Params;
use crate::hash_types::BlockHash; use crate::hash_types::BlockHash;
use crate::io::{self, Read, Write}; use crate::io::{self, Read, Write};
use crate::prelude::String;
use crate::string::FromHexStr;
/// Implements $int * $ty. Requires `u64::from($int)`. /// Implements $int * $ty. Requires `u64::from($int)`.
macro_rules! impl_int_mul { macro_rules! impl_int_mul {
@ -283,6 +285,15 @@ impl From<CompactTarget> for Target {
fn from(c: CompactTarget) -> Self { Target::from_compact(c) } fn from(c: CompactTarget) -> Self { Target::from_compact(c) }
} }
impl FromHexStr for CompactTarget {
type Error = crate::parse::ParseIntError;
fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
let compact_target = crate::parse::hex_u32(s)?;
Ok(Self::from_consensus(compact_target))
}
}
impl Encodable for CompactTarget { impl Encodable for CompactTarget {
#[inline] #[inline]
fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> { fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
@ -1403,6 +1414,27 @@ mod tests {
.is_err()); // invalid length .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] #[test]
fn target_from_compact() { fn target_from_compact() {
// (nBits, target) // (nBits, target)

76
bitcoin/src/string.rs Normal file
View File

@ -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<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, FromHexError<Self::Error>> {
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<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error>;
}
/// Hex parsing error
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum FromHexError<E> {
/// 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<E> From<E> for FromHexError<E> {
fn from(e: E) -> Self { FromHexError::ParseHex(e) }
}
impl<E: fmt::Display> fmt::Display for FromHexError<E>
{
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<E> std::error::Error for FromHexError<E>
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,
}
}
}