Skip to main content

nix_compat/store_path/
utils.rs

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