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}