Skip to main content

grit_lib/
pack_map.rs

1//! Memory-mapped pack (and MIDX) file bytes with an owned-buffer fallback.
2//!
3//! This module is the only place that uses `unsafe` for pack backing storage.
4
5use crate::error::{Error, Result};
6use crate::objects::HashAlgo;
7use memmap2::{Mmap, MmapOptions};
8use std::collections::VecDeque;
9use std::fs::File;
10use std::io::{Read, Seek, SeekFrom};
11use std::ops::Deref;
12use std::path::Path;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::{Arc, Mutex, Weak};
15
16const PACK_SIGNATURE: &[u8; 4] = b"PACK";
17const HEADER_LEN: usize = 12;
18
19/// Files at or below this size are read into an owned `Vec` (tiny packs, tests,
20/// platforms or filesystems where mapping fails).
21const OWNED_READ_THRESHOLD: u64 = 8192;
22const RANDOM_WINDOW_BYTES: usize = 16 * 1024;
23const RANDOM_WINDOW_COUNT: usize = 8;
24
25#[derive(Default)]
26struct ReadWindow {
27    start: Option<usize>,
28    bytes: Vec<u8>,
29}
30
31#[derive(Default)]
32struct ReadWindows {
33    entries: [ReadWindow; RANDOM_WINDOW_COUNT],
34    next: usize,
35}
36
37struct PositionedPack {
38    file: File,
39    windows: Mutex<ReadWindows>,
40}
41
42/// Header plus trailing pack checksum — enough to detect on-disk replacement when
43/// mtime/size are unchanged (same-second repack) without re-reading the whole file.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub(crate) struct PackFingerprint {
46    /// `PACK`, version, and object count (`12` bytes).
47    pub header: [u8; HEADER_LEN],
48    /// Trailer width in bytes (20 for SHA-1, 32 for SHA-256).
49    pub hash_bytes: u8,
50    /// Pack trailer hash (only the first `hash_bytes` entries are meaningful).
51    pub trailer: [u8; 32],
52}
53
54/// Repository-scoped ownership of descriptors used for sparse pack reads.
55///
56/// Mappings outlive descriptor eviction and continue to serve reads from the same
57/// inode. Borrowers hold an Arc only during positioned I/O; mapped packs hold Weak
58/// references so an unbounded catalog cannot exhaust the process descriptor limit.
59#[derive(Default)]
60pub(crate) struct SparseReadFiles {
61    files: Mutex<VecDeque<Arc<File>>>,
62}
63
64impl SparseReadFiles {
65    const CAPACITY: usize = 16;
66
67    fn retain(&self, file: File) -> Weak<File> {
68        let file = Arc::new(file);
69        let weak = Arc::downgrade(&file);
70        let mut files = self.files.lock().unwrap_or_else(|e| e.into_inner());
71        if files.len() == Self::CAPACITY {
72            files.pop_front();
73        }
74        files.push_back(file);
75        weak
76    }
77
78    /// Release descriptors when packs are invalidated or descriptor pressure prevents opening a pack.
79    pub(crate) fn clear(&self) {
80        self.files.lock().unwrap_or_else(|e| e.into_inner()).clear();
81    }
82}
83
84enum Storage {
85    Mapped { map: Mmap, file: Weak<File> },
86    Owned(Vec<u8>),
87}
88
89/// Immutable bytes of a `.pack`, pack index, commit-graph, MIDX, or `packed-refs` file.
90///
91/// # Safety (memory-mapped storage)
92///
93/// Pack, index, MIDX, and `packed-refs` files are immutable at a stable path: grit
94/// and Git publish updates by writing a temporary file and renaming it into place,
95/// so active readers never map a file that is being truncated in place. If a pack
96/// were truncated while mapped, reads could fault or return stale bytes — the same
97/// risk Git accepts for memory-mapped packfiles. `open` uses a single file
98/// descriptor and `fstat`s that handle before mapping so length and inode stay paired.
99pub struct PackData {
100    storage: Storage,
101    /// A descriptor paired with the mapping. Bounded catalogs limit retained readers;
102    /// active readers keep this exact file alive even after eviction or path replacement.
103    random_file: Option<Box<PositionedPack>>,
104}
105
106impl std::fmt::Debug for PackData {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("PackData")
109            .field("len", &self.len())
110            .field(
111                "backing",
112                &match &self.storage {
113                    Storage::Mapped { .. } => "mapped",
114                    Storage::Owned(_) => "owned",
115                },
116            )
117            .finish()
118    }
119}
120
121impl Deref for PackData {
122    type Target = [u8];
123
124    fn deref(&self) -> &[u8] {
125        match &self.storage {
126            Storage::Mapped { map, .. } => map.deref(),
127            Storage::Owned(v) => v.as_slice(),
128        }
129    }
130}
131
132impl PackData {
133    /// Map `path` read-only, or read it into an owned buffer when small or mapping fails.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`Error::Io`] when the file cannot be opened or read, and
138    /// [`Error::CorruptObject`] when the file is shorter than a pack header.
139    pub(crate) fn open(path: &Path) -> Result<Arc<Self>> {
140        Self::open_with_min_len(path, HEADER_LEN as u64)
141    }
142
143    /// Map or read `path`, requiring at least `min_len` bytes (pack header vs index fanout).
144    pub(crate) fn open_with_min_len(path: &Path, min_len: u64) -> Result<Arc<Self>> {
145        let file = File::open(path).map_err(Error::Io)?;
146        Self::open_from_file(file, min_len, None, false)
147    }
148
149    /// Retain the pack's descriptor in a bounded pool for positioned reads.
150    /// Indexes, graphs and other mapped metadata keep the descriptor-free `open` path.
151    pub(crate) fn open_for_sparse_reads(path: &Path, files: &SparseReadFiles) -> Result<Arc<Self>> {
152        let file = match File::open(path) {
153            Ok(file) => file,
154            #[cfg(unix)]
155            Err(err) if matches!(err.raw_os_error(), Some(libc::EMFILE) | Some(libc::ENFILE)) => {
156                // Mappings remain usable after eviction; make room before retrying a
157                // new open. Never reopen a cached mapping through its current path.
158                files.clear();
159                File::open(path)?
160            }
161            Err(err) => return Err(err.into()),
162        };
163        Self::open_from_file(file, HEADER_LEN as u64, Some(files), false)
164    }
165
166    /// Keep a descriptor for positioned entry reads instead of faulting scattered pack pages.
167    pub(crate) fn open_random_access(path: &Path) -> Result<Arc<Self>> {
168        Self::open_from_file(File::open(path)?, HEADER_LEN as u64, None, true)
169    }
170
171    fn open_from_file(
172        file: File,
173        min_len: u64,
174        sparse_reads: Option<&SparseReadFiles>,
175        random: bool,
176    ) -> Result<Arc<Self>> {
177        let len = file.metadata().map_err(Error::Io)?.len();
178        if len < min_len {
179            return Err(Error::CorruptObject(format!(
180                "file is too small ({len} bytes, need at least {min_len})"
181            )));
182        }
183        let len_usize = usize::try_from(len).map_err(|_| {
184            Error::CorruptObject(format!(
185                "pack file length {len} does not fit in address space"
186            ))
187        })?;
188        if len <= OWNED_READ_THRESHOLD {
189            let mut data = Vec::with_capacity(len_usize);
190            let mut reader = file;
191            reader.read_to_end(&mut data).map_err(Error::Io)?;
192            return Ok(Arc::new(Self {
193                storage: Storage::Owned(data),
194                random_file: None,
195            }));
196        }
197        match map_read_only(&file, len_usize) {
198            Ok(mmap) => {
199                let (file, random_file) = if random {
200                    (
201                        Weak::new(),
202                        Some(Box::new(PositionedPack {
203                            file,
204                            windows: Mutex::new(ReadWindows::default()),
205                        })),
206                    )
207                } else {
208                    (
209                        sparse_reads
210                            .map(|files| files.retain(file))
211                            .unwrap_or_default(),
212                        None,
213                    )
214                };
215                Ok(Arc::new(Self {
216                    storage: Storage::Mapped { map: mmap, file },
217                    random_file,
218                }))
219            }
220            Err(_) => {
221                let mut data = Vec::with_capacity(len_usize);
222                let mut reader = file;
223                reader.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
224                reader.read_to_end(&mut data).map_err(Error::Io)?;
225                Ok(Arc::new(Self {
226                    storage: Storage::Owned(data),
227                    random_file: None,
228                }))
229            }
230        }
231    }
232
233    /// Wrap an owned buffer (tests, small index files, and stale-byte injection).
234    #[must_use]
235    pub(crate) fn from_owned(data: Vec<u8>) -> Arc<Self> {
236        Arc::new(Self {
237            storage: Storage::Owned(data),
238            random_file: None,
239        })
240    }
241
242    /// Copy a bounded range without faulting scattered mmap windows into the process.
243    ///
244    /// Reads the same descriptor that backs the mapping, so replacing the path cannot
245    /// mix pack generations. Returns `None` after descriptor eviction, on I/O failure,
246    /// or at an invalid offset; callers retain their normal mapped parsing and
247    /// corruption/replacement recovery paths. Evicted descriptors are never reopened
248    /// through a path that might now name a different inode.
249    pub(crate) fn read_sparse(&self, offset: u64, out: &mut [u8]) -> Option<usize> {
250        let start = usize::try_from(offset).ok()?;
251        let len = out.len().min(self.len().checked_sub(start)?);
252        let out = &mut out[..len];
253        match &self.storage {
254            Storage::Mapped { file, .. } => {
255                let file = file.upgrade()?;
256                #[cfg(unix)]
257                {
258                    use std::os::unix::fs::FileExt;
259                    file.read_exact_at(out, offset).ok()?;
260                }
261                #[cfg(windows)]
262                {
263                    use std::os::windows::fs::FileExt;
264                    let mut filled = 0;
265                    while filled < len {
266                        let n = file
267                            .seek_read(&mut out[filled..], offset + filled as u64)
268                            .ok()?;
269                        if n == 0 {
270                            return None;
271                        }
272                        filled += n;
273                    }
274                }
275                #[cfg(not(any(unix, windows)))]
276                {
277                    let _ = file;
278                    out.copy_from_slice(&self[start..start + len]);
279                }
280            }
281            Storage::Owned(bytes) => out.copy_from_slice(&bytes[start..start + len]),
282        }
283        Some(len)
284    }
285
286    /// Read exactly one indexed entry through the retained file descriptor when enabled.
287    /// Nearby small entries share eight 16 KiB compressed windows; larger entries read
288    /// directly into the caller's reusable `buf`. The mapping stays untouched, and bounds
289    /// are checked before IO.
290    pub(crate) fn read_range<'a>(
291        &'a self,
292        start: usize,
293        end: usize,
294        buf: &'a mut Vec<u8>,
295    ) -> Result<&'a [u8]> {
296        if start > end || end > self.len() {
297            return Err(Error::CorruptObject("pack entry out of range".to_owned()));
298        }
299        if let Some(reader) = &self.random_file {
300            buf.clear();
301            let len = end - start;
302            if len == 0 {
303                return Ok(buf);
304            }
305            if len > RANDOM_WINDOW_BYTES {
306                buf.resize(len, 0);
307                crate::pack_delta_walk::read_exact_at(&reader.file, buf, start as u64)?;
308                return Ok(buf);
309            }
310            // Separate small windows retain nearby commit/tree entries across interleaved
311            // reads without keeping pack mappings resident. Large entries bypass the cache.
312            let mut windows = reader.windows.lock().unwrap_or_else(|e| e.into_inner());
313            if let Some(bytes) = windows.entries.iter().find_map(|window| {
314                let offset = start.checked_sub(window.start?)?;
315                window.bytes.get(offset..offset + len)
316            }) {
317                buf.extend_from_slice(bytes);
318                return Ok(buf);
319            }
320            let slot = windows.next;
321            windows.next = (slot + 1) % RANDOM_WINDOW_COUNT;
322            let window = &mut windows.entries[slot];
323            window.start = None;
324            window
325                .bytes
326                .resize(RANDOM_WINDOW_BYTES.min(self.len() - start), 0);
327            crate::pack_delta_walk::read_exact_at(&reader.file, &mut window.bytes, start as u64)?;
328            window.start = Some(start);
329            buf.extend_from_slice(&window.bytes[..len]);
330            return Ok(buf);
331        }
332        Ok(&self[start..end])
333    }
334
335    pub(crate) fn uses_positioned_reads(&self) -> bool {
336        self.random_file.is_some()
337    }
338
339    /// Drop this mapping's pages from the process's resident set.
340    ///
341    /// The bytes stay valid: later reads fault the pages back in from the file (usually
342    /// straight from the page cache). Owned buffers and non-Unix platforms are left as is.
343    pub(crate) fn release_resident(&self) {
344        #[cfg(unix)]
345        if let Storage::Mapped { map, .. } = &self.storage {
346            // SAFETY: the mapping is a read-only shared file mapping and the file is never
347            // modified while mapped (see the type docs), so dropping its pages only makes
348            // the next read reload the same bytes from the file; no borrow observes a change.
349            let _ = unsafe {
350                map.unchecked_advise_range(memmap2::UncheckedAdvice::DontNeed, 0, map.len())
351            };
352        }
353    }
354
355    /// True when backing storage is a memory map (not an owned buffer).
356    #[cfg(test)]
357    pub(crate) fn is_mapped_backing(&self) -> bool {
358        matches!(self.storage, Storage::Mapped { .. })
359    }
360
361    /// Fingerprint of the bytes currently held in memory.
362    ///
363    /// `hash_bytes` is the repository object-id / pack-trailer width (from the `.idx` or
364    /// repo format), not the pack header version field.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`Error::CorruptObject`] when the buffer is not a well-formed pack prefix/trailer.
369    pub(crate) fn fingerprint(&self, hash_bytes: usize) -> Result<PackFingerprint> {
370        fingerprint_from_slice(self, hash_bytes)
371    }
372}
373
374/// Caps how much of a memory-mapped pack a sequential pass over it keeps resident.
375///
376/// Readers [`charge`](Self::charge) the bytes they read; once the charges reach the
377/// limit, the whole mapping's pages are dropped and counting starts over. Charging is
378/// lock-free and shared by all worker threads. Meant for passes that move forward
379/// through the pack: scattered small reads map whole fault-around windows each, so
380/// they are better served by positioned file reads than by a budget.
381pub(crate) struct ResidentBudget<'a> {
382    pack: Option<&'a PackData>,
383    limit: usize,
384    charged: AtomicUsize,
385}
386
387impl<'a> ResidentBudget<'a> {
388    /// A budget that never releases (the caller owns the bytes, or they are not mapped).
389    #[must_use]
390    pub(crate) fn unbounded() -> Self {
391        Self {
392            pack: None,
393            limit: usize::MAX,
394            charged: AtomicUsize::new(0),
395        }
396    }
397
398    /// Keep at most about `limit` bytes of `pack` resident.
399    #[must_use]
400    pub(crate) fn new(pack: &'a PackData, limit: usize) -> Self {
401        Self {
402            pack: Some(pack),
403            limit,
404            charged: AtomicUsize::new(0),
405        }
406    }
407
408    /// Record a read of `bytes` from the pack, releasing its pages when the limit is reached.
409    pub(crate) fn charge(&self, bytes: usize) {
410        let Some(pack) = self.pack else {
411            return;
412        };
413        let before = self.charged.fetch_add(bytes, Ordering::Relaxed);
414        if before.saturating_add(bytes) < self.limit {
415            return;
416        }
417        // Of several threads crossing the limit together, only the one that resets wins.
418        if self.charged.swap(0, Ordering::Relaxed) >= self.limit {
419            pack.release_resident();
420        }
421    }
422
423    /// Checksum `bytes` with `algo`, charging as it goes so a full pass stays within budget.
424    pub(crate) fn checksum(&self, algo: HashAlgo, bytes: &[u8]) -> Vec<u8> {
425        const CHUNK: usize = 1024 * 1024;
426        let mut hasher = algo.checksum_hasher();
427        for chunk in bytes.chunks(CHUNK) {
428            hasher.update(chunk);
429            self.charge(chunk.len());
430        }
431        hasher.finalize()
432    }
433
434    /// CRC-32 of `bytes` (as recorded in pack index v2), charging as it goes so hashing a
435    /// large entry keeps at most about the budget's limit of the pack resident.
436    pub(crate) fn crc32(&self, bytes: &[u8]) -> u32 {
437        const CHUNK: usize = 1024 * 1024;
438        let mut hasher = crc32fast::Hasher::new();
439        for chunk in bytes.chunks(CHUNK) {
440            hasher.update(chunk);
441            self.charge(chunk.len());
442        }
443        hasher.finalize()
444    }
445}
446
447fn map_read_only(file: &File, len: usize) -> std::io::Result<Mmap> {
448    // SAFETY: the mapped file is only read through `&[u8]`; pack/MIDX paths are
449    // replaced atomically via rename, not truncated in place (see module docs).
450    // Length comes from fstat on this same `File` (see `open_from_file`).
451    unsafe { MmapOptions::new().len(len).map(file) }
452}
453
454/// Read only the pack header and trailer from disk for comparison with cached bytes.
455///
456/// # Errors
457///
458/// Returns [`Error::Io`] on read failure and [`Error::CorruptObject`] for malformed packs.
459pub(crate) fn fingerprint_from_file(path: &Path, hash_bytes: usize) -> Result<PackFingerprint> {
460    let mut file = File::open(path).map_err(Error::Io)?;
461    let file_len = file.metadata().map_err(Error::Io)?.len();
462    read_fingerprint_from_open_file(&mut file, file_len, hash_bytes)
463}
464
465fn read_fingerprint_from_open_file(
466    file: &mut File,
467    file_len: u64,
468    hash_bytes: usize,
469) -> Result<PackFingerprint> {
470    if hash_bytes != 20 && hash_bytes != 32 {
471        return Err(Error::CorruptObject(format!(
472            "unsupported pack trailer width {hash_bytes}"
473        )));
474    }
475    let mut header = [0u8; HEADER_LEN];
476    file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
477    file.read_exact(&mut header).map_err(Error::Io)?;
478    validate_pack_header(&header)?;
479    if file_len < u64::try_from(HEADER_LEN + hash_bytes).unwrap_or(u64::MAX) {
480        return Err(Error::CorruptObject(
481            "pack too small for trailer".to_owned(),
482        ));
483    }
484    let mut trailer = [0u8; 32];
485    file.seek(SeekFrom::Start(file_len - hash_bytes as u64))
486        .map_err(Error::Io)?;
487    file.read_exact(&mut trailer[..hash_bytes])
488        .map_err(Error::Io)?;
489    Ok(PackFingerprint {
490        header,
491        hash_bytes: hash_bytes as u8,
492        trailer,
493    })
494}
495
496pub(crate) fn fingerprint_from_slice(bytes: &[u8], hash_bytes: usize) -> Result<PackFingerprint> {
497    if hash_bytes != 20 && hash_bytes != 32 {
498        return Err(Error::CorruptObject(format!(
499            "unsupported pack trailer width {hash_bytes}"
500        )));
501    }
502    if bytes.len() < HEADER_LEN {
503        return Err(Error::CorruptObject("pack too small".to_owned()));
504    }
505    let mut header = [0u8; HEADER_LEN];
506    header.copy_from_slice(&bytes[..HEADER_LEN]);
507    validate_pack_header(&header)?;
508    if bytes.len() < HEADER_LEN + hash_bytes {
509        return Err(Error::CorruptObject(
510            "pack too small for trailer".to_owned(),
511        ));
512    }
513    let mut trailer = [0u8; 32];
514    trailer[..hash_bytes].copy_from_slice(&bytes[bytes.len() - hash_bytes..]);
515    Ok(PackFingerprint {
516        header,
517        hash_bytes: hash_bytes as u8,
518        trailer,
519    })
520}
521
522fn validate_pack_header(header: &[u8; HEADER_LEN]) -> Result<()> {
523    if header[..4] != *PACK_SIGNATURE {
524        return Err(Error::CorruptObject("bad pack signature".to_owned()));
525    }
526    let version = u32::from_be_bytes(header[4..8].try_into().unwrap_or([0; 4]));
527    if version != 2 && version != 3 {
528        return Err(Error::CorruptObject(format!(
529            "unsupported pack version {version}"
530        )));
531    }
532    Ok(())
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use std::fs;
539    use std::io::Write;
540
541    #[test]
542    fn positioned_ranges_reuse_buffer_and_preserve_open_file_identity() {
543        let dir = tempfile::tempdir().expect("dir");
544        let path = dir.path().join("random.pack");
545        let bytes: Vec<u8> = (0..131072).map(|i| (i % 251) as u8).collect();
546        fs::write(&path, &bytes).expect("write");
547        let pack = PackData::open_random_access(&path).expect("open");
548        assert!(pack.uses_positioned_reads());
549        let mut buf = Vec::with_capacity(8192);
550        let ptr = buf.as_ptr();
551        for (start, end) in [(20000, 24000), (100, 200), (bytes.len(), bytes.len())] {
552            assert_eq!(
553                pack.read_range(start, end, &mut buf).expect("range"),
554                &bytes[start..end]
555            );
556            assert_eq!(buf.as_ptr(), ptr);
557        }
558        assert_eq!(
559            pack.read_range(70000, 100000, &mut buf)
560                .expect("large entry"),
561            &bytes[70000..100000]
562        );
563        assert!(pack.read_range(50, 40, &mut buf).is_err());
564        assert!(pack.read_range(0, bytes.len() + 1, &mut buf).is_err());
565        // A replacement at the path cannot pair the old mapping with a new descriptor.
566        fs::rename(&path, path.with_extension("old")).expect("rename");
567        fs::write(&path, vec![0; bytes.len()]).expect("replace");
568        assert_eq!(
569            pack.read_range(123, 456, &mut buf).expect("old inode"),
570            &bytes[123..456]
571        );
572    }
573
574    #[test]
575    fn positioned_read_rejects_truncated_file() {
576        let dir = tempfile::tempdir().expect("dir");
577        let path = dir.path().join("truncated.pack");
578        fs::write(&path, vec![0; 32768]).expect("write");
579        let pack = PackData::open_random_access(&path).expect("open");
580        // Only positioned IO is used after truncation; never dereference the mapping.
581        fs::OpenOptions::new()
582            .write(true)
583            .open(&path)
584            .expect("file")
585            .set_len(0)
586            .expect("truncate");
587        assert!(matches!(
588            pack.read_range(100, 200, &mut Vec::new()),
589            Err(Error::Io(_))
590        ));
591    }
592
593    #[test]
594    fn owned_open_roundtrip() {
595        let dir = tempfile::tempdir().expect("tempdir");
596        let path = dir.path().join("tiny.pack");
597        fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
598        let data = PackData::open(&path).expect("open");
599        assert_eq!(&data[..], &b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
600    }
601
602    #[test]
603    fn fingerprint_detects_trailer_change() {
604        let dir = tempfile::tempdir().expect("tempdir");
605        let path = dir.path().join("p.pack");
606        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01" as &[u8]);
607        pack.extend_from_slice(&[0u8; 20]);
608        pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
609        fs::write(&path, &pack).expect("write");
610        let fp1 = fingerprint_from_file(&path, 20).expect("fp1");
611        let last = pack.len() - 1;
612        pack[last] = b'b';
613        fs::write(&path, &pack).expect("rewrite");
614        let fp2 = fingerprint_from_file(&path, 20).expect("fp2");
615        assert_ne!(fp1, fp2);
616    }
617
618    #[test]
619    fn fingerprint_sha256_trailer_on_pack_version_two() {
620        let dir = tempfile::tempdir().expect("tempdir");
621        let path = dir.path().join("sha256.pack");
622        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
623        let trailer = [0xAB_u8; 32];
624        pack.extend_from_slice(&trailer);
625        fs::write(&path, &pack).expect("write");
626        let fp = fingerprint_from_file(&path, 32).expect("fp");
627        assert_eq!(&fp.trailer[..fp.hash_bytes as usize], &trailer[..]);
628        assert_eq!(fp.header[4..8], 2u32.to_be_bytes());
629    }
630
631    #[test]
632    fn fingerprint_rejects_invalid_trailer_width_and_malformed_header() {
633        let tiny = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
634        let owned = PackData::from_owned(tiny.to_vec());
635        assert!(owned.fingerprint(19).is_err());
636        assert!(owned.fingerprint(33).is_err());
637        assert!(fingerprint_from_file(
638            &tempfile::tempdir().expect("d").path().join("missing.pack"),
639            20
640        )
641        .is_err());
642
643        let dir = tempfile::tempdir().expect("tempdir");
644        let path = dir.path().join("bad.pack");
645        fs::write(&path, b"NOTPACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
646        let err = fingerprint_from_file(&path, 20).expect_err("bad sig");
647        assert!(matches!(err, Error::CorruptObject(_)));
648
649        fs::write(&path, b"PACK\x00\x00\x00\x09\x00\x00\x00\x00").expect("bad ver");
650        let err = fingerprint_from_file(&path, 20).expect_err("bad ver");
651        assert!(matches!(err, Error::CorruptObject(_)));
652
653        fs::write(&path, b"PACK\x00\x00\x00\x02").expect("trunc");
654        assert!(matches!(
655            PackData::open(&path),
656            Err(Error::CorruptObject(_))
657        ));
658
659        let mut short_trailer = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
660        short_trailer.push(0);
661        fs::write(&path, &short_trailer).expect("short trailer");
662        let err = fingerprint_from_file(&path, 20).expect_err("short trailer");
663        assert!(matches!(err, Error::CorruptObject(_)));
664    }
665
666    #[test]
667    fn owned_buffer_when_pack_at_or_below_threshold() {
668        let dir = tempfile::tempdir().expect("tempdir");
669        let path = dir.path().join("medium.pack");
670        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
671        pack.resize(8192, 0);
672        pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
673        fs::write(&path, &pack).expect("write");
674        let data = PackData::open(&path).expect("open owned");
675        assert_eq!(data.len(), pack.len());
676        let fp = data.fingerprint(20).expect("fp");
677        assert_eq!(&fp.trailer[..20], b"aaaaaaaaaaaaaaaaaaaa");
678    }
679
680    #[test]
681    fn open_rejects_shorter_than_header() {
682        let dir = tempfile::tempdir().expect("tempdir");
683        let path = dir.path().join("short.pack");
684        fs::write(&path, b"PAC").expect("write");
685        assert!(matches!(
686            PackData::open(&path),
687            Err(Error::CorruptObject(_))
688        ));
689    }
690
691    #[test]
692    fn fingerprint_rejects_unsupported_trailer_width() {
693        let dir = tempfile::tempdir().expect("tempdir");
694        let path = dir.path().join("p.pack");
695        fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
696        assert!(fingerprint_from_file(&path, 16).is_err());
697        let data = PackData::open(&path).expect("open");
698        assert!(data.fingerprint(16).is_err());
699    }
700
701    #[test]
702    fn fingerprint_accepts_pack_version_three_header() {
703        let dir = tempfile::tempdir().expect("tempdir");
704        let path = dir.path().join("v3.pack");
705        let mut pack = Vec::from(b"PACK\x00\x00\x00\x03\x00\x00\x00\x00" as &[u8]);
706        pack.extend_from_slice(&[0u8; 20]);
707        fs::write(&path, &pack).expect("write");
708        let fp = fingerprint_from_file(&path, 20).expect("fp");
709        assert_eq!(u32::from_be_bytes(fp.header[4..8].try_into().unwrap()), 3);
710    }
711
712    #[test]
713    fn fingerprint_from_slice_rejects_bad_signature() {
714        let bytes = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00";
715        assert!(matches!(
716            fingerprint_from_slice(bytes, 20),
717            Err(Error::CorruptObject(_))
718        ));
719    }
720
721    /// Resident KiB of the mapping that contains `addr`, from `/proc/self/smaps`.
722    #[cfg(target_os = "linux")]
723    fn mapping_rss_kib(addr: *const u8) -> u64 {
724        let smaps = fs::read_to_string("/proc/self/smaps").expect("smaps");
725        let addr = addr as usize;
726        let mut inside = false;
727        for line in smaps.lines() {
728            let range = line.split_whitespace().next().unwrap_or_default();
729            if let Some((start, end)) = range.split_once('-') {
730                if let (Ok(start), Ok(end)) = (
731                    usize::from_str_radix(start, 16),
732                    usize::from_str_radix(end, 16),
733                ) {
734                    inside = (start..end).contains(&addr);
735                    continue;
736                }
737            }
738            if inside {
739                if let Some(kib) = line.strip_prefix("Rss:") {
740                    return kib
741                        .trim()
742                        .trim_end_matches("kB")
743                        .trim()
744                        .parse()
745                        .expect("kB");
746                }
747            }
748        }
749        panic!("no mapping contains {addr:#x}");
750    }
751
752    #[cfg(target_os = "linux")]
753    fn mapped_test_pack(dir: &Path, len: usize) -> (Arc<PackData>, Vec<u8>, SparseReadFiles) {
754        let path = dir.join("resident.pack");
755        let mut bytes = Vec::from(&b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
756        bytes.extend((0..len).map(|i| u8::try_from(i % 251).unwrap_or(0)));
757        fs::write(&path, &bytes).expect("write");
758        let files = SparseReadFiles::default();
759        let data = PackData::open_for_sparse_reads(&path, &files).expect("open");
760        assert!(data.is_mapped_backing());
761        (data, bytes, files)
762    }
763
764    #[cfg(target_os = "linux")]
765    #[test]
766    fn sparse_reads_do_not_make_scattered_mapping_windows_resident() {
767        let dir = tempfile::tempdir().expect("tempdir");
768        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
769        let mut prefix = [0; 16];
770        for offset in (0..bytes.len() - prefix.len()).step_by(64 << 10) {
771            assert_eq!(data.read_sparse(offset as u64, &mut prefix), Some(16));
772            assert_eq!(prefix, bytes[offset..offset + 16]);
773        }
774        assert!(mapping_rss_kib(data.as_ptr()) < 256);
775        // Keep using the inode that was mapped even after its path is replaced.
776        let path = dir.path().join("resident.pack");
777        fs::rename(&path, dir.path().join("old.pack")).expect("rename");
778        fs::write(&path, vec![0; bytes.len()]).expect("replacement");
779        assert_eq!(data.read_sparse(100, &mut prefix), Some(16));
780        assert_eq!(prefix, bytes[100..116]);
781    }
782
783    #[cfg(target_os = "linux")]
784    #[test]
785    fn evicted_sparse_descriptor_keeps_original_mapped_generation() {
786        let dir = tempfile::tempdir().expect("tempdir");
787        let (old, bytes, files) = mapped_test_pack(dir.path(), 16 << 10);
788        let path = dir.path().join("resident.pack");
789        let mut mappings = Vec::new();
790        for _ in 0..SparseReadFiles::CAPACITY {
791            mappings.push(PackData::open_for_sparse_reads(&path, &files).expect("map"));
792        }
793        assert_eq!(
794            files.files.lock().expect("pool").len(),
795            SparseReadFiles::CAPACITY
796        );
797        assert_eq!(old.read_sparse(100, &mut [0; 16]), None);
798        fs::rename(&path, dir.path().join("old.pack")).expect("rename");
799        fs::write(&path, vec![0; bytes.len()]).expect("replacement");
800        assert_eq!(&old[100..116], &bytes[100..116]);
801        let newest = mappings.last().expect("newest mapping");
802        let mut prefix = [0; 16];
803        assert_eq!(newest.read_sparse(100, &mut prefix), Some(16));
804        assert_eq!(prefix, bytes[100..116]);
805        files.clear();
806        assert_eq!(newest.read_sparse(100, &mut prefix), None);
807        assert_eq!(&newest[100..116], &bytes[100..116]);
808    }
809
810    #[test]
811    fn sparse_owned_reads_are_bounded_at_eof() {
812        let data = PackData::from_owned(b"some bytes".to_vec());
813        let mut buf = [0; 16];
814        assert_eq!(data.read_sparse(5, &mut buf), Some(5));
815        assert_eq!(&buf[..5], b"bytes");
816        assert_eq!(data.read_sparse(10, &mut buf), Some(0));
817        assert_eq!(data.read_sparse(11, &mut buf), None);
818        assert_eq!(data.read_sparse(u64::MAX, &mut buf), None);
819    }
820
821    #[cfg(target_os = "linux")]
822    #[test]
823    fn positioned_random_reads_leave_pack_mapping_nonresident() {
824        let dir = tempfile::tempdir().expect("tempdir");
825        let (_, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
826        let pack = PackData::open_random_access(&dir.path().join("resident.pack"))
827            .expect("positioned pack");
828        let mut buf = Vec::new();
829        for start in (HEADER_LEN..bytes.len() - 200).step_by(16381) {
830            assert_eq!(
831                pack.read_range(start, start + 200, &mut buf).expect("read"),
832                &bytes[start..start + 200]
833            );
834        }
835        let rss = mapping_rss_kib(pack.as_ptr());
836        assert!(rss < 1024, "random reads left {rss} KiB mapped");
837        let windows = pack
838            .random_file
839            .as_ref()
840            .expect("reader")
841            .windows
842            .lock()
843            .expect("windows");
844        assert!(windows
845            .entries
846            .iter()
847            .all(|w| w.bytes.len() <= RANDOM_WINDOW_BYTES));
848    }
849
850    #[cfg(target_os = "linux")]
851    #[test]
852    fn release_resident_drops_pages_but_keeps_the_bytes() {
853        let dir = tempfile::tempdir().expect("tempdir");
854        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
855        let digest = HashAlgo::Sha1.checksum_digest(&data);
856        assert!(
857            mapping_rss_kib(data.as_ptr()) > 20 << 10,
858            "a full read maps the file"
859        );
860        data.release_resident();
861        assert!(
862            mapping_rss_kib(data.as_ptr()) < 1 << 10,
863            "release drops the pages"
864        );
865        assert_eq!(&data[..], &bytes[..]);
866        assert_eq!(HashAlgo::Sha1.checksum_digest(&data), digest);
867    }
868
869    #[cfg(target_os = "linux")]
870    #[test]
871    fn resident_budget_bounds_a_full_pass() {
872        let dir = tempfile::tempdir().expect("tempdir");
873        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
874        data.release_resident();
875        let budget = ResidentBudget::new(&data, 4 << 20);
876        assert_eq!(
877            budget.checksum(HashAlgo::Sha1, &data),
878            HashAlgo::Sha1.checksum_digest(&bytes)
879        );
880        let rss = mapping_rss_kib(data.as_ptr());
881        assert!(rss <= 6 << 10, "budgeted pass left {rss} KiB resident");
882
883        data.release_resident();
884        let unbounded = ResidentBudget::unbounded();
885        let _ = unbounded.checksum(HashAlgo::Sha1, &data);
886        assert!(
887            mapping_rss_kib(data.as_ptr()) > 20 << 10,
888            "unbounded never releases"
889        );
890    }
891
892    #[cfg(target_os = "linux")]
893    #[test]
894    fn resident_budget_bounds_a_crc_pass() {
895        let dir = tempfile::tempdir().expect("tempdir");
896        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
897        data.release_resident();
898        let budget = ResidentBudget::new(&data, 4 << 20);
899        assert_eq!(budget.crc32(&data), crc32fast::hash(&bytes));
900        let rss = mapping_rss_kib(data.as_ptr());
901        assert!(rss <= 6 << 10, "budgeted CRC left {rss} KiB resident");
902    }
903
904    #[test]
905    fn resident_budget_crc_matches_on_owned_bytes() {
906        let bytes: Vec<u8> = (0..3_000_000u32).map(|i| (i % 251) as u8).collect();
907        let data = PackData::from_owned(bytes.clone());
908        assert_eq!(
909            ResidentBudget::new(&data, 1).crc32(&data),
910            crc32fast::hash(&bytes)
911        );
912        assert_eq!(ResidentBudget::unbounded().crc32(&[]), crc32fast::hash(&[]));
913    }
914
915    #[test]
916    fn release_resident_on_owned_bytes_is_a_no_op() {
917        let data = PackData::from_owned(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00".to_vec());
918        data.release_resident();
919        let budget = ResidentBudget::new(&data, 0);
920        budget.charge(1);
921        assert_eq!(&data[..4], b"PACK");
922    }
923
924    #[test]
925    fn mmap_large_sparse_pack() {
926        let dir = tempfile::tempdir().expect("tempdir");
927        let path = dir.path().join("sparse.pack");
928        let mut file = File::create(&path).expect("create");
929        file.write_all(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01")
930            .expect("header");
931        let huge: u64 = 1 << 32;
932        file.seek(SeekFrom::Start(huge)).expect("seek");
933        file.write_all(b"blob payload padding").expect("payload");
934        file.write_all(b"bbbbbbbbbbbbbbbbbbbb").expect("trailer");
935        drop(file);
936        let len = fs::metadata(&path).expect("meta").len();
937        assert!(len > huge);
938        let data = PackData::open(&path).expect("mmap sparse");
939        assert_eq!(&data[..4], b"PACK");
940        assert_eq!(data.len() as u64, len);
941    }
942}