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/// # Errors
46/// Returns the reader's IO failure.
47pub fn checksum_reader(reader: &mut impl Read) -> Result<ArtifactChecksumRecord, ArtifactError> {
48    let mut hasher = Sha256::new();
49    let mut buffer = vec![0; 64 * 1024];
50    loop {
51        let read = reader.read(&mut buffer)?;
52        if read == 0 {
53            break;
54        }
55        hasher.update(&buffer[..read]);
56    }
57    Ok(ArtifactChecksumRecord::from_digest(
58        hasher.finalize().into(),
59    ))
60}
61
62#[cfg(unix)]
63pub(crate) fn copy_from_reader(
64    reader: &mut impl Read,
65    writer: &mut impl Write,
66) -> Result<ArtifactChecksumRecord, ArtifactError> {
67    use ic_host_tools::artifact::{ArtifactError as InputError, CopyError};
68
69    // Artifacts have no total-size ceiling here. Descriptor admission, private
70    // staging, retained checksum comparison and publication remain local.
71    let identity = ic_host_tools::artifact::copy_reader(reader, writer, u64::MAX).map_err(
72        |error| match error {
73            CopyError::Input(InputError::Io(error)) | CopyError::Output(error) => {
74                ArtifactError::Io(error)
75            }
76            CopyError::Input(error) => ArtifactError::Io(io::Error::other(error)),
77        },
78    )?;
79    Ok(ArtifactChecksumRecord::from_digest(
80        *identity.sha256.as_bytes(),
81    ))
82}
83
84pub(crate) fn checksum_relative_files(
85    mut files: Vec<(PathBuf, ArtifactChecksumRecord)>,
86) -> ArtifactChecksumRecord {
87    files.sort_by(|left, right| left.0.cmp(&right.0));
88    let mut hasher = Sha256::new();
89    for (relative, checksum) in files {
90        hasher.update(relative.to_string_lossy().as_bytes());
91        hasher.update([0]);
92        hasher.update(checksum.hash().as_bytes());
93        hasher.update(*b"\n");
94    }
95    ArtifactChecksumRecord::from_digest(hasher.finalize().into())
96}
97
98#[cfg(unix)]
99fn require_utf8_tree_name(
100    name: &std::ffi::OsStr,
101    display_root: &Path,
102) -> Result<(), ArtifactError> {
103    if name.to_str().is_none() {
104        return Err(ArtifactError::NonUtf8Path {
105            path: display_root.join(name),
106        });
107    }
108    Ok(())
109}
110
111/// Checksum a normal relative path beneath an operator-selected root.
112///
113/// # Errors
114/// Rejects traversal, symlinks, special entries and IO failures.
115pub fn checksum_relative_path(
116    root: &Path,
117    relative: &Path,
118) -> Result<ArtifactChecksumRecord, ArtifactError> {
119    secure::checksum_relative_path(root, relative)
120}
121
122/// Stage exact source bytes in a new private file or directory and checksum them.
123///
124/// The caller owns a trusted destination parent. This copy is not durable
125/// publication; use the persistence operation after verifying its digest.
126///
127/// # Errors
128/// Rejects source traversal/symlinks, existing destinations and IO failures.
129pub fn stage_relative_path(
130    root: &Path,
131    relative: &Path,
132    destination: &Path,
133) -> Result<ArtifactChecksumRecord, ArtifactError> {
134    secure::stage_relative_path(root, relative, destination)
135}
136
137/// Typed artifact traversal or IO failure.
138#[derive(Debug, Error)]
139pub enum ArtifactError {
140    /// A path cannot be represented exactly in the maintained UTF-8 tree digest.
141    #[error("artifact path is not UTF-8: {path:?}")]
142    NonUtf8Path {
143        /// Exact rejected filesystem path.
144        path: PathBuf,
145    },
146    /// A filesystem operation or stream failed.
147    #[error(transparent)]
148    Io(#[from] io::Error),
149    /// A tree entry is neither a regular file nor a directory.
150    #[error("unsupported artifact entry at {path}: {kind}")]
151    UnsupportedEntry {
152        /// Entry path for diagnostics.
153        path: String,
154        /// Observed filesystem entry kind.
155        kind: String,
156    },
157    /// Secure descriptor traversal is unavailable on this host.
158    #[error("secure artifact traversal is unsupported on platform {0}")]
159    UnsupportedPlatform(&'static str),
160}