feat: summarize official validation results

Fixes #5
This commit is contained in:
2026-07-13 01:06:37 +08:00
parent e5fcf26e19
commit 1bcb9c828a
5 changed files with 201 additions and 2 deletions
+57 -1
View File
@@ -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<String> {
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<String>,
failures: Vec<VerificationItemReport>,
}
@@ -1837,6 +1866,12 @@ fn run_verify_command(options: &CliOptions) -> anyhow::Result<bool> {
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<bool> {
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!(
+1 -1
View File
@@ -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};
+50
View File
@@ -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<String>,
/// 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);
+66
View File
@@ -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(