Skip to main content

nix_compat/store_path/
utils.rs

1use crate::nixbase32;
2use crate::nixhash::{CAHash, NixHash};
3use crate::store_path::{Error, STORE_DIR, StorePath, StorePathRef};
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, SP, C>(
47    name: &'a str,
48    content: C,
49    references: impl IntoIterator<Item = StorePathRef<'a>>,
50) -> Result<StorePath<SP>, BuildStorePathError>
51where
52    SP: AsRef<str> + std::convert::From<&'a str>,
53    C: AsRef<[u8]>,
54{
55    // produce the sha256 digest of the contents
56    let content_digest = Sha256::digest(content.as_ref()).into();
57
58    build_ca_path(name, &CAHash::Text(content_digest), references, false)
59}
60
61/// This builds a store path from a [CAHash] and a list of references.
62pub fn build_ca_path<'a, SP>(
63    name: &'a str,
64    ca_hash: &CAHash,
65    references: impl IntoIterator<Item = StorePathRef<'a>>,
66    self_reference: bool,
67) -> Result<StorePath<SP>, BuildStorePathError>
68where
69    SP: AsRef<str> + std::convert::From<&'a str>,
70{
71    // self references are only allowed for CAHash::Nar(NixHash::Sha256(_)).
72    if self_reference && matches!(ca_hash, CAHash::Nar(NixHash::Sha256(_))) {
73        return Err(BuildStorePathError::InvalidReference());
74    }
75
76    /// Helper function, used for the non-sha256 [CAHash::Nar] and all [CAHash::Flat].
77    fn fixed_out_digest(prefix: &str, hash: &NixHash) -> [u8; 32] {
78        sha256!("{}:{}:", prefix, hash.to_nix_lowerhex_string())
79    }
80
81    let (ty, inner_digest) = match &ca_hash {
82        CAHash::Text(digest) => (make_references_string("text", references, false), *digest),
83        CAHash::Nar(NixHash::Sha256(digest)) => (
84            make_references_string("source", references, self_reference),
85            *digest,
86        ),
87
88        // for all other CAHash::Nar, another custom scheme is used.
89        CAHash::Nar(hash) => {
90            if references.into_iter().next().is_some() {
91                return Err(BuildStorePathError::InvalidReference());
92            }
93
94            (
95                "output:out".to_string(),
96                fixed_out_digest("fixed:out:r", hash),
97            )
98        }
99        // CaHash::Flat is using something very similar, except the `r:` prefix.
100        CAHash::Flat(hash) => {
101            if references.into_iter().next().is_some() {
102                return Err(BuildStorePathError::InvalidReference());
103            }
104
105            (
106                "output:out".to_string(),
107                fixed_out_digest("fixed:out", hash),
108            )
109        }
110    };
111
112    build_store_path_from_fingerprint_parts(&ty, &inner_digest, name)
113        .map_err(BuildStorePathError::InvalidStorePath)
114}
115
116/// This builds an input-addressed store path.
117///
118/// Input-addresed store paths are always derivation outputs, the "input" in question is the
119/// derivation and its closure.
120pub fn build_output_path<'a, SP>(
121    drv_sha256: &[u8; 32],
122    output_name: &str,
123    output_path_name: &'a str,
124) -> Result<StorePath<SP>, Error>
125where
126    SP: AsRef<str> + std::convert::From<&'a str>,
127{
128    build_store_path_from_fingerprint_parts(
129        &(String::from("output:") + output_name),
130        drv_sha256,
131        output_path_name,
132    )
133}
134
135/// This builds a store path from fingerprint parts.
136/// Usually, that function is used from [build_text_path] and
137/// passed a "text hash string" (starting with "text:" as fingerprint),
138/// but other fingerprints starting with "output:" are also used in Derivation
139/// output path calculation.
140///
141/// The fingerprint is hashed with sha256, and its digest is compressed to 20
142/// bytes.
143/// Inside a StorePath, that digest is printed nixbase32-encoded
144/// (32 characters).
145fn build_store_path_from_fingerprint_parts<'a, SP>(
146    ty: &str,
147    inner_digest: &[u8; 32],
148    name: &'a str,
149) -> Result<StorePath<SP>, Error>
150where
151    SP: AsRef<str> + std::convert::From<&'a str>,
152{
153    let fingerprint_hash = sha256!(
154        "{ty}:sha256:{}:{STORE_DIR}:{name}",
155        HEXLOWER.encode(inner_digest)
156    );
157    // name validation happens in here.
158    StorePath::from_name_and_digest_fixed(name, compress_hash(&fingerprint_hash))
159}
160
161/// This contains the Nix logic to create "text hash strings", which are used
162/// in `builtins.toFile`, as well as in Derivation Path calculation.
163///
164/// A text hash is calculated by concatenating the following fields, separated by a `:`:
165///
166///  - text
167///  - references, individually joined by `:`
168///  - the nix_hash_string representation of the sha256 digest of some contents
169///  - the value of `storeDir`
170///  - the name
171fn make_references_string<'a>(
172    ty: &str,
173    references: impl IntoIterator<Item = StorePathRef<'a>>,
174    self_ref: bool,
175) -> String {
176    let mut s = String::from(ty);
177    use std::fmt::Write;
178
179    for reference in references {
180        s.push(':');
181        write!(&mut s, "{}", reference.as_absolute_path_fmt()).unwrap();
182    }
183
184    if self_ref {
185        s.push_str(":self");
186    }
187
188    s
189}
190
191/// Nix placeholders (i.e. values returned by `builtins.placeholder`)
192/// are used to populate outputs with paths that must be
193/// string-replaced with the actual placeholders later, at runtime.
194///
195/// The actual placeholder is basically just a SHA256 hash encoded in
196/// cppnix format.
197pub fn hash_placeholder(name: &str) -> String {
198    format!("/{}", nixbase32::encode(&sha256!("nix-output:{name}")))
199}
200
201#[cfg(test)]
202mod test {
203    use hex_literal::hex;
204
205    use super::*;
206    use crate::{
207        nixhash::{CAHash, NixHash},
208        store_path::StorePathRef,
209    };
210
211    #[test]
212    fn build_text_path_with_zero_references() {
213        // This hash should match `builtins.toFile`, e.g.:
214        //
215        // nix-repl> builtins.toFile "foo" "bar"
216        // "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
217
218        let store_path: StorePathRef =
219            build_text_path("foo", "bar", []).expect("build_store_path() should succeed");
220
221        assert_eq!(
222            store_path.to_absolute_path().as_str(),
223            "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
224        );
225    }
226
227    #[test]
228    fn build_text_path_with_non_zero_references() {
229        // This hash should match:
230        //
231        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
232        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
233
234        let inner: StorePathRef =
235            build_text_path("foo", "bar", []).expect("path_with_references() should succeed");
236
237        let outer: StorePathRef = build_text_path("baz", inner.to_absolute_path(), [inner])
238            .expect("path_with_references() should succeed");
239
240        assert_eq!(
241            outer.to_absolute_path().as_str(),
242            "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
243        );
244    }
245
246    #[test]
247    fn build_sha1_path() {
248        let outer: StorePathRef = build_ca_path(
249            "bar",
250            &CAHash::Nar(NixHash::Sha1(hex!(
251                "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
252            ))),
253            [],
254            false,
255        )
256        .expect("path_with_references() should succeed");
257
258        assert_eq!(
259            outer.to_absolute_path().as_str(),
260            "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"
261        );
262    }
263
264    #[test]
265    fn build_store_path_with_non_zero_references() {
266        // This hash should match:
267        //
268        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
269        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
270        //
271        // $ nix store make-content-addressed /nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz
272        // rewrote '/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz' to '/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz'
273        let outer: StorePathRef = build_ca_path(
274            "baz",
275            &CAHash::Nar(NixHash::Sha256(
276                nixbase32::decode(b"1xqkzcb3909fp07qngljr4wcdnrh1gdam1m2n29i6hhrxlmkgkv1")
277                    .expect("nixbase32 should decode")
278                    .try_into()
279                    .expect("should have right len"),
280            )),
281            [
282                StorePathRef::from_bytes(b"dxwkwjzdaq7ka55pkk252gh32bgpmql4-foo")
283                    .expect("to parse"),
284            ],
285            false,
286        )
287        .expect("path_with_references() should succeed");
288
289        assert_eq!(
290            outer.to_absolute_path().as_str(),
291            "/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz"
292        );
293    }
294}