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}