Skip to main content

seedfaker_core/
lib.rs

1#![forbid(unsafe_code)]
2
3pub mod corrupt;
4pub mod ctx;
5pub mod eval;
6pub mod field;
7pub mod gen;
8pub mod locale;
9pub mod opts;
10pub mod pipeline;
11pub mod rng;
12pub mod script;
13pub mod temporal;
14pub mod tz;
15pub mod validate;
16
17pub const VERSION: &str = env!("CARGO_PKG_VERSION");
18pub const DEFAULT_TZ_OFFSET: i32 = 0;
19
20// Domain keys for sub-seed derivation. Must match across CLI, npm, pip.
21pub const DOMAIN_IDENTITY: &str = "__identity__";
22pub const DOMAIN_CORRUPT: &str = "__corrupt__";
23pub const DOMAIN_SCRIPT: &str = "__script__";
24pub const DOMAIN_LOCALE: &str = "__locale__";
25pub const DOMAIN_TPL: &str = "__tpl__";
26
27pub fn hash_seed(s: &str) -> u64 {
28    fnv1a(s.as_bytes())
29}
30
31fn fnv1a(bytes: &[u8]) -> u64 {
32    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
33    for &b in bytes {
34        h ^= u64::from(b);
35        h = h.wrapping_mul(0x0100_0000_01b3);
36    }
37    h
38}
39
40/// Build info JSON: version + fingerprint for runtime integrity checks.
41pub fn build_info() -> String {
42    let fp = fingerprint();
43    format!(r#"{{"version":"{VERSION}","fingerprint":"{fp}"}}"#)
44}
45
46/// Generator fingerprint: identifies the current deterministic algorithm version.
47///
48/// If this value changes between releases, seeded output has changed —
49/// users must regenerate fixtures. Hashes default output + every modifier
50/// variant for each registered field.
51/// Format: `sf0-<16 hex digits>`.
52pub fn fingerprint() -> String {
53    use field::REGISTRY;
54
55    const CANONICAL_SEED: &str = "__determinism__";
56    let master = hash_seed(CANONICAL_SEED);
57    let locales: Vec<&locale::Locale> = locale::get("en").into_iter().collect();
58
59    let since = temporal::DEFAULT_SINCE;
60    let until = temporal::date_to_epoch(2038, 1, 1, 0, 0, 0);
61
62    let mut buf = String::new();
63    let mut val_buf = String::new();
64
65    let mut hash_field = |f: &field::Field, modifier: &str| {
66        let domain =
67            if modifier.is_empty() { f.id.to_string() } else { format!("{}_{modifier}", f.id) };
68        let mut ctx = ctx::GenContext {
69            rng: rng::Rng::derive(master, 0, &domain),
70            locales: &locales,
71            modifier,
72            identity: None,
73            tz_offset_minutes: DEFAULT_TZ_OFFSET,
74            since,
75            until,
76            range: None,
77            ordering: field::Ordering::None,
78            zipf: None,
79            numeric: None,
80        };
81        val_buf.clear();
82        f.generate(&mut ctx, &mut val_buf);
83        buf.push_str(&val_buf);
84        buf.push('\0');
85    };
86
87    for f in REGISTRY {
88        hash_field(f, "");
89        let mods = field::field_modifiers(f.id);
90        if !mods.is_empty() {
91            for m in mods.split(", ") {
92                hash_field(f, m);
93            }
94        }
95    }
96
97    let h = fnv1a(buf.as_bytes());
98    format!("sf0-{h:016x}")
99}