mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 07:16:45 +08:00
fix(official-sync): 修正对抗性审查发现的 launcher 链错误码误归类
对 d2466d6 做三视角对抗性核查(错误码语义/消费者破坏面/测试与文档),
确认 8 项(去重后 5 类)误归类,全部集中在 official_game_main_config
的 From<String> 兜底面。逐项修正:
- launcher_package_url 改返回 DownloadError:非官方 CDN 根(来自远端
API 响应,安全边界拒绝)→ NON_OFFICIAL_URL;非法包路径(来自远端
manifest 内容)→ LAUNCHER_RESPONSE_INVALID。此前提交说明称其为
"硬编码常量上的内部不变量"不成立——三个生产调用点传入的都是远端
API 下发的 cdn_root。
- select_game_main_config_source / find_resources_assets:目录 source
缺 resources.assets 条目、无可用游戏包路径、包内容缺必需文件均为
远端可触发的内容缺陷 → LAUNCHER_RESPONSE_INVALID(此前落 INTERNAL,
且官方 manifest 布局已演进过一次,是现实的主要失败面)。
- extract_archive 解压失败 → ZIP_STRUCTURE_INVALID:结构校验不覆盖
压缩数据流,数据区损坏(bad CRC)在 unzip 阶段首次暴露,主导成因
是损坏/截断下载,归完整性域;本地原因保留在 stderr 消息中。
- verify_manifest_file_size 三种失败分码:size 字段无效 →
LAUNCHER_RESPONSE_INVALID、本地读文件失败 → INTERNAL、真正不符 →
SIZE_MISMATCH(此前三者统归 SIZE_MISMATCH)。
- 空 --launcher-version(用户 CLI 输入)→ INVALID_ARGUMENT,在发起
任何请求前拒绝(此前落 INTERNAL)。
- 统一 launcher 链内容口径:API 响应、远端 manifest、包内容的解析/
缺失问题全部归 LAUNCHER_RESPONSE_INVALID(600004),
MANIFEST_PARSE_FAILED(600001) 保留给资源侧 manifest/catalog;
fetch_remote_manifest 两处 600001 改 600004,error_code.rs 注释与
USERGUIDE 描述同步。
- download_file_with_fallback:备用 URL 构造失败不再经 `?` 丢弃主地址
失败上下文,错误码与"两次都失败以最终一次为准"策略一致。
新增 7 个错误码断言测试(launcher_package_url 两类拒绝、空版本、
manifest 无条目、fallback 终码保留、fallback 构造失败保留主上下文、
select/verify/extract 各失败面)。全量 fmt / clippy --workspace
--all-targets -D warnings / test --workspace 全绿。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -217,16 +217,19 @@ impl OfficialLauncherBootstrapService {
|
||||
command
|
||||
})?;
|
||||
|
||||
// launcher 链内容问题(API 响应、远端 manifest、包内容)统一归
|
||||
// LAUNCHER_RESPONSE_INVALID;MANIFEST_PARSE_FAILED 保留给资源侧
|
||||
// manifest / Addressables catalog。
|
||||
let manifest: YostarJpLauncherRemoteManifest = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|error| {
|
||||
DownloadError::new(
|
||||
ErrorCode::MANIFEST_PARSE_FAILED,
|
||||
ErrorCode::LAUNCHER_RESPONSE_INVALID,
|
||||
format!("解析官方启动器 manifest 失败:{error}"),
|
||||
)
|
||||
})?;
|
||||
if manifest.files.is_empty() {
|
||||
return Err(DownloadError::new(
|
||||
ErrorCode::MANIFEST_PARSE_FAILED,
|
||||
ErrorCode::LAUNCHER_RESPONSE_INVALID,
|
||||
"官方启动器远端 manifest 没有文件条目",
|
||||
));
|
||||
}
|
||||
@@ -258,6 +261,14 @@ impl OfficialLauncherBootstrapService {
|
||||
/// Fetches a signed official launcher API endpoint and parses the generic
|
||||
/// response envelope.
|
||||
pub fn fetch_launcher_envelope(&self, path: &str) -> Result<LauncherEnvelope, DownloadError> {
|
||||
// launcher_version 是用户可控的 CLI 输入(--launcher-version),空值是
|
||||
// 参数错误而非内部错误,在进入签名流程前显式归 INVALID_ARGUMENT。
|
||||
if self.launcher_version.is_empty() {
|
||||
return Err(DownloadError::new(
|
||||
ErrorCode::INVALID_ARGUMENT,
|
||||
"启动器版本不能为空",
|
||||
));
|
||||
}
|
||||
let url = launcher_api_url(path)?;
|
||||
let authorization = launcher_authorization_header(&self.launcher_version, "", None)?;
|
||||
|
||||
@@ -363,12 +374,21 @@ pub fn is_official_launcher_package_url(url: &str) -> bool {
|
||||
|
||||
/// Builds an official launcher package URL from an official CDN root and a
|
||||
/// relative package path.
|
||||
pub fn launcher_package_url(cdn_root: &str, file_path: &str) -> Result<String, String> {
|
||||
///
|
||||
/// 生产调用方传入的 `cdn_root` 来自官方 API 的远端响应(`fetch_cdn_config`),
|
||||
/// `file_path` 来自远端 manifest 内容——两者都是运行时外部输入而非硬编码常量,
|
||||
/// 因此这里的拒绝必须携带准确错误码:非官方根地址是安全边界拒绝
|
||||
/// (NON_OFFICIAL_URL),非法包路径是远端内容缺陷(LAUNCHER_RESPONSE_INVALID)。
|
||||
pub fn launcher_package_url(cdn_root: &str, file_path: &str) -> Result<String, DownloadError> {
|
||||
if !is_official_launcher_package_url(cdn_root) {
|
||||
return Err(format!("启动器 CDN 根地址不是官方地址:{cdn_root}"));
|
||||
return Err(DownloadError::new(
|
||||
ErrorCode::NON_OFFICIAL_URL,
|
||||
format!("启动器 CDN 根地址不是官方地址:{cdn_root}"),
|
||||
));
|
||||
}
|
||||
|
||||
validate_launcher_package_path(file_path)?;
|
||||
validate_launcher_package_path(file_path)
|
||||
.map_err(|error| DownloadError::new(ErrorCode::LAUNCHER_RESPONSE_INVALID, error))?;
|
||||
Ok(format!(
|
||||
"{}/{}",
|
||||
cdn_root.trim_end_matches('/'),
|
||||
@@ -703,6 +723,57 @@ exit 22
|
||||
assert_eq!(error.code().id(), ErrorCode::NON_OFFICIAL_URL.id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_package_url_maps_rejections_to_error_codes() {
|
||||
// cdn_root 来自远端 API 响应:非官方根是安全边界拒绝。
|
||||
let error = launcher_package_url(
|
||||
"https://launcher-pkg-ba-jp.bluearchive.cafe",
|
||||
"prod/game.zip",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code().id(), ErrorCode::NON_OFFICIAL_URL.id());
|
||||
|
||||
// file_path 来自远端 manifest 内容:非法路径是远端内容缺陷。
|
||||
let error = launcher_package_url(
|
||||
"https://launcher-pkg-ba-jp.yo-star.com",
|
||||
"prod/../escape.zip",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code().id(), ErrorCode::LAUNCHER_RESPONSE_INVALID.id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_launcher_version_maps_to_invalid_argument() {
|
||||
// 空 --launcher-version 是用户参数错误,应在发起任何请求前归
|
||||
// INVALID_ARGUMENT(curl 指向不存在的路径以证明未发起请求)。
|
||||
let service = OfficialLauncherBootstrapService::with_curl_command(
|
||||
"",
|
||||
"/nonexistent/curl-must-not-run",
|
||||
);
|
||||
let error = service.fetch_game_config().unwrap_err();
|
||||
assert_eq!(error.code().id(), ErrorCode::INVALID_ARGUMENT.id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_manifest_without_files_maps_to_launcher_response_invalid() {
|
||||
let bin_dir = TempDir::new().unwrap();
|
||||
let curl_path = bin_dir.path().join("curl");
|
||||
write_shell_script(
|
||||
&curl_path,
|
||||
r#"#!/bin/sh
|
||||
printf '%s' '{"source":"prod/game.zip","file":[]}'
|
||||
"#,
|
||||
);
|
||||
let service = OfficialLauncherBootstrapService::with_curl_command(
|
||||
"1.7.2",
|
||||
curl_path.to_string_lossy().to_string(),
|
||||
);
|
||||
let error = service
|
||||
.fetch_remote_manifest("https://launcher-pkg-ba-jp.yo-star.com/prod/manifest.json")
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code().id(), ErrorCode::LAUNCHER_RESPONSE_INVALID.id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_transient_launcher_api_failures() {
|
||||
let bin_dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user