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, BoundedWriter, 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/// Preserve native input/output I/O identity at an I/O-only boundary.
47///
48/// Non-I/O input failures remain a downcastable [`CopyError`] cause with kind
49/// [`io::ErrorKind::Other`]. Keep `CopyError` directly when the input/output
50/// distinction is needed: converting a native error discards that distinction.
51impl From<CopyError> for io::Error {
52 fn from(error: CopyError) -> Self {
53 match error {
54 CopyError::Input(ArtifactError::Io(source)) | CopyError::Output(source) => source,
55 other @ CopyError::Input(_) => Self::other(other),
56 }
57 }
58}
59
60/// Copy one bounded source stream and identify its bytes with constant memory.
61///
62/// Shares the read traversal used by [`super::hash_reader`] and
63/// [`super::read_reader`]: observes at most `max_bytes + 1` bytes, detects
64/// overflow before writing the overflowing chunk, and retries interrupted reads
65/// only. Writes each accepted chunk through `Write::write_all`, which handles
66/// short writes and interrupted writes according to the standard IO contract.
67/// The shared [`BoundedWriter`] validates accepted-byte counts before advancing
68/// through output. Impossible reader/writer counts return IO
69/// [`io::ErrorKind::InvalidData`] through the corresponding error variant.
70/// A blocking reader or writer's deadlines remain caller-owned.
71///
72/// Bytes reach the sink before the complete source is known. Supply a private
73/// staging sink, check the returned identity against your admitted digest, and
74/// validate it before publication. Copying does not freeze source contents or
75/// independently re-read the sink. On every failure, partial output remains
76/// available to the caller; no identity for the complete input is returned.
77///
78/// This function never opens paths, flushes, synchronizes, renames or deletes
79/// files. It chooses no filesystem authority or publication/recovery policy.
80///
81/// # Errors
82/// Returns distinct typed source/limit and sink failures. Output may contain a
83/// prefix even when the source is oversized or a write fails.
84pub fn copy_reader(
85 mut reader: impl Read,
86 writer: &mut impl Write,
87 max_bytes: u64,
88) -> Result<ArtifactIdentity, CopyError> {
89 let mut hasher = Sha256::new();
90 let mut writer = BoundedWriter::new(writer, max_bytes);
91 let bytes = visit_reader::<CopyError>(&mut reader, max_bytes, |chunk| {
92 writer.write_all(chunk).map_err(CopyError::Output)?;
93 hasher.update(chunk);
94 Ok(())
95 })?;
96 Ok(ArtifactIdentity {
97 bytes,
98 sha256: Sha256Digest::from_bytes(hasher.finalize().into()),
99 })
100}