qualia_core_db/inference/runtime/receipt/
source.rs1use std::collections::BTreeSet;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use sha2::{Digest, Sha256};
7
8use super::sha256_file;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct SourceProvenance {
12 pub commit: String,
13 pub dirty_source_hash: String,
14 pub executable_sha256: String,
15}
16
17pub fn capture_source_provenance() -> SourceProvenance {
18 let executable_sha256 = std::env::current_exe()
19 .ok()
20 .and_then(|path| sha256_file(&path).ok())
21 .unwrap_or_else(|| "unknown".into());
22 let root = git_stdout(&["rev-parse", "--show-toplevel"])
23 .and_then(|bytes| String::from_utf8(bytes).ok())
24 .map(|text| PathBuf::from(text.trim()))
25 .filter(|path| path.is_dir());
26 let commit = git_stdout(&["rev-parse", "HEAD"])
27 .and_then(|bytes| String::from_utf8(bytes).ok())
28 .map(|text| text.trim().to_string())
29 .filter(|text| !text.is_empty())
30 .unwrap_or_else(|| "unknown".into());
31 let dirty_source_hash = root
32 .as_deref()
33 .and_then(hash_relevant_dirty_sources)
34 .unwrap_or_else(|| "unknown".into());
35 SourceProvenance {
36 commit,
37 dirty_source_hash,
38 executable_sha256,
39 }
40}
41
42fn git_stdout(args: &[&str]) -> Option<Vec<u8>> {
43 let output = Command::new("git").args(args).output().ok()?;
44 output.status.success().then_some(output.stdout)
45}
46
47fn hash_relevant_dirty_sources(root: &Path) -> Option<String> {
48 const PATHS: &[&str] = &[
49 "crates/qualia-core-db",
50 "crates/qualia-cli",
51 "Cargo.toml",
52 "Cargo.lock",
53 ];
54 let mut changed = BTreeSet::new();
55 collect_git_paths(
56 root,
57 &[
58 "diff",
59 "--name-only",
60 "-z",
61 "HEAD",
62 "--",
63 PATHS[0],
64 PATHS[1],
65 PATHS[2],
66 PATHS[3],
67 ],
68 &mut changed,
69 )?;
70 collect_git_paths(
71 root,
72 &[
73 "ls-files",
74 "--others",
75 "--exclude-standard",
76 "-z",
77 "--",
78 PATHS[0],
79 PATHS[1],
80 PATHS[2],
81 PATHS[3],
82 ],
83 &mut changed,
84 )?;
85
86 let mut hasher = Sha256::new();
87 if changed.is_empty() {
88 hasher.update(b"clean");
89 }
90 let mut buffer = [0u8; 1024 * 1024];
91 for relative in changed {
92 hasher.update((relative.len() as u64).to_le_bytes());
93 hasher.update(relative.as_bytes());
94 let path = root.join(&relative);
95 match std::fs::File::open(path) {
96 Ok(mut file) => loop {
97 let read = file.read(&mut buffer).ok()?;
98 if read == 0 {
99 break;
100 }
101 hasher.update(&buffer[..read]);
102 },
103 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
104 hasher.update(b"<deleted>");
105 }
106 Err(_) => return None,
107 }
108 }
109 Some(hex::encode(hasher.finalize()))
110}
111
112fn collect_git_paths(root: &Path, args: &[&str], output: &mut BTreeSet<String>) -> Option<()> {
113 let command = Command::new("git")
114 .current_dir(root)
115 .args(args)
116 .output()
117 .ok()?;
118 if !command.status.success() {
119 return None;
120 }
121 for path in command.stdout.split(|byte| *byte == 0) {
122 if path.is_empty() {
123 continue;
124 }
125 let path = String::from_utf8(path.to_vec()).ok()?;
126 output.insert(path.replace('\\', "/"));
127 }
128 Some(())
129}