crates/dwarves-slsa/src/subjects.rs
//! The subjects: the sha256 of every output the build left in its out directory.
use std::io::Read;
use std::path::Path;
use sha2::{Digest, Sha256};
use crate::statement::Subject;
use crate::{bail, Result};
/// Lowercase hex sha256.
pub fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
/// The sha256 of a file, streamed.
pub fn sha256_file(path: &Path) -> Result<String> {
let mut f =
std::fs::File::open(path).map_err(|e| crate::Error(format!("{}: {e}", path.display())))?;
let mut h = Sha256::new();
let mut buf = vec![0u8; 1 << 16];
loop {
let n = f.read(&mut buf)?;
if n == 0 {
break;
}
h.update(&buf[..n]);
}
Ok(hex::encode(h.finalize()))
}
/// Every regular file below `dir`, named by its path relative to `dir` (`/`
/// separated), sorted by name. A symlink or any other kind of file is refused: the
/// build cannot point a subject at something outside its out directory.
pub fn subjects_of_dir(dir: &Path) -> Result<Vec<Subject>> {
if !std::fs::symlink_metadata(dir)
.map(|m| m.is_dir())
.unwrap_or(false)
{
bail!(
"out directory {} is missing (or not a directory)",
dir.display()
);
}
let mut out = Vec::new();
walk(dir, "", &mut out)?;
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
fn walk(dir: &Path, prefix: &str, out: &mut Vec<Subject>) -> Result<()> {
for e in std::fs::read_dir(dir)? {
let e = e?;
let name = e
.file_name()
.into_string()
.map_err(|n| crate::Error(format!("a file name that is not UTF-8: {n:?}")))?;
let rel = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
let t = e.file_type()?;
if t.is_symlink() {
bail!("{rel} in the out directory is a symlink: refused");
} else if t.is_dir() {
walk(&e.path(), &rel, out)?;
} else if t.is_file() {
out.push(Subject::sha256(rel, sha256_file(&e.path())?));
} else {
bail!("{rel} in the out directory is not a regular file: refused");
}
}
Ok(())
}