ic_host_process/provenance/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::tool::{AdmittedTool, CompletedExecution, ExecutionContext, OutputLimits, ToolError};
11use ic_host_artifacts::artifact::{ArtifactIdentity, Sha256Digest};
12use std::{ffi::OsString, fmt};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum UntrackedFiles {
17 No,
19 Normal,
21 All,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum IgnoreSubmodules {
28 None,
30 Untracked,
32 Dirty,
34 All,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct StatusOptions {
41 pub untracked: UntrackedFiles,
43 pub ignore_submodules: IgnoreSubmodules,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum GitQuery {
50 Revision,
52 Tree,
54 Status,
56}
57
58#[derive(Debug)]
60pub enum GitFailure {
61 Tool(Box<ToolError>),
63 InvalidObjectId,
65 UnterminatedStatus,
67}
68
69#[derive(Debug)]
71pub struct GitError {
72 pub query: GitQuery,
74 pub failure: GitFailure,
76 pub completed: Box<[Option<CompletedExecution>; 3]>,
79}
80
81impl fmt::Display for GitError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(f, "Git {:?} observation failed", self.query)
84 }
85}
86
87impl std::error::Error for GitError {
88 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89 match &self.failure {
90 GitFailure::Tool(source) => Some(source.as_ref()),
91 GitFailure::InvalidObjectId | GitFailure::UnterminatedStatus => None,
92 }
93 }
94}
95
96#[derive(Debug)]
98pub struct GitObservations {
99 pub options: StatusOptions,
101 pub status_identity: ArtifactIdentity,
103 pub tool_identity: ArtifactIdentity,
105 revision_evidence: CompletedExecution,
106 tree_evidence: CompletedExecution,
107 status_evidence: CompletedExecution,
108}
109
110impl GitObservations {
111 #[must_use]
113 pub fn revision(&self) -> &str {
114 object_id(&self.revision_evidence.stdout)
115 }
116
117 #[must_use]
119 pub fn tree(&self) -> &str {
120 object_id(&self.tree_evidence.stdout)
121 }
122
123 #[must_use]
125 pub const fn revision_evidence(&self) -> &CompletedExecution {
126 &self.revision_evidence
127 }
128
129 #[must_use]
131 pub const fn tree_evidence(&self) -> &CompletedExecution {
132 &self.tree_evidence
133 }
134
135 #[must_use]
137 pub const fn status_evidence(&self) -> &CompletedExecution {
138 &self.status_evidence
139 }
140
141 #[must_use]
144 pub const fn is_dirty(&self) -> bool {
145 self.status_identity.bytes != 0
146 }
147}
148
149pub fn capture_git(
161 git: &AdmittedTool,
162 context: &ExecutionContext<'_>,
163 options: StatusOptions,
164 limits: OutputLimits,
165) -> Result<GitObservations, GitError> {
166 let mut completed = [None, None, None];
167 for (index, query) in [GitQuery::Revision, GitQuery::Tree, GitQuery::Status]
168 .into_iter()
169 .enumerate()
170 {
171 let evidence = match git
172 .run(&arguments(query, options), context, limits)
173 .and_then(CompletedExecution::require_success)
174 .and_then(CompletedExecution::require_complete)
175 {
176 Ok(evidence) => evidence,
177 Err(source) => {
178 return Err(GitError {
179 query,
180 failure: GitFailure::Tool(Box::new(source)),
181 completed: Box::new(completed),
182 });
183 }
184 };
185 let failure = match query {
186 GitQuery::Revision | GitQuery::Tree if !valid_object_id(&evidence.stdout) => {
187 Some(GitFailure::InvalidObjectId)
188 }
189 GitQuery::Status
190 if !evidence.stdout.is_empty() && evidence.stdout.last() != Some(&0) =>
191 {
192 Some(GitFailure::UnterminatedStatus)
193 }
194 _ => None,
195 };
196 completed[index] = Some(evidence);
197 if let Some(failure) = failure {
198 return Err(GitError {
199 query,
200 failure,
201 completed: Box::new(completed),
202 });
203 }
204 }
205 let [Some(revision), Some(tree), Some(status)] = completed else {
206 unreachable!("every ordered query completed successfully");
207 };
208 let status_identity = ArtifactIdentity {
209 bytes: status.stdout.len() as u64,
210 sha256: Sha256Digest::compute(&status.stdout),
211 };
212 Ok(GitObservations {
213 options,
214 status_identity,
215 tool_identity: git.identity(),
216 revision_evidence: revision,
217 tree_evidence: tree,
218 status_evidence: status,
219 })
220}
221
222fn valid_object_id(bytes: &[u8]) -> bool {
223 matches!(bytes.len(), 41 | 65)
224 && bytes.last() == Some(&b'\n')
225 && bytes[..bytes.len() - 1]
226 .iter()
227 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
228}
229
230fn object_id(bytes: &[u8]) -> &str {
231 std::str::from_utf8(&bytes[..bytes.len() - 1])
233 .expect("captured object ID was validated as ASCII plus LF")
234}
235
236fn arguments(query: GitQuery, options: StatusOptions) -> Vec<OsString> {
237 let mut arguments = vec![
238 "--no-optional-locks".into(),
239 "-c".into(),
240 "core.fsmonitor=false".into(),
241 ];
242 match query {
243 GitQuery::Revision => {
244 arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD".into()]);
245 }
246 GitQuery::Tree => {
247 arguments.extend(["rev-parse".into(), "--verify".into(), "HEAD^{tree}".into()]);
248 }
249 GitQuery::Status => {
250 let untracked = match options.untracked {
251 UntrackedFiles::No => "no",
252 UntrackedFiles::Normal => "normal",
253 UntrackedFiles::All => "all",
254 };
255 let submodules = match options.ignore_submodules {
256 IgnoreSubmodules::None => "none",
257 IgnoreSubmodules::Untracked => "untracked",
258 IgnoreSubmodules::Dirty => "dirty",
259 IgnoreSubmodules::All => "all",
260 };
261 arguments.extend([
262 "status".into(),
263 "--porcelain=v1".into(),
264 "-z".into(),
265 format!("--untracked-files={untracked}").into(),
266 format!("--ignore-submodules={submodules}").into(),
267 ]);
268 }
269 }
270 arguments
271}