Skip to main content

ic_backup/ops/artifacts/
mod.rs

1//! Stream artifact bytes through descriptor-based no-follow traversal.
2
3#[cfg(test)]
4mod regressions;
5mod secure;
6#[cfg(test)]
7mod tests;
8
9use crate::model::artifacts::ArtifactChecksumRecord;
10use sha2::{Digest, Sha256};
11#[cfg(unix)]
12use std::io::Write;
13use std::{
14    io::{self, Read},
15    path::{Path, PathBuf},
16};
17use thiserror::Error;
18
19/// Checksum one regular filesystem file without following path symlinks.
20///
21/// # Errors
22/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
23pub fn checksum_file(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
24    secure::checksum_path(path, secure::ExpectedArtifactType::File)
25}
26
27/// Checksum one file or a deterministic directory listing.
28///
29/// # Errors
30/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
31pub fn checksum_path(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
32    secure::checksum_path(path, secure::ExpectedArtifactType::Any)
33}
34
35/// Checksum a directory using sorted relative-path/file-digest pairs.
36///
37/// # Errors
38/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
39pub fn checksum_directory(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
40    secure::checksum_path(path, secure::ExpectedArtifactType::Directory)
41}
42
43/// Stream an already-open reader using a bounded transfer buffer.
44///
45/// Interrupted reads retry internally. The caller owns blocking and timeouts;
46/// no network or paid-operation retry is performed.
47///
48/// # Errors
49/// Returns other reader IO failures unchanged; impossible byte counts reject as
50/// [`io::ErrorKind::InvalidData`] rather than indexing outside the transfer buffer.
51pub fn checksum_reader(reader: &mut impl Read) -> Result<ArtifactChecksumRecord, ArtifactError> {
52    use ic_host_artifacts::artifact::ArtifactError as InputError;
53
54    let identity =
55        ic_host_artifacts::artifact::hash_reader(reader, u64::MAX).map_err(
56            |error| match error {
57                InputError::Io(error) => ArtifactError::Io(error),
58                error => ArtifactError::Io(io::Error::other(error)),
59            },
60        )?;
61    Ok(ArtifactChecksumRecord::from_digest(
62        *identity.sha256.as_bytes(),
63    ))
64}
65
66#[cfg(unix)]
67pub(crate) fn copy_from_reader(
68    reader: &mut impl Read,
69    writer: &mut impl Write,
70) -> Result<ArtifactChecksumRecord, ArtifactError> {
71    use ic_host_artifacts::artifact::{ArtifactError as InputError, CopyError};
72
73    // Artifacts have no total-size ceiling here. Descriptor admission, private
74    // staging, retained checksum comparison and publication remain local.
75    let identity =
76        ic_host_artifacts::artifact::copy_reader(reader, writer, u64::MAX).map_err(|error| {
77            match error {
78                CopyError::Input(InputError::Io(error)) | CopyError::Output(error) => {
79                    ArtifactError::Io(error)
80                }
81                CopyError::Input(error) => ArtifactError::Io(io::Error::other(error)),
82            }
83        })?;
84    Ok(ArtifactChecksumRecord::from_digest(
85        *identity.sha256.as_bytes(),
86    ))
87}
88
89pub(crate) fn checksum_relative_files(
90    mut files: Vec<(PathBuf, ArtifactChecksumRecord)>,
91) -> ArtifactChecksumRecord {
92    files.sort_by(|left, right| left.0.cmp(&right.0));
93    let mut hasher = Sha256::new();
94    for (relative, checksum) in files {
95        hasher.update(relative.to_string_lossy().as_bytes());
96        hasher.update([0]);
97        hasher.update(checksum.hash().as_bytes());
98        hasher.update(*b"\n");
99    }
100    ArtifactChecksumRecord::from_digest(hasher.finalize().into())
101}
102
103#[cfg(unix)]
104fn require_utf8_tree_name(
105    name: &std::ffi::OsStr,
106    display_root: &Path,
107) -> Result<(), ArtifactError> {
108    if name.to_str().is_none() {
109        return Err(ArtifactError::NonUtf8Path {
110            path: display_root.join(name),
111        });
112    }
113    Ok(())
114}
115
116/// Checksum a normal relative path beneath an operator-selected root.
117///
118/// # Errors
119/// Rejects traversal, symlinks, special entries and IO failures.
120pub fn checksum_relative_path(
121    root: &Path,
122    relative: &Path,
123) -> Result<ArtifactChecksumRecord, ArtifactError> {
124    secure::checksum_relative_path(root, relative)
125}
126
127/// Stage exact source bytes in a new private file or directory and checksum them.
128///
129/// The caller owns a trusted destination parent. This copy is not durable
130/// publication; use the persistence operation after verifying its digest.
131///
132/// # Errors
133/// Rejects source traversal/symlinks, existing destinations and IO failures.
134pub fn stage_relative_path(
135    root: &Path,
136    relative: &Path,
137    destination: &Path,
138) -> Result<ArtifactChecksumRecord, ArtifactError> {
139    secure::stage_relative_path(root, relative, destination)
140}
141
142/// Typed artifact traversal or IO failure.
143#[derive(Debug, Error)]
144pub enum ArtifactError {
145    /// A path cannot be represented exactly in the maintained UTF-8 tree digest.
146    #[error("artifact path is not UTF-8: {path:?}")]
147    NonUtf8Path {
148        /// Exact rejected filesystem path.
149        path: PathBuf,
150    },
151    /// A filesystem operation or stream failed.
152    #[error(transparent)]
153    Io(#[from] io::Error),
154    /// A tree entry is neither a regular file nor a directory.
155    #[error("unsupported artifact entry at {path}: {kind}")]
156    UnsupportedEntry {
157        /// Entry path for diagnostics.
158        path: String,
159        /// Observed filesystem entry kind.
160        kind: String,
161    },
162    /// Secure descriptor traversal is unavailable on this host.
163    #[error("secure artifact traversal is unsupported on platform {0}")]
164    UnsupportedPlatform(&'static str),
165}