Skip to main content

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