Merge rust-bitcoin/rust-bitcoin#6694: units: Flatten Error Constructors
What changed, and why it matters
This commit is a code cleanup that rewrites how error values are constructed in the Rust Bitcoin library. It changes nested constructor calls like Err(OuterError(InnerError { ... })) into a flatter style using map_err. There is no functional change to how amounts, locktimes, or hex parsing behave, and no security bug is introduced or fixed.
No security action required. Treat as normal refactoring review; verify CI passes and behavior is unchanged.
Security signals we found
No strong security signals were identified.
Evidence from the diff
PR #6694 refactors error-construction patterns across units/src/amount and units/src/parse_int to avoid nested Err(…) wrappers that trigger the clippy unnecessary_map_on_constructor lint. The patch allows that lint in Cargo.toml and replaces direct nested constructors with chained .map_err() calls. The transformations are semantically equivalent: the same error variants are produced in the same control-flow paths. No logic, bounds checks, or public API semantics are altered.
Changed components
units/src/amount/mod.rsunits/src/amount/signed.rsunits/src/amount/unsigned.rsunits/src/locktime/relative/mod.rsunits/src/parse_int.rsCargo.toml clippy lint configurationInspect captured patch +58 / −54
### Cargo.toml
@@ -14,6 +14,7 @@ unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(chacha20_pol
# Exclude lints we don't think are valuable.
needless_question_mark = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
manual_range_contains = "allow" # More readable than clippy's format.
+unnecessary_map_on_constructor = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/issues/6539
# Exhaustive list of pedantic clippy lints
assigning_clones = "warn"
bool_to_int_with_if = "warn"
### units/src/amount/mod.rs
@@ -178,7 +178,7 @@ impl FromStr for Denomination {
use self::ParseDenominationError as E;
if CONFUSING_FORMS.contains(&s) {
- return Err(E::PossiblyConfusing(PossiblyConfusingDenominationError(s.into())));
+ return Err(PossiblyConfusingDenominationError(s.into())).map_err(E::PossiblyConfusing);
};
let form = Self::forms(s);
@@ -219,9 +219,8 @@ fn parse_signed_to_satoshi(
denom: Denomination,
) -> Result<(bool, SignedAmount), InnerParseError> {
if s.is_empty() {
- return Err(InnerParseError::MissingDigits(MissingDigitsError {
- kind: MissingDigitsKind::Empty,
- }));
+ return Err(MissingDigitsError { kind: MissingDigitsKind::Empty })
+ .map_err(InnerParseError::MissingDigits);
}
if s.len() > INPUT_STRING_LEN_LIMIT {
return Err(InnerParseError::InputTooLarge(s.len()));
@@ -230,9 +229,8 @@ fn parse_signed_to_satoshi(
let is_negative = s.starts_with('-');
if is_negative {
if s.len() == 1 {
- return Err(InnerParseError::MissingDigits(MissingDigitsError {
- kind: MissingDigitsKind::OnlyMinusSign,
- }));
+ return Err(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })
+ .map_err(InnerParseError::MissingDigits);
}
s = &s[1..];
}
@@ -251,9 +249,10 @@ fn parse_signed_to_satoshi(
match s.parse::<i64>() {
Ok(0) => return Ok((is_negative, SignedAmount::ZERO)),
_ =>
- return Err(InnerParseError::TooPrecise(TooPreciseError {
+ return Err(TooPreciseError {
position: position + usize::from(is_negative),
- })),
+ })
+ .map_err(InnerParseError::TooPrecise),
}
}
s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
@@ -283,26 +282,24 @@ fn parse_signed_to_satoshi(
None => None,
Some(d) if d < max_decimals => Some(d + 1),
_ =>
- return Err(InnerParseError::TooPrecise(TooPreciseError {
- position: i + usize::from(is_negative),
- })),
+ return Err(TooPreciseError { position: i + usize::from(is_negative) })
+ .map_err(InnerParseError::TooPrecise),
};
underscores = None;
}
'_' if i == 0 =>
// Leading underscore
- return Err(InnerParseError::BadPosition(BadPositionError {
- char: '_',
- position: i + usize::from(is_negative),
- })),
+ return Err(BadPositionError { char: '_', position: i + usize::from(is_negative) })
+ .map_err(InnerParseError::BadPosition),
'_' => match underscores {
None => underscores = Some(1),
// Consecutive underscores
_ =>
- return Err(InnerParseError::BadPosition(BadPositionError {
+ return Err(BadPositionError {
char: '_',
position: i + usize::from(is_negative),
- })),
+ })
+ .map_err(InnerParseError::BadPosition),
},
'.' => match decimals {
None if max_decimals <= 0 => break,
@@ -312,16 +309,18 @@ fn parse_signed_to_satoshi(
}
// Double decimal dot.
_ =>
- return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
+ return Err(InvalidCharacterError {
invalid_char: '.',
position: i + usize::from(is_negative),
- })),
+ })
+ .map_err(InnerParseError::InvalidCharacter),
},
c =>
- return Err(InnerParseError::InvalidCharacter(InvalidCharacterError {
+ return Err(InvalidCharacterError {
invalid_char: c,
position: i + usize::from(is_negative),
- })),
+ })
+ .map_err(InnerParseError::InvalidCharacter),
}
}
@@ -382,10 +381,12 @@ fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseE
} else {
let i = s
.find(|c: char| c.is_alphabetic())
- .ok_or(ParseError(ParseErrorInner::MissingDenomination(MissingDenominationError)))?;
+ .ok_or(MissingDenominationError)
+ .map_err(ParseErrorInner::MissingDenomination)
+ .map_err(ParseError)?;
(i, i)
};
- Ok((&s[..i], s[j..].parse().map_err(|e| ParseError(ParseErrorInner::Denomination(e)))?))
+ Ok((&s[..i], s[j..].parse().map_err(ParseErrorInner::Denomination).map_err(ParseError)?))
}
/// Options given by `fmt::Formatter`
### units/src/amount/signed.rs
@@ -140,13 +140,11 @@ impl SignedAmount {
#[allow(clippy::missing_panics_doc)]
fn from_sat_u64(satoshi: u64) -> Result<Self, ParseAmountError> {
// u64 -> i64 only fails if value is greater than i64::MAX, which is also > Self::MAX_MONEY.
- let amount = i64::try_from(satoshi).map_err(|_| {
- ParseAmountError(ParseAmountErrorInner::OutOfRange(OutOfRangeError {
- is_signed: true,
- is_greater_than_max: true,
- }))
- })?;
- Self::from_sat(amount).map_err(|e| ParseAmountError(ParseAmountErrorInner::OutOfRange(e)))
+ let amount = i64::try_from(satoshi)
+ .map_err(|_| OutOfRangeError { is_signed: true, is_greater_than_max: true })
+ .map_err(ParseAmountErrorInner::OutOfRange)
+ .map_err(ParseAmountError)?;
+ Self::from_sat(amount).map_err(ParseAmountErrorInner::OutOfRange).map_err(ParseAmountError)
}
/// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
@@ -227,7 +225,7 @@ impl SignedAmount {
#[inline]
pub fn from_str_with_denomination(s: &str) -> Result<Self, ParseError> {
let (amt, denom) = split_amount_and_denomination(s)?;
- Self::from_str_in(amt, denom).map_err(|e| ParseError(ParseErrorInner::Amount(e)))
+ Self::from_str_in(amt, denom).map_err(ParseErrorInner::Amount).map_err(ParseError)
}
/// Expresses this [`SignedAmount`] as a floating-point value in the given [`Denomination`].
@@ -260,7 +258,8 @@ impl SignedAmount {
#[inline]
pub fn from_sat_hex(s: &str) -> Result<Self, ParseAmountError> {
let amount = parse_int::hex_u64_prefixed(s)
- .map_err(|e| ParseAmountError(ParseAmountErrorInner::PrefixedHex(e)))?;
+ .map_err(ParseAmountErrorInner::PrefixedHex)
+ .map_err(ParseAmountError)?;
Self::from_sat_u64(amount)
}
@@ -275,7 +274,8 @@ impl SignedAmount {
#[inline]
pub fn from_sat_unprefixed_hex(s: &str) -> Result<Self, ParseAmountError> {
let amount = parse_int::hex_u64_unprefixed(s)
- .map_err(|e| ParseAmountError(ParseAmountErrorInner::UnprefixedHex(e)))?;
+ .map_err(ParseAmountErrorInner::UnprefixedHex)
+ .map_err(ParseAmountError)?;
Self::from_sat_u64(amount)
}
### units/src/amount/unsigned.rs
@@ -193,11 +193,11 @@ impl Amount {
let (is_neg, amount) =
parse_signed_to_satoshi(s, denom).map_err(|error| error.convert(false))?;
if is_neg {
- return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange(
- OutOfRangeError::negative(),
- )));
+ return Err(OutOfRangeError::negative())
+ .map_err(ParseAmountErrorInner::OutOfRange)
+ .map_err(ParseAmountError);
}
- Self::try_from(amount).map_err(|e| ParseAmountError(ParseAmountErrorInner::OutOfRange(e)))
+ Self::try_from(amount).map_err(ParseAmountErrorInner::OutOfRange).map_err(ParseAmountError)
}
/// Parses amounts with denomination suffix as produced by [`Self::to_string_with_denomination`]
@@ -220,7 +220,7 @@ impl Amount {
#[inline]
pub fn from_str_with_denomination(s: &str) -> Result<Self, ParseError> {
let (amt, denom) = split_amount_and_denomination(s)?;
- Self::from_str_in(amt, denom).map_err(|e| ParseError(ParseErrorInner::Amount(e)))
+ Self::from_str_in(amt, denom).map_err(ParseErrorInner::Amount).map_err(ParseError)
}
/// Expresses this [`Amount`] as a floating-point value in the given [`Denomination`].
@@ -269,9 +269,9 @@ impl Amount {
#[cfg(feature = "alloc")]
pub fn from_float_in(value: f64, denom: Denomination) -> Result<Self, ParseAmountError> {
if value < 0.0 {
- return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange(
- OutOfRangeError::negative(),
- )));
+ return Err(OutOfRangeError::negative())
+ .map_err(ParseAmountErrorInner::OutOfRange)
+ .map_err(ParseAmountError);
}
// This is inefficient, but the safest way to deal with this. The parsing logic is safe.
// Any performance-critical application should not be dealing with floats.
@@ -287,8 +287,9 @@ impl Amount {
#[inline]
pub fn from_sat_hex(s: &str) -> Result<Self, ParseAmountError> {
let amount = parse_int::hex_u64_prefixed(s)
- .map_err(|e| ParseAmountError(ParseAmountErrorInner::PrefixedHex(e)))?;
- Self::from_sat(amount).map_err(|e| ParseAmountError(ParseAmountErrorInner::OutOfRange(e)))
+ .map_err(ParseAmountErrorInner::PrefixedHex)
+ .map_err(ParseAmountError)?;
+ Self::from_sat(amount).map_err(ParseAmountErrorInner::OutOfRange).map_err(ParseAmountError)
}
/// Constructs a new `Amount` from an unprefixed hex string.
@@ -300,8 +301,9 @@ impl Amount {
#[inline]
pub fn from_sat_unprefixed_hex(s: &str) -> Result<Self, ParseAmountError> {
let amount = parse_int::hex_u64_unprefixed(s)
- .map_err(|e| ParseAmountError(ParseAmountErrorInner::UnprefixedHex(e)))?;
- Self::from_sat(amount).map_err(|e| ParseAmountError(ParseAmountErrorInner::OutOfRange(e)))
+ .map_err(ParseAmountErrorInner::UnprefixedHex)
+ .map_err(ParseAmountError)?;
+ Self::from_sat(amount).map_err(ParseAmountErrorInner::OutOfRange).map_err(ParseAmountError)
}
/// Constructs a new object that implements [`fmt::Display`] in the given [`Denomination`].
### units/src/locktime/relative/mod.rs
@@ -240,7 +240,7 @@ impl LockTime {
.is_satisfied_by(chain_tip, utxo_mined_at)
.map_err(IsSatisfiedByHeightError::Satisfaction),
Self::Time(time) =>
- Err(IsSatisfiedByHeightError::Incompatible(IncompatibleHeightError(time))),
+ Err(IncompatibleHeightError(time)).map_err(IsSatisfiedByHeightError::Incompatible),
}
}
@@ -260,7 +260,7 @@ impl LockTime {
.is_satisfied_by(chain_tip, utxo_mined_at)
.map_err(IsSatisfiedByTimeError::Satisfaction),
Self::Blocks(blocks) =>
- Err(IsSatisfiedByTimeError::Incompatible(IncompatibleTimeError(blocks))),
+ Err(IncompatibleTimeError(blocks)).map_err(IsSatisfiedByTimeError::Incompatible),
}
}
### units/src/parse_int.rs
@@ -225,9 +225,9 @@ pub fn hex_remove_prefix(s: &str) -> Result<&str, PrefixedHexError> {
} else if let Some(checked) = s.strip_prefix("0X") {
Ok(checked)
} else {
- Err(PrefixedHexError(error::PrefixedHexErrorInner::MissingPrefix(
- error::MissingPrefixError::new(s),
- )))
+ Err(error::MissingPrefixError::new(s))
+ .map_err(error::PrefixedHexErrorInner::MissingPrefix)
+ .map_err(PrefixedHexError)
}
}
@@ -239,9 +239,9 @@ pub fn hex_remove_prefix(s: &str) -> Result<&str, PrefixedHexError> {
#[inline]
pub fn hex_check_unprefixed(s: &str) -> Result<&str, UnprefixedHexError> {
if s.starts_with("0x") || s.starts_with("0X") {
- return Err(UnprefixedHexError(error::UnprefixedHexErrorInner::ContainsPrefix(
- error::ContainsPrefixError::new(s),
- )));
+ return Err(error::ContainsPrefixError::new(s))
+ .map_err(error::UnprefixedHexErrorInner::ContainsPrefix)
+ .map_err(UnprefixedHexError);
}
Ok(s)
}Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.