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        out.push('[');
255        out.push_str(&join_comma(self.input_derivations.iter().map(|(path, outputs)| {
256            let resolved = resolver(path);
257            let outs = join_comma(outputs.iter().map(|o| escape(o)));
258            format!("({},[{outs}])", escape(&resolved))
259        })));
260        out.push_str("],");
261
262        let mut sources: Vec<_> = self.input_sources.iter().collect();
263        sources.sort();
264        out.push('[');
265        out.push_str(&join_comma(sources.iter().map(|s| escape(s))));
266        out.push_str("],");
267
268        out.push_str(&escape(&self.system));
269        out.push(',');
270        out.push_str(&escape(&self.builder));
271
272        out.push_str(",[");
273        out.push_str(&join_comma(self.args.iter().map(|a| escape(a))));
274        out.push_str("],[");
275        out.push_str(&join_comma(self.env.iter().map(|(k, v)| {
276            format!("({},{})", escape(k), escape(v))
277        })));
278        out.push_str("])");
279        out
280    }
281}
282
283/// Join an iterator of string fragments with commas.
284fn join_comma(items: impl Iterator<Item = String>) -> String {
285    let mut out = String::new();
286    for (i, item) in items.enumerate() {
287        if i > 0 {
288            out.push(',');
289        }
290        out.push_str(&item);
291    }
292    out
293}
294
295/// Escape a string for ATerm serialization (backslash-escaping special chars).
296pub(crate) fn escape(s: &str) -> String {
297    let mut out = String::with_capacity(s.len() + 2);
298    out.push('"');
299    for ch in s.chars() {
300        match ch {
301            '"' => out.push_str("\\\""),
302            '\\' => out.push_str("\\\\"),
303            '\n' => out.push_str("\\n"),
304            '\r' => out.push_str("\\r"),
305            '\t' => out.push_str("\\t"),
306            _ => out.push(ch),
307        }
308    }
309    out.push('"');
310    out
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use proptest::prelude::*;
317
318    #[test]
319    fn serialize_roundtrip() {
320        let mut outputs = BTreeMap::new();
321        outputs.insert("out".to_string(), DerivationOutput {
322            path: "/nix/store/abc-hello".to_string(), hash_algo: String::new(), hash: String::new(),
323        });
324        let mut env = BTreeMap::new();
325        env.insert("name".to_string(), "hello".to_string());
326        let drv = Derivation {
327            outputs,
328            input_derivations: BTreeMap::new(),
329            input_sources: vec!["/nix/store/src".to_string()],
330            system: "x86_64-linux".to_string(),
331            builder: "/bin/sh".to_string(),
332            args: vec!["-e".to_string()],
333            env,
334        };
335        let s = drv.serialize();
336        let parsed = Derivation::parse(s.as_bytes()).unwrap();
337        assert_eq!(parsed, drv);
338    }
339
340    #[test]
341    fn escape_roundtrip() {
342        let mut env = BTreeMap::new();
343        env.insert("s".to_string(), "line1\nline2\r\ttab\\back\"quote".to_string());
344        let drv = Derivation {
345            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
346            input_derivations: BTreeMap::new(), input_sources: vec![], system: "x86_64-linux".to_string(),
347            builder: "/bin/sh".to_string(), args: vec![], env,
348        };
349        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
350        assert_eq!(parsed, drv);
351    }
352
353    #[test]
354    fn multiple_outputs() {
355        let mut outputs = BTreeMap::new();
356        for name in ["dev", "lib", "out"] {
357            outputs.insert(name.to_string(), DerivationOutput {
358                path: format!("/nix/store/{name}"), hash_algo: String::new(), hash: String::new(),
359            });
360        }
361        let drv = Derivation {
362            outputs, input_derivations: BTreeMap::new(), input_sources: vec![],
363            system: "x86_64-linux".to_string(), builder: "/bin/sh".to_string(), args: vec![], env: BTreeMap::new(),
364        };
365        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
366        assert_eq!(parsed.outputs.len(), 3);
367    }
368
369    #[test]
370    fn multiple_input_drvs() {
371        let mut input_drvs = BTreeMap::new();
372        input_drvs.insert("/nix/store/a.drv".to_string(), vec!["out".to_string()]);
373        input_drvs.insert("/nix/store/b.drv".to_string(), vec!["out".to_string(), "lib".to_string()]);
374        let drv = Derivation {
375            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
376            input_derivations: input_drvs, input_sources: vec![], system: "x86_64-linux".to_string(),
377            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.input_derivations.len(), 2);
381    }
382
383    #[test]
384    fn empty_everything() {
385        let drv = Derivation {
386            outputs: { let mut m = BTreeMap::new(); m.insert("out".to_string(), DerivationOutput { path: "/out".to_string(), hash_algo: String::new(), hash: String::new() }); m },
387            input_derivations: BTreeMap::new(), input_sources: vec![], system: "x86_64-linux".to_string(),
388            builder: "/bin/sh".to_string(), args: vec![], env: BTreeMap::new(),
389        };
390        let s = drv.serialize();
391        assert!(s.contains(",[],"));
392        let parsed = Derivation::parse(s.as_bytes()).unwrap();
393        assert_eq!(parsed, drv);
394    }
395
396    #[test]
397    fn fixed_output_derivation() {
398        let mut outputs = BTreeMap::new();
399        outputs.insert("out".to_string(), DerivationOutput {
400            path: "/nix/store/src.tar.gz".to_string(),
401            hash_algo: "sha256".to_string(),
402            hash: "1b0ri5lsf45dknj8bfxi1syz35kmab77apxxg1yrf33la1qm3kc7".to_string(),
403        });
404        let drv = Derivation {
405            outputs, input_derivations: BTreeMap::new(), input_sources: vec![],
406            system: "x86_64-linux".to_string(), builder: "/bin/curl".to_string(), args: vec!["-o".to_string()], env: BTreeMap::new(),
407        };
408        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
409        assert_eq!(parsed, drv);
410        assert_eq!(parsed.outputs["out"].hash_algo, "sha256");
411    }
412
413    // ── Many outputs ─────────────────────────────────────
414
415    #[test]
416    fn many_outputs_roundtrip() {
417        let mut outputs = BTreeMap::new();
418        for name in ["bin", "data", "debug", "dev", "doc", "info", "lib", "man", "out", "static"] {
419            outputs.insert(name.to_string(), DerivationOutput {
420                path: format!("/nix/store/hash-pkg-{name}"),
421                hash_algo: String::new(),
422                hash: String::new(),
423            });
424        }
425        let drv = Derivation {
426            outputs,
427            input_derivations: BTreeMap::new(),
428            input_sources: vec![],
429            system: "x86_64-linux".to_string(),
430            builder: "/bin/sh".to_string(),
431            args: vec![],
432            env: BTreeMap::new(),
433        };
434        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
435        assert_eq!(parsed.outputs.len(), 10);
436        assert_eq!(parsed, drv);
437    }
438
439    // ── Unusual env vars ─────────────────────────────────
440
441    #[test]
442    fn env_with_special_characters() {
443        let mut env = BTreeMap::new();
444        env.insert("multiline".to_string(), "line1\nline2\nline3".to_string());
445        env.insert("tabs_and_cr".to_string(), "col1\tcol2\r\n".to_string());
446        env.insert("backslash".to_string(), "C:\\Users\\nix".to_string());
447        env.insert("quotes".to_string(), r#"say "hello""#.to_string());
448        env.insert("empty".to_string(), String::new());
449
450        let drv = Derivation {
451            outputs: {
452                let mut m = BTreeMap::new();
453                m.insert("out".to_string(), DerivationOutput {
454                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
455                });
456                m
457            },
458            input_derivations: BTreeMap::new(),
459            input_sources: vec![],
460            system: "x86_64-linux".to_string(),
461            builder: "/bin/sh".to_string(),
462            args: vec![],
463            env,
464        };
465        let serialized = drv.serialize();
466        let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
467        assert_eq!(parsed, drv);
468    }
469
470    #[test]
471    fn env_with_long_value() {
472        let mut env = BTreeMap::new();
473        env.insert("NIX_CFLAGS_COMPILE".to_string(), "-I/nix/store/abc ".repeat(100));
474
475        let drv = Derivation {
476            outputs: {
477                let mut m = BTreeMap::new();
478                m.insert("out".to_string(), DerivationOutput {
479                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
480                });
481                m
482            },
483            input_derivations: BTreeMap::new(),
484            input_sources: vec![],
485            system: "x86_64-linux".to_string(),
486            builder: "/bin/sh".to_string(),
487            args: vec![],
488            env,
489        };
490        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
491        assert_eq!(parsed, drv);
492    }
493
494    // ── Multiple input sources (sorted output) ──────────
495
496    #[test]
497    fn input_sources_sorted_in_serialization() {
498        let drv = Derivation {
499            outputs: {
500                let mut m = BTreeMap::new();
501                m.insert("out".to_string(), DerivationOutput {
502                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
503                });
504                m
505            },
506            input_derivations: BTreeMap::new(),
507            input_sources: vec![
508                "/nix/store/zzz-last".to_string(),
509                "/nix/store/aaa-first".to_string(),
510                "/nix/store/mmm-middle".to_string(),
511            ],
512            system: "x86_64-linux".to_string(),
513            builder: "/bin/sh".to_string(),
514            args: vec![],
515            env: BTreeMap::new(),
516        };
517        let serialized = drv.serialize();
518        let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
519        assert_eq!(parsed.input_sources, vec![
520            "/nix/store/aaa-first".to_string(),
521            "/nix/store/mmm-middle".to_string(),
522            "/nix/store/zzz-last".to_string(),
523        ]);
524    }
525
526    // ── Error cases ──────────────────────────────────────
527
528    #[test]
529    fn parse_truncated_input() {
530        assert!(Derivation::parse(b"Derive(").is_err());
531        assert!(Derivation::parse(b"").is_err());
532        assert!(Derivation::parse(b"Derive").is_err());
533    }
534
535    #[test]
536    fn parse_invalid_prefix() {
537        assert!(Derivation::parse(b"NotDerive([])").is_err());
538    }
539
540    #[test]
541    fn parse_missing_closing_paren() {
542        let drv = Derivation {
543            outputs: {
544                let mut m = BTreeMap::new();
545                m.insert("out".to_string(), DerivationOutput {
546                    path: "/out".to_string(), hash_algo: String::new(), hash: String::new(),
547                });
548                m
549            },
550            input_derivations: BTreeMap::new(),
551            input_sources: vec![],
552            system: "x86_64-linux".to_string(),
553            builder: "/bin/sh".to_string(),
554            args: vec![],
555            env: BTreeMap::new(),
556        };
557        let mut serialized = drv.serialize();
558        serialized.pop(); // Remove the closing ')'
559        assert!(Derivation::parse(serialized.as_bytes()).is_err());
560    }
561
562    // ── Multiple input derivations with multiple outputs ──
563
564    proptest! {
565        #[test]
566        fn prop_escape_roundtrip(s in ".*") {
567            let escaped = escape(&s);
568            let input = format!(
569                "Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",{escaped})])"
570            );
571            let drv = Derivation::parse(input.as_bytes()).unwrap();
572            prop_assert_eq!(&drv.env["v"], &s);
573        }
574
575        #[test]
576        fn prop_serialize_parse_roundtrip(
577            name in "[a-z]{1,10}",
578            builder_arg in "[a-z0-9/]{1,20}",
579        ) {
580            let drv = Derivation {
581                outputs: {
582                    let mut m = BTreeMap::new();
583                    m.insert("out".to_string(), DerivationOutput {
584                        path: format!("/nix/store/hash-{name}"),
585                        hash_algo: String::new(),
586                        hash: String::new(),
587                    });
588                    m
589                },
590                input_derivations: BTreeMap::new(),
591                input_sources: vec![],
592                system: "x86_64-linux".to_string(),
593                builder: format!("/{builder_arg}"),
594                args: vec![],
595                env: BTreeMap::new(),
596            };
597            let serialized = drv.serialize();
598            let parsed = Derivation::parse(serialized.as_bytes()).unwrap();
599            prop_assert_eq!(parsed, drv);
600        }
601    }
602
603    // ── escape() unit tests ──────────────────────────────
604
605    #[test]
606    fn escape_empty_string() {
607        assert_eq!(escape(""), "\"\"");
608    }
609
610    #[test]
611    fn escape_no_special_chars() {
612        assert_eq!(escape("hello"), "\"hello\"");
613    }
614
615    #[test]
616    fn escape_double_quote() {
617        assert_eq!(escape("a\"b"), "\"a\\\"b\"");
618    }
619
620    #[test]
621    fn escape_backslash() {
622        assert_eq!(escape("a\\b"), "\"a\\\\b\"");
623    }
624
625    #[test]
626    fn escape_newline() {
627        assert_eq!(escape("a\nb"), "\"a\\nb\"");
628    }
629
630    #[test]
631    fn escape_carriage_return() {
632        assert_eq!(escape("a\rb"), "\"a\\rb\"");
633    }
634
635    #[test]
636    fn escape_tab() {
637        assert_eq!(escape("a\tb"), "\"a\\tb\"");
638    }
639
640    #[test]
641    fn escape_all_specials_combined() {
642        let s = "a\"b\\c\nd\re\tf";
643        let escaped = escape(s);
644        assert_eq!(escaped, "\"a\\\"b\\\\c\\nd\\re\\tf\"");
645    }
646
647    // ── Empty inputs everywhere ──────────────────────────
648
649    #[test]
650    fn empty_outputs_btreemap_serializes_brackets() {
651        let drv = Derivation {
652            outputs: BTreeMap::new(),
653            input_derivations: BTreeMap::new(),
654            input_sources: vec![],
655            system: String::new(),
656            builder: String::new(),
657            args: vec![],
658            env: BTreeMap::new(),
659        };
660        let s = drv.serialize();
661        // outputs is the very first list — must be empty `[]`
662        assert!(s.starts_with("Derive([],"));
663        let parsed = Derivation::parse(s.as_bytes()).unwrap();
664        assert_eq!(parsed, drv);
665    }
666
667    #[test]
668    fn unicode_env_values_roundtrip() {
669        let mut env = BTreeMap::new();
670        env.insert("greeting".to_string(), "こんにちは世界".to_string());
671        env.insert("emoji".to_string(), "🎉🚀".to_string());
672        env.insert("mixed".to_string(), "test 日本語 café".to_string());
673        let drv = Derivation {
674            outputs: {
675                let mut m = BTreeMap::new();
676                m.insert("out".to_string(), DerivationOutput {
677                    path: "/out".to_string(),
678                    hash_algo: String::new(),
679                    hash: String::new(),
680                });
681                m
682            },
683            input_derivations: BTreeMap::new(),
684            input_sources: vec![],
685            system: "x86_64-linux".to_string(),
686            builder: "/bin/sh".to_string(),
687            args: vec![],
688            env,
689        };
690        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
691        assert_eq!(parsed, drv);
692    }
693
694    #[test]
695    fn unknown_escape_sequences_preserved_literally() {
696        // The parser preserves unknown \X as \X (literal backslash + char)
697        let input = b"Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",\"a\\xb\")])";
698        let drv = Derivation::parse(input).unwrap();
699        // \x is not a recognized escape, so it's preserved as-is
700        assert_eq!(drv.env["v"], "a\\xb");
701    }
702
703    #[test]
704    fn parse_error_includes_position() {
705        // Truncated input — parsing fails somewhere
706        let result = Derivation::parse(b"Derive([(\"out\"");
707        assert!(result.is_err());
708    }
709
710    #[test]
711    fn parse_invalid_outputs_format() {
712        // Outputs missing the closing tuple paren
713        let result = Derivation::parse(b"Derive([(\"out\",\"/out\",\"\",\"\"],[],[],\"\",\"\",[],[])");
714        assert!(result.is_err());
715    }
716
717    #[test]
718    fn parse_missing_comma_separator() {
719        // Missing comma between fields
720        let result = Derivation::parse(b"Derive([],[][],\"\",\"\",[],[])");
721        assert!(result.is_err());
722    }
723
724    #[test]
725    fn args_with_many_entries() {
726        let drv = Derivation {
727            outputs: {
728                let mut m = BTreeMap::new();
729                m.insert("out".to_string(), DerivationOutput {
730                    path: "/out".to_string(),
731                    hash_algo: String::new(),
732                    hash: String::new(),
733                });
734                m
735            },
736            input_derivations: BTreeMap::new(),
737            input_sources: vec![],
738            system: "x86_64-linux".to_string(),
739            builder: "/bin/sh".to_string(),
740            args: (0..50).map(|i| format!("arg-{i}")).collect(),
741            env: BTreeMap::new(),
742        };
743        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
744        assert_eq!(parsed.args.len(), 50);
745        assert_eq!(parsed, drv);
746    }
747
748    #[test]
749    fn input_derivations_with_many_outputs_each() {
750        let mut input_drvs = BTreeMap::new();
751        let outputs: Vec<String> =
752            ["out", "dev", "lib", "bin", "doc", "man", "info", "static", "debug"]
753                .iter()
754                .map(ToString::to_string)
755                .collect();
756        for i in 0..5 {
757            input_drvs.insert(format!("/nix/store/dep{i}.drv"), outputs.clone());
758        }
759        let drv = Derivation {
760            outputs: {
761                let mut m = BTreeMap::new();
762                m.insert("out".to_string(), DerivationOutput {
763                    path: "/out".to_string(),
764                    hash_algo: String::new(),
765                    hash: String::new(),
766                });
767                m
768            },
769            input_derivations: input_drvs,
770            input_sources: vec![],
771            system: "x86_64-linux".to_string(),
772            builder: "/bin/sh".to_string(),
773            args: vec![],
774            env: BTreeMap::new(),
775        };
776        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
777        assert_eq!(parsed.input_derivations.len(), 5);
778        for outs in parsed.input_derivations.values() {
779            assert_eq!(outs.len(), 9);
780        }
781        assert_eq!(parsed, drv);
782    }
783
784    #[test]
785    fn fixed_output_md5_algorithm() {
786        let mut outputs = BTreeMap::new();
787        outputs.insert("out".to_string(), DerivationOutput {
788            path: "/nix/store/abc-x".to_string(),
789            hash_algo: "md5".to_string(),
790            hash: "0123456789abcdef0123456789abcdef".to_string(),
791        });
792        let drv = Derivation {
793            outputs,
794            input_derivations: BTreeMap::new(),
795            input_sources: vec![],
796            system: "x86_64-linux".to_string(),
797            builder: "/bin/sh".to_string(),
798            args: vec![],
799            env: BTreeMap::new(),
800        };
801        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
802        assert_eq!(parsed.outputs["out"].hash_algo, "md5");
803        assert_eq!(parsed, drv);
804    }
805
806    #[test]
807    fn empty_string_in_env_key() {
808        let mut env = BTreeMap::new();
809        env.insert(String::new(), "value-with-empty-key".to_string());
810        let drv = Derivation {
811            outputs: {
812                let mut m = BTreeMap::new();
813                m.insert("out".to_string(), DerivationOutput {
814                    path: "/out".to_string(),
815                    hash_algo: String::new(),
816                    hash: String::new(),
817                });
818                m
819            },
820            input_derivations: BTreeMap::new(),
821            input_sources: vec![],
822            system: "x86_64-linux".to_string(),
823            builder: "/bin/sh".to_string(),
824            args: vec![],
825            env,
826        };
827        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
828        assert_eq!(parsed.env[""], "value-with-empty-key");
829    }
830
831    #[test]
832    fn parser_handles_zero_outputs_zero_inputs() {
833        let input = b"Derive([],[],[],\"sys\",\"build\",[],[])";
834        let drv = Derivation::parse(input).unwrap();
835        assert!(drv.outputs.is_empty());
836        assert!(drv.input_derivations.is_empty());
837        assert!(drv.input_sources.is_empty());
838        assert!(drv.args.is_empty());
839        assert!(drv.env.is_empty());
840        assert_eq!(drv.system, "sys");
841        assert_eq!(drv.builder, "build");
842    }
843
844    #[test]
845    fn default_derivation_is_empty() {
846        let drv = Derivation::default();
847        assert!(drv.outputs.is_empty());
848        assert!(drv.input_derivations.is_empty());
849        assert!(drv.input_sources.is_empty());
850        assert!(drv.system.is_empty());
851        assert!(drv.builder.is_empty());
852        assert!(drv.args.is_empty());
853        assert!(drv.env.is_empty());
854    }
855
856    #[test]
857    fn default_derivation_output_is_empty() {
858        let out = DerivationOutput::default();
859        assert!(out.path.is_empty());
860        assert!(out.hash_algo.is_empty());
861        assert!(out.hash.is_empty());
862    }
863
864    #[test]
865    fn outputs_serialized_in_btreemap_order() {
866        // BTreeMap iterates keys in sorted order — confirm serialization
867        // emits outputs alphabetically.
868        let mut outputs = BTreeMap::new();
869        for name in ["zzz", "aaa", "mmm"] {
870            outputs.insert(name.to_string(), DerivationOutput {
871                path: format!("/out-{name}"),
872                hash_algo: String::new(),
873                hash: String::new(),
874            });
875        }
876        let drv = Derivation {
877            outputs,
878            input_derivations: BTreeMap::new(),
879            input_sources: vec![],
880            system: "sys".to_string(),
881            builder: "build".to_string(),
882            args: vec![],
883            env: BTreeMap::new(),
884        };
885        let s = drv.serialize();
886        // "aaa" should come before "mmm" before "zzz"
887        let aaa = s.find("\"aaa\"").unwrap();
888        let mmm = s.find("\"mmm\"").unwrap();
889        let zzz = s.find("\"zzz\"").unwrap();
890        assert!(aaa < mmm);
891        assert!(mmm < zzz);
892    }
893
894    #[test]
895    fn env_serialized_in_btreemap_order() {
896        let mut env = BTreeMap::new();
897        for k in ["zoo", "alpha", "middle"] {
898            env.insert(k.to_string(), format!("v-{k}"));
899        }
900        let drv = Derivation {
901            outputs: {
902                let mut m = BTreeMap::new();
903                m.insert("out".to_string(), DerivationOutput {
904                    path: "/out".to_string(),
905                    hash_algo: String::new(),
906                    hash: String::new(),
907                });
908                m
909            },
910            input_derivations: BTreeMap::new(),
911            input_sources: vec![],
912            system: "sys".to_string(),
913            builder: "build".to_string(),
914            args: vec![],
915            env,
916        };
917        let s = drv.serialize();
918        let alpha = s.find("\"alpha\"").unwrap();
919        let middle = s.find("\"middle\"").unwrap();
920        let zoo = s.find("\"zoo\"").unwrap();
921        assert!(alpha < middle);
922        assert!(middle < zoo);
923    }
924
925    #[test]
926    fn complex_input_derivations() {
927        let mut input_drvs = BTreeMap::new();
928        input_drvs.insert(
929            "/nix/store/abc-gcc.drv".to_string(),
930            vec!["out".to_string(), "lib".to_string(), "info".to_string()],
931        );
932        input_drvs.insert(
933            "/nix/store/def-glibc.drv".to_string(),
934            vec!["out".to_string(), "dev".to_string(), "static".to_string()],
935        );
936        input_drvs.insert(
937            "/nix/store/ghi-bash.drv".to_string(),
938            vec!["out".to_string()],
939        );
940
941        let drv = Derivation {
942            outputs: {
943                let mut m = BTreeMap::new();
944                m.insert("out".to_string(), DerivationOutput {
945                    path: "/nix/store/result".to_string(),
946                    hash_algo: String::new(),
947                    hash: String::new(),
948                });
949                m
950            },
951            input_derivations: input_drvs,
952            input_sources: vec!["/nix/store/src".to_string()],
953            system: "x86_64-linux".to_string(),
954            builder: "/nix/store/bash/bin/bash".to_string(),
955            args: vec!["-e".to_string(), "/nix/store/builder.sh".to_string()],
956            env: {
957                let mut e = BTreeMap::new();
958                e.insert("name".to_string(), "test-pkg".to_string());
959                e.insert("version".to_string(), "1.0.0".to_string());
960                e
961            },
962        };
963        let parsed = Derivation::parse(drv.serialize().as_bytes()).unwrap();
964        assert_eq!(parsed, drv);
965        assert_eq!(parsed.input_derivations.len(), 3);
966        assert_eq!(parsed.input_derivations["/nix/store/abc-gcc.drv"].len(), 3);
967    }
968}