Skip to main content

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}