What changed, and why it matters
This commit is a routine API design change, not a security fix. It renames an internal error enum and wraps it in a private struct so the library can keep the error type flexible before its first stable release. No vulnerability is patched and no exploit is possible.
No security action needed. Treat as a normal API-breaking change and update downstream code that pattern-matched on FromHexError variants.
Security signals we found
No security-relevant keywords in commit title or message
No bounds checks, input validation, or cryptographic logic changed
No memory-safety, panic, or unsafe-code modifications
Tests updated only to match new non-exhaustive error shape
Evidence from the diff
The patch changes the public FromHexError from an enum to a struct wrapping a private pub(crate) enum (FromHexErrorInner). This hides the enum variants from downstream users, preventing them from pattern-matching on specific error kinds and giving the library freedom to evolve the type before a 1.0 release. The change is purely about API stability and encapsulation; the underlying error information is still exposed through Display and source().
Changed components
bitcoin-consensus-encoding error typesdecode_from_hex helperTransaction::from_str hex parsing testsInspect captured patch +66 / −32
diff --git a/consensus_encoding/src/decode/mod.rs b/consensus_encoding/src/decode/mod.rs
index a1629ee7..137617e7 100644
--- a/consensus_encoding/src/decode/mod.rs
+++ b/consensus_encoding/src/decode/mod.rs
@@ -5,7 +5,7 @@
pub mod decoders;
#[cfg(feature = "hex")]
-use crate::FromHexError;
+use crate::error::{FromHexError, FromHexErrorInner};
#[cfg(feature = "std")]
use crate::ReadError;
use crate::{DecodeError, UnconsumedError};
@@ -136,22 +136,23 @@ impl DecoderStatus {
///
/// # Errors
///
-/// - [`FromHexError::OddLength`] if the string has an odd number of characters.
-/// - [`FromHexError::InvalidChar`] if any character is not a valid hex digit.
-/// - [`FromHexError::Decode`] if decoding the type fails, including if bytes remain unconsumed
-/// after the decoder completes.
+/// [`FromHexError`] if the string has an odd number of characters, any character is not a
+/// valid hex digit, or if decoding the type fails, including if bytes remain unconsumed
+/// after the decoder completes.
#[cfg(feature = "hex")]
pub fn decode_from_hex<T: Decode>(
hex: &str,
) -> Result<T, FromHexError<<T::Decoder as Decoder>::Error>> {
- let iter = hex::HexSliceToBytesIter::new(hex).map_err(FromHexError::OddLength)?;
+ let iter = hex::HexSliceToBytesIter::new(hex)
+ .map_err(FromHexErrorInner::OddLength)
+ .map_err(FromHexError)?;
let mut decoder = T::decoder();
let mut buffer = [0u8; 4096];
let mut index = 0;
for item in iter {
- let byte = item.map_err(FromHexError::InvalidChar)?;
+ let byte = item.map_err(FromHexErrorInner::InvalidChar).map_err(FromHexError)?;
if index == buffer.len() {
let mut to_flush = buffer.as_slice();
@@ -160,10 +161,12 @@ pub fn decode_from_hex<T: Decode>(
while !to_flush.is_empty() {
if decoder
.push_bytes(&mut to_flush)
- .map_err(|e| FromHexError::Decode(DecodeError::Parse(e)))?
+ .map_err(|e| FromHexError(FromHexErrorInner::Decode(DecodeError::Parse(e))))?
.is_ready()
{
- return Err(FromHexError::Decode(DecodeError::Unconsumed(UnconsumedError())));
+ return Err(FromHexError(FromHexErrorInner::Decode(DecodeError::Unconsumed(
+ UnconsumedError(),
+ ))));
}
}
index = 0;
@@ -176,7 +179,7 @@ pub fn decode_from_hex<T: Decode>(
while !to_flush.is_empty() {
if decoder
.push_bytes(&mut to_flush)
- .map_err(|e| FromHexError::Decode(DecodeError::Parse(e)))?
+ .map_err(|e| FromHexError(FromHexErrorInner::Decode(DecodeError::Parse(e))))?
.is_ready()
{
break;
@@ -184,9 +187,9 @@ pub fn decode_from_hex<T: Decode>(
}
if to_flush.is_empty() {
- decoder.end().map_err(|e| FromHexError::Decode(DecodeError::Parse(e)))
+ decoder.end().map_err(|e| FromHexError(FromHexErrorInner::Decode(DecodeError::Parse(e))))
} else {
- Err(FromHexError::Decode(DecodeError::Unconsumed(UnconsumedError())))
+ Err(FromHexError(FromHexErrorInner::Decode(DecodeError::Unconsumed(UnconsumedError()))))
}
}
diff --git a/consensus_encoding/src/error.rs b/consensus_encoding/src/error.rs
index 4af7c7bc..d01144a7 100644
--- a/consensus_encoding/src/error.rs
+++ b/consensus_encoding/src/error.rs
@@ -328,7 +328,11 @@ impl std::error::Error for UnexpectedEofError {
/// An error that can occur when decoding from a hex string.
#[cfg(feature = "hex")]
#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum FromHexError<ParseErr> {
+pub struct FromHexError<ParseErr>(pub(crate) FromHexErrorInner<ParseErr>);
+
+#[cfg(feature = "hex")]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum FromHexErrorInner<ParseErr> {
/// The hex string had an odd number of characters.
OddLength(hex::OddLengthStringError),
/// A character in the hex string was not a valid hex digit.
@@ -345,10 +349,10 @@ impl<ParseErr> From<Infallible> for FromHexError<ParseErr> {
#[cfg(feature = "hex")]
impl<ParseErr: fmt::Display> fmt::Display for FromHexError<ParseErr> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match *self {
- Self::OddLength(ref e) => write_err!(f, "odd length string"; e),
- Self::InvalidChar(ref e) => write_err!(f, "invalid character"; e),
- Self::Decode(ref e) => write_err!(f, "decode error"; e),
+ match self.0 {
+ FromHexErrorInner::OddLength(ref e) => write_err!(f, "odd length string"; e),
+ FromHexErrorInner::InvalidChar(ref e) => write_err!(f, "invalid character"; e),
+ FromHexErrorInner::Decode(ref e) => write_err!(f, "decode error"; e),
}
}
}
@@ -360,10 +364,10 @@ where
ParseErr: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match *self {
- Self::OddLength(ref e) => Some(e),
- Self::InvalidChar(ref e) => Some(e),
- Self::Decode(ref e) => Some(e),
+ match self.0 {
+ FromHexErrorInner::OddLength(ref e) => Some(e),
+ FromHexErrorInner::InvalidChar(ref e) => Some(e),
+ FromHexErrorInner::Decode(ref e) => Some(e),
}
}
}
diff --git a/consensus_encoding/tests/decode.rs b/consensus_encoding/tests/decode.rs
index b254a700..05c65e3d 100644
--- a/consensus_encoding/tests/decode.rs
+++ b/consensus_encoding/tests/decode.rs
@@ -7,7 +7,7 @@ use std::io::{Cursor, Read};
use bitcoin_consensus_encoding as encoding;
#[cfg(feature = "hex")]
-use bitcoin_consensus_encoding::{decode_from_hex, FromHexError};
+use bitcoin_consensus_encoding::decode_from_hex;
#[cfg(feature = "alloc")]
use encoding::check_decode;
use encoding::{
@@ -317,17 +317,36 @@ fn decode_from_hex_larger_than_internal_buffer() {
#[test]
#[cfg(feature = "hex")]
+#[cfg(feature = "std")]
+#[rustfmt::skip] // matches! statements become tall stacks from fmt
fn decode_from_hex_error() {
+ use std::error::Error as _;
+
let result: Result<TestArray, _> = decode_from_hex("0102030");
- assert!(matches!(result, Err(FromHexError::OddLength(_))));
+ assert!(matches!(
+ result.unwrap_err().source().unwrap().downcast_ref().unwrap(),
+ hex::OddLengthStringError { .. },
+ ));
let result: Result<TestArray, _> = decode_from_hex("0102GG04");
- assert!(matches!(result, Err(FromHexError::InvalidChar(_))));
+ assert!(matches!(
+ result.unwrap_err().source().unwrap().downcast_ref().unwrap(),
+ hex::InvalidCharError { .. },
+ ));
let result: Result<TestArray, _> = decode_from_hex("0102");
- assert!(matches!(result, Err(FromHexError::Decode(DecodeError::Parse(_)))));
+ assert!(matches!(
+ result.unwrap_err().source().unwrap().downcast_ref::<DecodeError<UnexpectedEofError>>().unwrap(),
+ DecodeError::Parse(_),
+ ));
let result: Result<TestArray, _> = decode_from_hex("");
- assert!(matches!(result, Err(FromHexError::Decode(DecodeError::Parse(_)))));
+ assert!(matches!(
+ result.unwrap_err().source().unwrap().downcast_ref::<DecodeError<UnexpectedEofError>>().unwrap(),
+ DecodeError::Parse(_),
+ ));
let result: Result<TestArray, _> = decode_from_hex("0102030405060708");
- assert!(matches!(result, Err(FromHexError::Decode(DecodeError::Unconsumed(_)))));
+ assert!(matches!(
+ result.unwrap_err().source().unwrap().downcast_ref::<DecodeError<UnexpectedEofError>>().unwrap(),
+ DecodeError::Unconsumed(_),
+ ));
}
#[test]
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index da9a66a3..905eb2a1 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -1608,8 +1608,6 @@ mod tests {
#[cfg(feature = "std")]
use std::error::Error as _;
- #[cfg(feature = "hex")]
- use encoding::FromHexError;
use encoding::{Decode as _, Decoder as _};
#[cfg(feature = "hex")]
use hex::hex;
@@ -1753,21 +1751,31 @@ mod tests {
#[test]
#[cfg(feature = "hex")]
+ #[cfg(feature = "std")]
fn transaction_from_hex_str_error() {
// OddLength error
let odd = "abc"; // 3 chars, odd length
let err = Transaction::from_str(odd).unwrap_err();
- assert!(matches!(err, FromHexError::OddLength(..)));
+ assert!(matches!(
+ err.source().unwrap().downcast_ref::<hex::OddLengthStringError>().unwrap(),
+ hex::OddLengthStringError { .. },
+ ));
// InvalidChar error
let invalid = "zz";
let err = Transaction::from_str(invalid).unwrap_err();
- assert!(matches!(err, FromHexError::InvalidChar(..)));
+ assert!(matches!(
+ err.source().unwrap().downcast_ref::<hex::InvalidCharError>().unwrap(),
+ hex::InvalidCharError { .. },
+ ));
// Decode error
let bad = "deadbeef00"; // arbitrary even-length hex that will fail decoding
let err = Transaction::from_str(bad).unwrap_err();
- assert!(matches!(err, FromHexError::Decode(..)));
+ assert!(matches!(
+ err.source().unwrap().source().unwrap().downcast_ref::<TransactionDecoderError>().unwrap(),
+ TransactionDecoderError { .. },
+ ));
}
#[test]
Why this scored 18/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.