snix_glue/builtins/
mod.rs

1//! Contains builtins that deal with the store or builder.
2
3use std::rc::Rc;
4
5use crate::snix_store_io::SnixStoreIO;
6
7mod derivation;
8mod errors;
9mod fetchers;
10mod import;
11mod utils;
12
13pub use errors::{DerivationError, FetcherError, ImportError};
14
15/// Adds derivation-related builtins to the passed [snix_eval::EvaluationBuilder]:
16///
17/// * `derivation`
18/// * `derivationStrict`
19/// * `toFile`
20///
21/// As they need to interact with `known_paths`, we also need to pass in
22/// `known_paths`.
23pub fn add_derivation_builtins<'co, 'ro, 'env, IO>(
24    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
25    io: Rc<SnixStoreIO>,
26) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
27    eval_builder
28        .add_builtins(derivation::derivation_builtins::builtins(Rc::clone(&io)))
29        // Add the actual `builtins.derivation` from compiled Nix code
30        .add_src_builtin("derivation", include_str!("derivation.nix"))
31}
32
33/// Adds fetcher builtins to the passed [snix_eval::EvaluationBuilder]:
34///
35/// * `fetchurl`
36/// * `fetchTarball`
37/// * `fetchGit`
38pub fn add_fetcher_builtins<'co, 'ro, 'env, IO>(
39    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
40    io: Rc<SnixStoreIO>,
41) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
42    eval_builder.add_builtins(fetchers::fetcher_builtins::builtins(Rc::clone(&io)))
43}
44
45/// Adds import-related builtins to the passed [snix_eval::EvaluationBuilder]:
46///
47///
48/// * `filterSource`
49/// * `path`
50/// * `storePath`
51///
52/// As they need to interact with the store implementation, we pass [`SnixStoreIO`].
53/// Due to #176, some IO still sidesteps `EvalIO` and accesses the filesystem directly.
54pub fn add_import_builtins<'co, 'ro, 'env, IO>(
55    eval_builder: snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
56    io: Rc<SnixStoreIO>,
57) -> snix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
58    eval_builder.add_builtins(import::import_builtins(io))
59}
60
61#[cfg(test)]
62mod tests {
63    use std::{fs, rc::Rc, sync::Arc};
64
65    use crate::snix_store_io::SnixStoreIO;
66
67    use super::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins};
68    use clap::Parser;
69    use nix_compat::store_path::hash_placeholder;
70    use rstest::rstest;
71    use snix_build::buildservice::DummyBuildService;
72    use snix_eval::{EvalIO, EvaluationResult};
73    use snix_store::utils::{ServiceUrlsMemory, construct_services};
74    use tempfile::TempDir;
75
76    /// evaluates a given nix expression and returns the result.
77    /// Takes care of setting up the evaluator so it knows about the
78    // `derivation` builtin.
79    fn eval(str: &str) -> EvaluationResult {
80        // We assemble a complete store in memory.
81        let runtime = tokio::runtime::Runtime::new().expect("Failed to build a Tokio runtime");
82        let (blob_service, directory_service, path_info_service, nar_calculation_service) = runtime
83            .block_on(async {
84                construct_services(ServiceUrlsMemory::parse_from(std::iter::empty::<&str>())).await
85            })
86            .expect("Failed to construct store services in memory");
87
88        let io = Rc::new(SnixStoreIO::new(
89            blob_service,
90            directory_service,
91            path_info_service,
92            nar_calculation_service.into(),
93            Arc::<DummyBuildService>::default(),
94            runtime.handle().clone(),
95            Vec::new(),
96        ));
97
98        let mut eval_builder = snix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>);
99        eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
100        eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
101        eval_builder = add_import_builtins(eval_builder, io);
102        let eval = eval_builder.build();
103
104        // run the evaluation itself.
105        eval.evaluate(str, None)
106    }
107
108    #[test]
109    fn derivation() {
110        let result = eval(
111            r#"(derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux";}).outPath"#,
112        );
113
114        assert!(result.errors.is_empty(), "expect evaluation to succeed");
115        let value = result.value.expect("must be some");
116
117        match value {
118            snix_eval::Value::String(s) => {
119                assert_eq!(*s, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo",);
120            }
121            _ => panic!("unexpected value type: {value:?}"),
122        }
123    }
124
125    /// a derivation with an empty name is an error.
126    #[test]
127    fn derivation_empty_name_fail() {
128        let result = eval(
129            r#"(derivation { name = ""; builder = "/bin/sh"; system = "x86_64-linux";}).outPath"#,
130        );
131
132        assert!(!result.errors.is_empty(), "expect evaluation to fail");
133    }
134
135    /// construct some calls to builtins.derivation and compare produced output
136    /// paths.
137    #[rstest]
138    #[case::r_sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
139    #[case::r_sha256_other_name(r#"(builtins.derivation { name = "foo2"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/gi0p8vd635vpk1nq029cz3aa3jkhar5k-foo2")]
140    #[case::r_sha1(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha1"; outputHash = "sha1-VUCRC+16gU5lcrLYHlPSUyx0Y/Q="; }).outPath"#, "/nix/store/p5sammmhpa84ama7ymkbgwwzrilva24x-foo")]
141    #[case::r_md5(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "md5"; outputHash = "md5-07BzhNET7exJ6qYjitX/AA=="; }).outPath"#, "/nix/store/gmmxgpy1jrzs86r5y05wy6wiy2m15xgi-foo")]
142    #[case::r_sha512(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha512"; outputHash = "sha512-DPkYCnZKuoY6Z7bXLwkYvBMcZ3JkLLLc5aNPCnAvlHDdwr8SXBIZixmVwjPDS0r9NGxUojNMNQqUilG26LTmtg=="; }).outPath"#, "/nix/store/lfi2bfyyap88y45mfdwi4j99gkaxaj19-foo")]
143    #[case::r_sha256_base16(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "4374173a8cbe88de152b609f96f46e958bcf65762017474eec5a05ec2bd61530"; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
144    #[case::r_sha256_nixbase32(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "0c0msqmyq1asxi74f5r0frjwz2wmdvs9d7v05caxx25yihx1fx23"; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
145    #[case::r_sha256_base64(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
146    #[case::r_sha256_base64_nopad(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8="; }).outPath"#, "/nix/store/xm1l9dx4zgycv9qdhcqqvji1z88z534b-foo")]
147    #[case::sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/q4pkwkxdib797fhk22p0k3g1q32jmxvf-foo")]
148    #[case::sha256_other_name(r#"(builtins.derivation { name = "foo2"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha256"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/znw17xlmx9r6gw8izjkqxkl6s28sza4l-foo2")]
149    #[case::sha1(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha1"; outputHash = "sha1-VUCRC+16gU5lcrLYHlPSUyx0Y/Q="; }).outPath"#, "/nix/store/zgpnjjmga53d8srp8chh3m9fn7nnbdv6-foo")]
150    #[case::md5(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "md5"; outputHash = "md5-07BzhNET7exJ6qYjitX/AA=="; }).outPath"#, "/nix/store/jfhcwnq1852ccy9ad9nakybp2wadngnd-foo")]
151    #[case::sha512(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "flat"; outputHashAlgo = "sha512"; outputHash = "sha512-DPkYCnZKuoY6Z7bXLwkYvBMcZ3JkLLLc5aNPCnAvlHDdwr8SXBIZixmVwjPDS0r9NGxUojNMNQqUilG26LTmtg=="; }).outPath"#, "/nix/store/as736rr116ian9qzg457f96j52ki8bm3-foo")]
152    #[case::r_sha256_outputhashalgo_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/17wgs52s7kcamcyin4ja58njkf91ipq8-foo")]
153    #[case::r_sha256_outputhashalgo_and_outputhashmode_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#, "/nix/store/q4pkwkxdib797fhk22p0k3g1q32jmxvf-foo")]
154    #[case::outputhash_omitted(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; }).outPath"#, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo")]
155    #[case::multiple_outputs(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; outputs = ["foo" "bar"]; system = "x86_64-linux"; }).outPath"#, "/nix/store/hkwdinvz2jpzgnjy9lv34d2zxvclj4s3-foo-foo")]
156    #[case::args(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; args = ["--foo" "42" "--bar"]; system = "x86_64-linux"; }).outPath"#, "/nix/store/365gi78n2z7vwc1bvgb98k0a9cqfp6as-foo")]
157    #[case::full(r#"
158                   let
159                     bar = builtins.derivation {
160                       name = "bar";
161                       builder = ":";
162                       system = ":";
163                       outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba";
164                       outputHashAlgo = "sha256";
165                       outputHashMode = "recursive";
166                     };
167                   in
168                   (builtins.derivation {
169                     name = "foo";
170                     builder = ":";
171                     system = ":";
172                     inherit bar;
173                   }).outPath
174        "#, "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo")]
175    #[case::pass_as_file(r#"(builtins.derivation { "name" = "foo"; passAsFile = ["bar"]; bar = "baz"; system = ":"; builder = ":";}).outPath"#, "/nix/store/25gf0r1ikgmh4vchrn8qlc4fnqlsa5a1-foo")]
176    // __ignoreNulls = true, but nothing set to null
177    #[case::ignore_nulls_true_no_arg_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
178    #[case::ignore_nulls_true_no_arg_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
179    // __ignoreNulls = true, with a null arg, same paths
180    #[case::ignore_nulls_true_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; ignoreme = null; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
181    #[case::ignore_nulls_true_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = true; ignoreme = null; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
182    // __ignoreNulls = false
183    #[case::ignore_nulls_false_no_arg_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; }).drvPath"#, "/nix/store/xa96w6d7fxrlkk60z1fmx2ffp2wzmbqx-foo.drv")]
184    #[case::ignore_nulls_false_no_arg_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; }).outPath"#, "/nix/store/pk2agn9za8r9bxsflgh1y7fyyrmwcqkn-foo")]
185    // __ignoreNulls = false, with a null arg
186    #[case::ignore_nulls_fales_arg_path_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; }).drvPath"#, "/nix/store/xwkwbajfiyhdqmksrbzm0s4g4ib8d4ms-foo.drv")]
187    #[case::ignore_nulls_fales_arg_path_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; }).outPath"#, "/nix/store/2n2jqm6l7r2ahi19m58pl896ipx9cyx6-foo")]
188    // structured attrs set to false will render an empty string inside env
189    #[case::structured_attrs_false_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = false; foo = "bar"; }).drvPath"#, "/nix/store/qs39krwr2lsw6ac910vqx4pnk6m63333-foo.drv")]
190    #[case::structured_attrs_false_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = false; foo = "bar"; }).outPath"#, "/nix/store/9yy3764rdip3fbm8ckaw4j9y7vh4d231-foo")]
191    // simple structured attrs
192    #[case::structured_attrs_simple_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; }).drvPath"#, "/nix/store/k6rlb4k10cb9iay283037ml1nv3xma2f-foo.drv")]
193    #[case::structured_attrs_simple_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; }).outPath"#, "/nix/store/6lmv3hyha1g4cb426iwjyifd7nrdv1xn-foo")]
194    // structured attrs with outputsCheck
195    #[case::structured_attrs_output_checks_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; outputChecks = {out = {maxClosureSize = 256 * 1024 * 1024; disallowedRequisites = [ "dev" ];};}; }).drvPath"#, "/nix/store/fx9qzpchh5wchchhy39bwsml978d6wp1-foo.drv")]
196    #[case::structured_attrs_output_checks_outpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; outputChecks = {out = {maxClosureSize = 256 * 1024 * 1024; disallowedRequisites = [ "dev" ];};}; }).outPath"#, "/nix/store/pcywah1nwym69rzqdvpp03sphfjgyw1l-foo")]
197    // structured attrs and __ignoreNulls. ignoreNulls is inactive (so foo ends up in __json, yet __ignoreNulls itself is not present.
198    #[case::structured_attrs_and_ignore_nulls_drvpath(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __ignoreNulls = false; foo = null; __structuredAttrs = true; }).drvPath"#, "/nix/store/rldskjdcwa3p7x5bqy3r217va1jsbjsc-foo.drv")]
199    // structured attrs, setting outputs.
200    #[case::structured_attrs_outputs_drvpath(r#"(builtins.derivation { name = "test"; system = "aarch64-linux"; builder = "/bin/sh"; __structuredAttrs = true; outputs = [ "out"]; }).drvPath"#, "/nix/store/6sgawp30zibsh525p7c948xxd22y2ngy-test.drv")]
201    // structured attrs, setting __json, which will show up as an encoded __json key inside the __json.
202    #[case::structured_attrs_json(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; __structuredAttrs = true; foo = "bar"; __json = "foo";}).drvPath"#, "/nix/store/98yvz8z0i6kzdcsv6zq8cv60dd784yxf-foo.drv")]
203    fn test_drvpath(#[case] code: &str, #[case] expected_path: &str) {
204        let value = eval(code).value.expect("must succeed");
205
206        match value {
207            snix_eval::Value::String(s) => {
208                assert_eq!(*s, expected_path);
209            }
210            _ => panic!("unexpected value type: {value:?}"),
211        }
212    }
213
214    /// construct some calls to builtins.derivation that should be rejected
215    #[rstest]
216    #[case::invalid_outputhash(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-00"; }).outPath"#)]
217    #[case::sha1_and_sha256(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha1"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath"#)]
218    #[case::duplicate_output_names(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; outputs = ["foo" "foo"]; system = "x86_64-linux"; }).outPath"#)]
219    #[case::unstructured_attrs_json(r#"(builtins.derivation { name = "foo"; system = ":"; builder = ":"; foo = "bar"; __json = "foo";}).drvPath"#)]
220    fn test_invalid(#[case] code: &str) {
221        let resp = eval(code);
222        assert!(resp.value.is_none(), "Value should be None");
223        assert!(
224            !resp.errors.is_empty(),
225            "There should have been some errors"
226        );
227    }
228
229    /// Construct two FODs with the same name, and same known output (but
230    /// slightly different recipe), ensure they have the same output hash.
231    #[test]
232    fn test_fod_outpath() {
233        let code = r#"
234          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath ==
235          (builtins.derivation { name = "foo"; builder = "/bin/aa"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath
236        "#;
237
238        let value = eval(code).value.expect("must succeed");
239        match value {
240            snix_eval::Value::Bool(v) => {
241                assert!(v);
242            }
243            _ => panic!("unexpected value type: {value:?}"),
244        }
245    }
246
247    /// Construct two FODs with the same name, and same known output (but
248    /// slightly different recipe), ensure they have the same output hash.
249    #[test]
250    fn test_fod_outpath_different_name() {
251        let code = r#"
252          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath ==
253          (builtins.derivation { name = "foo"; builder = "/bin/aa"; system = "x86_64-linux"; outputHash = "sha256-Q3QXOoy+iN4VK2CflvRulYvPZXYgF0dO7FoF7CvWFTA="; }).outPath
254        "#;
255
256        let value = eval(code).value.expect("must succeed");
257        match value {
258            snix_eval::Value::Bool(v) => {
259                assert!(v);
260            }
261            _ => panic!("unexpected value type: {value:?}"),
262        }
263    }
264
265    /// Construct two derivations with the same parameters except one of them lost a context string
266    /// for a dependency, causing the loss of an element in the `inputDrvs` derivation. Therefore,
267    /// making `outPath` different.
268    #[test]
269    fn test_unsafe_discard_string_context() {
270        let code = r#"
271        let
272            dep = builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; };
273        in
274          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; env = "${dep}"; }).outPath !=
275          (builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; env = "${builtins.unsafeDiscardStringContext dep}"; }).outPath
276        "#;
277
278        let value = eval(code).value.expect("must succeed");
279        match value {
280            snix_eval::Value::Bool(v) => {
281                assert!(v);
282            }
283            _ => panic!("unexpected value type: {value:?}"),
284        }
285    }
286
287    /// Construct an attribute set that coerces to a derivation and verify that the return type is
288    /// a string.
289    #[test]
290    fn test_unsafe_discard_string_context_of_coercible() {
291        let code = r#"
292        let
293            dep = builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; };
294            attr = { __toString = _: dep; };
295        in
296            builtins.typeOf (builtins.unsafeDiscardStringContext attr) == "string"
297        "#;
298
299        let value = eval(code).value.expect("must succeed");
300        match value {
301            snix_eval::Value::Bool(v) => {
302                assert!(v);
303            }
304            _ => panic!("unexpected value type: {value:?}"),
305        }
306    }
307
308    #[rstest]
309    #[case::input_in_args(r#"
310                   let
311                     bar = builtins.derivation {
312                       name = "bar";
313                       builder = ":";
314                       system = ":";
315                       outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba";
316                       outputHashAlgo = "sha256";
317                       outputHashMode = "recursive";
318                     };
319                   in
320                   (builtins.derivation {
321                     name = "foo";
322                     builder = ":";
323                     args = [ "${bar}" ];
324                     system = ":";
325                   }).drvPath
326        "#, "/nix/store/50yl2gmmljyl0lzyrp1mcyhn53vhjhkd-foo.drv")]
327    fn test_inputs_derivation_from_context(#[case] code: &str, #[case] expected_drvpath: &str) {
328        let eval_result = eval(code);
329
330        let value = eval_result.value.expect("must succeed");
331
332        match value {
333            snix_eval::Value::String(s) => {
334                assert_eq!(*s, expected_drvpath);
335            }
336
337            _ => panic!("unexpected value type: {value:?}"),
338        };
339    }
340
341    #[test]
342    fn builtins_placeholder_hashes() {
343        assert_eq!(
344            hash_placeholder("out").as_str(),
345            "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"
346        );
347
348        assert_eq!(
349            hash_placeholder("").as_str(),
350            "/171rf4jhx57xqz3p7swniwkig249cif71pa08p80mgaf0mqz5bmr"
351        );
352    }
353
354    /// constructs calls to builtins.derivation that should succeed, but produce warnings
355    #[rstest]
356    #[case::r_sha256_wrong_padding(r#"(builtins.derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; outputHash = "sha256-fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8===="; }).outPath"#, "/nix/store/xm1l9dx4zgycv9qdhcqqvji1z88z534b-foo")]
357    fn builtins_derivation_hash_wrong_padding_warn(
358        #[case] code: &str,
359        #[case] expected_path: &str,
360    ) {
361        let eval_result = eval(code);
362
363        let value = eval_result.value.expect("must succeed");
364
365        match value {
366            snix_eval::Value::String(s) => {
367                assert_eq!(*s, expected_path);
368            }
369            _ => panic!("unexpected value type: {value:?}"),
370        }
371
372        assert!(
373            !eval_result.warnings.is_empty(),
374            "warnings should not be empty"
375        );
376    }
377
378    /// Invokes `builtins.filterSource` on various carefully-crated subdirs, and
379    /// ensures the resulting store paths matches what Nix produces.
380    /// @fixtures is replaced to the fixtures directory.
381    #[rstest]
382    #[cfg(target_family = "unix")]
383    #[case::complicated_filter_nothing(
384        r#"(builtins.filterSource (p: t: true) @fixtures)"#,
385        "/nix/store/bqh6kd0x3vps2rzagzpl7qmbbgnx19cp-import_fixtures"
386    )]
387    #[case::complicated_filter_everything(
388        r#"(builtins.filterSource (p: t: false) @fixtures)"#,
389        "/nix/store/giq6czz24lpjg97xxcxk6rg950lcpib1-import_fixtures"
390    )]
391    #[case::simple_dir_with_one_file_filter_dirs(
392        r#"(builtins.filterSource (p: t: t != "directory") @fixtures/a_dir)"#,
393        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
394    )]
395    #[case::simple_dir_with_one_file_filter_files(
396        r#"(builtins.filterSource (p: t: t != "regular") @fixtures/a_dir)"#,
397        "/nix/store/zphlqc93s2iq4xm393l06hzf8hp85r4z-a_dir"
398    )]
399    #[case::simple_dir_with_one_file_filter_symlinks(
400        r#"(builtins.filterSource (p: t: t != "symlink") @fixtures/a_dir)"#,
401        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
402    )]
403    #[case::simple_dir_with_one_file_filter_nothing(
404        r#"(builtins.filterSource (p: t: true) @fixtures/a_dir)"#,
405        "/nix/store/8vbqaxapywkvv1hacdja3pi075r14d43-a_dir"
406    )]
407    #[case::simple_dir_with_one_file_filter_everything(
408        r#"(builtins.filterSource (p: t: false) @fixtures/a_dir)"#,
409        "/nix/store/zphlqc93s2iq4xm393l06hzf8hp85r4z-a_dir"
410    )]
411    #[case::simple_dir_with_one_dir_filter_dirs(
412        r#"builtins.filterSource (p: t: t != "directory") @fixtures/b_dir"#,
413        "/nix/store/xzsfzdgrxg93icaamjm8zq1jq6xvf2fz-b_dir"
414    )]
415    #[case::simple_dir_with_one_dir_filter_files(
416        r#"builtins.filterSource (p: t: t != "regular") @fixtures/b_dir"#,
417        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
418    )]
419    #[case::simple_dir_with_one_dir_filter_symlinks(
420        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/b_dir"#,
421        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
422    )]
423    #[case::simple_dir_with_one_dir_filter_nothing(
424        r#"builtins.filterSource (p: t: true) @fixtures/b_dir"#,
425        "/nix/store/8rjx64mm7173xp60rahv7cl3ixfkv3rf-b_dir"
426    )]
427    #[case::simple_dir_with_one_dir_filter_everything(
428        r#"builtins.filterSource (p: t: false) @fixtures/b_dir"#,
429        "/nix/store/xzsfzdgrxg93icaamjm8zq1jq6xvf2fz-b_dir"
430    )]
431    #[case::simple_dir_with_one_symlink_to_file_filter_dirs(
432        r#"builtins.filterSource (p: t: t != "directory") @fixtures/c_dir"#,
433        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
434    )]
435    #[case::simple_dir_with_one_symlink_to_file_filter_files(
436        r#"builtins.filterSource (p: t: t != "regular") @fixtures/c_dir"#,
437        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
438    )]
439    #[case::simple_dir_with_one_symlink_to_file_filter_symlinks(
440        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/c_dir"#,
441        "/nix/store/y5g1fz04vzjvf422q92qmv532axj5q26-c_dir"
442    )]
443    #[case::simple_dir_with_one_symlink_to_file_filter_nothing(
444        r#"builtins.filterSource (p: t: true) @fixtures/c_dir"#,
445        "/nix/store/riigfmmzzrq65zqiffcjk5sbqr9c9h09-c_dir"
446    )]
447    #[case::simple_dir_with_one_symlink_to_file_filter_everything(
448        r#"builtins.filterSource (p: t: false) @fixtures/c_dir"#,
449        "/nix/store/y5g1fz04vzjvf422q92qmv532axj5q26-c_dir"
450    )]
451    #[case::simple_dir_with_dangling_symlink_filter_dirs(
452        r#"builtins.filterSource (p: t: t != "directory") @fixtures/d_dir"#,
453        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
454    )]
455    #[case::simple_dir_with_dangling_symlink_filter_files(
456        r#"builtins.filterSource (p: t: t != "regular") @fixtures/d_dir"#,
457        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
458    )]
459    #[case::simple_dir_with_dangling_symlink_filter_symlinks(
460        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/d_dir"#,
461        "/nix/store/7l371xax8kknhpska4wrmyll1mzlhzvl-d_dir"
462    )]
463    #[case::simple_dir_with_dangling_symlink_filter_nothing(
464        r#"builtins.filterSource (p: t: true) @fixtures/d_dir"#,
465        "/nix/store/f2d1aixwiqy4lbzrd040ala2s4m2z199-d_dir"
466    )]
467    #[case::simple_dir_with_dangling_symlink_filter_everything(
468        r#"builtins.filterSource (p: t: false) @fixtures/d_dir"#,
469        "/nix/store/7l371xax8kknhpska4wrmyll1mzlhzvl-d_dir"
470    )]
471    #[case::simple_symlinked_dir_with_one_file_filter_dirs(
472        r#"builtins.filterSource (p: t: t != "directory") @fixtures/symlink_to_a_dir"#,
473        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
474    )]
475    #[case::simple_symlinked_dir_with_one_file_filter_files(
476        r#"builtins.filterSource (p: t: t != "regular") @fixtures/symlink_to_a_dir"#,
477        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
478    )]
479    #[case::simple_symlinked_dir_with_one_file_filter_symlinks(
480        r#"builtins.filterSource (p: t: t != "symlink") @fixtures/symlink_to_a_dir"#,
481        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
482    )]
483    #[case::simple_symlinked_dir_with_one_file_filter_nothing(
484        r#"builtins.filterSource (p: t: true) @fixtures/symlink_to_a_dir"#,
485        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
486    )]
487    #[case::simple_symlinked_dir_with_one_file_filter_everything(
488        r#"builtins.filterSource (p: t: false) @fixtures/symlink_to_a_dir"#,
489        "/nix/store/apmdprm8fwl2zrjpbyfcd99zrnhvf47q-symlink_to_a_dir"
490    )]
491    fn builtins_filter_source_succeed(#[case] code: &str, #[case] expected_outpath: &str) {
492        // populate the fixtures dir
493        let temp = TempDir::new().expect("create temporary directory");
494        let p = temp.path().join("import_fixtures");
495
496        // create the fixtures directory.
497        // We produce them at runtime rather than shipping it inside the source
498        // tree, as git can't model certain things - like directories without any
499        // items.
500        {
501            fs::create_dir(&p).expect("creating import_fixtures");
502
503            // `/a_dir` contains an empty `a_file` file
504            fs::create_dir(p.join("a_dir")).expect("creating /a_dir");
505            fs::write(p.join("a_dir").join("a_file"), "").expect("creating /a_dir/a_file");
506
507            // `/a_file` is an empty file
508            fs::write(p.join("a_file"), "").expect("creating /a_file");
509
510            // `/b_dir` contains an empty "a_dir" directory
511            fs::create_dir_all(p.join("b_dir").join("a_dir")).expect("creating /b_dir/a_dir");
512
513            // `/c_dir` contains a `symlink_to_a_file` symlink, pointing to `../a_dir/a_file`.
514            fs::create_dir(p.join("c_dir")).expect("creating /c_dir");
515            std::os::unix::fs::symlink(
516                "../a_dir/a_file",
517                p.join("c_dir").join("symlink_to_a_file"),
518            )
519            .expect("creating /c_dir/symlink_to_a_file");
520
521            // `/d_dir` contains a `dangling_symlink`, pointing to `a_dir/a_file`,
522            // which does not exist.
523            fs::create_dir(p.join("d_dir")).expect("creating /d_dir");
524            std::os::unix::fs::symlink("a_dir/a_file", p.join("d_dir").join("dangling_symlink"))
525                .expect("creating /d_dir/dangling_symlink");
526
527            // `/symlink_to_a_dir` is a symlink to `a_dir`, which exists.
528            std::os::unix::fs::symlink("a_dir", p.join("symlink_to_a_dir"))
529                .expect("creating /symlink_to_a_dir");
530        }
531
532        // replace @fixtures with the temporary path containing the fixtures
533        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
534
535        let eval_result = eval(&code_replaced);
536
537        let value = eval_result.value.expect("must succeed");
538
539        match value {
540            snix_eval::Value::String(s) => {
541                assert_eq!(expected_outpath, s.as_bstr());
542            }
543            _ => panic!("unexpected value type: {value:?}"),
544        }
545
546        assert!(eval_result.errors.is_empty(), "errors should be empty");
547    }
548
549    /// Space is an illegal character, but if we specify a name without spaces, it's ok.
550    #[rstest]
551    #[case::rename_success(
552        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; })"#,
553        true
554    )]
555    #[case::rename_with_spaces_fail(
556        r#"(builtins.path { name = "invalid name"; path = @fixtures + "/te st"; recursive = true; })"#,
557        false
558    )]
559    fn builtins_path_recursive_rename(#[case] code: &str, #[case] success: bool) {
560        // populate the fixtures dir
561        let temp = TempDir::new().expect("create temporary directory");
562        let p = temp.path().join("import_fixtures");
563
564        // create the fixtures directory.
565        // We produce them at runtime rather than shipping it inside the source
566        // tree, as git can't model certain things - like directories without any
567        // items.
568        {
569            fs::create_dir(&p).expect("creating import_fixtures");
570            fs::write(p.join("te st"), "").expect("creating `/te st`");
571        }
572        // replace @fixtures with the temporary path containing the fixtures
573        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
574
575        let eval_result = eval(&code_replaced);
576
577        let value = eval_result.value;
578
579        if success {
580            match value.expect("expected successful evaluation on legal rename") {
581                snix_eval::Value::String(s) => {
582                    assert_eq!(
583                        "/nix/store/nd5z11x7zjqqz44rkbhc6v7yifdkn659-valid-name",
584                        s.as_bstr()
585                    );
586                }
587                v => panic!("unexpected value type: {v:?}"),
588            }
589        } else {
590            assert!(value.is_none(), "unexpected success on illegal store paths");
591        }
592    }
593
594    /// Space is an illegal character, but if we specify a name without spaces, it's ok.
595    #[rstest]
596    #[case::rename_success(
597        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; })"#,
598        true
599    )]
600    #[case::rename_with_spaces_fail(
601        r#"(builtins.path { name = "invalid name"; path = @fixtures + "/te st"; recursive = false; })"#,
602        false
603    )]
604    // The non-recursive variant passes explicitly `recursive = false;`
605    fn builtins_path_nonrecursive_rename(#[case] code: &str, #[case] success: bool) {
606        // populate the fixtures dir
607        let temp = TempDir::new().expect("create temporary directory");
608        let p = temp.path().join("import_fixtures");
609
610        // create the fixtures directory.
611        // We produce them at runtime rather than shipping it inside the source
612        // tree, as git can't model certain things - like directories without any
613        // items.
614        {
615            fs::create_dir(&p).expect("creating import_fixtures");
616            fs::write(p.join("te st"), "").expect("creating `/te st`");
617        }
618        // replace @fixtures with the temporary path containing the fixtures
619        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
620
621        let eval_result = eval(&code_replaced);
622
623        let value = eval_result.value;
624
625        if success {
626            match value.expect("expected successful evaluation on legal rename") {
627                snix_eval::Value::String(s) => {
628                    assert_eq!(
629                        "/nix/store/il2rmfbqgs37rshr8w7x64hd4d3b4bsa-valid-name",
630                        s.as_bstr()
631                    );
632                }
633                v => panic!("unexpected value type: {v:?}"),
634            }
635        } else {
636            assert!(value.is_none(), "unexpected success on illegal store paths");
637        }
638    }
639
640    #[rstest]
641    #[case::flat_success(
642        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; sha256 = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; })"#,
643        true
644    )]
645    #[case::flat_fail(
646        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = false; sha256 = "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY="; })"#,
647        false
648    )]
649    #[case::recursive_success(
650        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; sha256 = "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY="; })"#,
651        true
652    )]
653    #[case::recursive_fail(
654        r#"(builtins.path { name = "valid-name"; path = @fixtures + "/te st"; recursive = true; sha256 = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; })"#,
655        false
656    )]
657    fn builtins_path_fod_locking(#[case] code: &str, #[case] exp_success: bool) {
658        // populate the fixtures dir
659        let temp = TempDir::new().expect("create temporary directory");
660        let p = temp.path().join("import_fixtures");
661
662        // create the fixtures directory.
663        // We produce them at runtime rather than shipping it inside the source
664        // tree, as git can't model certain things - like directories without any
665        // items.
666        {
667            fs::create_dir(&p).expect("creating import_fixtures");
668            fs::write(p.join("te st"), "").expect("creating `/te st`");
669        }
670        // replace @fixtures with the temporary path containing the fixtures
671        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
672
673        let eval_result = eval(&code_replaced);
674
675        let value = eval_result.value;
676
677        if exp_success {
678            assert!(
679                value.is_some(),
680                "expected successful evaluation on legal rename and valid FOD sha256"
681            );
682        } else {
683            assert!(value.is_none(), "unexpected success on invalid FOD sha256");
684        }
685    }
686
687    #[rstest]
688    #[case(
689        r#"(builtins.path { name = "valid-path"; path = @fixtures + "/te st dir"; filter = _: _: true; })"#,
690        "/nix/store/i28jmi4fwym4fw3flkrkp2mdxx50pdy0-valid-path"
691    )]
692    #[case(
693        r#"(builtins.path { name = "valid-path"; path = @fixtures + "/te st dir"; filter = _: _: false; })"#,
694        "/nix/store/pwza2ij9gk1fmzhbjnynmfv2mq2sgcap-valid-path"
695    )]
696    fn builtins_path_filter(#[case] code: &str, #[case] expected_outpath: &str) {
697        // populate the fixtures dir
698        let temp = TempDir::new().expect("create temporary directory");
699        let p = temp.path().join("import_fixtures");
700
701        // create the fixtures directory.
702        // We produce them at runtime rather than shipping it inside the source
703        // tree, as git can't model certain things - like directories without any
704        // items.
705        {
706            fs::create_dir(&p).expect("creating import_fixtures");
707            fs::create_dir(p.join("te st dir")).expect("creating `/te st dir`");
708            fs::write(p.join("te st dir").join("test"), "").expect("creating `/te st dir/test`");
709        }
710        // replace @fixtures with the temporary path containing the fixtures
711        let code_replaced = code.replace("@fixtures", &p.to_string_lossy());
712
713        let eval_result = eval(&code_replaced);
714
715        let value = eval_result.value.expect("must succeed");
716
717        match value {
718            snix_eval::Value::String(s) => {
719                assert_eq!(expected_outpath, s.as_bstr());
720            }
721            _ => panic!("unexpected value type: {value:?}"),
722        }
723
724        assert!(eval_result.errors.is_empty(), "errors should be empty");
725    }
726
727    // All tests filter out some unsupported (not representable in castore) nodes, confirming
728    // invalid, but filtered-out nodes don't prevent ingestion of a path.
729    #[rstest]
730    #[cfg(target_family = "unix")]
731    // There is a set of invalid filetypes.
732    // We write various filter functions filtering them out, but usually leaving
733    // some behind.
734    // In case there's still invalid filetypes left after the filtering, we
735    // expect the evaluation to fail.
736    #[case::fail_kept_unknowns(
737        r#"(builtins.filterSource (p: t: t == "unknown") @fixtures)"#,
738        false
739    )]
740    // We filter all invalid filetypes, so the evaluation has to succeed.
741    #[case::succeed_filter_unknowns(
742        r#"(builtins.filterSource (p: t: t != "unknown") @fixtures)"#,
743        true
744    )]
745    #[case::fail_kept_charnode(
746        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_charnode") @fixtures)"#,
747        false
748    )]
749    #[case::fail_kept_socket(
750        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_socket") @fixtures)"#,
751        false
752    )]
753    #[case::fail_kept_fifo(
754        r#"(builtins.filterSource (p: t: (builtins.baseNameOf p) != "a_fifo") @fixtures)"#,
755        false
756    )]
757    fn builtins_filter_source_unsupported_files(#[case] code: &str, #[case] exp_success: bool) {
758        use nix::errno::Errno;
759        use nix::sys::stat;
760        use nix::unistd;
761        use std::os::unix::net::UnixListener;
762        use tempfile::TempDir;
763
764        // We prepare a directory containing some unsupported file nodes:
765        // - character device
766        // - socket
767        // - FIFO
768        // and we run the evaluation inside that CWD.
769        //
770        // block devices cannot be tested because we don't have the right permissions.
771        let temp = TempDir::with_prefix("foo").expect("Failed to create a temporary directory");
772
773        // read, write, execute to the owner.
774        unistd::mkfifo(&temp.path().join("a_fifo"), stat::Mode::S_IRWXU)
775            .expect("Failed to create the FIFO");
776
777        UnixListener::bind(temp.path().join("a_socket")).expect("Failed to create the socket");
778
779        stat::mknod(
780            &temp.path().join("a_charnode"),
781            stat::SFlag::S_IFCHR,
782            stat::Mode::S_IRWXU,
783            0,
784        )
785        .inspect_err(|e| {
786            if *e == Errno::EPERM {
787                eprintln!(
788                    "\
789Missing permissions to create a character device node with mknod(2).
790Please run this test as root or set CAP_MKNOD."
791                );
792            }
793        })
794        .expect("Failed to create a character device node");
795
796        let code_replaced = code.replace("@fixtures", &temp.path().to_string_lossy());
797        let eval_result = eval(&code_replaced);
798
799        if exp_success {
800            assert!(
801                eval_result.value.is_some(),
802                "unexpected failure on a directory of unsupported file types but all filtered: {:?}",
803                eval_result.errors
804            );
805        } else {
806            assert!(
807                eval_result.value.is_none(),
808                "unexpected success on unsupported file type ingestion: {:?}",
809                eval_result.value
810            );
811        }
812    }
813}