use ic_host_artifacts::artifact::Sha256Digest;
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
use thiserror::Error;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(try_from = "ChecksumFields")]
pub struct ArtifactChecksumRecord {
hash: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ChecksumFields {
algorithm: String,
hash: String,
}
impl TryFrom<ChecksumFields> for ArtifactChecksumRecord {
type Error = ChecksumError;
fn try_from(value: ChecksumFields) -> Result<Self, Self::Error> {
if value.algorithm != "sha256" {
return Err(ChecksumError::UnsupportedAlgorithm(value.algorithm));
}
Self::from_hash(&value.hash)
}
}
impl Serialize for ArtifactChecksumRecord {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut record = serializer.serialize_struct("ArtifactChecksumRecord", 2)?;
record.serialize_field("algorithm", self.algorithm())?;
record.serialize_field("hash", self.hash())?;
record.end()
}
}
impl ArtifactChecksumRecord {
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
Self {
hash: Sha256Digest::compute(bytes).to_string(),
}
}
pub fn from_hash(hash: &str) -> Result<Self, ChecksumError> {
Ok(Self {
hash: canonical_hash(hash)?,
})
}
#[must_use]
pub fn from_digest(digest: [u8; 32]) -> Self {
Self {
hash: Sha256Digest::from_bytes(digest).to_string(),
}
}
#[must_use]
#[expect(
clippy::unnecessary_literal_bound,
reason = "preserve the public accessor signature while removing stored constant data"
)]
pub fn algorithm(&self) -> &str {
"sha256"
}
#[must_use]
pub fn hash(&self) -> &str {
&self.hash
}
pub fn verify(&self, expected_hash: &str) -> Result<(), ChecksumError> {
let expected = canonical_hash(expected_hash)?;
if self.hash == expected {
Ok(())
} else {
Err(ChecksumError::ChecksumMismatch {
expected,
actual: self.hash.clone(),
})
}
}
}
pub(crate) fn canonical_hash(hash: &str) -> Result<String, ChecksumError> {
let normalized = hash.to_ascii_lowercase();
normalized
.parse::<Sha256Digest>()
.map_err(|_| ChecksumError::InvalidHash(hash.to_owned()))?;
Ok(normalized)
}
#[derive(Debug, Error)]
pub enum ChecksumError {
#[error("checksum mismatch: expected {expected}, actual {actual}")]
ChecksumMismatch {
expected: String,
actual: String,
},
#[error("invalid SHA-256 checksum: {0}")]
InvalidHash(String),
#[error("unsupported checksum algorithm {0}")]
UnsupportedAlgorithm(String),
}
#[cfg(test)]
mod tests;