Skip to main content

nix_compat/store_path/
utils.rs

1use std::fmt::Display;
2
3use super::{ParseStorePathError, STORE_DIR, StorePathRef};
4use crate::derivation::OutputName;
5use crate::nixhash::{HashAlgo, NixHash, Sha256};
6use crate::{format_sha256, nixbase32};
7
8/// compress_hash takes an arbitrarily long sequence of bytes (usually
9/// a hash digest), and returns a sequence of bytes of length
10/// OUTPUT_SIZE.
11///
12/// It's calculated by rotating through the bytes in the output buffer
13/// (zero- initialized), and XOR'ing with each byte of the passed
14/// input. It consumes 1 byte at a time, and XOR's it with the current
15/// value in the output buffer.
16///
17/// This mimics equivalent functionality in C++ Nix.
18pub fn compress_hash<const OUTPUT_SIZE: usize>(input: &[u8]) -> [u8; OUTPUT_SIZE] {
19    let mut output = [0; OUTPUT_SIZE];
20
21    for (ii, ch) in input.iter().enumerate() {
22        output[ii % OUTPUT_SIZE] ^= ch;
23    }
24
25    output
26}
27
28/// This builds a store path, for a CAHash::Text type store path.
29/// If you don't want to have to pass the entire contents,
30/// you might want to use [build_text_path_from_content_digest] instead.
31pub fn build_text_path<'r, 'name>(
32    name: &'name str,
33    content: impl AsRef<[u8]>,
34    references: impl IntoIterator<Item = StorePathRef<'r>> + 'r,
35) -> Result<StorePathRef<'name>, ParseStorePathError> {
36    build_text_path_from_content_digest(name, Sha256::digest_bytes(content.as_ref()), references)
37}
38
39/// This builds a store path, for a CAHash::Text type store path.
40/// `content_digest` needs to be the sha256 digest of the contents.
41/// If you have the contents as a byte slice, you can also use [build_text_path].
42pub fn build_text_path_from_content_digest<'r, 'n>(
43    name: &'n str,
44    content_digest: impl Into<Sha256>,
45    references: impl IntoIterator<Item = StorePathRef<'r>> + 'r,
46) -> Result<StorePathRef<'n>, ParseStorePathError> {
47    // produce the sha256 digest of the contents
48
49    let ty = format_references("text", references, false);
50
51    build_store_path_from_fingerprint_parts(ty, &content_digest.into(), name)
52}
53
54/// This builds a store path for a content-addressed path (used for fetches and FODs).
55pub fn build_ca_path<'r, 'n>(
56    name: &'n str,
57    is_recursive: bool,
58    hash: &NixHash,
59    references: impl IntoIterator<Item = StorePathRef<'r>> + 'r,
60    has_self_ref: bool,
61) -> Result<StorePathRef<'n>, ParseStorePathError> {
62    let inner_digest = if let NixHash::Sha256(digest) = hash
63        && is_recursive
64    {
65        Sha256::new(*digest)
66    } else {
67        fod_digest(is_recursive, hash, None)
68    };
69
70    if hash.algo() == HashAlgo::Sha256 && is_recursive {
71        build_store_path_from_fingerprint_parts(
72            format_references("source", references, has_self_ref),
73            &inner_digest,
74            name,
75        )
76    } else {
77        // FUTUREWORK: dump when references are non-empty, and when has_self_ref is true.
78        // Add an assertion here?
79        build_store_path_from_fingerprint_parts("output:out", &inner_digest, name)
80    }
81}
82
83/// Builds an input-addressed store path.
84///
85/// Input-addresed store paths are always derivation outputs, the "input" in question is the
86/// derivation and its closure.
87pub fn build_output_path<'n>(
88    name: &'n str,
89    hash_derivation_modulo: &Sha256,
90    output_name: &OutputName,
91) -> Result<StorePathRef<'n>, ParseStorePathError> {
92    build_store_path_from_fingerprint_parts(
93        format_args!("output:{output_name}"),
94        hash_derivation_modulo,
95        name,
96    )
97}
98
99/// This builds a store path from fingerprint parts.
100///
101/// This is called from [build_text_path], [build_output_path] and
102/// [build_ca_path] to assemble the final path.
103///
104/// Using the inputs, it creates a fingerprint, hashes and compresses it,
105/// then uses the passed `name` to create a store path.
106///
107/// If that `name` doesn't match store path name requirements, the error is
108/// passed along.
109fn build_store_path_from_fingerprint_parts<'n>(
110    ty: impl Display,
111    inner_digest: &Sha256,
112    name: &'n str,
113) -> Result<StorePathRef<'n>, ParseStorePathError> {
114    let fingerprint_hash = format_sha256!("{ty}:sha256:{inner_digest:x}:{STORE_DIR}:{name}");
115    // name validation happens in here.
116    StorePathRef::from_name_and_digest_fixed(name, compress_hash(&fingerprint_hash))
117}
118
119pub(crate) fn fod_digest(
120    is_recursive: bool,
121    hash: &NixHash,
122    out_output_path: Option<StorePathRef<'_>>,
123) -> Sha256 {
124    let absolute_sp_optional = std::fmt::from_fn(|f| {
125        if let Some(sp) = &out_output_path {
126            write!(f, "{}", sp.as_absolute_path_fmt())?
127        }
128        Ok(())
129    });
130
131    if is_recursive {
132        format_sha256!(
133            "fixed:out:r:{}:{}",
134            hash.as_nix_lowerhex_string_fmt(),
135            absolute_sp_optional
136        )
137    } else {
138        format_sha256!(
139            "fixed:out:{}:{}",
140            hash.as_nix_lowerhex_string_fmt(),
141            absolute_sp_optional
142        )
143    }
144}
145
146/// This contains the Nix logic to create "reference strings", used for the
147/// output path calculation of ca paths and text paths.
148fn format_references<'a, R>(ty: &'a str, references: R, has_self_ref: bool) -> impl Display + 'a
149where
150    R: IntoIterator<Item = StorePathRef<'a>> + 'a,
151{
152    struct ReferencesFormatter<'a, R> {
153        ty: &'a str,
154        references: std::cell::RefCell<R>,
155        has_self_ref: bool,
156    }
157
158    impl<'a, R> Display for ReferencesFormatter<'a, R>
159    where
160        R: Iterator<Item = StorePathRef<'a>> + 'a,
161    {
162        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163            write!(f, "{}", self.ty)?;
164            while let Some(reference) = self.references.borrow_mut().next() {
165                write!(f, ":{}", reference.as_absolute_path_fmt()).unwrap();
166            }
167
168            if self.has_self_ref {
169                write!(f, ":self")?;
170            }
171
172            Ok(())
173        }
174    }
175
176    ReferencesFormatter {
177        ty,
178        references: std::cell::RefCell::new(references.into_iter()),
179        has_self_ref,
180    }
181}
182
183/// Nix placeholders (i.e. values returned by `builtins.placeholder`)
184/// are used to populate outputs with paths that must be
185/// string-replaced with the actual placeholders later, at runtime.
186///
187/// The actual placeholder is basically just a SHA256 hash encoded in
188/// cppnix format.
189pub fn hash_placeholder(name: &str) -> String {
190    format!(
191        "/{}",
192        nixbase32::encode(&format_sha256!("nix-output:{name}"))
193    )
194}
195
196#[cfg(test)]
197mod test {
198    use hex_literal::hex;
199
200    use super::*;
201    use crate::{nixhash::NixHash, store_path::StorePathRef};
202
203    #[test]
204    fn build_text_path_with_zero_references() {
205        // This hash should match `builtins.toFile`, e.g.:
206        //
207        // nix-repl> builtins.toFile "foo" "bar"
208        // "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
209
210        let store_path: StorePathRef =
211            build_text_path("foo", "bar", []).expect("build_store_path() should succeed");
212
213        assert_eq!(
214            store_path.to_absolute_path().as_str(),
215            "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
216        );
217    }
218
219    #[test]
220    fn build_text_path_with_non_zero_references() {
221        // This hash should match:
222        //
223        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
224        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
225
226        let inner: StorePathRef =
227            build_text_path("foo", "bar", []).expect("path_with_references() should succeed");
228
229        let outer: StorePathRef = build_text_path("baz", inner.to_absolute_path(), [inner])
230            .expect("path_with_references() should succeed");
231
232        assert_eq!(
233            outer.to_absolute_path().as_str(),
234            "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
235        );
236    }
237
238    #[test]
239    fn build_sha1_path() {
240        let outer: StorePathRef = build_ca_path(
241            "bar",
242            true,
243            &NixHash::Sha1(hex!("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")),
244            [],
245            false,
246        )
247        .expect("path_with_references() should succeed");
248
249        assert_eq!(
250            outer.to_absolute_path().as_str(),
251            "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"
252        );
253    }
254
255    #[test]
256    fn build_store_path_with_non_zero_references() {
257        // This hash should match:
258        //
259        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
260        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
261        //
262        // $ nix store make-content-addressed /nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz
263        // rewrote '/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz' to '/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz'
264        let outer: StorePathRef = build_ca_path(
265            "baz",
266            true,
267            &NixHash::Sha256(
268                nixbase32::decode(b"1xqkzcb3909fp07qngljr4wcdnrh1gdam1m2n29i6hhrxlmkgkv1")
269                    .expect("nixbase32 should decode")
270                    .try_into()
271                    .expect("should have right len"),
272            ),
273            [
274                StorePathRef::from_bytes(b"dxwkwjzdaq7ka55pkk252gh32bgpmql4-foo")
275                    .expect("to parse"),
276            ],
277            false,
278        )
279        .expect("path_with_references() should succeed");
280
281        assert_eq!(
282            outer.to_absolute_path().as_str(),
283            "/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz"
284        );
285    }
286}