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