#[cfg(test)]
mod regressions;
mod secure;
#[cfg(test)]
mod tests;
use crate::model::artifacts::ArtifactChecksumRecord;
use sha2::{Digest, Sha256};
#[cfg(unix)]
use std::io::Write;
use std::{
io::{self, Read},
path::{Component, Path, PathBuf},
};
use thiserror::Error;
pub fn checksum_file(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
secure::checksum_path(path, secure::ExpectedArtifactType::File)
}
pub fn checksum_path(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
secure::checksum_path(path, secure::ExpectedArtifactType::Any)
}
pub fn checksum_directory(path: &Path) -> Result<ArtifactChecksumRecord, ArtifactError> {
secure::checksum_path(path, secure::ExpectedArtifactType::Directory)
}
pub fn checksum_reader(reader: &mut impl Read) -> Result<ArtifactChecksumRecord, ArtifactError> {
let identity =
ic_host_artifacts::artifact::hash_reader(reader, u64::MAX).map_err(io::Error::from)?;
Ok(ArtifactChecksumRecord::from_digest(
*identity.sha256.as_bytes(),
))
}
#[cfg(unix)]
pub(crate) fn copy_from_reader(
reader: &mut impl Read,
writer: &mut impl Write,
) -> Result<ArtifactChecksumRecord, ArtifactError> {
use ic_host_artifacts::artifact::CopyError;
let identity =
ic_host_artifacts::artifact::copy_reader(reader, writer, u64::MAX).map_err(|error| {
match error {
CopyError::Input(error) => ArtifactError::Io(error.into()),
CopyError::Output(error) => ArtifactError::Io(error),
}
})?;
Ok(ArtifactChecksumRecord::from_digest(
*identity.sha256.as_bytes(),
))
}
pub fn checksum_relative_files(
mut files: Vec<(PathBuf, ArtifactChecksumRecord)>,
) -> Result<ArtifactChecksumRecord, DirectoryChecksumError> {
files.sort_by(|left, right| left.0.cmp(&right.0));
let mut hasher = Sha256::new();
let mut previous = None;
for (relative, checksum) in &files {
let name = relative
.to_str()
.ok_or_else(|| DirectoryChecksumError::NonUtf8Path {
path: relative.clone(),
})?;
if name.contains('\0')
|| name.split('/').any(|part| matches!(part, "" | "." | ".."))
|| !relative
.components()
.all(|part| matches!(part, Component::Normal(_)))
{
return Err(DirectoryChecksumError::InvalidRelativePath {
path: relative.clone(),
});
}
if previous == Some(relative) {
return Err(DirectoryChecksumError::DuplicatePath {
path: relative.clone(),
});
}
previous = Some(relative);
hasher.update(name.as_bytes());
hasher.update([0]);
hasher.update(checksum.hash().as_bytes());
hasher.update(*b"\n");
}
Ok(ArtifactChecksumRecord::from_digest(
hasher.finalize().into(),
))
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum DirectoryChecksumError {
#[error("directory checksum path is not UTF-8: {path:?}")]
NonUtf8Path {
path: PathBuf,
},
#[error("directory checksum path is not canonical and relative: {path:?}")]
InvalidRelativePath {
path: PathBuf,
},
#[error("duplicate directory checksum path: {path:?}")]
DuplicatePath {
path: PathBuf,
},
}
impl From<DirectoryChecksumError> for ArtifactError {
fn from(error: DirectoryChecksumError) -> Self {
match error {
DirectoryChecksumError::NonUtf8Path { path } => Self::NonUtf8Path { path },
error => Self::Io(io::Error::new(io::ErrorKind::InvalidData, error)),
}
}
}
#[cfg(unix)]
fn require_utf8_tree_name(
name: &std::ffi::OsStr,
display_root: &Path,
) -> Result<(), ArtifactError> {
if name.to_str().is_none() {
return Err(ArtifactError::NonUtf8Path {
path: display_root.join(name),
});
}
Ok(())
}
pub fn checksum_relative_path(
root: &Path,
relative: &Path,
) -> Result<ArtifactChecksumRecord, ArtifactError> {
secure::checksum_relative_path(root, relative)
}
pub fn stage_relative_path(
root: &Path,
relative: &Path,
destination: &Path,
) -> Result<ArtifactChecksumRecord, ArtifactError> {
secure::stage_relative_path(root, relative, destination)
}
#[derive(Debug, Error)]
pub enum ArtifactError {
#[error("artifact path is not UTF-8: {path:?}")]
NonUtf8Path {
path: PathBuf,
},
#[error(transparent)]
Io(#[from] io::Error),
#[error("unsupported artifact entry at {path}: {kind}")]
UnsupportedEntry {
path: String,
kind: String,
},
#[error("secure artifact traversal is unsupported on platform {0}")]
UnsupportedPlatform(&'static str),
}