canic_backup/artifacts/
mod.rs1mod secure;
8#[cfg(test)]
9mod tests;
10
11use crate::hash::{hex_bytes, sha256_hex};
12
13use std::{
14 io::{self, Read, Write},
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 secure::checksum_path(path, secure::ExpectedArtifactType::File)
60 }
61
62 pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
64 let mut hasher = Sha256::new();
65 let mut buffer = vec![0u8; 64 * 1024];
66
67 loop {
68 let read = reader.read(&mut buffer)?;
69 if read == 0 {
70 break;
71 }
72 hasher.update(&buffer[..read]);
73 }
74
75 Ok(Self {
76 algorithm: SHA256_ALGORITHM.to_string(),
77 hash: hex_bytes(hasher.finalize()),
78 })
79 }
80
81 pub(crate) fn copy_from_reader(
82 reader: &mut impl Read,
83 writer: &mut impl Write,
84 ) -> Result<Self, ArtifactChecksumError> {
85 let mut hasher = Sha256::new();
86 let mut buffer = vec![0u8; 64 * 1024];
87
88 loop {
89 let read = reader.read(&mut buffer)?;
90 if read == 0 {
91 break;
92 }
93 writer.write_all(&buffer[..read])?;
94 hasher.update(&buffer[..read]);
95 }
96
97 Ok(Self {
98 algorithm: SHA256_ALGORITHM.to_string(),
99 hash: hex_bytes(hasher.finalize()),
100 })
101 }
102
103 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
105 secure::checksum_path(path, secure::ExpectedArtifactType::Any)
106 }
107
108 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
110 secure::checksum_path(path, secure::ExpectedArtifactType::Directory)
111 }
112
113 pub(crate) fn from_relative_file_checksums(mut files: Vec<(PathBuf, Self)>) -> Self {
115 files.sort_by(|left, right| left.0.cmp(&right.0));
116 let mut hasher = Sha256::new();
117 for (relative_path, file_checksum) in files {
118 hasher.update(relative_path.to_string_lossy().as_bytes());
119 hasher.update([0]);
120 hasher.update(file_checksum.hash.as_bytes());
121 hasher.update(*b"\n");
122 }
123
124 Self {
125 algorithm: SHA256_ALGORITHM.to_string(),
126 hash: hex_bytes(hasher.finalize()),
127 }
128 }
129
130 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
132 self.validate()?;
133 Self::validate_hash(expected_hash)?;
134
135 if self.hash.eq_ignore_ascii_case(expected_hash) {
136 Ok(())
137 } else {
138 Err(ArtifactChecksumError::ChecksumMismatch {
139 expected: expected_hash.to_string(),
140 actual: self.hash.clone(),
141 })
142 }
143 }
144
145 pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
147 Self::validate_algorithm(&self.algorithm)?;
148 Self::validate_hash(&self.hash)
149 }
150
151 pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
153 if algorithm != SHA256_ALGORITHM {
154 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
155 algorithm.to_string(),
156 ));
157 }
158
159 Ok(())
160 }
161
162 pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
164 if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
165 return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
166 }
167
168 Ok(())
169 }
170
171 pub(crate) fn from_relative_path_no_follow(
172 root: &Path,
173 relative: &Path,
174 ) -> Result<Self, ArtifactChecksumError> {
175 secure::checksum_relative_path(root, relative)
176 }
177
178 pub(crate) fn stage_relative_path_no_follow(
179 root: &Path,
180 relative: &Path,
181 destination: &Path,
182 ) -> Result<Self, ArtifactChecksumError> {
183 secure::stage_relative_path(root, relative, destination)
184 }
185}
186
187#[derive(Debug, ThisError)]
195pub enum ArtifactChecksumError {
196 #[error("checksum mismatch: expected {expected}, actual {actual}")]
197 ChecksumMismatch { expected: String, actual: String },
198
199 #[error("invalid SHA-256 checksum: {0}")]
200 InvalidHash(String),
201
202 #[error(transparent)]
203 Io(#[from] io::Error),
204
205 #[error("unsupported checksum algorithm {0}")]
206 UnsupportedAlgorithm(String),
207
208 #[error("unsupported artifact filesystem entry at {path}: {kind}")]
209 UnsupportedEntry { path: String, kind: String },
210
211 #[error("secure artifact traversal is unsupported on platform {0}")]
212 UnsupportedPlatform(&'static str),
213}