Skip to main content

ic_host_artifacts/artifact/chunks/
mod.rs

1//! Bounded ordered chunk identities from one source traversal.
2
3#[cfg(test)]
4mod tests;
5
6use super::{ArtifactError, ArtifactIdentity, Sha256Digest, visit_reader};
7use sha2::{Digest, Sha256};
8use std::{collections::TryReserveError, fmt, io::Read, num::NonZeroUsize};
9
10/// Chunk hashing failed without returning partial identities.
11#[derive(Debug)]
12pub enum ChunkDigestError {
13    /// Source read or complete-input allowance failed.
14    Input(ArtifactError),
15    /// More chunks were needed than the caller permits retaining.
16    ChunkLimit {
17        /// Maximum number of retained chunk digests.
18        limit: usize,
19    },
20    /// Storage for a chunk digest could not be allocated.
21    Allocation(TryReserveError),
22}
23
24impl fmt::Display for ChunkDigestError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Input(source) => write!(f, "chunk input failed: {source}"),
28            Self::ChunkLimit { limit } => write!(f, "input exceeds {limit} chunks"),
29            Self::Allocation(source) => write!(f, "chunk digest allocation failed: {source}"),
30        }
31    }
32}
33
34impl std::error::Error for ChunkDigestError {
35    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36        match self {
37            Self::Input(source) => Some(source),
38            Self::Allocation(source) => Some(source),
39            Self::ChunkLimit { .. } => None,
40        }
41    }
42}
43
44impl From<ArtifactError> for ChunkDigestError {
45    fn from(source: ArtifactError) -> Self {
46        Self::Input(source)
47    }
48}
49
50/// Hash ordered fixed-size chunks and the complete input in one bounded pass.
51///
52/// Returns `(chunk_digests, complete_identity)`. Chunk boundaries depend only on
53/// `chunk_bytes`, never on reader fragmentation. Only the final chunk may be
54/// shorter; exact multiples add no empty chunk. Empty input has no chunks and
55/// the ordinary SHA-256 of an empty stream. Zero chunk size is excluded by type.
56///
57/// Reuses the bounded reader engine: reads at most `max_bytes + 1` bytes,
58/// retries interrupted reads, and rejects impossible reader byte counts.
59/// Working storage is constant apart from the fallibly allocated digest vector,
60/// bounded by `max_chunks`. No buffer proportional to chunk size is allocated.
61/// A blocking reader's deadline remains caller-owned.
62///
63/// Hashes exactly the supplied bytes, with no gzip detection/decompression or
64/// Wasm interpretation. A compressed artifact's upload chunks and whole identity
65/// differ from its decoded module identity. Chunk sizes, schemas, trusted digest
66/// comparison, source custody and upload/retry policy remain caller-owned.
67///
68/// # Errors
69/// Returns typed input, byte/chunk-bound or allocation failures. No partial
70/// digest vector or complete identity is returned on failure.
71pub fn chunk_digests(
72    mut reader: impl Read,
73    chunk_bytes: NonZeroUsize,
74    max_bytes: u64,
75    max_chunks: usize,
76) -> Result<(Vec<Sha256Digest>, ArtifactIdentity), ChunkDigestError> {
77    let mut digests = Vec::new();
78    let mut whole = Sha256::new();
79    let mut chunk = Sha256::new();
80    let mut filled = 0;
81    let bytes = visit_reader::<ChunkDigestError>(&mut reader, max_bytes, |mut buffer| {
82        whole.update(buffer);
83        while !buffer.is_empty() {
84            if filled == 0 {
85                if digests.len() == max_chunks {
86                    return Err(ChunkDigestError::ChunkLimit { limit: max_chunks });
87                }
88                digests
89                    .try_reserve_exact(1)
90                    .map_err(ChunkDigestError::Allocation)?;
91            }
92            let count = buffer.len().min(chunk_bytes.get() - filled);
93            chunk.update(&buffer[..count]);
94            filled += count;
95            buffer = &buffer[count..];
96            if filled == chunk_bytes.get() {
97                digests.push(Sha256Digest::from_bytes(chunk.finalize_reset().into()));
98                filled = 0;
99            }
100        }
101        Ok(())
102    })?;
103    if filled != 0 {
104        // Capacity was reserved before accepting the partial chunk's first byte.
105        digests.push(Sha256Digest::from_bytes(chunk.finalize().into()));
106    }
107    Ok((
108        digests,
109        ArtifactIdentity {
110            bytes,
111            sha256: Sha256Digest::from_bytes(whole.finalize().into()),
112        },
113    ))
114}