ic_backup/model/artifacts/
mod.rs1use crate::hash::{hex_bytes, sha256_hex};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
9#[serde(try_from = "ChecksumFields")]
10pub struct ArtifactChecksumRecord {
11 algorithm: String,
12 hash: String,
13}
14
15#[derive(Deserialize)]
16#[serde(deny_unknown_fields)]
17struct ChecksumFields {
18 algorithm: String,
19 hash: String,
20}
21
22impl TryFrom<ChecksumFields> for ArtifactChecksumRecord {
23 type Error = ChecksumError;
24
25 fn try_from(value: ChecksumFields) -> Result<Self, Self::Error> {
26 if value.algorithm != "sha256" {
27 return Err(ChecksumError::UnsupportedAlgorithm(value.algorithm));
28 }
29 Self::from_hash(&value.hash)
30 }
31}
32
33impl ArtifactChecksumRecord {
34 #[must_use]
36 pub fn from_bytes(bytes: &[u8]) -> Self {
37 Self {
38 algorithm: "sha256".to_owned(),
39 hash: sha256_hex(bytes),
40 }
41 }
42
43 pub fn from_hash(hash: &str) -> Result<Self, ChecksumError> {
48 validate_hash(hash)?;
49 Ok(Self {
50 algorithm: "sha256".to_owned(),
51 hash: hash.to_ascii_lowercase(),
52 })
53 }
54
55 pub(crate) fn from_digest(digest: [u8; 32]) -> Self {
56 Self {
57 algorithm: "sha256".to_owned(),
58 hash: hex_bytes(digest),
59 }
60 }
61
62 #[must_use]
64 pub fn algorithm(&self) -> &str {
65 &self.algorithm
66 }
67
68 #[must_use]
70 pub fn hash(&self) -> &str {
71 &self.hash
72 }
73
74 pub fn verify(&self, expected_hash: &str) -> Result<(), ChecksumError> {
79 validate_hash(expected_hash)?;
80 if self.hash.eq_ignore_ascii_case(expected_hash) {
81 Ok(())
82 } else {
83 Err(ChecksumError::ChecksumMismatch {
84 expected: expected_hash.to_ascii_lowercase(),
85 actual: self.hash.clone(),
86 })
87 }
88 }
89}
90
91fn validate_hash(hash: &str) -> Result<(), ChecksumError> {
92 if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
93 return Err(ChecksumError::InvalidHash(hash.to_owned()));
94 }
95 Ok(())
96}
97
98#[derive(Debug, Error)]
100pub enum ChecksumError {
101 #[error("checksum mismatch: expected {expected}, actual {actual}")]
103 ChecksumMismatch {
104 expected: String,
106 actual: String,
108 },
109 #[error("invalid SHA-256 checksum: {0}")]
111 InvalidHash(String),
112 #[error("unsupported checksum algorithm {0}")]
114 UnsupportedAlgorithm(String),
115}
116
117#[cfg(test)]
118mod tests;