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, buffer::reserve_bounded, 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>(
82        &mut reader,
83        &mut [0; 16 * 1024],
84        max_bytes,
85        |mut buffer| {
86            whole.update(buffer);
87            while !buffer.is_empty() {
88                if filled == 0 {
89                    if digests.len() == max_chunks {
90                        return Err(ChunkDigestError::ChunkLimit { limit: max_chunks });
91                    }
92                    reserve_bounded(&mut digests, 1, max_chunks)
93                        .map_err(ChunkDigestError::Allocation)?;
94                }
95                let count = buffer.len().min(chunk_bytes.get() - filled);
96                chunk.update(&buffer[..count]);
97                filled += count;
98                buffer = &buffer[count..];
99                if filled == chunk_bytes.get() {
100                    digests.push(Sha256Digest::from_bytes(chunk.finalize_reset().into()));
101                    filled = 0;
102                }
103            }
104            Ok(())
105        },
106    )?;
107    if filled != 0 {
108        // Capacity was reserved before accepting the partial chunk's first byte.
109        digests.push(Sha256Digest::from_bytes(chunk.finalize().into()));
110    }
111    Ok((
112        digests,
113        ArtifactIdentity {
114            bytes,
115            sha256: Sha256Digest::from_bytes(whole.finalize().into()),
116        },
117    ))
118}