use super::archive::{Cause, PackageArchiveSpec, PackageQualificationLimits};
use serde_json::Value;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use ic_host_fs::read::tree::{TreeVisitor, walk_tree};
use rustix::{
fd::OwnedFd,
fs::{FileType, Mode, OFlags, open},
};
pub(super) fn sources(
root: &Path,
packages: &[PackageArchiveSpec<'_>],
limits: PackageQualificationLimits,
) -> Result<BTreeMap<PathBuf, Value>, Cause> {
let mut files = BTreeMap::new();
let mut remaining = limits.source_entries;
for spec in packages {
let path = root.join(format!("{}-{}", spec.name, spec.version));
let fd = open(
&path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?;
let mut visitor = SourceInventory {
prefix: path.strip_prefix(root)?,
limits,
remaining: &mut remaining,
files: &mut files,
};
visitor.enter_directory(Path::new(""), &path)?;
walk_tree(&fd, Path::new(""), &path, &mut visitor)?;
}
Ok(files)
}
struct SourceInventory<'a> {
prefix: &'a Path,
limits: PackageQualificationLimits,
remaining: &'a mut usize,
files: &'a mut BTreeMap<PathBuf, Value>,
}
impl SourceInventory<'_> {
fn admit(&mut self, relative: &Path) -> Result<(), Cause> {
if relative.components().count() > self.limits.source_depth || *self.remaining == 0 {
return Err("extracted source traversal budget exceeded".into());
}
*self.remaining -= 1;
Ok(())
}
}
impl TreeVisitor for SourceInventory<'_> {
type Error = Cause;
fn file(&mut self, fd: &OwnedFd, relative: &Path, _display: &Path) -> Result<(), Cause> {
use std::os::unix::fs::MetadataExt as _;
self.admit(relative)?;
let file = std::fs::File::from(fd.try_clone()?);
let mode = file.metadata()?.mode() & 0o777;
let sha256 = ic_host_artifacts::artifact::hash_reader(file, self.limits.file_bytes as u64)?
.sha256
.to_string();
self.files.insert(
self.prefix.join(relative),
serde_json::json!({"sha256": sha256, "mode": mode}),
);
Ok(())
}
fn enter_directory(&mut self, relative: &Path, _display: &Path) -> Result<(), Cause> {
self.admit(relative)
}
fn leave_directory(
&mut self,
_fd: &OwnedFd,
_relative: &Path,
_display: &Path,
) -> Result<(), Cause> {
Ok(())
}
fn unsupported(&self, display: &Path, _kind: FileType) -> Cause {
format!("unsupported extracted source entry: {}", display.display()).into()
}
fn non_utf8(&self, display: &Path) -> Cause {
format!("non-UTF-8 extracted source entry: {}", display.display()).into()
}
}