Skip to main content

rns_ctl/
encode.rs

1/// Encode bytes as lowercase hex string.
2pub fn to_hex(bytes: &[u8]) -> String {
3    let mut s = String::with_capacity(bytes.len() * 2);
4    for &b in bytes {
5        s.push(HEX_CHARS[(b >> 4) as usize]);
6        s.push(HEX_CHARS[(b & 0x0f) as usize]);
7    }
8    s
9}
10
11const HEX_CHARS: [char; 16] = [
12    '0', '1', '2', '3', '4', '5', '6', '7',
13    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
14];
15
16/// Decode a hex string to bytes. Returns None on invalid input.
17pub fn from_hex(s: &str) -> Option<Vec<u8>> {
18    if s.len() % 2 != 0 {
19        return None;
20    }
21    let mut out = Vec::with_capacity(s.len() / 2);
22    let bytes = s.as_bytes();
23    let mut i = 0;
24    while i < bytes.len() {
25        let hi = hex_val(bytes[i])?;
26        let lo = hex_val(bytes[i + 1])?;
27        out.push((hi << 4) | lo);
28        i += 2;
29    }
30    Some(out)
31}
32
33fn hex_val(b: u8) -> Option<u8> {
34    match b {
35        b'0'..=b'9' => Some(b - b'0'),
36        b'a'..=b'f' => Some(b - b'a' + 10),
37        b'A'..=b'F' => Some(b - b'A' + 10),
38        _ => None,
39    }
40}
41
42/// Decode a hex string to a fixed-size array. Returns None on invalid input or wrong length.
43pub fn hex_to_array<const N: usize>(s: &str) -> Option<[u8; N]> {
44    let v = from_hex(s)?;
45    if v.len() != N {
46        return None;
47    }
48    let mut arr = [0u8; N];
49    arr.copy_from_slice(&v);
50    Some(arr)
51}
52
53const B64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
54
55/// Encode bytes as standard base64 with padding.
56pub fn to_base64(data: &[u8]) -> String {
57    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
58    let chunks = data.chunks(3);
59    for chunk in chunks {
60        let b0 = chunk[0] as u32;
61        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
62        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
63        let triple = (b0 << 16) | (b1 << 8) | b2;
64
65        out.push(B64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
66        out.push(B64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
67        if chunk.len() > 1 {
68            out.push(B64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
69        } else {
70            out.push('=');
71        }
72        if chunk.len() > 2 {
73            out.push(B64_CHARS[(triple & 0x3F) as usize] as char);
74        } else {
75            out.push('=');
76        }
77    }
78    out
79}
80
81/// Decode standard base64 (with or without padding) to bytes. Returns None on invalid input.
82pub fn from_base64(s: &str) -> Option<Vec<u8>> {
83    let s = s.trim_end_matches('=');
84    let mut out = Vec::with_capacity(s.len() * 3 / 4);
85    let bytes = s.as_bytes();
86    let chunks = bytes.chunks(4);
87    for chunk in chunks {
88        let mut vals = [0u32; 4];
89        let n = chunk.len();
90        for i in 0..n {
91            vals[i] = b64_val(chunk[i])? as u32;
92        }
93        if n >= 2 {
94            out.push(((vals[0] << 2) | (vals[1] >> 4)) as u8);
95        }
96        if n >= 3 {
97            out.push(((vals[1] << 4) | (vals[2] >> 2)) as u8);
98        }
99        if n >= 4 {
100            out.push(((vals[2] << 6) | vals[3]) as u8);
101        }
102    }
103    Some(out)
104}
105
106fn b64_val(b: u8) -> Option<u8> {
107    match b {
108        b'A'..=b'Z' => Some(b - b'A'),
109        b'a'..=b'z' => Some(b - b'a' + 26),
110        b'0'..=b'9' => Some(b - b'0' + 52),
111        b'+' => Some(62),
112        b'/' => Some(63),
113        _ => None,
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn hex_roundtrip() {
123        let data = b"\x00\x01\x0a\xff\xde\xad";
124        assert_eq!(to_hex(data), "00010affdead");
125        assert_eq!(from_hex("00010affDEAD").unwrap(), data);
126    }
127
128    #[test]
129    fn hex_empty() {
130        assert_eq!(to_hex(&[]), "");
131        assert_eq!(from_hex("").unwrap(), Vec::<u8>::new());
132    }
133
134    #[test]
135    fn hex_invalid() {
136        assert!(from_hex("0").is_none()); // odd length
137        assert!(from_hex("zz").is_none()); // bad chars
138    }
139
140    #[test]
141    fn hex_to_array_works() {
142        let arr: [u8; 3] = hex_to_array("aabbcc").unwrap();
143        assert_eq!(arr, [0xaa, 0xbb, 0xcc]);
144        assert!(hex_to_array::<4>("aabb").is_none()); // wrong length
145    }
146
147    #[test]
148    fn base64_roundtrip() {
149        let data = b"Hello, World!";
150        let encoded = to_base64(data);
151        assert_eq!(encoded, "SGVsbG8sIFdvcmxkIQ==");
152        assert_eq!(from_base64(&encoded).unwrap(), data);
153    }
154
155    #[test]
156    fn base64_empty() {
157        assert_eq!(to_base64(&[]), "");
158        assert_eq!(from_base64("").unwrap(), Vec::<u8>::new());
159    }
160
161    #[test]
162    fn base64_no_padding() {
163        // 3 bytes → 4 chars, no padding
164        assert_eq!(to_base64(b"abc"), "YWJj");
165        assert_eq!(from_base64("YWJj").unwrap(), b"abc");
166    }
167
168    #[test]
169    fn base64_one_pad() {
170        // 2 bytes → 3 chars + 1 pad
171        assert_eq!(to_base64(b"ab"), "YWI=");
172        assert_eq!(from_base64("YWI=").unwrap(), b"ab");
173        assert_eq!(from_base64("YWI").unwrap(), b"ab"); // without padding
174    }
175}