use crate::hash;
use sha2::{Digest, Sha256};
use thiserror::Error;
pub const STORE_PATH_HASH_LEN: usize = 32;
pub const DEFAULT_STORE_DIR: &str = "/nix/store";
const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
#[derive(Debug, Error)]
pub enum StorePathError {
#[error("invalid store path: {0}")]
Invalid(String),
#[error("invalid hash length: expected {expected}, got {got}")]
InvalidHashLength { expected: usize, got: usize },
#[error("invalid character in hash: {0}")]
InvalidHashChar(char),
#[error("empty name")]
EmptyName,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StorePath {
pub digest: [u8; 20],
pub name: String,
}
impl StorePath {
pub fn from_absolute_path(path: &str) -> Result<Self, StorePathError> {
let rest = path
.strip_prefix(DEFAULT_STORE_DIR)
.and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| StorePathError::Invalid(path.to_string()))?;
Self::from_basename(rest)
}
pub fn from_basename(basename: &str) -> Result<Self, StorePathError> {
if basename.len() < STORE_PATH_HASH_LEN + 2 {
return Err(StorePathError::Invalid(basename.to_string()));
}
let hash_str = &basename[..STORE_PATH_HASH_LEN];
let sep = basename.as_bytes()[STORE_PATH_HASH_LEN];
let name = &basename[STORE_PATH_HASH_LEN + 1..];
if sep != b'-' {
return Err(StorePathError::Invalid(basename.to_string()));
}
if name.is_empty() {
return Err(StorePathError::EmptyName);
}
let digest = nix_base32_decode(hash_str)?;
Ok(Self {
digest,
name: name.to_string(),
})
}
#[must_use]
pub fn to_absolute_path(&self) -> String {
format!("{}/{}", DEFAULT_STORE_DIR, self.to_basename())
}
#[must_use]
pub fn to_basename(&self) -> String {
format!("{}-{}", nix_base32_encode(&self.digest), self.name)
}
#[must_use]
pub fn hash(&self) -> String {
nix_base32_encode(&self.digest)
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for StorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_absolute_path())
}
}
impl std::str::FromStr for StorePath {
type Err = StorePathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_absolute_path(s)
}
}
#[must_use]
pub fn nix_base32_encode(input: &[u8]) -> String {
let len = (input.len() * 8).div_ceil(5);
let mut out = String::with_capacity(len);
for n in 0..len {
let b = (len - 1 - n) * 5;
let i = b / 8;
let j = b % 8;
let mut c = u16::from(input[i]) >> j;
if i + 1 < input.len() {
c |= u16::from(input[i + 1]) << (8 - j);
}
out.push(NIX_BASE32_CHARS[(c & 0x1f) as usize] as char);
}
out
}
pub fn nix_base32_decode(input: &str) -> Result<[u8; 20], StorePathError> {
let expected_len = 32; if input.len() != expected_len {
return Err(StorePathError::InvalidHashLength {
expected: expected_len,
got: input.len(),
});
}
let mut bytes = [0u8; 20];
let total = input.len();
for (n, c) in input.chars().enumerate() {
let digit = NIX_BASE32_CHARS
.iter()
.position(|&x| x == c as u8)
.ok_or(StorePathError::InvalidHashChar(c))?;
let b = (total - 1 - n) * 5;
let i = b / 8;
let j = b % 8;
bytes[i] |= ((digit as u16) << j) as u8;
if i + 1 < bytes.len() {
bytes[i + 1] |= ((digit as u16) >> (8 - j)) as u8;
}
}
Ok(bytes)
}
#[must_use]
pub fn compress_hash(hash: &[u8], output_len: usize) -> Vec<u8> {
let mut out = vec![0u8; output_len];
for (i, b) in hash.iter().enumerate() {
out[i % output_len] ^= *b;
}
out
}
#[must_use]
pub fn compute_store_path_from_fingerprint(fingerprint: &str, name: &str) -> String {
let hash = Sha256::digest(fingerprint.as_bytes());
let compressed = compress_hash(&hash, 20);
let b32 = nix_base32_encode(&compressed);
format!("{DEFAULT_STORE_DIR}/{b32}-{name}")
}
#[must_use]
pub fn compute_drv_path(drv_content: &[u8], name: &str) -> String {
compute_drv_path_with_refs(drv_content, name, &[])
}
#[must_use]
pub fn compute_drv_path_with_refs(drv_content: &[u8], name: &str, refs: &[String]) -> String {
let inner = Sha256::digest(drv_content);
let inner_hex = hash::hex::encode(&inner);
let drv_name = format!("{name}.drv");
let mut sorted_refs: Vec<&String> = refs.iter().collect();
sorted_refs.sort();
sorted_refs.dedup();
let mut fingerprint = String::from("text:");
for r in sorted_refs {
fingerprint.push_str(r);
fingerprint.push(':');
}
fingerprint.push_str("sha256:");
fingerprint.push_str(&inner_hex);
fingerprint.push(':');
fingerprint.push_str(DEFAULT_STORE_DIR);
fingerprint.push(':');
fingerprint.push_str(&drv_name);
compute_store_path_from_fingerprint(&fingerprint, &drv_name)
}
#[must_use]
pub fn compute_output_path(inner_hash_hex: &str, output_name: &str, name: &str) -> String {
let full_name = if output_name == "out" {
name.to_string()
} else {
format!("{name}-{output_name}")
};
let fingerprint = format!(
"output:{output_name}:sha256:{inner_hash_hex}:{DEFAULT_STORE_DIR}:{full_name}"
);
compute_store_path_from_fingerprint(&fingerprint, &full_name)
}
#[must_use]
pub fn compute_fixed_output_hash(
algo: &str,
hash: &str,
is_recursive: bool,
name: &str,
) -> String {
if is_recursive && algo == "sha256" {
let fingerprint = format!(
"source:sha256:{hash}:{DEFAULT_STORE_DIR}:{name}"
);
return compute_store_path_from_fingerprint(&fingerprint, name);
}
let mode = if is_recursive { "r:" } else { "" };
let inner = format!("fixed:out:{mode}{algo}:{hash}:");
let inner_hash = Sha256::digest(inner.as_bytes());
let inner_hex = hash::hex::encode(&inner_hash);
compute_output_path(&inner_hex, "out", name)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn parse_absolute_path() {
let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
let sp = StorePath::from_absolute_path(path).unwrap();
assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
assert_eq!(sp.to_absolute_path(), path);
}
#[test]
fn roundtrip_base32() {
let input = [0u8; 20];
let encoded = nix_base32_encode(&input);
assert_eq!(encoded.len(), 32);
let decoded = nix_base32_decode(&encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn reject_invalid_path() {
assert!(StorePath::from_absolute_path("/tmp/foo").is_err());
assert!(StorePath::from_absolute_path("/nix/store/short").is_err());
}
#[test]
fn invalid_base32_chars_e_and_u() {
let with_e = "00bgd045z0d4icpbc2yye4gx48ak44la";
assert!(nix_base32_decode(with_e).is_err());
let with_u = "00bgd045z0d4icpbc2yyu4gx48ak44la";
assert!(nix_base32_decode(with_u).is_err());
match nix_base32_decode("e0000000000000000000000000000000") {
Err(StorePathError::InvalidHashChar(c)) => assert_eq!(c, 'e'),
other => panic!("expected InvalidHashChar('e'), got {other:?}"),
}
match nix_base32_decode("u0000000000000000000000000000000") {
Err(StorePathError::InvalidHashChar(c)) => assert_eq!(c, 'u'),
other => panic!("expected InvalidHashChar('u'), got {other:?}"),
}
}
#[test]
fn path_with_minimum_valid_name_length() {
let hash = nix_base32_encode(&[0u8; 20]);
let basename = format!("{hash}-x");
let sp = StorePath::from_basename(&basename).unwrap();
assert_eq!(sp.name, "x");
}
#[test]
fn path_with_special_characters_in_name() {
let hash = nix_base32_encode(&[1u8; 20]);
let basename = format!("{hash}-my-pkg_v1.2.3+git");
let sp = StorePath::from_basename(&basename).unwrap();
assert_eq!(sp.name, "my-pkg_v1.2.3+git");
}
#[test]
fn basename_roundtrip_real_world_examples() {
let examples = [
"00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1",
"3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8",
"sn5lbjwwmkbzj7cx0hfnlwf4sh16cll6-hello-2.12.1",
];
for basename in examples {
let sp = StorePath::from_basename(basename).unwrap();
assert_eq!(sp.to_basename(), basename, "roundtrip failed for {basename}");
}
}
#[test]
fn store_path_display_trait() {
let path_str = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
let sp = StorePath::from_absolute_path(path_str).unwrap();
let displayed = format!("{sp}");
assert_eq!(displayed, path_str);
}
#[test]
fn empty_name_rejected() {
let hash = nix_base32_encode(&[0u8; 20]);
let basename = format!("{hash}-");
assert!(StorePath::from_basename(&basename).is_err());
}
#[test]
fn base32_encode_decode_roundtrip_various() {
let test_cases: [[u8; 20]; 4] = [
[0u8; 20],
[0xff; 20],
[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc,
0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0xde, 0xad, 0xbe, 0xef],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
];
for input in &test_cases {
let encoded = nix_base32_encode(input);
assert_eq!(encoded.len(), 32);
let decoded = nix_base32_decode(&encoded).unwrap();
assert_eq!(&decoded, input);
}
}
#[test]
fn wrong_hash_length_rejected() {
assert!(nix_base32_decode("abc").is_err());
assert!(nix_base32_decode("000000000000000000000000000000000").is_err());
match nix_base32_decode("abc") {
Err(StorePathError::InvalidHashLength { expected: 32, got: 3 }) => {}
other => panic!("expected InvalidHashLength, got {other:?}"),
}
}
#[test]
fn compress_hash_zero_input_zero_output() {
let zeros = [0u8; 32];
let out = compress_hash(&zeros, 20);
assert_eq!(out, vec![0u8; 20]);
}
#[test]
fn compress_hash_xor_fold_layout() {
let mut input = [0u8; 32];
input[0] = 0xAA;
input[20] = 0x55;
let out = compress_hash(&input, 20);
assert_eq!(out[0], 0xFF);
for i in 12..20 {
assert_eq!(out[i], 0);
}
}
#[test]
fn compress_hash_identity_when_lengths_match() {
let input: [u8; 20] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];
let out = compress_hash(&input, 20);
assert_eq!(out, input.to_vec());
}
#[test]
fn compress_hash_output_length_respected() {
let input = [0xFFu8; 64];
for &target_len in &[1usize, 5, 10, 20, 32] {
let out = compress_hash(&input, target_len);
assert_eq!(out.len(), target_len);
}
}
#[test]
fn fingerprint_path_format() {
let path = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:hello.drv", "hello.drv");
assert!(path.starts_with("/nix/store/"));
let basename = path.strip_prefix("/nix/store/").unwrap();
assert_eq!(basename.len(), STORE_PATH_HASH_LEN + 1 + "hello.drv".len());
assert!(basename.ends_with("-hello.drv"));
let hash = &basename[..STORE_PATH_HASH_LEN];
for c in hash.chars() {
assert!(NIX_BASE32_CHARS.contains(&(c as u8)), "invalid char: {c}");
}
}
#[test]
fn fingerprint_path_deterministic() {
let p1 = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:foo", "foo");
let p2 = compute_store_path_from_fingerprint("text:sha256:abc:/nix/store:foo", "foo");
assert_eq!(p1, p2);
}
#[test]
fn fingerprint_path_changes_with_input() {
let p1 = compute_store_path_from_fingerprint("a", "x");
let p2 = compute_store_path_from_fingerprint("b", "x");
assert_ne!(p1, p2);
}
#[test]
fn drv_path_format() {
let path = compute_drv_path(b"some-aterm-content", "hello");
assert!(path.starts_with("/nix/store/"));
assert!(path.ends_with("-hello.drv"));
let basename = path.strip_prefix("/nix/store/").unwrap();
assert_eq!(basename.len(), STORE_PATH_HASH_LEN + 1 + "hello.drv".len());
}
#[test]
fn drv_path_deterministic() {
let p1 = compute_drv_path(b"content", "name");
let p2 = compute_drv_path(b"content", "name");
assert_eq!(p1, p2);
}
#[test]
fn drv_path_changes_with_content() {
let p1 = compute_drv_path(b"a", "name");
let p2 = compute_drv_path(b"b", "name");
assert_ne!(p1, p2);
}
#[test]
fn drv_path_changes_with_name() {
let p1 = compute_drv_path(b"content", "foo");
let p2 = compute_drv_path(b"content", "bar");
assert_ne!(p1, p2);
}
#[test]
fn output_path_out_uses_bare_name() {
let path = compute_output_path("0123456789abcdef", "out", "hello");
assert!(path.ends_with("-hello"));
assert!(!path.ends_with("-hello-out"));
}
#[test]
fn output_path_named_output_uses_suffix() {
let path = compute_output_path("0123456789abcdef", "dev", "hello");
assert!(path.ends_with("-hello-dev"));
}
#[test]
fn output_path_deterministic() {
let p1 = compute_output_path("0123456789abcdef", "out", "hello");
let p2 = compute_output_path("0123456789abcdef", "out", "hello");
assert_eq!(p1, p2);
}
#[test]
fn output_path_changes_with_inner_hash() {
let p1 = compute_output_path("0000000000000000", "out", "hello");
let p2 = compute_output_path("ffffffffffffffff", "out", "hello");
assert_ne!(p1, p2);
}
#[test]
fn multiple_outputs_produce_distinct_paths() {
let inner = "deadbeef";
let p_out = compute_output_path(inner, "out", "lib");
let p_dev = compute_output_path(inner, "dev", "lib");
let p_man = compute_output_path(inner, "man", "lib");
assert_ne!(p_out, p_dev);
assert_ne!(p_out, p_man);
assert_ne!(p_dev, p_man);
}
#[test]
fn fixed_output_flat_path_format() {
let path = compute_fixed_output_hash(
"sha256",
"1b0ri5lsf45dknj8bfxi1syz35kmab77apxxg1yrf33la1qm3kc7",
false,
"src.tar.gz",
);
assert!(path.starts_with("/nix/store/"));
assert!(path.ends_with("-src.tar.gz"));
}
#[test]
fn fixed_output_recursive_differs_from_flat() {
let flat = compute_fixed_output_hash("sha256", "abc", false, "thing");
let rec = compute_fixed_output_hash("sha256", "abc", true, "thing");
assert_ne!(flat, rec);
}
#[test]
fn fixed_output_deterministic() {
let p1 = compute_fixed_output_hash("sha256", "deadbeef", false, "thing");
let p2 = compute_fixed_output_hash("sha256", "deadbeef", false, "thing");
assert_eq!(p1, p2);
}
#[test]
fn fixed_output_changes_with_hash_value() {
let p1 = compute_fixed_output_hash("sha256", "aaa", false, "thing");
let p2 = compute_fixed_output_hash("sha256", "bbb", false, "thing");
assert_ne!(p1, p2);
}
#[test]
fn fixed_output_changes_with_algo() {
let p1 = compute_fixed_output_hash("sha256", "abc", false, "thing");
let p2 = compute_fixed_output_hash("sha1", "abc", false, "thing");
assert_ne!(p1, p2);
}
#[test]
fn hex_encode_basic() {
assert_eq!(crate::hash::hex::encode(&[0x00, 0xff, 0xab]), "00ffab");
assert_eq!(crate::hash::hex::encode(&[]), "");
assert_eq!(crate::hash::hex::encode(&[0x12, 0x34, 0x56, 0x78]), "12345678");
}
#[test]
fn base32_encode_output_length_formula() {
for input_len in [0, 1, 5, 10, 16, 20, 32, 64] {
let input = vec![0xAB_u8; input_len];
let encoded = nix_base32_encode(&input);
let expected_len = (input_len * 8 + 4) / 5;
assert_eq!(
encoded.len(),
expected_len,
"wrong encode length for {input_len}-byte input"
);
}
}
#[test]
fn base32_encode_alphabet_only() {
for input_len in [5, 10, 20, 32, 64] {
let input = vec![0xFF_u8; input_len];
let encoded = nix_base32_encode(&input);
for c in encoded.chars() {
assert!(
NIX_BASE32_CHARS.contains(&(c as u8)),
"char '{c}' not in nix base32 alphabet (input_len={input_len})"
);
}
}
}
#[test]
fn base32_encode_all_zero_bytes() {
for input_len in [5, 10, 20, 32, 64] {
let input = vec![0x00_u8; input_len];
let encoded = nix_base32_encode(&input);
assert!(
encoded.chars().all(|c| c == '0'),
"all-zero {input_len}-byte input should encode to all '0's, got: {encoded}"
);
}
}
#[test]
fn base32_encode_all_ff_bytes() {
for input_len in [5, 10, 20, 32, 64] {
let input = vec![0xFF_u8; input_len];
let encoded = nix_base32_encode(&input);
assert!(
!encoded.is_empty(),
"encoding of all-0xFF input should be non-empty"
);
assert!(
encoded.chars().all(|c| NIX_BASE32_CHARS.contains(&(c as u8))),
"all chars must be in alphabet"
);
}
}
#[test]
fn base32_encode_alternating_bytes() {
let input: Vec<u8> = (0..32).map(|i| if i % 2 == 0 { 0xAA } else { 0x55 }).collect();
let encoded = nix_base32_encode(&input);
let expected_len = (32 * 8 + 4) / 5;
assert_eq!(encoded.len(), expected_len);
for c in encoded.chars() {
assert!(NIX_BASE32_CHARS.contains(&(c as u8)));
}
}
#[test]
fn base32_encode_empty_input() {
let encoded = nix_base32_encode(&[]);
assert_eq!(encoded, "");
}
#[test]
fn base32_encode_single_byte() {
let encoded = nix_base32_encode(&[0x42]);
assert_eq!(encoded.len(), 2);
let decoded_manual = nix_base32_encode(&[0x42]);
assert_eq!(encoded, decoded_manual);
}
#[test]
fn base32_roundtrip_20_byte_boundary_cases() {
let cases: Vec<[u8; 20]> = vec![
[0x00; 20],
[0xFF; 20],
[0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55,
0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
{
let mut a = [0u8; 20];
a[0] = 0x01;
a
},
{
let mut a = [0u8; 20];
a[19] = 0x01;
a
},
{
let mut a = [0u8; 20];
for (i, v) in a.iter_mut().enumerate() {
*v = i as u8;
}
a
},
];
for input in &cases {
let encoded = nix_base32_encode(input);
assert_eq!(encoded.len(), 32);
let decoded = nix_base32_decode(&encoded).unwrap();
assert_eq!(&decoded, input, "roundtrip failed for {input:?}");
}
}
#[test]
fn drv_path_with_refs_includes_refs_in_fingerprint() {
let content = b"Derive(...)";
let no_refs = compute_drv_path_with_refs(content, "hello", &[]);
let with_refs = compute_drv_path_with_refs(
content,
"hello",
&["/nix/store/abc-dep".to_string()],
);
assert_ne!(no_refs, with_refs);
}
#[test]
fn drv_path_with_refs_order_independent() {
let content = b"Derive(...)";
let refs_a = vec![
"/nix/store/bbb-b".to_string(),
"/nix/store/aaa-a".to_string(),
];
let refs_b = vec![
"/nix/store/aaa-a".to_string(),
"/nix/store/bbb-b".to_string(),
];
let p1 = compute_drv_path_with_refs(content, "test", &refs_a);
let p2 = compute_drv_path_with_refs(content, "test", &refs_b);
assert_eq!(p1, p2, "ref order should not affect output");
}
#[test]
fn drv_path_with_refs_deduplicates() {
let content = b"Derive(...)";
let with_dups = vec![
"/nix/store/aaa-a".to_string(),
"/nix/store/aaa-a".to_string(),
];
let without_dups = vec!["/nix/store/aaa-a".to_string()];
let p1 = compute_drv_path_with_refs(content, "test", &with_dups);
let p2 = compute_drv_path_with_refs(content, "test", &without_dups);
assert_eq!(p1, p2, "duplicate refs should be deduplicated");
}
proptest! {
#[test]
fn prop_base32_roundtrip_20_bytes(bytes in proptest::collection::vec(any::<u8>(), 20)) {
let arr: [u8; 20] = bytes.try_into().unwrap();
let encoded = nix_base32_encode(&arr);
prop_assert_eq!(encoded.len(), 32);
let decoded = nix_base32_decode(&encoded).unwrap();
prop_assert_eq!(decoded, arr);
}
#[test]
fn prop_base32_encode_uses_only_nix_alphabet(bytes in proptest::collection::vec(any::<u8>(), 1..=64)) {
let encoded = nix_base32_encode(&bytes);
for c in encoded.chars() {
prop_assert!(NIX_BASE32_CHARS.contains(&(c as u8)), "invalid char: {}", c);
}
}
#[test]
fn prop_compress_hash_output_length(
bytes in proptest::collection::vec(any::<u8>(), 1..=64),
target_len in 1_usize..=32
) {
let out = compress_hash(&bytes, target_len);
prop_assert_eq!(out.len(), target_len);
}
#[test]
fn prop_store_path_roundtrip(digest in proptest::collection::vec(any::<u8>(), 20)) {
let arr: [u8; 20] = digest.try_into().unwrap();
let sp = StorePath { digest: arr, name: "test-pkg".to_string() };
let abs = sp.to_absolute_path();
let reparsed = StorePath::from_absolute_path(&abs).unwrap();
prop_assert_eq!(reparsed.digest, sp.digest);
prop_assert_eq!(reparsed.name, sp.name);
}
#[test]
fn prop_base32_encode_length_5(bytes in proptest::collection::vec(any::<u8>(), 5)) {
let encoded = nix_base32_encode(&bytes);
prop_assert_eq!(encoded.len(), 8);
}
#[test]
fn prop_base32_encode_length_10(bytes in proptest::collection::vec(any::<u8>(), 10)) {
let encoded = nix_base32_encode(&bytes);
prop_assert_eq!(encoded.len(), 16);
}
#[test]
fn prop_base32_encode_length_32(bytes in proptest::collection::vec(any::<u8>(), 32)) {
let encoded = nix_base32_encode(&bytes);
prop_assert_eq!(encoded.len(), 52);
}
#[test]
fn prop_base32_encode_length_64(bytes in proptest::collection::vec(any::<u8>(), 64)) {
let encoded = nix_base32_encode(&bytes);
prop_assert_eq!(encoded.len(), 103);
}
#[test]
fn prop_base32_encode_uses_alphabet_only(bytes in proptest::collection::vec(any::<u8>(), 1..=128)) {
let encoded = nix_base32_encode(&bytes);
for c in encoded.chars() {
prop_assert!(NIX_BASE32_CHARS.contains(&(c as u8)));
}
}
#[test]
fn prop_drv_path_deterministic(
content in proptest::collection::vec(any::<u8>(), 0..200),
name in "[a-z][a-z0-9-]{0,30}",
) {
let p1 = compute_drv_path(&content, &name);
let p2 = compute_drv_path(&content, &name);
prop_assert_eq!(p1, p2);
}
#[test]
fn prop_drv_path_with_refs_permutation_invariant(
content in proptest::collection::vec(any::<u8>(), 0..50),
name in "[a-z]{1,10}",
n_refs in 0_usize..=8,
) {
let refs: Vec<String> = (0..n_refs).map(|i| format!("/nix/store/r{i}-x")).collect();
let mut shuffled = refs.clone();
shuffled.reverse();
let p1 = compute_drv_path_with_refs(&content, &name, &refs);
let p2 = compute_drv_path_with_refs(&content, &name, &shuffled);
prop_assert_eq!(p1, p2);
}
}
#[test]
fn from_basename_unicode_in_name_rejected_or_accepted() {
let hash = nix_base32_encode(&[0u8; 20]);
let basename = format!("{hash}-héllo");
let sp = StorePath::from_basename(&basename).unwrap();
assert_eq!(sp.name, "héllo");
}
#[test]
fn from_absolute_path_without_leading_slash_rejected() {
assert!(StorePath::from_absolute_path("nix/store/abc").is_err());
}
#[test]
fn from_absolute_path_with_extra_path_segments_rejected() {
let hash = nix_base32_encode(&[0u8; 20]);
let path = format!("/nix/store/{hash}-name/extra");
let sp = StorePath::from_absolute_path(&path).unwrap();
assert_eq!(sp.name, "name/extra");
}
#[test]
fn store_path_hash_trait_works_in_hashset() {
use std::collections::HashSet;
let p1 = StorePath {
digest: [1; 20],
name: "x".to_string(),
};
let p2 = p1.clone();
let p3 = StorePath {
digest: [2; 20],
name: "x".to_string(),
};
let mut set = HashSet::new();
set.insert(p1);
set.insert(p2); set.insert(p3);
assert_eq!(set.len(), 2);
}
#[test]
fn store_path_from_str_trait() {
use std::str::FromStr;
let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
let sp: StorePath = StorePath::from_str(path).unwrap();
assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
}
#[test]
fn store_path_parse_trait_via_str() {
let path = "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-hierarchical-0.1.0.1";
let sp: StorePath = path.parse().unwrap();
assert_eq!(sp.name, "net-hierarchical-0.1.0.1");
}
#[test]
fn store_path_error_invalid_format_includes_string() {
match StorePath::from_absolute_path("/tmp/foo") {
Err(StorePathError::Invalid(s)) => assert_eq!(s, "/tmp/foo"),
other => panic!("expected Invalid, got {other:?}"),
}
}
#[test]
fn store_path_error_empty_name_variant() {
let hash = nix_base32_encode(&[0u8; 20]);
let basename = format!("{hash}-");
match StorePath::from_basename(&basename) {
Err(StorePathError::Invalid(_)) => {}
other => panic!("expected Invalid, got {other:?}"),
}
}
#[test]
fn store_path_error_invalid_hash_length() {
match nix_base32_decode("abc") {
Err(StorePathError::InvalidHashLength { expected, got }) => {
assert_eq!(expected, 32);
assert_eq!(got, 3);
}
other => panic!("expected InvalidHashLength, got {other:?}"),
}
}
#[test]
fn compress_hash_to_one_byte_xor_all_input() {
let input = vec![0xFF, 0x0F, 0xF0, 0x55, 0xAA];
let out = compress_hash(&input, 1);
let expected = 0xFF ^ 0x0F ^ 0xF0 ^ 0x55 ^ 0xAA;
assert_eq!(out[0], expected);
}
#[test]
fn compress_hash_empty_input() {
let out = compress_hash(&[], 5);
assert_eq!(out, vec![0u8; 5]);
}
#[test]
fn compress_hash_smaller_input_than_output() {
let input = vec![0xAA, 0xBB, 0xCC];
let out = compress_hash(&input, 5);
assert_eq!(out[0], 0xAA);
assert_eq!(out[1], 0xBB);
assert_eq!(out[2], 0xCC);
assert_eq!(out[3], 0);
assert_eq!(out[4], 0);
}
#[test]
fn output_path_lib_format() {
let path = compute_output_path("0123456789abcdef", "lib", "openssl");
assert!(path.starts_with("/nix/store/"));
assert!(path.ends_with("-openssl-lib"));
}
#[test]
fn output_path_default_does_not_have_out_suffix() {
let path = compute_output_path("0123456789abcdef", "out", "hello");
let basename = path.strip_prefix("/nix/store/").unwrap();
assert!(!basename.ends_with("-hello-out"));
assert!(basename.ends_with("-hello"));
}
#[test]
fn fixed_output_recursive_sha256_uses_source_branch() {
let r = compute_fixed_output_hash("sha256", "abc", true, "thing");
let f = compute_fixed_output_hash("sha256", "abc", false, "thing");
assert_ne!(r, f);
}
#[test]
fn fixed_output_recursive_md5_does_not_use_source_branch() {
let r = compute_fixed_output_hash("md5", "abc", true, "thing");
let f = compute_fixed_output_hash("md5", "abc", false, "thing");
assert_ne!(r, f);
}
#[test]
fn compute_drv_path_equals_with_refs_empty_slice() {
let p1 = compute_drv_path(b"content", "name");
let p2 = compute_drv_path_with_refs(b"content", "name", &[]);
assert_eq!(p1, p2);
}
#[test]
fn store_constants() {
assert_eq!(DEFAULT_STORE_DIR, "/nix/store");
assert_eq!(STORE_PATH_HASH_LEN, 32);
}
}