Skip to main content

canic_backup/artifacts/
mod.rs

1//! Module: artifacts
2//!
3//! Responsibility: project shared artifact checksums into Canic manifests and validate them.
4//! Does not own: snapshot capture, artifact storage, or restore planning.
5//! Boundary: IC Backup owns no-follow traversal and private staging; Canic owns publication.
6
7#[cfg(test)]
8mod tests;
9
10use std::{
11    io::{self, Read},
12    path::{Path, PathBuf},
13};
14
15use ic_backup::{
16    model::artifacts::ArtifactChecksumRecord,
17    ops::artifacts::{self, ArtifactError},
18};
19use serde::{Deserialize, Serialize};
20use thiserror::Error as ThisError;
21
22const SHA256_ALGORITHM: &str = "sha256";
23
24pub(crate) fn artifact_path_segment(value: &str) -> String {
25    value
26        .chars()
27        .map(|ch| match ch {
28            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
29            _ => '_',
30        })
31        .collect()
32}
33
34///
35/// ArtifactChecksum
36///
37/// SHA-256 checksum metadata for a backup artifact file or directory.
38/// Owned by backup artifact support and serialized into manifests.
39///
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct ArtifactChecksum {
43    pub algorithm: String,
44    pub hash: String,
45}
46
47impl ArtifactChecksum {
48    /// Compute a SHA-256 checksum from in-memory bytes.
49    #[must_use]
50    pub fn from_bytes(bytes: &[u8]) -> Self {
51        Self::from_record(ArtifactChecksumRecord::from_bytes(bytes))
52    }
53
54    /// Compute a SHA-256 checksum from one filesystem file.
55    pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
56        artifacts::checksum_file(path)
57            .map(Self::from_record)
58            .map_err(artifact_error)
59    }
60
61    /// Compute a file checksum from an already-open artifact descriptor.
62    pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
63        artifacts::checksum_reader(reader)
64            .map(Self::from_record)
65            .map_err(artifact_error)
66    }
67
68    /// Compute a SHA-256 checksum from a file or deterministic directory listing.
69    pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
70        artifacts::checksum_path(path)
71            .map(Self::from_record)
72            .map_err(artifact_error)
73    }
74
75    /// Compute a deterministic SHA-256 checksum over all files in a directory.
76    pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
77        artifacts::checksum_directory(path)
78            .map(Self::from_record)
79            .map_err(artifact_error)
80    }
81
82    /// Compose the maintained directory checksum from relative file checksums.
83    pub(crate) fn from_relative_file_checksums(
84        files: Vec<(PathBuf, Self)>,
85    ) -> Result<Self, ArtifactChecksumError> {
86        let files = files
87            .into_iter()
88            .map(|(path, checksum)| {
89                checksum.validate()?;
90                ArtifactChecksumRecord::from_hash(&checksum.hash)
91                    .map(|record| (path, record))
92                    .map_err(|_| ArtifactChecksumError::InvalidHash(checksum.hash))
93            })
94            .collect::<Result<_, _>>()?;
95        artifacts::checksum_relative_files(files)
96            .map(Self::from_record)
97            .map_err(|error| artifact_error(error.into()))
98    }
99
100    /// Verify that the checksum matches an expected SHA-256 hash.
101    pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
102        self.validate()?;
103        Self::validate_hash(expected_hash)?;
104
105        if self.hash.eq_ignore_ascii_case(expected_hash) {
106            Ok(())
107        } else {
108            Err(ArtifactChecksumError::ChecksumMismatch {
109                expected: expected_hash.to_string(),
110                actual: self.hash.clone(),
111            })
112        }
113    }
114
115    /// Validate checksum metadata without comparing artifact bytes.
116    pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
117        Self::validate_algorithm(&self.algorithm)?;
118        Self::validate_hash(&self.hash)
119    }
120
121    /// Validate one artifact-checksum algorithm identifier.
122    pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
123        if algorithm != SHA256_ALGORITHM {
124            return Err(ArtifactChecksumError::UnsupportedAlgorithm(
125                algorithm.to_string(),
126            ));
127        }
128
129        Ok(())
130    }
131
132    /// Validate one artifact-checksum hash representation.
133    pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
134        if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
135            return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
136        }
137
138        Ok(())
139    }
140
141    pub(crate) fn from_relative_path_no_follow(
142        root: &Path,
143        relative: &Path,
144    ) -> Result<Self, ArtifactChecksumError> {
145        artifacts::checksum_relative_path(root, relative)
146            .map(Self::from_record)
147            .map_err(artifact_error)
148    }
149
150    pub(crate) fn stage_relative_path_no_follow(
151        root: &Path,
152        relative: &Path,
153        destination: &Path,
154    ) -> Result<Self, ArtifactChecksumError> {
155        artifacts::stage_relative_path(root, relative, destination)
156            .map(Self::from_record)
157            .map_err(artifact_error)
158    }
159
160    fn from_record(record: ArtifactChecksumRecord) -> Self {
161        Self {
162            algorithm: record.algorithm().to_owned(),
163            hash: record.hash().to_owned(),
164        }
165    }
166}
167
168fn artifact_error(error: ArtifactError) -> ArtifactChecksumError {
169    match error {
170        ArtifactError::Io(error) => ArtifactChecksumError::Io(error),
171        error => ArtifactChecksumError::Artifact(error),
172    }
173}
174
175///
176/// ArtifactChecksumError
177///
178/// Typed checksum failure returned by backup artifact hashing and validation.
179/// Owned by backup artifact support and surfaced to snapshot/runner callers.
180///
181
182#[derive(Debug, ThisError)]
183pub enum ArtifactChecksumError {
184    #[error("checksum mismatch: expected {expected}, actual {actual}")]
185    ChecksumMismatch { expected: String, actual: String },
186
187    #[error("invalid SHA-256 checksum: {0}")]
188    InvalidHash(String),
189
190    #[error(transparent)]
191    Io(#[from] io::Error),
192
193    #[error("unsupported checksum algorithm {0}")]
194    UnsupportedAlgorithm(String),
195
196    /// Shared no-follow traversal, path-identity or platform-admission failure.
197    #[error(transparent)]
198    Artifact(ArtifactError),
199}