canic_backup/artifacts/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::hash::hex_bytes;
11
12use std::{
13 io::{self, Read},
14 path::{Path, PathBuf},
15};
16
17use ic_backup::{
18 model::artifacts::ArtifactChecksumRecord,
19 ops::artifacts::{self, ArtifactError},
20};
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23use thiserror::Error as ThisError;
24
25const SHA256_ALGORITHM: &str = "sha256";
26
27pub(crate) fn artifact_path_segment(value: &str) -> String {
28 value
29 .chars()
30 .map(|ch| match ch {
31 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
32 _ => '_',
33 })
34 .collect()
35}
36
37#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
45pub struct ArtifactChecksum {
46 pub algorithm: String,
47 pub hash: String,
48}
49
50impl ArtifactChecksum {
51 #[must_use]
53 pub fn from_bytes(bytes: &[u8]) -> Self {
54 Self::from_record(ArtifactChecksumRecord::from_bytes(bytes))
55 }
56
57 pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
59 artifacts::checksum_file(path)
60 .map(Self::from_record)
61 .map_err(artifact_error)
62 }
63
64 pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
66 artifacts::checksum_reader(reader)
67 .map(Self::from_record)
68 .map_err(artifact_error)
69 }
70
71 pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
73 artifacts::checksum_path(path)
74 .map(Self::from_record)
75 .map_err(artifact_error)
76 }
77
78 pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
80 artifacts::checksum_directory(path)
81 .map(Self::from_record)
82 .map_err(artifact_error)
83 }
84
85 pub(crate) fn from_relative_file_checksums(mut files: Vec<(PathBuf, Self)>) -> Self {
87 files.sort_by(|left, right| left.0.cmp(&right.0));
88 let mut hasher = Sha256::new();
89 for (relative_path, file_checksum) in files {
90 let relative_path = relative_path
91 .to_str()
92 .expect("artifact publication admitted UTF-8 relative paths");
93 hasher.update(relative_path.as_bytes());
94 hasher.update([0]);
95 hasher.update(file_checksum.hash.as_bytes());
96 hasher.update(*b"\n");
97 }
98
99 Self {
100 algorithm: SHA256_ALGORITHM.to_string(),
101 hash: hex_bytes(hasher.finalize()),
102 }
103 }
104
105 pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
107 self.validate()?;
108 Self::validate_hash(expected_hash)?;
109
110 if self.hash.eq_ignore_ascii_case(expected_hash) {
111 Ok(())
112 } else {
113 Err(ArtifactChecksumError::ChecksumMismatch {
114 expected: expected_hash.to_string(),
115 actual: self.hash.clone(),
116 })
117 }
118 }
119
120 pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
122 Self::validate_algorithm(&self.algorithm)?;
123 Self::validate_hash(&self.hash)
124 }
125
126 pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
128 if algorithm != SHA256_ALGORITHM {
129 return Err(ArtifactChecksumError::UnsupportedAlgorithm(
130 algorithm.to_string(),
131 ));
132 }
133
134 Ok(())
135 }
136
137 pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
139 if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
140 return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
141 }
142
143 Ok(())
144 }
145
146 pub(crate) fn from_relative_path_no_follow(
147 root: &Path,
148 relative: &Path,
149 ) -> Result<Self, ArtifactChecksumError> {
150 artifacts::checksum_relative_path(root, relative)
151 .map(Self::from_record)
152 .map_err(artifact_error)
153 }
154
155 pub(crate) fn stage_relative_path_no_follow(
156 root: &Path,
157 relative: &Path,
158 destination: &Path,
159 ) -> Result<Self, ArtifactChecksumError> {
160 artifacts::stage_relative_path(root, relative, destination)
161 .map(Self::from_record)
162 .map_err(artifact_error)
163 }
164
165 fn from_record(record: ArtifactChecksumRecord) -> Self {
166 Self {
167 algorithm: record.algorithm().to_owned(),
168 hash: record.hash().to_owned(),
169 }
170 }
171}
172
173fn artifact_error(error: ArtifactError) -> ArtifactChecksumError {
174 match error {
175 ArtifactError::Io(error) => ArtifactChecksumError::Io(error),
176 error => ArtifactChecksumError::Artifact(error),
177 }
178}
179
180#[derive(Debug, ThisError)]
188pub enum ArtifactChecksumError {
189 #[error("checksum mismatch: expected {expected}, actual {actual}")]
190 ChecksumMismatch { expected: String, actual: String },
191
192 #[error("invalid SHA-256 checksum: {0}")]
193 InvalidHash(String),
194
195 #[error(transparent)]
196 Io(#[from] io::Error),
197
198 #[error("unsupported checksum algorithm {0}")]
199 UnsupportedAlgorithm(String),
200
201 #[error(transparent)]
203 Artifact(ArtifactError),
204}