1use alloc::string::ToString;
8use alloc::sync::Arc;
9use core::fmt;
10
11#[cfg(feature = "std")]
12use std::collections::BTreeSet;
13#[cfg(feature = "std")]
14use std::sync::{Condvar, Mutex, MutexGuard};
15#[cfg(all(feature = "std", test))]
16use std::time::Duration;
17
18#[cfg(feature = "std")]
19use rand10::TryRng;
20#[cfg(feature = "std")]
21use rand10::rngs::SysRng;
22
23use crate::srp::cache::ArtifactCache;
24use crate::srp::identity::{ArtifactDomain, ArtifactId, DerivationVersion, Seed, derive_seed};
25
26#[derive(Clone, Debug)]
28pub enum Mode {
29 Random,
31
32 Deterministic { master: Seed },
34}
35
36struct Inner {
37 mode: Mode,
38 cache: ArtifactCache,
39 #[cfg(feature = "std")]
40 init_locks: InitLocks,
41}
42
43#[cfg(feature = "std")]
44#[derive(Default)]
45struct InitLocks {
46 active: Mutex<BTreeSet<ArtifactId>>,
47 ready: Condvar,
48}
49
50#[cfg(feature = "std")]
51struct InitGuard<'a> {
52 locks: &'a InitLocks,
53 id: ArtifactId,
54}
55
56#[cfg(feature = "std")]
57impl InitLocks {
58 fn acquire(&self, id: &ArtifactId) -> InitGuard<'_> {
59 let mut active = self.active();
60 loop {
61 if active.insert(id.clone()) {
62 break;
63 }
64
65 #[cfg(test)]
66 {
67 let (next, wait_result) = self
68 .ready
69 .wait_timeout(active, Duration::from_secs(2))
70 .unwrap_or_else(|err| err.into_inner());
71 assert!(
72 !wait_result.timed_out(),
73 "timed out waiting for init guard for {id:?}"
74 );
75 active = next;
76 }
77
78 #[cfg(not(test))]
79 {
80 active = self
81 .ready
82 .wait(active)
83 .unwrap_or_else(|err| err.into_inner());
84 }
85 }
86
87 InitGuard {
88 locks: self,
89 id: id.clone(),
90 }
91 }
92
93 fn active(&self) -> MutexGuard<'_, BTreeSet<ArtifactId>> {
94 self.active.lock().unwrap_or_else(|err| err.into_inner())
95 }
96}
97
98#[cfg(feature = "std")]
99impl Drop for InitGuard<'_> {
100 fn drop(&mut self) {
101 let mut active = self.locks.active();
102 active.remove(&self.id);
103 self.locks.ready.notify_all();
104 }
105}
106
107#[derive(Clone)]
111pub struct Factory {
112 inner: Arc<Inner>,
113}
114
115impl fmt::Debug for Factory {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("Factory")
118 .field("mode", &self.inner.mode)
119 .field("cache_size", &self.inner.cache.len())
120 .finish()
121 }
122}
123
124impl Factory {
125 pub fn new(mode: Mode) -> Self {
127 Self {
128 inner: Arc::new(Inner {
129 mode,
130 cache: ArtifactCache::new(),
131 #[cfg(feature = "std")]
132 init_locks: InitLocks::default(),
133 }),
134 }
135 }
136
137 pub fn random() -> Self {
139 Self::new(Mode::Random)
140 }
141
142 pub fn deterministic(master: Seed) -> Self {
144 Self::new(Mode::Deterministic { master })
145 }
146
147 pub fn deterministic_from_str(text: &str) -> Self {
153 Self::deterministic(Seed::from_text(text))
154 }
155
156 #[cfg(feature = "std")]
166 pub fn deterministic_from_env(var: &str) -> Result<Self, crate::Error> {
167 let raw = std::env::var(var).map_err(|_| crate::Error::MissingEnvVar {
168 var: var.to_string(),
169 })?;
170
171 let seed = Seed::from_env_value(&raw).map_err(|message| crate::Error::InvalidSeed {
172 var: var.to_string(),
173 message,
174 })?;
175
176 Ok(Self::deterministic(seed))
177 }
178
179 pub fn mode(&self) -> &Mode {
181 &self.inner.mode
182 }
183
184 pub fn clear_cache(&self) {
186 self.inner.cache.clear();
187 }
188
189 pub fn get_or_init<T, F>(
194 &self,
195 domain: ArtifactDomain,
196 label: &str,
197 spec_bytes: &[u8],
198 variant: &str,
199 init: F,
200 ) -> Arc<T>
201 where
202 T: core::any::Any + Send + Sync + 'static,
203 F: FnOnce(Seed) -> T,
204 {
205 let id = ArtifactId::new(
206 domain,
207 label.to_string(),
208 spec_bytes,
209 variant.to_string(),
210 DerivationVersion::V1,
211 );
212
213 if let Some(entry) = self.inner.cache.get_typed::<T>(&id) {
214 return entry;
215 }
216
217 #[cfg(feature = "std")]
218 let _init_guard = self.inner.init_locks.acquire(&id);
219
220 if let Some(entry) = self.inner.cache.get_typed::<T>(&id) {
221 return entry;
222 }
223
224 let seed = self.seed_for(&id);
225 let value = init(seed);
226 let arc: Arc<T> = Arc::new(value);
227
228 self.inner.cache.insert_if_absent_typed(id, arc)
229 }
230
231 fn seed_for(&self, id: &ArtifactId) -> Seed {
232 match &self.inner.mode {
233 Mode::Random => random_seed(),
234 Mode::Deterministic { master } => derive_seed(master, id),
235 }
236 }
237}
238
239#[cfg(feature = "std")]
240pub(crate) fn random_seed() -> Seed {
241 let mut bytes = [0u8; 32];
242 SysRng
243 .try_fill_bytes(&mut bytes)
244 .expect("failed to read operating-system randomness");
245 Seed::new(bytes)
246}
247
248#[cfg(not(feature = "std"))]
249pub(crate) fn random_seed() -> Seed {
250 panic!("uselesskey-core-factory: Mode::Random requires the `std` feature")
251}
252
253#[cfg(all(test, feature = "std"))]
254mod tests {
255 use super::{Factory, InitLocks, Mode, random_seed};
256 use crate::Seed;
257 use crate::srp::identity::{ArtifactId, DerivationVersion};
258 use std::panic::{AssertUnwindSafe, catch_unwind};
259 use std::sync::Arc;
260 use std::sync::atomic::{AtomicUsize, Ordering};
261
262 fn draw_u64(seed: Seed) -> u64 {
263 let mut bytes = [0u8; 8];
264 seed.fill_bytes(&mut bytes);
265 u64::from_le_bytes(bytes)
266 }
267
268 #[test]
269 fn clear_cache_forces_reinit() {
270 let fx = Factory::random();
271 let hits = AtomicUsize::new(0);
272
273 let first = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| {
274 hits.fetch_add(1, Ordering::SeqCst);
275 42u8
276 });
277
278 assert_eq!(hits.load(Ordering::SeqCst), 1);
279 let second = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| {
280 hits.fetch_add(1, Ordering::SeqCst);
281 99u8
282 });
283 assert!(Arc::ptr_eq(&first, &second));
284
285 fx.clear_cache();
286 let third = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| {
287 hits.fetch_add(1, Ordering::SeqCst);
288 44u8
289 });
290
291 assert_eq!(hits.load(Ordering::SeqCst), 2);
292 assert!(!Arc::ptr_eq(&first, &third));
293 }
294
295 #[test]
296 fn get_or_init_type_mismatch_panics() {
297 let fx = Factory::random();
298 let _ = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| 123u32);
299 let result = catch_unwind(AssertUnwindSafe(|| {
300 let _ = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| {
301 "oops".to_string()
302 });
303 }));
304
305 assert!(result.is_err(), "expected panic on type mismatch");
306 }
307
308 #[test]
309 fn random_seed_has_expected_length() {
310 let seed = random_seed();
311 assert_eq!(seed.bytes().len(), 32);
312 }
313
314 #[test]
315 fn init_lock_marks_id_active_until_guard_drop() {
316 let locks = InitLocks::default();
317 let id = ArtifactId::new(
318 "domain:init-lock",
319 "label",
320 b"spec",
321 "good",
322 DerivationVersion::V1,
323 );
324
325 let guard = locks.acquire(&id);
326
327 assert!(
328 locks.active().contains(&id),
329 "init guard should mark the artifact identity active"
330 );
331
332 drop(guard);
333
334 assert!(
335 !locks.active().contains(&id),
336 "dropping init guard should clear the active artifact identity"
337 );
338 }
339
340 #[test]
341 fn same_identity_returns_cached_value_without_rerunning_initializer() {
342 let fx = Factory::deterministic(Seed::new([9u8; 32]));
343 let hits = AtomicUsize::new(0);
344
345 let first = fx.get_or_init("domain:concurrent", "label", b"spec", "good", |_seed| {
346 hits.fetch_add(1, Ordering::SeqCst);
347 77u32
348 });
349 let second = fx.get_or_init("domain:concurrent", "label", b"spec", "good", |_seed| {
350 hits.fetch_add(1, Ordering::SeqCst);
351 99u32
352 });
353
354 assert!(Arc::ptr_eq(&first, &second));
355 assert_eq!(*first, 77);
356 assert_eq!(hits.load(Ordering::SeqCst), 1);
357 }
358
359 #[test]
360 fn get_or_init_reentrant_does_not_deadlock() {
361 let fx = Factory::deterministic(Seed::new([42u8; 32]));
362
363 let outer: Arc<String> = fx.get_or_init("test:outer", "label", b"spec", "good", |_rng| {
364 let inner: Arc<u64> =
365 fx.get_or_init("test:inner", "label", b"spec", "good", |_rng| 42u64);
366 format!("outer-{}", *inner)
367 });
368
369 assert_eq!(*outer, "outer-42");
370 }
371
372 #[test]
373 fn debug_includes_cache_size() {
374 let fx = Factory::random();
375 let dbg = format!("{:?}", fx);
376 assert!(dbg.contains("cache_size: 0"), "empty factory: {dbg}");
377
378 let _ = fx.get_or_init("domain:test", "label", b"spec", "good", |_rng| 7u8);
379 let dbg = format!("{:?}", fx);
380 assert!(dbg.contains("cache_size: 1"), "after insert: {dbg}");
381 }
382
383 #[test]
384 fn mode_pattern_matches_deterministic() {
385 let seed = Seed::new([1u8; 32]);
386 let fx = Factory::deterministic(seed);
387 match fx.mode() {
388 Mode::Deterministic { master } => assert_eq!(master.bytes(), seed.bytes()),
389 Mode::Random => panic!("wrong mode"),
390 }
391 }
392
393 #[test]
394 fn mode_pattern_matches_random() {
395 let fx = Factory::random();
396 assert!(matches!(fx.mode(), Mode::Random));
397 }
398
399 #[test]
400 fn deterministic_same_inputs_yield_same_output() {
401 let fx = Factory::deterministic(Seed::new([7u8; 32]));
402 let a: Arc<u64> = fx.get_or_init("domain:det", "lbl", b"sp", "good", draw_u64);
403 fx.clear_cache();
405 let b: Arc<u64> = fx.get_or_init("domain:det", "lbl", b"sp", "good", draw_u64);
406 assert_eq!(*a, *b, "deterministic mode must reproduce the same value");
407 }
408
409 #[test]
410 fn clone_shares_cache() {
411 let fx = Factory::random();
412 let _ = fx.get_or_init("domain:clone", "lbl", b"sp", "good", |_| 99u32);
413 let fx2 = fx.clone();
414 let val = fx2.get_or_init("domain:clone", "lbl", b"sp", "good", |_| 0u32);
415 assert_eq!(*val, 99, "clone must share the same cache");
416 }
417
418 #[test]
419 fn different_domains_produce_distinct_entries() {
420 let fx = Factory::deterministic(Seed::new([1u8; 32]));
421 let a: Arc<u64> = fx.get_or_init("domain:a", "lbl", b"sp", "good", draw_u64);
422 let b: Arc<u64> = fx.get_or_init("domain:b", "lbl", b"sp", "good", draw_u64);
423 assert_ne!(*a, *b);
424 }
425
426 #[test]
427 fn different_variants_produce_distinct_entries() {
428 let fx = Factory::deterministic(Seed::new([2u8; 32]));
429 let a: Arc<u64> = fx.get_or_init("domain:v", "lbl", b"sp", "good", draw_u64);
430 let b: Arc<u64> = fx.get_or_init("domain:v", "lbl", b"sp", "bad", draw_u64);
431 assert_ne!(*a, *b);
432 }
433
434 #[test]
435 fn different_specs_produce_distinct_entries() {
436 let fx = Factory::deterministic(Seed::new([3u8; 32]));
437 let a: Arc<u64> = fx.get_or_init("domain:s", "lbl", b"RS256", "good", draw_u64);
438 let b: Arc<u64> = fx.get_or_init("domain:s", "lbl", b"RS384", "good", draw_u64);
439 assert_ne!(*a, *b);
440 }
441
442 #[test]
443 fn debug_mode_random() {
444 let fx = Factory::random();
445 let dbg = format!("{:?}", fx);
446 assert!(
447 dbg.contains("Random"),
448 "debug should show Random mode: {dbg}"
449 );
450 }
451
452 #[test]
453 fn debug_mode_deterministic() {
454 let fx = Factory::deterministic(Seed::new([0u8; 32]));
455 let dbg = format!("{:?}", fx);
456 assert!(
457 dbg.contains("Deterministic"),
458 "debug should show Deterministic mode: {dbg}"
459 );
460 assert!(
461 dbg.contains("redacted"),
462 "seed must be redacted in debug output: {dbg}"
463 );
464 }
465
466 #[test]
467 fn deterministic_from_str_preserves_whitespace() {
468 let exact = Factory::deterministic_from_str(" seed-value ");
469 let trimmed = Factory::deterministic_from_str("seed-value");
470
471 let a: Arc<u64> = exact.get_or_init("domain:text", "label", b"spec", "good", draw_u64);
472 let b: Arc<u64> = trimmed.get_or_init("domain:text", "label", b"spec", "good", draw_u64);
473
474 assert_ne!(
475 *a, *b,
476 "deterministic_from_str should hash the text verbatim, including whitespace"
477 );
478 }
479
480 #[test]
481 fn deterministic_from_str_matches_seed_from_text() {
482 let from_str = Factory::deterministic_from_str("seed-value");
483 let from_seed = Factory::deterministic(Seed::from_text("seed-value"));
484
485 let a: Arc<u64> = from_str.get_or_init("domain:text", "label", b"spec", "good", draw_u64);
486 let b: Arc<u64> = from_seed.get_or_init("domain:text", "label", b"spec", "good", draw_u64);
487
488 assert_eq!(*a, *b);
489 }
490}