git_attest/
fingerprint.rs1use std::path::Path;
6
7use crate::format::is_oid;
8use crate::git;
9use crate::spec::{implicit_inputs, Spec};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Error {
16 NotDeclared,
18 MissingPath,
20 EmptyListing,
22 Malformed,
24 Git,
26}
27
28impl std::fmt::Display for Error {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.write_str(match self {
31 Error::NotDeclared => "the spec declares no such gate",
32 Error::MissingPath => "a declared path does not exist in this tree",
33 Error::EmptyListing => "the declared paths list nothing",
34 Error::Malformed => "the tree listing is malformed",
35 Error::Git => "git failed",
36 })
37 }
38}
39
40impl std::error::Error for Error {}
41
42fn hash(root: &Path, bytes: &[u8]) -> Result<String, Error> {
44 git::stdout_with_input_in(root, &["hash-object", "--stdin"], bytes)
45 .filter(|s| is_oid(s))
46 .ok_or(Error::Git)
47}
48
49pub fn fingerprint(root: &Path, tree: &str, spec: &Spec, gate: &str) -> Result<String, Error> {
52 let toks = spec.paths_of(gate).ok_or(Error::NotDeclared)?;
53 let paths: Vec<String> = toks.iter().map(|t| t.path.clone()).collect();
54 let required: Vec<&str> = toks
58 .iter()
59 .filter(|t| !t.optional)
60 .map(|t| t.path.as_str())
61 .collect();
62 if !required.is_empty() {
63 let names: String = required.iter().map(|p| format!("{tree}:{p}\n")).collect();
64 let answers =
65 git::stdout_with_input_in(root, &["cat-file", "--batch-check"], names.as_bytes())
66 .ok_or(Error::Git)?;
67 if answers.lines().count() != required.len() {
68 return Err(Error::Git);
69 }
70 if answers.lines().any(|l| l.ends_with(" missing")) {
71 return Err(Error::MissingPath);
72 }
73 }
74 let mut args: Vec<String> = vec![
75 "ls-tree".into(),
76 "-r".into(),
77 "-z".into(),
78 "--full-tree".into(),
79 tree.to_string(),
80 "--".into(),
81 ];
82 args.extend(implicit_inputs(&paths));
83 args.extend(paths.iter().cloned());
84 let argv: Vec<&str> = args.iter().map(String::as_str).collect();
85 let listing = git::stdout_bytes_in(root, &argv).ok_or(Error::Git)?;
86 if listing.is_empty() {
87 return Err(Error::EmptyListing);
88 }
89 hash(root, &listing)
90}
91
92pub fn key_preimage(gate: &str, fp: &str) -> String {
94 format!("amont-attest-input {gate} {fp}\n")
95}
96
97pub fn key(root: &Path, gate: &str, fp: &str) -> Result<String, Error> {
100 hash(root, key_preimage(gate, fp).as_bytes())
101}
102
103pub fn canon_text(listing: &str) -> Option<String> {
110 let mut out = String::with_capacity(listing.len());
111 for line in listing.lines() {
112 let (head, path) = line.split_once('\t')?;
113 let mut f = head.split(' ');
114 let (mode, kind, oid) = (f.next()?, f.next()?, f.next()?);
115 if f.next().is_some() {
116 return None;
117 }
118 let under_plans = path.starts_with("docs/plans/") || path.starts_with("\"docs/plans/");
120 if matches!(mode, "100644" | "100755") && under_plans {
121 out.push_str(&format!(
122 "{mode} {kind} {}\t{path}\n",
123 "0".repeat(oid.len())
124 ));
125 } else {
126 out.push_str(line);
127 out.push('\n');
128 }
129 }
130 (!out.is_empty()).then_some(out)
131}
132
133pub fn canon(root: &Path, tree: &str) -> Result<String, Error> {
136 let listing = git::stdout_bytes_in(
137 root,
138 &[
139 "-c",
140 "core.quotePath=true",
141 "ls-tree",
142 "-r",
143 "--full-tree",
144 tree,
145 ],
146 )
147 .ok_or(Error::Git)?;
148 let listing = std::str::from_utf8(&listing).map_err(|_| Error::Malformed)?;
149 let text = canon_text(listing).ok_or(Error::Malformed)?;
150 hash(root, text.as_bytes())
151}
152
153pub fn canon_key_preimage(canon: &str) -> String {
155 format!("amont-attest-canon {canon}\n")
156}
157
158pub fn canon_key(root: &Path, canon: &str) -> Result<String, Error> {
160 hash(root, canon_key_preimage(canon).as_bytes())
161}
162
163pub fn absent_optional(root: &Path, tree: &str, spec: &Spec, gate: &str) -> Vec<String> {
168 spec.paths_of(gate)
169 .unwrap_or(&[])
170 .iter()
171 .filter(|t| t.optional)
172 .filter(|t| git::stdout_in(root, &["ls-tree", tree, "--", &t.path]).is_none())
173 .map(|t| t.path.clone())
174 .collect()
175}