Skip to main content

uselesskey_core/srp/
factory.rs

1//! Factory orchestration and cache lookup for uselesskey fixtures.
2//!
3//! Implements the core `Factory` type that manages deterministic derivation,
4//! caching, and artifact generation. Operates in either Random or Deterministic
5//! mode based on seed configuration.
6
7use 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/// How a [`Factory`] generates artifacts.
27#[derive(Clone, Debug)]
28pub enum Mode {
29    /// Artifacts are generated using platform randomness.
30    Random,
31
32    /// Artifacts are generated deterministically from a master seed.
33    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/// A factory for generating and caching test artifacts.
108///
109/// `Factory` is cheap to clone; clones share the same cache.
110#[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    /// Create a new factory with the specified mode.
126    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    /// Create a factory in random mode.
138    pub fn random() -> Self {
139        Self::new(Mode::Random)
140    }
141
142    /// Create a factory in deterministic mode from a master seed.
143    pub fn deterministic(master: Seed) -> Self {
144        Self::new(Mode::Deterministic { master })
145    }
146
147    /// Create a deterministic factory from plain text.
148    ///
149    /// This hashes the provided string verbatim with BLAKE3. Unlike
150    /// [`Seed::from_env_value`], it does not trim whitespace or interpret
151    /// hex-shaped strings specially.
152    pub fn deterministic_from_str(text: &str) -> Self {
153        Self::deterministic(Seed::from_text(text))
154    }
155
156    /// Create a deterministic factory from an environment variable.
157    ///
158    /// The environment variable can contain:
159    /// - A 64-character hex string (with optional `0x` prefix)
160    /// - Any other string (hashed to produce a 32-byte seed)
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the environment variable is not set.
165    #[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    /// Return the active mode.
180    pub fn mode(&self) -> &Mode {
181        &self.inner.mode
182    }
183
184    /// Clear the artifact cache.
185    pub fn clear_cache(&self) {
186        self.inner.cache.clear();
187    }
188
189    /// Return a cached value by `(domain, label, spec, variant)` or generate one.
190    ///
191    /// The initializer receives the derived seed for this artifact identity.
192    /// Callers that need an RNG should instantiate it privately from that seed.
193    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        // Clear cache so init runs again from the same derived seed.
404        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}