What changed, and why it matters
This commit adds new helper functions for decoding data from a reader in the rust-bitcoin `io` crate. It is a feature addition that copies existing decoding logic from another module and includes tests. There is no indication in the commit or supplied references that this fixes a security vulnerability.
No security action required. Review as normal code-quality/feature addition if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces decode_from_read, decode_from_read_unbuffered, and decode_from_read_unbuffered_with functions, plus a ReadError enum, to the io crate. It also wires the encoding crate dependency into the std/alloc feature flags. The code is a straightforward port of standalone decoding functions from consensus_encoding for use with the crate’s BufRead/Read traits. The change is additive and includes unit tests for success, EOF, empty input, extra data, trait-object, and by-reference usage.
Changed components
io/src/lib.rsio/Cargo.tomlInspect captured patch +272 / −4
diff --git a/io/Cargo.toml b/io/Cargo.toml
index 4129ae92..bc18c87f 100644
--- a/io/Cargo.toml
+++ b/io/Cargo.toml
@@ -15,8 +15,8 @@ exclude = ["tests", "contrib"]
[features]
default = ["std"]
-std = ["alloc", "hashes?/std", "internals/std"]
-alloc = ["hashes?/alloc", "internals/alloc"]
+std = ["alloc", "encoding/std", "hashes?/std", "internals/std"]
+alloc = ["encoding/alloc", "hashes?/alloc", "internals/alloc"]
[dependencies]
internals = { package = "bitcoin-internals", path = "../internals" }
diff --git a/io/src/lib.rs b/io/src/lib.rs
index 40952362..3eb384ec 100644
--- a/io/src/lib.rs
+++ b/io/src/lib.rs
@@ -41,7 +41,7 @@ mod hash;
use alloc::vec::Vec;
use core::cmp;
-use encoding::Encoder;
+use encoding::{Decodable, Decoder, Encoder};
#[rustfmt::skip] // Keep public re-exports separate.
pub use self::error::{Error, ErrorKind};
@@ -437,12 +437,172 @@ where
Ok(())
}
+/// Decodes an object from a buffered reader.
+///
+/// # Performance
+///
+/// For unbuffered readers (like [`std::fs::File`] or [`std::net::TcpStream`]), consider wrapping
+/// your reader with [`std::io::BufReader`] in order to use this function. This avoids frequent
+/// small reads, which can significantly impact performance.
+///
+/// # Errors
+///
+/// Returns [`ReadError::Decode`] if the decoder encounters an error while parsing
+/// the data, or [`ReadError::Io`] if an I/O error occurs while reading.
+pub fn decode_from_read<T, R>(mut reader: R) -> core::result::Result<T, ReadError<<T::Decoder as Decoder>::Error>>
+where
+ T: Decodable,
+ R: BufRead,
+{
+ let mut decoder = T::decoder();
+
+ loop {
+ let mut buffer = match reader.fill_buf() {
+ Ok(buffer) => buffer,
+ // Auto retry read for non-fatal error.
+ Err(error) if error.kind() == ErrorKind::Interrupted => continue,
+ Err(error) => return Err(ReadError::Io(error)),
+ };
+
+ if buffer.is_empty() {
+ // EOF, but still try to finalize the decoder.
+ return decoder.end().map_err(ReadError::Decode);
+ }
+
+ let original_len = buffer.len();
+ let need_more = decoder.push_bytes(&mut buffer).map_err(ReadError::Decode)?;
+ let consumed = original_len - buffer.len();
+ reader.consume(consumed);
+
+ if !need_more {
+ return decoder.end().map_err(ReadError::Decode);
+ }
+ }
+}
+
+/// Decodes an object from an unbuffered reader using a fixed-size buffer.
+///
+/// For most use cases, prefer [`decode_from_read`] with a [`std::io::BufReader`].
+/// This function is only needed when you have an unbuffered reader which you
+/// cannot wrap. It will probably have worse performance.
+///
+/// # Buffer
+///
+/// Uses a fixed 4KB (4096 bytes) stack-allocated buffer that is reused across
+/// read operations. This size is a good balance between memory usage and
+/// system call efficiency for most use cases.
+///
+/// For different buffer sizes, use [`decode_from_read_unbuffered_with`].
+///
+/// # Errors
+///
+/// Returns [`ReadError::Decode`] if the decoder encounters an error while parsing
+/// the data, or [`ReadError::Io`] if an I/O error occurs while reading.
+pub fn decode_from_read_unbuffered<T, R>(
+ reader: R,
+) -> core::result::Result<T, ReadError<<T::Decoder as Decoder>::Error>>
+where
+ T: Decodable,
+ R: Read,
+{
+ decode_from_read_unbuffered_with::<T, R, 4096>(reader)
+}
+
+/// Decodes an object from an unbuffered reader using a custom-sized buffer.
+///
+/// For most use cases, prefer [`decode_from_read`] with a [`std::io::BufReader`].
+/// This function is only needed when you have an unbuffered reader which you
+/// cannot wrap. It will probably have worse performance.
+///
+/// # Buffer
+///
+/// The `BUFFER_SIZE` parameter controls the intermediate buffer size used for
+/// reading. The buffer is allocated on the stack (not heap) and reused across
+/// read operations. Larger buffers reduce the number of system calls, but use
+/// more memory.
+///
+/// # Errors
+///
+/// Returns [`ReadError::Decode`] if the decoder encounters an error while parsing
+/// the data, or [`ReadError::Io`] if an I/O error occurs while reading.
+pub fn decode_from_read_unbuffered_with<T, R, const BUFFER_SIZE: usize>(
+ mut reader: R,
+) -> core::result::Result<T, ReadError<<T::Decoder as Decoder>::Error>>
+where
+ T: Decodable,
+ R: Read,
+{
+ let mut decoder = T::decoder();
+ let mut buffer = [0u8; BUFFER_SIZE];
+
+ while decoder.read_limit() > 0 {
+ // Only read what we need, up to buffer size.
+ let clamped_buffer = &mut buffer[..decoder.read_limit().min(BUFFER_SIZE)];
+ match reader.read(clamped_buffer) {
+ Ok(0) => {
+ // EOF, but still try to finalize the decoder.
+ return decoder.end().map_err(ReadError::Decode);
+ }
+ Ok(bytes_read) => {
+ if !decoder
+ .push_bytes(&mut &clamped_buffer[..bytes_read])
+ .map_err(ReadError::Decode)?
+ {
+ return decoder.end().map_err(ReadError::Decode);
+ }
+ }
+ Err(ref e) if e.kind() == ErrorKind::Interrupted => {
+ // Auto retry read for non-fatal error.
+ }
+ Err(e) => return Err(ReadError::Io(e)),
+ }
+ }
+
+ decoder.end().map_err(ReadError::Decode)
+}
+
+/// An error that can occur when reading and decoding from a buffered reader.
+#[derive(Debug)]
+pub enum ReadError<D> {
+ /// An I/O error occurred while reading from the reader.
+ Io(Error),
+ /// The decoder encountered an error while parsing the data.
+ Decode(D),
+}
+
+impl<D: core::fmt::Display> core::fmt::Display for ReadError<D> {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Self::Io(e) => write!(f, "I/O error: {}", e),
+ Self::Decode(e) => write!(f, "decode error: {}", e),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl<D> std::error::Error for ReadError<D>
+where
+ D: core::fmt::Debug + core::fmt::Display + std::error::Error + 'static,
+{
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::Io(e) => Some(e),
+ Self::Decode(e) => Some(e),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl<D> From<Error> for ReadError<D> {
+ fn from(e: Error) -> Self { Self::Io(e) }
+}
+
#[cfg(test)]
mod tests {
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::{string::ToString, vec};
- use encoding::ArrayEncoder;
+ use encoding::{ArrayDecoder, ArrayEncoder, UnexpectedEofError};
use super::*;
@@ -617,4 +777,112 @@ mod tests {
assert_eq!(buf, [0x78, 0x56, 0x34, 0x12]);
}
+
+ #[derive(Debug, PartialEq)]
+ struct TestArray([u8; 4]);
+
+ impl Decodable for TestArray {
+ type Decoder = TestArrayDecoder;
+ fn decoder() -> Self::Decoder { TestArrayDecoder { inner: ArrayDecoder::new() } }
+ }
+
+ struct TestArrayDecoder {
+ inner: ArrayDecoder<4>,
+ }
+
+ impl Decoder for TestArrayDecoder {
+ type Output = TestArray;
+ type Error = UnexpectedEofError;
+
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> core::result::Result<bool, Self::Error> {
+ self.inner.push_bytes(bytes)
+ }
+
+ fn end(self) -> core::result::Result<Self::Output, Self::Error> { self.inner.end().map(TestArray) }
+
+ fn read_limit(&self) -> usize { self.inner.read_limit() }
+ }
+
+ #[test]
+ fn decode_from_read_success() {
+ let data = [1, 2, 3, 4];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read(cursor);
+ assert!(result.is_ok());
+ let decoded = result.unwrap();
+ assert_eq!(decoded.0, [1, 2, 3, 4]);
+ }
+
+ #[test]
+ fn decode_from_read_unexpected_eof() {
+ let data = [1, 2, 3];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read(cursor);
+ assert!(matches!(result, Err(ReadError::Decode(_))));
+ }
+
+ #[test]
+ fn decode_from_read_trait_object() {
+ let data = [1, 2, 3, 4];
+ let mut cursor = Cursor::new(&data);
+ // Test that we can pass a trait object (&mut dyn BufRead implements BufRead).
+ let reader: &mut dyn BufRead = &mut cursor;
+ let result: core::result::Result<TestArray, _> = decode_from_read(reader);
+ assert!(result.is_ok());
+ let decoded = result.unwrap();
+ assert_eq!(decoded.0, [1, 2, 3, 4]);
+ }
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn decode_from_read_by_reference() {
+ use crate::alloc::vec::Vec;
+
+ let data = [1, 2, 3, 4];
+ let mut cursor = Cursor::new(&data);
+ // Test that we can pass by reference (&mut T implements BufRead when T: BufRead).
+ let result: core::result::Result<TestArray, _> = decode_from_read(&mut cursor);
+ assert!(result.is_ok());
+ let decoded = result.unwrap();
+ assert_eq!(decoded.0, [1, 2, 3, 4]);
+
+ let mut buf = Vec::new();
+ let _ = cursor.read_to_limit(&mut buf, 100);
+ }
+
+ #[test]
+ fn decode_from_read_unbuffered_success() {
+ let data = [1, 2, 3, 4];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read_unbuffered(cursor);
+ assert!(result.is_ok());
+ let decoded = result.unwrap();
+ assert_eq!(decoded.0, [1, 2, 3, 4]);
+ }
+
+ #[test]
+ fn decode_from_read_unbuffered_unexpected_eof() {
+ let data = [1, 2, 3];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read_unbuffered(cursor);
+ assert!(matches!(result, Err(ReadError::Decode(_))));
+ }
+
+ #[test]
+ fn decode_from_read_unbuffered_empty() {
+ let data = [];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read_unbuffered(cursor);
+ assert!(matches!(result, Err(ReadError::Decode(_))));
+ }
+
+ #[test]
+ fn decode_from_read_unbuffered_extra_data() {
+ let data = [1, 2, 3, 4, 5, 6];
+ let cursor = Cursor::new(&data);
+ let result: core::result::Result<TestArray, _> = decode_from_read_unbuffered(cursor);
+ assert!(result.is_ok());
+ let decoded = result.unwrap();
+ assert_eq!(decoded.0, [1, 2, 3, 4]);
+ }
}
Why this scored 12/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.