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};
11use std::{
12    io::{self, Read, Write},
13    path::{Path, PathBuf},
14};
15use thiserror::Error;
16
17/// Checksum one regular filesystem file without following path symlinks.
18///
19/// # Errors
20/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
21pub fn checksum_file(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
22    secure::checksum_path(path, secure::ExpectedArtifactType::File)
23}
24
25/// Checksum one file or a deterministic directory listing.
26///
27/// # Errors
28/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
29pub fn checksum_path(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
30    secure::checksum_path(path, secure::ExpectedArtifactType::Any)
31}
32
33/// Checksum a directory using sorted relative-path/file-digest pairs.
34///
35/// # Errors
36/// Rejects unsafe entry types, unsupported platforms and filesystem failures.
37pub fn checksum_directory(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
38    secure::checksum_path(path, secure::ExpectedArtifactType::Directory)
39}
40
41/// Stream an already-open reader using a bounded transfer buffer.
42///
43/// # Errors
44/// Returns the reader's IO failure.
45pub fn checksum_reader(reader: &mut impl Read) -> Result<ArtifactChecksumRecord, ArtifactError> {
46    let mut hasher = Sha256::new();
47    let mut buffer = vec![0; 64 * 1024];
48    loop {
49        let read = reader.read(&mut buffer)?;
50        if read == 0 {
51            break;
52        }
53        hasher.update(&buffer[..read]);
54    }
55    Ok(ArtifactChecksumRecord::from_digest(
56        hasher.finalize().into(),
57    ))
58}
59
60pub(crate) fn copy_from_reader(
61    reader: &mut impl Read,
62    writer: &mut impl Write,
63) -> Result<ArtifactChecksumRecord, ArtifactError> {
64    let mut hasher = Sha256::new();
65    let mut buffer = vec![0; 64 * 1024];
66    loop {
67        let read = reader.read(&mut buffer)?;
68        if read == 0 {
69            break;
70        }
71        writer.write_all(&buffer[..read])?;
72        hasher.update(&buffer[..read]);
73    }
74    Ok(ArtifactChecksumRecord::from_digest(
75        hasher.finalize().into(),
76    ))
77}
78
79pub(crate) fn checksum_relative_files(
80    mut files: Vec<(PathBuf, ArtifactChecksumRecord)>,
81) -> ArtifactChecksumRecord {
82    files.sort_by(|left, right| left.0.cmp(&right.0));
83    let mut hasher = Sha256::new();
84    for (relative, checksum) in files {
85        hasher.update(relative.to_string_lossy().as_bytes());
86        hasher.update([0]);
87        hasher.update(checksum.hash().as_bytes());
88        hasher.update(*b"\n");
89    }
90    ArtifactChecksumRecord::from_digest(hasher.finalize().into())
91}
92
93#[cfg(unix)]
94fn require_utf8_tree_name(
95    name: &std::ffi::OsStr,
96    display_root: &Path,
97) -> Result<(), ArtifactError> {
98    if name.to_str().is_none() {
99        return Err(ArtifactError::NonUtf8Path {
100            path: display_root.join(name),
101        });
102    }
103    Ok(())
104}
105
106/// Checksum a normal relative path beneath an operator-selected root.
107///
108/// # Errors
109/// Rejects traversal, symlinks, special entries and IO failures.
110pub fn checksum_relative_path(
111    root: &Path,
112    relative: &Path,
113) -> Result<ArtifactChecksumRecord, ArtifactError> {
114    secure::checksum_relative_path(root, relative)
115}
116
117/// Stage exact source bytes in a new private file or directory and checksum them.
118///
119/// The caller owns a trusted destination parent. This copy is not durable
120/// publication; use the persistence operation after verifying its digest.
121///
122/// # Errors
123/// Rejects source traversal/symlinks, existing destinations and IO failures.
124pub fn stage_relative_path(
125    root: &Path,
126    relative: &Path,
127    destination: &Path,
128) -> Result<ArtifactChecksumRecord, ArtifactError> {
129    secure::stage_relative_path(root, relative, destination)
130}
131
132/// Typed artifact traversal or IO failure.
133#[derive(Debug, Error)]
134pub enum ArtifactError {
135    /// A path cannot be represented exactly in the maintained UTF-8 tree digest.
136    #[error("artifact path is not UTF-8: {path:?}")]
137    NonUtf8Path {
138        /// Exact rejected filesystem path.
139        path: PathBuf,
140    },
141    /// A filesystem operation or stream failed.
142    #[error(transparent)]
143    Io(#[from] io::Error),
144    /// A tree entry is neither a regular file nor a directory.
145    #[error("unsupported artifact entry at {path}: {kind}")]
146    UnsupportedEntry {
147        /// Entry path for diagnostics.
148        path: String,
149        /// Observed filesystem entry kind.
150        kind: String,
151    },
152    /// Secure descriptor traversal is unavailable on this host.
153    #[error("secure artifact traversal is unsupported on platform {0}")]
154    UnsupportedPlatform(&'static str),
155}