canic_backup/artifacts/
mod.rs1use 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18pub struct ArtifactChecksum {
19 pub algorithm: String,
20 pub hash: String,
21}
22
23impl ArtifactChecksum {
24 #[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 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 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 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 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#[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
119fn 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;