1use serde::{Deserialize, Serialize};
29use tatara_lisp::DeriveTataraDomain;
30
31use crate::SpecError;
32
33#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
37#[tatara(keyword = "defhash-algorithm")]
38pub struct HashAlgorithm {
39 pub name: String,
40 #[serde(rename = "bitLength")]
41 pub bit_length: u32,
42 pub weakness: HashWeakness,
43 #[serde(rename = "nixPrefix")]
44 pub nix_prefix: String,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
50pub enum HashWeakness {
51 Strong,
53 Deprecated,
56 Broken,
59}
60
61#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
65#[tatara(keyword = "defhash-encoding")]
66pub struct HashEncoding {
67 pub name: String,
68 pub alphabet: String,
71 #[serde(default, rename = "preferredByNixFor")]
73 pub preferred_by_nix_for: Vec<NixHashContext>,
74}
75
76#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum NixHashContext {
79 NarHash,
81 StorePathHash,
83 NarSignature,
85 FodOutputHash,
87 FlakeInputNarHash,
89 FlakeInputSri,
91}
92
93fn from_base16(s: &str) -> Result<Vec<u8>, SpecError> {
97 if s.len() % 2 != 0 {
98 return Err(SpecError::Interp {
99 phase: "hash-decode".into(),
100 message: format!("base16 string `{s}` has odd length {}", s.len()),
101 });
102 }
103 let mut out = Vec::with_capacity(s.len() / 2);
104 let chars: Vec<char> = s.chars().collect();
105 for chunk in chars.chunks(2) {
106 let pair: String = chunk.iter().collect();
107 let byte = u8::from_str_radix(&pair, 16).map_err(|e| SpecError::Interp {
108 phase: "hash-decode".into(),
109 message: format!("invalid hex byte `{pair}`: {e}"),
110 })?;
111 out.push(byte);
112 }
113 Ok(out)
114}
115
116fn to_base16(bytes: &[u8]) -> String {
117 let mut s = String::with_capacity(bytes.len() * 2);
118 for b in bytes {
119 s.push_str(&format!("{b:02x}"));
120 }
121 s
122}
123
124fn from_nix_base32(s: &str) -> Result<Vec<u8>, SpecError> {
129 const ALPHABET: &[u8] = b"0123456789abcdfghijklmnpqrsvwxyz";
130 let mut idx_table = [0xff_u8; 256];
131 for (i, b) in ALPHABET.iter().enumerate() {
132 idx_table[*b as usize] = i as u8;
133 }
134 let n = (s.len() * 5) / 8;
139 let mut bytes = vec![0u8; n];
140 let n_chars = s.len();
143 for (n_offset, c) in s.chars().enumerate() {
144 let digit = idx_table[c as usize] as u16;
145 if (digit as u8) == 0xff {
146 return Err(SpecError::Interp {
147 phase: "hash-decode".into(),
148 message: format!("char `{c}` not in nix-base32 alphabet"),
149 });
150 }
151 let encoder_n = n_chars - 1 - n_offset;
152 let b = encoder_n * 5;
153 let i = b / 8;
154 let j = b % 8;
155 if i < n {
156 bytes[i] |= digit.wrapping_shl(j as u32) as u8 & 0xff;
157 }
158 if j + 5 > 8 && i + 1 < n {
159 bytes[i + 1] |= digit.wrapping_shr((8 - j) as u32) as u8;
160 }
161 }
162 Ok(bytes)
163}
164
165fn to_nix_base32(bytes: &[u8]) -> String {
166 const ALPHABET: &[u8] = b"0123456789abcdfghijklmnpqrsvwxyz";
167 let n_chars = (bytes.len() * 8).div_ceil(5);
168 let mut out = String::with_capacity(n_chars);
169 for n in (0..n_chars).rev() {
170 let b = n * 5;
171 let i = b / 8;
172 let j = b % 8;
173 let c = (bytes[i] as u16).wrapping_shr(j as u32)
174 | if i + 1 < bytes.len() {
175 (bytes[i + 1] as u16).wrapping_shl((8 - j) as u32)
176 } else { 0 };
177 out.push(ALPHABET[(c & 0x1f) as usize] as char);
178 }
179 out
180}
181
182fn from_base64(s: &str) -> Result<Vec<u8>, SpecError> {
183 use base64::Engine as _;
184 let s = s.trim_end_matches('=');
185 base64::engine::general_purpose::STANDARD_NO_PAD
186 .decode(s.as_bytes())
187 .map_err(|e| SpecError::Interp {
188 phase: "hash-decode".into(),
189 message: format!("invalid base64 `{s}`: {e}"),
190 })
191}
192
193fn to_base64(bytes: &[u8]) -> String {
194 use base64::Engine as _;
195 base64::engine::general_purpose::STANDARD.encode(bytes)
196}
197
198pub fn decode_hash(input: &str) -> Result<(String, Vec<u8>), SpecError> {
213 if let Some(rest) = input.find(':') {
214 let algo = &input[..rest];
216 let payload = &input[rest + 1..];
217 if payload.chars().all(|c| c.is_ascii_hexdigit())
220 && payload.len() % 2 == 0
221 {
222 return Ok((algo.into(), from_base16(payload)?));
223 }
224 return Ok((algo.into(), from_nix_base32(payload)?));
226 }
227 if let Some(dash) = input.find('-') {
228 let algo = &input[..dash];
230 let payload = &input[dash + 1..];
231 return Ok((algo.into(), from_base64(payload)?));
232 }
233 Ok((String::new(), from_base16(input)?))
235}
236
237pub fn encode_hash(
246 algorithm: &str,
247 encoding: &str,
248 bytes: &[u8],
249) -> Result<String, SpecError> {
250 match encoding {
251 "base16" => Ok(to_base16(bytes)),
252 "nix-base32" => Ok(format!("{algorithm}:{}", to_nix_base32(bytes))),
253 "base64" => Ok(format!("{algorithm}:{}", to_base64(bytes))),
254 "sri" => Ok(format!("{algorithm}-{}", to_base64(bytes))),
255 _ => Err(SpecError::Interp {
256 phase: "hash-encode".into(),
257 message: format!("unknown encoding `{encoding}`"),
258 }),
259 }
260}
261
262pub fn apply_conversion(
269 from_encoding: &str,
270 to_encoding: &str,
271 input: &str,
272) -> Result<String, SpecError> {
273 let _ = from_encoding;
277 let (algo, bytes) = decode_hash(input)?;
278 encode_hash(&algo, to_encoding, &bytes)
279}
280
281pub const CANONICAL_HASH_LISP: &str = include_str!("../specs/hash.lisp");
284
285pub fn load_canonical_algorithms() -> Result<Vec<HashAlgorithm>, SpecError> {
291 crate::loader::load_all::<HashAlgorithm>(CANONICAL_HASH_LISP)
292}
293
294pub fn load_canonical_encodings() -> Result<Vec<HashEncoding>, SpecError> {
300 crate::loader::load_all::<HashEncoding>(CANONICAL_HASH_LISP)
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use std::collections::HashSet;
307
308 #[test]
309 fn canonical_algorithms_parse() {
310 let algos = load_canonical_algorithms().unwrap();
311 assert!(!algos.is_empty());
312 let names: HashSet<&str> = algos.iter().map(|a| a.name.as_str()).collect();
313 for required in ["sha1", "sha256", "sha512", "md5", "blake3"] {
314 assert!(
315 names.contains(required),
316 "canonical hash algos missing `{required}`",
317 );
318 }
319 }
320
321 #[test]
322 fn sha256_is_strong() {
323 let algos = load_canonical_algorithms().unwrap();
324 let sha256 = algos.iter().find(|a| a.name == "sha256").unwrap();
325 assert_eq!(sha256.weakness, HashWeakness::Strong);
326 assert_eq!(sha256.bit_length, 256);
327 }
328
329 #[test]
330 fn md5_is_broken() {
331 let algos = load_canonical_algorithms().unwrap();
332 let md5 = algos.iter().find(|a| a.name == "md5").unwrap();
333 assert_eq!(md5.weakness, HashWeakness::Broken);
334 }
335
336 #[test]
337 fn canonical_encodings_parse() {
338 let encs = load_canonical_encodings().unwrap();
339 let names: HashSet<&str> = encs.iter().map(|e| e.name.as_str()).collect();
340 for required in ["base16", "nix-base32", "base64", "sri"] {
341 assert!(
342 names.contains(required),
343 "canonical encodings missing `{required}`",
344 );
345 }
346 }
347
348 #[test]
349 fn nix_base32_is_preferred_for_storepath() {
350 let encs = load_canonical_encodings().unwrap();
351 let b32 = encs.iter().find(|e| e.name == "nix-base32").unwrap();
352 assert!(
353 b32.preferred_by_nix_for.contains(&NixHashContext::StorePathHash),
354 "nix-base32 must be the canonical store-path encoding",
355 );
356 }
357
358 #[test]
359 fn sri_is_preferred_for_flake_input() {
360 let encs = load_canonical_encodings().unwrap();
361 let sri = encs.iter().find(|e| e.name == "sri").unwrap();
362 assert!(
363 sri.preferred_by_nix_for.contains(&NixHashContext::FlakeInputSri),
364 "sri must be the canonical flake-input hash format",
365 );
366 }
367
368 #[test]
371 fn base16_roundtrip() {
372 let bytes = b"hello world";
373 let hex = to_base16(bytes);
374 let back = from_base16(&hex).unwrap();
375 assert_eq!(back, bytes);
376 }
377
378 #[test]
379 fn base16_rejects_odd_length() {
380 let err = from_base16("abc").unwrap_err();
381 match err {
382 SpecError::Interp { phase, .. } => assert_eq!(phase, "hash-decode"),
383 _ => panic!("expected hash-decode"),
384 }
385 }
386
387 #[test]
388 fn base16_rejects_invalid_chars() {
389 let err = from_base16("xyzw").unwrap_err();
390 match err {
391 SpecError::Interp { phase, .. } => assert_eq!(phase, "hash-decode"),
392 _ => panic!("expected hash-decode"),
393 }
394 }
395
396 #[test]
397 fn base64_roundtrip() {
398 let bytes = b"some bytes here";
399 let b64 = to_base64(bytes);
400 let back = from_base64(&b64).unwrap();
401 assert_eq!(back, bytes);
402 }
403
404 #[test]
405 fn nix_base32_uses_alphabet_only() {
406 const ALPHABET: &[u8] = b"0123456789abcdfghijklmnpqrsvwxyz";
410 let bytes = b"some test bytes";
411 let b32 = to_nix_base32(bytes);
412 for c in b32.bytes() {
413 assert!(
414 ALPHABET.contains(&c),
415 "char `{}` not in nix-base32 alphabet",
416 c as char,
417 );
418 }
419 let expected_len = (bytes.len() * 8).div_ceil(5);
421 assert_eq!(b32.len(), expected_len);
422 }
423
424 #[test]
425 fn decode_hash_handles_colon_hex() {
426 let (algo, bytes) = decode_hash("sha256:deadbeef").unwrap();
427 assert_eq!(algo, "sha256");
428 assert_eq!(bytes, [0xde, 0xad, 0xbe, 0xef]);
429 }
430
431 #[test]
432 fn decode_hash_handles_sri() {
433 let bytes_in = b"some bytes";
434 let b64 = to_base64(bytes_in);
435 let sri = format!("sha256-{b64}");
436 let (algo, bytes) = decode_hash(&sri).unwrap();
437 assert_eq!(algo, "sha256");
438 assert_eq!(bytes, bytes_in);
439 }
440
441 #[test]
442 fn encode_hash_emits_sri_with_dash() {
443 let bytes = b"x";
444 let out = encode_hash("sha256", "sri", bytes).unwrap();
445 assert!(out.starts_with("sha256-"));
446 }
447
448 #[test]
449 fn encode_hash_emits_nix_base32_with_colon() {
450 let bytes = b"x";
451 let out = encode_hash("sha256", "nix-base32", bytes).unwrap();
452 assert!(out.starts_with("sha256:"));
453 }
454
455 #[test]
456 fn encode_unknown_encoding_errors() {
457 let err = encode_hash("sha256", "rot13", b"x").unwrap_err();
458 match err {
459 SpecError::Interp { phase, .. } => assert_eq!(phase, "hash-encode"),
460 _ => panic!("expected hash-encode"),
461 }
462 }
463
464 #[test]
465 fn convert_hex_to_sri() {
466 let out = apply_conversion("base16", "sri", "sha256:deadbeef").unwrap();
467 assert!(out.starts_with("sha256-"));
468 assert!(out.len() > 10);
471 }
472}