Skip to main content

tiff_reader/
cache.rs

1//! LRU cache for decompressed strips and tiles.
2
3use std::num::NonZeroUsize;
4use std::sync::Arc;
5
6use lru::LruCache;
7use parking_lot::Mutex;
8
9/// Cache key for a decoded strip or tile.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct BlockKey {
12    /// File offset of the owning IFD.
13    ///
14    /// The IFD offset uniquely identifies an image no matter how it was
15    /// reached (top-level chain position or SubIFD pointer), unlike the chain
16    /// index, which can collide with the byte offset of an IFD parsed via
17    /// `TiffFile::read_ifd_at_offset`.
18    pub ifd_offset: u64,
19    pub kind: BlockKind,
20    pub block_index: usize,
21}
22
23/// Whether the cached block came from a strip- or tile-backed image.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum BlockKind {
26    Strip,
27    Tile,
28}
29
30/// Thread-safe LRU cache for decoded block payloads.
31pub struct BlockCache {
32    inner: Mutex<BlockCacheState>,
33    max_bytes: usize,
34    enabled: bool,
35}
36
37struct BlockCacheState {
38    cache: LruCache<BlockKey, Arc<Vec<u8>>>,
39    current_bytes: usize,
40}
41
42impl BlockCache {
43    /// Create a new cache with byte and slot limits.
44    pub fn new(max_bytes: usize, max_slots: usize) -> Self {
45        let slots = NonZeroUsize::new(max_slots.max(1)).unwrap();
46        Self {
47            inner: Mutex::new(BlockCacheState {
48                cache: LruCache::new(slots),
49                current_bytes: 0,
50            }),
51            max_bytes,
52            enabled: max_bytes > 0 && max_slots > 0,
53        }
54    }
55
56    /// Return a cached block and promote it in LRU order.
57    pub fn get(&self, key: &BlockKey) -> Option<Arc<Vec<u8>>> {
58        if !self.enabled {
59            return None;
60        }
61        let mut state = self.inner.lock();
62        state.cache.get(key).cloned()
63    }
64
65    /// Insert a decoded block into the cache.
66    pub fn insert(&self, key: BlockKey, data: Vec<u8>) -> Arc<Vec<u8>> {
67        let data_len = data.len();
68        let value = Arc::new(data);
69
70        let mut state = self.inner.lock();
71        if let Some(previous) = state.cache.pop(&key) {
72            state.current_bytes = state.current_bytes.saturating_sub(previous.len());
73        }
74
75        if !self.enabled || data_len > self.max_bytes {
76            return value;
77        }
78
79        while state.current_bytes > self.max_bytes - data_len && !state.cache.is_empty() {
80            if let Some((_, evicted)) = state.cache.pop_lru() {
81                state.current_bytes = state.current_bytes.saturating_sub(evicted.len());
82            }
83        }
84
85        state.current_bytes += data_len;
86        if let Some((_, evicted)) = state.cache.push(key, value.clone()) {
87            state.current_bytes = state.current_bytes.saturating_sub(evicted.len());
88        }
89
90        value
91    }
92}
93
94impl Default for BlockCache {
95    fn default() -> Self {
96        Self::new(64 * 1024 * 1024, 257)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::{BlockCache, BlockKey, BlockKind};
103
104    #[test]
105    fn caches_and_promotes_entries() {
106        let cache = BlockCache::new(12, 8);
107        let a = BlockKey {
108            ifd_offset: 0,
109            kind: BlockKind::Strip,
110            block_index: 0,
111        };
112        let b = BlockKey {
113            ifd_offset: 0,
114            kind: BlockKind::Strip,
115            block_index: 1,
116        };
117        let c = BlockKey {
118            ifd_offset: 0,
119            kind: BlockKind::Strip,
120            block_index: 2,
121        };
122
123        cache.insert(a, vec![0; 4]);
124        cache.insert(b, vec![0; 4]);
125        cache.insert(c, vec![0; 4]);
126
127        let promoted = BlockKey {
128            ifd_offset: 0,
129            kind: BlockKind::Strip,
130            block_index: 0,
131        };
132        assert!(cache.get(&promoted).is_some());
133
134        let d = BlockKey {
135            ifd_offset: 0,
136            kind: BlockKind::Strip,
137            block_index: 3,
138        };
139        cache.insert(d, vec![0; 4]);
140
141        let evicted = BlockKey {
142            ifd_offset: 0,
143            kind: BlockKind::Strip,
144            block_index: 1,
145        };
146        assert!(cache.get(&promoted).is_some());
147        assert!(cache.get(&evicted).is_none());
148    }
149
150    #[test]
151    fn disabled_cache_bypasses_storage() {
152        let cache = BlockCache::new(0, 4);
153        let key = BlockKey {
154            ifd_offset: 0,
155            kind: BlockKind::Tile,
156            block_index: 0,
157        };
158        cache.insert(key, vec![1, 2, 3]);
159        assert!(cache.get(&key).is_none());
160    }
161
162    #[test]
163    fn zero_slots_disable_cache_storage() {
164        let cache = BlockCache::new(1024, 0);
165        let key = BlockKey {
166            ifd_offset: 0,
167            kind: BlockKind::Tile,
168            block_index: 0,
169        };
170        cache.insert(key, vec![1, 2, 3]);
171        assert!(cache.get(&key).is_none());
172        assert_eq!(cache.inner.lock().current_bytes, 0);
173    }
174
175    #[test]
176    fn slot_eviction_updates_byte_accounting() {
177        let cache = BlockCache::new(100, 2);
178        for block_index in 0..3 {
179            cache.insert(
180                BlockKey {
181                    ifd_offset: 0,
182                    kind: BlockKind::Strip,
183                    block_index,
184                },
185                vec![0; 4],
186            );
187        }
188
189        assert_eq!(cache.inner.lock().current_bytes, 8);
190    }
191
192    #[test]
193    fn replacing_mru_entry_preserves_other_cached_blocks() {
194        let cache = BlockCache::new(10, 8);
195        let a = BlockKey {
196            ifd_offset: 0,
197            kind: BlockKind::Tile,
198            block_index: 0,
199        };
200        let b = BlockKey {
201            ifd_offset: 0,
202            kind: BlockKind::Tile,
203            block_index: 1,
204        };
205
206        cache.insert(a, vec![0; 8]);
207        cache.insert(b, vec![0; 2]);
208        assert!(cache.get(&a).is_some());
209
210        cache.insert(a, vec![0; 7]);
211
212        assert!(cache.get(&a).is_some());
213        assert!(cache.get(&b).is_some());
214        assert_eq!(cache.inner.lock().current_bytes, 9);
215    }
216
217    #[test]
218    fn oversized_replacement_removes_stale_entry() {
219        let cache = BlockCache::new(8, 8);
220        let key = BlockKey {
221            ifd_offset: 0,
222            kind: BlockKind::Tile,
223            block_index: 0,
224        };
225
226        cache.insert(key, vec![0; 4]);
227        cache.insert(key, vec![0; 9]);
228
229        assert!(cache.get(&key).is_none());
230        assert_eq!(cache.inner.lock().current_bytes, 0);
231    }
232}