relay-knowledge 1.1.10

Graph-database-based knowledge graph project.
Documentation
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct ContentHashCache {
    capacity: usize,
    entries: HashMap<PathBuf, u64>,
    insertion_order: VecDeque<PathBuf>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentHashObservation {
    pub changed: bool,
    pub hash: u64,
}

impl ContentHashCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            capacity,
            entries: HashMap::new(),
            insertion_order: VecDeque::new(),
        }
    }

    pub fn check_and_update(&mut self, path: PathBuf, content: &[u8]) -> ContentHashObservation {
        self.check_hash_and_update(path, content_hash64(content))
    }

    pub fn check_hash_and_update(
        &mut self,
        path: PathBuf,
        new_hash: u64,
    ) -> ContentHashObservation {
        let observation = self.observe_hash(&path, new_hash);
        if observation.changed {
            self.record_hash(path, new_hash);
        }
        observation
    }

    pub fn observe_hash(&self, path: &PathBuf, new_hash: u64) -> ContentHashObservation {
        if self.capacity == 0 {
            return ContentHashObservation {
                changed: true,
                hash: new_hash,
            };
        }
        let changed = match self.entries.get(path) {
            Some(&existing) => existing != new_hash,
            None => true,
        };
        ContentHashObservation {
            changed,
            hash: new_hash,
        }
    }

    pub fn record_hash(&mut self, path: PathBuf, new_hash: u64) {
        if self.capacity == 0 {
            return;
        }
        if self.entries.len() >= self.capacity && !self.entries.contains_key(&path) {
            self.evict_oldest();
        }
        if !self.entries.contains_key(&path) {
            self.insertion_order.push_back(path.clone());
        }
        self.entries.insert(path, new_hash);
    }

    pub fn remove(&mut self, path: &PathBuf) {
        self.entries.remove(path);
        self.insertion_order.retain(|entry| entry != path);
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn capacity(&self) -> usize {
        self.capacity
    }

    pub fn clear(&mut self) {
        self.entries.clear();
        self.insertion_order.clear();
    }

    fn evict_oldest(&mut self) {
        while let Some(oldest_key) = self.insertion_order.pop_front() {
            if self.entries.remove(&oldest_key).is_some() {
                break;
            }
        }
    }
}

pub(super) fn content_hash64(bytes: &[u8]) -> u64 {
    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
    const FNV_PRIME: u64 = 0x100000001b3;

    let mut hash = FNV_OFFSET_BASIS;
    for byte in bytes {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(FNV_PRIME);
    }
    hash
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_new_file_as_changed() {
        let mut cache = ContentHashCache::new(100);
        assert!(
            cache
                .check_and_update(PathBuf::from("a.rs"), b"hello")
                .changed
        );
    }

    #[test]
    fn detects_unchanged_file_as_not_changed() {
        let mut cache = ContentHashCache::new(100);
        cache.check_and_update(PathBuf::from("a.rs"), b"hello");
        assert!(
            !cache
                .check_and_update(PathBuf::from("a.rs"), b"hello")
                .changed
        );
    }

    #[test]
    fn detects_modified_file_as_changed() {
        let mut cache = ContentHashCache::new(100);
        cache.check_and_update(PathBuf::from("a.rs"), b"hello");
        assert!(
            cache
                .check_and_update(PathBuf::from("a.rs"), b"world")
                .changed
        );
    }

    #[test]
    fn returns_stable_content_hash_for_same_bytes() {
        let mut cache = ContentHashCache::new(100);
        let first = cache.check_and_update(PathBuf::from("a.rs"), b"hello");
        let second = cache.check_and_update(PathBuf::from("a.rs"), b"hello");
        assert_eq!(first.hash, second.hash);
        assert!(!second.changed);
    }

    #[test]
    fn evicts_when_at_capacity() {
        let mut cache = ContentHashCache::new(2);
        cache.check_and_update(PathBuf::from("a.rs"), b"a");
        cache.check_and_update(PathBuf::from("b.rs"), b"b");
        cache.check_and_update(PathBuf::from("c.rs"), b"c");
        assert_eq!(cache.len(), 2);
    }

    #[test]
    fn remove_clears_entry() {
        let mut cache = ContentHashCache::new(100);
        cache.check_and_update(PathBuf::from("a.rs"), b"a");
        let key = PathBuf::from("a.rs");
        cache.remove(&key);
        assert!(cache.is_empty());
    }

    #[test]
    fn clear_empties_all_entries() {
        let mut cache = ContentHashCache::new(100);
        cache.check_and_update(PathBuf::from("a.rs"), b"a");
        cache.check_and_update(PathBuf::from("b.rs"), b"b");
        cache.clear();
        assert!(cache.is_empty());
    }

    #[test]
    fn update_at_capacity_replaces_existing_without_eviction() {
        let mut cache = ContentHashCache::new(2);
        cache.check_and_update(PathBuf::from("a.rs"), b"a");
        cache.check_and_update(PathBuf::from("b.rs"), b"b");
        assert!(!cache.check_and_update(PathBuf::from("a.rs"), b"a").changed);
        assert_eq!(cache.len(), 2);
    }

    #[test]
    fn evicts_in_insertion_order() {
        let mut cache = ContentHashCache::new(2);
        cache.check_and_update(PathBuf::from("a.rs"), b"a");
        cache.check_and_update(PathBuf::from("b.rs"), b"b");
        cache.check_and_update(PathBuf::from("c.rs"), b"c");
        assert_eq!(cache.len(), 2);
        assert!(cache.check_and_update(PathBuf::from("a.rs"), b"a").changed);
        assert_eq!(cache.len(), 2);
    }

    #[test]
    fn zero_capacity_tracks_no_entries() {
        let mut cache = ContentHashCache::new(0);
        let observation = cache.check_and_update(PathBuf::from("a.rs"), b"a");
        assert!(observation.changed);
        assert_ne!(observation.hash, 0);
        assert!(cache.is_empty());
    }
}