Skip to main content

uor_addr/canonical/
hex.rs

1//! Lowercase-hex byte-emit — `no_std`, `no_alloc`.
2//!
3//! The κ-label's 64-byte suffix is the lowercase-hex serialization of
4//! the σ-projection's 32-byte SHA-256 digest. This module's
5//! [`encode_lower_into`] is the canonical emit-path; callers reach it
6//! through [`crate::label::AddressLabel`] in normal use, but it is
7//! `pub` so any realization wiring κ-derivation against a different
8//! hash axis (per ARCHITECTURE.md "Alternate hash axes") reuses the
9//! same lowercase-hex discipline.
10//!
11//! # Output discipline
12//!
13//! Each input byte produces exactly two ASCII characters from
14//! `0123456789abcdef`. Output length is always `2 × input.len()`.
15//! ASCII case is canonical: the κ-label IRI's wire-format pins
16//! lowercase hex (`sha256:7a38…`, not `sha256:7A38…`).
17
18/// Output-buffer-too-small error. Surfaced to callers when `out` cannot
19/// hold `2 × input.len()` bytes.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct HexOutputOverflow;
22
23/// Emit `input` as lowercase ASCII hex into `out`. Returns the number
24/// of bytes written — always `2 × input.len()` on success.
25///
26/// # Errors
27///
28/// - [`HexOutputOverflow`] — `out.len() < 2 * input.len()`.
29pub fn encode_lower_into(input: &[u8], out: &mut [u8]) -> Result<usize, HexOutputOverflow> {
30    let need = input.len().checked_mul(2).ok_or(HexOutputOverflow)?;
31    if out.len() < need {
32        return Err(HexOutputOverflow);
33    }
34    const HEX: &[u8; 16] = b"0123456789abcdef";
35    for (i, &byte) in input.iter().enumerate() {
36        out[i * 2] = HEX[(byte >> 4) as usize];
37        out[i * 2 + 1] = HEX[(byte & 0x0f) as usize];
38    }
39    Ok(need)
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn empty_input_emits_nothing() {
48        let mut out = [0u8; 4];
49        assert_eq!(encode_lower_into(&[], &mut out).unwrap(), 0);
50    }
51
52    #[test]
53    fn each_byte_yields_two_lowercase_hex_chars() {
54        let mut out = [0u8; 4];
55        let n = encode_lower_into(&[0x00, 0xff], &mut out).unwrap();
56        assert_eq!(n, 4);
57        assert_eq!(&out[..n], b"00ff");
58    }
59
60    #[test]
61    fn sha256_zero_digest_round_trips_published_fixture() {
62        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
63        let digest: [u8; 32] = [
64            0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
65            0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
66            0x78, 0x52, 0xb8, 0x55,
67        ];
68        let mut out = [0u8; 64];
69        let n = encode_lower_into(&digest, &mut out).unwrap();
70        assert_eq!(n, 64);
71        assert_eq!(
72            &out[..n],
73            b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
74        );
75    }
76
77    #[test]
78    fn rejects_undersized_output_buffer() {
79        let mut out = [0u8; 1];
80        assert_eq!(
81            encode_lower_into(&[0xab, 0xcd], &mut out),
82            Err(HexOutputOverflow)
83        );
84    }
85
86    #[test]
87    fn rejects_zero_length_output_for_nonempty_input() {
88        let mut out = [0u8; 0];
89        assert_eq!(encode_lower_into(&[0xff], &mut out), Err(HexOutputOverflow));
90    }
91}