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 let mut hasher = Sha256::new();
51 let mut buffer = vec![0u8; 64 * 1024];
52
53 loop {
54 let read = file.read(&mut buffer)?;
55 if read == 0 {
56 break;
57 }
58 hasher.update(&buffer[..read]);
59 }
60
61 Ok(Self {
62 algorithm: SHA256_ALGORITHM.to_string(),
63 hash: hex_bytes(hasher.finalize()),
64 })
65 }
66
67 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
69 if path.is_dir() {
70 Self::from_directory(path)
71 } else {
72 Self::from_file(path)
73 }
74 }
75
76 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
78 let mut files = Vec::new();
79 collect_files(path, path, &mut files)?;
80 files.sort();
81
82 let mut hasher = Sha256::new();
83 for relative_path in files {
84 let full_path = path.join(&relative_path);
85 let file_checksum = Self::from_file(&full_path)?;
86 hasher.update(relative_path.to_string_lossy().as_bytes());
87 hasher.update([0]);
88 hasher.update(file_checksum.hash.as_bytes());
89 hasher.update([b'\n']);
90 }
91
92 Ok(Self {
93 algorithm: SHA256_ALGORITHM.to_string(),
94 hash: hex_bytes(hasher.finalize()),
95 })
96 }
97
98 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
100 if self.algorithm != SHA256_ALGORITHM {
101 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
102 self.algorithm.clone(),
103 ));
104 }
105
106 if self.hash == expected_hash {
107 Ok(())
108 } else {
109 Err(ArtifactChecksumError::ChecksumMismatch {
110 expected: expected_hash.to_string(),
111 actual: self.hash.clone(),
112 })
113 }
114 }
115}
116
117#[derive(Debug, ThisError)]
125pub enum ArtifactChecksumError {
126 #[error(transparent)]
127 Io(#[from] io::Error),
128
129 #[error("unsupported checksum algorithm {0}")]
130 UnsupportedAlgorithm(String),
131
132 #[error("checksum mismatch: expected {expected}, actual {actual}")]
133 ChecksumMismatch { expected: String, actual: String },
134}
135
136fn collect_files(
138 root: &Path,
139 path: &Path,
140 files: &mut Vec<PathBuf>,
141) -> Result<(), ArtifactChecksumError> {
142 for entry in fs::read_dir(path)? {
143 let entry = entry?;
144 let path = entry.path();
145 if path.is_dir() {
146 collect_files(root, &path, files)?;
147 } else if path.is_file() {
148 let relative = path
149 .strip_prefix(root)
150 .map_err(io::Error::other)?
151 .to_path_buf();
152 files.push(relative);
153 }
154 }
155 Ok(())
156}