canic_backup/artifacts/
mod.rs1#[cfg(test)]
8mod tests;
9
10use std::{io, path::Path};
11#[cfg(test)]
12use std::{io::Read, path::PathBuf};
13
14use ic_backup::{
15 model::artifacts::ArtifactChecksumRecord,
16 ops::artifacts::{self, ArtifactError},
17};
18use serde::{Deserialize, Serialize};
19use thiserror::Error as ThisError;
20
21const SHA256_ALGORITHM: &str = "sha256";
22
23pub(crate) fn artifact_path_segment(value: &str) -> String {
24 value
25 .chars()
26 .map(|ch| match ch {
27 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
28 _ => '_',
29 })
30 .collect()
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41pub struct ArtifactChecksum {
42 pub algorithm: String,
43 pub hash: String,
44}
45
46impl ArtifactChecksum {
47 #[must_use]
49 pub fn from_bytes(bytes: &[u8]) -> Self {
50 Self::from_record(ArtifactChecksumRecord::from_bytes(bytes))
51 }
52
53 pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
55 artifacts::checksum_file(path)
56 .map(Self::from_record)
57 .map_err(artifact_error)
58 }
59
60 #[cfg(test)]
62 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 #[cfg(test)]
84 pub(crate) fn from_relative_file_checksums(
85 files: Vec<(PathBuf, Self)>,
86 ) -> Result<Self, ArtifactChecksumError> {
87 let files = files
88 .into_iter()
89 .map(|(path, checksum)| {
90 checksum.validate()?;
91 ArtifactChecksumRecord::from_hash(&checksum.hash)
92 .map(|record| (path, record))
93 .map_err(|_| ArtifactChecksumError::InvalidHash(checksum.hash))
94 })
95 .collect::<Result<_, _>>()?;
96 artifacts::checksum_relative_files(files)
97 .map(Self::from_record)
98 .map_err(|error| artifact_error(error.into()))
99 }
100
101 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
103 self.validate()?;
104 Self::validate_hash(expected_hash)?;
105
106 if self.hash.eq_ignore_ascii_case(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 pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
118 Self::validate_algorithm(&self.algorithm)?;
119 Self::validate_hash(&self.hash)
120 }
121
122 pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
124 if algorithm != SHA256_ALGORITHM {
125 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
126 algorithm.to_string(),
127 ));
128 }
129
130 Ok(())
131 }
132
133 pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
135 if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
136 return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
137 }
138
139 Ok(())
140 }
141
142 pub(crate) fn from_relative_path_no_follow(
143 root: &Path,
144 relative: &Path,
145 ) -> Result<Self, ArtifactChecksumError> {
146 artifacts::checksum_relative_path(root, relative)
147 .map(Self::from_record)
148 .map_err(artifact_error)
149 }
150
151 pub(crate) fn stage_relative_path_no_follow(
152 root: &Path,
153 relative: &Path,
154 destination: &Path,
155 ) -> Result<Self, ArtifactChecksumError> {
156 artifacts::stage_relative_path(root, relative, destination)
157 .map(Self::from_record)
158 .map_err(artifact_error)
159 }
160
161 fn from_record(record: ArtifactChecksumRecord) -> Self {
162 Self {
163 algorithm: record.algorithm().to_owned(),
164 hash: record.hash().to_owned(),
165 }
166 }
167}
168
169fn artifact_error(error: ArtifactError) -> ArtifactChecksumError {
170 match error {
171 ArtifactError::Io(error) => ArtifactChecksumError::Io(error),
172 error => ArtifactChecksumError::Artifact(error),
173 }
174}
175
176#[derive(Debug, ThisError)]
184pub enum ArtifactChecksumError {
185 #[error("checksum mismatch: expected {expected}, actual {actual}")]
186 ChecksumMismatch { expected: String, actual: String },
187
188 #[error("invalid SHA-256 checksum: {0}")]
189 InvalidHash(String),
190
191 #[error(transparent)]
192 Io(#[from] io::Error),
193
194 #[error("unsupported checksum algorithm {0}")]
195 UnsupportedAlgorithm(String),
196
197 #[error(transparent)]
199 Artifact(ArtifactError),
200}