1use crate::hash;
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7pub const STORE_PATH_HASH_LEN: usize = 32;
9
10pub const DEFAULT_STORE_DIR: &str = "/nix/store";
12
13const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
15
16#[derive(Debug, Error)]
17pub enum StorePathError {
18 #[error("invalid store path: {0}")]
19 Invalid(String),
20 #[error("invalid hash length: expected {expected}, got {got}")]
21 InvalidHashLength { expected: usize, got: usize },
22 #[error("invalid character in hash: {0}")]
23 InvalidHashChar(char),
24 #[error("empty name")]
25 EmptyName,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct StorePath {
31 pub digest: [u8; 20],
33 pub name: String,
35}
36
37impl StorePath {
38 pub fn from_absolute_path(path: &str) -> Result<Self, StorePathError> {
40 let rest = path
41 .strip_prefix(DEFAULT_STORE_DIR)
42 .and_then(|s| s.strip_prefix('/'))
43 .ok_or_else(|| StorePathError::Invalid(path.to_string()))?;
44
45 Self::from_basename(rest)
46 }
47
48 pub fn from_basename(basename: &str) -> Result<Self, StorePathError> {
50 if basename.len() < STORE_PATH_HASH_LEN + 2 {
51 return Err(StorePathError::Invalid(basename.to_string()));
52 }
53
54 let hash_str = &basename[..STORE_PATH_HASH_LEN];
55 let sep = basename.as_bytes()[STORE_PATH_HASH_LEN];
56 let name = &basename[STORE_PATH_HASH_LEN + 1..];
57
58 if sep != b'-' {
59 return Err(StorePathError::Invalid(basename.to_string()));
60 }
61 if name.is_empty() {
62 return Err(StorePathError::EmptyName);
63 }
64
65 let digest = nix_base32_decode(hash_str)?;
66
67 Ok(Self {
68 digest,
69 name: name.to_string(),
70 })
71 }
72
73 #[must_use]
75 pub fn to_absolute_path(&self) -> String {
76 format!("{}/{}", DEFAULT_STORE_DIR, self.to_basename())
77 }
78
79 #[must_use]
81 pub fn to_basename(&self) -> String {
82 format!("{}-{}", nix_base32_encode(&self.digest), self.name)
83 }
84
85 #[must_use]
90 pub fn hash(&self) -> String {
91 nix_base32_encode(&self.digest)
92 }
93
94 #[must_use]
97 pub fn name(&self) -> &str {
98 &self.name
99 }
100}
101
102impl std::fmt::Display for StorePath {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 write!(f, "{}", self.to_absolute_path())
105 }
106}
107
108impl std::str::FromStr for StorePath {
109 type Err = StorePathError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 Self::from_absolute_path(s)
113 }
114}
115
116#[must_use]
126pub fn nix_base32_encode(input: &[u8]) -> String {
127 let len = (input.len() * 8).div_ceil(5);
128 let mut out = String::with_capacity(len);
129
130 for n in 0..len {
131 let b = (len - 1 - n) * 5;
132 let i = b / 8;
133 let j = b % 8;
134 let mut c = u16::from(input[i]) >> j;
135 if i + 1 < input.len() {
136 c |= u16::from(input[i + 1]) << (8 - j);
137 }
138 out.push(NIX_BASE32_CHARS[(c & 0x1f) as usize] as char);
139 }
140
141 out
142}
143
144pub fn nix_base32_decode(input: &str) -> Result<[u8; 20], StorePathError> {
148 let expected_len = 32; if input.len() != expected_len {
150 return Err(StorePathError::InvalidHashLength {
151 expected: expected_len,
152 got: input.len(),
153 });
154 }
155
156 let mut bytes = [0u8; 20];
157 let total = input.len();
158
159 for (n, c) in input.chars().enumerate() {
160 let digit = NIX_BASE32_CHARS
161 .iter()
162 .position(|&x| x == c as u8)
163 .ok_or(StorePathError::InvalidHashChar(c))?;
164 let b = (total - 1 - n) * 5;
165 let i = b / 8;
166 let j = b % 8;
167 bytes[i] |= ((digit as u16) << j) as u8;
168 if i + 1 < bytes.len() {
169 bytes[i + 1] |= ((digit as u16) >> (8 - j)) as u8;
170 }
171 }
172
173 Ok(bytes)
174}
175
176#[must_use]
199pub fn compress_hash(hash: &[u8], output_len: usize) -> Vec<u8> {
200 let mut out = vec![0u8; output_len];
201 for (i, b) in hash.iter().enumerate() {
202 out[i % output_len] ^= *b;
203 }
204 out
205}
206
207#[must_use]
213pub fn compute_store_path_from_fingerprint(fingerprint: &str, name: &str) -> String {
214 let hash = Sha256::digest(fingerprint.as_bytes());
215 let compressed = compress_hash(&hash, 20);
216 let b32 = nix_base32_encode(&compressed);
217 format!("{DEFAULT_STORE_DIR}/{b32}-{name}")
218}
219
220#[must_use]
232pub fn compute_drv_path(drv_content: &[u8], name: &str) -> String {
233 compute_drv_path_with_refs(drv_content, name, &[])
234}
235
236#[must_use]
254pub fn compute_drv_path_with_refs(drv_content: &[u8], name: &str, refs: &[String]) -> String {
255 let inner = Sha256::digest(drv_content);
256 let inner_hex = hash::hex::encode(&inner);
257 let drv_name = format!("{name}.drv");
258
259 let mut sorted_refs: Vec<&String> = refs.iter().collect();
262 sorted_refs.sort();
263 sorted_refs.dedup();
264
265 let mut fingerprint = String::from("text:");
266 for r in sorted_refs {
267 fingerprint.push_str(r);
268 fingerprint.push(':');
269 }
270 fingerprint.push_str("sha256:");
271 fingerprint.push_str(&inner_hex);
272 fingerprint.push(':');
273 fingerprint.push_str(DEFAULT_STORE_DIR);
274 fingerprint.push(':');
275 fingerprint.push_str(&drv_name);
276
277 compute_store_path_from_fingerprint(&fingerprint, &drv_name)
278}
279
280#[must_use]
285pub fn compute_output_path(inner_hash_hex: &str, output_name: &str, name: &str) -> String {
286 let full_name = if output_name == "out" {
287 name.to_string()
288 } else {
289 format!("{name}-{output_name}")
290 };
291 let fingerprint = format!(
292 "output:{output_name}:sha256:{inner_hash_hex}:{DEFAULT_STORE_DIR}:{full_name}"
293 );
294 compute_store_path_from_fingerprint(&fingerprint, &full_name)
295}
296
297#[must_use]
313pub fn compute_fixed_output_hash(
314 algo: &str,
315 hash: &str,
316 is_recursive: bool,
317 name: &str,
318) -> String {
319 if is_recursive && algo == "sha256" {
320 let fingerprint = format!(
323 "source:sha256:{hash}:{DEFAULT_STORE_DIR}:{name}"
324 );
325 return compute_store_path_from_fingerprint(&fingerprint, name);
326 }
327
328 let mode = if is_recursive { "r:" } else { "" };
329 let inner = format!("fixed:out:{mode}{algo}:{hash}:");
330 let inner_hash = Sha256::digest(inner.as_bytes());
331 let inner_hex = hash::hex::encode(&inner_hash);
332 compute_output_path(&inner_hex, "out", name)
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use proptest::prelude::*;
339
340 #[test]
341 fn parse_absolute_path() {
342 let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
343 let sp = StorePath::from_absolute_path(path).unwrap();
344 assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
345 assert_eq!(sp.to_absolute_path(), path);
346 }
347
348 #[test]
349 fn roundtrip_base32() {
350 let input = [0u8; 20];
351 let encoded = nix_base32_encode(&input);
352 assert_eq!(encoded.len(), 32);
353 let decoded = nix_base32_decode(&encoded).unwrap();
354 assert_eq!(decoded, input);
355 }
356
357 #[test]
358 fn reject_invalid_path() {
359 assert!(StorePath::from_absolute_path("/tmp/foo").is_err());
360 assert!(StorePath::from_absolute_path("/nix/store/short").is_err());
361 }
362
363 #[test]
364 fn invalid_base32_chars_e_and_u() {
365 let with_e = "00bgd045z0d4icpbc2yye4gx48ak44la";
368 assert!(nix_base32_decode(with_e).is_err());
369
370 let with_u = "00bgd045z0d4icpbc2yyu4gx48ak44la";
371 assert!(nix_base32_decode(with_u).is_err());
372
373 match nix_base32_decode("e0000000000000000000000000000000") {
375 Err(StorePathError::InvalidHashChar(c)) => assert_eq!(c, 'e'),
376 other => panic!("expected InvalidHashChar('e'), got {other:?}"),
377 }
378 match nix_base32_decode("u0000000000000000000000000000000") {
379 Err(StorePathError::InvalidHashChar(c)) => assert_eq!(c, 'u'),
380 other => panic!("expected InvalidHashChar('u'), got {other:?}"),
381 }
382 }
383
384 #[test]
385 fn path_with_minimum_valid_name_length() {
386 let hash = nix_base32_encode(&[0u8; 20]);
388 let basename = format!("{hash}-x");
389 let sp = StorePath::from_basename(&basename).unwrap();
390 assert_eq!(sp.name, "x");
391 }
392
393 #[test]
394 fn path_with_special_characters_in_name() {
395 let hash = nix_base32_encode(&[1u8; 20]);
396 let basename = format!("{hash}-my-pkg_v1.2.3+git");
398 let sp = StorePath::from_basename(&basename).unwrap();
399 assert_eq!(sp.name, "my-pkg_v1.2.3+git");
400 }
401
402 #[test]
403 fn basename_roundtrip_real_world_examples() {
404 let examples = [
405 "00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1",
406 "3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8",
407 "sn5lbjwwmkbzj7cx0hfnlwf4sh16cll6-hello-2.12.1",
408 ];
409
410 for basename in examples {
411 let sp = StorePath::from_basename(basename).unwrap();
412 assert_eq!(sp.to_basename(), basename, "roundtrip failed for {basename}");
413 }
414 }
415
416 #[test]
417 fn store_path_display_trait() {
418 let path_str = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
419 let sp = StorePath::from_absolute_path(path_str).unwrap();
420 let displayed = format!("{sp}");
421 assert_eq!(displayed, path_str);
422 }
423
424 #[test]
425 fn empty_name_rejected() {
426 let hash = nix_base32_encode(&[0u8; 20]);
427 let basename = format!("{hash}-");
429 assert!(StorePath::from_basename(&basename).is_err());
430 }
431
432 #[test]
433 fn base32_encode_decode_roundtrip_various() {
434 let test_cases: [[u8; 20]; 4] = [
435 [0u8; 20],
436 [0xff; 20],
437 [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc,
438 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0xde, 0xad, 0xbe, 0xef],
439 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
440 ];
441
442 for input in &test_cases {
443 let encoded = nix_base32_encode(input);
444 assert_eq!(encoded.len(), 32);
445 let decoded = nix_base32_decode(&encoded).unwrap();
446 assert_eq!(&decoded, input);
447 }
448 }
449
450 #[test]
451 fn wrong_hash_length_rejected() {
452 assert!(nix_base32_decode("abc").is_err());
454 assert!(nix_base32_decode("000000000000000000000000000000000").is_err());
456 match nix_base32_decode("abc") {
458 Err(StorePathError::InvalidHashLength { expected: 32, got: 3 }) => {}
459 other => panic!("expected InvalidHashLength, got {other:?}"),
460 }
461 }
462
463 #[test]
466 fn compress_hash_zero_input_zero_output() {
467 let zeros = [0u8; 32];
468 let out = compress_hash(&zeros, 20);
469 assert_eq!(out, vec![0u8; 20]);
470 }
471
472 #[test]
473 fn compress_hash_xor_fold_layout() {
474 let mut input = [0u8; 32];
476 input[0] = 0xAA;
477 input[20] = 0x55;
478 let out = compress_hash(&input, 20);
480 assert_eq!(out[0], 0xFF);
481 for i in 12..20 {
483 assert_eq!(out[i], 0);
484 }
485 }
486
487 #[test]
488 fn compress_hash_identity_when_lengths_match() {
489 let input: [u8; 20] = [
491 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
492 ];
493 let out = compress_hash(&input, 20);
494 assert_eq!(out, input.to_vec());
495 }
496
497 #[test]
498 fn compress_hash_output_length_respected() {
499 let input = [0xFFu8; 64];
500 for &target_len in &[1usize, 5, 10, 20, 32] {
501 let out = compress_hash(&input, target_len);
502 assert_eq!(out.len(), target_len);
503 }
504 }
505
506 #[test]
509 fn fingerprint_path_format() {
510 let path = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:hello.drv", "hello.drv");
511 assert!(path.starts_with("/nix/store/"));
513 let basename = path.strip_prefix("/nix/store/").unwrap();
514 assert_eq!(basename.len(), STORE_PATH_HASH_LEN + 1 + "hello.drv".len());
515 assert!(basename.ends_with("-hello.drv"));
516 let hash = &basename[..STORE_PATH_HASH_LEN];
518 for c in hash.chars() {
519 assert!(NIX_BASE32_CHARS.contains(&(c as u8)), "invalid char: {c}");
520 }
521 }
522
523 #[test]
524 fn fingerprint_path_deterministic() {
525 let p1 = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:foo", "foo");
526 let p2 = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:foo", "foo");
527 assert_eq!(p1, p2);
528 }
529
530 #[test]
531 fn fingerprint_path_changes_with_input() {
532 let p1 = compute_store_path_from_fingerprint("a", "x");
533 let p2 = compute_store_path_from_fingerprint("b", "x");
534 assert_ne!(p1, p2);
535 }
536
537 #[test]
540 fn drv_path_format() {
541 let path = compute_drv_path(b"some-aterm-content", "hello");
542 assert!(path.starts_with("/nix/store/"));
543 assert!(path.ends_with("-hello.drv"));
544 let basename = path.strip_prefix("/nix/store/").unwrap();
545 assert_eq!(basename.len(), STORE_PATH_HASH_LEN + 1 + "hello.drv".len());
546 }
547
548 #[test]
549 fn drv_path_deterministic() {
550 let p1 = compute_drv_path(b"content", "name");
551 let p2 = compute_drv_path(b"content", "name");
552 assert_eq!(p1, p2);
553 }
554
555 #[test]
556 fn drv_path_changes_with_content() {
557 let p1 = compute_drv_path(b"a", "name");
558 let p2 = compute_drv_path(b"b", "name");
559 assert_ne!(p1, p2);
560 }
561
562 #[test]
563 fn drv_path_changes_with_name() {
564 let p1 = compute_drv_path(b"content", "foo");
565 let p2 = compute_drv_path(b"content", "bar");
566 assert_ne!(p1, p2);
567 }
568
569 #[test]
572 fn output_path_out_uses_bare_name() {
573 let path = compute_output_path("0123456789abcdef", "out", "hello");
574 assert!(path.ends_with("-hello"));
575 assert!(!path.ends_with("-hello-out"));
577 }
578
579 #[test]
580 fn output_path_named_output_uses_suffix() {
581 let path = compute_output_path("0123456789abcdef", "dev", "hello");
582 assert!(path.ends_with("-hello-dev"));
583 }
584
585 #[test]
586 fn output_path_deterministic() {
587 let p1 = compute_output_path("0123456789abcdef", "out", "hello");
588 let p2 = compute_output_path("0123456789abcdef", "out", "hello");
589 assert_eq!(p1, p2);
590 }
591
592 #[test]
593 fn output_path_changes_with_inner_hash() {
594 let p1 = compute_output_path("0000000000000000", "out", "hello");
595 let p2 = compute_output_path("ffffffffffffffff", "out", "hello");
596 assert_ne!(p1, p2);
597 }
598
599 #[test]
600 fn multiple_outputs_produce_distinct_paths() {
601 let inner = "deadbeef";
602 let p_out = compute_output_path(inner, "out", "lib");
603 let p_dev = compute_output_path(inner, "dev", "lib");
604 let p_man = compute_output_path(inner, "man", "lib");
605 assert_ne!(p_out, p_dev);
606 assert_ne!(p_out, p_man);
607 assert_ne!(p_dev, p_man);
608 }
609
610 #[test]
613 fn fixed_output_flat_path_format() {
614 let path = compute_fixed_output_hash(
615 "sha256",
616 "1b0ri5lsf45dknj8bfxi1syz35kmab77apxxg1yrf33la1qm3kc7",
617 false,
618 "src.tar.gz",
619 );
620 assert!(path.starts_with("/nix/store/"));
621 assert!(path.ends_with("-src.tar.gz"));
622 }
623
624 #[test]
625 fn fixed_output_recursive_differs_from_flat() {
626 let flat = compute_fixed_output_hash("sha256", "abc", false, "thing");
627 let rec = compute_fixed_output_hash("sha256", "abc", true, "thing");
628 assert_ne!(flat, rec);
629 }
630
631 #[test]
632 fn fixed_output_deterministic() {
633 let p1 = compute_fixed_output_hash("sha256", "deadbeef", false, "thing");
634 let p2 = compute_fixed_output_hash("sha256", "deadbeef", false, "thing");
635 assert_eq!(p1, p2);
636 }
637
638 #[test]
639 fn fixed_output_changes_with_hash_value() {
640 let p1 = compute_fixed_output_hash("sha256", "aaa", false, "thing");
641 let p2 = compute_fixed_output_hash("sha256", "bbb", false, "thing");
642 assert_ne!(p1, p2);
643 }
644
645 #[test]
646 fn fixed_output_changes_with_algo() {
647 let p1 = compute_fixed_output_hash("sha256", "abc", false, "thing");
648 let p2 = compute_fixed_output_hash("sha1", "abc", false, "thing");
649 assert_ne!(p1, p2);
650 }
651
652 #[test]
655 fn hex_encode_basic() {
656 assert_eq!(crate::hash::hex::encode(&[0x00, 0xff, 0xab]), "00ffab");
657 assert_eq!(crate::hash::hex::encode(&[]), "");
658 assert_eq!(crate::hash::hex::encode(&[0x12, 0x34, 0x56, 0x78]), "12345678");
659 }
660
661 #[test]
664 fn base32_encode_output_length_formula() {
665 for input_len in [0, 1, 5, 10, 16, 20, 32, 64] {
666 let input = vec![0xAB_u8; input_len];
667 let encoded = nix_base32_encode(&input);
668 let expected_len = (input_len * 8 + 4) / 5;
669 assert_eq!(
670 encoded.len(),
671 expected_len,
672 "wrong encode length for {input_len}-byte input"
673 );
674 }
675 }
676
677 #[test]
678 fn base32_encode_alphabet_only() {
679 for input_len in [5, 10, 20, 32, 64] {
680 let input = vec![0xFF_u8; input_len];
681 let encoded = nix_base32_encode(&input);
682 for c in encoded.chars() {
683 assert!(
684 NIX_BASE32_CHARS.contains(&(c as u8)),
685 "char '{c}' not in nix base32 alphabet (input_len={input_len})"
686 );
687 }
688 }
689 }
690
691 #[test]
692 fn base32_encode_all_zero_bytes() {
693 for input_len in [5, 10, 20, 32, 64] {
694 let input = vec![0x00_u8; input_len];
695 let encoded = nix_base32_encode(&input);
696 assert!(
697 encoded.chars().all(|c| c == '0'),
698 "all-zero {input_len}-byte input should encode to all '0's, got: {encoded}"
699 );
700 }
701 }
702
703 #[test]
704 fn base32_encode_all_ff_bytes() {
705 for input_len in [5, 10, 20, 32, 64] {
706 let input = vec![0xFF_u8; input_len];
707 let encoded = nix_base32_encode(&input);
708 assert!(
709 !encoded.is_empty(),
710 "encoding of all-0xFF input should be non-empty"
711 );
712 assert!(
713 encoded.chars().all(|c| NIX_BASE32_CHARS.contains(&(c as u8))),
714 "all chars must be in alphabet"
715 );
716 }
717 }
718
719 #[test]
720 fn base32_encode_alternating_bytes() {
721 let input: Vec<u8> = (0..32).map(|i| if i % 2 == 0 { 0xAA } else { 0x55 }).collect();
722 let encoded = nix_base32_encode(&input);
723 let expected_len = (32 * 8 + 4) / 5;
724 assert_eq!(encoded.len(), expected_len);
725 for c in encoded.chars() {
726 assert!(NIX_BASE32_CHARS.contains(&(c as u8)));
727 }
728 }
729
730 #[test]
731 fn base32_encode_empty_input() {
732 let encoded = nix_base32_encode(&[]);
733 assert_eq!(encoded, "");
734 }
735
736 #[test]
737 fn base32_encode_single_byte() {
738 let encoded = nix_base32_encode(&[0x42]);
739 assert_eq!(encoded.len(), 2);
740 let decoded_manual = nix_base32_encode(&[0x42]);
741 assert_eq!(encoded, decoded_manual);
742 }
743
744 #[test]
745 fn base32_roundtrip_20_byte_boundary_cases() {
746 let cases: Vec<[u8; 20]> = vec![
747 [0x00; 20],
748 [0xFF; 20],
749 [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55,
750 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
751 {
752 let mut a = [0u8; 20];
753 a[0] = 0x01;
754 a
755 },
756 {
757 let mut a = [0u8; 20];
758 a[19] = 0x01;
759 a
760 },
761 {
762 let mut a = [0u8; 20];
763 for (i, v) in a.iter_mut().enumerate() {
764 *v = i as u8;
765 }
766 a
767 },
768 ];
769
770 for input in &cases {
771 let encoded = nix_base32_encode(input);
772 assert_eq!(encoded.len(), 32);
773 let decoded = nix_base32_decode(&encoded).unwrap();
774 assert_eq!(&decoded, input, "roundtrip failed for {input:?}");
775 }
776 }
777
778 #[test]
781 fn drv_path_with_refs_includes_refs_in_fingerprint() {
782 let content = b"Derive(...)";
783 let no_refs = compute_drv_path_with_refs(content, "hello", &[]);
784 let with_refs = compute_drv_path_with_refs(
785 content,
786 "hello",
787 &["/nix/store/abc-dep".to_string()],
788 );
789 assert_ne!(no_refs, with_refs);
790 }
791
792 #[test]
793 fn drv_path_with_refs_order_independent() {
794 let content = b"Derive(...)";
795 let refs_a = vec![
796 "/nix/store/bbb-b".to_string(),
797 "/nix/store/aaa-a".to_string(),
798 ];
799 let refs_b = vec![
800 "/nix/store/aaa-a".to_string(),
801 "/nix/store/bbb-b".to_string(),
802 ];
803 let p1 = compute_drv_path_with_refs(content, "test", &refs_a);
804 let p2 = compute_drv_path_with_refs(content, "test", &refs_b);
805 assert_eq!(p1, p2, "ref order should not affect output");
806 }
807
808 #[test]
809 fn drv_path_with_refs_deduplicates() {
810 let content = b"Derive(...)";
811 let with_dups = vec![
812 "/nix/store/aaa-a".to_string(),
813 "/nix/store/aaa-a".to_string(),
814 ];
815 let without_dups = vec!["/nix/store/aaa-a".to_string()];
816 let p1 = compute_drv_path_with_refs(content, "test", &with_dups);
817 let p2 = compute_drv_path_with_refs(content, "test", &without_dups);
818 assert_eq!(p1, p2, "duplicate refs should be deduplicated");
819 }
820
821 proptest! {
824 #[test]
825 fn prop_base32_roundtrip_20_bytes(bytes in proptest::collection::vec(any::<u8>(), 20)) {
826 let arr: [u8; 20] = bytes.try_into().unwrap();
827 let encoded = nix_base32_encode(&arr);
828 prop_assert_eq!(encoded.len(), 32);
829 let decoded = nix_base32_decode(&encoded).unwrap();
830 prop_assert_eq!(decoded, arr);
831 }
832
833 #[test]
834 fn prop_base32_encode_uses_only_nix_alphabet(bytes in proptest::collection::vec(any::<u8>(), 1..=64)) {
835 let encoded = nix_base32_encode(&bytes);
836 for c in encoded.chars() {
837 prop_assert!(NIX_BASE32_CHARS.contains(&(c as u8)), "invalid char: {}", c);
838 }
839 }
840
841 #[test]
842 fn prop_compress_hash_output_length(
843 bytes in proptest::collection::vec(any::<u8>(), 1..=64),
844 target_len in 1_usize..=32
845 ) {
846 let out = compress_hash(&bytes, target_len);
847 prop_assert_eq!(out.len(), target_len);
848 }
849
850 #[test]
851 fn prop_store_path_roundtrip(digest in proptest::collection::vec(any::<u8>(), 20)) {
852 let arr: [u8; 20] = digest.try_into().unwrap();
853 let sp = StorePath { digest: arr, name: "test-pkg".to_string() };
854 let abs = sp.to_absolute_path();
855 let reparsed = StorePath::from_absolute_path(&abs).unwrap();
856 prop_assert_eq!(reparsed.digest, sp.digest);
857 prop_assert_eq!(reparsed.name, sp.name);
858 }
859
860 #[test]
866 fn prop_base32_encode_length_5(bytes in proptest::collection::vec(any::<u8>(), 5)) {
867 let encoded = nix_base32_encode(&bytes);
868 prop_assert_eq!(encoded.len(), 8);
870 }
871
872 #[test]
873 fn prop_base32_encode_length_10(bytes in proptest::collection::vec(any::<u8>(), 10)) {
874 let encoded = nix_base32_encode(&bytes);
875 prop_assert_eq!(encoded.len(), 16);
877 }
878
879 #[test]
880 fn prop_base32_encode_length_32(bytes in proptest::collection::vec(any::<u8>(), 32)) {
881 let encoded = nix_base32_encode(&bytes);
882 prop_assert_eq!(encoded.len(), 52);
884 }
885
886 #[test]
887 fn prop_base32_encode_length_64(bytes in proptest::collection::vec(any::<u8>(), 64)) {
888 let encoded = nix_base32_encode(&bytes);
889 prop_assert_eq!(encoded.len(), 103);
891 }
892
893 #[test]
894 fn prop_base32_encode_uses_alphabet_only(bytes in proptest::collection::vec(any::<u8>(), 1..=128)) {
895 let encoded = nix_base32_encode(&bytes);
896 for c in encoded.chars() {
897 prop_assert!(NIX_BASE32_CHARS.contains(&(c as u8)));
898 }
899 }
900
901 #[test]
903 fn prop_drv_path_deterministic(
904 content in proptest::collection::vec(any::<u8>(), 0..200),
905 name in "[a-z][a-z0-9-]{0,30}",
906 ) {
907 let p1 = compute_drv_path(&content, &name);
908 let p2 = compute_drv_path(&content, &name);
909 prop_assert_eq!(p1, p2);
910 }
911
912 #[test]
914 fn prop_drv_path_with_refs_permutation_invariant(
915 content in proptest::collection::vec(any::<u8>(), 0..50),
916 name in "[a-z]{1,10}",
917 n_refs in 0_usize..=8,
918 ) {
919 let refs: Vec<String> = (0..n_refs).map(|i| format!("/nix/store/r{i}-x")).collect();
920 let mut shuffled = refs.clone();
921 shuffled.reverse();
922 let p1 = compute_drv_path_with_refs(&content, &name, &refs);
923 let p2 = compute_drv_path_with_refs(&content, &name, &shuffled);
924 prop_assert_eq!(p1, p2);
925 }
926 }
927
928 #[test]
931 fn from_basename_unicode_in_name_rejected_or_accepted() {
932 let hash = nix_base32_encode(&[0u8; 20]);
935 let basename = format!("{hash}-héllo");
936 let sp = StorePath::from_basename(&basename).unwrap();
937 assert_eq!(sp.name, "héllo");
938 }
939
940 #[test]
941 fn from_absolute_path_without_leading_slash_rejected() {
942 assert!(StorePath::from_absolute_path("nix/store/abc").is_err());
943 }
944
945 #[test]
946 fn from_absolute_path_with_extra_path_segments_rejected() {
947 let hash = nix_base32_encode(&[0u8; 20]);
949 let path = format!("/nix/store/{hash}-name/extra");
950 let sp = StorePath::from_absolute_path(&path).unwrap();
953 assert_eq!(sp.name, "name/extra");
954 }
955
956 #[test]
957 fn store_path_hash_trait_works_in_hashset() {
958 use std::collections::HashSet;
959 let p1 = StorePath {
960 digest: [1; 20],
961 name: "x".to_string(),
962 };
963 let p2 = p1.clone();
964 let p3 = StorePath {
965 digest: [2; 20],
966 name: "x".to_string(),
967 };
968 let mut set = HashSet::new();
969 set.insert(p1);
970 set.insert(p2); set.insert(p3);
972 assert_eq!(set.len(), 2);
973 }
974
975 #[test]
978 fn store_path_from_str_trait() {
979 use std::str::FromStr;
980 let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
981 let sp: StorePath = StorePath::from_str(path).unwrap();
982 assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
983 }
984
985 #[test]
986 fn store_path_parse_trait_via_str() {
987 let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
988 let sp: StorePath = path.parse().unwrap();
989 assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
990 }
991
992 #[test]
995 fn store_path_error_invalid_format_includes_string() {
996 match StorePath::from_absolute_path("/tmp/foo") {
997 Err(StorePathError::Invalid(s)) => assert_eq!(s, "/tmp/foo"),
998 other => panic!("expected Invalid, got {other:?}"),
999 }
1000 }
1001
1002 #[test]
1003 fn store_path_error_empty_name_variant() {
1004 let hash = nix_base32_encode(&[0u8; 20]);
1013 let basename = format!("{hash}-");
1014 match StorePath::from_basename(&basename) {
1015 Err(StorePathError::Invalid(_)) => {}
1016 other => panic!("expected Invalid, got {other:?}"),
1017 }
1018 }
1019
1020 #[test]
1021 fn store_path_error_invalid_hash_length() {
1022 match nix_base32_decode("abc") {
1023 Err(StorePathError::InvalidHashLength { expected, got }) => {
1024 assert_eq!(expected, 32);
1025 assert_eq!(got, 3);
1026 }
1027 other => panic!("expected InvalidHashLength, got {other:?}"),
1028 }
1029 }
1030
1031 #[test]
1034 fn compress_hash_to_one_byte_xor_all_input() {
1035 let input = vec![0xFF, 0x0F, 0xF0, 0x55, 0xAA];
1037 let out = compress_hash(&input, 1);
1038 let expected = 0xFF ^ 0x0F ^ 0xF0 ^ 0x55 ^ 0xAA;
1039 assert_eq!(out[0], expected);
1040 }
1041
1042 #[test]
1043 fn compress_hash_empty_input() {
1044 let out = compress_hash(&[], 5);
1045 assert_eq!(out, vec![0u8; 5]);
1046 }
1047
1048 #[test]
1049 fn compress_hash_smaller_input_than_output() {
1050 let input = vec![0xAA, 0xBB, 0xCC];
1052 let out = compress_hash(&input, 5);
1053 assert_eq!(out[0], 0xAA);
1054 assert_eq!(out[1], 0xBB);
1055 assert_eq!(out[2], 0xCC);
1056 assert_eq!(out[3], 0);
1057 assert_eq!(out[4], 0);
1058 }
1059
1060 #[test]
1063 fn output_path_lib_format() {
1064 let path = compute_output_path("0123456789abcdef", "lib", "openssl");
1065 assert!(path.starts_with("/nix/store/"));
1066 assert!(path.ends_with("-openssl-lib"));
1067 }
1068
1069 #[test]
1070 fn output_path_default_does_not_have_out_suffix() {
1071 let path = compute_output_path("0123456789abcdef", "out", "hello");
1072 let basename = path.strip_prefix("/nix/store/").unwrap();
1073 assert!(!basename.ends_with("-hello-out"));
1074 assert!(basename.ends_with("-hello"));
1075 }
1076
1077 #[test]
1080 fn fixed_output_recursive_sha256_uses_source_branch() {
1081 let r = compute_fixed_output_hash("sha256", "abc", true, "thing");
1084 let f = compute_fixed_output_hash("sha256", "abc", false, "thing");
1085 assert_ne!(r, f);
1086 }
1087
1088 #[test]
1089 fn fixed_output_recursive_md5_does_not_use_source_branch() {
1090 let r = compute_fixed_output_hash("md5", "abc", true, "thing");
1092 let f = compute_fixed_output_hash("md5", "abc", false, "thing");
1093 assert_ne!(r, f);
1095 }
1096
1097 #[test]
1100 fn compute_drv_path_equals_with_refs_empty_slice() {
1101 let p1 = compute_drv_path(b"content", "name");
1102 let p2 = compute_drv_path_with_refs(b"content", "name", &[]);
1103 assert_eq!(p1, p2);
1104 }
1105
1106 #[test]
1109 fn store_constants() {
1110 assert_eq!(DEFAULT_STORE_DIR, "/nix/store");
1111 assert_eq!(STORE_PATH_HASH_LEN, 32);
1112 }
1113}