canic_backup/artifacts/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::hash::{hex_bytes, sha256_hex};
11
12use std::{
13 fs::{self, File},
14 io::{self, Read},
15 path::{Path, PathBuf},
16};
17
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct ArtifactChecksum {
43 pub algorithm: String,
44 pub hash: String,
45}
46
47impl ArtifactChecksum {
48 #[must_use]
50 pub fn from_bytes(bytes: &[u8]) -> Self {
51 Self {
52 algorithm: SHA256_ALGORITHM.to_string(),
53 hash: sha256_hex(bytes),
54 }
55 }
56
57 pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
59 let mut file = File::open(path)?;
60 Self::from_reader(&mut file)
61 }
62
63 pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
65 let mut hasher = Sha256::new();
66 let mut buffer = vec![0u8; 64 * 1024];
67
68 loop {
69 let read = reader.read(&mut buffer)?;
70 if read == 0 {
71 break;
72 }
73 hasher.update(&buffer[..read]);
74 }
75
76 Ok(Self {
77 algorithm: SHA256_ALGORITHM.to_string(),
78 hash: hex_bytes(hasher.finalize()),
79 })
80 }
81
82 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
84 if path.is_dir() {
85 Self::from_directory(path)
86 } else {
87 Self::from_file(path)
88 }
89 }
90
91 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
93 let mut files = Vec::new();
94 collect_files(path, path, &mut files)?;
95 files.sort();
96
97 let checksums = files
98 .into_iter()
99 .map(|relative_path| {
100 let checksum = Self::from_file(&path.join(&relative_path))?;
101 Ok((relative_path, checksum))
102 })
103 .collect::<Result<Vec<_>, ArtifactChecksumError>>()?;
104 Ok(Self::from_relative_file_checksums(checksums))
105 }
106
107 pub(crate) fn from_relative_file_checksums(mut files: Vec<(PathBuf, Self)>) -> Self {
109 files.sort_by(|left, right| left.0.cmp(&right.0));
110 let mut hasher = Sha256::new();
111 for (relative_path, file_checksum) in files {
112 hasher.update(relative_path.to_string_lossy().as_bytes());
113 hasher.update([0]);
114 hasher.update(file_checksum.hash.as_bytes());
115 hasher.update(*b"\n");
116 }
117
118 Self {
119 algorithm: SHA256_ALGORITHM.to_string(),
120 hash: hex_bytes(hasher.finalize()),
121 }
122 }
123
124 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
126 if self.algorithm != SHA256_ALGORITHM {
127 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
128 self.algorithm.clone(),
129 ));
130 }
131
132 if self.hash == expected_hash {
133 Ok(())
134 } else {
135 Err(ArtifactChecksumError::ChecksumMismatch {
136 expected: expected_hash.to_string(),
137 actual: self.hash.clone(),
138 })
139 }
140 }
141}
142
143#[derive(Debug, ThisError)]
151pub enum ArtifactChecksumError {
152 #[error(transparent)]
153 Io(#[from] io::Error),
154
155 #[error("unsupported checksum algorithm {0}")]
156 UnsupportedAlgorithm(String),
157
158 #[error("checksum mismatch: expected {expected}, actual {actual}")]
159 ChecksumMismatch { expected: String, actual: String },
160}
161
162fn collect_files(
164 root: &Path,
165 path: &Path,
166 files: &mut Vec<PathBuf>,
167) -> Result<(), ArtifactChecksumError> {
168 for entry in fs::read_dir(path)? {
169 let entry = entry?;
170 let path = entry.path();
171 if path.is_dir() {
172 collect_files(root, &path, files)?;
173 } else if path.is_file() {
174 let relative = path
175 .strip_prefix(root)
176 .map_err(io::Error::other)?
177 .to_path_buf();
178 files.push(relative);
179 }
180 }
181 Ok(())
182}