Skip to main content

qualia_core_db/inference/runtime/receipt/
source.rs

1use 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}