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 self.release_resident_through(usize::MAX);
345 }
346
347 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 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 #[cfg(test)]
369 pub(crate) fn is_mapped_backing(&self) -> bool {
370 matches!(self.storage, Storage::Mapped { .. })
371 }
372
373 pub(crate) fn fingerprint(&self, hash_bytes: usize) -> Result<PackFingerprint> {
382 fingerprint_from_slice(self, hash_bytes)
383 }
384}
385
386pub(crate) struct ResidentBudget<'a> {
394 pack: Option<&'a PackData>,
395 limit: usize,
396 charged: AtomicUsize,
397}
398
399impl<'a> ResidentBudget<'a> {
400 #[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 #[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 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 if self.charged.swap(0, Ordering::Relaxed) >= self.limit {
431 pack.release_resident();
432 }
433 }
434
435 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 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 unsafe { MmapOptions::new().len(len).map(file) }
464}
465
466pub(crate) fn map_read_only_file(file: &File, len: usize) -> std::io::Result<Mmap> {
468 map_read_only(file, len)
469}
470
471pub(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 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 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 #[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 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}