Skip to main content

pulith_verify/
reader.rs

1use std::io::{self, Read};
2
3use crate::{Hasher, Result, VerifyError};
4
5/// Streaming reader that hashes data as it passes through.
6/// Wraps any `Read` source for zero-copy verification.
7pub struct VerifiedReader<R, H> {
8    reader: R,
9    hasher: H,
10    bytes_processed: u64,
11}
12
13impl<R, H> VerifiedReader<R, H> {
14    /// Create a new verified reader.
15    pub fn new(reader: R, hasher: H) -> Self {
16        Self {
17            reader,
18            hasher,
19            bytes_processed: 0,
20        }
21    }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct VerificationReceipt {
26    /// Expected digest bytes supplied by the caller.
27    pub expected_digest: Vec<u8>,
28    /// Actual digest bytes computed from the stream.
29    pub actual_digest: Vec<u8>,
30    /// Number of bytes consumed from the wrapped reader.
31    pub bytes_processed: u64,
32}
33
34impl<R: Read, H: Hasher> VerifiedReader<R, H> {
35    /// Read data, hashing it in-place.
36    /// Delegates to inner reader.
37    pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
38        let n = self.reader.read(buf)?;
39        if n > 0 {
40            self.hasher.update(&buf[..n]);
41            self.bytes_processed += n as u64;
42        }
43        Ok(n)
44    }
45
46    pub fn bytes_processed(&self) -> u64 {
47        self.bytes_processed
48    }
49
50    /// Finalize verification against expected hash.
51    /// Returns error on mismatch.
52    pub fn finish(self, expected: &[u8]) -> Result<()> {
53        self.finish_with_constraints(expected, None)?;
54        Ok(())
55    }
56
57    /// Finalize verification with optional stream length enforcement.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`VerifyError::HashMismatch`] when digest verification fails.
62    /// Returns [`VerifyError::SizeMismatch`] when `expected_bytes` is provided
63    /// and differs from the consumed stream length.
64    pub fn finish_with_constraints(
65        self,
66        expected: &[u8],
67        expected_bytes: Option<u64>,
68    ) -> Result<VerificationReceipt> {
69        let actual = self.hasher.finalize();
70        if actual != expected {
71            return Err(VerifyError::HashMismatch {
72                expected: expected.to_vec(),
73                actual,
74            });
75        }
76
77        if let Some(expected_bytes) = expected_bytes
78            && self.bytes_processed != expected_bytes
79        {
80            return Err(VerifyError::SizeMismatch {
81                expected: expected_bytes,
82                actual: self.bytes_processed,
83            });
84        }
85
86        Ok(VerificationReceipt {
87            expected_digest: expected.to_vec(),
88            actual_digest: actual,
89            bytes_processed: self.bytes_processed,
90        })
91    }
92}
93
94/// Verifies an entire stream by reading it to EOF.
95///
96/// # Errors
97///
98/// Returns any I/O error from the wrapped reader.
99/// Returns [`VerifyError::HashMismatch`] or [`VerifyError::SizeMismatch`]
100/// when verification constraints fail.
101pub fn verify_stream<R: Read, H: Hasher>(
102    reader: R,
103    hasher: H,
104    expected: &[u8],
105    expected_bytes: Option<u64>,
106) -> Result<VerificationReceipt> {
107    let mut verified = VerifiedReader::new(reader, hasher);
108    let mut buffer = [0_u8; 8192];
109    loop {
110        let read = verified.read(&mut buffer)?;
111        if read == 0 {
112            break;
113        }
114    }
115    verified.finish_with_constraints(expected, expected_bytes)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::io::Cursor;
122
123    #[cfg(feature = "sha256")]
124    use crate::Sha256Hasher;
125
126    #[cfg(feature = "sha256")]
127    #[test]
128    fn test_sha256_hasher() {
129        let mut hasher = Sha256Hasher::new();
130        hasher.update(b"hello world");
131        let hash = hasher.finalize();
132
133        // Expected SHA-256 hash of "hello world" (actual computed value)
134        let expected =
135            hex::decode("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
136                .unwrap();
137        assert_eq!(hash, expected);
138    }
139
140    #[cfg(feature = "sha256")]
141    #[test]
142    fn test_verified_reader_success() {
143        let data = b"test data for verification";
144
145        // First, compute the expected hash from the data
146        let mut hasher = Sha256Hasher::new();
147        hasher.update(data);
148        let expected = hasher.finalize();
149
150        // Now test the verified reader with the computed hash
151        let reader = Cursor::new(data);
152        let hasher = Sha256Hasher::new();
153        let mut verified = VerifiedReader::new(reader, hasher);
154
155        let mut buffer = [0; 32];
156        verified.read(&mut buffer).unwrap();
157
158        // Test that verification succeeds with the computed hash
159        let receipt = verified
160            .finish_with_constraints(&expected, Some(data.len() as u64))
161            .unwrap();
162        assert_eq!(receipt.bytes_processed, data.len() as u64);
163    }
164
165    #[cfg(feature = "sha256")]
166    #[test]
167    fn test_verified_reader_hash_mismatch() {
168        let data = b"test data";
169        let reader = Cursor::new(data);
170        let hasher = Sha256Hasher::new();
171        let mut verified = VerifiedReader::new(reader, hasher);
172
173        let mut buffer = [0; 32];
174        verified.read(&mut buffer).unwrap();
175
176        // Wrong hash should cause error
177        let wrong_hash = vec![0; 32];
178        let result = verified.finish(&wrong_hash);
179        assert!(result.is_err());
180
181        if let Err(VerifyError::HashMismatch { expected, actual }) = result {
182            assert_eq!(expected, vec![0; 32]);
183            assert_ne!(actual, vec![0; 32]);
184        } else {
185            panic!("Expected HashMismatch error");
186        }
187    }
188
189    #[cfg(feature = "sha256")]
190    #[test]
191    fn test_verified_reader_size_mismatch() {
192        let data = b"test data";
193
194        let mut expected_hasher = Sha256Hasher::new();
195        expected_hasher.update(data);
196        let expected = expected_hasher.finalize();
197
198        let reader = Cursor::new(data);
199        let hasher = Sha256Hasher::new();
200        let mut verified = VerifiedReader::new(reader, hasher);
201
202        let mut buffer = [0; 32];
203        verified.read(&mut buffer).unwrap();
204
205        let result = verified.finish_with_constraints(&expected, Some((data.len() as u64) + 1));
206        assert!(matches!(
207            result,
208            Err(VerifyError::SizeMismatch {
209                expected,
210                actual
211            }) if expected == (data.len() as u64) + 1 && actual == data.len() as u64
212        ));
213    }
214
215    #[cfg(feature = "sha256")]
216    #[test]
217    fn test_verify_stream_consumes_full_reader() {
218        let data = b"stream verify content";
219        let mut expected_hasher = Sha256Hasher::new();
220        expected_hasher.update(data);
221        let expected = expected_hasher.finalize();
222
223        let receipt = verify_stream(
224            Cursor::new(data),
225            Sha256Hasher::new(),
226            &expected,
227            Some(data.len() as u64),
228        )
229        .unwrap();
230
231        assert_eq!(receipt.bytes_processed, data.len() as u64);
232    }
233}