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
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32pub struct ArtifactChecksum {
33 pub algorithm: String,
34 pub hash: String,
35}
36
37impl ArtifactChecksum {
38 #[must_use]
40 pub fn from_bytes(bytes: &[u8]) -> Self {
41 Self {
42 algorithm: SHA256_ALGORITHM.to_string(),
43 hash: sha256_hex(bytes),
44 }
45 }
46
47 pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
49 let mut file = File::open(path)?;
50 Self::from_reader(&mut file)
51 }
52
53 pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
55 let mut hasher = Sha256::new();
56 let mut buffer = vec![0u8; 64 * 1024];
57
58 loop {
59 let read = reader.read(&mut buffer)?;
60 if read == 0 {
61 break;
62 }
63 hasher.update(&buffer[..read]);
64 }
65
66 Ok(Self {
67 algorithm: SHA256_ALGORITHM.to_string(),
68 hash: hex_bytes(hasher.finalize()),
69 })
70 }
71
72 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
74 if path.is_dir() {
75 Self::from_directory(path)
76 } else {
77 Self::from_file(path)
78 }
79 }
80
81 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
83 let mut files = Vec::new();
84 collect_files(path, path, &mut files)?;
85 files.sort();
86
87 let checksums = files
88 .into_iter()
89 .map(|relative_path| {
90 let checksum = Self::from_file(&path.join(&relative_path))?;
91 Ok((relative_path, checksum))
92 })
93 .collect::<Result<Vec<_>, ArtifactChecksumError>>()?;
94 Ok(Self::from_relative_file_checksums(checksums))
95 }
96
97 pub(crate) fn from_relative_file_checksums(mut files: Vec<(PathBuf, Self)>) -> Self {
99 files.sort_by(|left, right| left.0.cmp(&right.0));
100 let mut hasher = Sha256::new();
101 for (relative_path, file_checksum) in files {
102 hasher.update(relative_path.to_string_lossy().as_bytes());
103 hasher.update([0]);
104 hasher.update(file_checksum.hash.as_bytes());
105 hasher.update(*b"\n");
106 }
107
108 Self {
109 algorithm: SHA256_ALGORITHM.to_string(),
110 hash: hex_bytes(hasher.finalize()),
111 }
112 }
113
114 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
116 if self.algorithm != SHA256_ALGORITHM {
117 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
118 self.algorithm.clone(),
119 ));
120 }
121
122 if self.hash == expected_hash {
123 Ok(())
124 } else {
125 Err(ArtifactChecksumError::ChecksumMismatch {
126 expected: expected_hash.to_string(),
127 actual: self.hash.clone(),
128 })
129 }
130 }
131}
132
133#[derive(Debug, ThisError)]
141pub enum ArtifactChecksumError {
142 #[error(transparent)]
143 Io(#[from] io::Error),
144
145 #[error("unsupported checksum algorithm {0}")]
146 UnsupportedAlgorithm(String),
147
148 #[error("checksum mismatch: expected {expected}, actual {actual}")]
149 ChecksumMismatch { expected: String, actual: String },
150}
151
152fn collect_files(
154 root: &Path,
155 path: &Path,
156 files: &mut Vec<PathBuf>,
157) -> Result<(), ArtifactChecksumError> {
158 for entry in fs::read_dir(path)? {
159 let entry = entry?;
160 let path = entry.path();
161 if path.is_dir() {
162 collect_files(root, &path, files)?;
163 } else if path.is_file() {
164 let relative = path
165 .strip_prefix(root)
166 .map_err(io::Error::other)?
167 .to_path_buf();
168 files.push(relative);
169 }
170 }
171 Ok(())
172}