use std::io;
use std::path::Path;
pub(crate) struct BeamFacts<'a> {
pub files: &'a [(String, String)],
pub commands: &'a [String],
pub cwd: &'a str,
pub env: &'a [(String, String)],
pub args: &'a [String],
pub needs: &'a [String],
pub executor: &'a str,
}
pub(crate) fn fingerprint(facts: &BeamFacts<'_>) -> String {
let mut hasher = blake3::Hasher::new();
item(&mut hasher, "files");
for (path, hash) in facts.files {
item(&mut hasher, path);
item(&mut hasher, hash);
}
item(&mut hasher, "commands");
for command in facts.commands {
item(&mut hasher, command);
}
item(&mut hasher, "executor");
item(&mut hasher, facts.executor);
item(&mut hasher, "cwd");
item(&mut hasher, facts.cwd);
item(&mut hasher, "env");
for (name, value) in facts.env {
item(&mut hasher, name);
item(&mut hasher, value);
}
item(&mut hasher, "args");
for arg in facts.args {
item(&mut hasher, arg);
}
item(&mut hasher, "needs");
for need in facts.needs {
item(&mut hasher, need);
}
hasher.finalize().to_hex().to_string()
}
pub(crate) fn static_contribution(
commands: &[String],
cwd: &str,
env: &[(String, String)],
args: &[String],
executor: &str,
) -> String {
fingerprint(&BeamFacts {
files: &[],
commands,
cwd,
env,
args,
needs: &[],
executor,
})
}
pub(crate) fn hash_file(path: &Path) -> io::Result<String> {
Ok(blake3::hash(&std::fs::read(path)?).to_hex().to_string())
}
fn item(hasher: &mut blake3::Hasher, text: &str) {
hasher.update(&(text.len() as u64).to_le_bytes());
hasher.update(text.as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
fn strings(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn pairs(items: &[(&str, &str)]) -> Vec<(String, String)> {
items
.iter()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect()
}
fn facts_fingerprint(
files: &[(&str, &str)],
commands: &[&str],
cwd: &str,
env: &[(&str, &str)],
args: &[&str],
needs: &[&str],
executor: &'static str,
) -> String {
fingerprint(&BeamFacts {
files: &pairs(files),
commands: &strings(commands),
cwd,
env: &pairs(env),
args: &strings(args),
needs: &strings(needs),
executor,
})
}
#[test]
fn identical_facts_produce_identical_fingerprints() {
let a = facts_fingerprint(
&[("src/a.rs", "h1")],
&["build"],
"/p",
&[("K", "v")],
&[],
&[],
"embedded",
);
let b = facts_fingerprint(
&[("src/a.rs", "h1")],
&["build"],
"/p",
&[("K", "v")],
&[],
&[],
"embedded",
);
assert_eq!(a, b);
}
#[test]
fn every_field_participates_in_the_fingerprint() {
let base = facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/p",
&[("K", "v")],
&["arg"],
&["n1"],
"embedded",
);
let variants = [
facts_fingerprint(
&[("a", "h2")],
&["cmd"],
"/p",
&[("K", "v")],
&["arg"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("b", "h1")],
&["cmd"],
"/p",
&[("K", "v")],
&["arg"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd2"],
"/p",
&[("K", "v")],
&["arg"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/q",
&[("K", "v")],
&["arg"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/p",
&[("K", "w")],
&["arg"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/p",
&[("K", "v")],
&["other"],
&["n1"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/p",
&[("K", "v")],
&["arg"],
&["n2"],
"embedded",
),
facts_fingerprint(
&[("a", "h1")],
&["cmd"],
"/p",
&[("K", "v")],
&["arg"],
&["n1"],
"system",
),
];
for variant in variants {
assert_ne!(base, variant);
}
}
#[test]
fn adjacent_items_cannot_collide_by_concatenation() {
let joined = facts_fingerprint(&[], &["ab"], "/p", &[], &[], &[], "embedded");
let split = facts_fingerprint(&[], &["a", "b"], "/p", &[], &[], &[], "embedded");
assert_ne!(joined, split);
}
#[test]
fn the_static_contribution_ignores_files_and_needs() {
let contribution = static_contribution(
&strings(&["cmd"]),
"/p",
&pairs(&[("K", "v")]),
&[],
"embedded",
);
assert_eq!(
contribution,
facts_fingerprint(&[], &["cmd"], "/p", &[("K", "v")], &[], &[], "embedded")
);
}
#[test]
fn hash_file_reflects_content_not_metadata() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("input.txt");
std::fs::write(&path, "v1").unwrap();
let first = hash_file(&path).unwrap();
std::fs::write(&path, "v2").unwrap();
let second = hash_file(&path).unwrap();
std::fs::write(&path, "v1").unwrap();
let third = hash_file(&path).unwrap();
assert_ne!(first, second);
assert_eq!(first, third);
}
#[test]
fn hash_file_reports_a_missing_file_as_an_error() {
assert!(hash_file(std::path::Path::new("/does/not/exist")).is_err());
}
}