1use std::path::Path;
6
7use crate::format::{is_oid, Names, WRITE};
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 key_preimage_in(&WRITE, gate, fp)
95}
96
97pub fn key_preimage_in(names: &Names, gate: &str, fp: &str) -> String {
99 format!("{} {gate} {fp}\n", names.input_domain)
100}
101
102pub fn key(root: &Path, gate: &str, fp: &str) -> Result<String, Error> {
105 key_in(root, &WRITE, gate, fp)
106}
107
108pub fn key_in(root: &Path, names: &Names, gate: &str, fp: &str) -> Result<String, Error> {
111 hash(root, key_preimage_in(names, gate, fp).as_bytes())
112}
113
114pub fn canon_text(listing: &str) -> Option<String> {
121 let mut out = String::with_capacity(listing.len());
122 for line in listing.lines() {
123 let (head, path) = line.split_once('\t')?;
124 let mut f = head.split(' ');
125 let (mode, kind, oid) = (f.next()?, f.next()?, f.next()?);
126 if f.next().is_some() {
127 return None;
128 }
129 let under_plans = path.starts_with("docs/plans/") || path.starts_with("\"docs/plans/");
131 if matches!(mode, "100644" | "100755") && under_plans {
132 out.push_str(&format!(
133 "{mode} {kind} {}\t{path}\n",
134 "0".repeat(oid.len())
135 ));
136 } else {
137 out.push_str(line);
138 out.push('\n');
139 }
140 }
141 (!out.is_empty()).then_some(out)
142}
143
144pub fn canon(root: &Path, tree: &str) -> Result<String, Error> {
147 let listing = git::stdout_bytes_in(
148 root,
149 &[
150 "-c",
151 "core.quotePath=true",
152 "ls-tree",
153 "-r",
154 "--full-tree",
155 tree,
156 ],
157 )
158 .ok_or(Error::Git)?;
159 let listing = std::str::from_utf8(&listing).map_err(|_| Error::Malformed)?;
160 let text = canon_text(listing).ok_or(Error::Malformed)?;
161 hash(root, text.as_bytes())
162}
163
164pub fn canon_key_preimage(canon: &str) -> String {
166 canon_key_preimage_in(&WRITE, canon)
167}
168
169pub fn canon_key_preimage_in(names: &Names, canon: &str) -> String {
171 format!("{} {canon}\n", names.canon_domain)
172}
173
174pub fn canon_key(root: &Path, canon: &str) -> Result<String, Error> {
177 canon_key_in(root, &WRITE, canon)
178}
179
180pub fn canon_key_in(root: &Path, names: &Names, canon: &str) -> Result<String, Error> {
182 hash(root, canon_key_preimage_in(names, canon).as_bytes())
183}
184
185pub fn absent_optional(root: &Path, tree: &str, spec: &Spec, gate: &str) -> Vec<String> {
190 spec.paths_of(gate)
191 .unwrap_or(&[])
192 .iter()
193 .filter(|t| t.optional)
194 .filter(|t| git::stdout_in(root, &["ls-tree", tree, "--", &t.path]).is_none())
195 .map(|t| t.path.clone())
196 .collect()
197}