ztheme 1.2.0

Fast asynchronous Zsh prompt
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());
    }
}