ic_backup/model/artifacts/
mod.rs1use ic_host_artifacts::artifact::Sha256Digest;
4use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
5use thiserror::Error;
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
9#[serde(try_from = "ChecksumFields")]
10pub struct ArtifactChecksumRecord {
11 hash: String,
12}
13
14#[derive(Deserialize)]
15#[serde(deny_unknown_fields)]
16struct ChecksumFields {
17 algorithm: String,
18 hash: String,
19}
20
21impl TryFrom<ChecksumFields> for ArtifactChecksumRecord {
22 type Error = ChecksumError;
23
24 fn try_from(value: ChecksumFields) -> Result<Self, Self::Error> {
25 if value.algorithm != "sha256" {
26 return Err(ChecksumError::UnsupportedAlgorithm(value.algorithm));
27 }
28 Self::from_hash(&value.hash)
29 }
30}
31
32impl Serialize for ArtifactChecksumRecord {
33 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
34 let mut record = serializer.serialize_struct("ArtifactChecksumRecord", 2)?;
35 record.serialize_field("algorithm", self.algorithm())?;
36 record.serialize_field("hash", self.hash())?;
37 record.end()
38 }
39}
40
41impl ArtifactChecksumRecord {
42 #[must_use]
44 pub fn from_bytes(bytes: &[u8]) -> Self {
45 Self {
46 hash: Sha256Digest::compute(bytes).to_string(),
47 }
48 }
49
50 pub fn from_hash(hash: &str) -> Result<Self, ChecksumError> {
55 Ok(Self {
56 hash: canonical_hash(hash)?,
57 })
58 }
59
60 #[must_use]
64 pub fn from_digest(digest: [u8; 32]) -> Self {
65 Self {
66 hash: Sha256Digest::from_bytes(digest).to_string(),
67 }
68 }
69
70 pub(crate) fn into_hash(self) -> String {
72 self.hash
73 }
74
75 pub(crate) fn sha256(&self) -> Sha256Digest {
76 self.hash
77 .parse()
78 .expect("checksum records contain validated SHA-256 text")
79 }
80
81 #[must_use]
83 #[expect(
84 clippy::unnecessary_literal_bound,
85 reason = "preserve the public accessor signature while removing stored constant data"
86 )]
87 pub fn algorithm(&self) -> &str {
88 "sha256"
89 }
90
91 #[must_use]
93 pub fn hash(&self) -> &str {
94 &self.hash
95 }
96
97 pub fn verify(&self, expected_hash: &str) -> Result<(), ChecksumError> {
102 let expected = canonical_hash(expected_hash)?;
103 if self.hash == expected {
104 Ok(())
105 } else {
106 Err(ChecksumError::ChecksumMismatch {
107 expected,
108 actual: self.hash.clone(),
109 })
110 }
111 }
112}
113
114pub(crate) fn canonical_hash(hash: &str) -> Result<String, ChecksumError> {
115 let normalized = hash.to_ascii_lowercase();
116 normalized
117 .parse::<Sha256Digest>()
118 .map_err(|_| ChecksumError::InvalidHash(hash.to_owned()))?;
119 Ok(normalized)
120}
121
122#[derive(Debug, Error)]
124pub enum ChecksumError {
125 #[error("checksum mismatch: expected {expected}, actual {actual}")]
127 ChecksumMismatch {
128 expected: String,
130 actual: String,
132 },
133 #[error("invalid SHA-256 checksum: {0}")]
135 InvalidHash(String),
136 #[error("unsupported checksum algorithm {0}")]
138 UnsupportedAlgorithm(String),
139}
140
141#[cfg(test)]
142mod tests;