1use std::io::{self, Read, Write};
10use std::path::Path;
11
12use crate::wire;
13use thiserror::Error;
14
15pub const NAR_MAGIC: &str = "nix-archive-1";
17
18pub const MAX_NAR_STRING: u64 = 4 * 1024 * 1024 * 1024;
27
28#[derive(Debug, Error)]
29pub enum NarError {
30 #[error("io error: {0}")]
31 Io(#[from] io::Error),
32 #[error("invalid NAR: {0}")]
33 Invalid(String),
34 #[error("unexpected token: expected {expected}, got {got}")]
35 UnexpectedToken { expected: String, got: String },
36}
37
38fn write_str(w: &mut impl Write, s: &[u8]) -> io::Result<()> {
45 wire::write_bytes(w, s)
46}
47
48fn read_str(r: &mut impl Read) -> Result<Vec<u8>, NarError> {
49 let len_u64 = wire::read_u64(r)?;
50 if len_u64 > MAX_NAR_STRING {
51 return Err(NarError::Invalid(format!(
52 "nar string too long: {len_u64} bytes exceeds {MAX_NAR_STRING} cap"
53 )));
54 }
55 let len = len_u64 as usize;
56 let mut buf = vec![0u8; len];
57 r.read_exact(&mut buf)?;
58 let pad = (8 - (len % 8)) % 8;
59 if pad > 0 {
60 let mut pad_buf = vec![0u8; pad];
61 r.read_exact(&mut pad_buf)?;
62 }
63 Ok(buf)
64}
65
66fn read_str_utf8(r: &mut impl Read) -> Result<String, NarError> {
67 let bytes = read_str(r)?;
68 String::from_utf8(bytes).map_err(|e| NarError::Invalid(format!("invalid UTF-8: {e}")))
69}
70
71fn expect_str(r: &mut impl Read, expected: &str) -> Result<(), NarError> {
72 let got = read_str_utf8(r)?;
73 if got != expected {
74 return Err(NarError::UnexpectedToken {
75 expected: expected.to_string(),
76 got,
77 });
78 }
79 Ok(())
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum NarNode {
87 Regular {
89 executable: bool,
91 contents: Vec<u8>,
93 },
94 Symlink {
96 target: String,
98 },
99 Directory {
101 entries: Vec<NarEntry>,
103 },
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct NarEntry {
109 pub name: String,
111 pub node: NarNode,
113}
114
115pub struct NarWriter;
119
120impl NarWriter {
121 pub fn write(w: &mut impl Write, node: &NarNode) -> Result<(), NarError> {
123 write_str(w, NAR_MAGIC.as_bytes())?;
124 Self::write_node(w, node)?;
125 Ok(())
126 }
127
128 fn write_node(w: &mut impl Write, node: &NarNode) -> Result<(), NarError> {
129 write_str(w, b"(")?;
130 match node {
131 NarNode::Regular { executable, contents } => {
132 write_str(w, b"type")?;
133 write_str(w, b"regular")?;
134 if *executable {
135 write_str(w, b"executable")?;
136 write_str(w, b"")?;
137 }
138 write_str(w, b"contents")?;
139 write_str(w, contents)?;
140 }
141 NarNode::Symlink { target } => {
142 write_str(w, b"type")?;
143 write_str(w, b"symlink")?;
144 write_str(w, b"target")?;
145 write_str(w, target.as_bytes())?;
146 }
147 NarNode::Directory { entries } => {
148 write_str(w, b"type")?;
149 write_str(w, b"directory")?;
150 for entry in entries {
151 write_str(w, b"entry")?;
152 write_str(w, b"(")?;
153 write_str(w, b"name")?;
154 write_str(w, entry.name.as_bytes())?;
155 write_str(w, b"node")?;
156 Self::write_node(w, &entry.node)?;
157 write_str(w, b")")?;
158 }
159 }
160 }
161 write_str(w, b")")?;
162 Ok(())
163 }
164
165 pub fn write_path(w: &mut impl Write, path: &Path) -> Result<(), NarError> {
167 let encoder = nix_nar::Encoder::new(path)
168 .map_err(|e| NarError::Invalid(format!("nix-nar encoder error: {e}")))?;
169 let mut reader = std::io::BufReader::new(encoder);
170 std::io::copy(&mut reader, w)?;
171 Ok(())
172 }
173}
174
175pub struct NarReader;
179
180impl NarReader {
181 pub fn read_complete(r: &mut impl Read) -> Result<NarNode, NarError> {
183 expect_str(r, NAR_MAGIC)?;
184 Self::read_node(r)
185 }
186
187 fn read_node(r: &mut impl Read) -> Result<NarNode, NarError> {
188 expect_str(r, "(")?;
189 expect_str(r, "type")?;
190 let node_type = read_str_utf8(r)?;
191
192 match node_type.as_str() {
193 "regular" => {
194 let node = Self::read_regular(r)?;
195 expect_str(r, ")")?;
196 Ok(node)
197 }
198 "symlink" => {
199 let node = Self::read_symlink(r)?;
200 expect_str(r, ")")?;
201 Ok(node)
202 }
203 "directory" => Self::read_directory(r),
204 _ => Err(NarError::Invalid(format!("unknown node type: {node_type}"))),
205 }
206 }
207
208 fn read_regular(r: &mut impl Read) -> Result<NarNode, NarError> {
209 let mut executable = false;
210 let token = read_str_utf8(r)?;
211 if token == "executable" {
212 executable = true;
213 read_str(r)?;
214 expect_str(r, "contents")?;
215 } else if token != "contents" {
216 return Err(NarError::UnexpectedToken {
217 expected: "executable or contents".to_string(),
218 got: token,
219 });
220 }
221 let contents = read_str(r)?;
222 Ok(NarNode::Regular { executable, contents })
223 }
224
225 fn read_symlink(r: &mut impl Read) -> Result<NarNode, NarError> {
226 expect_str(r, "target")?;
227 let target = read_str_utf8(r)?;
228 Ok(NarNode::Symlink { target })
229 }
230
231 fn read_directory(r: &mut impl Read) -> Result<NarNode, NarError> {
232 let mut entries = Vec::new();
233 loop {
234 let token = read_str_utf8(r)?;
235 if token == ")" {
236 return Ok(NarNode::Directory { entries });
237 }
238 if token != "entry" {
239 return Err(NarError::UnexpectedToken {
240 expected: "entry or )".to_string(),
241 got: token,
242 });
243 }
244 expect_str(r, "(")?;
245 expect_str(r, "name")?;
246 let name = read_str_utf8(r)?;
247 expect_str(r, "node")?;
248 let node = Self::read_node(r)?;
249 expect_str(r, ")")?;
250 entries.push(NarEntry { name, node });
251 }
252 }
253}
254
255pub fn unpack_nar(nar_data: &[u8], dest: &Path) -> Result<(), NarError> {
257 let decoder = nix_nar::Decoder::new(std::io::Cursor::new(nar_data))
258 .map_err(|e| NarError::Invalid(format!("nix-nar decoder error: {e}")))?;
259 decoder
260 .unpack(dest)
261 .map_err(|e| NarError::Invalid(format!("nix-nar unpack error: {e}")))
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use proptest::prelude::*;
268 use std::io::Cursor;
269
270 #[test]
271 fn roundtrip_regular_file() {
272 let node = NarNode::Regular { executable: false, contents: b"hello world".to_vec() };
273 let mut buf = Vec::new();
274 NarWriter::write(&mut buf, &node).unwrap();
275 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
276 assert_eq!(parsed, node);
277 }
278
279 #[test]
280 fn roundtrip_executable() {
281 let node = NarNode::Regular { executable: true, contents: b"#!/bin/sh\necho hi".to_vec() };
282 let mut buf = Vec::new();
283 NarWriter::write(&mut buf, &node).unwrap();
284 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
285 assert_eq!(parsed, node);
286 }
287
288 #[test]
289 fn roundtrip_symlink() {
290 let node = NarNode::Symlink { target: "/nix/store/abc-foo/bin/foo".to_string() };
291 let mut buf = Vec::new();
292 NarWriter::write(&mut buf, &node).unwrap();
293 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
294 assert_eq!(parsed, node);
295 }
296
297 #[test]
298 fn roundtrip_directory() {
299 let node = NarNode::Directory {
300 entries: vec![
301 NarEntry { name: "bar".to_string(), node: NarNode::Regular { executable: false, contents: b"bar".to_vec() } },
302 NarEntry { name: "foo".to_string(), node: NarNode::Symlink { target: "bar".to_string() } },
303 ],
304 };
305 let mut buf = Vec::new();
306 NarWriter::write(&mut buf, &node).unwrap();
307 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
308 assert_eq!(parsed, node);
309 }
310
311 #[test]
312 fn roundtrip_nested_directory() {
313 let node = NarNode::Directory {
314 entries: vec![
315 NarEntry { name: "bin".to_string(), node: NarNode::Directory {
316 entries: vec![NarEntry { name: "hello".to_string(), node: NarNode::Regular { executable: true, contents: b"ELF".to_vec() } }],
317 }},
318 NarEntry { name: "lib".to_string(), node: NarNode::Directory { entries: vec![] } },
319 ],
320 };
321 let mut buf = Vec::new();
322 NarWriter::write(&mut buf, &node).unwrap();
323 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
324 assert_eq!(parsed, node);
325 }
326
327 #[test]
328 fn nar_magic_header() {
329 let node = NarNode::Regular { executable: false, contents: vec![] };
330 let mut buf = Vec::new();
331 NarWriter::write(&mut buf, &node).unwrap();
332 let len = u64::from_le_bytes(buf[..8].try_into().unwrap());
333 assert_eq!(len, 13);
334 assert_eq!(&buf[8..21], NAR_MAGIC.as_bytes());
335 }
336
337 #[test]
338 fn eight_byte_alignment() {
339 let node = NarNode::Regular { executable: false, contents: b"hello".to_vec() };
340 let mut buf = Vec::new();
341 NarWriter::write(&mut buf, &node).unwrap();
342 assert_eq!(buf.len() % 8, 0);
343 }
344
345 #[test]
346 fn empty_file() {
347 let node = NarNode::Regular { executable: false, contents: vec![] };
348 let mut buf = Vec::new();
349 NarWriter::write(&mut buf, &node).unwrap();
350 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
351 assert_eq!(parsed, node);
352 }
353
354 #[test]
355 fn empty_directory() {
356 let node = NarNode::Directory { entries: vec![] };
357 let mut buf = Vec::new();
358 NarWriter::write(&mut buf, &node).unwrap();
359 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
360 assert_eq!(parsed, node);
361 }
362
363 #[test]
364 fn very_large_file_content() {
365 let contents = vec![0xAB; 1_000_000];
366 let node = NarNode::Regular { executable: false, contents: contents.clone() };
367 let mut buf = Vec::new();
368 NarWriter::write(&mut buf, &node).unwrap();
369 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
370 assert_eq!(parsed, node);
371 }
372
373 #[test]
374 fn deeply_nested_5_levels() {
375 let leaf = NarNode::Regular { executable: false, contents: b"deep".to_vec() };
376 let mut node = leaf;
377 for i in (0..5).rev() {
378 node = NarNode::Directory {
379 entries: vec![NarEntry { name: format!("level{i}"), node }],
380 };
381 }
382 let mut buf = Vec::new();
383 NarWriter::write(&mut buf, &node).unwrap();
384 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
385 assert_eq!(parsed, node);
386 }
387
388 #[test]
389 fn directory_with_many_entries() {
390 let entries: Vec<NarEntry> = (0..60).map(|i| NarEntry {
391 name: format!("file-{i:03}"),
392 node: NarNode::Regular { executable: false, contents: format!("content {i}").into_bytes() },
393 }).collect();
394 let node = NarNode::Directory { entries };
395 let mut buf = Vec::new();
396 NarWriter::write(&mut buf, &node).unwrap();
397 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
398 assert_eq!(parsed, node);
399 }
400
401 #[test]
402 fn write_path_on_real_temp_directory() {
403 let dir = tempfile::tempdir().unwrap();
404 std::fs::write(dir.path().join("hello.txt"), b"Hello!").unwrap();
405 std::fs::create_dir(dir.path().join("sub")).unwrap();
406 std::fs::write(dir.path().join("sub").join("nested.txt"), b"nested").unwrap();
407
408 let mut buf = Vec::new();
409 NarWriter::write_path(&mut buf, dir.path()).unwrap();
410 assert!(buf.len() > 20);
411 assert_eq!(buf.len() % 8, 0);
412 }
413
414 #[test]
415 fn mixed_node_types() {
416 let node = NarNode::Directory {
417 entries: vec![
418 NarEntry { name: "exec".to_string(), node: NarNode::Regular { executable: true, contents: b"#!/bin/sh".to_vec() } },
419 NarEntry { name: "link".to_string(), node: NarNode::Symlink { target: "exec".to_string() } },
420 NarEntry { name: "reg".to_string(), node: NarNode::Regular { executable: false, contents: b"data".to_vec() } },
421 NarEntry { name: "sub".to_string(), node: NarNode::Directory { entries: vec![] } },
422 ],
423 };
424 let mut buf = Vec::new();
425 NarWriter::write(&mut buf, &node).unwrap();
426 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
427 assert_eq!(parsed, node);
428 }
429
430 #[test]
433 fn roundtrip_executable_empty_file() {
434 let node = NarNode::Regular { executable: true, contents: vec![] };
435 let mut buf = Vec::new();
436 NarWriter::write(&mut buf, &node).unwrap();
437 assert_eq!(buf.len() % 8, 0);
438 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
439 assert_eq!(parsed, node);
440 }
441
442 #[test]
445 fn roundtrip_large_symlink_target() {
446 let long_target = "/nix/store/".to_string() + &"a".repeat(500) + "-long-package/lib/libfoo.so.1.2.3";
447 let node = NarNode::Symlink { target: long_target };
448 let mut buf = Vec::new();
449 NarWriter::write(&mut buf, &node).unwrap();
450 assert_eq!(buf.len() % 8, 0);
451 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
452 assert_eq!(parsed, node);
453 }
454
455 #[test]
458 fn deeply_nested_10_levels() {
459 let leaf = NarNode::Regular { executable: false, contents: b"leaf data".to_vec() };
460 let mut node = leaf;
461 for i in (0..10).rev() {
462 node = NarNode::Directory {
463 entries: vec![NarEntry { name: format!("d{i}"), node }],
464 };
465 }
466 let mut buf = Vec::new();
467 NarWriter::write(&mut buf, &node).unwrap();
468 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
469 assert_eq!(parsed, node);
470 }
471
472 #[test]
475 fn deeply_nested_with_siblings() {
476 let leaf_file = NarNode::Regular { executable: false, contents: b"f".to_vec() };
477 let leaf_link = NarNode::Symlink { target: "f".to_string() };
478
479 let inner = NarNode::Directory {
480 entries: vec![
481 NarEntry { name: "data".to_string(), node: leaf_file.clone() },
482 NarEntry { name: "link".to_string(), node: leaf_link },
483 ],
484 };
485 let mid = NarNode::Directory {
486 entries: vec![
487 NarEntry { name: "inner".to_string(), node: inner },
488 NarEntry { name: "readme".to_string(), node: NarNode::Regular { executable: false, contents: b"README".to_vec() } },
489 ],
490 };
491 let root = NarNode::Directory {
492 entries: vec![
493 NarEntry { name: "bin".to_string(), node: NarNode::Regular { executable: true, contents: b"#!/bin/sh".to_vec() } },
494 NarEntry { name: "lib".to_string(), node: mid },
495 ],
496 };
497
498 let mut buf = Vec::new();
499 NarWriter::write(&mut buf, &root).unwrap();
500 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
501 assert_eq!(parsed, root);
502 }
503
504 #[test]
507 fn roundtrip_binary_content_all_byte_values() {
508 let contents: Vec<u8> = (0..=255).collect();
509 let node = NarNode::Regular { executable: false, contents };
510 let mut buf = Vec::new();
511 NarWriter::write(&mut buf, &node).unwrap();
512 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
513 assert_eq!(parsed, node);
514 }
515
516 #[test]
519 fn roundtrip_symlink_with_special_chars() {
520 let node = NarNode::Symlink { target: "../foo bar/baz\ttab".to_string() };
521 let mut buf = Vec::new();
522 NarWriter::write(&mut buf, &node).unwrap();
523 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
524 assert_eq!(parsed, node);
525 }
526
527 #[test]
530 fn reader_rejects_bad_magic() {
531 let mut buf = Vec::new();
532 write_str(&mut buf, b"not-nar-magic").unwrap();
534 let result = NarReader::read_complete(&mut Cursor::new(&buf));
535 assert!(result.is_err());
536 }
537
538 #[test]
539 fn reader_rejects_empty_input() {
540 let result = NarReader::read_complete(&mut Cursor::new(&[]));
541 assert!(result.is_err());
542 }
543
544 proptest! {
547 #[test]
548 fn prop_regular_file_roundtrip(contents in proptest::collection::vec(any::<u8>(), 0..1000), executable in any::<bool>()) {
549 let node = NarNode::Regular { executable, contents };
550 let mut buf = Vec::new();
551 NarWriter::write(&mut buf, &node).unwrap();
552 prop_assert_eq!(buf.len() % 8, 0);
553 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
554 prop_assert_eq!(parsed, node);
555 }
556
557 #[test]
558 fn prop_symlink_roundtrip(target in "[a-zA-Z0-9/_.-]{1,200}") {
559 let node = NarNode::Symlink { target };
560 let mut buf = Vec::new();
561 NarWriter::write(&mut buf, &node).unwrap();
562 prop_assert_eq!(buf.len() % 8, 0);
563 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
564 prop_assert_eq!(parsed, node);
565 }
566 }
567
568 #[test]
571 fn read_str_rejects_oversized_length_prefix() {
572 let mut buf = Vec::new();
576 write_str(&mut buf, NAR_MAGIC.as_bytes()).unwrap();
578 write_str(&mut buf, b"(").unwrap();
580 write_str(&mut buf, b"type").unwrap();
582 wire::write_u64(&mut buf, MAX_NAR_STRING + 1).unwrap();
585 let result = NarReader::read_complete(&mut Cursor::new(&buf));
586 assert!(result.is_err());
587 match result {
588 Err(NarError::Invalid(s)) => assert!(s.contains("too long")),
589 other => panic!("expected Invalid error about size, got {other:?}"),
590 }
591 }
592
593 #[test]
596 fn reader_rejects_unknown_node_type() {
597 let mut buf = Vec::new();
598 write_str(&mut buf, NAR_MAGIC.as_bytes()).unwrap();
599 write_str(&mut buf, b"(").unwrap();
600 write_str(&mut buf, b"type").unwrap();
601 write_str(&mut buf, b"socket").unwrap(); let result = NarReader::read_complete(&mut Cursor::new(&buf));
603 match result {
604 Err(NarError::Invalid(s)) => assert!(s.contains("unknown node type")),
605 other => panic!("expected Invalid for unknown type, got {other:?}"),
606 }
607 }
608
609 #[test]
612 fn reader_rejects_regular_with_wrong_token() {
613 let mut buf = Vec::new();
614 write_str(&mut buf, NAR_MAGIC.as_bytes()).unwrap();
615 write_str(&mut buf, b"(").unwrap();
616 write_str(&mut buf, b"type").unwrap();
617 write_str(&mut buf, b"regular").unwrap();
618 write_str(&mut buf, b"garbage").unwrap(); let result = NarReader::read_complete(&mut Cursor::new(&buf));
620 match result {
621 Err(NarError::UnexpectedToken { expected, .. }) => {
622 assert!(expected.contains("executable") || expected.contains("contents"));
623 }
624 other => panic!("expected UnexpectedToken, got {other:?}"),
625 }
626 }
627
628 #[test]
631 fn reader_rejects_directory_with_wrong_token() {
632 let mut buf = Vec::new();
633 write_str(&mut buf, NAR_MAGIC.as_bytes()).unwrap();
634 write_str(&mut buf, b"(").unwrap();
635 write_str(&mut buf, b"type").unwrap();
636 write_str(&mut buf, b"directory").unwrap();
637 write_str(&mut buf, b"garbage").unwrap(); let result = NarReader::read_complete(&mut Cursor::new(&buf));
639 match result {
640 Err(NarError::UnexpectedToken { expected, .. }) => {
641 assert!(expected.contains("entry") || expected.contains(")"));
642 }
643 other => panic!("expected UnexpectedToken, got {other:?}"),
644 }
645 }
646
647 #[test]
650 fn roundtrip_kilobyte_symlink_target() {
651 let target: String = "abcdefgh".repeat(150); assert!(target.len() >= 1024);
653 let node = NarNode::Symlink { target };
654 let mut buf = Vec::new();
655 NarWriter::write(&mut buf, &node).unwrap();
656 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
657 assert_eq!(parsed, node);
658 }
659
660 #[test]
663 fn directory_with_100_entries_roundtrip() {
664 let entries: Vec<NarEntry> = (0..120)
665 .map(|i| NarEntry {
666 name: format!("file-{i:04}"),
667 node: NarNode::Regular {
668 executable: false,
669 contents: format!("body-{i}").into_bytes(),
670 },
671 })
672 .collect();
673 let node = NarNode::Directory { entries };
674 let mut buf = Vec::new();
675 NarWriter::write(&mut buf, &node).unwrap();
676 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
677 assert_eq!(parsed, node);
678 }
679
680 #[test]
683 fn roundtrip_file_with_all_256_byte_values() {
684 let contents: Vec<u8> = (0..=255).collect();
685 assert_eq!(contents.len(), 256);
686 let node = NarNode::Regular { executable: false, contents };
687 let mut buf = Vec::new();
688 NarWriter::write(&mut buf, &node).unwrap();
689 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
690 assert_eq!(parsed, node);
691 }
692
693 #[test]
696 fn nar_error_invalid_display() {
697 let err = NarError::Invalid("custom".to_string());
698 let s = format!("{err}");
699 assert!(s.contains("custom"));
700 }
701
702 #[test]
703 fn nar_error_unexpected_token_display() {
704 let err = NarError::UnexpectedToken {
705 expected: "foo".to_string(),
706 got: "bar".to_string(),
707 };
708 let s = format!("{err}");
709 assert!(s.contains("foo"));
710 assert!(s.contains("bar"));
711 }
712
713 #[test]
716 fn nar_magic_is_nix_archive_1() {
717 assert_eq!(NAR_MAGIC, "nix-archive-1");
718 assert_eq!(NAR_MAGIC.len(), 13);
719 }
720
721 #[test]
722 fn max_nar_string_is_4gib() {
723 assert_eq!(MAX_NAR_STRING, 4 * 1024 * 1024 * 1024);
724 }
725
726 #[test]
729 fn unpack_nar_roundtrip_single_file() {
730 let dir = tempfile::tempdir().unwrap();
731 let src = dir.path().join("src");
732 std::fs::create_dir(&src).unwrap();
733 std::fs::write(src.join("hello.txt"), b"hello world").unwrap();
734
735 let mut nar_data = Vec::new();
736 NarWriter::write_path(&mut nar_data, &src).unwrap();
737
738 let dest = dir.path().join("dest");
739 unpack_nar(&nar_data, &dest).unwrap();
740
741 let restored = std::fs::read(dest.join("hello.txt")).unwrap();
742 assert_eq!(restored, b"hello world");
743 }
744
745 #[test]
748 fn write_path_on_single_file() {
749 let dir = tempfile::tempdir().unwrap();
750 let path = dir.path().join("plain.txt");
751 std::fs::write(&path, b"plain content").unwrap();
752
753 let mut buf = Vec::new();
754 NarWriter::write_path(&mut buf, &path).unwrap();
755 assert!(buf.len() >= 8);
756 assert_eq!(buf.len() % 8, 0);
757 }
758
759 #[test]
762 fn magic_header_layout() {
763 let node = NarNode::Regular { executable: false, contents: vec![] };
764 let mut buf = Vec::new();
765 NarWriter::write(&mut buf, &node).unwrap();
766 let len = u64::from_le_bytes(buf[..8].try_into().unwrap());
768 assert_eq!(len, NAR_MAGIC.len() as u64);
769 assert_eq!(&buf[8..21], NAR_MAGIC.as_bytes());
771 assert_eq!(&buf[21..24], &[0u8, 0u8, 0u8]);
773 }
774
775 #[test]
778 fn nar_node_equality_and_clone() {
779 let n1 = NarNode::Regular { executable: false, contents: vec![1, 2, 3] };
780 let n2 = n1.clone();
781 assert_eq!(n1, n2);
782 let n3 = NarNode::Regular { executable: true, contents: vec![1, 2, 3] };
783 assert_ne!(n1, n3);
784 }
785
786 #[test]
787 fn nar_entry_equality_and_clone() {
788 let e1 = NarEntry {
789 name: "x".to_string(),
790 node: NarNode::Symlink { target: "y".to_string() },
791 };
792 let e2 = e1.clone();
793 assert_eq!(e1, e2);
794 }
795
796 #[test]
799 fn roundtrip_empty_symlink_target() {
800 let node = NarNode::Symlink { target: String::new() };
801 let mut buf = Vec::new();
802 NarWriter::write(&mut buf, &node).unwrap();
803 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
804 assert_eq!(parsed, node);
805 }
806
807 #[test]
810 fn nested_directory_with_executable_inside() {
811 let node = NarNode::Directory {
812 entries: vec![
813 NarEntry {
814 name: "bin".to_string(),
815 node: NarNode::Directory {
816 entries: vec![NarEntry {
817 name: "tool".to_string(),
818 node: NarNode::Regular {
819 executable: true,
820 contents: b"binary content".to_vec(),
821 },
822 }],
823 },
824 },
825 ],
826 };
827 let mut buf = Vec::new();
828 NarWriter::write(&mut buf, &node).unwrap();
829 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
830 assert_eq!(parsed, node);
831 }
832
833 proptest! {
836 #[test]
837 fn prop_directory_entries_roundtrip(
838 count in 0_usize..=20,
839 ) {
840 let entries: Vec<NarEntry> = (0..count).map(|i| NarEntry {
841 name: format!("e{i:03}"),
842 node: NarNode::Regular {
843 executable: i % 2 == 0,
844 contents: vec![i as u8; i],
845 },
846 }).collect();
847 let node = NarNode::Directory { entries };
848 let mut buf = Vec::new();
849 NarWriter::write(&mut buf, &node).unwrap();
850 prop_assert_eq!(buf.len() % 8, 0);
851 let parsed = NarReader::read_complete(&mut Cursor::new(&buf)).unwrap();
852 prop_assert_eq!(parsed, node);
853 }
854 }
855}