Skip to main content

ic_host_artifacts/artifact/hashing/
mod.rs

1//! Hash accepted bytes from a push producer without buffering its complete output.
2
3#[cfg(test)]
4mod tests;
5
6use super::{ArtifactIdentity, BoundedWriter, Sha256Digest};
7use sha2::{Digest, Sha256};
8use std::io::{self, Write};
9
10/// A bounded caller-owned sink with a raw SHA-256 of successful writes.
11///
12/// Counts and hashes only the bytes reported as accepted by the sink. Short
13/// writes, interrupted writes and byte allowances use [`BoundedWriter`]'s
14/// existing contract; [`Write::write_all`] handles retries through this wrapper.
15/// Sink errors can have unreported partial effects, which cannot be identified.
16/// Encoding, flush/sync, publication and recovery remain caller-owned.
17///
18/// Accept a complete artifact identity only after the producer/serializer
19/// succeeds. Recovering parts after failure describes an observed prefix,
20/// not a completed or durable artifact. This wrapper never publishes output.
21pub struct HashingWriter<W> {
22    writer: BoundedWriter<W>,
23    hasher: Sha256,
24}
25
26impl<W> HashingWriter<W> {
27    /// Select the sink and inclusive allowance for accepted bytes.
28    #[must_use]
29    pub fn new(writer: W, max_bytes: u64) -> Self {
30        Self {
31            writer: BoundedWriter::new(writer, max_bytes),
32            hasher: Sha256::new(),
33        }
34    }
35
36    /// Number of bytes reported as accepted by successful writes.
37    #[must_use]
38    pub const fn bytes_written(&self) -> u64 {
39        self.writer.bytes_written()
40    }
41
42    /// Whether an offered buffer has exceeded the selected allowance.
43    #[must_use]
44    pub const fn limit_exceeded(&self) -> bool {
45        self.writer.limit_exceeded()
46    }
47
48    /// Borrow the original sink for caller-owned inspection without finalizing.
49    ///
50    /// Writes through shared references, duplicated handles or interior mutability
51    /// bypass this wrapper's accounting and must not be inferred from its identity.
52    #[must_use]
53    pub const fn get_ref(&self) -> &W {
54        self.writer.get_ref()
55    }
56
57    /// Snapshot the identity of accepted bytes while keeping the writer usable.
58    ///
59    /// This clones only the fixed-size hash state. It performs no sink IO, flush,
60    /// sync or publication, and cannot establish producer success or byte custody.
61    /// After an error it describes the observed prefix, not a complete artifact.
62    #[must_use]
63    pub fn identity(&self) -> ArtifactIdentity {
64        ArtifactIdentity {
65            bytes: self.writer.bytes_written(),
66            sha256: Sha256Digest::from_bytes(self.hasher.clone().finalize().into()),
67        }
68    }
69
70    /// Recover the sink and identity of the observed accepted bytes.
71    ///
72    /// Does not flush, certify producer success or discard partial output.
73    /// A failed producer must not use this prefix identity to publish an artifact.
74    #[must_use]
75    pub fn into_parts(self) -> (W, ArtifactIdentity) {
76        let identity = ArtifactIdentity {
77            bytes: self.writer.bytes_written(),
78            sha256: Sha256Digest::from_bytes(self.hasher.finalize().into()),
79        };
80        (self.writer.into_inner(), identity)
81    }
82}
83
84impl<W: Write> Write for HashingWriter<W> {
85    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
86        let accepted = self.writer.write(buffer)?;
87        self.hasher.update(&buffer[..accepted]);
88        Ok(accepted)
89    }
90
91    fn flush(&mut self) -> io::Result<()> {
92        self.writer.flush()
93    }
94}