flybrain/services/flysim/crates/flybrain-core/src/version.rs
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

76 lines
2.6 KiB
Rust

//! Version-string hashing for non-default kernel configurations, matching `model/version.ts`.
use crate::jsmath::number_to_string;
/// FNV-1a-32 over the UTF-16 code units of `text`, as eight lowercase hex digits.
pub fn fnv1a32_hex(text: &str) -> String {
format!("{:08x}", fnv1a32(text))
}
/// FNV-1a-32 over the UTF-16 code units of `text`.
///
/// `Math.imul(hash ^ code, 16777619) >>> 0` in the original: a wrapping 32-bit multiply.
pub fn fnv1a32(text: &str) -> u32 {
let mut hash: u32 = 2_166_136_261;
for unit in text.encode_utf16() {
hash = (hash ^ u32::from(unit)).wrapping_mul(16_777_619);
}
hash
}
/// FNV-1a-32 fold of one signed 32-bit value into a running hash.
///
/// The topology hash in `model/plasticity.ts` folds numbers, not characters:
/// `hash = Math.imul(hash ^ value, 16777619) >>> 0`. `hash ^ value` is an int32 XOR, so a negative
/// weight sign-extends, which is why the caller passes `i32` rather than `u32`.
#[inline]
pub fn fnv1a32_step(hash: u32, value: i32) -> u32 {
((hash as i32) ^ value).wrapping_mul(16_777_619) as u32
}
/// `base` when every parameter equals its default, otherwise `prefix` plus the FNV-1a-32 hash of
/// the parameters joined with `,`. Parameter order is part of the version contract.
pub fn version_for(base: &str, prefix: &str, params: &[f64], defaults: &[f64]) -> String {
if params.len() == defaults.len()
&& params
.iter()
.zip(defaults)
// `===` on numbers: NaN never equals itself, +0 equals -0.
.all(|(value, default)| value == default)
{
return base.to_string();
}
let joined = params
.iter()
.map(|value| number_to_string(*value))
.collect::<Vec<_>>()
.join(",");
format!("{prefix}{}", fnv1a32_hex(&joined))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a32_matches_the_reference() {
// node -e "const h=t=>{let x=2166136261;for(const c of t)x=Math.imul(x^c.charCodeAt(0),16777619)>>>0;
// return x.toString(16).padStart(8,'0')};console.log(h(''),h('a'),h('hello'))"
assert_eq!(fnv1a32_hex(""), "811c9dc5");
assert_eq!(fnv1a32_hex("a"), "e40c292c");
assert_eq!(fnv1a32_hex("hello"), "4f9f2cab");
}
#[test]
fn version_for_keeps_the_base_when_defaults_are_unchanged() {
let defaults = [20.0, 1.0, 2.0];
assert_eq!(
version_for("base", "base:", &defaults, &defaults),
"base".to_string()
);
assert_eq!(
version_for("base", "base:", &[25.0, 1.0, 2.0], &defaults),
format!("base:{}", fnv1a32_hex("25,1,2"))
);
}
}