What changed, and why it matters
This commit adds a new helper decoder called ArrayDecoder to the rust-bitcoin consensus_encoding crate. It simply reads a fixed number of bytes from a stream and returns them as an array. There is no bug fix, behavior change, or security-sensitive modification here—just new code and tests.
No security action needed. Review as normal new-feature code if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces ArrayDecoder
Changed components
consensus_encoding/src/decode/decoders.rsconsensus_encoding/src/decode/mod.rsconsensus_encoding/src/lib.rsconsensus_encoding/tests/decode.rsInspect captured patch +122 / −0
diff --git a/consensus_encoding/src/decode/decoders.rs b/consensus_encoding/src/decode/decoders.rs
new file mode 100644
index 00000000..56534a9d
--- /dev/null
+++ b/consensus_encoding/src/decode/decoders.rs
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Primitive decoders.
+
+use super::Decoder;
+
+/// Not enough bytes given to decoder.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct UnexpectedEof {
+ /// Number of bytes missing to complete decoder.
+ missing: usize,
+}
+
+impl core::fmt::Display for UnexpectedEof {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "not enough bytes for decoder, {} more bytes required", self.missing)
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for UnexpectedEof {}
+
+/// A decoder that expects exactly N bytes and returns them as an array.
+pub struct ArrayDecoder<const N: usize> {
+ buffer: [u8; N],
+ bytes_written: usize,
+}
+
+impl<const N: usize> ArrayDecoder<N> {
+ /// Constructs a new array decoder that expects exactly N bytes.
+ pub fn new() -> Self { Self { buffer: [0; N], bytes_written: 0 } }
+}
+
+impl<const N: usize> Default for ArrayDecoder<N> {
+ fn default() -> Self { Self::new() }
+}
+
+impl<const N: usize> Decoder for ArrayDecoder<N> {
+ type Output = [u8; N];
+ type Error = UnexpectedEof;
+
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
+ let remaining_space = N - self.bytes_written;
+ let copy_len = bytes.len().min(remaining_space);
+
+ if copy_len > 0 {
+ self.buffer[self.bytes_written..self.bytes_written + copy_len]
+ .copy_from_slice(&bytes[..copy_len]);
+ self.bytes_written += copy_len;
+ // Advance the slice reference to consume the bytes.
+ *bytes = &bytes[copy_len..];
+ }
+
+ // Return true if we still need more data.
+ Ok(self.bytes_written < N)
+ }
+
+ fn end(self) -> Result<Self::Output, Self::Error> {
+ if self.bytes_written == N {
+ Ok(self.buffer)
+ } else {
+ Err(UnexpectedEof { missing: N - self.bytes_written })
+ }
+ }
+}
diff --git a/consensus_encoding/src/decode/mod.rs b/consensus_encoding/src/decode/mod.rs
index 1e8c658d..c08ed13b 100644
--- a/consensus_encoding/src/decode/mod.rs
+++ b/consensus_encoding/src/decode/mod.rs
@@ -2,6 +2,8 @@
//! Consensus Decoding Traits
+pub mod decoders;
+
/// A Bitcoin object which can be consensus-decoded using a push decoder.
///
/// To decode something, create a [`Self::Decoder`] and push byte slices
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 2050acb7..9663f05b 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -22,6 +22,7 @@ extern crate std;
mod decode;
mod encode;
+pub use self::decode::decoders::{ArrayDecoder, UnexpectedEof};
pub use self::decode::{Decodable, Decoder};
#[cfg(feature = "alloc")]
pub use self::encode::encode_to_vec;
diff --git a/consensus_encoding/tests/decode.rs b/consensus_encoding/tests/decode.rs
new file mode 100644
index 00000000..d1aaa301
--- /dev/null
+++ b/consensus_encoding/tests/decode.rs
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Integration tests for decode module.
+
+use consensus_encoding::{ArrayDecoder, Decoder, UnexpectedEof};
+
+const EMPTY: &[u8] = &[];
+
+#[test]
+fn decode_array_excess_data_ignored() {
+ let mut decoder = ArrayDecoder::<4>::new();
+ let mut data = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06][..];
+ let needs_more = decoder.push_bytes(&mut data).unwrap();
+ assert!(!needs_more, "ArrayDecoder should be complete after consuming all needed bytes");
+ assert_eq!(data, &[0x05, 0x06]);
+ let result = decoder.end().unwrap();
+ assert_eq!(result, [0x01, 0x02, 0x03, 0x04]);
+}
+
+#[test]
+fn decode_array_streaming_behavior() {
+ let mut decoder = ArrayDecoder::<4>::new();
+
+ let mut data = &[0x01][..];
+ let needs_more = decoder.push_bytes(&mut data).unwrap();
+ assert!(needs_more, "ArrayDecoder should need more data after 1 byte");
+ assert_eq!(data, EMPTY);
+
+ let mut data = &[0x02, 0x03][..];
+ let needs_more = decoder.push_bytes(&mut data).unwrap();
+ assert!(needs_more, "ArrayDecoder should need more data after 3 bytes");
+ assert_eq!(data, EMPTY);
+
+ let mut data = &[0x04, 0x05, 0x06][..];
+ let needs_more = decoder.push_bytes(&mut data).unwrap();
+ assert!(!needs_more, "ArrayDecoder should be complete after 4 bytes");
+ assert_eq!(data, &[0x05, 0x06]);
+
+ let result = decoder.end().unwrap();
+ assert_eq!(result, [0x01, 0x02, 0x03, 0x04]);
+}
+
+#[test]
+fn decode_array_insufficient_data_error() {
+ let mut decoder = ArrayDecoder::<5>::new();
+ let mut data = &[0xAA, 0xBB][..];
+
+ let needs_more = decoder.push_bytes(&mut data).unwrap();
+ assert!(needs_more, "ArrayDecoder should need more data after 2 bytes for 5-byte array");
+ assert_eq!(data, EMPTY);
+
+ let err = decoder.end().unwrap_err();
+ assert!(matches!(err, UnexpectedEof { .. }));
+}
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.