uselesskey_core_seed/
lib.rs1#![forbid(unsafe_code)]
2#![cfg_attr(not(feature = "std"), no_std)]
3extern crate alloc;
10
11use alloc::string::String;
12
13#[derive(Clone, Copy, Eq, PartialEq, Hash)]
15pub struct Seed(pub(crate) [u8; 32]);
16
17impl Seed {
18 pub fn new(bytes: [u8; 32]) -> Self {
20 Self(bytes)
21 }
22
23 pub fn bytes(&self) -> &[u8; 32] {
25 &self.0
26 }
27
28 pub fn from_text(text: &str) -> Self {
34 Self(*blake3::hash(text.as_bytes()).as_bytes())
35 }
36
37 pub fn from_env_value(value: &str) -> Result<Self, String> {
43 let v = value.trim();
44 let hex = v.strip_prefix("0x").unwrap_or(v);
45
46 if hex.len() == 64 {
47 return parse_hex_32(hex).map(Self);
48 }
49
50 Ok(Self::from_text(v))
51 }
52}
53
54impl core::fmt::Debug for Seed {
55 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56 f.write_str("Seed(**redacted**)")
57 }
58}
59
60fn parse_hex_32(hex: &str) -> Result<[u8; 32], String> {
61 fn val(c: u8) -> Option<u8> {
62 match c {
63 b'0'..=b'9' => Some(c - b'0'),
64 b'a'..=b'f' => Some(c - b'a' + 10),
65 b'A'..=b'F' => Some(c - b'A' + 10),
66 _ => None,
67 }
68 }
69
70 if hex.len() != 64 {
71 return Err(alloc::format!("expected 64 hex chars, got {}", hex.len()));
72 }
73
74 let bytes = hex.as_bytes();
75 let mut out = [0u8; 32];
76
77 for (i, chunk) in bytes.chunks_exact(2).enumerate() {
78 let hi = val(chunk[0])
79 .ok_or_else(|| alloc::format!("invalid hex char: {}", chunk[0] as char))?;
80 let lo = val(chunk[1])
81 .ok_or_else(|| alloc::format!("invalid hex char: {}", chunk[1] as char))?;
82 out[i] = (hi << 4) | lo;
83 }
84
85 Ok(out)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::{Seed, parse_hex_32};
91
92 #[test]
93 fn seed_debug_is_redacted() {
94 let seed = Seed::new([7u8; 32]);
95 assert_eq!(format!("{:?}", seed), "Seed(**redacted**)");
96 }
97
98 #[test]
99 fn parse_hex_32_rejects_wrong_length() {
100 let err = parse_hex_32("abcd").unwrap_err();
101 assert!(err.contains("expected 64 hex chars"));
102 }
103
104 #[test]
105 fn parse_hex_32_rejects_invalid_char() {
106 let mut s = "0".repeat(64);
107 s.replace_range(10..11, "g");
108
109 let err = parse_hex_32(&s).unwrap_err();
110 assert!(err.contains("invalid hex char"));
111 }
112
113 #[test]
114 fn seed_from_env_value_parses_hex_with_prefix_and_whitespace() {
115 let hex = "0x0000000000000000000000000000000000000000000000000000000000000001";
116 let seed = Seed::from_env_value(&format!(" {hex} ")).unwrap();
117 assert_eq!(seed.bytes()[31], 1);
118 assert!(seed.bytes()[..31].iter().all(|b| *b == 0));
119 }
120
121 #[test]
122 fn seed_from_env_value_parses_uppercase_hex() {
123 let hex = "F".repeat(64);
124 let seed = Seed::from_env_value(&hex).unwrap();
125 assert!(seed.bytes().iter().all(|b| *b == 0xFF));
126 }
127
128 #[test]
129 fn string_seed_is_hashed_with_blake3() {
130 let seed = Seed::from_env_value(" deterministic-seed-value ").unwrap();
131 let expected = blake3::hash("deterministic-seed-value".as_bytes());
132 assert_eq!(seed.bytes(), expected.as_bytes());
133 }
134
135 #[test]
136 fn from_text_hashes_verbatim_input() {
137 let text = " deterministic-seed-value ";
138 let seed = Seed::from_text(text);
139 let expected = blake3::hash(text.as_bytes());
140 assert_eq!(seed.bytes(), expected.as_bytes());
141 assert_ne!(seed, Seed::from_env_value(text).unwrap());
142 }
143
144 #[test]
145 fn from_text_does_not_parse_hex_shaped_strings() {
146 let text = "ab".repeat(32);
147 let seed = Seed::from_text(&text);
148 let expected = blake3::hash(text.as_bytes());
149 assert_eq!(seed.bytes(), expected.as_bytes());
150 assert_ne!(seed, Seed::from_env_value(&text).unwrap());
151 }
152
153 #[test]
154 fn parse_hex_32_lowercase_valid() {
155 let hex = "aa".repeat(32);
156 let result = parse_hex_32(&hex).unwrap();
157 assert!(result.iter().all(|b| *b == 0xAA));
158 }
159
160 #[test]
161 fn parse_hex_32_mixed_case_valid() {
162 let hex = "aAbBcCdDeEfF".repeat(5);
163 let hex = format!("{hex}0000");
165 assert_eq!(hex.len(), 64);
166 assert!(parse_hex_32(&hex).is_ok());
167 }
168
169 #[test]
170 fn parse_hex_32_invalid_lo_nibble() {
171 let mut hex = "0".repeat(64);
173 hex.replace_range(1..2, "z");
174 let err = parse_hex_32(&hex).unwrap_err();
175 assert!(err.contains("invalid hex char: z"));
176 }
177
178 #[test]
179 fn seed_equality_and_clone() {
180 let a = Seed::new([42u8; 32]);
181 let b = a;
182 assert_eq!(a, b);
183 assert_eq!(a.bytes(), b.bytes());
184 }
185
186 #[test]
187 fn seed_inequality() {
188 let a = Seed::new([1u8; 32]);
189 let b = Seed::new([2u8; 32]);
190 assert_ne!(a, b);
191 }
192
193 #[test]
194 fn seed_hash_consistent() {
195 use core::hash::{Hash, Hasher};
196 let seed = Seed::new([99u8; 32]);
197
198 let mut h1 = std::collections::hash_map::DefaultHasher::new();
199 seed.hash(&mut h1);
200 let hash1 = h1.finish();
201
202 let mut h2 = std::collections::hash_map::DefaultHasher::new();
203 seed.hash(&mut h2);
204 assert_eq!(hash1, h2.finish());
205 }
206
207 #[test]
208 fn from_env_value_short_string_uses_blake3() {
209 let seed = Seed::from_env_value("abc").unwrap();
210 let expected = blake3::hash(b"abc");
211 assert_eq!(seed.bytes(), expected.as_bytes());
212 }
213
214 #[test]
215 fn from_env_value_63_char_non_hex_uses_blake3() {
216 let input = "a".repeat(63);
218 let seed = Seed::from_env_value(&input).unwrap();
219 let expected = blake3::hash(input.as_bytes());
220 assert_eq!(seed.bytes(), expected.as_bytes());
221 }
222
223 #[test]
224 fn from_env_value_65_char_non_hex_uses_blake3() {
225 let input = "a".repeat(65);
227 let seed = Seed::from_env_value(&input).unwrap();
228 let expected = blake3::hash(input.as_bytes());
229 assert_eq!(seed.bytes(), expected.as_bytes());
230 }
231
232 #[test]
233 fn from_env_value_64_char_invalid_hex_returns_error() {
234 let input = "g".repeat(64);
236 assert!(Seed::from_env_value(&input).is_err());
237 }
238}