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