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/// 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/// The shared [`BoundedWriter`] validates accepted-byte counts before advancing
54/// through output. Impossible reader/writer counts return IO
55/// [`io::ErrorKind::InvalidData`] through the corresponding error variant.
56/// A blocking reader or writer's deadlines remain caller-owned.
57///
58/// Bytes reach the sink before the complete source is known. Supply a private
59/// staging sink, check the returned identity against your admitted digest, and
60/// validate it before publication. Copying does not freeze source contents or
61/// independently re-read the sink. On every failure, partial output remains
62/// available to the caller; no identity for the complete input is returned.
63///
64/// This function never opens paths, flushes, synchronizes, renames or deletes
65/// files. It chooses no filesystem authority or publication/recovery policy.
66///
67/// # Errors
68/// Returns distinct typed source/limit and sink failures. Output may contain a
69/// prefix even when the source is oversized or a write fails.
70pub fn copy_reader(
71 mut reader: impl Read,
72 writer: &mut impl Write,
73 max_bytes: u64,
74) -> Result<ArtifactIdentity, CopyError> {
75 let mut hasher = Sha256::new();
76 let mut writer = BoundedWriter::new(writer, max_bytes);
77 let bytes = visit_reader::<CopyError>(&mut reader, max_bytes, |chunk| {
78 writer.write_all(chunk).map_err(CopyError::Output)?;
79 hasher.update(chunk);
80 Ok(())
81 })?;
82 Ok(ArtifactIdentity {
83 bytes,
84 sha256: Sha256Digest::from_bytes(hasher.finalize().into()),
85 })
86}