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 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
297fn 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
309pub(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 #[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 #[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 #[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 #[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(); assert!(Derivation::parse(serialized.as_bytes()).is_err());
574 }
575
576 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 #[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 #[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 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 let input = b"Derive([(\"out\",\"/out\",\"\",\"\")],[],[],\"x86_64-linux\",\"/bin/sh\",[],[(\"v\",\"a\\xb\")])";
712 let drv = Derivation::parse(input).unwrap();
713 assert_eq!(drv.env["v"], "a\\xb");
715 }
716
717 #[test]
718 fn parse_error_includes_position() {
719 let result = Derivation::parse(b"Derive([(\"out\"");
721 assert!(result.is_err());
722 }
723
724 #[test]
725 fn parse_invalid_outputs_format() {
726 let result = Derivation::parse(b"Derive([(\"out\",\"/out\",\"\",\"\"],[],[],\"\",\"\",[],[])");
728 assert!(result.is_err());
729 }
730
731 #[test]
732 fn parse_missing_comma_separator() {
733 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 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 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}