sui_compat/source.rs
1//! Source-tree store-path computation — the primitive behind
2//! `builtins.getFlake "path:..."`.
3//!
4//! CppNix, when asked for a `path:` flake ref, serializes the
5//! source tree as a NAR archive (excluding `.git` by default),
6//! hashes the NAR with sha256, and produces:
7//!
8//! - a store path of the form `/nix/store/<hash>-source`
9//! (computed via the `fixed-output-hash` "source" branch),
10//! - a SRI-format `narHash` of the form `sha256-<base64>`.
11//!
12//! Both are surfaced on the flake result as `outPath` + `narHash`
13//! (top level) and duplicated inside `sourceInfo`.
14//!
15//! This module is the single place we serialize + hash a source
16//! tree. Callers (currently just the flake evaluator in
17//! sui-eval) go through one function and get both outputs
18//! atomically — no chance of the hash drifting from the path.
19
20use std::io::Cursor;
21use std::path::Path;
22
23use sha2::{Digest, Sha256};
24
25use crate::hash::{base64_encode, HashAlgorithm, NixHash};
26use crate::nar::{NarError, NarWriter};
27use crate::store_path::compute_fixed_output_hash;
28
29/// Result of serializing + hashing a source tree.
30#[derive(Debug, Clone)]
31pub struct SourceHash {
32 /// Store path the source would be materialized under, e.g.
33 /// `/nix/store/p8zn7x0860a3h5xf1dg01a3sfxs3s46i-source`.
34 pub store_path: String,
35 /// SRI-format NAR hash, e.g.
36 /// `sha256-fpA5m7tc6t4Oe6Uku9gKvul7CrR7urWE1K+DA0nhLPI=`.
37 /// This is what CppNix exposes as the `narHash` attribute on
38 /// flake results.
39 pub nar_hash_sri: String,
40 /// Raw NAR bytes. Callers that want to cache or upload the
41 /// archive (binary cache push, store materialization) use this
42 /// directly — re-serializing would be both wasteful and risks
43 /// nondeterminism.
44 pub nar_bytes: Vec<u8>,
45}
46
47/// NAR-serialize `dir`, hash it, and compute the CppNix source
48/// store path + SRI narHash.
49///
50/// The `name` argument is the final `-<name>` segment of the
51/// resulting store path. For flake `path:` refs CppNix uses
52/// `"source"` unconditionally.
53///
54/// # Errors
55///
56/// Returns a [`NarError`] if the path can't be serialized (e.g.
57/// broken symlink, unreadable directory).
58pub fn nar_hash_source_tree(dir: &Path, name: &str) -> Result<SourceHash, NarError> {
59 let mut nar_bytes = Vec::new();
60 {
61 let mut cursor = Cursor::new(&mut nar_bytes);
62 NarWriter::write_path(&mut cursor, dir)?;
63 }
64
65 // Inner sha256 of the NAR, in lowercase hex — fed to
66 // `compute_fixed_output_hash` which expects hex.
67 let digest = Sha256::digest(&nar_bytes);
68 let digest_bytes = digest.to_vec();
69 let hex: String = digest_bytes.iter().map(|b| format!("{b:02x}")).collect();
70
71 let store_path = compute_fixed_output_hash("sha256", &hex, true, name);
72
73 // SRI = `sha256-<base64>` over the RAW digest bytes (not the hex).
74 let nar_hash = NixHash::new(HashAlgorithm::Sha256, digest_bytes.clone());
75 let nar_hash_sri = nar_hash.to_sri();
76
77 Ok(SourceHash {
78 store_path,
79 nar_hash_sri,
80 nar_bytes,
81 })
82}
83
84/// Base64 encode the SHA-256 of `bytes` without the `sha256-`
85/// prefix. Exposed for callers that already have NAR bytes in
86/// hand (e.g. a cache hit).
87#[must_use]
88pub fn base64_sha256(bytes: &[u8]) -> String {
89 base64_encode(&Sha256::digest(bytes))
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use std::io::Write;
96
97 fn mk_flake_dir() -> tempfile::TempDir {
98 let dir = tempfile::tempdir().unwrap();
99 let flake_nix = dir.path().join("flake.nix");
100 let mut f = std::fs::File::create(&flake_nix).unwrap();
101 // Exact bytes we probed against CppNix.
102 write!(f, "{{ outputs = {{ self }}: {{ value = 42; }}; }}\n").unwrap();
103 dir
104 }
105
106 #[test]
107 fn source_tree_produces_a_store_path_and_an_sri_hash() {
108 let dir = mk_flake_dir();
109 let sh = nar_hash_source_tree(dir.path(), "source").expect("nar hash");
110 // Structural assertions — any NAR-hash-of-a-tree must have
111 // these shapes. The exact CppNix parity is asserted in an
112 // integration test (requires nix binary).
113 assert!(sh.store_path.starts_with("/nix/store/"));
114 assert!(sh.store_path.ends_with("-source"));
115 assert!(sh.nar_hash_sri.starts_with("sha256-"));
116 assert!(!sh.nar_bytes.is_empty());
117 assert!(sh.nar_bytes.starts_with(b"\r\x00\x00\x00\x00\x00\x00\x00nix-archive-1"),
118 "NAR must begin with the magic header — got {:?}",
119 &sh.nar_bytes[..16]);
120 }
121}