nix_compat/store_path/
utils.rs

1use crate::nixbase32;
2use crate::nixhash::{CAHash, NixHash};
3use crate::store_path::{Error, StorePath, STORE_DIR};
4use data_encoding::HEXLOWER;
5use sha2::{Digest, Sha256};
6use thiserror;
7
8/// Errors that can occur when creating a content-addressed store path.
9///
10/// This wraps the main [crate::store_path::Error]..
11#[derive(Debug, PartialEq, Eq, thiserror::Error)]
12pub enum BuildStorePathError {
13    #[error("Invalid Store Path: {0}")]
14    InvalidStorePath(Error),
15    /// This error occurs when we have references outside the SHA-256 +
16    /// Recursive case. The restriction comes from upstream Nix. It may be
17    /// lifted at some point but there isn't a pressing need to anticipate that.
18    #[error("References were not supported as much as requested")]
19    InvalidReference(),
20}
21
22/// compress_hash takes an arbitrarily long sequence of bytes (usually
23/// a hash digest), and returns a sequence of bytes of length
24/// OUTPUT_SIZE.
25///
26/// It's calculated by rotating through the bytes in the output buffer
27/// (zero- initialized), and XOR'ing with each byte of the passed
28/// input. It consumes 1 byte at a time, and XOR's it with the current
29/// value in the output buffer.
30///
31/// This mimics equivalent functionality in C++ Nix.
32pub fn compress_hash<const OUTPUT_SIZE: usize>(input: &[u8]) -> [u8; OUTPUT_SIZE] {
33    let mut output = [0; OUTPUT_SIZE];
34
35    for (ii, ch) in input.iter().enumerate() {
36        output[ii % OUTPUT_SIZE] ^= ch;
37    }
38
39    output
40}
41
42/// This builds a store path, by calculating the text_hash_string of either a
43/// derivation or a literal text file that may contain references.
44/// If you don't want to have to pass the entire contents, you might want to use
45/// [build_ca_path] instead.
46pub fn build_text_path<'a, S, SP, I, C>(
47    name: &'a str,
48    content: C,
49    references: I,
50) -> Result<StorePath<SP>, BuildStorePathError>
51where
52    S: AsRef<str>,
53    SP: AsRef<str> + std::convert::From<&'a str>,
54    I: IntoIterator<Item = S>,
55    C: AsRef<[u8]>,
56{
57    // produce the sha256 digest of the contents
58    let content_digest = Sha256::new_with_prefix(content).finalize().into();
59
60    build_ca_path(name, &CAHash::Text(content_digest), references, false)
61}
62
63/// This builds a store path from a [CAHash] and a list of references.
64pub fn build_ca_path<'a, S, SP, I>(
65    name: &'a str,
66    ca_hash: &CAHash,
67    references: I,
68    self_reference: bool,
69) -> Result<StorePath<SP>, BuildStorePathError>
70where
71    S: AsRef<str>,
72    SP: AsRef<str> + std::convert::From<&'a str>,
73    I: IntoIterator<Item = S>,
74{
75    // self references are only allowed for CAHash::Nar(NixHash::Sha256(_)).
76    if self_reference && matches!(ca_hash, CAHash::Nar(NixHash::Sha256(_))) {
77        return Err(BuildStorePathError::InvalidReference());
78    }
79
80    /// Helper function, used for the non-sha256 [CAHash::Nar] and all [CAHash::Flat].
81    fn fixed_out_digest(prefix: &str, hash: &NixHash) -> [u8; 32] {
82        Sha256::new_with_prefix(format!("{}:{}:", prefix, hash.to_nix_hex_string()))
83            .finalize()
84            .into()
85    }
86
87    let (ty, inner_digest) = match &ca_hash {
88        CAHash::Text(ref digest) => (make_references_string("text", references, false), *digest),
89        CAHash::Nar(NixHash::Sha256(ref digest)) => (
90            make_references_string("source", references, self_reference),
91            *digest,
92        ),
93
94        // for all other CAHash::Nar, another custom scheme is used.
95        CAHash::Nar(ref hash) => {
96            if references.into_iter().next().is_some() {
97                return Err(BuildStorePathError::InvalidReference());
98            }
99
100            (
101                "output:out".to_string(),
102                fixed_out_digest("fixed:out:r", hash),
103            )
104        }
105        // CaHash::Flat is using something very similar, except the `r:` prefix.
106        CAHash::Flat(ref hash) => {
107            if references.into_iter().next().is_some() {
108                return Err(BuildStorePathError::InvalidReference());
109            }
110
111            (
112                "output:out".to_string(),
113                fixed_out_digest("fixed:out", hash),
114            )
115        }
116    };
117
118    build_store_path_from_fingerprint_parts(&ty, &inner_digest, name)
119        .map_err(BuildStorePathError::InvalidStorePath)
120}
121
122/// This builds an input-addressed store path.
123///
124/// Input-addresed store paths are always derivation outputs, the "input" in question is the
125/// derivation and its closure.
126pub fn build_output_path<'a, SP>(
127    drv_sha256: &[u8; 32],
128    output_name: &str,
129    output_path_name: &'a str,
130) -> Result<StorePath<SP>, Error>
131where
132    SP: AsRef<str> + std::convert::From<&'a str>,
133{
134    build_store_path_from_fingerprint_parts(
135        &(String::from("output:") + output_name),
136        drv_sha256,
137        output_path_name,
138    )
139}
140
141/// This builds a store path from fingerprint parts.
142/// Usually, that function is used from [build_text_path] and
143/// passed a "text hash string" (starting with "text:" as fingerprint),
144/// but other fingerprints starting with "output:" are also used in Derivation
145/// output path calculation.
146///
147/// The fingerprint is hashed with sha256, and its digest is compressed to 20
148/// bytes.
149/// Inside a StorePath, that digest is printed nixbase32-encoded
150/// (32 characters).
151fn build_store_path_from_fingerprint_parts<'a, SP>(
152    ty: &str,
153    inner_digest: &[u8; 32],
154    name: &'a str,
155) -> Result<StorePath<SP>, Error>
156where
157    SP: AsRef<str> + std::convert::From<&'a str>,
158{
159    let fingerprint = format!(
160        "{ty}:sha256:{}:{STORE_DIR}:{name}",
161        HEXLOWER.encode(inner_digest)
162    );
163    // name validation happens in here.
164    StorePath::from_name_and_digest_fixed(
165        name,
166        compress_hash(&Sha256::new_with_prefix(fingerprint).finalize()),
167    )
168}
169
170/// This contains the Nix logic to create "text hash strings", which are used
171/// in `builtins.toFile`, as well as in Derivation Path calculation.
172///
173/// A text hash is calculated by concatenating the following fields, separated by a `:`:
174///
175///  - text
176///  - references, individually joined by `:`
177///  - the nix_hash_string representation of the sha256 digest of some contents
178///  - the value of `storeDir`
179///  - the name
180fn make_references_string<S: AsRef<str>, I: IntoIterator<Item = S>>(
181    ty: &str,
182    references: I,
183    self_ref: bool,
184) -> String {
185    let mut s = String::from(ty);
186
187    for reference in references {
188        s.push(':');
189        s.push_str(reference.as_ref());
190    }
191
192    if self_ref {
193        s.push_str(":self");
194    }
195
196    s
197}
198
199/// Nix placeholders (i.e. values returned by `builtins.placeholder`)
200/// are used to populate outputs with paths that must be
201/// string-replaced with the actual placeholders later, at runtime.
202///
203/// The actual placeholder is basically just a SHA256 hash encoded in
204/// cppnix format.
205pub fn hash_placeholder(name: &str) -> String {
206    let digest = Sha256::new_with_prefix(format!("nix-output:{}", name)).finalize();
207
208    format!("/{}", nixbase32::encode(&digest))
209}
210
211#[cfg(test)]
212mod test {
213    use hex_literal::hex;
214
215    use super::*;
216    use crate::{
217        nixhash::{CAHash, NixHash},
218        store_path::StorePathRef,
219    };
220
221    #[test]
222    fn build_text_path_with_zero_references() {
223        // This hash should match `builtins.toFile`, e.g.:
224        //
225        // nix-repl> builtins.toFile "foo" "bar"
226        // "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
227
228        let store_path: StorePathRef = build_text_path("foo", "bar", Vec::<String>::new())
229            .expect("build_store_path() should succeed");
230
231        assert_eq!(
232            store_path.to_absolute_path().as_str(),
233            "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
234        );
235    }
236
237    #[test]
238    fn build_text_path_with_non_zero_references() {
239        // This hash should match:
240        //
241        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
242        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
243
244        let inner: StorePathRef = build_text_path("foo", "bar", Vec::<String>::new())
245            .expect("path_with_references() should succeed");
246        let inner_path = inner.to_absolute_path();
247
248        let outer: StorePathRef = build_text_path("baz", &inner_path, vec![inner_path.as_str()])
249            .expect("path_with_references() should succeed");
250
251        assert_eq!(
252            outer.to_absolute_path().as_str(),
253            "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
254        );
255    }
256
257    #[test]
258    fn build_sha1_path() {
259        let outer: StorePathRef = build_ca_path(
260            "bar",
261            &CAHash::Nar(NixHash::Sha1(hex!(
262                "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
263            ))),
264            Vec::<String>::new(),
265            false,
266        )
267        .expect("path_with_references() should succeed");
268
269        assert_eq!(
270            outer.to_absolute_path().as_str(),
271            "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"
272        );
273    }
274
275    #[test]
276    fn build_store_path_with_non_zero_references() {
277        // This hash should match:
278        //
279        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
280        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
281        //
282        // $ nix store make-content-addressed /nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz
283        // rewrote '/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz' to '/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz'
284        let outer: StorePathRef = build_ca_path(
285            "baz",
286            &CAHash::Nar(NixHash::Sha256(
287                nixbase32::decode(b"1xqkzcb3909fp07qngljr4wcdnrh1gdam1m2n29i6hhrxlmkgkv1")
288                    .expect("nixbase32 should decode")
289                    .try_into()
290                    .expect("should have right len"),
291            )),
292            vec!["/nix/store/dxwkwjzdaq7ka55pkk252gh32bgpmql4-foo"],
293            false,
294        )
295        .expect("path_with_references() should succeed");
296
297        assert_eq!(
298            outer.to_absolute_path().as_str(),
299            "/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz"
300        );
301    }
302}