Skip to main content

relay_knowledge/watcher/
hash_cache.rs

1use std::collections::{HashMap, VecDeque};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone)]
5pub struct ContentHashCache {
6    capacity: usize,
7    entries: HashMap<PathBuf, u64>,
8    insertion_order: VecDeque<PathBuf>,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ContentHashObservation {
13    pub changed: bool,
14    pub hash: u64,
15}
16
17impl ContentHashCache {
18    pub fn new(capacity: usize) -> Self {
19        Self {
20            capacity,
21            entries: HashMap::new(),
22            insertion_order: VecDeque::new(),
23        }
24    }
25
26    pub fn check_and_update(&mut self, path: PathBuf, content: &[u8]) -> ContentHashObservation {
27        self.check_hash_and_update(path, content_hash64(content))
28    }
29
30    pub fn check_hash_and_update(
31        &mut self,
32        path: PathBuf,
33        new_hash: u64,
34    ) -> ContentHashObservation {
35        let observation = self.observe_hash(&path, new_hash);
36        if observation.changed {
37            self.record_hash(path, new_hash);
38        }
39        observation
40    }
41
42    pub fn observe_hash(&self, path: &PathBuf, new_hash: u64) -> ContentHashObservation {
43        if self.capacity == 0 {
44            return ContentHashObservation {
45                changed: true,
46                hash: new_hash,
47            };
48        }
49        let changed = match self.entries.get(path) {
50            Some(&existing) => existing != new_hash,
51            None => true,
52        };
53        ContentHashObservation {
54            changed,
55            hash: new_hash,
56        }
57    }
58
59    pub fn record_hash(&mut self, path: PathBuf, new_hash: u64) {
60        if self.capacity == 0 {
61            return;
62        }
63        if self.entries.len() >= self.capacity && !self.entries.contains_key(&path) {
64            self.evict_oldest();
65        }
66        if !self.entries.contains_key(&path) {
67            self.insertion_order.push_back(path.clone());
68        }
69        self.entries.insert(path, new_hash);
70    }
71
72    pub fn remove(&mut self, path: &PathBuf) {
73        self.entries.remove(path);
74        self.insertion_order.retain(|entry| entry != path);
75    }
76
77    pub fn len(&self) -> usize {
78        self.entries.len()
79    }
80
81    pub fn is_empty(&self) -> bool {
82        self.entries.is_empty()
83    }
84
85    pub fn capacity(&self) -> usize {
86        self.capacity
87    }
88
89    pub fn clear(&mut self) {
90        self.entries.clear();
91        self.insertion_order.clear();
92    }
93
94    fn evict_oldest(&mut self) {
95        while let Some(oldest_key) = self.insertion_order.pop_front() {
96            if self.entries.remove(&oldest_key).is_some() {
97                break;
98            }
99        }
100    }
101}
102
103pub(super) fn content_hash64(bytes: &[u8]) -> u64 {
104    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
105    const FNV_PRIME: u64 = 0x100000001b3;
106
107    let mut hash = FNV_OFFSET_BASIS;
108    for byte in bytes {
109        hash ^= u64::from(*byte);
110        hash = hash.wrapping_mul(FNV_PRIME);
111    }
112    hash
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn detects_new_file_as_changed() {
121        let mut cache = ContentHashCache::new(100);
122        assert!(
123            cache
124                .check_and_update(PathBuf::from("a.rs"), b"hello")
125                .changed
126        );
127    }
128
129    #[test]
130    fn detects_unchanged_file_as_not_changed() {
131        let mut cache = ContentHashCache::new(100);
132        cache.check_and_update(PathBuf::from("a.rs"), b"hello");
133        assert!(
134            !cache
135                .check_and_update(PathBuf::from("a.rs"), b"hello")
136                .changed
137        );
138    }
139
140    #[test]
141    fn detects_modified_file_as_changed() {
142        let mut cache = ContentHashCache::new(100);
143        cache.check_and_update(PathBuf::from("a.rs"), b"hello");
144        assert!(
145            cache
146                .check_and_update(PathBuf::from("a.rs"), b"world")
147                .changed
148        );
149    }
150
151    #[test]
152    fn returns_stable_content_hash_for_same_bytes() {
153        let mut cache = ContentHashCache::new(100);
154        let first = cache.check_and_update(PathBuf::from("a.rs"), b"hello");
155        let second = cache.check_and_update(PathBuf::from("a.rs"), b"hello");
156        assert_eq!(first.hash, second.hash);
157        assert!(!second.changed);
158    }
159
160    #[test]
161    fn evicts_when_at_capacity() {
162        let mut cache = ContentHashCache::new(2);
163        cache.check_and_update(PathBuf::from("a.rs"), b"a");
164        cache.check_and_update(PathBuf::from("b.rs"), b"b");
165        cache.check_and_update(PathBuf::from("c.rs"), b"c");
166        assert_eq!(cache.len(), 2);
167    }
168
169    #[test]
170    fn remove_clears_entry() {
171        let mut cache = ContentHashCache::new(100);
172        cache.check_and_update(PathBuf::from("a.rs"), b"a");
173        let key = PathBuf::from("a.rs");
174        cache.remove(&key);
175        assert!(cache.is_empty());
176    }
177
178    #[test]
179    fn clear_empties_all_entries() {
180        let mut cache = ContentHashCache::new(100);
181        cache.check_and_update(PathBuf::from("a.rs"), b"a");
182        cache.check_and_update(PathBuf::from("b.rs"), b"b");
183        cache.clear();
184        assert!(cache.is_empty());
185    }
186
187    #[test]
188    fn update_at_capacity_replaces_existing_without_eviction() {
189        let mut cache = ContentHashCache::new(2);
190        cache.check_and_update(PathBuf::from("a.rs"), b"a");
191        cache.check_and_update(PathBuf::from("b.rs"), b"b");
192        assert!(!cache.check_and_update(PathBuf::from("a.rs"), b"a").changed);
193        assert_eq!(cache.len(), 2);
194    }
195
196    #[test]
197    fn evicts_in_insertion_order() {
198        let mut cache = ContentHashCache::new(2);
199        cache.check_and_update(PathBuf::from("a.rs"), b"a");
200        cache.check_and_update(PathBuf::from("b.rs"), b"b");
201        cache.check_and_update(PathBuf::from("c.rs"), b"c");
202        assert_eq!(cache.len(), 2);
203        assert!(cache.check_and_update(PathBuf::from("a.rs"), b"a").changed);
204        assert_eq!(cache.len(), 2);
205    }
206
207    #[test]
208    fn zero_capacity_tracks_no_entries() {
209        let mut cache = ContentHashCache::new(0);
210        let observation = cache.check_and_update(PathBuf::from("a.rs"), b"a");
211        assert!(observation.changed);
212        assert_ne!(observation.hash, 0);
213        assert!(cache.is_empty());
214    }
215}