1use 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct DerivationOutput {
22 pub path: String,
24 pub hash_algo: String,
26 pub hash: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
32pub struct Derivation {
33 pub outputs: BTreeMap<String, DerivationOutput>,
35 pub input_derivations: BTreeMap<String, Vec<String>>,
37 pub input_sources: Vec<String>,
39 pub system: String,
41 pub builder: String,
43 pub args: Vec<String>,
45 pub env: BTreeMap<String, String>,
47}
48
49struct 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 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 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 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 #[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 #[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
283fn 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
295pub(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 #[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 #[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 #[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 #[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(); assert!(Derivation::parse(serialized.as_bytes()).is_err());
560 }
561
562 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 #[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 #[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 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 let input = b"Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",\"a\\xb\")])";
698 let drv = Derivation::parse(input).unwrap();
699 assert_eq!(drv.env["v"], "a\\xb");
701 }
702
703 #[test]
704 fn parse_error_includes_position() {
705 let result = Derivation::parse(b"Derive([(\"out\"");
707 assert!(result.is_err());
708 }
709
710 #[test]
711 fn parse_invalid_outputs_format() {
712 let result = Derivation::parse(b"Derive([(\"out\",\"/out\",\"\",\"\"],[],[],\"\",\"\",[],[])");
714 assert!(result.is_err());
715 }
716
717 #[test]
718 fn parse_missing_comma_separator() {
719 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 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 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}