canic_backup/artifacts/
mod.rs1#[cfg(test)]
8mod tests;
9
10use std::{
11 io::{self, Read},
12 path::{Path, PathBuf},
13};
14
15use ic_backup::{
16 model::artifacts::ArtifactChecksumRecord,
17 ops::artifacts::{self, ArtifactError},
18};
19use serde::{Deserialize, Serialize};
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::from_record(ArtifactChecksumRecord::from_bytes(bytes))
52 }
53
54 pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
56 artifacts::checksum_file(path)
57 .map(Self::from_record)
58 .map_err(artifact_error)
59 }
60
61 pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
63 artifacts::checksum_reader(reader)
64 .map(Self::from_record)
65 .map_err(artifact_error)
66 }
67
68 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
70 artifacts::checksum_path(path)
71 .map(Self::from_record)
72 .map_err(artifact_error)
73 }
74
75 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
77 artifacts::checksum_directory(path)
78 .map(Self::from_record)
79 .map_err(artifact_error)
80 }
81
82 pub(crate) fn from_relative_file_checksums(
84 files: Vec<(PathBuf, Self)>,
85 ) -> Result<Self, ArtifactChecksumError> {
86 let files = files
87 .into_iter()
88 .map(|(path, checksum)| {
89 checksum.validate()?;
90 ArtifactChecksumRecord::from_hash(&checksum.hash)
91 .map(|record| (path, record))
92 .map_err(|_| ArtifactChecksumError::InvalidHash(checksum.hash))
93 })
94 .collect::<Result<_, _>>()?;
95 artifacts::checksum_relative_files(files)
96 .map(Self::from_record)
97 .map_err(|error| artifact_error(error.into()))
98 }
99
100 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
102 self.validate()?;
103 Self::validate_hash(expected_hash)?;
104
105 if self.hash.eq_ignore_ascii_case(expected_hash) {
106 Ok(())
107 } else {
108 Err(ArtifactChecksumError::ChecksumMismatch {
109 expected: expected_hash.to_string(),
110 actual: self.hash.clone(),
111 })
112 }
113 }
114
115 pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
117 Self::validate_algorithm(&self.algorithm)?;
118 Self::validate_hash(&self.hash)
119 }
120
121 pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
123 if algorithm != SHA256_ALGORITHM {
124 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
125 algorithm.to_string(),
126 ));
127 }
128
129 Ok(())
130 }
131
132 pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
134 if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
135 return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
136 }
137
138 Ok(())
139 }
140
141 pub(crate) fn from_relative_path_no_follow(
142 root: &Path,
143 relative: &Path,
144 ) -> Result<Self, ArtifactChecksumError> {
145 artifacts::checksum_relative_path(root, relative)
146 .map(Self::from_record)
147 .map_err(artifact_error)
148 }
149
150 pub(crate) fn stage_relative_path_no_follow(
151 root: &Path,
152 relative: &Path,
153 destination: &Path,
154 ) -> Result<Self, ArtifactChecksumError> {
155 artifacts::stage_relative_path(root, relative, destination)
156 .map(Self::from_record)
157 .map_err(artifact_error)
158 }
159
160 fn from_record(record: ArtifactChecksumRecord) -> Self {
161 Self {
162 algorithm: record.algorithm().to_owned(),
163 hash: record.hash().to_owned(),
164 }
165 }
166}
167
168fn artifact_error(error: ArtifactError) -> ArtifactChecksumError {
169 match error {
170 ArtifactError::Io(error) => ArtifactChecksumError::Io(error),
171 error => ArtifactChecksumError::Artifact(error),
172 }
173}
174
175#[derive(Debug, ThisError)]
183pub enum ArtifactChecksumError {
184 #[error("checksum mismatch: expected {expected}, actual {actual}")]
185 ChecksumMismatch { expected: String, actual: String },
186
187 #[error("invalid SHA-256 checksum: {0}")]
188 InvalidHash(String),
189
190 #[error(transparent)]
191 Io(#[from] io::Error),
192
193 #[error("unsupported checksum algorithm {0}")]
194 UnsupportedAlgorithm(String),
195
196 #[error(transparent)]
198 Artifact(ArtifactError),
199}