#[cfg(test)]
mod tests;
use crate::hash::hex_bytes;
use std::{
io::{self, Read},
path::{Path, PathBuf},
};
use ic_backup::{
model::artifacts::ArtifactChecksumRecord,
ops::artifacts::{self, ArtifactError},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error as ThisError;
const SHA256_ALGORITHM: &str = "sha256";
pub(crate) fn artifact_path_segment(value: &str) -> String {
value
.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
_ => '_',
})
.collect()
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ArtifactChecksum {
pub algorithm: String,
pub hash: String,
}
impl ArtifactChecksum {
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
Self::from_record(ArtifactChecksumRecord::from_bytes(bytes))
}
pub fn from_file(path: &Path) -> Result<Self, ArtifactChecksumError> {
artifacts::checksum_file(path)
.map(Self::from_record)
.map_err(artifact_error)
}
pub(crate) fn from_reader(reader: &mut impl Read) -> Result<Self, ArtifactChecksumError> {
artifacts::checksum_reader(reader)
.map(Self::from_record)
.map_err(artifact_error)
}
pub fn from_path(path: &Path) -> Result<Self, ArtifactChecksumError> {
artifacts::checksum_path(path)
.map(Self::from_record)
.map_err(artifact_error)
}
pub fn from_directory(path: &Path) -> Result<Self, ArtifactChecksumError> {
artifacts::checksum_directory(path)
.map(Self::from_record)
.map_err(artifact_error)
}
pub(crate) fn from_relative_file_checksums(mut files: Vec<(PathBuf, Self)>) -> Self {
files.sort_by(|left, right| left.0.cmp(&right.0));
let mut hasher = Sha256::new();
for (relative_path, file_checksum) in files {
let relative_path = relative_path
.to_str()
.expect("artifact publication admitted UTF-8 relative paths");
hasher.update(relative_path.as_bytes());
hasher.update([0]);
hasher.update(file_checksum.hash.as_bytes());
hasher.update(*b"\n");
}
Self {
algorithm: SHA256_ALGORITHM.to_string(),
hash: hex_bytes(hasher.finalize()),
}
}
pub fn verify(&self, expected_hash: &str) -> Result<(), ArtifactChecksumError> {
self.validate()?;
Self::validate_hash(expected_hash)?;
if self.hash.eq_ignore_ascii_case(expected_hash) {
Ok(())
} else {
Err(ArtifactChecksumError::ChecksumMismatch {
expected: expected_hash.to_string(),
actual: self.hash.clone(),
})
}
}
pub fn validate(&self) -> Result<(), ArtifactChecksumError> {
Self::validate_algorithm(&self.algorithm)?;
Self::validate_hash(&self.hash)
}
pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), ArtifactChecksumError> {
if algorithm != SHA256_ALGORITHM {
return Err(ArtifactChecksumError::UnsupportedAlgorithm(
algorithm.to_string(),
));
}
Ok(())
}
pub(crate) fn validate_hash(hash: &str) -> Result<(), ArtifactChecksumError> {
if hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(ArtifactChecksumError::InvalidHash(hash.to_string()));
}
Ok(())
}
pub(crate) fn from_relative_path_no_follow(
root: &Path,
relative: &Path,
) -> Result<Self, ArtifactChecksumError> {
artifacts::checksum_relative_path(root, relative)
.map(Self::from_record)
.map_err(artifact_error)
}
pub(crate) fn stage_relative_path_no_follow(
root: &Path,
relative: &Path,
destination: &Path,
) -> Result<Self, ArtifactChecksumError> {
artifacts::stage_relative_path(root, relative, destination)
.map(Self::from_record)
.map_err(artifact_error)
}
fn from_record(record: ArtifactChecksumRecord) -> Self {
Self {
algorithm: record.algorithm().to_owned(),
hash: record.hash().to_owned(),
}
}
}
fn artifact_error(error: ArtifactError) -> ArtifactChecksumError {
match error {
ArtifactError::Io(error) => ArtifactChecksumError::Io(error),
error => ArtifactChecksumError::Artifact(error),
}
}
#[derive(Debug, ThisError)]
pub enum ArtifactChecksumError {
#[error("checksum mismatch: expected {expected}, actual {actual}")]
ChecksumMismatch { expected: String, actual: String },
#[error("invalid SHA-256 checksum: {0}")]
InvalidHash(String),
#[error(transparent)]
Io(#[from] io::Error),
#[error("unsupported checksum algorithm {0}")]
UnsupportedAlgorithm(String),
#[error(transparent)]
Artifact(ArtifactError),
}