1use 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
19const 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub(crate) struct PackFingerprint {
46 pub header: [u8; HEADER_LEN],
48 pub hash_bytes: u8,
50 pub trailer: [u8; 32],
52}
53
54#[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 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
89pub struct PackData {
100 storage: Storage,
101 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 pub(crate) fn open(path: &Path) -> Result<Arc<Self>> {
140 Self::open_with_min_len(path, HEADER_LEN as u64)
141 }
142
143 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 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 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 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 #[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 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 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 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 pub(crate) fn release_resident(&self) {
344 #[cfg(unix)]
345 if let Storage::Mapped { map, .. } = &self.storage {
346 let _ = unsafe {
350 map.unchecked_advise_range(memmap2::UncheckedAdvice::DontNeed, 0, map.len())
351 };
352 }
353 }
354
355 #[cfg(test)]
357 pub(crate) fn is_mapped_backing(&self) -> bool {
358 matches!(self.storage, Storage::Mapped { .. })
359 }
360
361 pub(crate) fn fingerprint(&self, hash_bytes: usize) -> Result<PackFingerprint> {
370 fingerprint_from_slice(self, hash_bytes)
371 }
372}
373
374pub(crate) struct ResidentBudget<'a> {
382 pack: Option<&'a PackData>,
383 limit: usize,
384 charged: AtomicUsize,
385}
386
387impl<'a> ResidentBudget<'a> {
388 #[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 #[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 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 if self.charged.swap(0, Ordering::Relaxed) >= self.limit {
419 pack.release_resident();
420 }
421 }
422
423 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 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 unsafe { MmapOptions::new().len(len).map(file) }
452}
453
454pub(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 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 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 #[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 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}