Skip to main content

mtp_mount/
sparse_cache.rs

1//! Sparse byte-range cache for on-demand partial file downloads.
2//!
3//! Backs each open file handle with a tempfile sized to the MTP object's
4//! total length. Tracks which byte ranges have been populated. When a FUSE
5//! `read(offset, size)` arrives, [`SparseCache::missing_ranges`] tells the
6//! caller which ranges still need to be fetched from MTP; after the caller
7//! writes the fetched bytes via [`SparseCache::write_at`], [`SparseCache::read_at`]
8//! serves the requested slice from the tempfile.
9//!
10//! Ranges are kept sorted and merged so that adjacent writes coalesce.
11
12use std::io::{self, Read as _, Seek as _, SeekFrom, Write as _};
13use std::ops::Range;
14use std::path::Path;
15
16/// A tempfile-backed cache that tracks populated byte ranges.
17#[derive(Debug)]
18pub struct SparseCache {
19    file: std::fs::File,
20    /// Sorted, non-overlapping, non-adjacent byte ranges that have been written.
21    ranges: Vec<Range<u64>>,
22    total_size: u64,
23}
24
25impl SparseCache {
26    /// Create a new sparse cache for a file of the given total size.
27    ///
28    /// Allocates an unlinked temp file in `spool_dir` (see [`crate::spool`]; the
29    /// cache holds every byte read so far, so it must not land in a tmpfs) and
30    /// sets its length so that sparse reads past the end don't accidentally
31    /// return 0 bytes before the caller has fetched them.
32    pub fn new(total_size: u64, spool_dir: &Path) -> io::Result<Self> {
33        let file = tempfile::tempfile_in(spool_dir)?;
34        // Costs no disk until something is written into it, because the spool
35        // sits on a sparse-file filesystem (APFS, ext4, xfs, btrfs).
36        file.set_len(total_size)?;
37        Ok(Self {
38            file,
39            ranges: Vec::new(),
40            total_size,
41        })
42    }
43
44    /// Returns the byte ranges within `[offset, offset+size)` that are NOT yet populated.
45    ///
46    /// The returned ranges are sorted and clipped to `[0, total_size)`.
47    /// If the entire requested range is already populated, returns an empty `Vec`.
48    pub fn missing_ranges(&self, offset: u64, size: u64) -> Vec<Range<u64>> {
49        let end = offset.saturating_add(size).min(self.total_size);
50        if offset >= end {
51            return Vec::new();
52        }
53
54        let mut missing = Vec::new();
55        let mut cursor = offset;
56
57        for populated in &self.ranges {
58            if populated.end <= cursor {
59                continue;
60            }
61            if populated.start >= end {
62                break;
63            }
64            if populated.start > cursor {
65                missing.push(cursor..populated.start.min(end));
66            }
67            cursor = populated.end;
68            if cursor >= end {
69                break;
70            }
71        }
72
73        if cursor < end {
74            missing.push(cursor..end);
75        }
76
77        missing
78    }
79
80    /// Write `data` at `offset` and mark `[offset, offset+data.len())` as populated.
81    pub fn write_at(&mut self, offset: u64, data: &[u8]) -> io::Result<()> {
82        if data.is_empty() {
83            return Ok(());
84        }
85        self.file.seek(SeekFrom::Start(offset))?;
86        self.file.write_all(data)?;
87
88        let new_range = offset..offset + data.len() as u64;
89        self.insert_range(new_range);
90        Ok(())
91    }
92
93    /// Read `size` bytes at `offset` from the tempfile.
94    ///
95    /// Callers must ensure the requested range is fully populated (check with
96    /// [`missing_ranges`](Self::missing_ranges) and fill gaps via [`write_at`](Self::write_at)).
97    /// Reads past `total_size` return a short slice.
98    pub fn read_at(&mut self, offset: u64, size: u64) -> io::Result<Vec<u8>> {
99        if offset >= self.total_size {
100            return Ok(Vec::new());
101        }
102        let read_len = size.min(self.total_size - offset) as usize;
103        let mut buf = vec![0u8; read_len];
104        self.file.seek(SeekFrom::Start(offset))?;
105        self.file.read_exact(&mut buf)?;
106        Ok(buf)
107    }
108
109    /// Insert a new range into `self.ranges`, merging with any overlapping or
110    /// adjacent existing ranges. Maintains the sorted/merged invariant.
111    fn insert_range(&mut self, new: Range<u64>) {
112        let mut start = new.start;
113        let mut end = new.end;
114
115        // Remove and merge any existing range that overlaps or touches [start, end).
116        self.ranges.retain(|r| {
117            if r.end < start || r.start > end {
118                true
119            } else {
120                start = start.min(r.start);
121                end = end.max(r.end);
122                false
123            }
124        });
125
126        // Find insertion point to keep ranges sorted by start.
127        let pos = self
128            .ranges
129            .binary_search_by(|r| r.start.cmp(&start))
130            .unwrap_or_else(|p| p);
131        self.ranges.insert(pos, start..end);
132    }
133
134    #[cfg(test)]
135    pub fn populated_ranges(&self) -> &[Range<u64>] {
136        &self.ranges
137    }
138}
139
140#[cfg(test)]
141#[allow(clippy::single_range_in_vec_init)] // intentional: asserting populated_ranges matches a one-range slice
142mod tests {
143    use super::*;
144    use std::path::PathBuf;
145
146    /// Unlinked temp files, so the system temp dir is fine for tests; production
147    /// resolves a disk-backed spool dir instead.
148    fn spool() -> PathBuf {
149        std::env::temp_dir()
150    }
151
152    #[test]
153    fn missing_ranges_empty_cache() {
154        let cache = SparseCache::new(1000, &spool()).unwrap();
155        assert_eq!(cache.missing_ranges(0, 100), vec![0..100]);
156        assert_eq!(cache.missing_ranges(500, 100), vec![500..600]);
157    }
158
159    #[test]
160    fn missing_ranges_full_hit() {
161        let mut cache = SparseCache::new(1000, &spool()).unwrap();
162        cache.write_at(0, &[0u8; 500]).unwrap();
163        assert_eq!(cache.missing_ranges(100, 200), Vec::<Range<u64>>::new());
164        assert_eq!(cache.missing_ranges(0, 500), Vec::<Range<u64>>::new());
165    }
166
167    #[test]
168    fn missing_ranges_partial_hit_at_start() {
169        let mut cache = SparseCache::new(1000, &spool()).unwrap();
170        // Populate [0, 100).
171        cache.write_at(0, &[0u8; 100]).unwrap();
172        // Request [0, 200) — first 100 cached, 100..200 missing.
173        assert_eq!(cache.missing_ranges(0, 200), vec![100..200]);
174    }
175
176    #[test]
177    fn missing_ranges_partial_hit_at_end() {
178        let mut cache = SparseCache::new(1000, &spool()).unwrap();
179        // Populate [100, 200).
180        cache.write_at(100, &[0u8; 100]).unwrap();
181        // Request [0, 200) — first 100 missing, last 100 cached.
182        assert_eq!(cache.missing_ranges(0, 200), vec![0..100]);
183    }
184
185    #[test]
186    fn missing_ranges_gap_in_middle() {
187        let mut cache = SparseCache::new(1000, &spool()).unwrap();
188        cache.write_at(0, &[0u8; 100]).unwrap();
189        cache.write_at(200, &[0u8; 100]).unwrap();
190        // Request [0, 300) — gap at [100, 200).
191        assert_eq!(cache.missing_ranges(0, 300), vec![100..200]);
192    }
193
194    #[test]
195    fn missing_ranges_multiple_gaps() {
196        let mut cache = SparseCache::new(1000, &spool()).unwrap();
197        cache.write_at(100, &[0u8; 50]).unwrap();
198        cache.write_at(300, &[0u8; 50]).unwrap();
199        // Request [0, 400) — gaps at [0,100), [150,300), [350,400).
200        assert_eq!(
201            cache.missing_ranges(0, 400),
202            vec![0..100, 150..300, 350..400]
203        );
204    }
205
206    #[test]
207    fn missing_ranges_clips_to_total_size() {
208        let mut cache = SparseCache::new(500, &spool()).unwrap();
209        // Request extends beyond total_size; should clip.
210        assert_eq!(cache.missing_ranges(400, 1000), vec![400..500]);
211        cache.write_at(400, &[0u8; 100]).unwrap();
212        assert_eq!(cache.missing_ranges(400, 1000), Vec::<Range<u64>>::new());
213    }
214
215    #[test]
216    fn missing_ranges_offset_past_end() {
217        let cache = SparseCache::new(100, &spool()).unwrap();
218        assert_eq!(cache.missing_ranges(200, 100), Vec::<Range<u64>>::new());
219    }
220
221    #[test]
222    fn adjacent_ranges_merge() {
223        let mut cache = SparseCache::new(1000, &spool()).unwrap();
224        cache.write_at(0, &[0u8; 100]).unwrap();
225        cache.write_at(100, &[0u8; 100]).unwrap();
226        assert_eq!(cache.populated_ranges(), &[0..200]);
227    }
228
229    #[test]
230    fn overlapping_ranges_merge() {
231        let mut cache = SparseCache::new(1000, &spool()).unwrap();
232        cache.write_at(0, &[0u8; 100]).unwrap();
233        cache.write_at(50, &[0u8; 100]).unwrap();
234        assert_eq!(cache.populated_ranges(), &[0..150]);
235    }
236
237    #[test]
238    fn disjoint_ranges_preserved() {
239        let mut cache = SparseCache::new(1000, &spool()).unwrap();
240        cache.write_at(0, &[0u8; 100]).unwrap();
241        cache.write_at(500, &[0u8; 100]).unwrap();
242        assert_eq!(cache.populated_ranges(), &[0..100, 500..600]);
243    }
244
245    #[test]
246    fn insertion_sorted() {
247        let mut cache = SparseCache::new(1000, &spool()).unwrap();
248        cache.write_at(500, &[0u8; 100]).unwrap();
249        cache.write_at(0, &[0u8; 100]).unwrap();
250        cache.write_at(300, &[0u8; 50]).unwrap();
251        assert_eq!(cache.populated_ranges(), &[0..100, 300..350, 500..600]);
252    }
253
254    #[test]
255    fn write_read_roundtrip() {
256        let mut cache = SparseCache::new(1000, &spool()).unwrap();
257        let data: Vec<u8> = (0..200).map(|i| (i % 256) as u8).collect();
258        cache.write_at(100, &data).unwrap();
259        let read = cache.read_at(100, 200).unwrap();
260        assert_eq!(read, data);
261    }
262
263    #[test]
264    fn read_at_clips_to_total_size() {
265        let mut cache = SparseCache::new(150, &spool()).unwrap();
266        cache.write_at(100, &[0u8; 50]).unwrap();
267        // Request extends past total_size; should return only the available bytes.
268        let read = cache.read_at(100, 200).unwrap();
269        assert_eq!(read.len(), 50);
270    }
271
272    #[test]
273    fn read_at_past_end_returns_empty() {
274        let mut cache = SparseCache::new(100, &spool()).unwrap();
275        let read = cache.read_at(200, 50).unwrap();
276        assert_eq!(read, Vec::<u8>::new());
277    }
278
279    #[test]
280    fn three_way_merge() {
281        // Writing a range that bridges two existing ranges should merge all three.
282        let mut cache = SparseCache::new(1000, &spool()).unwrap();
283        cache.write_at(0, &[0u8; 100]).unwrap();
284        cache.write_at(200, &[0u8; 100]).unwrap();
285        cache.write_at(100, &[0u8; 100]).unwrap();
286        assert_eq!(cache.populated_ranges(), &[0..300]);
287    }
288}