Remove alloc requirement for HexPrimitive
What changed, and why it matters
This is a routine code cleanup in the rust-bitcoin library. It removes the need for the 'alloc' feature when formatting certain Bitcoin data types as hex, making the hex formatting work in more constrained environments. There is no indication this fixes a security vulnerability.
No security action required. This is a feature-gate refactoring and test expansion change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit refactors hex formatting for HexPrimitive and Header types. Previously, hex_write_with_case required the alloc feature because it collected hex bytes into a String before padding. The new fmt_hex implementation writes directly to the formatter using core::fmt::Write, removing the alloc dependency. Feature gates are adjusted accordingly, and Header’s Display implementation is simplified to delegate to HexPrimitive. Extensive formatting tests are added.
Changed components
primitives/src/block.rsprimitives/src/lib.rsprimitives/src/transaction.rsInspect captured patch +107 / −53
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index ad863d1a..005ef6b4 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -15,8 +15,6 @@ use core::marker::PhantomData;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
use encoding::{Encodable, Decodable, Decoder, Decoder6};
-#[cfg(feature = "hex")]
-use encoding::EncodableByteIter;
#[cfg(feature = "alloc")]
use encoding::{
CompactSizeEncoder, Decoder2, Encoder2, SliceEncoder, VecDecoder,
@@ -26,7 +24,7 @@ use internals::write_err;
use crate::pow::{CompactTargetDecoder, CompactTargetDecoderError};
#[cfg(feature = "hex")]
-use crate::hex_codec::ParsePrimitiveError;
+use crate::hex_codec::{HexPrimitive, ParsePrimitiveError};
#[cfg(feature = "alloc")]
use crate::prelude::Vec;
use crate::time::{BlockTimeDecoder, BlockTimeDecoderError};
@@ -512,32 +510,23 @@ impl core::str::FromStr for Header {
type Err = ParseHeaderError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- crate::hex_codec::HexPrimitive::from_str(s).map_err(ParseHeaderError)
+ HexPrimitive::from_str(s).map_err(ParseHeaderError)
}
}
#[cfg(feature = "hex")]
impl fmt::Display for Header {
- #[allow(clippy::use_self)]
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- use hex_unstable::{fmt_hex_exact, Case};
-
- fmt_hex_exact!(f, Header::SIZE, EncodableByteIter::new(self), Case::Lower)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&HexPrimitive(self), f) }
}
-#[cfg(all(feature = "hex", feature = "alloc"))]
+#[cfg(feature = "hex")]
impl fmt::LowerHex for Header {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::LowerHex::fmt(&crate::hex_codec::HexPrimitive(self), f)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&HexPrimitive(self), f) }
}
-#[cfg(all(feature = "hex", feature = "alloc"))]
+#[cfg(feature = "hex")]
impl fmt::UpperHex for Header {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::UpperHex::fmt(&crate::hex_codec::HexPrimitive(self), f)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&HexPrimitive(self), f) }
}
impl fmt::Debug for Header {
@@ -1263,6 +1252,10 @@ mod tests {
let want = format!("{:.20}", want);
let got = format!("{:.20}", header);
assert_eq!(got, want);
+
+ let want = format!("{:.0}", want);
+ let got = format!("{:.0}", header);
+ assert_eq!(got, want);
}
#[test]
@@ -1271,7 +1264,7 @@ mod tests {
fn header_hex() {
let header = dummy_header();
- let want = concat!(
+ let lower_hex = concat!(
"01000000", // version
"9999999999999999999999999999999999999999999999999999999999999999", // prev_blockhash
"7777777777777777777777777777777777777777777777777777777777777777", // merkle_root
@@ -1281,13 +1274,33 @@ mod tests {
);
// All of these should yield a lowercase hex
- assert_eq!(want, format!("{:x}", header));
- assert_eq!(want, format!("{}", header));
+ assert_eq!(lower_hex, format!("{:x}", header));
+ assert_eq!(lower_hex, format!("{}", header));
// And these should yield uppercase hex
- let upper_encoded =
- want.chars().map(|chr| chr.to_ascii_uppercase()).collect::<alloc::string::String>();
- assert_eq!(upper_encoded, format!("{:X}", header));
+ let upper_hex = lower_hex.to_ascii_uppercase();
+ assert_eq!(upper_hex, format!("{:X}", header));
+
+ // Check padding (right, left, center, custom char)
+ assert_eq!(format!("{:>164}", lower_hex), format!("{:>164x}", header));
+ assert_eq!(format!("{:<164}", lower_hex), format!("{:<164x}", header));
+ assert_eq!(format!("{:^164}", lower_hex), format!("{:^164x}", header));
+ assert_eq!(format!("{:_>164}", lower_hex), format!("{:_>164x}", header));
+
+ // Alt forms
+ let lower_hex_alt = format!("0x{}", lower_hex);
+ assert_eq!(lower_hex_alt, format!("{:#x}", header));
+ assert_eq!(format!("0X{}", upper_hex), format!("{:#X}", header));
+
+ // Alternate + padding
+ assert_eq!(format!("{:>166}", lower_hex_alt), format!("{:>#166x}", header));
+ assert_eq!(format!("{:<166}", lower_hex_alt), format!("{:<#166x}", header));
+ assert_eq!(format!("{:^166}", lower_hex_alt), format!("{:^#166x}", header));
+
+ // Alt + truncate
+ assert_eq!(format!("{:>.20}", lower_hex_alt), format!("{:>#.20x}", header));
+ assert_eq!(format!("{:<.20}", lower_hex_alt), format!("{:<#.20x}", header));
+ assert_eq!(format!("{:^.20}", lower_hex_alt), format!("{:^#.20x}", header));
}
#[test]
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index f945afd5..130fbd12 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -118,9 +118,9 @@ pub(crate) fn compact_size_encode(value: usize) -> ArrayVec<u8, 9> {
#[cfg(feature = "hex")]
pub(crate) mod hex_codec {
use core::fmt;
+ use core::fmt::Write as _;
use encoding::{Decodable, Decoder, Encodable, EncodableByteIter};
- #[cfg(feature = "alloc")]
use hex_unstable::{BytesToHexIter, Case};
use internals::write_err;
@@ -171,19 +171,6 @@ pub(crate) mod hex_codec {
}
}
- /// Writes an Encodable object to the given formatter in the requested case.
- #[inline]
- #[cfg(feature = "alloc")]
- fn hex_write_with_case<T: Encodable + Decodable>(
- obj: &HexPrimitive<T>,
- f: &mut fmt::Formatter,
- case: Case,
- ) -> fmt::Result {
- let iter = BytesToHexIter::new(encoding::EncodableByteIter::new(obj.0), case);
- let collection = iter.collect::<alloc::string::String>();
- f.pad(&collection)
- }
-
/// Hex encoding wrapper type for Encodable + Decodable types.
///
/// Provides default implementations for `Display`, `Debug`, `LowerHex`, and `UpperHex`.
@@ -236,34 +223,88 @@ pub(crate) mod hex_codec {
decoder.end().map_err(ParsePrimitiveError::Decode)
}
+
+ /// Writes an Encodable object to the given formatter in the requested case.
+ #[inline]
+ fn fmt_hex(
+ &self,
+ f: &mut fmt::Formatter,
+ case: Case,
+ ) -> fmt::Result {
+ // Closure to write a given pad character out a given number of times.
+ let write_pad = |f: &mut fmt::Formatter, pad_len: usize| -> fmt::Result {
+ for _ in 0..pad_len {
+ f.write_char(f.fill())?;
+ }
+ Ok(())
+ };
+
+ // Count hex chars
+ let len = EncodableByteIter::new(self.0).count() * 2;
+ let iter = BytesToHexIter::new(
+ EncodableByteIter::new(self.0),
+ case,
+ );
+
+ let extra_len = if f.alternate() { 2 } else { 0 };
+ let total_len = len + extra_len;
+
+ // We pad for width, and truncate for precision, but not vice-versa
+ let pad_width = f.width().unwrap_or(total_len);
+ let trunc_width = f.precision()
+ .map_or(len, |v| v.saturating_sub(extra_len));
+
+ let pad_diff = pad_width.saturating_sub(total_len);
+
+
+ // Left padding
+ let left_pad = match f.align() {
+ Some(fmt::Alignment::Left) => 0,
+ Some(fmt::Alignment::Center) => pad_diff / 2,
+ Some(fmt::Alignment::Right) => pad_diff,
+ None => 0,
+ };
+ write_pad(f, left_pad)?;
+
+ // Alt characters
+ if f.alternate() {
+ f.write_str(match case {
+ hex_unstable::Case::Lower => "0x",
+ hex_unstable::Case::Upper => "0X",
+ })?;
+ }
+
+ // Hex data
+ for (i, ch) in iter.enumerate() {
+ if i >= trunc_width { break; }
+ f.write_char(ch)?;
+ }
+
+ // Right padding
+ write_pad(f, pad_diff.saturating_sub(left_pad))?;
+
+ Ok(())
+ }
}
- #[cfg(feature = "alloc")]
impl<T: Encodable + Decodable> fmt::Display for HexPrimitive<'_, T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self, f) }
}
- #[cfg(feature = "alloc")]
impl<T: Encodable + Decodable> fmt::Debug for HexPrimitive<'_, T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self, f) }
}
- #[cfg(feature = "alloc")]
impl<T: Encodable + Decodable> fmt::LowerHex for HexPrimitive<'_, T> {
#[inline]
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- hex_write_with_case(self, f, Case::Lower)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_hex(f, Case::Lower) }
}
- #[cfg(feature = "alloc")]
impl<T: Encodable + Decodable> fmt::UpperHex for HexPrimitive<'_, T> {
#[inline]
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- hex_write_with_case(self, f, Case::Upper)
- }
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_hex(f, Case::Upper) }
}
}
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index c8190919..3618eeb4 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -35,7 +35,7 @@ use units::parse_int;
#[cfg(feature = "alloc")]
use crate::amount::{AmountDecoder, AmountEncoder};
#[cfg(all(feature = "hex", feature = "alloc"))]
-use crate::hex_codec::ParsePrimitiveError;
+use crate::hex_codec::{HexPrimitive, ParsePrimitiveError};
#[cfg(feature = "alloc")]
use crate::locktime::absolute::{LockTimeDecoder, LockTimeDecoderError, LockTimeEncoder};
#[cfg(feature = "alloc")]
@@ -381,28 +381,28 @@ impl core::str::FromStr for Transaction {
type Err = ParseTransactionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- crate::hex_codec::HexPrimitive::from_str(s).map_err(ParseTransactionError)
+ HexPrimitive::from_str(s).map_err(ParseTransactionError)
}
}
#[cfg(all(feature = "hex", feature = "alloc"))]
impl fmt::Display for Transaction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::Display::fmt(&crate::hex_codec::HexPrimitive(self), f)
+ fmt::Display::fmt(&HexPrimitive(self), f)
}
}
#[cfg(all(feature = "hex", feature = "alloc"))]
impl fmt::LowerHex for Transaction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::LowerHex::fmt(&crate::hex_codec::HexPrimitive(self), f)
+ fmt::LowerHex::fmt(&HexPrimitive(self), f)
}
}
#[cfg(all(feature = "hex", feature = "alloc"))]
impl fmt::UpperHex for Transaction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::UpperHex::fmt(&crate::hex_codec::HexPrimitive(self), f)
+ fmt::UpperHex::fmt(&HexPrimitive(self), f)
}
}
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.