Skip to main content

ic_host_tools/artifact/copy/
mod.rs

1//! Stream bytes to a caller-owned staging sink with bounded identity capture.
2
3#[cfg(test)]
4mod tests;
5
6use super::{ArtifactError, ArtifactIdentity, Sha256Digest, visit_reader};
7use sha2::{Digest, Sha256};
8use std::{
9    fmt,
10    io::{self, Read, Write},
11};
12
13/// A bounded copy failed before a complete source identity could be returned.
14#[derive(Debug)]
15pub enum CopyError {
16    /// Source IO or complete-input byte allowance failed.
17    Input(ArtifactError),
18    /// The caller's sink failed, possibly after accepting a partial chunk.
19    Output(io::Error),
20}
21
22impl fmt::Display for CopyError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Input(source) => write!(f, "copy input failed: {source}"),
26            Self::Output(source) => write!(f, "copy output failed: {source}"),
27        }
28    }
29}
30
31impl std::error::Error for CopyError {
32    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
33        match self {
34            Self::Input(source) => Some(source),
35            Self::Output(source) => Some(source),
36        }
37    }
38}
39
40impl From<ArtifactError> for CopyError {
41    fn from(source: ArtifactError) -> Self {
42        Self::Input(source)
43    }
44}
45
46/// Copy one bounded source stream and identify its bytes with constant memory.
47///
48/// Shares the read traversal used by [`super::hash_reader`] and
49/// [`super::read_reader`]: observes at most `max_bytes + 1` bytes, detects
50/// overflow before writing the overflowing chunk, and retries interrupted reads
51/// only. Writes each accepted chunk through `Write::write_all`, which handles
52/// short writes and interrupted writes according to the standard IO contract.
53/// A blocking reader or writer's deadlines remain caller-owned.
54///
55/// Bytes reach the sink before the complete source is known. Supply a private
56/// staging sink, check the returned identity against your admitted digest, and
57/// validate it before publication. Copying does not freeze source contents or
58/// independently re-read the sink. On every failure, partial output remains
59/// available to the caller; no identity for the complete input is returned.
60///
61/// This function never opens paths, flushes, synchronizes, renames or deletes
62/// files. It chooses no filesystem authority or publication/recovery policy.
63///
64/// # Errors
65/// Returns distinct typed source/limit and sink failures. Output may contain a
66/// prefix even when the source is oversized or a write fails.
67pub fn copy_reader(
68    mut reader: impl Read,
69    writer: &mut impl Write,
70    max_bytes: u64,
71) -> Result<ArtifactIdentity, CopyError> {
72    let mut hasher = Sha256::new();
73    let bytes = visit_reader::<CopyError>(&mut reader, max_bytes, |chunk| {
74        writer.write_all(chunk).map_err(CopyError::Output)?;
75        hasher.update(chunk);
76        Ok(())
77    })?;
78    Ok(ArtifactIdentity {
79        bytes,
80        sha256: Sha256Digest::from_bytes(hasher.finalize().into()),
81    })
82}