use super::{TreeVisitor, walk_tree};
use crate::read::{OpenEntryError, OpenedEntry, open_directory_no_follow, open_entry_beneath};
use ic_host_artifacts::artifact::{
ArtifactError, Sha256Digest, hash_reader,
tree::{DirectoryChecksumError, checksum_relative_files},
};
use rustix::{
fd::{AsFd, OwnedFd},
fs::FileType,
};
use std::{
fs::File,
io,
path::{Component, Path, PathBuf},
};
pub fn checksum_directory(
path: &Path,
max_file_bytes: u64,
) -> Result<Sha256Digest, TreeChecksumError> {
let (root, relative) = if path.is_absolute() {
(
Path::new("/"),
path.strip_prefix("/").map_err(io::Error::other)?,
)
} else {
(Path::new("."), path)
};
if relative.as_os_str().is_empty()
|| !relative
.components()
.all(|part| matches!(part, Component::Normal(name) if name.to_str().is_some()))
{
return Err(TreeChecksumError::InvalidPath {
path: path.to_path_buf(),
});
}
let root = open_directory_no_follow(root)?;
let directory = match open_entry_beneath(root.as_fd(), relative) {
Ok(OpenedEntry::Directory(file)) => OwnedFd::from(file),
Ok(OpenedEntry::RegularFile(_)) => {
return Err(TreeChecksumError::UnsupportedEntry {
path: path.to_path_buf(),
kind: FileType::RegularFile,
});
}
Err(OpenEntryError::UnsupportedEntry { kind }) => {
return Err(TreeChecksumError::UnsupportedEntry {
path: path.to_path_buf(),
kind,
});
}
Err(OpenEntryError::Io(error)) => return Err(error.into()),
};
checksum_directory_at(&directory, path, max_file_bytes)
}
pub fn checksum_directory_at(
directory: &OwnedFd,
display_root: &Path,
max_file_bytes: u64,
) -> Result<Sha256Digest, TreeChecksumError> {
let mut visitor = ChecksumVisitor::new(max_file_bytes);
walk_tree(directory, Path::new(""), display_root, &mut visitor)?;
visitor.finish()
}
pub(crate) struct ChecksumVisitor {
checksums: Vec<(PathBuf, Sha256Digest)>,
max_file_bytes: u64,
}
impl ChecksumVisitor {
pub(crate) const fn new(max_file_bytes: u64) -> Self {
Self {
checksums: Vec::new(),
max_file_bytes,
}
}
pub(crate) fn record_file(
&mut self,
file: File,
relative: &Path,
) -> Result<(), TreeChecksumError> {
let identity = hash_reader(file, self.max_file_bytes).map_err(TreeChecksumError::Input)?;
self.checksums
.push((relative.to_path_buf(), identity.sha256));
Ok(())
}
pub(crate) fn finish(self) -> Result<Sha256Digest, TreeChecksumError> {
checksum_relative_files(self.checksums).map_err(TreeChecksumError::Declaration)
}
}
impl TreeVisitor for ChecksumVisitor {
type Error = TreeChecksumError;
fn file(&mut self, fd: &OwnedFd, relative: &Path, _display: &Path) -> Result<(), Self::Error> {
self.record_file(File::from(fd.try_clone()?), relative)
}
fn enter_directory(&mut self, _relative: &Path, _display: &Path) -> Result<(), Self::Error> {
Ok(())
}
fn leave_directory(
&mut self,
_fd: &OwnedFd,
_relative: &Path,
_display: &Path,
) -> Result<(), Self::Error> {
Ok(())
}
fn unsupported(&self, display: &Path, kind: FileType) -> Self::Error {
TreeChecksumError::UnsupportedEntry {
path: display.to_path_buf(),
kind,
}
}
fn non_utf8(&self, display: &Path) -> Self::Error {
TreeChecksumError::NonUtf8Path {
path: display.to_path_buf(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum TreeChecksumError {
#[error("{0}")]
Io(#[from] io::Error),
#[error("{0}")]
Input(#[from] ArtifactError),
#[error("{0}")]
Declaration(#[from] DirectoryChecksumError),
#[error("invalid directory checksum path: {path:?}")]
InvalidPath {
path: PathBuf,
},
#[error("directory checksum path is not UTF-8: {path:?}")]
NonUtf8Path {
path: PathBuf,
},
#[error("unsupported directory checksum entry at {path:?}: {kind:?}")]
UnsupportedEntry {
path: PathBuf,
kind: FileType,
},
}
#[cfg(test)]
mod tests;