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        self.release_resident_through(usize::MAX);
345    }
346
347    /// Drop mapped pages for the pack prefix `[0, end)` from the resident set.
348    ///
349    /// Later reads fault those pages back in from the file. `end` is clamped to the map length.
350    pub(crate) fn release_resident_through(&self, end: usize) {
351        #[cfg(unix)]
352        if end > 0 {
353            if let Storage::Mapped { map, .. } = &self.storage {
354                let end = end.min(map.len());
355                // SAFETY: read-only mapping; reloading pages returns the same bytes.
356                let _ = unsafe {
357                    map.unchecked_advise_range(memmap2::UncheckedAdvice::DontNeed, 0, end)
358                };
359            }
360        }
361        #[cfg(not(unix))]
362        {
363            let _ = end;
364        }
365    }
366
367    /// True when backing storage is a memory map (not an owned buffer).
368    #[cfg(test)]
369    pub(crate) fn is_mapped_backing(&self) -> bool {
370        matches!(self.storage, Storage::Mapped { .. })
371    }
372
373    /// Fingerprint of the bytes currently held in memory.
374    ///
375    /// `hash_bytes` is the repository object-id / pack-trailer width (from the `.idx` or
376    /// repo format), not the pack header version field.
377    ///
378    /// # Errors
379    ///
380    /// Returns [`Error::CorruptObject`] when the buffer is not a well-formed pack prefix/trailer.
381    pub(crate) fn fingerprint(&self, hash_bytes: usize) -> Result<PackFingerprint> {
382        fingerprint_from_slice(self, hash_bytes)
383    }
384}
385
386/// Caps how much of a memory-mapped pack a sequential pass over it keeps resident.
387///
388/// Readers [`charge`](Self::charge) the bytes they read; once the charges reach the
389/// limit, the whole mapping's pages are dropped and counting starts over. Charging is
390/// lock-free and shared by all worker threads. Meant for passes that move forward
391/// through the pack: scattered small reads map whole fault-around windows each, so
392/// they are better served by positioned file reads than by a budget.
393pub(crate) struct ResidentBudget<'a> {
394    pack: Option<&'a PackData>,
395    limit: usize,
396    charged: AtomicUsize,
397}
398
399impl<'a> ResidentBudget<'a> {
400    /// A budget that never releases (the caller owns the bytes, or they are not mapped).
401    #[must_use]
402    pub(crate) fn unbounded() -> Self {
403        Self {
404            pack: None,
405            limit: usize::MAX,
406            charged: AtomicUsize::new(0),
407        }
408    }
409
410    /// Keep at most about `limit` bytes of `pack` resident.
411    #[must_use]
412    pub(crate) fn new(pack: &'a PackData, limit: usize) -> Self {
413        Self {
414            pack: Some(pack),
415            limit,
416            charged: AtomicUsize::new(0),
417        }
418    }
419
420    /// Record a read of `bytes` from the pack, releasing its pages when the limit is reached.
421    pub(crate) fn charge(&self, bytes: usize) {
422        let Some(pack) = self.pack else {
423            return;
424        };
425        let before = self.charged.fetch_add(bytes, Ordering::Relaxed);
426        if before.saturating_add(bytes) < self.limit {
427            return;
428        }
429        // Of several threads crossing the limit together, only the one that resets wins.
430        if self.charged.swap(0, Ordering::Relaxed) >= self.limit {
431            pack.release_resident();
432        }
433    }
434
435    /// Checksum `bytes` with `algo`, charging as it goes so a full pass stays within budget.
436    pub(crate) fn checksum(&self, algo: HashAlgo, bytes: &[u8]) -> Vec<u8> {
437        const CHUNK: usize = 1024 * 1024;
438        let mut hasher = algo.checksum_hasher();
439        for chunk in bytes.chunks(CHUNK) {
440            hasher.update(chunk);
441            self.charge(chunk.len());
442        }
443        hasher.finalize()
444    }
445
446    /// CRC-32 of `bytes` (as recorded in pack index v2), charging as it goes so hashing a
447    /// large entry keeps at most about the budget's limit of the pack resident.
448    pub(crate) fn crc32(&self, bytes: &[u8]) -> u32 {
449        const CHUNK: usize = 1024 * 1024;
450        let mut hasher = crc32fast::Hasher::new();
451        for chunk in bytes.chunks(CHUNK) {
452            hasher.update(chunk);
453            self.charge(chunk.len());
454        }
455        hasher.finalize()
456    }
457}
458
459fn map_read_only(file: &File, len: usize) -> std::io::Result<Mmap> {
460    // SAFETY: the mapped file is only read through `&[u8]`; pack/MIDX paths are
461    // replaced atomically via rename, not truncated in place (see module docs).
462    // Length comes from fstat on this same `File` (see `open_from_file`).
463    unsafe { MmapOptions::new().len(len).map(file) }
464}
465
466/// Map `len` bytes of an open read-only file (reftable tables, shared cache).
467pub(crate) fn map_read_only_file(file: &File, len: usize) -> std::io::Result<Mmap> {
468    map_read_only(file, len)
469}
470
471/// Read only the pack header and trailer from disk for comparison with cached bytes.
472///
473/// # Errors
474///
475/// Returns [`Error::Io`] on read failure and [`Error::CorruptObject`] for malformed packs.
476pub(crate) fn fingerprint_from_file(path: &Path, hash_bytes: usize) -> Result<PackFingerprint> {
477    let mut file = File::open(path).map_err(Error::Io)?;
478    let file_len = file.metadata().map_err(Error::Io)?.len();
479    read_fingerprint_from_open_file(&mut file, file_len, hash_bytes)
480}
481
482fn read_fingerprint_from_open_file(
483    file: &mut File,
484    file_len: u64,
485    hash_bytes: usize,
486) -> Result<PackFingerprint> {
487    if hash_bytes != 20 && hash_bytes != 32 {
488        return Err(Error::CorruptObject(format!(
489            "unsupported pack trailer width {hash_bytes}"
490        )));
491    }
492    let mut header = [0u8; HEADER_LEN];
493    file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
494    file.read_exact(&mut header).map_err(Error::Io)?;
495    validate_pack_header(&header)?;
496    if file_len < u64::try_from(HEADER_LEN + hash_bytes).unwrap_or(u64::MAX) {
497        return Err(Error::CorruptObject(
498            "pack too small for trailer".to_owned(),
499        ));
500    }
501    let mut trailer = [0u8; 32];
502    file.seek(SeekFrom::Start(file_len - hash_bytes as u64))
503        .map_err(Error::Io)?;
504    file.read_exact(&mut trailer[..hash_bytes])
505        .map_err(Error::Io)?;
506    Ok(PackFingerprint {
507        header,
508        hash_bytes: hash_bytes as u8,
509        trailer,
510    })
511}
512
513pub(crate) fn fingerprint_from_slice(bytes: &[u8], hash_bytes: usize) -> Result<PackFingerprint> {
514    if hash_bytes != 20 && hash_bytes != 32 {
515        return Err(Error::CorruptObject(format!(
516            "unsupported pack trailer width {hash_bytes}"
517        )));
518    }
519    if bytes.len() < HEADER_LEN {
520        return Err(Error::CorruptObject("pack too small".to_owned()));
521    }
522    let mut header = [0u8; HEADER_LEN];
523    header.copy_from_slice(&bytes[..HEADER_LEN]);
524    validate_pack_header(&header)?;
525    if bytes.len() < HEADER_LEN + hash_bytes {
526        return Err(Error::CorruptObject(
527            "pack too small for trailer".to_owned(),
528        ));
529    }
530    let mut trailer = [0u8; 32];
531    trailer[..hash_bytes].copy_from_slice(&bytes[bytes.len() - hash_bytes..]);
532    Ok(PackFingerprint {
533        header,
534        hash_bytes: hash_bytes as u8,
535        trailer,
536    })
537}
538
539fn validate_pack_header(header: &[u8; HEADER_LEN]) -> Result<()> {
540    if header[..4] != *PACK_SIGNATURE {
541        return Err(Error::CorruptObject("bad pack signature".to_owned()));
542    }
543    let version = u32::from_be_bytes(header[4..8].try_into().unwrap_or([0; 4]));
544    if version != 2 && version != 3 {
545        return Err(Error::CorruptObject(format!(
546            "unsupported pack version {version}"
547        )));
548    }
549    Ok(())
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use std::fs;
556    use std::io::Write;
557
558    #[test]
559    fn positioned_ranges_reuse_buffer_and_preserve_open_file_identity() {
560        let dir = tempfile::tempdir().expect("dir");
561        let path = dir.path().join("random.pack");
562        let bytes: Vec<u8> = (0..131072).map(|i| (i % 251) as u8).collect();
563        fs::write(&path, &bytes).expect("write");
564        let pack = PackData::open_random_access(&path).expect("open");
565        assert!(pack.uses_positioned_reads());
566        let mut buf = Vec::with_capacity(8192);
567        let ptr = buf.as_ptr();
568        for (start, end) in [(20000, 24000), (100, 200), (bytes.len(), bytes.len())] {
569            assert_eq!(
570                pack.read_range(start, end, &mut buf).expect("range"),
571                &bytes[start..end]
572            );
573            assert_eq!(buf.as_ptr(), ptr);
574        }
575        assert_eq!(
576            pack.read_range(70000, 100000, &mut buf)
577                .expect("large entry"),
578            &bytes[70000..100000]
579        );
580        assert!(pack.read_range(50, 40, &mut buf).is_err());
581        assert!(pack.read_range(0, bytes.len() + 1, &mut buf).is_err());
582        // A replacement at the path cannot pair the old mapping with a new descriptor.
583        fs::rename(&path, path.with_extension("old")).expect("rename");
584        fs::write(&path, vec![0; bytes.len()]).expect("replace");
585        assert_eq!(
586            pack.read_range(123, 456, &mut buf).expect("old inode"),
587            &bytes[123..456]
588        );
589    }
590
591    #[test]
592    fn positioned_read_rejects_truncated_file() {
593        let dir = tempfile::tempdir().expect("dir");
594        let path = dir.path().join("truncated.pack");
595        fs::write(&path, vec![0; 32768]).expect("write");
596        let pack = PackData::open_random_access(&path).expect("open");
597        // Only positioned IO is used after truncation; never dereference the mapping.
598        fs::OpenOptions::new()
599            .write(true)
600            .open(&path)
601            .expect("file")
602            .set_len(0)
603            .expect("truncate");
604        assert!(matches!(
605            pack.read_range(100, 200, &mut Vec::new()),
606            Err(Error::Io(_))
607        ));
608    }
609
610    #[test]
611    fn owned_open_roundtrip() {
612        let dir = tempfile::tempdir().expect("tempdir");
613        let path = dir.path().join("tiny.pack");
614        fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
615        let data = PackData::open(&path).expect("open");
616        assert_eq!(&data[..], &b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
617    }
618
619    #[test]
620    fn fingerprint_detects_trailer_change() {
621        let dir = tempfile::tempdir().expect("tempdir");
622        let path = dir.path().join("p.pack");
623        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01" as &[u8]);
624        pack.extend_from_slice(&[0u8; 20]);
625        pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
626        fs::write(&path, &pack).expect("write");
627        let fp1 = fingerprint_from_file(&path, 20).expect("fp1");
628        let last = pack.len() - 1;
629        pack[last] = b'b';
630        fs::write(&path, &pack).expect("rewrite");
631        let fp2 = fingerprint_from_file(&path, 20).expect("fp2");
632        assert_ne!(fp1, fp2);
633    }
634
635    #[test]
636    fn fingerprint_sha256_trailer_on_pack_version_two() {
637        let dir = tempfile::tempdir().expect("tempdir");
638        let path = dir.path().join("sha256.pack");
639        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
640        let trailer = [0xAB_u8; 32];
641        pack.extend_from_slice(&trailer);
642        fs::write(&path, &pack).expect("write");
643        let fp = fingerprint_from_file(&path, 32).expect("fp");
644        assert_eq!(&fp.trailer[..fp.hash_bytes as usize], &trailer[..]);
645        assert_eq!(fp.header[4..8], 2u32.to_be_bytes());
646    }
647
648    #[test]
649    fn fingerprint_rejects_invalid_trailer_width_and_malformed_header() {
650        let tiny = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
651        let owned = PackData::from_owned(tiny.to_vec());
652        assert!(owned.fingerprint(19).is_err());
653        assert!(owned.fingerprint(33).is_err());
654        assert!(fingerprint_from_file(
655            &tempfile::tempdir().expect("d").path().join("missing.pack"),
656            20
657        )
658        .is_err());
659
660        let dir = tempfile::tempdir().expect("tempdir");
661        let path = dir.path().join("bad.pack");
662        fs::write(&path, b"NOTPACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
663        let err = fingerprint_from_file(&path, 20).expect_err("bad sig");
664        assert!(matches!(err, Error::CorruptObject(_)));
665
666        fs::write(&path, b"PACK\x00\x00\x00\x09\x00\x00\x00\x00").expect("bad ver");
667        let err = fingerprint_from_file(&path, 20).expect_err("bad ver");
668        assert!(matches!(err, Error::CorruptObject(_)));
669
670        fs::write(&path, b"PACK\x00\x00\x00\x02").expect("trunc");
671        assert!(matches!(
672            PackData::open(&path),
673            Err(Error::CorruptObject(_))
674        ));
675
676        let mut short_trailer = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
677        short_trailer.push(0);
678        fs::write(&path, &short_trailer).expect("short trailer");
679        let err = fingerprint_from_file(&path, 20).expect_err("short trailer");
680        assert!(matches!(err, Error::CorruptObject(_)));
681    }
682
683    #[test]
684    fn owned_buffer_when_pack_at_or_below_threshold() {
685        let dir = tempfile::tempdir().expect("tempdir");
686        let path = dir.path().join("medium.pack");
687        let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
688        pack.resize(8192, 0);
689        pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
690        fs::write(&path, &pack).expect("write");
691        let data = PackData::open(&path).expect("open owned");
692        assert_eq!(data.len(), pack.len());
693        let fp = data.fingerprint(20).expect("fp");
694        assert_eq!(&fp.trailer[..20], b"aaaaaaaaaaaaaaaaaaaa");
695    }
696
697    #[test]
698    fn open_rejects_shorter_than_header() {
699        let dir = tempfile::tempdir().expect("tempdir");
700        let path = dir.path().join("short.pack");
701        fs::write(&path, b"PAC").expect("write");
702        assert!(matches!(
703            PackData::open(&path),
704            Err(Error::CorruptObject(_))
705        ));
706    }
707
708    #[test]
709    fn fingerprint_rejects_unsupported_trailer_width() {
710        let dir = tempfile::tempdir().expect("tempdir");
711        let path = dir.path().join("p.pack");
712        fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
713        assert!(fingerprint_from_file(&path, 16).is_err());
714        let data = PackData::open(&path).expect("open");
715        assert!(data.fingerprint(16).is_err());
716    }
717
718    #[test]
719    fn fingerprint_accepts_pack_version_three_header() {
720        let dir = tempfile::tempdir().expect("tempdir");
721        let path = dir.path().join("v3.pack");
722        let mut pack = Vec::from(b"PACK\x00\x00\x00\x03\x00\x00\x00\x00" as &[u8]);
723        pack.extend_from_slice(&[0u8; 20]);
724        fs::write(&path, &pack).expect("write");
725        let fp = fingerprint_from_file(&path, 20).expect("fp");
726        assert_eq!(u32::from_be_bytes(fp.header[4..8].try_into().unwrap()), 3);
727    }
728
729    #[test]
730    fn fingerprint_from_slice_rejects_bad_signature() {
731        let bytes = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00";
732        assert!(matches!(
733            fingerprint_from_slice(bytes, 20),
734            Err(Error::CorruptObject(_))
735        ));
736    }
737
738    /// Resident KiB of the mapping that contains `addr`, from `/proc/self/smaps`.
739    #[cfg(target_os = "linux")]
740    fn mapping_rss_kib(addr: *const u8) -> u64 {
741        let smaps = fs::read_to_string("/proc/self/smaps").expect("smaps");
742        let addr = addr as usize;
743        let mut inside = false;
744        for line in smaps.lines() {
745            let range = line.split_whitespace().next().unwrap_or_default();
746            if let Some((start, end)) = range.split_once('-') {
747                if let (Ok(start), Ok(end)) = (
748                    usize::from_str_radix(start, 16),
749                    usize::from_str_radix(end, 16),
750                ) {
751                    inside = (start..end).contains(&addr);
752                    continue;
753                }
754            }
755            if inside {
756                if let Some(kib) = line.strip_prefix("Rss:") {
757                    return kib
758                        .trim()
759                        .trim_end_matches("kB")
760                        .trim()
761                        .parse()
762                        .expect("kB");
763                }
764            }
765        }
766        panic!("no mapping contains {addr:#x}");
767    }
768
769    #[cfg(target_os = "linux")]
770    fn mapped_test_pack(dir: &Path, len: usize) -> (Arc<PackData>, Vec<u8>, SparseReadFiles) {
771        let path = dir.join("resident.pack");
772        let mut bytes = Vec::from(&b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
773        bytes.extend((0..len).map(|i| u8::try_from(i % 251).unwrap_or(0)));
774        fs::write(&path, &bytes).expect("write");
775        let files = SparseReadFiles::default();
776        let data = PackData::open_for_sparse_reads(&path, &files).expect("open");
777        assert!(data.is_mapped_backing());
778        (data, bytes, files)
779    }
780
781    #[cfg(target_os = "linux")]
782    #[test]
783    fn sparse_reads_do_not_make_scattered_mapping_windows_resident() {
784        let dir = tempfile::tempdir().expect("tempdir");
785        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
786        let mut prefix = [0; 16];
787        for offset in (0..bytes.len() - prefix.len()).step_by(64 << 10) {
788            assert_eq!(data.read_sparse(offset as u64, &mut prefix), Some(16));
789            assert_eq!(prefix, bytes[offset..offset + 16]);
790        }
791        assert!(mapping_rss_kib(data.as_ptr()) < 256);
792        // Keep using the inode that was mapped even after its path is replaced.
793        let path = dir.path().join("resident.pack");
794        fs::rename(&path, dir.path().join("old.pack")).expect("rename");
795        fs::write(&path, vec![0; bytes.len()]).expect("replacement");
796        assert_eq!(data.read_sparse(100, &mut prefix), Some(16));
797        assert_eq!(prefix, bytes[100..116]);
798    }
799
800    #[cfg(target_os = "linux")]
801    #[test]
802    fn evicted_sparse_descriptor_keeps_original_mapped_generation() {
803        let dir = tempfile::tempdir().expect("tempdir");
804        let (old, bytes, files) = mapped_test_pack(dir.path(), 16 << 10);
805        let path = dir.path().join("resident.pack");
806        let mut mappings = Vec::new();
807        for _ in 0..SparseReadFiles::CAPACITY {
808            mappings.push(PackData::open_for_sparse_reads(&path, &files).expect("map"));
809        }
810        assert_eq!(
811            files.files.lock().expect("pool").len(),
812            SparseReadFiles::CAPACITY
813        );
814        assert_eq!(old.read_sparse(100, &mut [0; 16]), None);
815        fs::rename(&path, dir.path().join("old.pack")).expect("rename");
816        fs::write(&path, vec![0; bytes.len()]).expect("replacement");
817        assert_eq!(&old[100..116], &bytes[100..116]);
818        let newest = mappings.last().expect("newest mapping");
819        let mut prefix = [0; 16];
820        assert_eq!(newest.read_sparse(100, &mut prefix), Some(16));
821        assert_eq!(prefix, bytes[100..116]);
822        files.clear();
823        assert_eq!(newest.read_sparse(100, &mut prefix), None);
824        assert_eq!(&newest[100..116], &bytes[100..116]);
825    }
826
827    #[test]
828    fn sparse_owned_reads_are_bounded_at_eof() {
829        let data = PackData::from_owned(b"some bytes".to_vec());
830        let mut buf = [0; 16];
831        assert_eq!(data.read_sparse(5, &mut buf), Some(5));
832        assert_eq!(&buf[..5], b"bytes");
833        assert_eq!(data.read_sparse(10, &mut buf), Some(0));
834        assert_eq!(data.read_sparse(11, &mut buf), None);
835        assert_eq!(data.read_sparse(u64::MAX, &mut buf), None);
836    }
837
838    #[cfg(target_os = "linux")]
839    #[test]
840    fn positioned_random_reads_leave_pack_mapping_nonresident() {
841        let dir = tempfile::tempdir().expect("tempdir");
842        let (_, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
843        let pack = PackData::open_random_access(&dir.path().join("resident.pack"))
844            .expect("positioned pack");
845        let mut buf = Vec::new();
846        for start in (HEADER_LEN..bytes.len() - 200).step_by(16381) {
847            assert_eq!(
848                pack.read_range(start, start + 200, &mut buf).expect("read"),
849                &bytes[start..start + 200]
850            );
851        }
852        let rss = mapping_rss_kib(pack.as_ptr());
853        assert!(rss < 1024, "random reads left {rss} KiB mapped");
854        let windows = pack
855            .random_file
856            .as_ref()
857            .expect("reader")
858            .windows
859            .lock()
860            .expect("windows");
861        assert!(windows
862            .entries
863            .iter()
864            .all(|w| w.bytes.len() <= RANDOM_WINDOW_BYTES));
865    }
866
867    #[cfg(target_os = "linux")]
868    #[test]
869    fn release_resident_drops_pages_but_keeps_the_bytes() {
870        let dir = tempfile::tempdir().expect("tempdir");
871        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
872        let digest = HashAlgo::Sha1.checksum_digest(&data);
873        assert!(
874            mapping_rss_kib(data.as_ptr()) > 20 << 10,
875            "a full read maps the file"
876        );
877        data.release_resident();
878        assert!(
879            mapping_rss_kib(data.as_ptr()) < 1 << 10,
880            "release drops the pages"
881        );
882        assert_eq!(&data[..], &bytes[..]);
883        assert_eq!(HashAlgo::Sha1.checksum_digest(&data), digest);
884    }
885
886    #[cfg(target_os = "linux")]
887    #[test]
888    fn resident_budget_bounds_a_full_pass() {
889        let dir = tempfile::tempdir().expect("tempdir");
890        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
891        data.release_resident();
892        let budget = ResidentBudget::new(&data, 4 << 20);
893        assert_eq!(
894            budget.checksum(HashAlgo::Sha1, &data),
895            HashAlgo::Sha1.checksum_digest(&bytes)
896        );
897        let rss = mapping_rss_kib(data.as_ptr());
898        assert!(rss <= 6 << 10, "budgeted pass left {rss} KiB resident");
899
900        data.release_resident();
901        let unbounded = ResidentBudget::unbounded();
902        let _ = unbounded.checksum(HashAlgo::Sha1, &data);
903        assert!(
904            mapping_rss_kib(data.as_ptr()) > 20 << 10,
905            "unbounded never releases"
906        );
907    }
908
909    #[cfg(target_os = "linux")]
910    #[test]
911    fn resident_budget_bounds_a_crc_pass() {
912        let dir = tempfile::tempdir().expect("tempdir");
913        let (data, bytes, _files) = mapped_test_pack(dir.path(), 24 << 20);
914        data.release_resident();
915        let budget = ResidentBudget::new(&data, 4 << 20);
916        assert_eq!(budget.crc32(&data), crc32fast::hash(&bytes));
917        let rss = mapping_rss_kib(data.as_ptr());
918        assert!(rss <= 6 << 10, "budgeted CRC left {rss} KiB resident");
919    }
920
921    #[test]
922    fn resident_budget_crc_matches_on_owned_bytes() {
923        let bytes: Vec<u8> = (0..3_000_000u32).map(|i| (i % 251) as u8).collect();
924        let data = PackData::from_owned(bytes.clone());
925        assert_eq!(
926            ResidentBudget::new(&data, 1).crc32(&data),
927            crc32fast::hash(&bytes)
928        );
929        assert_eq!(ResidentBudget::unbounded().crc32(&[]), crc32fast::hash(&[]));
930    }
931
932    #[test]
933    fn release_resident_on_owned_bytes_is_a_no_op() {
934        let data = PackData::from_owned(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00".to_vec());
935        data.release_resident();
936        let budget = ResidentBudget::new(&data, 0);
937        budget.charge(1);
938        assert_eq!(&data[..4], b"PACK");
939    }
940
941    #[test]
942    fn mmap_large_sparse_pack() {
943        let dir = tempfile::tempdir().expect("tempdir");
944        let path = dir.path().join("sparse.pack");
945        let mut file = File::create(&path).expect("create");
946        file.write_all(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01")
947            .expect("header");
948        let huge: u64 = 1 << 32;
949        file.seek(SeekFrom::Start(huge)).expect("seek");
950        file.write_all(b"blob payload padding").expect("payload");
951        file.write_all(b"bbbbbbbbbbbbbbbbbbbb").expect("trailer");
952        drop(file);
953        let len = fs::metadata(&path).expect("meta").len();
954        assert!(len > huge);
955        let data = PackData::open(&path).expect("mmap sparse");
956        assert_eq!(&data[..4], b"PACK");
957        assert_eq!(data.len() as u64, len);
958    }
959}