ic_backup/model/artifacts/
mod.rs1use ic_host_artifacts::artifact::Sha256Digest;
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: Sha256Digest::compute(bytes).to_string(),
40 }
41 }
42
43 pub fn from_hash(hash: &str) -> Result<Self, ChecksumError> {
48 Ok(Self {
49 algorithm: "sha256".to_owned(),
50 hash: canonical_hash(hash)?,
51 })
52 }
53
54 pub(crate) fn from_digest(digest: [u8; 32]) -> Self {
55 Self {
56 algorithm: "sha256".to_owned(),
57 hash: Sha256Digest::from_bytes(digest).to_string(),
58 }
59 }
60
61 #[must_use]
63 pub fn algorithm(&self) -> &str {
64 &self.algorithm
65 }
66
67 #[must_use]
69 pub fn hash(&self) -> &str {
70 &self.hash
71 }
72
73 pub fn verify(&self, expected_hash: &str) -> Result<(), ChecksumError> {
78 let expected = canonical_hash(expected_hash)?;
79 if self.hash == expected {
80 Ok(())
81 } else {
82 Err(ChecksumError::ChecksumMismatch {
83 expected,
84 actual: self.hash.clone(),
85 })
86 }
87 }
88}
89
90pub(crate) fn canonical_hash(hash: &str) -> Result<String, ChecksumError> {
91 let normalized = hash.to_ascii_lowercase();
92 normalized
93 .parse::<Sha256Digest>()
94 .map_err(|_| ChecksumError::InvalidHash(hash.to_owned()))?;
95 Ok(normalized)
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;