1use crate::error::{Error, Result};
6use crate::objects::HashAlgo;
7use memmap2::{Mmap, MmapOptions};
8use std::fs::File;
9use std::io::{Read, Seek, SeekFrom};
10use std::ops::Deref;
11use std::path::Path;
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::Arc;
14
15const PACK_SIGNATURE: &[u8; 4] = b"PACK";
16const HEADER_LEN: usize = 12;
17
18const OWNED_READ_THRESHOLD: u64 = 8192;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub(crate) struct PackFingerprint {
26 pub header: [u8; HEADER_LEN],
28 pub hash_bytes: u8,
30 pub trailer: [u8; 32],
32}
33
34enum Storage {
35 Mapped(Mmap),
36 Owned(Vec<u8>),
37}
38
39pub struct PackData {
50 storage: Storage,
51}
52
53impl std::fmt::Debug for PackData {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("PackData")
56 .field("len", &self.len())
57 .field(
58 "backing",
59 &match &self.storage {
60 Storage::Mapped(_) => "mapped",
61 Storage::Owned(_) => "owned",
62 },
63 )
64 .finish()
65 }
66}
67
68impl Deref for PackData {
69 type Target = [u8];
70
71 fn deref(&self) -> &[u8] {
72 match &self.storage {
73 Storage::Mapped(m) => m.deref(),
74 Storage::Owned(v) => v.as_slice(),
75 }
76 }
77}
78
79impl PackData {
80 pub(crate) fn open(path: &Path) -> Result<Arc<Self>> {
87 Self::open_with_min_len(path, HEADER_LEN as u64)
88 }
89
90 pub(crate) fn open_with_min_len(path: &Path, min_len: u64) -> Result<Arc<Self>> {
92 let file = File::open(path).map_err(Error::Io)?;
93 Self::open_from_file(file, min_len)
94 }
95
96 fn open_from_file(file: File, min_len: u64) -> Result<Arc<Self>> {
97 let len = file.metadata().map_err(Error::Io)?.len();
98 if len < min_len {
99 return Err(Error::CorruptObject(format!(
100 "file is too small ({len} bytes, need at least {min_len})"
101 )));
102 }
103 let len_usize = usize::try_from(len).map_err(|_| {
104 Error::CorruptObject(format!(
105 "pack file length {len} does not fit in address space"
106 ))
107 })?;
108 if len <= OWNED_READ_THRESHOLD {
109 let mut data = Vec::with_capacity(len_usize);
110 let mut reader = file;
111 reader.read_to_end(&mut data).map_err(Error::Io)?;
112 return Ok(Arc::new(Self {
113 storage: Storage::Owned(data),
114 }));
115 }
116 match map_read_only(&file, len_usize) {
117 Ok(mmap) => Ok(Arc::new(Self {
118 storage: Storage::Mapped(mmap),
119 })),
120 Err(_) => {
121 let mut data = Vec::with_capacity(len_usize);
122 let mut reader = file;
123 reader.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
124 reader.read_to_end(&mut data).map_err(Error::Io)?;
125 Ok(Arc::new(Self {
126 storage: Storage::Owned(data),
127 }))
128 }
129 }
130 }
131
132 #[must_use]
134 pub(crate) fn from_owned(data: Vec<u8>) -> Arc<Self> {
135 Arc::new(Self {
136 storage: Storage::Owned(data),
137 })
138 }
139
140 pub(crate) fn release_resident(&self) {
145 #[cfg(unix)]
146 if let Storage::Mapped(map) = &self.storage {
147 let _ = unsafe {
151 map.unchecked_advise_range(memmap2::UncheckedAdvice::DontNeed, 0, map.len())
152 };
153 }
154 }
155
156 #[cfg(test)]
158 pub(crate) fn is_mapped_backing(&self) -> bool {
159 matches!(self.storage, Storage::Mapped(_))
160 }
161
162 pub(crate) fn fingerprint(&self, hash_bytes: usize) -> Result<PackFingerprint> {
171 fingerprint_from_slice(self, hash_bytes)
172 }
173}
174
175pub(crate) struct ResidentBudget<'a> {
183 pack: Option<&'a PackData>,
184 limit: usize,
185 charged: AtomicUsize,
186}
187
188impl<'a> ResidentBudget<'a> {
189 #[must_use]
191 pub(crate) fn unbounded() -> Self {
192 Self {
193 pack: None,
194 limit: usize::MAX,
195 charged: AtomicUsize::new(0),
196 }
197 }
198
199 #[must_use]
201 pub(crate) fn new(pack: &'a PackData, limit: usize) -> Self {
202 Self {
203 pack: Some(pack),
204 limit,
205 charged: AtomicUsize::new(0),
206 }
207 }
208
209 pub(crate) fn charge(&self, bytes: usize) {
211 let Some(pack) = self.pack else {
212 return;
213 };
214 let before = self.charged.fetch_add(bytes, Ordering::Relaxed);
215 if before.saturating_add(bytes) < self.limit {
216 return;
217 }
218 if self.charged.swap(0, Ordering::Relaxed) >= self.limit {
220 pack.release_resident();
221 }
222 }
223
224 pub(crate) fn checksum(&self, algo: HashAlgo, bytes: &[u8]) -> Vec<u8> {
226 const CHUNK: usize = 1024 * 1024;
227 let mut hasher = algo.checksum_hasher();
228 for chunk in bytes.chunks(CHUNK) {
229 hasher.update(chunk);
230 self.charge(chunk.len());
231 }
232 hasher.finalize()
233 }
234}
235
236fn map_read_only(file: &File, len: usize) -> std::io::Result<Mmap> {
237 unsafe { MmapOptions::new().len(len).map(file) }
241}
242
243pub(crate) fn fingerprint_from_file(path: &Path, hash_bytes: usize) -> Result<PackFingerprint> {
249 let mut file = File::open(path).map_err(Error::Io)?;
250 let file_len = file.metadata().map_err(Error::Io)?.len();
251 read_fingerprint_from_open_file(&mut file, file_len, hash_bytes)
252}
253
254fn read_fingerprint_from_open_file(
255 file: &mut File,
256 file_len: u64,
257 hash_bytes: usize,
258) -> Result<PackFingerprint> {
259 if hash_bytes != 20 && hash_bytes != 32 {
260 return Err(Error::CorruptObject(format!(
261 "unsupported pack trailer width {hash_bytes}"
262 )));
263 }
264 let mut header = [0u8; HEADER_LEN];
265 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
266 file.read_exact(&mut header).map_err(Error::Io)?;
267 validate_pack_header(&header)?;
268 if file_len < u64::try_from(HEADER_LEN + hash_bytes).unwrap_or(u64::MAX) {
269 return Err(Error::CorruptObject(
270 "pack too small for trailer".to_owned(),
271 ));
272 }
273 let mut trailer = [0u8; 32];
274 file.seek(SeekFrom::Start(file_len - hash_bytes as u64))
275 .map_err(Error::Io)?;
276 file.read_exact(&mut trailer[..hash_bytes])
277 .map_err(Error::Io)?;
278 Ok(PackFingerprint {
279 header,
280 hash_bytes: hash_bytes as u8,
281 trailer,
282 })
283}
284
285pub(crate) fn fingerprint_from_slice(bytes: &[u8], hash_bytes: usize) -> Result<PackFingerprint> {
286 if hash_bytes != 20 && hash_bytes != 32 {
287 return Err(Error::CorruptObject(format!(
288 "unsupported pack trailer width {hash_bytes}"
289 )));
290 }
291 if bytes.len() < HEADER_LEN {
292 return Err(Error::CorruptObject("pack too small".to_owned()));
293 }
294 let mut header = [0u8; HEADER_LEN];
295 header.copy_from_slice(&bytes[..HEADER_LEN]);
296 validate_pack_header(&header)?;
297 if bytes.len() < HEADER_LEN + hash_bytes {
298 return Err(Error::CorruptObject(
299 "pack too small for trailer".to_owned(),
300 ));
301 }
302 let mut trailer = [0u8; 32];
303 trailer[..hash_bytes].copy_from_slice(&bytes[bytes.len() - hash_bytes..]);
304 Ok(PackFingerprint {
305 header,
306 hash_bytes: hash_bytes as u8,
307 trailer,
308 })
309}
310
311fn validate_pack_header(header: &[u8; HEADER_LEN]) -> Result<()> {
312 if header[..4] != *PACK_SIGNATURE {
313 return Err(Error::CorruptObject("bad pack signature".to_owned()));
314 }
315 let version = u32::from_be_bytes(header[4..8].try_into().unwrap_or([0; 4]));
316 if version != 2 && version != 3 {
317 return Err(Error::CorruptObject(format!(
318 "unsupported pack version {version}"
319 )));
320 }
321 Ok(())
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use std::fs;
328 use std::io::Write;
329
330 #[test]
331 fn owned_open_roundtrip() {
332 let dir = tempfile::tempdir().expect("tempdir");
333 let path = dir.path().join("tiny.pack");
334 fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
335 let data = PackData::open(&path).expect("open");
336 assert_eq!(&data[..], &b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
337 }
338
339 #[test]
340 fn fingerprint_detects_trailer_change() {
341 let dir = tempfile::tempdir().expect("tempdir");
342 let path = dir.path().join("p.pack");
343 let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01" as &[u8]);
344 pack.extend_from_slice(&[0u8; 20]);
345 pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
346 fs::write(&path, &pack).expect("write");
347 let fp1 = fingerprint_from_file(&path, 20).expect("fp1");
348 let last = pack.len() - 1;
349 pack[last] = b'b';
350 fs::write(&path, &pack).expect("rewrite");
351 let fp2 = fingerprint_from_file(&path, 20).expect("fp2");
352 assert_ne!(fp1, fp2);
353 }
354
355 #[test]
356 fn fingerprint_sha256_trailer_on_pack_version_two() {
357 let dir = tempfile::tempdir().expect("tempdir");
358 let path = dir.path().join("sha256.pack");
359 let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
360 let trailer = [0xAB_u8; 32];
361 pack.extend_from_slice(&trailer);
362 fs::write(&path, &pack).expect("write");
363 let fp = fingerprint_from_file(&path, 32).expect("fp");
364 assert_eq!(&fp.trailer[..fp.hash_bytes as usize], &trailer[..]);
365 assert_eq!(fp.header[4..8], 2u32.to_be_bytes());
366 }
367
368 #[test]
369 fn fingerprint_rejects_invalid_trailer_width_and_malformed_header() {
370 let tiny = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
371 let owned = PackData::from_owned(tiny.to_vec());
372 assert!(owned.fingerprint(19).is_err());
373 assert!(owned.fingerprint(33).is_err());
374 assert!(fingerprint_from_file(
375 &tempfile::tempdir().expect("d").path().join("missing.pack"),
376 20
377 )
378 .is_err());
379
380 let dir = tempfile::tempdir().expect("tempdir");
381 let path = dir.path().join("bad.pack");
382 fs::write(&path, b"NOTPACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
383 let err = fingerprint_from_file(&path, 20).expect_err("bad sig");
384 assert!(matches!(err, Error::CorruptObject(_)));
385
386 fs::write(&path, b"PACK\x00\x00\x00\x09\x00\x00\x00\x00").expect("bad ver");
387 let err = fingerprint_from_file(&path, 20).expect_err("bad ver");
388 assert!(matches!(err, Error::CorruptObject(_)));
389
390 fs::write(&path, b"PACK\x00\x00\x00\x02").expect("trunc");
391 assert!(matches!(
392 PackData::open(&path),
393 Err(Error::CorruptObject(_))
394 ));
395
396 let mut short_trailer = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
397 short_trailer.push(0);
398 fs::write(&path, &short_trailer).expect("short trailer");
399 let err = fingerprint_from_file(&path, 20).expect_err("short trailer");
400 assert!(matches!(err, Error::CorruptObject(_)));
401 }
402
403 #[test]
404 fn owned_buffer_when_pack_at_or_below_threshold() {
405 let dir = tempfile::tempdir().expect("tempdir");
406 let path = dir.path().join("medium.pack");
407 let mut pack = Vec::from(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00" as &[u8]);
408 pack.resize(8192, 0);
409 pack.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaa");
410 fs::write(&path, &pack).expect("write");
411 let data = PackData::open(&path).expect("open owned");
412 assert_eq!(data.len(), pack.len());
413 let fp = data.fingerprint(20).expect("fp");
414 assert_eq!(&fp.trailer[..20], b"aaaaaaaaaaaaaaaaaaaa");
415 }
416
417 #[test]
418 fn open_rejects_shorter_than_header() {
419 let dir = tempfile::tempdir().expect("tempdir");
420 let path = dir.path().join("short.pack");
421 fs::write(&path, b"PAC").expect("write");
422 assert!(matches!(
423 PackData::open(&path),
424 Err(Error::CorruptObject(_))
425 ));
426 }
427
428 #[test]
429 fn fingerprint_rejects_unsupported_trailer_width() {
430 let dir = tempfile::tempdir().expect("tempdir");
431 let path = dir.path().join("p.pack");
432 fs::write(&path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("write");
433 assert!(fingerprint_from_file(&path, 16).is_err());
434 let data = PackData::open(&path).expect("open");
435 assert!(data.fingerprint(16).is_err());
436 }
437
438 #[test]
439 fn fingerprint_accepts_pack_version_three_header() {
440 let dir = tempfile::tempdir().expect("tempdir");
441 let path = dir.path().join("v3.pack");
442 let mut pack = Vec::from(b"PACK\x00\x00\x00\x03\x00\x00\x00\x00" as &[u8]);
443 pack.extend_from_slice(&[0u8; 20]);
444 fs::write(&path, &pack).expect("write");
445 let fp = fingerprint_from_file(&path, 20).expect("fp");
446 assert_eq!(u32::from_be_bytes(fp.header[4..8].try_into().unwrap()), 3);
447 }
448
449 #[test]
450 fn fingerprint_from_slice_rejects_bad_signature() {
451 let bytes = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00";
452 assert!(matches!(
453 fingerprint_from_slice(bytes, 20),
454 Err(Error::CorruptObject(_))
455 ));
456 }
457
458 #[cfg(target_os = "linux")]
460 fn mapping_rss_kib(addr: *const u8) -> u64 {
461 let smaps = fs::read_to_string("/proc/self/smaps").expect("smaps");
462 let addr = addr as usize;
463 let mut inside = false;
464 for line in smaps.lines() {
465 let range = line.split_whitespace().next().unwrap_or_default();
466 if let Some((start, end)) = range.split_once('-') {
467 if let (Ok(start), Ok(end)) = (
468 usize::from_str_radix(start, 16),
469 usize::from_str_radix(end, 16),
470 ) {
471 inside = (start..end).contains(&addr);
472 continue;
473 }
474 }
475 if inside {
476 if let Some(kib) = line.strip_prefix("Rss:") {
477 return kib
478 .trim()
479 .trim_end_matches("kB")
480 .trim()
481 .parse()
482 .expect("kB");
483 }
484 }
485 }
486 panic!("no mapping contains {addr:#x}");
487 }
488
489 #[cfg(target_os = "linux")]
490 fn mapped_test_pack(dir: &Path, len: usize) -> (Arc<PackData>, Vec<u8>) {
491 let path = dir.join("resident.pack");
492 let mut bytes = Vec::from(&b"PACK\x00\x00\x00\x02\x00\x00\x00\x00"[..]);
493 bytes.extend((0..len).map(|i| u8::try_from(i % 251).unwrap_or(0)));
494 fs::write(&path, &bytes).expect("write");
495 let data = PackData::open(&path).expect("open");
496 assert!(data.is_mapped_backing());
497 (data, bytes)
498 }
499
500 #[cfg(target_os = "linux")]
501 #[test]
502 fn release_resident_drops_pages_but_keeps_the_bytes() {
503 let dir = tempfile::tempdir().expect("tempdir");
504 let (data, bytes) = mapped_test_pack(dir.path(), 24 << 20);
505 let digest = HashAlgo::Sha1.checksum_digest(&data);
506 assert!(
507 mapping_rss_kib(data.as_ptr()) > 20 << 10,
508 "a full read maps the file"
509 );
510 data.release_resident();
511 assert!(
512 mapping_rss_kib(data.as_ptr()) < 1 << 10,
513 "release drops the pages"
514 );
515 assert_eq!(&data[..], &bytes[..]);
516 assert_eq!(HashAlgo::Sha1.checksum_digest(&data), digest);
517 }
518
519 #[cfg(target_os = "linux")]
520 #[test]
521 fn resident_budget_bounds_a_full_pass() {
522 let dir = tempfile::tempdir().expect("tempdir");
523 let (data, bytes) = mapped_test_pack(dir.path(), 24 << 20);
524 data.release_resident();
525 let budget = ResidentBudget::new(&data, 4 << 20);
526 assert_eq!(
527 budget.checksum(HashAlgo::Sha1, &data),
528 HashAlgo::Sha1.checksum_digest(&bytes)
529 );
530 let rss = mapping_rss_kib(data.as_ptr());
531 assert!(rss <= 6 << 10, "budgeted pass left {rss} KiB resident");
532
533 data.release_resident();
534 let unbounded = ResidentBudget::unbounded();
535 let _ = unbounded.checksum(HashAlgo::Sha1, &data);
536 assert!(
537 mapping_rss_kib(data.as_ptr()) > 20 << 10,
538 "unbounded never releases"
539 );
540 }
541
542 #[test]
543 fn release_resident_on_owned_bytes_is_a_no_op() {
544 let data = PackData::from_owned(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00".to_vec());
545 data.release_resident();
546 let budget = ResidentBudget::new(&data, 0);
547 budget.charge(1);
548 assert_eq!(&data[..4], b"PACK");
549 }
550
551 #[test]
552 fn mmap_large_sparse_pack() {
553 let dir = tempfile::tempdir().expect("tempdir");
554 let path = dir.path().join("sparse.pack");
555 let mut file = File::create(&path).expect("create");
556 file.write_all(b"PACK\x00\x00\x00\x02\x00\x00\x00\x01")
557 .expect("header");
558 let huge: u64 = 1 << 32;
559 file.seek(SeekFrom::Start(huge)).expect("seek");
560 file.write_all(b"blob payload padding").expect("payload");
561 file.write_all(b"bbbbbbbbbbbbbbbbbbbb").expect("trailer");
562 drop(file);
563 let len = fs::metadata(&path).expect("meta").len();
564 assert!(len > huge);
565 let data = PackData::open(&path).expect("mmap sparse");
566 assert_eq!(&data[..4], b"PACK");
567 assert_eq!(data.len() as u64, len);
568 }
569}