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