Add regression test for decode_from_read_unbuffered_with bug
What changed, and why it matters
This commit only adds a new regression test for an already-fixed bug in a Rust Bitcoin IO decoding function. It does not change any production code, so by itself it cannot introduce or fix a security vulnerability. The test documents a subtle edge case where a decoder consumes input one byte at a time.
No action required for this commit alone. If auditing the related bug, locate the prior commit that fixed decode_from_read_unbuffered_with and review its diff for security relevance.
Security signals we found
Regression test for prior bug in decode_from_read_unbuffered_with
No production code changes
Custom decoder with partial byte consumption and read_limit smaller than remaining need
Evidence from the diff
The diff adds a single unit test, decode_from_read_unbuffered_partial_consume, in io/src/lib.rs. The test constructs a custom Decoder that consumes one byte per push_bytes call, reports a read_limit smaller than its internal target, and verifies that decode_from_read_unbuffered_with correctly handles partial consumption. No implementation code is modified. The commit message references a pre-existing bug in decode_from_read_unbuffered_with and attributes the test case to Project Loupe.
Changed components
io/src/lib.rs (tests only)Inspect captured patch +60 / −0
diff --git a/io/src/lib.rs b/io/src/lib.rs
index f51e8b79..5d9d5d0f 100644
--- a/io/src/lib.rs
+++ b/io/src/lib.rs
@@ -947,4 +947,64 @@ mod tests {
let decoded = result.unwrap();
assert_eq!(decoded.0, [1, 2, 3, 4]);
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn decode_from_read_unbuffered_partial_consume() {
+ use encoding::DecoderStatus;
+
+ // Consumes one byte per push_bytes call. Returns Ready after
+ // it has accumulated 4 bytes. Checks for correct behaviour on
+ // partial consumption in push_bytes.
+ #[derive(Default)]
+ struct OneAtATimeDecoder {
+ buf: Vec<u8>,
+ }
+
+ #[derive(Debug, PartialEq)]
+ struct OneAtATime(Vec<u8>);
+
+ #[derive(Debug)]
+ struct OneAtATimeError;
+
+ impl Decoder for OneAtATimeDecoder {
+ type Output = OneAtATime;
+ type Error = OneAtATimeError;
+
+ fn push_bytes(
+ &mut self,
+ bytes: &mut &[u8],
+ ) -> core::result::Result<DecoderStatus, Self::Error> {
+ if self.buf.len() < 4 && !bytes.is_empty() {
+ self.buf.push(bytes[0]);
+ *bytes = &bytes[1..];
+ }
+ if self.buf.len() == 4 {
+ Ok(DecoderStatus::Ready)
+ } else {
+ Ok(DecoderStatus::NeedsMore)
+ }
+ }
+
+ fn end(self) -> core::result::Result<Self::Output, Self::Error> {
+ if self.buf.len() == 4 {
+ Ok(OneAtATime(self.buf))
+ } else {
+ Err(OneAtATimeError)
+ }
+ }
+
+ fn read_limit(&self) -> usize { 4 - self.buf.len() }
+ }
+
+ impl Decode for OneAtATime {
+ type Decoder = OneAtATimeDecoder;
+ }
+
+ let data = [0x11, 0x22, 0x33, 0x44];
+ let cursor = Cursor::new(&data);
+ let decoded: OneAtATime =
+ decode_from_read_unbuffered_with::<_, _, 16>(cursor).expect("decode succeeds");
+ assert_eq!(decoded.0, vec![0x11, 0x22, 0x33, 0x44]);
+ }
}
Why this scored 11/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.