forked from
atscan.net/plcbundle-rs
High-performance implementation of plcbundle written in Rust
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}