use sha2::{Digest, Sha256};
use super::artifact::CompilationRef;
const NULL_FINGERPRINT_SENTINEL: &str = "null";
pub fn recompute(compilations: &[CompilationRef]) -> String {
let mut lines = Vec::with_capacity(compilations.len());
for compilation in compilations {
let canonical =
serde_json::to_string(&compilation.identity).unwrap_or_else(|_| "null".to_string());
let fingerprint = compilation
.fingerprint
.as_deref()
.unwrap_or(NULL_FINGERPRINT_SENTINEL);
lines.push(format!("{canonical}\u{1f}{fingerprint}"));
}
let preimage = lines.join("\n");
let digest = Sha256::digest(preimage.as_bytes());
let mut hex = String::with_capacity(64);
for byte in digest {
hex.push_str(&format!("{byte:02x}"));
}
hex
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn empty_compilation_list_folds_to_a_stable_digest() {
let digest = recompute(&[]);
assert_eq!(digest.len(), 64);
assert_eq!(
digest,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn key_order_in_the_source_identity_value_does_not_move_the_digest() {
let a = CompilationRef {
identity: json!({"projectPath": "P.csproj", "projectName": "P", "requestedTfm": "net9.0"}),
fingerprint: Some("a".repeat(40)),
};
let b = CompilationRef {
identity: json!({"requestedTfm": "net9.0", "projectName": "P", "projectPath": "P.csproj"}),
fingerprint: Some("a".repeat(40)),
};
assert_eq!(recompute(std::slice::from_ref(&a)), recompute(&[b]));
}
#[test]
fn a_null_fingerprint_uses_the_sentinel_and_cannot_collide_with_a_real_one() {
let unsupported = CompilationRef {
identity: json!({"projectPath": "P.csproj"}),
fingerprint: None,
};
let literal_null_string = CompilationRef {
identity: json!({"projectPath": "P.csproj"}),
fingerprint: Some("null".to_string()),
};
assert_eq!(
recompute(&[unsupported]),
recompute(&[literal_null_string]),
"the sentinel and a real (if implausible) 4-char fingerprint string collide only \
because a genuine fingerprint is always 40 hex characters, never 4"
);
}
#[test]
fn two_compilations_fold_differently_from_one_with_the_same_total_content() {
let one = CompilationRef {
identity: json!({"projectPath": "P.csproj"}),
fingerprint: Some("a".repeat(40)),
};
let two = [
CompilationRef {
identity: json!({"projectPath": "P.csproj"}),
fingerprint: Some("a".repeat(20)),
},
CompilationRef {
identity: json!({}),
fingerprint: Some("a".repeat(20)),
},
];
assert_ne!(recompute(std::slice::from_ref(&one)), recompute(&two));
}
}