1use std::collections::HashSet;
7use std::io::{self, Cursor, Read};
8use std::ops::ControlFlow;
9use std::path::Path;
10use std::sync::Arc;
11
12use flate2::{Decompress, FlushDecompress, Status};
13
14use crate::error::{Error, Result};
15use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
16use crate::pack::{self, read_local_pack_indexes_cached, reprepare_pack_directory_on_miss};
17use crate::pack_index::PackIndex;
18use crate::pack_store::PackStore;
19
20use super::{ObjectStore, ObjectStream};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct PackFilter {
25 pub include_promisor: bool,
27 pub include_cruft: bool,
29}
30
31impl Default for PackFilter {
32 fn default() -> Self {
33 Self {
34 include_promisor: false,
35 include_cruft: true,
36 }
37 }
38}
39
40pub struct PackedObjects {
42 store: Arc<PackStore>,
43}
44
45impl std::fmt::Debug for PackedObjects {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("PackedObjects")
48 .field("objects_dir", &self.store.objects_dir())
49 .finish()
50 }
51}
52
53impl PackedObjects {
54 #[must_use]
56 pub fn new(store: Arc<PackStore>) -> Self {
57 Self { store }
58 }
59
60 #[must_use]
62 pub fn pack_store(&self) -> &Arc<PackStore> {
63 &self.store
64 }
65
66 pub fn object_ids_from_index_path(idx_path: &Path) -> Result<HashSet<ObjectId>> {
72 let idx = pack::read_pack_index(idx_path)?;
73 let mut out = HashSet::new();
74 for entry in idx.iter() {
75 if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
76 out.insert(oid);
77 }
78 }
79 Ok(out)
80 }
81
82 pub fn contains_filtered(&self, oid: &ObjectId, filter: PackFilter) -> Result<bool> {
91 self.with_pack(|objects_dir| {
92 for idx in filtered_indexes(objects_dir, filter)? {
93 if idx.contains(oid)? {
94 return Ok(true);
95 }
96 }
97 Ok(false)
98 })
99 }
100
101 pub fn for_each_promisor_object(
107 &self,
108 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
109 ) -> Result<()> {
110 self.with_pack(|objects_dir| {
111 let indexes = read_local_pack_indexes_cached(objects_dir)?;
112 let mut seen = HashSet::new();
113 for idx in indexes {
114 if !idx.is_promisor {
115 continue;
116 }
117 for entry in idx.iter() {
118 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
119 continue;
120 };
121 if !seen.insert(oid) {
122 continue;
123 }
124 if f(&oid).is_break() {
125 return Ok(());
126 }
127 }
128 }
129 Ok(())
130 })
131 }
132
133 pub fn kept_pack_object_ids(&self) -> Result<HashSet<ObjectId>> {
139 self.with_pack(collect_kept_pack_object_ids)
140 }
141
142 pub fn for_each_removable_pack_object(
151 &self,
152 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
153 ) -> Result<()> {
154 self.with_pack(|objects_dir| for_each_removable_pack_object_ids(objects_dir, f))
155 }
156
157 pub fn freshen_pack_containing(&self, oid: &ObjectId) -> Result<bool> {
163 self.with_pack(|objects_dir| {
164 let indexes = read_local_pack_indexes_cached(objects_dir)?;
165 for idx in &indexes {
166 if !idx.contains(oid)? {
167 continue;
168 }
169 if idx.is_cruft {
170 return Ok(false);
171 }
172 return Ok(touch_pack_mtime(&idx.pack_path));
173 }
174 Ok(false)
175 })
176 }
177
178 fn with_pack<R>(&self, f: impl FnOnce(&std::path::Path) -> Result<R>) -> Result<R> {
179 if PackStore::read_context_matches_objects_dir(self.store.objects_dir()) {
180 return f(self.store.objects_dir());
181 }
182 PackStore::with_context(Arc::clone(&self.store), || f(self.store.objects_dir()))
183 }
184
185 fn hash_algo_from_indexes(&self) -> HashAlgo {
186 self.with_pack(|objects_dir| {
187 let Ok(indexes) = read_local_pack_indexes_cached(objects_dir) else {
188 return Ok(HashAlgo::Sha1);
189 };
190 Ok(indexes
191 .first()
192 .and_then(|idx| HashAlgo::from_len(idx.hash_bytes()))
193 .unwrap_or(HashAlgo::Sha1))
194 })
195 .unwrap_or(HashAlgo::Sha1)
196 }
197
198 fn read_from_packs(&self, oid: &ObjectId) -> Result<Option<Object>> {
199 self.with_pack(
200 |objects_dir| match pack::read_object_from_packs(objects_dir, oid) {
201 Ok(obj) => Ok(Some(obj)),
202 Err(Error::ObjectNotFound(_)) => Ok(None),
203 Err(err) => Err(err),
204 },
205 )
206 }
207
208 fn read_from_packs_into(
209 &self,
210 oid: &ObjectId,
211 buf: &mut Vec<u8>,
212 ) -> Result<Option<ObjectKind>> {
213 self.with_pack(|objects_dir| {
214 match pack::read_object_from_packs_into(objects_dir, oid, buf) {
215 Ok(kind) => Ok(Some(kind)),
216 Err(Error::ObjectNotFound(_)) => Ok(None),
217 Err(err) => Err(err),
218 }
219 })
220 }
221
222 fn read_info_from_packs(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
223 self.with_pack(
224 |objects_dir| match pack::read_object_info_from_packs(objects_dir, oid) {
225 Ok(info) => Ok(Some(info)),
226 Err(Error::ObjectNotFound(_)) => Ok(None),
227 Err(err) => Err(err),
228 },
229 )
230 }
231}
232
233impl ObjectStore for PackedObjects {
234 fn hash_algo(&self) -> HashAlgo {
235 self.hash_algo_from_indexes()
236 }
237
238 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
239 self.read_from_packs(oid)
240 }
241
242 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
243 self.read_from_packs_into(oid, buf)
244 }
245
246 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
247 self.read_info_from_packs(oid)
248 }
249
250 fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
251 self.with_pack(|dir| {
252 let mut entry = pack::locate_packed_whole_object(dir, oid, true)?;
253 if matches!(entry, pack::PackedEntry::Absent) && reprepare_pack_directory_on_miss(dir)?
254 {
255 entry = pack::locate_packed_whole_object(dir, oid, true)?;
256 }
257 let pack::PackedEntry::Whole(whole) = entry else {
258 return stream_from_buffered_read(self, oid);
259 };
260 Ok(Some(ObjectStream {
261 kind: whole.kind,
262 size: whole.size,
263 reader: Box::new(PackPayloadStream::new(whole)),
264 }))
265 })
266 }
267
268 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
269 self.with_pack(|objects_dir| {
270 let indexes = read_local_pack_indexes_cached(objects_dir)?;
271 let mut seen = HashSet::new();
272 for idx in &indexes {
273 for entry in idx.iter() {
274 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
275 continue;
276 };
277 if !seen.insert(oid) {
278 continue;
279 }
280 if f(&oid).is_break() {
281 return Ok(());
282 }
283 }
284 }
285 Ok(())
286 })
287 }
288
289 fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
290 if prefix.len() > self.hash_algo().hex_len() {
291 return Ok(());
292 }
293 let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
294 self.with_pack(|objects_dir| {
295 let indexes = read_local_pack_indexes_cached(objects_dir)?;
296 let mut seen = HashSet::new();
297 let mut count = 0usize;
298 for idx in &indexes {
299 collect_prefix_from_index(idx, &prefix, limit, &mut count, out, &mut seen)?;
300 if limit != 0 && count >= limit {
301 break;
302 }
303 }
304 Ok(())
305 })
306 }
307
308 fn refresh(&self) -> Result<bool> {
309 self.with_pack(reprepare_pack_directory_on_miss)
310 }
311}
312
313fn for_each_removable_pack_object_ids(
314 objects_dir: &Path,
315 f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
316) -> Result<()> {
317 let pack_dir = objects_dir.join("pack");
318 let indexes = read_local_pack_indexes_cached(objects_dir)?;
319 let mut seen = HashSet::new();
320 for idx in indexes {
321 if idx.is_promisor || pack_index_has_keep_marker(&pack_dir, &idx.pack_path) {
322 continue;
323 }
324 let hash_bytes = idx.hash_bytes();
325 for entry in idx.iter() {
326 if entry.oid().len() != hash_bytes {
327 continue;
328 }
329 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
330 continue;
331 };
332 if !seen.insert(oid) {
333 continue;
334 }
335 if f(&oid).is_break() {
336 return Ok(());
337 }
338 }
339 }
340 Ok(())
341}
342
343fn pack_index_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
344 let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
345 return false;
346 };
347 pack_dir.join(format!("{stem}.keep")).is_file()
348}
349
350fn collect_kept_pack_object_ids(objects_dir: &Path) -> Result<HashSet<ObjectId>> {
351 let pack_dir = objects_dir.join("pack");
352 let mut kept = HashSet::new();
353 if !pack_dir.is_dir() {
354 return Ok(kept);
355 }
356 for entry in std::fs::read_dir(&pack_dir).map_err(Error::Io)? {
357 let entry = entry.map_err(Error::Io)?;
358 let path = entry.path();
359 if path.extension().is_some_and(|ext| ext == "keep") {
360 let idx_path = path.with_extension("idx");
361 if idx_path.is_file() {
362 if let Ok(oids) = pack::read_idx_object_ids(&idx_path) {
363 kept.extend(oids);
364 }
365 }
366 }
367 }
368 Ok(kept)
369}
370
371fn filtered_indexes(
372 objects_dir: &std::path::Path,
373 filter: PackFilter,
374) -> Result<Vec<Arc<PackIndex>>> {
375 let indexes = read_local_pack_indexes_cached(objects_dir)?;
376 Ok(indexes
377 .into_iter()
378 .filter(|idx| {
379 (filter.include_promisor || !idx.is_promisor) && (filter.include_cruft || !idx.is_cruft)
380 })
381 .collect())
382}
383
384fn collect_prefix_from_index(
385 idx: &PackIndex,
386 prefix: &ObjectIdPrefix,
387 limit: usize,
388 count: &mut usize,
389 out: &mut Vec<ObjectId>,
390 seen: &mut HashSet<ObjectId>,
391) -> Result<()> {
392 let lo = lower_bound_oid(idx, prefix.lowest().as_bytes());
393 let hi = upper_bound_oid(idx, prefix.highest().as_bytes());
394 for i in lo..hi {
395 let digest = idx.oid_at(i);
396 if !prefix.matches_digest(digest) {
397 continue;
398 }
399 let Ok(oid) = ObjectId::from_bytes(digest) else {
400 continue;
401 };
402 if !seen.insert(oid) {
403 continue;
404 }
405 out.push(oid);
406 *count += 1;
407 if limit != 0 && *count >= limit {
408 break;
409 }
410 }
411 Ok(())
412}
413
414fn lower_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
415 let mut lo = 0usize;
416 let mut hi = idx.len();
417 while lo < hi {
418 let mid = lo + (hi - lo) / 2;
419 if idx.oid_at(mid) < needle {
420 lo = mid + 1;
421 } else {
422 hi = mid;
423 }
424 }
425 lo
426}
427
428fn upper_bound_oid(idx: &PackIndex, needle: &[u8]) -> usize {
429 let mut lo = 0usize;
430 let mut hi = idx.len();
431 while lo < hi {
432 let mid = lo + (hi - lo) / 2;
433 if idx.oid_at(mid) <= needle {
434 lo = mid + 1;
435 } else {
436 hi = mid;
437 }
438 }
439 lo
440}
441
442pub(crate) fn touch_pack_mtime(pack_path: &std::path::Path) -> bool {
445 let now = filetime::FileTime::now();
446 if filetime::set_file_times(pack_path, now, now).is_err() {
447 return false;
448 }
449 let touched = std::time::UNIX_EPOCH
450 + std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds());
451 pack::refresh_pack_bytes_signature(pack_path, touched);
452 true
453}
454
455const STREAM_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
459
460struct PackPayloadStream {
465 whole: pack::PackedWholeObject,
466 dec: Decompress,
467 pos: usize,
468 remaining: u64,
469 stream_ended: bool,
470 resident: usize,
472}
473
474impl PackPayloadStream {
475 fn new(whole: pack::PackedWholeObject) -> Self {
476 Self {
477 pos: whole.zlib_offset,
478 remaining: whole.size,
479 whole,
480 dec: Decompress::new(true),
481 stream_ended: false,
482 resident: 0,
483 }
484 }
485
486 fn inflate(&mut self, out: &mut [u8]) -> io::Result<usize> {
488 let input = &self.whole.pack[self.pos..self.whole.data_end];
489 let before_in = self.dec.total_in();
490 let before_out = self.dec.total_out();
491 let status = self
492 .dec
493 .decompress(input, out, FlushDecompress::None)
494 .map_err(|e| corrupt_pack_stream(&e.to_string()))?;
495 let consumed = (self.dec.total_in() - before_in) as usize;
496 let produced = (self.dec.total_out() - before_out) as usize;
497 self.pos += consumed;
498 self.resident += consumed;
499 if self.resident >= STREAM_RESIDENT_PACK_BYTES {
500 self.whole.pack.release_resident();
502 self.resident = 0;
503 }
504 if status == Status::StreamEnd {
505 self.stream_ended = true;
506 } else if consumed == 0 && produced == 0 {
507 return Err(corrupt_pack_stream("zlib stream truncated"));
508 }
509 Ok(produced)
510 }
511}
512
513fn corrupt_pack_stream(detail: &str) -> io::Error {
514 io::Error::new(
515 io::ErrorKind::InvalidData,
516 format!("corrupt packed object: {detail}"),
517 )
518}
519
520impl Read for PackPayloadStream {
521 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
522 if self.remaining == 0 {
523 let mut excess = [0u8; 1];
524 while !self.stream_ended {
525 if self.inflate(&mut excess)? > 0 {
526 return Err(corrupt_pack_stream("more data than declared size"));
527 }
528 }
529 return Ok(0);
530 }
531 if buf.is_empty() {
532 return Ok(0);
533 }
534 let max = usize::try_from(self.remaining).map_or(buf.len(), |r| r.min(buf.len()));
535 loop {
536 if self.stream_ended {
537 return Err(corrupt_pack_stream("less data than declared size"));
538 }
539 let produced = self.inflate(&mut buf[..max])?;
540 if produced > 0 {
541 self.remaining -= produced as u64;
542 return Ok(produced);
543 }
544 }
545 }
546}
547
548fn stream_from_buffered_read(
549 store: &PackedObjects,
550 oid: &ObjectId,
551) -> Result<Option<ObjectStream<'static>>> {
552 let Some(obj) = store.read(oid)? else {
553 return Ok(None);
554 };
555 let size = u64::try_from(obj.data.len())
556 .map_err(|_| Error::CorruptObject(format!("object size overflow for {}", oid.to_hex())))?;
557 Ok(Some(ObjectStream {
558 kind: obj.kind,
559 size,
560 reader: Box::new(Cursor::new(obj.data)),
561 }))
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use flate2::write::ZlibEncoder;
568 use flate2::Compression;
569 use std::io::Write;
570
571 use crate::pack_map::PackData;
572
573 const TRAILER: usize = 20;
574
575 fn zlib(data: &[u8]) -> Vec<u8> {
576 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
577 enc.write_all(data).expect("deflate");
578 enc.finish().expect("deflate")
579 }
580
581 fn whole(zlib_stream: &[u8], declared_size: u64) -> pack::PackedWholeObject {
583 let mut bytes = b"PACK\0\0\0\x02\0\0\0\x01\xb0".to_vec();
584 let zlib_offset = bytes.len();
585 bytes.extend_from_slice(zlib_stream);
586 let data_end = bytes.len();
587 bytes.extend_from_slice(&[0u8; TRAILER]);
588 pack::PackedWholeObject {
589 pack: PackData::from_owned(bytes),
590 zlib_offset,
591 data_end,
592 kind: ObjectKind::Blob,
593 size: declared_size,
594 }
595 }
596
597 fn read_all(whole: pack::PackedWholeObject) -> io::Result<Vec<u8>> {
598 let mut stream = PackPayloadStream::new(whole);
599 let mut out = Vec::new();
600 let mut chunk = [0u8; 1000];
601 loop {
602 match stream.read(&mut chunk)? {
603 0 => return Ok(out),
604 n => out.extend_from_slice(&chunk[..n]),
605 }
606 }
607 }
608
609 fn data() -> Vec<u8> {
610 (0..200_000u32)
611 .flat_map(|i| (i % 251).to_le_bytes())
612 .collect()
613 }
614
615 #[test]
616 fn pack_payload_stream_yields_the_whole_payload() {
617 let data = data();
618 let out = read_all(whole(&zlib(&data), data.len() as u64)).expect("stream");
619 assert!(out == data);
620 }
621
622 #[test]
623 fn pack_payload_stream_rejects_truncated_entry() {
624 let data = data();
625 let stream = zlib(&data);
626 let err =
627 read_all(whole(&stream[..stream.len() / 2], data.len() as u64)).expect_err("truncated");
628 assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
629 }
630
631 #[test]
632 fn pack_payload_stream_rejects_size_mismatch_both_ways() {
633 let data = data();
634 let stream = zlib(&data);
635 let err = read_all(whole(&stream, data.len() as u64 + 1)).expect_err("short");
636 assert!(err.to_string().contains("less data"), "{err}");
637 let err = read_all(whole(&stream, data.len() as u64 - 1)).expect_err("long");
638 assert!(err.to_string().contains("more data"), "{err}");
639 }
640
641 #[test]
642 fn pack_payload_stream_rejects_bad_checksum() {
643 let data = data();
644 let mut stream = zlib(&data);
645 let last = stream.len() - 1;
646 stream[last] ^= 0xff;
647 let err = read_all(whole(&stream, data.len() as u64)).expect_err("adler mismatch");
648 assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
649 }
650}