diff --git a/infrastructure/src/bin/bat_official_sync.rs b/infrastructure/src/bin/bat_official_sync.rs index dc9c212..e5e8d96 100644 --- a/infrastructure/src/bin/bat_official_sync.rs +++ b/infrastructure/src/bin/bat_official_sync.rs @@ -1,7 +1,7 @@ use bat_adapters::official::yostar_jp::PatchPlatform; use bat_infrastructure::{ OfficialServerInfoSource, OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, - OfficialUpdateService, OfficialUpdateStatus, + OfficialUpdateService, OfficialUpdateStatus, OfficialVerificationSummary, }; use serde::{Deserialize, Serialize}; use std::env; @@ -1523,6 +1523,32 @@ fn print_list(label: &str, values: &[String], limit: usize) { } } +fn print_verification_summary(summary: &OfficialVerificationSummary) { + println!(" 校验摘要:"); + for line in verification_summary_lines(summary) { + println!(" - {line}"); + } +} + +fn verification_summary_lines(summary: &OfficialVerificationSummary) -> Vec { + vec![ + format!( + "官方 .hash 强校验: {} 对 ({})", + summary.official_hash_verified_count, summary.official_hash_scope + ), + format!( + "本地 BLAKE3 复用校验: {} 项通过, {} 项需修复 ({})", + summary.local_manifest_blake3_verified_count, + summary.local_manifest_repair_needed_count, + summary.local_manifest_blake3_scope + ), + format!( + "ZIP 结构校验: {} 个 ZIP 通过 ({})", + summary.zip_structure_verified_count, summary.zip_structure_scope + ), + ] +} + fn format_bool(value: bool) -> &'static str { if value { "yes" @@ -1590,6 +1616,7 @@ impl HumanReport for OfficialUpdateReport { print_field("本地校验通过", self.local_manifest_verified_count); print_field("需修复", self.local_manifest_repair_needed_count); print_field("官方 hash 校验", self.official_seed_hash_verified_count); + print_verification_summary(&self.verification_summary); print_field("catalog marker", self.addressables_marker_checked_count); print_list("变更 endpoint", &self.changed_endpoint_urls, 8); print_list("计划 URL", &self.download_urls, 8); @@ -1684,6 +1711,7 @@ impl HumanReport for VerifyCommandReport { print_field("本地失败数", self.local_manifest_failure_count); print_field("官方 hash 对", self.official_hash_pair_count); print_field("官方 hash 通过", self.official_hash_verified_count); + print_verification_summary(&self.verification_summary); if !self.official_hash_errors.is_empty() { print_list("官方 hash 错误", &self.official_hash_errors, 8); } @@ -1798,6 +1826,7 @@ struct VerifyCommandReport { local_manifest_failure_count: usize, official_hash_pair_count: usize, official_hash_verified_count: usize, + verification_summary: OfficialVerificationSummary, official_hash_errors: Vec, failures: Vec, } @@ -1837,6 +1866,12 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result { let healthy = update_report.update_status == OfficialUpdateStatus::UpToDate && update_report.local_manifest_repair_needed_count == 0 && verification.is_clean(); + let verification_summary = OfficialVerificationSummary::new( + verification.manifest_blake3_verified_count(), + verification.failure_count(), + verification.official_hash_verified_count, + verification.zip_structure_verified_count(), + ); let report = VerifyCommandReport { command: "verify", status: if healthy { "verified" } else { "failed" }, @@ -1854,6 +1889,7 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result { local_manifest_failure_count: verification.failure_count(), official_hash_pair_count: verification.official_hash_pair_count, official_hash_verified_count: verification.official_hash_verified_count, + verification_summary, official_hash_errors: verification.official_hash_errors, failures, }; @@ -3384,6 +3420,26 @@ mod tests { assert!(STARTUP_BANNER.contains("BlueArchiveToolkit")); } + #[test] + fn verification_summary_output_names_all_validation_types() { + let summary = OfficialVerificationSummary::new(7, 1, 2, 3); + let lines = verification_summary_lines(&summary); + + assert_eq!(lines.len(), 3); + assert!(lines[0].contains("官方 .hash 强校验")); + assert!(lines[0].contains("xxHash32")); + assert!(lines[1].contains("本地 BLAKE3 复用校验")); + assert!(lines[1].contains("需修复")); + assert!(lines[2].contains("ZIP 结构校验")); + assert!(lines[2].contains("central directory")); + + let json = serde_json::to_value(&summary).unwrap(); + assert_eq!(json["official_hash_verified_count"], 2); + assert_eq!(json["local_manifest_blake3_verified_count"], 7); + assert_eq!(json["local_manifest_repair_needed_count"], 1); + assert_eq!(json["zip_structure_verified_count"], 3); + } + #[test] fn daemon_socket_path_uses_state_dir() { assert_eq!( diff --git a/infrastructure/src/lib.rs b/infrastructure/src/lib.rs index 08f8284..b658d90 100644 --- a/infrastructure/src/lib.rs +++ b/infrastructure/src/lib.rs @@ -50,7 +50,7 @@ pub use official_update::{ GameMainConfigSnapshot, LauncherMetadataSnapshot, OfficialBootstrapCache, OfficialEndpointMarkerRole, OfficialEndpointMarkerSnapshot, OfficialServerInfoSource, OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService, - OfficialUpdateSnapshot, OfficialUpdateStatus, ResolvedBootstrap, + OfficialUpdateSnapshot, OfficialUpdateStatus, OfficialVerificationSummary, ResolvedBootstrap, }; pub use resources::{InMemoryResourceRepository, SqliteResourceRepository}; diff --git a/infrastructure/src/official_download.rs b/infrastructure/src/official_download.rs index db56db0..bd5e8a9 100644 --- a/infrastructure/src/official_download.rs +++ b/infrastructure/src/official_download.rs @@ -270,6 +270,8 @@ pub struct OfficialLocalManifestAuditItem { /// ZIP structure validation error, when the URL or destination is a ZIP /// and the local file did not pass ZIP structure validation. pub zip_error: Option, + /// Whether this item required and passed ZIP structure validation. + pub zip_structure_verified: bool, } /// Local download-manifest audit result for a pull plan. @@ -297,6 +299,23 @@ impl OfficialLocalManifestAuditReport { pub fn repair_needed_count(&self) -> usize { self.items.len().saturating_sub(self.verified_count()) } + + /// Returns the number of files that passed local manifest path, size, and + /// BLAKE3 validation. + pub fn manifest_blake3_verified_count(&self) -> usize { + self.items + .iter() + .filter(|item| item.status.is_verified()) + .count() + } + + /// Returns the number of ZIP files that passed structural validation. + pub fn zip_structure_verified_count(&self) -> usize { + self.items + .iter() + .filter(|item| item.zip_structure_verified) + .count() + } } /// Full local verification report for all entries currently recorded in the @@ -334,6 +353,23 @@ impl OfficialLocalVerificationReport { pub fn failure_count(&self) -> usize { self.items.len().saturating_sub(self.verified_count()) } + + /// Returns the number of local manifest entries that passed path, size, + /// and BLAKE3 validation. + pub fn manifest_blake3_verified_count(&self) -> usize { + self.items + .iter() + .filter(|item| item.status.is_verified()) + .count() + } + + /// Returns the number of ZIP files that passed structural validation. + pub fn zip_structure_verified_count(&self) -> usize { + self.items + .iter() + .filter(|item| item.zip_structure_verified) + .count() + } } /// Existing local state for an official pull plan. @@ -1018,6 +1054,7 @@ impl OfficialResourcePullService { expected_blake3: None, actual_blake3: None, zip_error: None, + zip_structure_verified: false, }); }; @@ -1036,6 +1073,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: None, zip_error: None, + zip_structure_verified: false, }); } @@ -1051,6 +1089,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: None, zip_error: None, + zip_structure_verified: false, }); } @@ -1065,6 +1104,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: None, zip_error: None, + zip_structure_verified: false, }); }; @@ -1079,6 +1119,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: None, zip_error: None, + zip_structure_verified: false, }); } @@ -1094,9 +1135,12 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: Some(actual_blake3), zip_error: None, + zip_structure_verified: false, }); } + let zip_structure_required = + url_or_path_has_zip_extension(url) || path_has_zip_extension(&destination); if let Err(error) = self.validate_zip_if_needed(url, &destination) { return Ok(OfficialLocalManifestAuditItem { url: url.to_string(), @@ -1108,6 +1152,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: Some(actual_blake3), zip_error: Some(error), + zip_structure_verified: false, }); } @@ -1121,6 +1166,7 @@ impl OfficialResourcePullService { expected_blake3, actual_blake3: Some(actual_blake3), zip_error: None, + zip_structure_verified: zip_structure_required, }) } @@ -1841,6 +1887,8 @@ fi let clean_audit = service.audit_local_manifest(&plan).unwrap(); assert!(clean_audit.is_clean()); assert_eq!(clean_audit.verified_count(), first_report.items.len()); + assert_eq!(clean_audit.manifest_blake3_verified_count(), 7); + assert_eq!(clean_audit.zip_structure_verified_count(), 4); let corrupted = first_report .items @@ -1879,6 +1927,8 @@ fi assert!(verification.is_clean()); assert_eq!(verification.items.len(), 7); assert_eq!(verification.verified_count(), 7); + assert_eq!(verification.manifest_blake3_verified_count(), 7); + assert_eq!(verification.zip_structure_verified_count(), 4); assert_eq!(verification.official_hash_pair_count, 1); assert_eq!(verification.official_hash_verified_count, 1); diff --git a/infrastructure/src/official_update.rs b/infrastructure/src/official_update.rs index 12b8888..16877e2 100644 --- a/infrastructure/src/official_update.rs +++ b/infrastructure/src/official_update.rs @@ -314,6 +314,50 @@ impl ExtendedSnapshotDelta { } } +/// Structured verification summary for CLI and JSON reports. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OfficialVerificationSummary { + /// Number of resources verified by the local download manifest's path, + /// byte size, and BLAKE3 digest. + pub local_manifest_blake3_verified_count: usize, + /// Number of resources that still need local repair after manifest audit. + pub local_manifest_repair_needed_count: usize, + /// Human-facing description of the local manifest validation boundary. + pub local_manifest_blake3_scope: String, + /// Number of official seed `.bytes`/`.hash` pairs verified by the official + /// decimal xxHash32 rule. + pub official_hash_verified_count: usize, + /// Human-facing description of the official hash validation boundary. + pub official_hash_scope: String, + /// Number of ZIP files that passed structural validation. + pub zip_structure_verified_count: usize, + /// Human-facing description of the ZIP structure validation boundary. + pub zip_structure_scope: String, +} + +impl OfficialVerificationSummary { + /// Creates a verification summary from validation counters. + pub fn new( + local_manifest_blake3_verified_count: usize, + local_manifest_repair_needed_count: usize, + official_hash_verified_count: usize, + zip_structure_verified_count: usize, + ) -> Self { + Self { + local_manifest_blake3_verified_count, + local_manifest_repair_needed_count, + local_manifest_blake3_scope: + "本地 download manifest 的 URL/path、size 和 BLAKE3 复用校验".to_string(), + official_hash_verified_count, + official_hash_scope: "官方 seed .bytes/.hash 对,按官方十进制 xxHash32(seed=0) 强校验" + .to_string(), + zip_structure_verified_count, + zip_structure_scope: + "所有 .zip 文件的 EOCD、central directory、local header 和边界结构校验".to_string(), + } + } +} + /// Structured report returned by an official update run. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct OfficialUpdateReport { @@ -381,6 +425,8 @@ pub struct OfficialUpdateReport { pub transferred_bytes: u64, /// Number of official seed hashes verified. pub official_seed_hash_verified_count: usize, + /// Verification summary with explicit boundaries for each validation type. + pub verification_summary: OfficialVerificationSummary, /// Download manifest path. pub download_manifest: PathBuf, /// Snapshot path written after success. @@ -683,6 +729,14 @@ impl OfficialUpdateService { .as_ref() .map(|audit| audit.repair_needed_count()) .unwrap_or(0); + let local_manifest_blake3_verified_count = local_audit + .as_ref() + .map(|audit| audit.manifest_blake3_verified_count()) + .unwrap_or(0); + let local_zip_structure_verified_count = local_audit + .as_ref() + .map(|audit| audit.zip_structure_verified_count()) + .unwrap_or(0); let repair_needed = config.repair && local_audit .as_ref() @@ -743,6 +797,12 @@ impl OfficialUpdateService { final_bytes: 0, transferred_bytes: 0, official_seed_hash_verified_count: 0, + verification_summary: OfficialVerificationSummary::new( + local_manifest_blake3_verified_count, + local_manifest_repair_needed_count, + 0, + local_zip_structure_verified_count, + ), download_manifest: fetcher.download_manifest_path(), snapshot_written: None, }; @@ -830,6 +890,12 @@ impl OfficialUpdateService { report.official_seed_hash_verified_count = pull_report.official_hash_verified_count(); report.local_manifest_verified_count = final_audit.verified_count(); report.local_manifest_repair_needed_count = final_audit.repair_needed_count(); + report.verification_summary = OfficialVerificationSummary::new( + final_audit.manifest_blake3_verified_count(), + final_audit.repair_needed_count(), + pull_report.official_hash_verified_count(), + final_audit.zip_structure_verified_count(), + ); report.snapshot_written = Some(snapshot_path); progress(OfficialUpdateProgress::new( diff --git a/infrastructure/tests/official_game_main_config_bootstrap.rs b/infrastructure/tests/official_game_main_config_bootstrap.rs index 6e0ab02..be10b0a 100644 --- a/infrastructure/tests/official_game_main_config_bootstrap.rs +++ b/infrastructure/tests/official_game_main_config_bootstrap.rs @@ -200,6 +200,26 @@ fn official_update_first_run_pulls_without_pre_audit() { assert_eq!(report.downloaded_count, 19); assert_eq!(report.skipped_count, 0); assert_eq!(report.local_manifest_repair_needed_count, 0); + assert_eq!( + report + .verification_summary + .local_manifest_blake3_verified_count, + 19 + ); + assert_eq!(report.verification_summary.official_hash_verified_count, 5); + assert_eq!(report.verification_summary.zip_structure_verified_count, 6); + assert!(report + .verification_summary + .official_hash_scope + .contains("xxHash32")); + assert!(report + .verification_summary + .local_manifest_blake3_scope + .contains("BLAKE3")); + assert!(report + .verification_summary + .zip_structure_scope + .contains("central directory")); assert!(events.iter().any(|event| { event.stage == "local-state" && event.message.contains("是否已有本地资源=false") })); @@ -226,6 +246,13 @@ fn official_update_second_run_audits_existing_resources_before_reuse() { assert_eq!(report.downloaded_count, 0); assert_eq!(report.local_manifest_verified_count, 19); assert_eq!(report.local_manifest_repair_needed_count, 0); + assert_eq!( + report + .verification_summary + .local_manifest_blake3_verified_count, + 19 + ); + assert_eq!(report.verification_summary.zip_structure_verified_count, 6); assert!(events.iter().any(|event| { event.stage == "local-state" && event.message.contains("是否已有本地资源=true") }));