use std::ffi::OsStr;
use std::fs::Metadata;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::MetadataExt as _;
use std::path::Path;
use std::time::UNIX_EPOCH;
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
#[derive(Clone, Debug)]
pub(crate) struct HashBuilder {
state: u64,
}
impl HashBuilder {
pub(crate) fn new(namespace: &[u8]) -> Self {
let mut hash = Self { state: FNV_OFFSET };
hash.add_bytes(b"domain", namespace);
hash
}
pub(crate) fn add_bytes(&mut self, label: &[u8], value: &[u8]) {
self.add_raw(&length(label).to_be_bytes());
self.add_raw(label);
self.add_raw(&length(value).to_be_bytes());
self.add_raw(value);
}
pub(crate) fn add_os(&mut self, label: &[u8], value: &OsStr) {
self.add_bytes(label, value.as_bytes());
}
pub(crate) fn add_optional_os(&mut self, label: &[u8], value: Option<&OsStr>) {
match value {
Some(value) => {
self.add_bytes(b"present", b"1");
self.add_os(label, value);
}
None => self.add_bytes(b"present", b"0"),
}
}
pub(crate) fn add_path(&mut self, label: &[u8], value: &Path) {
self.add_os(label, value.as_os_str());
}
pub(crate) fn add_u64(&mut self, label: &[u8], value: u64) {
self.add_bytes(label, &value.to_be_bytes());
}
pub(crate) fn add_metadata(&mut self, label: &[u8], metadata: Option<&Metadata>) {
let Some(metadata) = metadata else {
self.add_bytes(label, b"missing");
return;
};
self.add_bytes(label, b"present");
self.add_u64(b"metadata-length", metadata.len());
self.add_u64(b"metadata-device", metadata.dev());
self.add_u64(b"metadata-inode", metadata.ino());
self.add_u64(b"metadata-mode", u64::from(metadata.mode()));
self.add_u64(
b"metadata-change-seconds",
u64::try_from(metadata.ctime()).unwrap_or(0),
);
self.add_u64(
b"metadata-change-nanos",
u64::try_from(metadata.ctime_nsec()).unwrap_or(0),
);
match metadata
.modified()
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
{
Some(modified) => {
self.add_u64(b"metadata-modified-seconds", modified.as_secs());
self.add_u64(
b"metadata-modified-nanos",
u64::from(modified.subsec_nanos()),
);
}
None => self.add_bytes(b"metadata-modified", b"unknown"),
}
}
pub(crate) const fn finish(self) -> u64 {
self.state
}
fn add_raw(&mut self, bytes: &[u8]) {
for byte in bytes {
self.state ^= u64::from(*byte);
self.state = self.state.wrapping_mul(FNV_PRIME);
}
}
}
fn length(value: &[u8]) -> u64 {
u64::try_from(value.len()).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use super::HashBuilder;
#[test]
fn outputs_are_stable() {
assert_eq!(HashBuilder::new(b"one").finish(), 0x9020_a6e7_aadb_4182);
let mut hash = HashBuilder::new(b"one");
hash.add_bytes(b"a", b"bc");
assert_eq!(hash.finish(), 0x1585_6e18_310c_25b9);
}
#[test]
fn namespaces_labels_boundaries_and_optional_values_are_distinct() {
let mut first = HashBuilder::new(b"one");
first.add_bytes(b"a", b"bc");
let mut second = HashBuilder::new(b"one");
second.add_bytes(b"ab", b"c");
let mut other_namespace = HashBuilder::new(b"two");
other_namespace.add_bytes(b"a", b"bc");
let mut missing = HashBuilder::new(b"optional");
missing.add_optional_os(b"value", None);
let mut present = HashBuilder::new(b"optional");
present.add_optional_os(b"value", Some(OsStr::new("")));
let first = first.finish();
assert_ne!(first, second.finish());
assert_ne!(first, other_namespace.finish());
assert_ne!(missing.finish(), present.finish());
}
}