use std::path::Path;
const BASIS: u32 = 2_166_136_261;
const PRIME: u32 = 16_777_619;
fn fnv1a(bytes: &[u8], mut hash: u32) -> u32 {
for byte in bytes {
hash ^= *byte as u32;
hash = hash.wrapping_mul(PRIME);
}
hash
}
pub fn version_of_sources<I, B>(sources: I) -> u32
where
I: IntoIterator<Item = B>,
B: AsRef<[u8]>,
{
let mut hash = BASIS;
for source in sources {
let text: Vec<u8> = source.as_ref().iter().copied().filter(|b| *b != b'\r').collect();
hash = fnv1a(&text, hash);
}
hash.max(1)
}
pub fn version_of<P: AsRef<Path>>(sources: &[P]) -> u32 {
let contents: Vec<Vec<u8>> = sources
.iter()
.map(|source| {
let path = source.as_ref();
std::fs::read(path).unwrap_or_else(|e| panic!("wire version: cannot read {}: {e}", path.display()))
})
.collect();
version_of_sources(contents)
}
pub fn emit<P: AsRef<Path>>(sources: &[P]) {
for source in sources {
println!("cargo:rerun-if-changed={}", source.as_ref().display());
}
let version = version_of(sources);
println!("cargo:rustc-env=WIRE_PROTOCOL={version}");
let out_dir = std::env::var("OUT_DIR").expect("wire version: OUT_DIR is unset, so this is not running as a build script");
let path = Path::new(&out_dir).join("wire_protocol.rs");
let generated = format!(
"// Generated by plaza_wire::build. Do not edit; it is rewritten every build.\n\
/// The wire format's version, hashed from the sources that define it.\n\
pub const WIRE_PROTOCOL: u32 = {version};\n"
);
std::fs::write(&path, generated).unwrap_or_else(|e| panic!("wire version: cannot write {}: {e}", path.display()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_same_sources_always_give_the_same_version() {
let sources = [&b"enum Op { Ping }"[..], &b"struct Packet;"[..]];
assert_eq!(version_of_sources(sources), version_of_sources(sources));
}
#[test]
fn changing_a_source_changes_the_version() {
let before = version_of_sources([&b"enum Op { Ping }"[..]]);
let after = version_of_sources([&b"enum Op { Ping, Pong }"[..]]);
assert_ne!(before, after, "a wire change that did not move the version is the bug this prevents");
}
#[test]
fn line_endings_do_not_change_the_version() {
let unix = version_of_sources([&b"enum Op {\n Ping,\n}\n"[..]]);
let dos = version_of_sources([&b"enum Op {\r\n Ping,\r\n}\r\n"[..]]);
assert_eq!(unix, dos);
}
#[test]
fn the_version_is_never_zero() {
assert_ne!(version_of_sources(Vec::<&[u8]>::new()), 0);
assert_ne!(version_of_sources([&b""[..]]), 0);
}
#[test]
fn every_source_in_the_list_counts() {
let one = version_of_sources([&b"struct A;"[..]]);
let both = version_of_sources([&b"struct A;"[..], &b"struct B;"[..]]);
assert_ne!(one, both);
}
}