Skip to main content

sui_compat/
derivation.rs

1//! Nix derivation (.drv) ATerm format — parse/serialize.
2//!
3//! Clean recursive-descent parser using a `Cursor` abstraction over the input.
4//! The ATerm format is simple enough that parser combinators are unnecessary.
5
6use std::collections::BTreeMap;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum DerivationError {
11    #[error("parse error at byte {pos}: {message}")]
12    Parse { pos: usize, message: String },
13    #[error("unexpected end of input")]
14    UnexpectedEof,
15    #[error("io error: {0}")]
16    Io(#[from] std::io::Error),
17}
18
19/// A derivation output descriptor.
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct DerivationOutput {
22    /// Store path for this output (empty for floating content-addressed outputs).
23    pub path: String,
24    /// Hash algorithm (e.g. `"sha256"`), empty for input-addressed outputs.
25    pub hash_algo: String,
26    /// Expected hash value, empty for input-addressed outputs.
27    pub hash: String,
28}
29
30/// A parsed Nix derivation.
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
32pub struct Derivation {
33    /// Named outputs (e.g. `"out"`, `"dev"`, `"lib"`).
34    pub outputs: BTreeMap<String, DerivationOutput>,
35    /// Input derivations: maps `.drv` store path to the list of outputs used.
36    pub input_derivations: BTreeMap<String, Vec<String>>,
37    /// Input source store paths (non-derivation dependencies).
38    pub input_sources: Vec<String>,
39    /// Target system triple (e.g. `"x86_64-linux"`).
40    pub system: String,
41    /// Path to the builder executable.
42    pub builder: String,
43    /// Arguments passed to the builder.
44    pub args: Vec<String>,
45    /// Environment variables set during the build.
46    pub env: BTreeMap<String, String>,
47}
48
49// ── Cursor ───────────────────────────────────────────────────
50
51/// Simple cursor over a byte slice for zero-copy parsing.
52struct Cursor<'a> {
53    data: &'a [u8],
54    pos: usize,
55}
56
57impl<'a> Cursor<'a> {
58    fn new(data: &'a [u8]) -> Self {
59        Self { data, pos: 0 }
60    }
61
62    fn peek(&self) -> Result<u8, DerivationError> {
63        self.data.get(self.pos).copied().ok_or(DerivationError::UnexpectedEof)
64    }
65
66    fn advance(&mut self) { self.pos += 1; }
67
68    fn expect(&mut self, ch: u8) -> Result<(), DerivationError> {
69        let got = self.peek()?;
70        if got != ch {
71            return Err(DerivationError::Parse {
72                pos: self.pos,
73                message: format!("expected '{}', got '{}'", ch as char, got as char),
74            });
75        }
76        self.advance();
77        Ok(())
78    }
79
80    fn expect_str(&mut self, s: &[u8]) -> Result<(), DerivationError> {
81        for &ch in s {
82            self.expect(ch)?;
83        }
84        Ok(())
85    }
86
87    /// Parse a quoted string with escape handling.
88    fn string(&mut self) -> Result<String, DerivationError> {
89        self.expect(b'"')?;
90        let mut result = Vec::new();
91        loop {
92            let ch = self.peek()?;
93            self.advance();
94            match ch {
95                b'"' => return String::from_utf8(result).map_err(|e| DerivationError::Parse {
96                    pos: self.pos, message: format!("invalid UTF-8: {e}"),
97                }),
98                b'\\' => {
99                    let esc = self.peek()?;
100                    self.advance();
101                    match esc {
102                        b'n' => result.push(b'\n'),
103                        b'r' => result.push(b'\r'),
104                        b't' => result.push(b'\t'),
105                        b'\\' => result.push(b'\\'),
106                        b'"' => result.push(b'"'),
107                        _ => { result.push(b'\\'); result.push(esc); }
108                    }
109                }
110                _ => result.push(ch),
111            }
112        }
113    }
114
115    /// Parse `[item, item, ...]` using the given item parser.
116    fn list<T>(&mut self, parse_item: impl Fn(&mut Self) -> Result<T, DerivationError>) -> Result<Vec<T>, DerivationError> {
117        self.expect(b'[')?;
118        let mut items = Vec::new();
119        if self.peek()? != b']' {
120            items.push(parse_item(self)?);
121            while self.peek()? == b',' {
122                self.advance();
123                items.push(parse_item(self)?);
124            }
125        }
126        self.expect(b']')?;
127        Ok(items)
128    }
129}
130
131impl Derivation {
132    /// Parse a derivation from its ATerm bytes.
133    pub fn parse(input: &[u8]) -> Result<Self, DerivationError> {
134        let mut c = Cursor::new(input);
135        c.expect_str(b"Derive(")?;
136
137        let outputs_list = c.list(|c| {
138            c.expect(b'(')?;
139            let name = c.string()?;
140            c.expect(b',')?;
141            let path = c.string()?;
142            c.expect(b',')?;
143            let hash_algo = c.string()?;
144            c.expect(b',')?;
145            let hash = c.string()?;
146            c.expect(b')')?;
147            Ok((name, DerivationOutput { path, hash_algo, hash }))
148        })?;
149        c.expect(b',')?;
150
151        let input_drvs_list = c.list(|c| {
152            c.expect(b'(')?;
153            let path = c.string()?;
154            c.expect(b',')?;
155            let outputs = c.list(|c| c.string())?;
156            c.expect(b')')?;
157            Ok((path, outputs))
158        })?;
159        c.expect(b',')?;
160
161        let input_sources = c.list(|c| c.string())?;
162        c.expect(b',')?;
163        let system = c.string()?;
164        c.expect(b',')?;
165        let builder = c.string()?;
166        c.expect(b',')?;
167        let args = c.list(|c| c.string())?;
168        c.expect(b',')?;
169
170        let env_list = c.list(|c| {
171            c.expect(b'(')?;
172            let key = c.string()?;
173            c.expect(b',')?;
174            let value = c.string()?;
175            c.expect(b')')?;
176            Ok((key, value))
177        })?;
178
179        c.expect(b')')?;
180
181        Ok(Derivation {
182            outputs: outputs_list.into_iter().collect(),
183            input_derivations: input_drvs_list.into_iter().collect(),
184            input_sources,
185            system,
186            builder,
187            args,
188            env: env_list.into_iter().collect(),
189        })
190    }
191
192    /// Serialize the derivation to ATerm format.
193    #[must_use]
194    pub fn serialize(&self) -> String {
195        let mut out = String::from("Derive(");
196
197        out.push('[');
198        out.push_str(&join_comma(self.outputs.iter().map(|(name, o)| {
199            format!("({},{},{},{})", escape(name), escape(&o.path), escape(&o.hash_algo), escape(&o.hash))
200        })));
201        out.push_str("],");
202
203        out.push('[');
204        out.push_str(&join_comma(self.input_derivations.iter().map(|(path, outputs)| {
205            let outs = join_comma(outputs.iter().map(|o| escape(o)));
206            format!("({},[{outs}])", escape(path))
207        })));
208        out.push_str("],");
209
210        let mut sources: Vec<_> = self.input_sources.iter().collect();
211        sources.sort();
212        out.push('[');
213        out.push_str(&join_comma(sources.iter().map(|s| escape(s))));
214        out.push_str("],");
215
216        out.push_str(&escape(&self.system));
217        out.push(',');
218        out.push_str(&escape(&self.builder));
219
220        out.push_str(",[");
221        out.push_str(&join_comma(self.args.iter().map(|a| escape(a))));
222        out.push_str("],[");
223        out.push_str(&join_comma(self.env.iter().map(|(k, v)| {
224            format!("({},{})", escape(k), escape(v))
225        })));
226        out.push_str("])");
227        out
228    }
229
230    /// Serialize to ATerm in "modulo" form: each `input_derivations`
231    /// key (a `.drv` store path) is replaced by the result of
232    /// `resolver(drv_path)` — CppNix's `hashDerivationModulo`
233    /// recursion.  Used when computing the hash that becomes the
234    /// *dependent* derivation's store path.
235    ///
236    /// The resolver typically looks up a pre-computed modulo hash
237    /// (lowercase hex) in an evaluator-level cache.  If the resolver
238    /// returns the drv path unchanged, this is identical to
239    /// [`Self::serialize`].
240    ///
241    /// # Panics
242    ///
243    /// Does not panic; any resolver-returned string is inserted verbatim.
244    #[must_use]
245    pub fn serialize_modulo(&self, resolver: impl Fn(&str) -> String) -> String {
246        let mut out = String::from("Derive(");
247
248        out.push('[');
249        out.push_str(&join_comma(self.outputs.iter().map(|(name, o)| {
250            format!("({},{},{},{})", escape(name), escape(&o.path), escape(&o.hash_algo), escape(&o.hash))
251        })));
252        out.push_str("],");
253
254        // CppNix's hashDerivationModulo keys the input derivations by their
255        // MODULO hash and serializes them SORTED BY THAT KEY — not by the
256        // original drv path. So resolve every path to its modulo first, then
257        // re-sort by the resolved value before emitting. Without the re-sort a
258        // derivation with ≥2 input drvs whose modulo order differs from their
259        // drv-path order serializes its modulo ATerm in the wrong order, and its
260        // output path (and everything transitively above it) diverges from nix —
261        // even though its full drv ATerm is byte-identical. Single-input drvs
262        // never expose this, which is why it survived until a 2-input stdenv drv.
263        let mut resolved_inputs: Vec<(String, &Vec<String>)> = self
264            .input_derivations
265            .iter()
266            .map(|(path, outputs)| (resolver(path), outputs))
267            .collect();
268        resolved_inputs.sort_by(|a, b| a.0.cmp(&b.0));
269        out.push('[');
270        out.push_str(&join_comma(resolved_inputs.into_iter().map(|(resolved, outputs)| {
271            let outs = join_comma(outputs.iter().map(|o| escape(o)));
272            format!("({},[{outs}])", escape(&resolved))
273        })));
274        out.push_str("],");
275
276        let mut sources: Vec<_> = self.input_sources.iter().collect();
277        sources.sort();
278        out.push('[');
279        out.push_str(&join_comma(sources.iter().map(|s| escape(s))));
280        out.push_str("],");
281
282        out.push_str(&escape(&self.system));
283        out.push(',');
284        out.push_str(&escape(&self.builder));
285
286        out.push_str(",[");
287        out.push_str(&join_comma(self.args.iter().map(|a| escape(a))));
288        out.push_str("],[");
289        out.push_str(&join_comma(self.env.iter().map(|(k, v)| {
290            format!("({},{})", escape(k), escape(v))
291        })));
292        out.push_str("])");
293        out
294    }
295}
296
297/// Join an iterator of string fragments with commas.
298fn join_comma(items: impl Iterator<Item = String>) -> String {
299    let mut out = String::new();
300    for (i, item) in items.enumerate() {
301        if i > 0 {
302            out.push(',');
303        }
304        out.push_str(&item);
305    }
306    out
307}
308
309/// Escape a string for ATerm serialization (backslash-escaping special chars).
310pub(crate) fn escape(s: &str) -> String {
311    let mut out = String::with_capacity(s.len() + 2);
312    out.push('"');
313    for ch in s.chars() {
314        match ch {
315            '"' => out.push_str("\\\""),
316            '\\' => out.push_str("\\\\"),
317            '\n' => out.push_str("\\n"),
318            '\r' => out.push_str("\\r"),
319            '\t' => out.push_str("\\t"),
320            _ => out.push(ch),
321        }
322    }
323    out.push('"');
324    out
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use proptest::prelude::*;
331
332    #[test]
333    fn serialize_roundtrip() {
334        let mut outputs = BTreeMap::new();
335        outputs.insert("out".to_string(), DerivationOutput {
336            path: "/nix/store/abc-hello".to_string(), hash_algo: String::new(), hash: String::new(),
337        });
338        let mut env = BTreeMap::new();
339        env.insert("name".to_string(), "hello".to_string());
340        let drv = Derivation {
341            outputs,
342            input_derivations: BTreeMap::new(),
343            input_sources: vec!["/nix/store/src".to_string()],
344            system: "x86_64-linux".to_string(),
345            builder: "/bin/sh".to_string(),
346            args: vec!["-e".to_string()],
347            env,
348        };
349        let s = drv.serialize();
350        let parsed = Derivation::parse(s.as_bytes()).unwrap();
351        assert_eq!(parsed, drv);
352    }
353
354    #[test]
355    fn escape_roundtrip() {
356        let mut env = BTreeMap::new();
357        env.insert("s".to_string(), "line1\nline2\r\ttab\\back\"quote".to_string());
358        let drv = Derivation {
359            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
360            input_derivations: BTreeMap::new(), input_sources: vec![], system: "x86_64-linux".to_string(),
361            builder: "/bin/sh".to_string(), args: vec![], env,
362        };
363        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
364        assert_eq!(parsed, drv);
365    }
366
367    #[test]
368    fn multiple_outputs() {
369        let mut outputs = BTreeMap::new();
370        for name in ["dev", "lib", "out"] {
371            outputs.insert(name.to_string(), DerivationOutput {
372                path: format!("/nix/store/{name}"), hash_algo: String::new(), hash: String::new(),
373            });
374        }
375        let drv = Derivation {
376            outputs, input_derivations: BTreeMap::new(), input_sources: vec![],
377            system: "x86_64-linux".to_string(), builder: "/bin/sh".to_string(), args: vec![], env: BTreeMap::new(),
378        };
379        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
380        assert_eq!(parsed.outputs.len(), 3);
381    }
382
383    #[test]
384    fn multiple_input_drvs() {
385        let mut input_drvs = BTreeMap::new();
386        input_drvs.insert("/nix/store/a.drv".to_string(), vec!["out".to_string()]);
387        input_drvs.insert("/nix/store/b.drv".to_string(), vec!["out".to_string(), "lib".to_string()]);
388        let drv = Derivation {
389            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
390            input_derivations: input_drvs, input_sources: vec![], system: "x86_64-linux".to_string(),
391            builder: "/bin/sh".to_string(), args: vec![], env: BTreeMap::new(),
392        };
393        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
394        assert_eq!(parsed.input_derivations.len(), 2);
395    }
396
397    #[test]
398    fn empty_everything() {
399        let drv = Derivation {
400            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
401            input_derivations: BTreeMap::new(), input_sources: vec![], system: "x86_64-linux".to_string(),
402            builder: "/bin/sh".to_string(), args: vec![], env: BTreeMap::new(),
403        };
404        let s = drv.serialize();
405        assert!(s.contains(",[],"));
406        let parsed = Derivation::parse(s.as_bytes()).unwrap();
407        assert_eq!(parsed, drv);
408    }
409
410    #[test]
411    fn fixed_output_derivation() {
412        let mut outputs = BTreeMap::new();
413        outputs.insert("out".to_string(), DerivationOutput {
414            path: "/nix/store/src.tar.gz".to_string(),
415            hash_algo: "sha256".to_string(),
416            hash: "1b0ri5lsf45dknj8bfxi1syz35kmab77apxxg1yrf33la1qm3kc7".to_string(),
417        });
418        let drv = Derivation {
419            outputs, input_derivations: BTreeMap::new(), input_sources: vec![],
420            system: "x86_64-linux".to_string(), builder: "/bin/curl".to_string(), args: vec!["-o".to_string()], env: BTreeMap::new(),
421        };
422        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
423        assert_eq!(parsed, drv);
424        assert_eq!(parsed.outputs["out"].hash_algo, "sha256");
425    }
426
427    // ── Many outputs ─────────────────────────────────────
428
429    #[test]
430    fn many_outputs_roundtrip() {
431        let mut outputs = BTreeMap::new();
432        for name in ["bin", "data", "debug", "dev", "doc", "info", "lib", "man", "out", "static"] {
433            outputs.insert(name.to_string(), DerivationOutput {
434                path: format!("/nix/store/hash-pkg-{name}"),
435                hash_algo: String::new(),
436                hash: String::new(),
437            });
438        }
439        let drv = Derivation {
440            outputs,
441            input_derivations: BTreeMap::new(),
442            input_sources: vec![],
443            system: "x86_64-linux".to_string(),
444            builder: "/bin/sh".to_string(),
445            args: vec![],
446            env: BTreeMap::new(),
447        };
448        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
449        assert_eq!(parsed.outputs.len(), 10);
450        assert_eq!(parsed, drv);
451    }
452
453    // ── Unusual env vars ─────────────────────────────────
454
455    #[test]
456    fn env_with_special_characters() {
457        let mut env = BTreeMap::new();
458        env.insert("multiline".to_string(), "line1\nline2\nline3".to_string());
459        env.insert("tabs_and_cr".to_string(), "col1\tcol2\r\n".to_string());
460        env.insert("backslash".to_string(), "C:\\Users\\nix".to_string());
461        env.insert("quotes".to_string(), r#"say "hello""#.to_string());
462        env.insert("empty".to_string(), String::new());
463
464        let drv = Derivation {
465            outputs: {
466                let mut m = BTreeMap::new();
467                m.insert("out".to_string(), DerivationOutput {
468                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
469                });
470                m
471            },
472            input_derivations: BTreeMap::new(),
473            input_sources: vec![],
474            system: "x86_64-linux".to_string(),
475            builder: "/bin/sh".to_string(),
476            args: vec![],
477            env,
478        };
479        let serialized = drv.serialize();
480        let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
481        assert_eq!(parsed, drv);
482    }
483
484    #[test]
485    fn env_with_long_value() {
486        let mut env = BTreeMap::new();
487        env.insert("NIX_CFLAGS_COMPILE".to_string(), "-I/nix/store/abc ".repeat(100));
488
489        let drv = Derivation {
490            outputs: {
491                let mut m = BTreeMap::new();
492                m.insert("out".to_string(), DerivationOutput {
493                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
494                });
495                m
496            },
497            input_derivations: BTreeMap::new(),
498            input_sources: vec![],
499            system: "x86_64-linux".to_string(),
500            builder: "/bin/sh".to_string(),
501            args: vec![],
502            env,
503        };
504        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
505        assert_eq!(parsed, drv);
506    }
507
508    // ── Multiple input sources (sorted output) ──────────
509
510    #[test]
511    fn input_sources_sorted_in_serialization() {
512        let drv = Derivation {
513            outputs: {
514                let mut m = BTreeMap::new();
515                m.insert("out".to_string(), DerivationOutput {
516                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
517                });
518                m
519            },
520            input_derivations: BTreeMap::new(),
521            input_sources: vec![
522                "/nix/store/zzz-last".to_string(),
523                "/nix/store/aaa-first".to_string(),
524                "/nix/store/mmm-middle".to_string(),
525            ],
526            system: "x86_64-linux".to_string(),
527            builder: "/bin/sh".to_string(),
528            args: vec![],
529            env: BTreeMap::new(),
530        };
531        let serialized = drv.serialize();
532        let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
533        assert_eq!(parsed.input_sources, vec![
534            "/nix/store/aaa-first".to_string(),
535            "/nix/store/mmm-middle".to_string(),
536            "/nix/store/zzz-last".to_string(),
537        ]);
538    }
539
540    // ── Error cases ──────────────────────────────────────
541
542    #[test]
543    fn parse_truncated_input() {
544        assert!(Derivation::parse(b"Derive(").is_err());
545        assert!(Derivation::parse(b"").is_err());
546        assert!(Derivation::parse(b"Derive").is_err());
547    }
548
549    #[test]
550    fn parse_invalid_prefix() {
551        assert!(Derivation::parse(b"NotDerive([])").is_err());
552    }
553
554    #[test]
555    fn parse_missing_closing_paren() {
556        let drv = Derivation {
557            outputs: {
558                let mut m = BTreeMap::new();
559                m.insert("out".to_string(), DerivationOutput {
560                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
561                });
562                m
563            },
564            input_derivations: BTreeMap::new(),
565            input_sources: vec![],
566            system: "x86_64-linux".to_string(),
567            builder: "/bin/sh".to_string(),
568            args: vec![],
569            env: BTreeMap::new(),
570        };
571        let mut serialized = drv.serialize();
572        serialized.pop(); // Remove the closing ')'
573        assert!(Derivation::parse(serialized.as_bytes()).is_err());
574    }
575
576    // ── Multiple input derivations with multiple outputs ──
577
578    proptest! {
579        #[test]
580        fn prop_escape_roundtrip(s in ".*") {
581            let escaped = escape(&s);
582            let input = format!(
583                "Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",{escaped})])"
584            );
585            let drv = Derivation::parse(input.as_bytes()).unwrap();
586            prop_assert_eq!(&drv.env["v"], &s);
587        }
588
589        #[test]
590        fn prop_serialize_parse_roundtrip(
591            name in "[a-z]{1,10}",
592            builder_arg in "[a-z0-9/]{1,20}",
593        ) {
594            let drv = Derivation {
595                outputs: {
596                    let mut m = BTreeMap::new();
597                    m.insert("out".to_string(), DerivationOutput {
598                        path: format!("/nix/store/hash-{name}"),
599                        hash_algo: String::new(),
600                        hash: String::new(),
601                    });
602                    m
603                },
604                input_derivations: BTreeMap::new(),
605                input_sources: vec![],
606                system: "x86_64-linux".to_string(),
607                builder: format!("/{builder_arg}"),
608                args: vec![],
609                env: BTreeMap::new(),
610            };
611            let serialized = drv.serialize();
612            let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
613            prop_assert_eq!(parsed, drv);
614        }
615    }
616
617    // ── escape() unit tests ──────────────────────────────
618
619    #[test]
620    fn escape_empty_string() {
621        assert_eq!(escape(""), "\"\"");
622    }
623
624    #[test]
625    fn escape_no_special_chars() {
626        assert_eq!(escape("hello"), "\"hello\"");
627    }
628
629    #[test]
630    fn escape_double_quote() {
631        assert_eq!(escape("a\"b"), "\"a\\\"b\"");
632    }
633
634    #[test]
635    fn escape_backslash() {
636        assert_eq!(escape("a\\b"), "\"a\\\\b\"");
637    }
638
639    #[test]
640    fn escape_newline() {
641        assert_eq!(escape("a\nb"), "\"a\\nb\"");
642    }
643
644    #[test]
645    fn escape_carriage_return() {
646        assert_eq!(escape("a\rb"), "\"a\\rb\"");
647    }
648
649    #[test]
650    fn escape_tab() {
651        assert_eq!(escape("a\tb"), "\"a\\tb\"");
652    }
653
654    #[test]
655    fn escape_all_specials_combined() {
656        let s = "a\"b\\c\nd\re\tf";
657        let escaped = escape(s);
658        assert_eq!(escaped, "\"a\\\"b\\\\c\\nd\\re\\tf\"");
659    }
660
661    // ── Empty inputs everywhere ──────────────────────────
662
663    #[test]
664    fn empty_outputs_btreemap_serializes_brackets() {
665        let drv = Derivation {
666            outputs: BTreeMap::new(),
667            input_derivations: BTreeMap::new(),
668            input_sources: vec![],
669            system: String::new(),
670            builder: String::new(),
671            args: vec![],
672            env: BTreeMap::new(),
673        };
674        let s = drv.serialize();
675        // outputs is the very first list — must be empty `[]`
676        assert!(s.starts_with("Derive([],"));
677        let parsed = Derivation::parse(s.as_bytes()).unwrap();
678        assert_eq!(parsed, drv);
679    }
680
681    #[test]
682    fn unicode_env_values_roundtrip() {
683        let mut env = BTreeMap::new();
684        env.insert("greeting".to_string(), "こんにちは世界".to_string());
685        env.insert("emoji".to_string(), "🎉🚀".to_string());
686        env.insert("mixed".to_string(), "test 日本語 café".to_string());
687        let drv = Derivation {
688            outputs: {
689                let mut m = BTreeMap::new();
690                m.insert("out".to_string(), DerivationOutput {
691                    path: "/out".to_string(),
692                    hash_algo: String::new(),
693                    hash: String::new(),
694                });
695                m
696            },
697            input_derivations: BTreeMap::new(),
698            input_sources: vec![],
699            system: "x86_64-linux".to_string(),
700            builder: "/bin/sh".to_string(),
701            args: vec![],
702            env,
703        };
704        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
705        assert_eq!(parsed, drv);
706    }
707
708    #[test]
709    fn unknown_escape_sequences_preserved_literally() {
710        // The parser preserves unknown \X as \X (literal backslash + char)
711        let input = b"Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",\"a\\xb\")])";
712        let drv = Derivation::parse(input).unwrap();
713        // \x is not a recognized escape, so it's preserved as-is
714        assert_eq!(drv.env["v"], "a\\xb");
715    }
716
717    #[test]
718    fn parse_error_includes_position() {
719        // Truncated input — parsing fails somewhere
720        let result = Derivation::parse(b"Derive([(\"out\"");
721        assert!(result.is_err());
722    }
723
724    #[test]
725    fn parse_invalid_outputs_format() {
726        // Outputs missing the closing tuple paren
727        let result = Derivation::parse(b"Derive([(\"out\",\"/out\",\"\",\"\"],[],[],\"\",\"\",[],[])");
728        assert!(result.is_err());
729    }
730
731    #[test]
732    fn parse_missing_comma_separator() {
733        // Missing comma between fields
734        let result = Derivation::parse(b"Derive([],[][],\"\",\"\",[],[])");
735        assert!(result.is_err());
736    }
737
738    #[test]
739    fn args_with_many_entries() {
740        let drv = Derivation {
741            outputs: {
742                let mut m = BTreeMap::new();
743                m.insert("out".to_string(), DerivationOutput {
744                    path: "/out".to_string(),
745                    hash_algo: String::new(),
746                    hash: String::new(),
747                });
748                m
749            },
750            input_derivations: BTreeMap::new(),
751            input_sources: vec![],
752            system: "x86_64-linux".to_string(),
753            builder: "/bin/sh".to_string(),
754            args: (0..50).map(|i| format!("arg-{i}")).collect(),
755            env: BTreeMap::new(),
756        };
757        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
758        assert_eq!(parsed.args.len(), 50);
759        assert_eq!(parsed, drv);
760    }
761
762    #[test]
763    fn input_derivations_with_many_outputs_each() {
764        let mut input_drvs = BTreeMap::new();
765        let outputs: Vec<String> =
766            ["out", "dev", "lib", "bin", "doc", "man", "info", "static", "debug"]
767                .iter()
768                .map(ToString::to_string)
769                .collect();
770        for i in 0..5 {
771            input_drvs.insert(format!("/nix/store/dep{i}.drv"), outputs.clone());
772        }
773        let drv = Derivation {
774            outputs: {
775                let mut m = BTreeMap::new();
776                m.insert("out".to_string(), DerivationOutput {
777                    path: "/out".to_string(),
778                    hash_algo: String::new(),
779                    hash: String::new(),
780                });
781                m
782            },
783            input_derivations: input_drvs,
784            input_sources: vec![],
785            system: "x86_64-linux".to_string(),
786            builder: "/bin/sh".to_string(),
787            args: vec![],
788            env: BTreeMap::new(),
789        };
790        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
791        assert_eq!(parsed.input_derivations.len(), 5);
792        for outs in parsed.input_derivations.values() {
793            assert_eq!(outs.len(), 9);
794        }
795        assert_eq!(parsed, drv);
796    }
797
798    #[test]
799    fn fixed_output_md5_algorithm() {
800        let mut outputs = BTreeMap::new();
801        outputs.insert("out".to_string(), DerivationOutput {
802            path: "/nix/store/abc-x".to_string(),
803            hash_algo: "md5".to_string(),
804            hash: "0123456789abcdef0123456789abcdef".to_string(),
805        });
806        let drv = Derivation {
807            outputs,
808            input_derivations: BTreeMap::new(),
809            input_sources: vec![],
810            system: "x86_64-linux".to_string(),
811            builder: "/bin/sh".to_string(),
812            args: vec![],
813            env: BTreeMap::new(),
814        };
815        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
816        assert_eq!(parsed.outputs["out"].hash_algo, "md5");
817        assert_eq!(parsed, drv);
818    }
819
820    #[test]
821    fn empty_string_in_env_key() {
822        let mut env = BTreeMap::new();
823        env.insert(String::new(), "value-with-empty-key".to_string());
824        let drv = Derivation {
825            outputs: {
826                let mut m = BTreeMap::new();
827                m.insert("out".to_string(), DerivationOutput {
828                    path: "/out".to_string(),
829                    hash_algo: String::new(),
830                    hash: String::new(),
831                });
832                m
833            },
834            input_derivations: BTreeMap::new(),
835            input_sources: vec![],
836            system: "x86_64-linux".to_string(),
837            builder: "/bin/sh".to_string(),
838            args: vec![],
839            env,
840        };
841        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
842        assert_eq!(parsed.env[""], "value-with-empty-key");
843    }
844
845    #[test]
846    fn parser_handles_zero_outputs_zero_inputs() {
847        let input = b"Derive([],[],[],\"sys\",\"build\",[],[])";
848        let drv = Derivation::parse(input).unwrap();
849        assert!(drv.outputs.is_empty());
850        assert!(drv.input_derivations.is_empty());
851        assert!(drv.input_sources.is_empty());
852        assert!(drv.args.is_empty());
853        assert!(drv.env.is_empty());
854        assert_eq!(drv.system, "sys");
855        assert_eq!(drv.builder, "build");
856    }
857
858    #[test]
859    fn default_derivation_is_empty() {
860        let drv = Derivation::default();
861        assert!(drv.outputs.is_empty());
862        assert!(drv.input_derivations.is_empty());
863        assert!(drv.input_sources.is_empty());
864        assert!(drv.system.is_empty());
865        assert!(drv.builder.is_empty());
866        assert!(drv.args.is_empty());
867        assert!(drv.env.is_empty());
868    }
869
870    #[test]
871    fn default_derivation_output_is_empty() {
872        let out = DerivationOutput::default();
873        assert!(out.path.is_empty());
874        assert!(out.hash_algo.is_empty());
875        assert!(out.hash.is_empty());
876    }
877
878    #[test]
879    fn outputs_serialized_in_btreemap_order() {
880        // BTreeMap iterates keys in sorted order — confirm serialization
881        // emits outputs alphabetically.
882        let mut outputs = BTreeMap::new();
883        for name in ["zzz", "aaa", "mmm"] {
884            outputs.insert(name.to_string(), DerivationOutput {
885                path: format!("/out-{name}"),
886                hash_algo: String::new(),
887                hash: String::new(),
888            });
889        }
890        let drv = Derivation {
891            outputs,
892            input_derivations: BTreeMap::new(),
893            input_sources: vec![],
894            system: "sys".to_string(),
895            builder: "build".to_string(),
896            args: vec![],
897            env: BTreeMap::new(),
898        };
899        let s = drv.serialize();
900        // "aaa" should come before "mmm" before "zzz"
901        let aaa = s.find("\"aaa\"").unwrap();
902        let mmm = s.find("\"mmm\"").unwrap();
903        let zzz = s.find("\"zzz\"").unwrap();
904        assert!(aaa < mmm);
905        assert!(mmm < zzz);
906    }
907
908    #[test]
909    fn env_serialized_in_btreemap_order() {
910        let mut env = BTreeMap::new();
911        for k in ["zoo", "alpha", "middle"] {
912            env.insert(k.to_string(), format!("v-{k}"));
913        }
914        let drv = Derivation {
915            outputs: {
916                let mut m = BTreeMap::new();
917                m.insert("out".to_string(), DerivationOutput {
918                    path: "/out".to_string(),
919                    hash_algo: String::new(),
920                    hash: String::new(),
921                });
922                m
923            },
924            input_derivations: BTreeMap::new(),
925            input_sources: vec![],
926            system: "sys".to_string(),
927            builder: "build".to_string(),
928            args: vec![],
929            env,
930        };
931        let s = drv.serialize();
932        let alpha = s.find("\"alpha\"").unwrap();
933        let middle = s.find("\"middle\"").unwrap();
934        let zoo = s.find("\"zoo\"").unwrap();
935        assert!(alpha < middle);
936        assert!(middle < zoo);
937    }
938
939    #[test]
940    fn complex_input_derivations() {
941        let mut input_drvs = BTreeMap::new();
942        input_drvs.insert(
943            "/nix/store/abc-gcc.drv".to_string(),
944            vec!["out".to_string(), "lib".to_string(), "info".to_string()],
945        );
946        input_drvs.insert(
947            "/nix/store/def-glibc.drv".to_string(),
948            vec!["out".to_string(), "dev".to_string(), "static".to_string()],
949        );
950        input_drvs.insert(
951            "/nix/store/ghi-bash.drv".to_string(),
952            vec!["out".to_string()],
953        );
954
955        let drv = Derivation {
956            outputs: {
957                let mut m = BTreeMap::new();
958                m.insert("out".to_string(), DerivationOutput {
959                    path: "/nix/store/result".to_string(),
960                    hash_algo: String::new(),
961                    hash: String::new(),
962                });
963                m
964            },
965            input_derivations: input_drvs,
966            input_sources: vec!["/nix/store/src".to_string()],
967            system: "x86_64-linux".to_string(),
968            builder: "/nix/store/bash/bin/bash".to_string(),
969            args: vec!["-e".to_string(), "/nix/store/builder.sh".to_string()],
970            env: {
971                let mut e = BTreeMap::new();
972                e.insert("name".to_string(), "test-pkg".to_string());
973                e.insert("version".to_string(), "1.0.0".to_string());
974                e
975            },
976        };
977        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
978        assert_eq!(parsed, drv);
979        assert_eq!(parsed.input_derivations.len(), 3);
980        assert_eq!(parsed.input_derivations["/nix/store/abc-gcc.drv"].len(), 3);
981    }
982}