use std::io::Read as _;
use std::path::{Path, PathBuf};
use aws_lc_rs::digest::{Context, SHA256};
use yapi_types::time::hex;
use super::{ChestError, invalid, io};
pub fn tree_hash(root: &Path) -> Result<String, ChestError> {
let metadata = root.symlink_metadata().map_err(io(root))?;
if metadata.is_symlink() {
return Err(refused(root));
}
let mut files = Vec::new();
if metadata.is_dir() {
collect(root, "", &mut files)?;
} else {
files.push((String::new(), root.to_path_buf()));
}
files.sort();
let mut context = Context::new(&SHA256);
let mut buffer = vec![0; 64 * 1024];
for (relative, path) in files {
let metadata = path.symlink_metadata().map_err(io(&path))?;
context.update(relative.as_bytes());
context.update(b"\0");
context.update(if executable(&metadata) { b"x" } else { b"-" });
let mut file = std::fs::File::open(&path).map_err(io(&path))?;
let mut contents = Context::new(&SHA256);
loop {
match file.read(&mut buffer) {
Ok(0) => break,
Ok(read) => contents.update(&buffer[..read]),
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
Err(err) => return Err(io(&path)(err)),
}
}
context.update(contents.finish().as_ref());
}
Ok(format!("sha256-{}", hex(context.finish().as_ref())))
}
fn refused(path: &Path) -> ChestError {
invalid(format!(
"{}: symbolic links are not allowed in a package",
path.display()
))
}
fn collect(dir: &Path, prefix: &str, files: &mut Vec<(String, PathBuf)>) -> Result<(), ChestError> {
for entry in std::fs::read_dir(dir).map_err(io(dir))? {
let entry = entry.map_err(io(dir))?;
let path = entry.path();
let kind = entry.file_type().map_err(io(&path))?;
let name = entry
.file_name()
.into_string()
.map_err(|_| invalid(format!("{}: the name is not UTF-8", path.display())))?;
if name == ".git" {
continue;
}
let relative = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
if kind.is_symlink() {
return Err(refused(&path));
} else if kind.is_dir() {
collect(&path, &relative, files)?;
} else if kind.is_file() {
files.push((relative, path));
}
}
Ok(())
}
#[cfg(unix)]
fn executable(metadata: &std::fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt as _;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
fn executable(_: &std::fs::Metadata) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("yapi-chest-hash-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write(root: &Path, path: &str, contents: &str) {
let path = root.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
#[cfg(unix)]
fn chmod(root: &Path, path: &str, mode: u32) {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(root.join(path), std::fs::Permissions::from_mode(mode)).unwrap();
}
#[cfg(unix)]
#[test]
fn tree_hash_vector() {
let root = scratch("vector");
write(&root, "a.txt", "hello\n");
write(&root, "bin/run", "#!/bin/sh\n");
chmod(&root, "a.txt", 0o644);
chmod(&root, "bin/run", 0o755);
let vector = "sha256-8ec032806587aaad014b497c7476c1dd2321f8c3acc4e7b03c45101290abc097";
assert_eq!(tree_hash(&root).unwrap(), vector);
write(&root, ".git/HEAD", "ref: refs/heads/main\n");
write(&root, "bin/.git/config", "[core]\n");
write(&root, "sub/.git", "gitdir: ../.git/modules/sub\n");
assert_eq!(tree_hash(&root).unwrap(), vector);
chmod(&root, "a.txt", 0o654);
assert_ne!(tree_hash(&root).unwrap(), vector);
chmod(&root, "a.txt", 0o644);
std::os::unix::fs::symlink("a.txt", root.join("bin/link")).unwrap();
let error = tree_hash(&root).unwrap_err().to_string();
assert!(
error.ends_with("bin/link: symbolic links are not allowed in a package"),
"{error}"
);
std::fs::remove_file(root.join("bin/link")).unwrap();
let link = scratch("vector-link");
std::fs::remove_dir(&link).unwrap();
std::os::unix::fs::symlink(&root, &link).unwrap();
assert!(tree_hash(&link).is_err());
assert_eq!(
tree_hash(&root.join("a.txt")).unwrap(),
"sha256-89c31f91aca023cafd6de1c6c60492fe1c63117041231bdf5370629f71254cec"
);
let (one, two) = (scratch("ab-c"), scratch("a-bc"));
write(&one, "ab", "c");
write(&two, "a", "bc");
assert_ne!(tree_hash(&one).unwrap(), tree_hash(&two).unwrap());
for dir in [root, link, one, two] {
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&dir);
}
}
}