fix(official-sync): 拒绝携带 query/fragment 的资源 URL

destination_for_url 原来对每个路径片段静默剥除 query/fragment,导致仅 query
不同的两个官方 URL 映射到同一目标文件而相互覆盖,并使每轮 hash 复用校验失配、
反复重下。官方资源 URL 本不携带 query(发现 URL 由固定拼接生成),改为直接拒绝,
消除该碰撞路径。新增单测。

对应 issue #18 维护清单 2-3。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 08:03:40 -07:00
co-authored by Claude Fable 5
parent 02d5bc537e
commit 42d69dd4bd
+29 -3
View File
@@ -1320,9 +1320,11 @@ impl OfficialResourcePullService {
return Err(format!("官方 URL 包含不安全路径片段:{url}"));
}
let segment = segment.split(['?', '#']).next().unwrap_or(segment);
if segment.is_empty() {
continue;
// 官方资源 URL 不携带 query/fragment。若出现则直接拒绝,而非静默剥除——
// 否则仅 query 不同的两个 URL 会映射到同一目标文件而相互覆盖,
// 并导致每轮 hash 复用校验失配、反复重下。
if segment.contains('?') || segment.contains('#') {
return Err(format!("官方资源 URL 不允许包含 query 或 fragment{url}"));
}
relative_destination.push(sanitize_segment(segment));
}
@@ -2193,6 +2195,30 @@ exit 22
assert!(error.contains("危险路径"));
}
#[test]
fn rejects_resource_url_with_query_or_fragment() {
let temp = TempDir::new().unwrap();
let service = OfficialResourcePullService::new(temp.path().join("out"));
for url in [
"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes?v=1",
"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes#frag",
] {
let error = service.destination_for_url(url).unwrap_err();
assert!(
error.contains("query 或 fragment"),
"unexpected error for {url}: {error}"
);
}
// 无 query 的正常资源 URL 仍可映射。
assert!(service
.destination_for_url(
"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes",
)
.is_ok());
}
#[cfg(unix)]
#[test]
fn pull_rejects_symlink_parent_escape() {