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 type_definitions(source: &[u8]) -> String {
let text = String::from_utf8_lossy(source);
let mut kept = String::new();
let mut pending_attrs = String::new();
let mut depth = 0usize;
let mut capturing = false;
for raw_line in text.lines() {
let line = match raw_line.find("//") {
Some(at) => &raw_line[..at],
None => raw_line,
};
let line = line.trim();
if line.is_empty() && !capturing {
continue;
}
if capturing {
push_normalised(&mut kept, line);
depth += line.matches('{').count();
depth = depth.saturating_sub(line.matches('}').count());
if depth == 0 && (line.ends_with('}') || line.ends_with(';')) {
capturing = false;
}
continue;
}
if line.starts_with("#[") || line.starts_with("#!") {
push_normalised(&mut pending_attrs, line);
continue;
}
if declares_type(line) {
kept.push_str(&pending_attrs);
pending_attrs.clear();
push_normalised(&mut kept, line);
depth = line.matches('{').count().saturating_sub(line.matches('}').count());
capturing = depth > 0;
continue;
}
pending_attrs.clear();
}
kept
}
fn declares_type(line: &str) -> bool {
let mut words = line.split_whitespace().peekable();
while let Some(word) = words.next() {
match word {
"pub" => continue,
w if w.starts_with("pub(") => continue,
"struct" | "enum" | "union" => return words.peek().is_some(),
_ => return false,
}
}
false
}
fn push_normalised(out: &mut String, line: &str) {
for word in line.split_whitespace() {
out.push_str(word);
}
}
fn strip_trailing_commas(text: &str) -> String {
text.replace(",}", "}").replace(",)", ")")
}
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(strip_trailing_commas(&type_definitions(&text)).as_bytes(), 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 a_shape_change_moves_the_version() {
let before = version_of_sources([&b"enum Op { Ping }"[..]]);
assert_ne!(before, version_of_sources([&b"enum Op { Ping, Pong }"[..]]), "a new variant");
assert_ne!(before, version_of_sources([&b"enum Op { Pong }"[..]]), "a renamed variant");
let s = &b"struct P { a: u8, b: u8 }"[..];
assert_ne!(
version_of_sources([s]),
version_of_sources([&b"struct P { b: u8, a: u8 }"[..]]),
"reordering, which changes any positional encoding"
);
assert_ne!(
version_of_sources([s]),
version_of_sources([&b"struct P { a: u8, b: u16 }"[..]]),
"a field's type"
);
assert_ne!(
version_of_sources([&b"enum Op { A = 0 }"[..]]),
version_of_sources([&b"enum Op { A = 7 }"[..]]),
"an explicit discriminant"
);
assert_ne!(
version_of_sources([&b"struct P { #[serde(rename = \"a\")] alpha: u8 }"[..]]),
version_of_sources([&b"struct P { #[serde(rename = \"z\")] alpha: u8 }"[..]]),
"a serde attribute, which changes the wire without changing the type"
);
}
#[test]
fn a_bug_fix_does_not_move_the_version() {
let before = version_of_sources([&b"enum Op { Ping }\nfn apply(x: u8) -> u8 { x + 1 }\n"[..]]);
let after = version_of_sources([&b"enum Op { Ping }\nfn apply(x: u8) -> u8 { x.saturating_add(1) }\n"[..]]);
assert_eq!(before, after, "a fix to a function sharing the file");
}
#[test]
fn comments_and_formatting_do_not_move_the_version() {
let plain = version_of_sources([&b"enum Op { Ping }"[..]]);
assert_eq!(plain, version_of_sources([&b"/// Docs.\nenum Op { Ping }"[..]]), "a doc comment");
assert_eq!(plain, version_of_sources([&b"enum Op {\n Ping,\n}"[..]].map(|s| s)), "reformatting");
assert_eq!(plain, version_of_sources([&b"enum Op { Ping } // trailing"[..]]), "a trailing comment");
assert_eq!(plain, version_of_sources([&b"use std::fmt;\nenum Op { Ping }"[..]]), "an added import");
}
#[test]
fn impls_are_not_part_of_the_shape() {
let before = version_of_sources([&b"struct P { a: u8 }"[..]]);
let after = version_of_sources([&b"struct P { a: u8 }\nimpl P { fn a(&self) -> u8 { self.a } }"[..]]);
assert_eq!(before, after);
}
#[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);
}
}