1#[must_use]
17pub const fn armor_char_to_sixbit(c: u8) -> u8 {
18 let v = c.wrapping_sub(48);
19 if v > 40 { v.wrapping_sub(8) } else { v }
20}
21
22#[must_use]
24pub const fn sixbit_to_armor_char(v: u8) -> u8 {
25 let c = v + 48;
26 if c > 87 { c + 8 } else { c }
27}
28
29#[must_use]
31pub const fn is_valid_armor_byte(c: u8) -> bool {
32 matches!(c, 48..=87 | 96..=119)
33}
34
35#[must_use]
37pub const fn sixbit_to_ascii(v: u8) -> u8 {
38 if v < 32 { v + 64 } else { v }
39}
40
41#[must_use]
46pub const fn ascii_to_sixbit(c: u8) -> u8 {
47 match c {
48 b'@'..=b'_' => c - 64,
49 b' '..=b'?' => c,
50 _ => 32,
51 }
52}
53
54#[derive(Debug, Clone, Copy)]
59pub struct FixedStr<const N: usize> {
60 buf: [u8; N],
61 len: usize,
62}
63
64impl<const N: usize> PartialEq for FixedStr<N> {
65 fn eq(&self, other: &Self) -> bool {
66 self.as_str() == other.as_str()
67 }
68}
69
70impl<const N: usize> Eq for FixedStr<N> {}
71
72impl<const N: usize> FixedStr<N> {
73 #[must_use]
76 pub const fn from_raw(buf: [u8; N], len: usize) -> Self {
77 Self { buf, len }
78 }
79
80 #[must_use]
85 pub fn as_str(&self) -> &str {
86 let mut end = self.len;
87 while end > 0 && matches!(self.buf[end - 1], b'@' | b' ') {
88 end -= 1;
89 }
90 core::str::from_utf8(&self.buf[..end]).unwrap_or_default()
91 }
92}
93
94impl<const N: usize> core::fmt::Display for FixedStr<N> {
95 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100#[cfg(test)]
105pub(crate) fn test_padded<const N: usize>(s: &str) -> FixedStr<N> {
106 let mut buf = [b'@'; N];
107 let bytes = s.as_bytes();
108 buf[..bytes.len()].copy_from_slice(bytes);
109 FixedStr::from_raw(buf, N)
110}
111
112#[cfg(kani)]
113mod kani_proofs {
114 use super::*;
115
116 #[kani::proof]
120 fn armor_roundtrip() {
121 let v: u8 = kani::any();
122 kani::assume(v < 64);
123 let c = sixbit_to_armor_char(v);
124 assert!(is_valid_armor_byte(c));
125 assert_eq!(armor_char_to_sixbit(c), v);
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn armor_roundtrip_covers_full_range() {
135 for v in 0u8..64 {
136 let c = sixbit_to_armor_char(v);
137 assert!(is_valid_armor_byte(c), "char {c} for value {v} not valid");
138 assert_eq!(armor_char_to_sixbit(c), v);
139 }
140 }
141
142 #[test]
143 fn armor_known_values() {
144 assert_eq!(sixbit_to_armor_char(0), b'0');
145 assert_eq!(sixbit_to_armor_char(39), b'W');
146 assert_eq!(sixbit_to_armor_char(40), b'`');
147 assert_eq!(sixbit_to_armor_char(63), b'w');
148 }
149
150 #[test]
151 fn text_alphabet_roundtrip_covers_full_range() {
152 for v in 0u8..64 {
153 let c = sixbit_to_ascii(v);
154 assert_eq!(ascii_to_sixbit(c), v);
155 }
156 }
157
158 #[test]
159 fn fixed_str_trims_padding() {
160 let mut buf = [0u8; 8];
161 buf[..5].copy_from_slice(b"ABC@@");
162 let s: FixedStr<8> = FixedStr::from_raw(buf, 5);
163 assert_eq!(s.as_str(), "ABC");
164 }
165}