High-performance implementation of plcbundle written in Rust
at main 810 lines 28 kB view raw
1use super::progress::ProgressBar; 2use super::utils::{HasGlobalFlags, format_bytes, format_bytes_per_sec, format_number}; 3use anyhow::{Result, bail}; 4use clap::Args; 5use plcbundle::{BundleManager, VerifyResult, VerifySpec}; 6use std::path::PathBuf; 7use std::time::Instant; 8 9#[derive(Args)] 10#[command( 11 about = "Verify bundle integrity and chain", 12 long_about = "Validates the cryptographic integrity of bundles and ensures the chain 13of bundles is properly linked. This is essential for verifying that your 14repository hasn't been corrupted or tampered with. 15 16Verification operates in three modes: 17 • fast - Only check metadata frame (fastest, least thorough) 18 • normal - Verify compressed hash (default, balanced) 19 • full - Verify compressed + content hash (slowest, most thorough) 20 21Fast mode is useful for quick checks, while full mode provides complete 22assurance that bundle contents match their cryptographic commitments. 23 24When verifying the entire chain, the command performs a two-pass validation: 25first verifying all bundle hashes in parallel, then sequentially checking 26that each bundle correctly references its parent's hash. This ensures both 27individual bundle integrity and chain continuity. 28 29Use --bundles to verify specific bundles or ranges, or omit it to verify 30the entire repository chain.", 31 help_template = crate::clap_help!( 32 examples: " # Verify entire chain\n \ 33 {bin} verify\n \ 34 {bin} verify --chain\n\n \ 35 # Verify specific bundle\n \ 36 {bin} verify --bundles 42\n\n \ 37 # Verify range of bundles\n \ 38 {bin} verify --bundles 1-100\n\n \ 39 # Verify multiple ranges\n \ 40 {bin} verify --bundles 1-10,20-30\n\n \ 41 # Fast verification (metadata only)\n \ 42 {bin} verify --fast\n\n \ 43 # Full verification (content hash)\n \ 44 {bin} verify --full\n\n \ 45 # Parallel verification (faster for ranges)\n \ 46 {bin} verify --bundles 1-1000 -j 8" 47 ) 48)] 49pub struct VerifyCommand { 50 /// Bundle range to verify (e.g., "42", "1-100", or "1-10,20-30") 51 #[arg(long)] 52 pub bundles: Option<String>, 53 54 /// Verify entire chain (default) 55 #[arg(short, long)] 56 pub chain: bool, 57 58 /// Full verification (includes content hash check) 59 #[arg(long)] 60 pub full: bool, 61 62 /// Fast verification (only check metadata frame, skip hash calculations) 63 #[arg(long)] 64 pub fast: bool, 65 66 /// Number of threads to use (0 = auto-detect) 67 #[arg(short = 'j', long, default_value = "0")] 68 pub threads: usize, 69} 70 71impl HasGlobalFlags for VerifyCommand { 72 fn verbose(&self) -> bool { 73 false 74 } 75 fn quiet(&self) -> bool { 76 false 77 } 78} 79 80pub fn run(cmd: VerifyCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 81 let manager = super::utils::create_manager_from_cmd(dir.clone(), &cmd, false)?; 82 83 // Determine number of threads 84 let num_threads = if cmd.threads == 0 { 85 std::thread::available_parallelism() 86 .map(|n| n.get()) 87 .unwrap_or(4) 88 } else { 89 cmd.threads 90 }; 91 92 // Show thread count in debug/verbose mode 93 if global_verbose { 94 eprintln!("[DEBUG] Using {} thread(s) for verification", num_threads); 95 } 96 97 if !global_verbose { 98 eprintln!( 99 "\n📁 Working in: {}\n", 100 super::utils::display_path(&dir).display() 101 ); 102 } 103 104 // Determine what to verify 105 if let Some(bundles_str) = cmd.bundles { 106 let last_bundle = manager.get_last_bundle(); 107 let bundle_nums = super::utils::parse_bundle_spec(Some(bundles_str), last_bundle)?; 108 109 if bundle_nums.len() == 1 { 110 verify_single_bundle(&manager, bundle_nums[0], global_verbose, cmd.full, cmd.fast)?; 111 } else { 112 // For multiple bundles, verify as range 113 let start = bundle_nums[0]; 114 let end = bundle_nums[bundle_nums.len() - 1]; 115 verify_range( 116 &manager, 117 start, 118 end, 119 global_verbose, 120 cmd.full, 121 cmd.fast, 122 num_threads, 123 )?; 124 } 125 } else { 126 // Default: verify entire chain 127 verify_chain(&manager, global_verbose, cmd.full, cmd.fast, num_threads)?; 128 } 129 130 Ok(()) 131} 132 133fn verify_single_bundle( 134 manager: &BundleManager, 135 bundle_num: u32, 136 verbose: bool, 137 full: bool, 138 fast: bool, 139) -> Result<()> { 140 // Print verification mode at the beginning 141 eprintln!("\n🔍 Verification Mode:"); 142 if fast { 143 eprintln!(" ⚡ FAST (metadata frame only)"); 144 eprintln!(" ℹ️ Use without --fast for normal verification (compressed hash)"); 145 eprintln!(" ℹ️ Use --full for complete verification (compressed + content hash)"); 146 } else if full { 147 eprintln!(" 🔐 FULL (compressed hash + content hash)"); 148 eprintln!(" ℹ️ Use without --full for normal verification (compressed hash only)"); 149 eprintln!(" ℹ️ Use --fast for fast verification (metadata frame only)"); 150 } else { 151 eprintln!(" ✓ NORMAL (compressed hash only)"); 152 eprintln!(" ℹ️ Use --full for complete verification (compressed + content hash)"); 153 eprintln!(" ℹ️ Use --fast for fast verification (metadata frame only)"); 154 } 155 eprintln!(); 156 157 eprintln!("🔬 Verifying bundle {}...", bundle_num); 158 159 let start = Instant::now(); 160 // For single bundle, check content hash if --full flag is set 161 let spec = VerifySpec { 162 check_hash: !fast, // Skip hash check in fast mode 163 check_content_hash: full && !fast, // Skip content hash in fast mode 164 check_operations: full && !fast, // Skip operation count in fast mode 165 fast, 166 }; 167 let result = manager.verify_bundle(bundle_num, spec)?; 168 let elapsed = start.elapsed(); 169 170 if result.valid { 171 eprintln!("✅ Bundle {} is valid ({:?})", bundle_num, elapsed); 172 173 // Show what was verified 174 let mut verified_items = Vec::new(); 175 if fast { 176 verified_items.push("metadata frame"); 177 } else { 178 verified_items.push("compressed hash"); 179 if full { 180 verified_items.push("content hash"); 181 } 182 } 183 eprintln!(" ✓ Verified: {}", verified_items.join(", ")); 184 185 if fast { 186 eprintln!("\nℹ️ Note: This was a fast verification (metadata frame only)."); 187 eprintln!(" Use without --fast for normal verification (compressed hash)"); 188 eprintln!(" Use --full for complete verification (compressed + content hash)"); 189 } else if !full { 190 eprintln!("\n⚠️ Note: This was a partial verification (compressed hash only)."); 191 eprintln!(" Use --full for complete verification (compressed + content hash)"); 192 eprintln!(" Use --fast for fast verification (metadata frame only)"); 193 } 194 195 if verbose { 196 eprintln!("\nDetails:"); 197 eprintln!( 198 " Errors: {}", 199 if result.errors.is_empty() { 200 "none" 201 } else { 202 "yes" 203 } 204 ); 205 if !result.errors.is_empty() { 206 for err in &result.errors { 207 eprintln!(" - {}", err); 208 } 209 } 210 eprintln!(" Verification time: {:?}", elapsed); 211 } 212 Ok(()) 213 } else { 214 eprintln!("❌ Bundle {} is invalid ({:?})", bundle_num, elapsed); 215 if !result.errors.is_empty() { 216 eprintln!("\n⚠️ Errors:"); 217 for err in &result.errors { 218 eprintln!("{}", err); 219 } 220 } 221 bail!("bundle verification failed") 222 } 223} 224 225fn verify_chain( 226 manager: &BundleManager, 227 verbose: bool, 228 full: bool, 229 fast: bool, 230 num_threads: usize, 231) -> Result<()> { 232 if super::utils::is_repository_empty(manager) { 233 eprintln!("ℹ️ No bundles to verify"); 234 return Ok(()); 235 } 236 237 // Get all bundle metadata 238 let bundles = super::utils::get_all_bundle_metadata(manager); 239 240 if bundles.is_empty() { 241 eprintln!("ℹ️ No bundles to verify"); 242 return Ok(()); 243 } 244 245 // Print verification mode at the beginning 246 eprintln!("\n🔍 Verification Mode:"); 247 if fast { 248 eprintln!(" ⚡ FAST (metadata frame only)"); 249 eprintln!(" ℹ️ Use without --fast for normal verification (compressed hash)"); 250 eprintln!(" ℹ️ Use --full for complete verification (compressed + content hash)"); 251 } else if full { 252 eprintln!(" 🔐 FULL (compressed hash + content hash)"); 253 eprintln!(" ℹ️ Use without --full for normal verification (compressed hash only)"); 254 eprintln!(" ℹ️ Use --fast for fast verification (metadata frame only)"); 255 } else { 256 eprintln!(" ✓ NORMAL (compressed hash only)"); 257 eprintln!(" ℹ️ Use --full for complete verification (compressed + content hash)"); 258 eprintln!(" ℹ️ Use --fast for fast verification (metadata frame only)"); 259 } 260 eprintln!(); 261 262 // Print root hash (first bundle) and head hash (latest) at start 263 eprintln!("🔗 Chain Information:"); 264 eprintln!( 265 " Root: {} (bundle {})", 266 bundles[0].hash, bundles[0].bundle_number 267 ); 268 eprintln!( 269 " Head: {} (bundle {})", 270 bundles[bundles.len() - 1].hash, 271 bundles[bundles.len() - 1].bundle_number 272 ); 273 eprintln!(" Total: {} bundles", format_number(bundles.len() as u64)); 274 eprintln!(); 275 276 eprintln!( 277 "🔬 Verifying chain of {} bundles...\n", 278 format_number(bundles.len() as u64) 279 ); 280 281 let start = Instant::now(); 282 283 // Two-pass parallel verification: 284 // Pass 1: Verify all bundle hashes in parallel 285 // Pass 2: Verify chain links sequentially (needs previous results) 286 287 let spec = VerifySpec { 288 check_hash: !fast, // Skip hash check in fast mode 289 check_content_hash: full && !fast, // Skip content hash in fast mode 290 check_operations: false, 291 fast, 292 }; 293 294 // Calculate total uncompressed size for progress tracking 295 let total_uncompressed_size: u64 = bundles.iter().map(|b| b.uncompressed_size).sum(); 296 297 // Pass 1: Parallel bundle hash verification 298 eprintln!("📦 Pass 1: Verifying bundle hashes..."); 299 // Always show progress bar (it will detect if TTY and show appropriate format) 300 let progress = Some(ProgressBar::with_bytes( 301 bundles.len(), 302 total_uncompressed_size, 303 )); 304 305 use std::sync::Arc; 306 use std::sync::mpsc; 307 use std::thread; 308 309 let (job_tx, job_rx) = mpsc::channel(); 310 let (result_tx, result_rx) = mpsc::channel(); 311 let manager_clone = manager.clone_for_arc(); 312 let job_rx = Arc::new(std::sync::Mutex::new(job_rx)); 313 314 // Spawn worker threads 315 let num_workers = num_threads.min(bundles.len()); 316 for _ in 0..num_workers { 317 let job_rx = Arc::clone(&job_rx); 318 let result_tx = result_tx.clone(); 319 let manager = manager_clone.clone_for_arc(); 320 let spec = spec.clone(); 321 322 thread::spawn(move || { 323 loop { 324 let job = { 325 let rx = job_rx.lock().unwrap(); 326 rx.recv() 327 }; 328 match job { 329 Ok((idx, bundle_num)) => { 330 let result = manager.verify_bundle(bundle_num, spec.clone()); 331 let verify_result = match result { 332 Ok(vr) => vr, 333 Err(e) => { 334 // Convert error to VerifyResult for consistency 335 VerifyResult { 336 valid: false, 337 errors: vec![e.to_string()], 338 } 339 } 340 }; 341 result_tx.send((idx, bundle_num, verify_result)).unwrap(); 342 } 343 Err(_) => break, // Channel closed, worker done 344 } 345 } 346 }); 347 } 348 349 // Send jobs 350 for (idx, meta) in bundles.iter().enumerate() { 351 job_tx.send((idx, meta.bundle_number))?; 352 } 353 drop(job_tx); // Close sender, workers will finish 354 355 // Collect results and update progress in real-time 356 let mut results: Vec<(usize, u32, VerifyResult)> = Vec::with_capacity(bundles.len()); 357 358 let mut verified_count = 0; 359 let mut error_count = 0; 360 let mut first_error: Option<anyhow::Error> = None; 361 let mut failed_bundles: Vec<(u32, Vec<String>)> = Vec::new(); 362 let mut completed = 0; 363 364 // Collect results as they arrive and update progress immediately 365 let mut total_uncompressed_processed = 0u64; 366 for _ in 0..bundles.len() { 367 let (idx, bundle_num, verify_result) = result_rx.recv()?; 368 completed += 1; 369 370 // Track uncompressed bytes processed 371 if let Some(meta) = bundles.iter().find(|b| b.bundle_number == bundle_num) { 372 total_uncompressed_processed += meta.uncompressed_size; 373 } 374 375 // Update progress bar immediately with bytes 376 if let Some(ref pb) = progress { 377 pb.set_with_bytes(completed, total_uncompressed_processed); 378 } 379 380 if !verify_result.valid { 381 error_count += 1; 382 let errors = verify_result.errors.clone(); 383 failed_bundles.push((bundle_num, errors.clone())); 384 385 // Only print per-bundle errors in verbose mode 386 if verbose { 387 eprintln!("\n❌ Bundle {} verification failed", bundle_num); 388 if !errors.is_empty() { 389 eprintln!(" ⚠️ Errors:"); 390 for err in &errors { 391 eprintln!("{}", err); 392 } 393 394 // Provide helpful hint for common issues 395 let has_hash_mismatch = errors 396 .iter() 397 .any(|e| e.contains("hash") && e.contains("mismatch")); 398 if has_hash_mismatch { 399 eprintln!( 400 " 💡 Hint: Bundle file may have been migrated but index wasn't updated." 401 ); 402 eprintln!(" Run 'migrate --force' to recalculate all hashes."); 403 } 404 } else { 405 eprintln!(" ⚠️ Verification failed (no error details available)"); 406 } 407 } 408 409 // Store first error for summary 410 if first_error.is_none() 411 && let Some(first_err) = errors.first() 412 { 413 first_error = Some(anyhow::anyhow!("{}", first_err)); 414 } 415 } else { 416 verified_count += 1; 417 } 418 419 results.push((idx, bundle_num, verify_result)); 420 } 421 422 // Sort results by index for consistent error reporting 423 results.sort_by_key(|r| r.0); 424 425 if let Some(ref pb) = progress { 426 pb.finish(); 427 } 428 429 // Pass 2: Verify chain links sequentially 430 if error_count == 0 { 431 eprintln!("\n🔗 Pass 2: Verifying chain links..."); 432 for i in 1..bundles.len() { 433 let prev_meta = &bundles[i - 1]; 434 let meta = &bundles[i]; 435 436 if meta.parent != prev_meta.hash { 437 eprintln!("\n❌ Chain broken at bundle {}", meta.bundle_number); 438 eprintln!( 439 " ⚠️ Expected parent: {}...", 440 &prev_meta.hash[..16.min(prev_meta.hash.len())] 441 ); 442 eprintln!( 443 " ⚠️ Actual parent: {}...", 444 &meta.parent[..16.min(meta.parent.len())] 445 ); 446 error_count += 1; 447 if first_error.is_none() { 448 first_error = Some(anyhow::anyhow!( 449 "chain broken at bundle {}", 450 meta.bundle_number 451 )); 452 } 453 } 454 } 455 if error_count == 0 { 456 eprintln!("✅ All chain links valid"); 457 } 458 } 459 460 let elapsed = start.elapsed(); 461 462 eprintln!(); 463 if error_count == 0 { 464 // Show what was verified 465 let mut verified_items = Vec::new(); 466 if fast { 467 verified_items.push("metadata frames"); 468 } else { 469 verified_items.push("compressed hashes"); 470 if full { 471 verified_items.push("content hashes"); 472 } 473 } 474 verified_items.push("chain links"); 475 eprintln!( 476 "\n✅ Chain is valid ({} bundles verified)", 477 format_number(verified_count as u64) 478 ); 479 eprintln!(" ✓ Verified: {}", verified_items.join(", ")); 480 eprintln!(); 481 482 if fast { 483 eprintln!("ℹ️ Note: This was a fast verification (metadata frame only)."); 484 eprintln!(" Use without --fast for normal verification (compressed hash)"); 485 eprintln!(" Use --full for complete verification (compressed + content hash)"); 486 } else if !full { 487 eprintln!("⚠️ Note: This was a partial verification (compressed hash only)."); 488 eprintln!(" Use --full for complete verification (compressed + content hash)"); 489 eprintln!(" Use --fast for fast verification (metadata frame only)"); 490 } 491 eprintln!(); 492 493 eprintln!("📊 Chain Summary:"); 494 eprintln!(" First bundle: {}", bundles[0].bundle_number); 495 eprintln!( 496 " Last bundle: {}", 497 bundles[bundles.len() - 1].bundle_number 498 ); 499 eprintln!(" Chain root: {}", bundles[0].hash); 500 eprintln!(" Chain head: {}", bundles[bundles.len() - 1].hash); 501 502 // Additional stats 503 let total_size: u64 = bundles.iter().map(|b| b.compressed_size).sum(); 504 let total_ops: u64 = bundles.iter().map(|b| b.operation_count as u64).sum(); 505 let total_dids: u64 = bundles.iter().map(|b| b.did_count as u64).sum(); 506 507 eprintln!("\n📈 Statistics:"); 508 eprintln!( 509 " Total size: {} (compressed)", 510 format_bytes(total_size) 511 ); 512 eprintln!(" Total ops: {}", format_number(total_ops)); 513 eprintln!(" Total DIDs: {}", format_number(total_dids)); 514 eprintln!( 515 " Avg ops/bundle: {}", 516 format_number(total_ops / bundles.len() as u64) 517 ); 518 eprintln!( 519 " Avg size/bundle: {}", 520 format_bytes(total_size / bundles.len() as u64) 521 ); 522 523 // Timing information 524 eprintln!("\n⚡ Performance:"); 525 eprintln!(" Time: {:?}", elapsed); 526 if elapsed.as_secs_f64() > 0.0 { 527 let bundles_per_sec = verified_count as f64 / elapsed.as_secs_f64(); 528 eprintln!(" Throughput: {:.1} bundles/sec", bundles_per_sec); 529 530 if total_size > 0 { 531 let bytes_per_sec_compressed = total_size as f64 / elapsed.as_secs_f64(); 532 eprintln!( 533 " Data rate: {} (compressed)", 534 format_bytes_per_sec(bytes_per_sec_compressed) 535 ); 536 } 537 538 if total_uncompressed_size > 0 { 539 let bytes_per_sec_uncompressed = 540 total_uncompressed_size as f64 / elapsed.as_secs_f64(); 541 eprintln!( 542 " Data rate: {} (uncompressed)", 543 format_bytes_per_sec(bytes_per_sec_uncompressed) 544 ); 545 } 546 } 547 Ok(()) 548 } else { 549 eprintln!("\n❌ Chain verification failed"); 550 eprintln!(" Verified: {}/{} bundles", verified_count, bundles.len()); 551 eprintln!(" Errors: {}", error_count); 552 553 // Show failed bundles with their error messages 554 print_failed_bundles(&failed_bundles, 10); 555 556 eprintln!(" Time: {:?}", elapsed); 557 558 // Show helpful hint if hash mismatch detected 559 if let Some(ref err) = first_error { 560 let err_msg = err.to_string(); 561 if err_msg.contains("hash") && err_msg.contains("mismatch") { 562 eprintln!( 563 "\n💡 Hint: Bundle files may have been migrated but index wasn't updated." 564 ); 565 eprintln!(" Run 'migrate --force' to recalculate all hashes."); 566 } 567 } 568 569 if !verbose { 570 eprintln!("\n Use --verbose to see details of each failed bundle as they are found."); 571 } 572 573 if let Some(err) = first_error { 574 Err(err) 575 } else { 576 bail!("chain verification failed") 577 } 578 } 579} 580 581fn verify_range( 582 manager: &BundleManager, 583 start: u32, 584 end: u32, 585 verbose: bool, 586 full: bool, 587 fast: bool, 588 num_threads: usize, 589) -> Result<()> { 590 let use_parallel = num_threads > 1; 591 592 eprintln!("\n🔬 Verifying bundles {} - {}", start, end); 593 if use_parallel { 594 eprintln!(" Using {} worker thread(s)", num_threads); 595 } 596 eprintln!(); 597 598 let total = end - start + 1; 599 let overall_start = Instant::now(); 600 601 let verify_err = if use_parallel { 602 verify_range_parallel(manager, start, end, num_threads, verbose, full, fast) 603 } else { 604 verify_range_sequential(manager, start, end, total as usize, verbose, full, fast) 605 }; 606 607 let elapsed = overall_start.elapsed(); 608 609 // Add timing summary 610 eprintln!("\n⚡ Performance:"); 611 eprintln!(" Time: {:?}", elapsed); 612 if elapsed.as_secs_f64() > 0.0 { 613 let bundles_per_sec = total as f64 / elapsed.as_secs_f64(); 614 eprintln!(" Throughput: {:.1} bundles/sec", bundles_per_sec); 615 616 let avg_time = elapsed / total; 617 eprintln!(" Avg/bundle: {:?}", avg_time); 618 } 619 620 verify_err 621} 622 623fn verify_range_sequential( 624 manager: &BundleManager, 625 start: u32, 626 end: u32, 627 total: usize, 628 verbose: bool, 629 full: bool, 630 fast: bool, 631) -> Result<()> { 632 // Prefer bytes-aware progress bar to show MB/s if metadata available 633 let (progress, mut processed_uncompressed, per_bundle_uncompressed): ( 634 Option<ProgressBar>, 635 u64, 636 Vec<(u32, u64)>, 637 ) = if !verbose { 638 let index = manager.get_index(); 639 let mut sizes = Vec::with_capacity(total); 640 let mut total_uncompressed_size: u64 = 0; 641 for num in start..=end { 642 if let Some(meta) = index.get_bundle(num) { 643 sizes.push((num, meta.uncompressed_size)); 644 total_uncompressed_size += meta.uncompressed_size; 645 } else { 646 sizes.push((num, 0)); 647 } 648 } 649 ( 650 Some(ProgressBar::with_bytes(total, total_uncompressed_size)), 651 0u64, 652 sizes, 653 ) 654 } else { 655 (None, 0, Vec::new()) 656 }; 657 658 let mut verified = 0; 659 let mut failed = 0; 660 let mut failed_bundles: Vec<(u32, Vec<String>)> = Vec::new(); 661 662 // Verify compressed hash, content hash only if --full 663 let spec = VerifySpec { 664 check_hash: !fast, // Skip hash check in fast mode 665 check_content_hash: full && !fast, // Skip content hash in fast mode 666 check_operations: false, 667 fast, 668 }; 669 670 for bundle_num in start..=end { 671 let result = manager.verify_bundle(bundle_num, spec.clone()); 672 673 if verbose { 674 eprint!("Bundle {}: ", bundle_num); 675 } 676 677 match result { 678 Err(e) => { 679 let errors = vec![e.to_string()]; 680 if verbose { 681 eprintln!("❌ ERROR - {}", errors[0]); 682 } 683 failed += 1; 684 failed_bundles.push((bundle_num, errors)); 685 } 686 Ok(result) => { 687 if !result.valid { 688 if verbose { 689 if result.errors.is_empty() { 690 eprintln!("❌ INVALID - verification failed (no error details)"); 691 } else { 692 eprintln!("❌ INVALID:"); 693 for err in &result.errors { 694 eprintln!("{}", err); 695 } 696 } 697 } 698 failed += 1; 699 failed_bundles.push((bundle_num, result.errors)); 700 } else { 701 if verbose { 702 eprintln!(""); 703 } 704 verified += 1; 705 } 706 } 707 } 708 709 if let Some(ref pb) = progress { 710 if let Some((_, sz)) = per_bundle_uncompressed 711 .get((bundle_num - start) as usize) 712 .cloned() 713 { 714 processed_uncompressed = processed_uncompressed.saturating_add(sz); 715 pb.set_with_bytes((bundle_num - start + 1) as usize, processed_uncompressed); 716 } else { 717 pb.set((bundle_num - start + 1) as usize); 718 } 719 } 720 } 721 722 if let Some(ref pb) = progress { 723 pb.finish(); 724 } 725 726 eprintln!(); 727 if failed == 0 { 728 eprintln!("✅ All {} bundles verified successfully", verified); 729 Ok(()) 730 } else { 731 eprintln!("❌ Verification failed"); 732 eprintln!(" Verified: {}/{}", verified, total); 733 eprintln!(" Failed: {}", failed); 734 735 print_failed_bundles(&failed_bundles, 20); 736 737 bail!("verification failed for {} bundles", failed) 738 } 739} 740 741fn verify_range_parallel( 742 manager: &BundleManager, 743 start: u32, 744 end: u32, 745 workers: usize, 746 verbose: bool, 747 full: bool, 748 fast: bool, 749) -> Result<()> { 750 // Note: Parallel verification requires Arc<BundleManager> which needs to be implemented 751 // For now, fall back to sequential with progress 752 let total = (end - start + 1) as usize; 753 if verbose { 754 eprintln!( 755 "[DEBUG] Using {} worker thread(s) for parallel verification", 756 workers 757 ); 758 } 759 eprintln!("Note: Parallel verification not yet fully implemented, using sequential"); 760 verify_range_sequential(manager, start, end, total, verbose, full, fast) 761} 762 763/// Print failed bundles with their error messages 764/// 765/// # Arguments 766/// * `failed_bundles` - Vector of (bundle_num, errors) tuples 767/// * `threshold` - Maximum number of bundles to list in full before truncating (e.g., 10 or 20) 768fn print_failed_bundles(failed_bundles: &[(u32, Vec<String>)], threshold: usize) { 769 if failed_bundles.is_empty() { 770 return; 771 } 772 773 if failed_bundles.len() <= threshold { 774 eprintln!("\n ⚠️ Failed bundles:"); 775 for (bundle_num, errors) in failed_bundles { 776 eprintln!(" Bundle {}:", bundle_num); 777 if errors.is_empty() { 778 eprintln!(" • Verification failed (no error details)"); 779 } else { 780 for err in errors { 781 eprintln!("{}", err); 782 } 783 } 784 } 785 } else { 786 eprintln!( 787 " ⚠️ Failed bundles: {} (too many to list)", 788 failed_bundles.len() 789 ); 790 // Show first few with details 791 eprintln!("\n First few failures:"); 792 for (bundle_num, errors) in failed_bundles.iter().take(5) { 793 eprintln!(" Bundle {}:", bundle_num); 794 if errors.is_empty() { 795 eprintln!(" • Verification failed (no error details)"); 796 } else { 797 for err in errors.iter().take(3) { 798 eprintln!("{}", err); 799 } 800 if errors.len() > 3 { 801 eprintln!(" • ... and {} more error(s)", errors.len() - 3); 802 } 803 } 804 } 805 eprintln!( 806 " ... and {} more failed bundles", 807 failed_bundles.len() - 5 808 ); 809 } 810}