1#![forbid(unsafe_code)]
2
3use std::fmt;
12use std::sync::Arc;
13
14use uselesskey_core::Factory;
15
16pub const DOMAIN_ENTROPY_FIXTURE: &str = "uselesskey:entropy:fixture";
20
21#[derive(Clone)]
23pub struct EntropyFixture {
24 factory: Factory,
25 label: String,
26 variant: String,
27}
28
29struct Inner {
30 bytes: Vec<u8>,
31}
32
33impl fmt::Debug for EntropyFixture {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.debug_struct("EntropyFixture")
36 .field("label", &self.label)
37 .field("variant", &self.variant)
38 .finish_non_exhaustive()
39 }
40}
41
42pub trait EntropyFactoryExt {
44 fn entropy(&self, label: impl AsRef<str>) -> EntropyFixture;
46
47 fn entropy_with_variant(
49 &self,
50 label: impl AsRef<str>,
51 variant: impl AsRef<str>,
52 ) -> EntropyFixture;
53}
54
55impl EntropyFactoryExt for Factory {
56 fn entropy(&self, label: impl AsRef<str>) -> EntropyFixture {
57 EntropyFixture::new(self.clone(), label.as_ref(), "good")
58 }
59
60 fn entropy_with_variant(
61 &self,
62 label: impl AsRef<str>,
63 variant: impl AsRef<str>,
64 ) -> EntropyFixture {
65 EntropyFixture::new(self.clone(), label.as_ref(), variant.as_ref())
66 }
67}
68
69impl EntropyFixture {
70 fn new(factory: Factory, label: &str, variant: &str) -> Self {
71 Self {
72 factory,
73 label: label.to_string(),
74 variant: variant.to_string(),
75 }
76 }
77
78 pub fn label(&self) -> &str {
80 &self.label
81 }
82
83 pub fn variant(&self) -> &str {
85 &self.variant
86 }
87
88 pub fn bytes(&self, len: usize) -> Vec<u8> {
90 self.bytes_with_variant(len, &self.variant)
91 }
92
93 pub fn bytes_with_variant(&self, len: usize, variant: impl AsRef<str>) -> Vec<u8> {
95 load_inner(&self.factory, &self.label, len, variant.as_ref())
96 .bytes
97 .clone()
98 }
99
100 pub fn fill_bytes(&self, dest: &mut [u8]) {
102 self.fill_bytes_with_variant(dest, &self.variant);
103 }
104
105 pub fn fill_bytes_with_variant(&self, dest: &mut [u8], variant: impl AsRef<str>) {
107 let bytes = load_inner(&self.factory, &self.label, dest.len(), variant.as_ref());
108 dest.copy_from_slice(&bytes.bytes);
109 }
110}
111
112fn load_inner(factory: &Factory, label: &str, len: usize, variant: &str) -> Arc<Inner> {
113 factory.get_or_init(
114 DOMAIN_ENTROPY_FIXTURE,
115 label,
116 &len.to_le_bytes(),
117 variant,
118 |seed| {
119 let mut bytes = vec![0u8; len];
120 seed.fill_bytes(&mut bytes);
121 Inner { bytes }
122 },
123 )
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use proptest::prop_assert_eq;
130 use uselesskey_core::Seed;
131 use uselesskey_test_support::{TestResult, require_ok};
132
133 #[test]
134 fn deterministic_entropy_is_stable() -> TestResult<()> {
135 let fx = Factory::deterministic(require_ok(
136 Seed::from_env_value("entropy-det"),
137 "entropy-det must parse as a deterministic seed",
138 )?);
139 let a = fx.entropy("svc").bytes(32);
140 let b = fx.entropy("svc").bytes(32);
141 assert_eq!(a, b);
142 Ok(())
143 }
144
145 #[test]
146 fn random_mode_still_caches_per_identity() {
147 let fx = Factory::random();
148 let a = fx.entropy("svc").bytes(32);
149 let b = fx.entropy("svc").bytes(32);
150 assert_eq!(a, b);
151 }
152
153 #[test]
154 fn different_labels_produce_different_bytes() -> TestResult<()> {
155 let fx = Factory::deterministic(require_ok(
156 Seed::from_env_value("entropy-labels"),
157 "entropy-labels must parse as a deterministic seed",
158 )?);
159 let a = fx.entropy("a").bytes(32);
160 let b = fx.entropy("b").bytes(32);
161 assert_ne!(a, b);
162 Ok(())
163 }
164
165 #[test]
166 fn different_variants_produce_different_bytes() -> TestResult<()> {
167 let fx = Factory::deterministic(require_ok(
168 Seed::from_env_value("entropy-variants"),
169 "entropy-variants must parse as a deterministic seed",
170 )?);
171 let fixture = fx.entropy("svc");
172 let good = fixture.bytes(32);
173 let alt = fixture.bytes_with_variant(32, "alt");
174 assert_ne!(good, alt);
175 Ok(())
176 }
177
178 #[test]
179 fn accessors_report_fixture_identity() -> TestResult<()> {
180 let fx = Factory::deterministic(require_ok(
181 Seed::from_env_value("entropy-identity"),
182 "entropy-identity must parse as a deterministic seed",
183 )?);
184 let default = fx.entropy("svc");
185 let custom = fx.entropy_with_variant("svc-alt", "custom");
186
187 assert_eq!(default.label(), "svc");
188 assert_eq!(default.variant(), "good");
189 assert_eq!(custom.label(), "svc-alt");
190 assert_eq!(custom.variant(), "custom");
191 Ok(())
192 }
193
194 #[test]
195 fn fill_bytes_matches_allocating_path() -> TestResult<()> {
196 let fx = Factory::deterministic(require_ok(
197 Seed::from_env_value("entropy-fill"),
198 "entropy-fill must parse as a deterministic seed",
199 )?);
200 let fixture = fx.entropy("svc");
201
202 let expected = fixture.bytes(24);
203 let mut actual = [0u8; 24];
204 fixture.fill_bytes(&mut actual);
205
206 assert_eq!(expected, actual);
207 Ok(())
208 }
209
210 #[test]
211 fn debug_does_not_include_bytes() -> TestResult<()> {
212 let fx = Factory::deterministic(require_ok(
213 Seed::from_env_value("entropy-debug"),
214 "entropy-debug must parse as a deterministic seed",
215 )?);
216 let fixture = fx.entropy("svc");
217 let dbg = format!("{fixture:?}");
218
219 assert!(dbg.contains("EntropyFixture"));
220 assert!(dbg.contains("svc"));
221 assert!(!dbg.contains("["));
222 Ok(())
223 }
224
225 proptest::proptest! {
226 #[test]
227 fn requested_length_is_preserved(len in 0usize..2048) {
228 let fx = Factory::deterministic(Seed::new([7u8; 32]));
229 let bytes = fx.entropy("prop").bytes(len);
230 prop_assert_eq!(bytes.len(), len);
231 }
232 }
233
234 #[test]
235 fn entropy_with_variant_constructor_derives_from_custom_variant() -> TestResult<()> {
236 let fx = Factory::deterministic(require_ok(
237 Seed::from_env_value("entropy-with-variant-ctor"),
238 "must parse seed",
239 )?);
240
241 let good = fx.entropy("svc").bytes(32);
242 let custom = fx.entropy_with_variant("svc", "alt").bytes(32);
243
244 assert_ne!(
245 good, custom,
246 "entropy_with_variant must derive from the variant, not the default"
247 );
248
249 let custom_again = fx.entropy("svc").bytes_with_variant(32, "alt");
252 assert_eq!(custom, custom_again);
253 Ok(())
254 }
255
256 #[test]
257 fn fill_bytes_with_variant_matches_allocating_path() -> TestResult<()> {
258 let fx = Factory::deterministic(require_ok(
259 Seed::from_env_value("entropy-fill-with-variant"),
260 "must parse seed",
261 )?);
262 let fixture = fx.entropy("svc");
263
264 let expected = fixture.bytes_with_variant(40, "alt");
265 let mut actual = [0u8; 40];
266 fixture.fill_bytes_with_variant(&mut actual, "alt");
267
268 assert_eq!(expected.as_slice(), &actual[..]);
269
270 let mut default_filled = [0u8; 40];
272 fixture.fill_bytes(&mut default_filled);
273 assert_ne!(actual, default_filled);
274 Ok(())
275 }
276
277 #[test]
278 fn zero_length_request_returns_empty_buffer() -> TestResult<()> {
279 let fx = Factory::deterministic(require_ok(
280 Seed::from_env_value("entropy-zero-len"),
281 "must parse seed",
282 )?);
283 let fixture = fx.entropy("svc");
284
285 let empty = fixture.bytes(0);
286 assert!(empty.is_empty());
287
288 let mut buf: [u8; 0] = [];
290 fixture.fill_bytes(&mut buf);
291 Ok(())
292 }
293
294 #[test]
295 fn distinct_lengths_produce_distinct_caches() -> TestResult<()> {
296 let fx = Factory::deterministic(require_ok(
300 Seed::from_env_value("entropy-len-distinct"),
301 "must parse seed",
302 )?);
303 let fixture = fx.entropy("svc");
304
305 let short = fixture.bytes(16);
306 let long = fixture.bytes(32);
307
308 assert_eq!(short.len(), 16);
309 assert_eq!(long.len(), 32);
310 assert_ne!(
311 short.as_slice(),
312 &long[..16],
313 "different lengths must hit distinct cache entries, not slice the same stream"
314 );
315 Ok(())
316 }
317
318 #[test]
319 fn cloned_fixture_handles_share_cache_identity() -> TestResult<()> {
320 let fx = Factory::deterministic(require_ok(
321 Seed::from_env_value("entropy-clone-handle"),
322 "must parse seed",
323 )?);
324 let fixture = fx.entropy("svc");
325 let cloned = fixture.clone();
326
327 assert_eq!(fixture.label(), cloned.label());
328 assert_eq!(fixture.variant(), cloned.variant());
329 assert_eq!(fixture.bytes(48), cloned.bytes(48));
330 Ok(())
331 }
332
333 #[test]
334 fn domain_constant_is_stable_for_the_lifetime_of_v1() {
335 assert_eq!(DOMAIN_ENTROPY_FIXTURE, "uselesskey:entropy:fixture");
339 }
340}