Skip to main content

amont_runtime/
attest_inputs.rs

1//! amont's side of attest's input fingerprints and canonical tree.
2//!
3//! The format itself (the `attest-inputs` grammar, fingerprints, the
4//! canonical tree, note keys, the opt-out, the completeness proof) belongs to
5//! the `git_attest` library, attest's one implementation of it (decisions
6//! ADR-0037). What stays here is amont's own knowledge on top of it: which
7//! route its LOCAL gate stamps take, and the key those stamps are filed
8//! under. The hook runs at the worktree top, so "the repository" is `.`.
9
10use std::path::Path;
11
12use git_attest::spec::{at, coverage, plan_exempt, PlanExempt, DEPRECATED_OPT_OUT};
13
14pub use git_attest::spec::{conf_says_plan_exempt_off, CoverageError, Findings, Spec, EXEMPT};
15
16/// The repository a hook stands in: git starts hooks at the worktree top.
17fn here() -> &'static Path {
18    Path::new(".")
19}
20
21/// The spec `tree` commits, when it is there and valid. No spec, an invalid
22/// or ambiguous one, a tree git cannot list: `None`, and the gate stamps fall
23/// back to the tree (or, with no spec at all, the canonical route).
24pub(crate) fn spec_at(tree: &str) -> Option<Spec> {
25    match at(here(), tree) {
26        Ok(Some((_, spec))) => Some(spec),
27        _ => None,
28    }
29}
30
31/// The fingerprint of `gate` on `tree`, as attest computes it.
32pub(crate) fn fingerprint(tree: &str, spec: &Spec, gate: &str) -> Option<String> {
33    git_attest::fingerprint::fingerprint(here(), tree, spec, gate).ok()
34}
35
36/// The synthetic key of (gate, fingerprint) in the inputs ref.
37pub(crate) fn input_key(gate: &str, fp: &str) -> Option<String> {
38    git_attest::fingerprint::key(here(), gate, fp).ok()
39}
40
41/// Does `gate` declare `file`, itself or a directory above it? Asked of
42/// attest's coverage rule, so the ancestry is attest's, not a copy.
43pub(crate) fn declares(spec: &Spec, gate: &str, file: &str) -> bool {
44    coverage(
45        spec,
46        &[gate.to_string()],
47        &[file.as_bytes().to_vec()],
48        |_| true,
49    )
50    .is_empty()
51}
52
53/// The canonical fingerprint of `tree` when its gates are reused canonically:
54/// no spec at all (a successful listing that lists neither location), the
55/// repository has not opted out, and git could compute it. An invalid,
56/// ambiguous or unlistable spec opens no canonical route, so no canonical
57/// stamps.
58pub(crate) fn canonical_route(tree: &str) -> Option<String> {
59    match at(here(), tree) {
60        Ok(None) => {}
61        _ => return None,
62    }
63    if plan_exempt(here(), tree).is_off() {
64        return None;
65    }
66    git_attest::fingerprint::canon(here(), tree).ok()
67}
68
69/// The key under which the local gate stamps of a canonical fingerprint are
70/// filed in amont's own inputs ref: one note per fingerprint, holding the
71/// tokens of every gate that passed on it. amont's own preimage, never
72/// attest's, so a stamp can never be read as an attestation.
73pub(crate) fn canon_gate_key(canon: &str) -> Option<String> {
74    crate::git::stdout_piped_in(
75        here(),
76        &["hash-object", "--stdin"],
77        format!("amont-gate-canon {canon}\n").as_bytes(),
78    )
79    .filter(|s| git_attest::format::is_oid(s))
80}
81
82/// The line to print when HEAD opts out of the canonical tree through
83/// `set planExempt off` in `amont.conf` alone: still honoured, deprecated,
84/// and read by attest only until 2.0.0 (decisions ADR-0037).
85pub fn deprecated_opt_out() -> Option<&'static str> {
86    let tree = crate::git::stdout(&["rev-parse", "--verify", "--quiet", "HEAD^{tree}"])?;
87    (plan_exempt(here(), &tree) == PlanExempt::OffByAmontConf).then_some(DEPRECATED_OPT_OUT)
88}
89
90/// Do the required gates declare, completely, everything `tree` tracks?
91/// attest's proof (`git-attest check-inputs` runs the same one).
92pub fn check(tree: &str, required: &[String]) -> Result<Findings, CoverageError> {
93    git_attest::spec::check_coverage(here(), tree, required)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    /// A tree git cannot list has no known spec, so it opens no route, and a
101    /// failing listing is not an absent opt-out: fail narrow.
102    #[test]
103    fn a_tree_git_cannot_list_opens_no_route() {
104        let bogus = "0123456789012345678901234567890123456789";
105        assert_eq!(spec_at(bogus), None);
106        assert_eq!(canonical_route(bogus), None);
107        assert!(plan_exempt(here(), bogus).is_off());
108    }
109
110    #[test]
111    fn a_gate_declares_a_file_or_a_directory_above_it() {
112        let spec = git_attest::spec::parse_spec(b"lint src amont.conf\ntest src\n").unwrap();
113        assert!(declares(&spec, "lint", "amont.conf"));
114        assert!(declares(&spec, "lint", "src/main.rs"));
115        assert!(!declares(&spec, "test", "amont.conf"));
116        assert!(
117            !declares(&spec, "fmt", "amont.conf"),
118            "an undeclared gate declares nothing"
119        );
120    }
121}