This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-pack / src / objects.rs
31 kB 993 lines
1use std::collections::{HashMap, HashSet}; 2use std::error::Error; 3use std::io::Write; 4use std::path::{Path, PathBuf}; 5use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 6use std::sync::{Arc, mpsc}; 7use std::time::{Duration, Instant}; 8 9use gix::ObjectId; 10use gix::prelude::FindExt; 11use gix::progress::Discard; 12use gix_pack::data::{Version, output}; 13use knot_types::{ObjectCount, Oid}; 14 15use crate::error::{PackError, PackLimit}; 16use crate::ids::{Crc32, MaxObjectBytes, PackOffset}; 17use crate::meter::{self, PackLimits, check_depth}; 18use crate::resolve; 19 20type OidStream = Box<dyn Iterator<Item = Result<ObjectId, Box<dyn Error + Send + Sync>>> + Send>; 21 22fn odb_at(objects_dir: &Path, kind: gix::hash::Kind) -> Result<gix::odb::Handle, PackError> { 23 gix::odb::at_opts( 24 objects_dir, 25 std::iter::empty(), 26 gix::odb::store::init::Options { 27 object_hash: kind, 28 ..Default::default() 29 }, 30 ) 31 .map_err(|error| PackError::Pack(error.to_string())) 32} 33 34pub fn write_pack( 35 objects_dir: &Path, 36 oids: Vec<Oid>, 37 thin_bases: Option<&HashSet<Oid>>, 38 out: &mut dyn Write, 39 kind: gix::hash::Kind, 40) -> Result<(), PackError> { 41 let mut odb = odb_at(objects_dir, kind)?; 42 odb.prevent_pack_unload(); 43 odb.refresh_never(); 44 45 let interrupt = AtomicBool::new(false); 46 let permitted_bases: Option<HashSet<ObjectId>> = 47 thin_bases.map(|bases| bases.iter().map(|oid| oid.object_id()).collect()); 48 let oids: OidStream = Box::new(oids.into_iter().map(|oid| Ok(oid.object_id()))); 49 50 let (counts, _) = output::count::objects( 51 odb.clone(), 52 oids, 53 &Discard, 54 &interrupt, 55 output::count::objects::Options { 56 thread_limit: knot_resource::gix_thread_limit().map(knot_resource::ThreadCount::get), 57 chunk_size: 50, 58 input_object_expansion: output::count::objects::ObjectExpansion::AsIs, 59 }, 60 ) 61 .map_err(|error| PackError::Pack(error.to_string()))?; 62 63 write_counts(counts, odb, permitted_bases, out, kind) 64} 65 66pub struct ExpandedPack { 67 counts: Vec<output::Count>, 68 odb: gix::odb::Handle, 69 kind: gix::hash::Kind, 70} 71 72impl ExpandedPack { 73 pub fn len(&self) -> usize { 74 self.counts.len() 75 } 76 77 pub fn is_empty(&self) -> bool { 78 self.counts.is_empty() 79 } 80} 81 82pub fn count_expanded( 83 objects_dir: &Path, 84 roots: Vec<Oid>, 85 max_objects: ObjectCount, 86 stall: Duration, 87 kind: gix::hash::Kind, 88) -> Result<ExpandedPack, PackError> { 89 let mut odb = odb_at(objects_dir, kind)?; 90 odb.prevent_pack_unload(); 91 odb.refresh_never(); 92 93 let interrupt = Arc::new(AtomicBool::new(false)); 94 let counter = Arc::new(AtomicUsize::new(0)); 95 let progress = SharedCount(Arc::clone(&counter)); 96 let oids: OidStream = Box::new(roots.into_iter().map(|oid| Ok(oid.object_id()))); 97 let result = { 98 let _watchdog = Watchdog::arm(interrupt.clone(), Arc::clone(&counter), max_objects, stall); 99 output::count::objects( 100 Interruptible { 101 inner: odb.clone(), 102 flag: Arc::clone(&interrupt), 103 }, 104 oids, 105 &progress, 106 &interrupt, 107 output::count::objects::Options { 108 thread_limit: knot_resource::gix_thread_limit() 109 .map(knot_resource::ThreadCount::get), 110 chunk_size: 50, 111 input_object_expansion: output::count::objects::ObjectExpansion::TreeContents, 112 }, 113 ) 114 }; 115 let over_limit = counter.load(Ordering::Relaxed) > max_objects.get(); 116 let timed_out = interrupt.load(Ordering::Relaxed); 117 let counts = match result { 118 Ok((counts, _)) => counts, 119 Err(_) if over_limit => return Err(PackError::SelectionTooLarge), 120 Err(_) if timed_out => return Err(PackError::SelectionTimeout), 121 Err(error) => return Err(PackError::Pack(error.to_string())), 122 }; 123 if counts.len() > max_objects.get() { 124 return Err(PackError::SelectionTooLarge); 125 } 126 if timed_out { 127 return Err(PackError::SelectionTimeout); 128 } 129 Ok(ExpandedPack { counts, odb, kind }) 130} 131 132pub fn write_expanded(pack: ExpandedPack, out: &mut dyn Write) -> Result<(), PackError> { 133 write_counts(pack.counts, pack.odb, None, out, pack.kind) 134} 135 136fn write_counts( 137 counts: Vec<output::Count>, 138 odb: gix::odb::Handle, 139 permitted_bases: Option<HashSet<ObjectId>>, 140 out: &mut dyn Write, 141 kind: gix::hash::Kind, 142) -> Result<(), PackError> { 143 let num_entries = counts.len() as u32; 144 let allow_thin_pack = permitted_bases.is_some(); 145 let counted = output::entry::iter_from_counts( 146 counts, 147 odb.clone(), 148 Box::new(Discard), 149 output::entry::iter_from_counts::Options { 150 thread_limit: knot_resource::gix_thread_limit().map(knot_resource::ThreadCount::get), 151 mode: output::entry::iter_from_counts::Mode::PackCopyAndBaseObjects, 152 allow_thin_pack, 153 chunk_size: 50, 154 version: Version::V2, 155 }, 156 ); 157 158 let entries = gix::parallel::InOrderIter::from(counted).map( 159 move |chunk| -> Result<Vec<output::Entry>, PackError> { 160 let entries = chunk.map_err(|error| PackError::Pack(error.to_string()))?; 161 entries 162 .into_iter() 163 .map(|entry| restrict_thin_base(&odb, permitted_bases.as_ref(), entry)) 164 .collect() 165 }, 166 ); 167 168 let mut writer = 169 output::bytes::FromEntriesIter::new(entries, out, num_entries, Version::V2, kind); 170 writer 171 .try_fold((), |(), written| written.map(|_| ())) 172 .map_err(|error| PackError::Pack(error.to_string()))?; 173 Ok(()) 174} 175 176struct SharedCount(Arc<AtomicUsize>); 177 178impl gix::progress::Count for SharedCount { 179 fn set(&self, step: usize) { 180 self.0.store(step, Ordering::Relaxed); 181 } 182 183 fn step(&self) -> usize { 184 self.0.load(Ordering::Relaxed) 185 } 186 187 fn inc_by(&self, step: usize) { 188 self.0.fetch_add(step, Ordering::Relaxed); 189 } 190 191 fn counter(&self) -> Arc<AtomicUsize> { 192 Arc::clone(&self.0) 193 } 194} 195 196#[derive(Debug)] 197struct Halted; 198 199impl std::fmt::Display for Halted { 200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 201 f.write_str("object enumeration halted by selection budget") 202 } 203} 204 205impl Error for Halted {} 206 207#[derive(Clone)] 208struct Interruptible { 209 inner: gix::odb::Handle, 210 flag: Arc<AtomicBool>, 211} 212 213impl gix_pack::Find for Interruptible { 214 fn contains(&self, id: &gix::hash::oid) -> bool { 215 self.inner.contains(id) 216 } 217 218 fn try_find_cached<'a>( 219 &self, 220 id: &gix::hash::oid, 221 buffer: &'a mut Vec<u8>, 222 pack_cache: &mut dyn gix_pack::cache::DecodeEntry, 223 ) -> Result< 224 Option<(gix::objs::Data<'a>, Option<gix_pack::data::entry::Location>)>, 225 gix::objs::find::Error, 226 > { 227 if self.flag.load(Ordering::Relaxed) { 228 return Err(Box::new(Halted)); 229 } 230 self.inner.try_find_cached(id, buffer, pack_cache) 231 } 232 233 fn location_by_oid( 234 &self, 235 id: &gix::hash::oid, 236 buf: &mut Vec<u8>, 237 ) -> Option<gix_pack::data::entry::Location> { 238 self.inner.location_by_oid(id, buf) 239 } 240 241 fn pack_offsets_and_oid( 242 &self, 243 pack_id: u32, 244 ) -> Option<Vec<(gix_pack::data::Offset, gix::hash::ObjectId)>> { 245 self.inner.pack_offsets_and_oid(pack_id) 246 } 247 248 fn entry_by_location( 249 &self, 250 location: &gix_pack::data::entry::Location, 251 ) -> Option<gix_pack::find::Entry> { 252 self.inner.entry_by_location(location) 253 } 254} 255 256struct Watchdog { 257 idle: Option<mpsc::Sender<()>>, 258 watch: Option<std::thread::JoinHandle<()>>, 259} 260 261impl Watchdog { 262 fn arm( 263 flag: Arc<AtomicBool>, 264 counter: Arc<AtomicUsize>, 265 max_objects: ObjectCount, 266 stall: Duration, 267 ) -> Self { 268 let (idle, wake) = mpsc::channel::<()>(); 269 let watch = std::thread::spawn(move || { 270 let poll = Duration::from_millis(25).min(stall); 271 let mut last = counter.load(Ordering::Relaxed); 272 let mut since = Instant::now(); 273 loop { 274 let now = counter.load(Ordering::Relaxed); 275 if now != last { 276 last = now; 277 since = Instant::now(); 278 } 279 if since.elapsed() >= stall || now > max_objects.get() { 280 flag.store(true, Ordering::Relaxed); 281 return; 282 } 283 if !matches!( 284 wake.recv_timeout(poll), 285 Err(mpsc::RecvTimeoutError::Timeout) 286 ) { 287 return; 288 } 289 } 290 }); 291 Self { 292 idle: Some(idle), 293 watch: Some(watch), 294 } 295 } 296} 297 298impl Drop for Watchdog { 299 fn drop(&mut self) { 300 self.idle.take(); 301 if let Some(watch) = self.watch.take() { 302 let _ = watch.join(); 303 } 304 } 305} 306 307fn restrict_thin_base( 308 odb: &gix::odb::Handle, 309 permitted: Option<&HashSet<ObjectId>>, 310 entry: output::Entry, 311) -> Result<output::Entry, PackError> { 312 let base = match &entry.kind { 313 output::entry::Kind::DeltaOid { id } => *id, 314 _ => return Ok(entry), 315 }; 316 if permitted.is_some_and(|bases| bases.contains(&base)) { 317 return Ok(entry); 318 } 319 let mut buf = Vec::new(); 320 let object = odb 321 .find(&entry.id, &mut buf) 322 .map_err(|error| PackError::Pack(error.to_string()))?; 323 let count = output::Count::from_data(entry.id, None); 324 output::Entry::from_data(&count, &object).map_err(|error| PackError::Pack(error.to_string())) 325} 326 327pub fn index_pack( 328 objects_dir: &Path, 329 pack: &[u8], 330 limits: &PackLimits, 331 kind: gix::hash::Kind, 332) -> Result<(), PackError> { 333 if pack.is_empty() { 334 return Ok(()); 335 } 336 if !pack.starts_with(b"PACK") { 337 return Err(PackError::Pack( 338 "packfile is missing its PACK signature".to_string(), 339 )); 340 } 341 std::fs::create_dir_all(objects_dir)?; 342 let mut tmp = tempfile::NamedTempFile::new_in(objects_dir)?; 343 tmp.write_all(pack)?; 344 tmp.flush()?; 345 let file = gix_pack::data::File::at(tmp.path(), kind) 346 .map_err(|error| PackError::Pack(error.to_string()))?; 347 index_pack_bounded(objects_dir, &file, limits, kind) 348} 349 350pub(crate) fn index_pack_bounded( 351 objects_dir: &Path, 352 pack: &gix_pack::data::File, 353 limits: &PackLimits, 354 kind: gix::hash::Kind, 355) -> Result<(), PackError> { 356 if pack.data_len() < 12 + kind.len_in_bytes() { 357 return Ok(()); 358 } 359 let thin = meter::meter_file(pack, limits, kind)?; 360 361 let pack_dir = objects_dir.join("pack"); 362 std::fs::create_dir_all(&pack_dir)?; 363 364 inline_and_index(objects_dir, pack, &pack_dir, limits, kind, thin).or_else(|_| { 365 let bytes = std::fs::read(pack.path())?; 366 resolve::resolve(objects_dir, &bytes, limits, kind) 367 }) 368} 369 370fn inline_and_index( 371 objects_dir: &Path, 372 pack: &gix_pack::data::File, 373 pack_dir: &Path, 374 limits: &PackLimits, 375 kind: gix::hash::Kind, 376 thin: bool, 377) -> Result<(), PackError> { 378 let (pack_path, pack_hash) = if thin { 379 let pack_hash = inline_thin_bases(objects_dir, pack, pack_dir, kind)?; 380 ( 381 pack_dir.join(format!("pack-{}.pack", pack_hash.to_hex())), 382 pack_hash, 383 ) 384 } else { 385 let pack_hash = pack.checksum(); 386 let pack_path = pack_dir.join(format!("pack-{}.pack", pack_hash.to_hex())); 387 let source = pack.path().to_owned(); 388 persist_atomic(&pack_path, |writer| { 389 std::io::copy(&mut std::fs::File::open(&source)?, writer)?; 390 Ok(()) 391 })?; 392 (pack_path, pack_hash) 393 }; 394 let pack_cleanup = RemoveOnDrop::arm(&pack_path); 395 396 let nodes = scan_offsets(&pack_path, kind)?; 397 let spool = spool_pack( 398 &pack_path, 399 nodes, 400 kind, 401 knot_resource::ingest_base_budget(), 402 limits.max_object_bytes, 403 None, 404 )?; 405 let (_present, idx_cleanup) = persist_index(pack_dir, &pack_hash, spool, kind)?; 406 pack_cleanup.disarm(); 407 idx_cleanup.disarm(); 408 Ok(()) 409} 410 411fn inline_thin_bases( 412 objects_dir: &Path, 413 pack: &gix_pack::data::File, 414 pack_dir: &Path, 415 kind: gix::hash::Kind, 416) -> Result<ObjectId, PackError> { 417 let odb = odb_at(objects_dir, kind)?; 418 let staged = tempfile::NamedTempFile::new_in(pack_dir)?; 419 let writer = std::fs::OpenOptions::new() 420 .read(true) 421 .write(true) 422 .open(staged.path())?; 423 let reader = std::io::BufReader::new(std::fs::File::open(pack.path())?); 424 let entries = gix_pack::data::input::BytesToEntriesIter::new_from_header( 425 reader, 426 gix_pack::data::input::Mode::Verify, 427 gix_pack::data::input::EntryDataMode::KeepAndCrc32, 428 kind, 429 ) 430 .map_err(|error| PackError::Pack(error.to_string()))?; 431 let version = entries.version(); 432 let lookup = gix_pack::data::input::LookupRefDeltaObjectsIter::new(entries, odb); 433 let mut sink = gix_pack::data::input::EntriesToBytesIter::new(lookup, writer, version, kind); 434 sink.try_for_each(|entry| { 435 entry 436 .map(|_| ()) 437 .map_err(|error| PackError::Pack(error.to_string())) 438 })?; 439 let pack_hash = sink 440 .digest() 441 .ok_or_else(|| PackError::Pack("resolved pack has no trailer".to_string()))?; 442 drop(sink); 443 444 let pack_path = pack_dir.join(format!("pack-{}.pack", pack_hash.to_hex())); 445 staged 446 .persist(&pack_path) 447 .map_err(|error| PackError::Pack(error.to_string()))?; 448 Ok(pack_hash) 449} 450 451fn scan_offsets(pack_path: &Path, kind: gix::hash::Kind) -> Result<Vec<Node>, PackError> { 452 let reader = std::io::BufReader::new(std::fs::File::open(pack_path)?); 453 let mut entries = gix_pack::data::input::BytesToEntriesIter::new_from_header( 454 reader, 455 gix_pack::data::input::Mode::Verify, 456 gix_pack::data::input::EntryDataMode::Crc32, 457 kind, 458 ) 459 .map_err(|error| PackError::Pack(error.to_string()))?; 460 let mut nodes: Vec<Node> = Vec::new(); 461 entries.try_for_each(|entry| -> Result<(), PackError> { 462 let entry = entry.map_err(|error| PackError::Pack(error.to_string()))?; 463 nodes.push(Node { 464 offset: PackOffset::new(entry.pack_offset), 465 crc32: Crc32::new( 466 entry 467 .crc32 468 .ok_or_else(|| PackError::Pack("entry crc32 not computed".to_string()))?, 469 ), 470 }); 471 Ok(()) 472 })?; 473 Ok(nodes) 474} 475 476pub(crate) struct PresentSet(gix_pack::index::File); 477 478impl PresentSet { 479 fn open(idx_path: &Path, kind: gix::hash::Kind) -> Result<Self, PackError> { 480 gix_pack::index::File::at(idx_path, kind) 481 .map(Self) 482 .map_err(|error| PackError::Pack(error.to_string())) 483 } 484 485 pub(crate) fn contains(&self, oid: &Oid) -> bool { 486 self.0.lookup(oid.object_id()).is_some() 487 } 488 489 fn sorted_offsets(&self) -> Vec<PackOffset> { 490 self.0 491 .sorted_offsets() 492 .into_iter() 493 .map(PackOffset::new) 494 .collect() 495 } 496} 497 498pub(crate) struct FreshClosure { 499 pub self_contained: bool, 500 pub present: PresentSet, 501} 502 503#[derive(Clone)] 504struct Node { 505 offset: PackOffset, 506 crc32: Crc32, 507} 508 509const PRESENT: u8 = 1; 510const REFERENCED: u8 = 2; 511 512struct Connectivity { 513 objects: scc::HashMap<Oid, u8>, 514 empty_tree: Oid, 515} 516 517impl Connectivity { 518 fn mark_present(&self, oid: Oid) { 519 self.objects 520 .entry_sync(oid) 521 .and_modify(|flags| *flags |= PRESENT) 522 .or_insert(PRESENT); 523 } 524 525 fn check(&self, reference: Oid) { 526 if reference != self.empty_tree { 527 self.objects 528 .entry_sync(reference) 529 .and_modify(|flags| *flags |= REFERENCED) 530 .or_insert(REFERENCED); 531 } 532 } 533 534 fn self_contained(&self) -> bool { 535 self.objects 536 .any_sync(|_, flags| *flags & REFERENCED != 0 && *flags & PRESENT == 0) 537 .is_none() 538 } 539} 540 541enum Scan { 542 Thin, 543 Limit(PackLimit), 544 Pack(String), 545} 546 547const INGEST_BYTES_PER_OBJECT: u64 = 128; 548 549fn fits_in_memory(num_objects: ObjectCount) -> bool { 550 knot_resource::ingest_admits(knot_resource::PayloadBytes::new( 551 (num_objects.get() as u64).saturating_mul(INGEST_BYTES_PER_OBJECT), 552 )) 553} 554 555pub(crate) fn admit_ingest( 556 pack: &gix_pack::data::File, 557 kind: gix::hash::Kind, 558) -> Result<(), PackError> { 559 if pack.data_len() < 12 + kind.len_in_bytes() { 560 return Ok(()); 561 } 562 if fits_in_memory(ObjectCount::from(pack.num_objects())) { 563 Ok(()) 564 } else { 565 Err(PackError::InsufficientMemory) 566 } 567} 568 569struct RemoveOnDrop(Option<PathBuf>); 570 571impl RemoveOnDrop { 572 fn arm(path: &Path) -> Self { 573 Self(Some(path.to_owned())) 574 } 575 576 fn disarm(mut self) { 577 self.0 = None; 578 } 579} 580 581impl Drop for RemoveOnDrop { 582 fn drop(&mut self) { 583 if let Some(path) = self.0.take() { 584 let _ = std::fs::remove_file(path); 585 } 586 } 587} 588 589pub(crate) fn ingest_and_close( 590 objects_dir: &Path, 591 pack: &gix_pack::data::File, 592 limits: &PackLimits, 593 kind: gix::hash::Kind, 594 base_budget: Option<usize>, 595 force_external: bool, 596) -> Result<Option<FreshClosure>, PackError> { 597 let hash_len = kind.len_in_bytes(); 598 if pack.data_len() < 12 + hash_len { 599 return Ok(None); 600 } 601 if ObjectCount::from(pack.num_objects()) > limits.max_objects { 602 return Err(PackError::LimitExceeded(PackLimit::Objects)); 603 } 604 605 let reader = std::io::BufReader::new(std::fs::File::open(pack.path())?); 606 let mut entries = gix_pack::data::input::BytesToEntriesIter::new_from_header( 607 reader, 608 gix_pack::data::input::Mode::Verify, 609 gix_pack::data::input::EntryDataMode::Crc32, 610 kind, 611 ) 612 .map_err(|error| PackError::Pack(error.to_string()))?; 613 let mut nodes: Vec<Node> = Vec::with_capacity(pack.num_objects() as usize); 614 let mut base_of: HashMap<PackOffset, PackOffset> = HashMap::new(); 615 let mut total_decompressed: u64 = 0; 616 let mut max_object: u64 = 0; 617 let scan = entries.try_for_each(|entry| -> Result<(), Scan> { 618 let entry = entry.map_err(|error| Scan::Pack(error.to_string()))?; 619 match entry.header { 620 gix_pack::data::entry::Header::RefDelta { .. } => return Err(Scan::Thin), 621 gix_pack::data::entry::Header::OfsDelta { base_distance } => { 622 let pack_offset = PackOffset::new(entry.pack_offset); 623 let base = pack_offset 624 .checked_sub_distance(base_distance) 625 .ok_or_else(|| Scan::Pack("ofs-delta base out of range".to_string()))?; 626 base_of.insert(pack_offset, base); 627 } 628 _ => {} 629 } 630 if limits.max_object_bytes.exceeded_by(entry.decompressed_size) { 631 return Err(Scan::Limit(PackLimit::ObjectBytes)); 632 } 633 max_object = max_object.max(entry.decompressed_size); 634 total_decompressed = total_decompressed 635 .checked_add(entry.decompressed_size) 636 .ok_or_else(|| Scan::Pack("decompressed size overflow".to_string()))?; 637 if limits.max_total_bytes.exceeded_by(total_decompressed) { 638 return Err(Scan::Limit(PackLimit::TotalBytes)); 639 } 640 nodes.push(Node { 641 offset: PackOffset::new(entry.pack_offset), 642 crc32: Crc32::new( 643 entry 644 .crc32 645 .ok_or_else(|| Scan::Pack("entry crc32 not computed".to_string()))?, 646 ), 647 }); 648 Ok(()) 649 }); 650 match scan { 651 Ok(()) => {} 652 Err(Scan::Thin) => return Ok(None), 653 Err(Scan::Limit(limit)) => return Err(PackError::LimitExceeded(limit)), 654 Err(Scan::Pack(message)) => return Err(PackError::Pack(message)), 655 } 656 check_depth(&base_of, limits.max_delta_depth)?; 657 drop(base_of); 658 let entry_count = nodes.len(); 659 660 let base_cache = knot_resource::ingest_base_budget().unwrap_or(0) as u64; 661 let working_set = (entry_count as u64) 662 .saturating_mul(INGEST_BYTES_PER_OBJECT) 663 .saturating_add(base_cache) 664 .saturating_add(max_object); 665 if !knot_resource::ingest_admits_churn( 666 knot_resource::WorkingSetBytes::new(working_set), 667 knot_resource::ChurnBytes::new(total_decompressed), 668 ) { 669 return Err(PackError::InsufficientMemory); 670 } 671 672 let pack_hash = pack.checksum(); 673 let pack_dir = objects_dir.join("pack"); 674 std::fs::create_dir_all(&pack_dir)?; 675 let stem = format!("pack-{}", pack_hash.to_hex()); 676 let pack_path = pack_dir.join(format!("{stem}.pack")); 677 let source = pack.path().to_owned(); 678 persist_atomic(&pack_path, |writer| { 679 let mut reader = std::fs::File::open(&source)?; 680 std::io::copy(&mut reader, writer)?; 681 Ok(()) 682 })?; 683 let pack_cleanup = RemoveOnDrop::arm(&pack_path); 684 685 let externalize = force_external 686 || knot_resource::externalize_connectivity(knot_resource::ConnectivityObjects::new( 687 entry_count as u64, 688 )); 689 let connectivity = (!externalize).then(|| Connectivity { 690 objects: scc::HashMap::with_capacity(entry_count), 691 empty_tree: Oid::from(ObjectId::empty_tree(kind)), 692 }); 693 let spool = spool_pack( 694 &pack_path, 695 nodes, 696 kind, 697 base_budget, 698 limits.max_object_bytes, 699 connectivity.as_ref(), 700 )?; 701 let self_contained_in_ram = connectivity.as_ref().map(Connectivity::self_contained); 702 drop(connectivity); 703 let (present, idx_cleanup) = persist_index(&pack_dir, &pack_hash, spool, kind)?; 704 705 let self_contained = match self_contained_in_ram { 706 Some(value) => value, 707 None => verify_connectivity_retraverse( 708 &pack_path, 709 &present, 710 kind, 711 base_budget, 712 limits.max_object_bytes, 713 )?, 714 }; 715 716 pack_cleanup.disarm(); 717 idx_cleanup.disarm(); 718 Ok(Some(FreshClosure { 719 self_contained, 720 present, 721 })) 722} 723 724fn spool_pack( 725 pack_path: &Path, 726 nodes: Vec<Node>, 727 kind: gix::hash::Kind, 728 base_budget: Option<usize>, 729 max_object_bytes: MaxObjectBytes, 730 connectivity: Option<&Connectivity>, 731) -> Result<crate::idxwrite::Spool, PackError> { 732 let interrupt = AtomicBool::new(false); 733 let tree = gix_pack::cache::delta::Tree::from_offsets_in_pack( 734 pack_path, 735 nodes.into_iter(), 736 &|node: &Node| node.offset.get(), 737 &|_id| None, 738 &mut Discard, 739 &interrupt, 740 kind, 741 ) 742 .map_err(|error| PackError::Pack(error.to_string()))?; 743 let stored = gix_pack::data::File::at(pack_path, kind) 744 .map_err(|error| PackError::Pack(error.to_string()))?; 745 let spool = crate::idxwrite::Spool::new(kind); 746 run_ingest_traverse( 747 tree, 748 &stored, 749 &interrupt, 750 base_budget, 751 max_object_bytes, 752 kind, 753 |node: &mut Node, _progress, context| harvest(node, context, kind, connectivity, &spool), 754 )?; 755 Ok(spool) 756} 757 758fn persist_index( 759 pack_dir: &Path, 760 pack_hash: &ObjectId, 761 spool: crate::idxwrite::Spool, 762 kind: gix::hash::Kind, 763) -> Result<(PresentSet, RemoveOnDrop), PackError> { 764 let idx_path = pack_dir.join(format!("pack-{}.idx", pack_hash.to_hex())); 765 persist_atomic(&idx_path, |writer| { 766 crate::idxwrite::write_v2_index(writer, &spool, pack_hash, kind).map(|_| ()) 767 })?; 768 let idx_cleanup = RemoveOnDrop::arm(&idx_path); 769 drop(spool); 770 let present = PresentSet::open(&idx_path, kind)?; 771 Ok((present, idx_cleanup)) 772} 773 774// he wishes he was on the farm already 775fn harvest( 776 node: &Node, 777 context: gix_pack::cache::delta::traverse::Context<'_>, 778 kind: gix::hash::Kind, 779 connectivity: Option<&Connectivity>, 780 spool: &crate::idxwrite::Spool, 781) -> Result<(), PackError> { 782 let id = gix::objs::compute_hash(kind, context.object_kind, context.decompressed) 783 .map_err(|error| PackError::Pack(error.to_string()))?; 784 spool.push(id, node.crc32, node.offset)?; 785 if let Some(connectivity) = connectivity { 786 connectivity.mark_present(Oid::from(id)); 787 parse_references( 788 context.object_kind, 789 context.decompressed, 790 kind, 791 &mut |reference| connectivity.check(reference), 792 )?; 793 } 794 Ok(()) 795} 796 797fn parse_references( 798 object_kind: gix::object::Kind, 799 bytes: &[u8], 800 kind: gix::hash::Kind, 801 check: &mut dyn FnMut(Oid), 802) -> Result<(), PackError> { 803 match object_kind { 804 gix::object::Kind::Blob => Ok(()), 805 gix::object::Kind::Tree => harvest_tree(bytes, kind, check), 806 gix::object::Kind::Commit => harvest_commit(bytes, kind, check), 807 gix::object::Kind::Tag => harvest_tag(bytes, kind, check), 808 } 809} 810 811fn harvest_tree( 812 bytes: &[u8], 813 kind: gix::hash::Kind, 814 check: &mut dyn FnMut(Oid), 815) -> Result<(), PackError> { 816 gix::objs::TreeRefIter::from_bytes(bytes, kind).try_for_each(|entry| { 817 let entry = entry.map_err(|error| PackError::Pack(error.to_string()))?; 818 if !entry.mode.is_commit() { 819 check(Oid::from(entry.oid.to_owned())); 820 } 821 Ok(()) 822 }) 823} 824 825fn harvest_commit( 826 bytes: &[u8], 827 kind: gix::hash::Kind, 828 check: &mut dyn FnMut(Oid), 829) -> Result<(), PackError> { 830 let mut iter = gix::objs::CommitRefIter::from_bytes(bytes, kind); 831 let tree = iter 832 .tree_id() 833 .map_err(|error| PackError::Pack(error.to_string()))?; 834 std::iter::once(tree) 835 .chain(iter.parent_ids()) 836 .for_each(|oid| check(Oid::from(oid))); 837 Ok(()) 838} 839 840fn harvest_tag( 841 bytes: &[u8], 842 kind: gix::hash::Kind, 843 check: &mut dyn FnMut(Oid), 844) -> Result<(), PackError> { 845 let target = gix::objs::TagRefIter::from_bytes(bytes, kind) 846 .target_id() 847 .map_err(|error| PackError::Pack(error.to_string()))?; 848 check(Oid::from(target)); 849 Ok(()) 850} 851 852fn verify_connectivity_retraverse( 853 pack_path: &Path, 854 present: &PresentSet, 855 kind: gix::hash::Kind, 856 base_budget: Option<usize>, 857 max_object_bytes: MaxObjectBytes, 858) -> Result<bool, PackError> { 859 let interrupt = AtomicBool::new(false); 860 let tree = gix_pack::cache::delta::Tree::from_offsets_in_pack( 861 pack_path, 862 present.sorted_offsets().into_iter(), 863 &|offset: &PackOffset| offset.get(), 864 &|_id| None, 865 &mut Discard, 866 &interrupt, 867 kind, 868 ) 869 .map_err(|error| PackError::Pack(error.to_string()))?; 870 let stored = gix_pack::data::File::at(pack_path, kind) 871 .map_err(|error| PackError::Pack(error.to_string()))?; 872 let empty_tree = Oid::from(ObjectId::empty_tree(kind)); 873 let missing = AtomicBool::new(false); 874 run_ingest_traverse( 875 tree, 876 &stored, 877 &interrupt, 878 base_budget, 879 max_object_bytes, 880 kind, 881 |_offset: &mut PackOffset, _progress, context| { 882 parse_references( 883 context.object_kind, 884 context.decompressed, 885 kind, 886 &mut |reference| { 887 if reference != empty_tree && !present.contains(&reference) { 888 missing.store(true, Ordering::Relaxed); 889 } 890 }, 891 ) 892 }, 893 )?; 894 Ok(!missing.load(Ordering::Relaxed)) 895} 896 897fn run_ingest_traverse<T, H>( 898 tree: gix_pack::cache::delta::Tree<T>, 899 stored: &gix_pack::data::File, 900 interrupt: &AtomicBool, 901 base_budget: Option<usize>, 902 max_object_bytes: MaxObjectBytes, 903 kind: gix::hash::Kind, 904 harvest: H, 905) -> Result<(), PackError> 906where 907 T: Send + Sync + Clone, 908 H: FnMut( 909 &mut T, 910 &dyn gix::progress::Progress, 911 gix_pack::cache::delta::traverse::Context<'_>, 912 ) -> Result<(), PackError> 913 + Send 914 + Clone, 915{ 916 let base_spill = match base_budget { 917 Some(budget) => Some(Arc::new(gix_pack::cache::delta::traverse::BaseSpill::new( 918 tempfile::tempfile()?, 919 budget, 920 ))), 921 None => None, 922 }; 923 tree.traverse( 924 |range: gix_pack::data::EntryRange, source: &gix_pack::data::File, buf: &mut Vec<u8>| { 925 source.read_into(range, buf) 926 }, 927 stored, 928 stored.pack_end() as u64, 929 harvest, 930 gix_pack::cache::delta::traverse::Options { 931 object_progress: Box::new(Discard), 932 size_progress: &mut Discard, 933 thread_limit: Some(knot_resource::ingest_thread_limit()), 934 should_interrupt: interrupt, 935 object_hash: kind, 936 base_spill, 937 collect_items: false, 938 max_object_bytes: Some(max_object_bytes.get()), 939 }, 940 ) 941 .map(|_| ()) 942 .map_err(|error| PackError::Pack(error.to_string())) 943} 944 945fn persist_atomic( 946 path: &Path, 947 write: impl FnOnce(&mut dyn Write) -> Result<(), PackError>, 948) -> Result<(), PackError> { 949 let dir = path 950 .parent() 951 .ok_or_else(|| PackError::Pack("object path has no parent".to_string()))?; 952 let mut tmp = tempfile::NamedTempFile::new_in(dir)?; 953 let mut writer = std::io::BufWriter::new(tmp.as_file_mut()); 954 write(&mut writer)?; 955 writer.flush()?; 956 drop(writer); 957 tmp.as_file().sync_all()?; 958 tmp.persist(path) 959 .map_err(|error| PackError::Pack(error.to_string()))?; 960 std::fs::File::open(dir)?.sync_all()?; 961 Ok(()) 962} 963 964#[cfg(test)] 965mod tests { 966 use gix_pack::Find; 967 968 use super::*; 969 970 #[test] 971 fn interruptible_find_errors_once_the_flag_is_set() { 972 let dir = tempfile::tempdir().unwrap(); 973 let flag = Arc::new(AtomicBool::new(false)); 974 let find = Interruptible { 975 inner: gix::odb::at(dir.path()).unwrap(), 976 flag: Arc::clone(&flag), 977 }; 978 let absent = Oid::null().object_id(); 979 let mut buf = Vec::new(); 980 981 assert!( 982 find.try_find(&absent, &mut buf).unwrap().is_none(), 983 "before budget trips, lookup of an absent object is a plain miss" 984 ); 985 986 flag.store(true, Ordering::Relaxed); 987 assert!( 988 find.try_find(&absent, &mut buf).is_err(), 989 "once tripped, every decode errors, which is what aborts a breadthfirst tree walk \ 990 mid-closure instead of waiting for the next commit-root boundary" 991 ); 992 } 993}