Skip to main content

canic_backup/artifacts/
mod.rs

1use canic_cdk::utils::hash::{hex_bytes, sha256_hex};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::{
5    fs::{self, File},
6    io::{self, Read},
7    path::{Path, PathBuf},
8};
9use thiserror::Error as ThisError;
10
11const SHA256_ALGORITHM: &str = "sha256";
12
13///
14/// ArtifactChecksum
15///
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18pub struct ArtifactChecksum {
19    pub algorithm: String,
20    pub hash: String,
21}
22
23impl ArtifactChecksum {
24    /// Compute a SHA-256 checksum from in-memory bytes.
25    #[must_use]
26    pub fn from_bytes(bytes: &[u8]) -> Self {
27        Self {
28            algorithm: SHA256_ALGORITHM.to_string(),
29            hash: sha256_hex(bytes),
30        }
31    }
32
33    /// Compute a SHA-256 checksum from one filesystem file.
34    pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
35        let mut file = File::open(path)?;
36        let mut hasher = Sha256::new();
37        let mut buffer = vec![0u8; 64 * 1024];
38
39        loop {
40            let read = file.read(&mut buffer)?;
41            if read == 0 {
42                break;
43            }
44            hasher.update(&buffer[..read]);
45        }
46
47        Ok(Self {
48            algorithm: SHA256_ALGORITHM.to_string(),
49            hash: hex_bytes(hasher.finalize()),
50        })
51    }
52
53    /// Compute a SHA-256 checksum from a file or deterministic directory listing.
54    pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
55        if path.is_dir() {
56            Self::from_directory(path)
57        } else {
58            Self::from_file(path)
59        }
60    }
61
62    /// Compute a deterministic SHA-256 checksum over all files in a directory.
63    pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
64        let mut files = Vec::new();
65        collect_files(path, path, &mut files)?;
66        files.sort();
67
68        let mut hasher = Sha256::new();
69        for relative_path in files {
70            let full_path = path.join(&relative_path);
71            let file_checksum = Self::from_file(&full_path)?;
72            hasher.update(relative_path.to_string_lossy().as_bytes());
73            hasher.update([0]);
74            hasher.update(file_checksum.hash.as_bytes());
75            hasher.update([b'\n']);
76        }
77
78        Ok(Self {
79            algorithm: SHA256_ALGORITHM.to_string(),
80            hash: hex_bytes(hasher.finalize()),
81        })
82    }
83
84    /// Verify that the checksum matches an expected SHA-256 hash.
85    pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
86        if self.algorithm != SHA256_ALGORITHM {
87            return Err(ArtifactChecksumError::UnsupportedAlgorithm(
88                self.algorithm.clone(),
89            ));
90        }
91
92        if self.hash == expected_hash {
93            Ok(())
94        } else {
95            Err(ArtifactChecksumError::ChecksumMismatch {
96                expected: expected_hash.to_string(),
97                actual: self.hash.clone(),
98            })
99        }
100    }
101}
102
103///
104/// ArtifactChecksumError
105///
106
107#[derive(Debug, ThisError)]
108pub enum ArtifactChecksumError {
109    #[error(transparent)]
110    Io(#[from] io::Error),
111
112    #[error("unsupported checksum algorithm {0}")]
113    UnsupportedAlgorithm(String),
114
115    #[error("checksum mismatch: expected {expected}, actual {actual}")]
116    ChecksumMismatch { expected: String, actual: String },
117}
118
119// Recursively collect file paths relative to a directory root.
120fn collect_files(
121    root: &Path,
122    path: &Path,
123    files: &mut Vec<PathBuf>,
124) -> Result<(), ArtifactChecksumError> {
125    for entry in fs::read_dir(path)? {
126        let entry = entry?;
127        let path = entry.path();
128        if path.is_dir() {
129            collect_files(root, &path, files)?;
130        } else if path.is_file() {
131            let relative = path
132                .strip_prefix(root)
133                .map_err(io::Error::other)?
134                .to_path_buf();
135            files.push(relative);
136        }
137    }
138    Ok(())
139}
140
141#[cfg(test)]
142mod tests;