mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 07:16:45 +08:00
feat: improve bat daemon cli output
This commit is contained in:
@@ -226,6 +226,19 @@ impl OfficialLocalManifestAuditStatus {
|
||||
pub fn is_verified(self) -> bool {
|
||||
self == Self::Verified
|
||||
}
|
||||
|
||||
/// Returns a stable status label for CLI and JSON reports.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Verified => "verified",
|
||||
Self::MissingManifestEntry => "missing_manifest_entry",
|
||||
Self::UrlMismatch => "url_mismatch",
|
||||
Self::DestinationMismatch => "destination_mismatch",
|
||||
Self::MissingFile => "missing_file",
|
||||
Self::SizeMismatch => "size_mismatch",
|
||||
Self::Blake3Mismatch => "blake3_mismatch",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Local download-manifest audit result for one expected official URL.
|
||||
@@ -276,6 +289,43 @@ impl OfficialLocalManifestAuditReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Full local verification report for all entries currently recorded in the
|
||||
/// official download manifest.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct OfficialLocalVerificationReport {
|
||||
/// Per-entry manifest audit results.
|
||||
pub items: Vec<OfficialLocalManifestAuditItem>,
|
||||
/// Official seed hash pairs found in the local manifest.
|
||||
pub official_hash_pair_count: usize,
|
||||
/// Official seed hash pairs that passed xxHash32 verification.
|
||||
pub official_hash_verified_count: usize,
|
||||
/// Official seed hash verification failures.
|
||||
pub official_hash_errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl OfficialLocalVerificationReport {
|
||||
/// Returns true when every local manifest entry and every local official
|
||||
/// seed hash pair passed verification.
|
||||
pub fn is_clean(&self) -> bool {
|
||||
self.items.iter().all(|item| item.status.is_verified())
|
||||
&& self.official_hash_errors.is_empty()
|
||||
&& self.official_hash_verified_count == self.official_hash_pair_count
|
||||
}
|
||||
|
||||
/// Returns the number of manifest entries that passed verification.
|
||||
pub fn verified_count(&self) -> usize {
|
||||
self.items
|
||||
.iter()
|
||||
.filter(|item| item.status.is_verified())
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Returns the number of manifest entries that need attention.
|
||||
pub fn failure_count(&self) -> usize {
|
||||
self.items.len().saturating_sub(self.verified_count())
|
||||
}
|
||||
}
|
||||
|
||||
/// Existing local state for an official pull plan.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OfficialLocalResourceState {
|
||||
@@ -362,7 +412,7 @@ impl OfficialResourcePullService {
|
||||
) -> Result<OfficialResourcePullReport, String> {
|
||||
fs::create_dir_all(&self.output_root).map_err(|error| {
|
||||
format!(
|
||||
"Failed to create output root {}: {error}",
|
||||
"创建资源输出目录失败 {}:{error}",
|
||||
self.output_root.display()
|
||||
)
|
||||
})?;
|
||||
@@ -378,7 +428,7 @@ impl OfficialResourcePullService {
|
||||
let mut processed_urls = HashSet::<String>::new();
|
||||
for (offset, url) in urls.into_iter().enumerate() {
|
||||
if should_cancel() {
|
||||
return Err("official resource pull interrupted by shutdown request".to_string());
|
||||
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||
}
|
||||
|
||||
let index = offset + 1;
|
||||
@@ -389,16 +439,13 @@ impl OfficialResourcePullService {
|
||||
));
|
||||
|
||||
if !is_official_yostar_jp_url(&url) {
|
||||
return Err(format!("Refusing to download non-official URL: {url}"));
|
||||
return Err(format!("拒绝下载非官方 URL:{url}"));
|
||||
}
|
||||
|
||||
let destination = self.destination_for_url(&url)?;
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
format!(
|
||||
"Failed to create destination directory {}: {error}",
|
||||
parent.display()
|
||||
)
|
||||
format!("创建下载目标目录失败 {}:{error}", parent.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -443,7 +490,7 @@ impl OfficialResourcePullService {
|
||||
}
|
||||
|
||||
if should_cancel() {
|
||||
return Err("official resource pull interrupted by shutdown request".to_string());
|
||||
return Err("官方资源拉取已被停止请求中断".to_string());
|
||||
}
|
||||
self.verify_all_official_hashes_are_complete(&official_hash_pairs, &verified_hash_urls)?;
|
||||
|
||||
@@ -473,6 +520,42 @@ impl OfficialResourcePullService {
|
||||
Ok(OfficialLocalManifestAuditReport { items })
|
||||
}
|
||||
|
||||
/// Verifies every entry currently present in the local download manifest.
|
||||
///
|
||||
/// This is intentionally network-free. It validates each recorded file
|
||||
/// using the local manifest's size and BLAKE3 digest, then verifies every
|
||||
/// local seed `.bytes`/`.hash` pair that is present using the official
|
||||
/// decimal xxHash32 rule.
|
||||
pub fn verify_local_download_manifest(
|
||||
&self,
|
||||
) -> Result<OfficialLocalVerificationReport, String> {
|
||||
let manifest = self.read_download_manifest()?;
|
||||
let urls = manifest.entries.keys().cloned().collect::<Vec<_>>();
|
||||
let mut items = Vec::with_capacity(urls.len());
|
||||
|
||||
for url in urls {
|
||||
let destination = self.destination_for_url(&url)?;
|
||||
items.push(self.audit_one(&url, destination, &manifest)?);
|
||||
}
|
||||
|
||||
let pairs = local_manifest_seed_hash_pairs(&manifest);
|
||||
let mut official_hash_verified_count = 0;
|
||||
let mut official_hash_errors = Vec::new();
|
||||
for pair in &pairs {
|
||||
match self.verify_official_seed_hash_pair_from_disk(pair) {
|
||||
Ok(_) => official_hash_verified_count += 1,
|
||||
Err(error) => official_hash_errors.push(error),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OfficialLocalVerificationReport {
|
||||
items,
|
||||
official_hash_pair_count: pairs.len(),
|
||||
official_hash_verified_count,
|
||||
official_hash_errors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the current output root already contains local state for
|
||||
/// this plan. This is intentionally cheaper than a full audit and is used
|
||||
/// to choose between audit-then-repair and first-time full pull flows.
|
||||
@@ -508,7 +591,7 @@ impl OfficialResourcePullService {
|
||||
/// `MediaCatalog.bytes`.
|
||||
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||
if !is_official_yostar_jp_url(url) {
|
||||
return Err(format!("Refusing to fetch non-official URL: {url}"));
|
||||
return Err(format!("拒绝拉取非官方 URL:{url}"));
|
||||
}
|
||||
|
||||
let attempts = self.retry_attempts.max(1);
|
||||
@@ -517,12 +600,12 @@ impl OfficialResourcePullService {
|
||||
match self.fetch_bytes_once(url) {
|
||||
Ok(bytes) => return Ok(bytes),
|
||||
Err(error) => {
|
||||
last_error = Some(format!("attempt {attempt}/{attempts}: {error}"));
|
||||
last_error = Some(format!("第 {attempt}/{attempts} 次尝试失败:{error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or_else(|| format!("curl failed for {url} without an attempt")))
|
||||
Err(last_error.unwrap_or_else(|| format!("curl 未执行就已失败:{url}")))
|
||||
}
|
||||
|
||||
fn fetch_bytes_once(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||
@@ -536,14 +619,14 @@ impl OfficialResourcePullService {
|
||||
.output()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Failed to launch curl command {}: {error}",
|
||||
"启动 curl 命令失败 {}:{error}",
|
||||
self.curl_command.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("curl failed for {url}: {}", stderr.trim()));
|
||||
return Err(format!("curl 拉取失败 {url}:{}", stderr.trim()));
|
||||
}
|
||||
|
||||
Ok(output.stdout)
|
||||
@@ -559,9 +642,7 @@ impl OfficialResourcePullService {
|
||||
let _ = fs::remove_file(&partial);
|
||||
self.download_one(url, &partial, false)
|
||||
.map_err(|retry_error| {
|
||||
format!(
|
||||
"resume failed for {url}: {error}; clean retry also failed: {retry_error}"
|
||||
)
|
||||
format!("续传失败 {url}:{error};清理后重新下载也失败:{retry_error}")
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
@@ -570,28 +651,20 @@ impl OfficialResourcePullService {
|
||||
|
||||
if file_len_if_exists(destination)?.is_some() {
|
||||
fs::remove_file(destination).map_err(|error| {
|
||||
format!(
|
||||
"Failed to replace existing destination {}: {error}",
|
||||
destination.display()
|
||||
)
|
||||
format!("替换已有目标文件失败 {}:{error}", destination.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
fs::rename(&partial, destination).map_err(|error| {
|
||||
format!(
|
||||
"Failed to move partial download {} -> {}: {error}",
|
||||
"移动临时下载文件失败 {} -> {}:{error}",
|
||||
partial.display(),
|
||||
destination.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let bytes = fs::metadata(destination)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Downloaded file missing at {}: {error}",
|
||||
destination.display()
|
||||
)
|
||||
})?
|
||||
.map_err(|error| format!("下载完成后目标文件缺失 {}:{error}", destination.display()))?
|
||||
.len();
|
||||
|
||||
Ok(PullOneResult {
|
||||
@@ -612,17 +685,13 @@ impl OfficialResourcePullService {
|
||||
match self.download_one_once(url, destination, resume) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(error) => {
|
||||
last_error = Some(format!("attempt {attempt}/{attempts}: {error}"));
|
||||
last_error = Some(format!("第 {attempt}/{attempts} 次尝试失败:{error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or_else(|| {
|
||||
format!(
|
||||
"curl failed for {url} -> {} without an attempt",
|
||||
destination.display()
|
||||
)
|
||||
}))
|
||||
Err(last_error
|
||||
.unwrap_or_else(|| format!("curl 未执行就已失败:{url} -> {}", destination.display())))
|
||||
}
|
||||
|
||||
fn download_one_once(&self, url: &str, destination: &Path, resume: bool) -> Result<(), String> {
|
||||
@@ -641,7 +710,7 @@ impl OfficialResourcePullService {
|
||||
|
||||
let output = command.output().map_err(|error| {
|
||||
format!(
|
||||
"Failed to launch curl command {}: {error}",
|
||||
"启动 curl 命令失败 {}:{error}",
|
||||
self.curl_command.display()
|
||||
)
|
||||
})?;
|
||||
@@ -649,7 +718,7 @@ impl OfficialResourcePullService {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"curl failed for {url} -> {}: {}",
|
||||
"curl 下载失败 {url} -> {}:{}",
|
||||
destination.display(),
|
||||
stderr.trim()
|
||||
));
|
||||
@@ -687,7 +756,7 @@ impl OfficialResourcePullService {
|
||||
self.invalidate_download_manifest_hash_pair(manifest, pair)
|
||||
{
|
||||
return Err(format!(
|
||||
"{error}; additionally failed to invalidate local manifest entries for official hash pair: {cleanup_error}"
|
||||
"{error};同时清理官方 hash 对应的本地 manifest 条目失败:{cleanup_error}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -716,13 +785,13 @@ impl OfficialResourcePullService {
|
||||
let hash_path = self.destination_for_url(&pair.hash_url)?;
|
||||
let data = fs::read(&data_path).map_err(|error| {
|
||||
format!(
|
||||
"Failed to read official hash data file {}: {error}",
|
||||
"读取官方 hash 对应数据文件失败 {}:{error}",
|
||||
data_path.display()
|
||||
)
|
||||
})?;
|
||||
let hash = fs::read(&hash_path).map_err(|error| {
|
||||
format!(
|
||||
"Failed to read official hash sidecar {}: {error}",
|
||||
"读取官方 hash sidecar 失败 {}:{error}",
|
||||
hash_path.display()
|
||||
)
|
||||
})?;
|
||||
@@ -748,7 +817,7 @@ impl OfficialResourcePullService {
|
||||
for pair in pairs {
|
||||
if !verified_hash_urls.contains(&pair.hash_url) {
|
||||
return Err(format!(
|
||||
"official hash pair was not verified for {} using {}",
|
||||
"官方 hash 对未完成校验:数据={} hash={}",
|
||||
pair.data_url, pair.hash_url
|
||||
));
|
||||
}
|
||||
@@ -766,23 +835,18 @@ impl OfficialResourcePullService {
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"Failed to read download manifest {}: {error}",
|
||||
"读取下载 manifest 失败 {}:{error}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let manifest: OfficialDownloadManifest =
|
||||
serde_json::from_slice(&bytes).map_err(|error| {
|
||||
format!(
|
||||
"Failed to parse download manifest {}: {error}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let manifest: OfficialDownloadManifest = serde_json::from_slice(&bytes)
|
||||
.map_err(|error| format!("解析下载 manifest 失败 {}:{error}", path.display()))?;
|
||||
|
||||
if manifest.version != DOWNLOAD_MANIFEST_VERSION {
|
||||
return Err(format!(
|
||||
"Unsupported download manifest version {} in {}",
|
||||
"不支持的下载 manifest 版本 {},文件 {}",
|
||||
manifest.version,
|
||||
path.display()
|
||||
));
|
||||
@@ -795,29 +859,22 @@ impl OfficialResourcePullService {
|
||||
let path = self.download_manifest_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
format!(
|
||||
"Failed to create download manifest directory {}: {error}",
|
||||
parent.display()
|
||||
)
|
||||
format!("创建下载 manifest 目录失败 {}:{error}", parent.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let temporary = path.with_extension("json.tmp");
|
||||
let bytes = serde_json::to_vec_pretty(manifest).map_err(|error| {
|
||||
format!(
|
||||
"Failed to serialize download manifest {}: {error}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let bytes = serde_json::to_vec_pretty(manifest)
|
||||
.map_err(|error| format!("序列化下载 manifest 失败 {}:{error}", path.display()))?;
|
||||
fs::write(&temporary, bytes).map_err(|error| {
|
||||
format!(
|
||||
"Failed to write temporary download manifest {}: {error}",
|
||||
"写入临时下载 manifest 失败 {}:{error}",
|
||||
temporary.display()
|
||||
)
|
||||
})?;
|
||||
fs::rename(&temporary, &path).map_err(|error| {
|
||||
format!(
|
||||
"Failed to move download manifest {} -> {}: {error}",
|
||||
"移动下载 manifest 失败 {} -> {}:{error}",
|
||||
temporary.display(),
|
||||
path.display()
|
||||
)
|
||||
@@ -871,12 +928,7 @@ impl OfficialResourcePullService {
|
||||
destination: &Path,
|
||||
) -> Result<(), String> {
|
||||
let bytes = fs::metadata(destination)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Failed to inspect downloaded file {}: {error}",
|
||||
destination.display()
|
||||
)
|
||||
})?
|
||||
.map_err(|error| format!("读取已下载文件信息失败 {}:{error}", destination.display()))?
|
||||
.len();
|
||||
let digest = blake3_file_hex(destination)?;
|
||||
let relative_destination = self.relative_destination(destination)?;
|
||||
@@ -899,7 +951,7 @@ impl OfficialResourcePullService {
|
||||
.strip_prefix(&self.output_root)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Destination {} is not under output root {}: {error}",
|
||||
"目标路径 {} 不在输出目录 {} 下:{error}",
|
||||
destination.display(),
|
||||
self.output_root.display()
|
||||
)
|
||||
@@ -1013,15 +1065,15 @@ impl OfficialResourcePullService {
|
||||
|
||||
fn destination_for_url(&self, url: &str) -> Result<PathBuf, String> {
|
||||
if !is_official_yostar_jp_url(url) {
|
||||
return Err(format!("URL is not an official JP host: {url}"));
|
||||
return Err(format!("URL 不是官方 JP host:{url}"));
|
||||
}
|
||||
|
||||
let rest = url
|
||||
.strip_prefix("https://")
|
||||
.ok_or_else(|| format!("Official URL must use https: {url}"))?;
|
||||
.ok_or_else(|| format!("官方 URL 必须使用 https:{url}"))?;
|
||||
let (host, path) = rest
|
||||
.split_once('/')
|
||||
.ok_or_else(|| format!("Official URL has no path: {url}"))?;
|
||||
.ok_or_else(|| format!("官方 URL 缺少路径:{url}"))?;
|
||||
|
||||
let mut destination = PathBuf::from(sanitize_segment(host));
|
||||
for segment in path.split('/') {
|
||||
@@ -1029,7 +1081,7 @@ impl OfficialResourcePullService {
|
||||
continue;
|
||||
}
|
||||
if segment == "." || segment == ".." {
|
||||
return Err(format!("Official URL has unsafe path segment: {url}"));
|
||||
return Err(format!("官方 URL 包含不安全路径片段:{url}"));
|
||||
}
|
||||
|
||||
let segment = segment.split(['?', '#']).next().unwrap_or(segment);
|
||||
@@ -1060,7 +1112,7 @@ pub fn verify_official_seed_catalog_hash(
|
||||
|
||||
if actual != expected {
|
||||
return Err(format!(
|
||||
"official hash mismatch for {data_url}: expected {expected} from {hash_url}, actual {actual}"
|
||||
"官方 hash 校验失败 {data_url}:期望 {expected}(来自 {hash_url}),实际 {actual}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1097,6 +1149,28 @@ fn official_seed_hash_pairs(plan: &OfficialResourcePullPlan) -> Vec<OfficialSeed
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn local_manifest_seed_hash_pairs(
|
||||
manifest: &OfficialDownloadManifest,
|
||||
) -> Vec<OfficialSeedHashPair> {
|
||||
manifest
|
||||
.entries
|
||||
.keys()
|
||||
.filter_map(|data_url| {
|
||||
let hash_url = data_url
|
||||
.strip_suffix(".bytes")
|
||||
.map(|prefix| format!("{prefix}.hash"))?;
|
||||
if !manifest.entries.contains_key(&hash_url) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(OfficialSeedHashPair {
|
||||
data_url: data_url.clone(),
|
||||
hash_url,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn official_hash_refresh_urls(pairs: &[OfficialSeedHashPair]) -> HashSet<String> {
|
||||
let mut urls = HashSet::new();
|
||||
for pair in pairs {
|
||||
@@ -1166,12 +1240,9 @@ struct PullOneResult {
|
||||
fn file_len_if_exists(path: &Path) -> Result<Option<u64>, String> {
|
||||
match fs::metadata(path) {
|
||||
Ok(metadata) if metadata.is_file() => Ok(Some(metadata.len())),
|
||||
Ok(_) => Err(format!(
|
||||
"Expected file path but found non-file: {}",
|
||||
path.display()
|
||||
)),
|
||||
Ok(_) => Err(format!("期望文件路径,但实际不是文件:{}", path.display())),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(format!("Failed to inspect {}: {error}", path.display())),
|
||||
Err(error) => Err(format!("读取路径信息失败 {}:{error}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1183,14 +1254,14 @@ fn partial_path_for(destination: &Path) -> PathBuf {
|
||||
|
||||
fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
||||
let mut file =
|
||||
File::open(path).map_err(|error| format!("Failed to open {}: {error}", path.display()))?;
|
||||
File::open(path).map_err(|error| format!("打开文件失败 {}:{error}", path.display()))?;
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
|
||||
loop {
|
||||
let read = file
|
||||
.read(&mut buffer)
|
||||
.map_err(|error| format!("Failed to read {}: {error}", path.display()))?;
|
||||
.map_err(|error| format!("读取文件失败 {}:{error}", path.display()))?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -1202,11 +1273,10 @@ fn blake3_file_hex(path: &Path) -> Result<String, String> {
|
||||
|
||||
fn parse_official_xxhash32_decimal(hash_url: &str, bytes: &[u8]) -> Result<u32, String> {
|
||||
let text = std::str::from_utf8(bytes)
|
||||
.map_err(|error| format!("Official hash sidecar is not UTF-8 at {hash_url}: {error}"))?
|
||||
.map_err(|error| format!("官方 hash sidecar 不是 UTF-8:{hash_url}:{error}"))?
|
||||
.trim();
|
||||
text.parse::<u32>().map_err(|error| {
|
||||
format!("Official hash sidecar is not decimal xxHash32 at {hash_url}: {error}")
|
||||
})
|
||||
text.parse::<u32>()
|
||||
.map_err(|error| format!("官方 hash sidecar 不是十进制 xxHash32:{hash_url}:{error}"))
|
||||
}
|
||||
|
||||
fn xxhash32(bytes: &[u8]) -> u32 {
|
||||
@@ -1649,6 +1719,37 @@ fi
|
||||
assert!(service.audit_local_manifest(&plan).unwrap().is_clean());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_local_verification_checks_manifest_and_official_hash_pairs() {
|
||||
let out_dir = TempDir::new().unwrap();
|
||||
let bin_dir = TempDir::new().unwrap();
|
||||
let curl_path = bin_dir.path().join("curl");
|
||||
write_fake_curl(&curl_path);
|
||||
|
||||
let service = OfficialResourcePullService::with_curl_command(out_dir.path(), &curl_path);
|
||||
let plan = build_official_pull_plan(discovery_plan(), inventory());
|
||||
service.pull(&plan).unwrap();
|
||||
|
||||
let verification = service.verify_local_download_manifest().unwrap();
|
||||
assert!(verification.is_clean());
|
||||
assert_eq!(verification.items.len(), 7);
|
||||
assert_eq!(verification.verified_count(), 7);
|
||||
assert_eq!(verification.official_hash_pair_count, 1);
|
||||
assert_eq!(verification.official_hash_verified_count, 1);
|
||||
|
||||
let data_path = service
|
||||
.destination_for_url(
|
||||
"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(data_path, b"corrupted").unwrap();
|
||||
|
||||
let corrupted = service.verify_local_download_manifest().unwrap();
|
||||
assert!(!corrupted.is_clean());
|
||||
assert_eq!(corrupted.failure_count(), 1);
|
||||
assert_eq!(corrupted.official_hash_errors.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refreshes_official_hash_pairs_and_skips_manifest_verified_content() {
|
||||
let out_dir = TempDir::new().unwrap();
|
||||
@@ -1833,7 +1934,7 @@ fi
|
||||
};
|
||||
|
||||
let error = service.pull(&plan).unwrap_err();
|
||||
assert!(error.contains("official hash mismatch"));
|
||||
assert!(error.contains("官方 hash 校验失败"));
|
||||
|
||||
let manifest = service.read_download_manifest().unwrap();
|
||||
assert!(manifest.entries.is_empty());
|
||||
|
||||
Reference in New Issue
Block a user