Repository navigation
Feat/GetServerCustomResources - #4
Applesaber wants to merge 7 commits into
Conversation
复用游戏正常的 GetGameSettingApi 响应作为信令通道:服务器在根对象追加 _appleChuResources,客户端按条件选中条目、下载 Option 包、RSA 验签, 并在内存中挂到 option 根,资源不落盘(仅缓存包本体)。 架构骨架对齐 AquaMai ServerResources,差异在载荷:AquaMai 是 Unity AssetBundle,chusan 是 Option 包(AXXX 目录树)。 - net_packet: 在 WinHTTP 响应层取完整响应体与请求路径,解析 JSON 后 广播给订阅者;分发器 sub_99FA60 需固定 RVA,故先不挂钩 - json: 零依赖最小 JSON(读+写),保留键序与数字字面量以保证回写不变; 兼容整数被序列化为字符串(MinatoCore SegaStringSerializer) - entry: 条目解析与条件筛选,游戏版本取自同一响应的 romVersion - download: 进程内 WinHTTP 直连下载,不硬编码服务器地址 - crypto/hash: RSA PKCS#1 v1.5 + SHA-256 验签、base64 解码、内存版 SHA-256 - package: Option 包(zip)解析为内存目录树,校验单根 AXXX - handle/vfs_bridge: 目录枚举注入虚拟 AXXX 根,文件内容由内存服务; 文件句柄用真实 \\.\NUL 承载,避免伪造 HANDLE
32 位目标上 `usize >> 32` 会触发 deny(arithmetic_overflow), 先转 u64 再取高低位。
取包桥在内部订阅者处理完后,把 `{"api":..,"response":..}` 广播到
`net_packet` 主题,模组可据此读取或协作处理响应,对应需求文档
「模组和服务器通讯」一节。
- dispatcher: 挂钩 sub_99FA60(特征码定位),直接从原生收包分发器拿到 逻辑 API 名与明文响应,取代从 URL 猜名;WinHTTP 层保留为兜底, 由 [ServerResources].native_dispatcher 切换 - power_on_gate: 挂钩 projGame::PowerOn 状态机每帧更新(虚表 0x19249BC 第 0 槽),下载期间停在当前帧,结束或超时(60s)放行 - deflate: 复用游戏内嵌 zlib 1.2.8,经子 9AB850 解压;zip 的 deflate 条目不再报错,改为真正解压 - 新增配置项 native_dispatcher / hold_boot
审查者指南该 PR 在 x86 Windows 客户端中实现服务器自定义 Option 资源链路:从游戏网络响应发现并筛选资源,下载后进行 RSA 验签和缓存,解析 ZIP 为内存文件树,再通过 Windows 文件 API 钩子注入 VFS;同时提供原生分发器/WinHTTP 双路径、开机阻塞保护和网络响应广播。 加载服务器下发的 Option 资源的时序图sequenceDiagram
participant Game as Game
participant Bridge as DispatcherOrWinHTTP
participant Resources as ServerResources
participant Server as ResourceServer
participant Crypto as RSA
participant Package as OptionPackage
participant VFS as VFS
Game->>Bridge: GetGameSettingApi response
Bridge->>Resources: on_net_response(api_name, body)
Resources->>Resources: json::parse(body)
Resources->>Resources: select_entry(entries, locale, game_version)
Resources->>Server: download(url)
Server-->>Resources: package bytes
Resources->>Crypto: verify_signature(public_key, bytes, sign)
Crypto-->>Resources: valid signature
Resources->>Package: OptionPackage::parse(bytes)
Package-->>Resources: in-memory file tree
Resources->>VFS: package becomes available
Game->>VFS: CreateFileW / ReadFile / GetFileSize
VFS-->>Game: virtual Option file content
Option 包分发与虚拟文件访问流程图flowchart TD
Start["GetGameSettingApi response"] --> Parse["Parse JSON"]
Parse --> Field{"_appleChuResources present?"}
Field -- No --> Ignore["Ignore response"]
Field -- Yes --> Match["Filter entries by version and locale"]
Match --> Selected{"Entry selected?"}
Selected -- No --> Ignore
Selected -- Yes --> CacheCheck{"Valid signed cache?"}
CacheCheck -- Yes --> Load["Load cached package"]
CacheCheck -- No --> Download["Download resource URL"]
Download --> Verify{"RSA signature valid?"}
Verify -- No --> Fail["Reject package"]
Verify -- Yes --> Save["Write cache"]
Save --> Load
Load --> Zip["Parse ZIP and build memory file tree"]
Zip --> Inject["Inject via Windows file API hooks"]
Inject --> Read["Game reads virtual Option files"]
文件级变更
提示与命令与 Sourcery 交互
自定义使用体验访问你的控制面板:
获取帮助Original review guide in EnglishReviewer's Guide该 PR 在 x86 Windows 客户端中实现服务器自定义 Option 资源链路:从游戏网络响应发现并筛选资源,下载后进行 RSA 验签和缓存,解析 ZIP 为内存文件树,再通过 Windows 文件 API 钩子注入 VFS;同时提供原生分发器/WinHTTP 双路径、开机阻塞保护和网络响应广播。 Sequence diagram for loading server-delivered Option resourcessequenceDiagram
participant Game as Game
participant Bridge as DispatcherOrWinHTTP
participant Resources as ServerResources
participant Server as ResourceServer
participant Crypto as RSA
participant Package as OptionPackage
participant VFS as VFS
Game->>Bridge: GetGameSettingApi response
Bridge->>Resources: on_net_response(api_name, body)
Resources->>Resources: json::parse(body)
Resources->>Resources: select_entry(entries, locale, game_version)
Resources->>Server: download(url)
Server-->>Resources: package bytes
Resources->>Crypto: verify_signature(public_key, bytes, sign)
Crypto-->>Resources: valid signature
Resources->>Package: OptionPackage::parse(bytes)
Package-->>Resources: in-memory file tree
Resources->>VFS: package becomes available
Game->>VFS: CreateFileW / ReadFile / GetFileSize
VFS-->>Game: virtual Option file content
Flow diagram for Option package delivery and virtual file accessflowchart TD
Start["GetGameSettingApi response"] --> Parse["Parse JSON"]
Parse --> Field{"_appleChuResources present?"}
Field -- No --> Ignore["Ignore response"]
Field -- Yes --> Match["Filter entries by version and locale"]
Match --> Selected{"Entry selected?"}
Selected -- No --> Ignore
Selected -- Yes --> CacheCheck{"Valid signed cache?"}
CacheCheck -- Yes --> Load["Load cached package"]
CacheCheck -- No --> Download["Download resource URL"]
Download --> Verify{"RSA signature valid?"}
Verify -- No --> Fail["Reject package"]
Verify -- Yes --> Save["Write cache"]
Save --> Load
Load --> Zip["Parse ZIP and build memory file tree"]
Zip --> Inject["Inject via Windows file API hooks"]
Inject --> Read["Game reads virtual Option files"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
_appleChuResources 与 AquaMai 的 _aquaMaiResources 结构一致,仅换品牌 前缀。为让服务端一份响应同时服务两个客户端,版本范围字段同时接受 minimum/maximumAquaMaiVersion(AppleChu 命名优先)。 另外下发字段除根级外,再兼容嵌在 gameSetting 内的情况。
There was a problem hiding this comment.
你好——我发现了 14 个问题
给 AI Agent 的提示
请处理这次代码审查中的评论。
## 单独评论
### 评论 1
**零字节读取会消耗文件数据**
当调用方从虚拟句柄请求读取零字节时,`handle::read` 会将其视为读取剩余文件,推进位置,并由 `hooked_read_file` 将这些字节复制到调用方的缓冲区。Win32 将此定义为无操作,因此调用方会看到错误的字节数,且其缓冲区可能被覆盖。
应返回成功的零字节结果,不复制数据,也不推进虚拟文件位置。
另见 `packages/applechu/src/server_resources/vfs_bridge.rs:311-330`。
### 评论 2
**包文件无法被发现**
当选项目录存在,或游戏在虚拟包中进行枚举时,`hooked_find_first_file_w` 会返回未合并包根目录的物理列表,或者创建只有根目录而没有子项的合成列表。嵌套包枚举会落回物理文件系统,因此游戏无法发现已下载的资源。
应将虚拟包条目合并到物理列表中,并为每个虚拟目录合成相应条目。
### 评论 3
**混合大小写的包路径会被遗漏**
当 ZIP 条目包含大写字符(例如 `AXXX` 根目录或 `Music.xml`)时,`normalize_path` 会将请求路径转换为小写,但 `OptionPackage::contains` 会与保留原始大小写的 ZIP 条目名称进行区分大小写的比较。查找失败后,`CreateFileW` 会落回磁盘,游戏无法打开虚拟文件。
应使包查找不区分大小写,或统一规范化归档条目名称和请求 key。
### 评论 4
**相对于文件末尾的定位结果不正确**
调用方使用 `FILE_END` 定位虚拟文件时,两个虚拟文件定位钩子都会对除 `FILE_BEGIN` 之外的来源使用当前位置,因此 `FILE_END` 使用的是光标而不是文件长度。调用方会定位到错误偏移并读取错误字节。
对于 `FILE_END` 定位,应使用虚拟文件长度作为基准。
### 评论 5
**下载失败会阻止重试**
当首个选定的包下载或加载失败,随后又收到另一个匹配响应时,`fetch_and_load` 失败后 `DOWNLOAD_STARTED` 仍为 true,因此 `start_download` 会忽略后续匹配响应。该包在本次进程剩余时间内都不可用。
下载或加载失败时应清除 `DOWNLOAD_STARTED`,以便后续匹配响应能够重试。
### 评论 6
**WinHTTP 调用会进入已卸载的钩子**
安装 WinHTTP 回退机制后,如果模块关闭或卸载,`net_packet::install` 会替换游戏的 WinHTTP IAT 条目,但关闭操作不会恢复它们。之后的 WinHTTP 调用仍会进入已关闭或已卸载模块中的 detour,可能导致进程崩溃。
应在关闭时恢复 WinHTTP 的 IAT 条目。
### 评论 7
**钩子调用会丢失进行中的操作**
当游戏线程在钩子安装过程中进入该钩子时,`hook_enable` 会先使 detour 可调用,而 `ORIGINAL` 或下一个函数指针尚未保存。detour 看到空指针后无法转发调用,导致响应丢失或进行中的操作中断。
应在启用 detour 之前发布每个 trampoline 或下一个函数指针。
### 评论 8
**虚拟打开失败会泄漏句柄**
当虚拟句柄表已满,或 NUL 打开成功后注册以其他方式失败时,`hooked_create_file_w` 会落回原始路径,却没有在 `handle::register` 失败时关闭已经打开的 NUL 句柄。反复失败会泄漏内核句柄,并可能耗尽进程句柄上限。
在落回原始路径之前,只要注册失败就应关闭 NUL 句柄。
### 评论 9
**有效 URL 无法解析**
当 URL 没有路径但包含查询,或使用 IPv6 主机时,`ParsedUrl::parse` 会将无路径查询包含在 authority 中,并把 IPv6 字面量最后的冒号错误地当作端口分隔符。随后 `WinHttpConnect` 会收到无效的主机或端口,导致下载失败。
应按照各自的 URL 语法解析 authority、查询和 IPv6 主机。
### 评论 10
**HTTPS 下载失败**
当选定资源 URL 使用 HTTPS 时,`download` 将 `WINHTTP_FLAG_SECURE_DEFAULTS` 传给 `WinHttpOpenRequest`,但该标志属于 `WinHttpOpen`。WinHTTP 会在发送请求前拒绝它。
应从请求标志中移除 `WINHTTP_FLAG_SECURE_DEFAULTS`,如有需要,仅在创建会话时使用它。
### 评论 11
**虚拟文件存在性检查失败**
当游戏检查一个只存在于已下载包中的文件属性时,`platform::path_hook` 会将 `GetFileAttributesW` 重定向到物理选项目录,但 VFS bridge 不会拦截该 API,也不会检查内存中的包。先检查虚拟文件属性再打开文件的游戏加载器会认为文件不存在并跳过它。
应添加支持包查询的 `GetFileAttributesW` 处理,并在落回路径钩子之前处理游戏使用的其他对应属性 API。
### 评论 12
**格式错误的响应仍能通过测试**
当 `dispatch` 错误地为非 JSON 正文调用订阅者时,`dispatch_ignores_non_json_bodies` 虽然在 `net_packet.rs:380-381` 调用 `dispatch`,却没有断言,也没有注册订阅者。因此,即使错误的 dispatcher 将格式错误的响应发送给订阅者,测试仍会通过。
应注册一个记录调用次数的处理器,并断言派发格式错误正文后调用次数仍为零。
### 评论 13
**闸门测试会在共享状态上发生竞态**
Rust 并发运行闸门测试时,`stale_start_time_opens_the_gate` 会修改 `power_on_gate.rs:186-191` 的全局闸门状态,而 `gate_is_open_by_default` 在第 169 行假定状态已清除;并行执行可能使任一测试在另一个测试断言期间修改状态,导致正确代码也间歇性失败。
应串行运行这些测试,或为每个测试提供隔离的闸门状态。
### 评论 14
**Base64 测试向量无法通过测试**
运行 `base64_round_trips_known_vectors` 测试时,它会将 `+/8=` 解码得到的两个字节与第 143 行字符串 `"\u{ff}\u{fe}"` 的四个 UTF-8 字节进行比较,因此即使解码器正确,测试也会失败。
应为解码后的二进制向量使用字节切片作为期望值,或将该向量替换为期望字符串 UTF-8 字节对应的 Base64 编码。Sourcery 评估
需要人工审查。 请先处理 14 个发现;如果签名验证、原生钩子或虚拟文件处理存在错误,启用后的安装可能会接受或提供非预期的资源内容,或者冻结启动状态机并造成中断。回滚只能阻止未来运行中的功能,但已下载的缓存数据以及已经提供的资源会在回滚后继续存在,需要清理或重启。
阻塞性发现:packages/applechu/src/server_resources/handle.rs:79、packages/applechu/src/server_resources/vfs_bridge.rs:220、packages/applechu/src/server_resources/vfs_bridge.rs:181、packages/applechu/src/server_resources/vfs_bridge.rs:377、packages/applechu/src/server_resources/mod.rs:195,以及另外 9 项
Original comment in English
Hey - I've found 14 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="packages/applechu/src/server_resources/handle.rs" line_range="77-79" />
<code_context>
+ let file = files.get_mut(&handle)?;
+ let remaining = file.length.saturating_sub(file.position);
+ let count = if requested == 0 {
+ remaining
+ } else {
+ requested.min(remaining)
+ };
+ let offset = file.position;
</code_context>
<issue_to_address>
**Zero-byte reads consume file data**
When a caller requests zero bytes from a virtual handle, `handle::read` treats a zero-byte request as a request for the remaining file, advances the position, and `hooked_read_file` copies those bytes into the caller’s buffer. Win32 defines this as a no-op, so the caller sees an incorrect byte count and can have its buffer overwritten.
Return a successful zero-byte result without copying data or advancing the virtual file position.
Also at `packages/applechu/src/server_resources/vfs_bridge.rs:311-330`.
</issue_to_address>
### Comment 2
<location path="packages/applechu/src/server_resources/vfs_bridge.rs" line_range="220" />
<code_context>
+ // 先让真实枚举跑一遍:目录存在时交给它(本功能只做增量挂载)
+ let real = next(path, data);
+ if real != INVALID_HANDLE_VALUE {
+ return real;
+ }
+ // 目录不存在:用合成句柄承载注入条目
</code_context>
<issue_to_address>
**Package files stay undiscoverable**
When the option directory exists or the game enumerates inside the virtual package, `hooked_find_first_file_w` returns the physical listing without merging the package root, or creates a synthetic listing with only that root and no children. Nested package enumeration falls through to the physical filesystem, so the game cannot discover the downloaded resources.
Merge virtual package entries into physical listings and synthesize the entries for each virtual directory.
Also at `packages/applechu/src/server_resources/vfs_bridge.rs:163-170`, `packages/applechu/src/server_resources/vfs_bridge.rs:223-227`, `packages/applechu/src/server_resources/vfs_bridge.rs:231`, `packages/applechu/src/server_resources/vfs_bridge.rs:243`.
</issue_to_address>
### Comment 3
<location path="packages/applechu/src/server_resources/vfs_bridge.rs" line_range="177-181" />
<code_context>
+/// 游戏请求的是重定向前的源路径 `c:\mount\option\*`,因此用源前缀比对。
+fn option_root_glob(path: &str) -> Option<String> {
+ let source = vfs::option_source_prefix();
+ let normalized = winapi::normalize_path(path);
+ let directory = normalized.trim_end_matches('\\').trim_end_matches("*");
+ if directory.trim_end_matches('\\') != source.trim_end_matches('\\') {
+ return None;
+ }
+ crate::server_resources::package().map(|package| package.root().to_owned())
+}
+
+/// 路径是否指向虚拟包内文件,返回包内 key
+fn package_key(path: &str) -> Option<String> {
+ let package = crate::server_resources::package()?;
+ let source = vfs::option_source_prefix();
+ let normalized = winapi::normalize_path(path);
+ let prefix = format!("{}\\", source.trim_end_matches('\\'));
+ let relative = normalized.strip_prefix(&prefix)?;
+ let key = relative.replace('\\', "/");
+ package.contains(&key).then_some(key)
+}
+
</code_context>
<issue_to_address>
**Mixed-case package paths are missed**
When a ZIP entry contains uppercase characters, such as the `AXXX` root or `Music.xml`, `normalize_path` lowercases the requested path, but `OptionPackage::contains` compares it case-sensitively with ZIP entry names that retain their original case. The lookup misses, `CreateFileW` falls through to disk, and the game cannot open the virtual file.
Make package lookup case-insensitive or normalize archive entry names and requested keys consistently.
Also at `packages/applechu/src/server_resources/package.rs:82`.
</issue_to_address>
### Comment 4
<location path="packages/applechu/src/server_resources/vfs_bridge.rs" line_range="377" />
<code_context>
+ method: u32,
+) -> u32 {
+ if let Some((_, position)) = handle::size_and_position(file as usize) {
+ let base = if method == FILE_BEGIN { 0isize } else { position as isize };
+ let target = base.saturating_add(distance as isize).max(0) as usize;
+ if handle::seek(file as usize, target).is_some() {
</code_context>
<issue_to_address>
**End-relative seeks land incorrectly**
When a caller seeks a virtual file with `FILE_END`, both virtual-file seek hooks use the current position for every origin other than `FILE_BEGIN`, so `FILE_END` uses the cursor instead of the file length. The caller lands at the wrong offset and reads incorrect bytes.
Use the virtual file length as the base for `FILE_END` seeks.
Also at `packages/applechu/src/server_resources/vfs_bridge.rs:401-408`.
</issue_to_address>
### Comment 5
<location path="packages/applechu/src/server_resources/mod.rs" line_range="193-195" />
<code_context>
+/// 下载与解析放在独立线程,避免阻塞收包线程;期间由开机闸门把
+/// `projGame::PowerOn` 状态机停在当前帧,下载结束后放行。
+fn start_download(selected: &entry::ServerResourcesEntry) {
+ if DOWNLOAD_STARTED.swap(true, Ordering::AcqRel) {
+ return;
+ }
+ power_on_gate::begin_block();
+ let url = selected.url.clone();
</code_context>
<issue_to_address>
**Failed downloads block retries**
When the first selected package fails to download or load and another matching response arrives, `DOWNLOAD_STARTED` remains true after `fetch_and_load` fails, so `start_download` ignores later matching responses. The package stays unavailable for the rest of the process.
Clear `DOWNLOAD_STARTED` when the download or load fails so a later matching response can retry.
</issue_to_address>
### Comment 6
<location path="packages/applechu/src/server_resources/mod.rs" line_range="109-110" />
<code_context>
+/// 关闭:还原钩子并释放虚拟句柄
+pub(crate) fn shutdown() {
+ power_on_gate::shutdown();
+ dispatcher::shutdown();
+ vfs_bridge::shutdown();
+}
+
</code_context>
<issue_to_address>
**WinHTTP calls reach unloaded hooks**
When the WinHTTP fallback is installed and the module shuts down or unloads, `net_packet::install` replaces the game’s WinHTTP IAT entries, but shutdown does not restore them. Later WinHTTP calls still enter detours in the shut-down or unloaded module, risking a process crash.
Restore the WinHTTP IAT entries during shutdown.
Also at `packages/applechu/src/server_resources/net_packet.rs:88-94`, `packages/applechu/src/server_resources/mod.rs:92-94`.
</issue_to_address>
### Comment 7
<location path="packages/applechu/src/server_resources/dispatcher.rs" line_range="46-50" />
<code_context>
+ let Some(trampoline) = api.hook_create(target, hooked_dispatcher as *const () as usize) else {
+ return Err("client dispatcher hook could not be created".to_owned());
+ };
+ if !api.hook_enable(target) {
+ api.hook_remove(target);
+ return Err("client dispatcher hook could not be enabled".to_owned());
+ }
+ ORIGINAL.store(trampoline, std::sync::atomic::Ordering::Release);
+ TARGET.store(target, std::sync::atomic::Ordering::Release);
+ api.log_info(&format!(
</code_context>
<issue_to_address>
**Hook calls lose in-flight operations**
When a game thread enters a hook while it is being installed, `hook_enable` makes the detour callable before `ORIGINAL` or the next-function pointer is stored. The detour sees a zero pointer and fails to forward the call, dropping the response or disrupting the in-flight operation.
Publish each trampoline or next-function pointer before enabling its detour.
Also at `packages/applechu/src/server_resources/net_packet.rs:88-97`, `packages/applechu/src/server_resources/vfs_bridge.rs:117-121`, `packages/applechu/src/server_resources/power_on_gate.rs:72-79`.
</issue_to_address>
### Comment 8
<location path="packages/applechu/src/server_resources/vfs_bridge.rs" line_range="296-299" />
<code_context>
+ // \\.\NUL 必然可打开,且 CloseHandle 语义正常
+ let nul = winapi::string_to_wide("\\\\.\\NUL");
+ let real = next(nul.as_ptr(), access, share, security, creation, flags, template);
+ if real != INVALID_HANDLE_VALUE && handle::register(real as usize, &key, length) {
+ return real;
+ }
+ }
+ }
</code_context>
<issue_to_address>
**Failed virtual opens leak handles**
When the virtual-handle table is full or registration otherwise fails after a NUL open succeeds, `hooked_create_file_w` falls through to the original path without closing the already-opened NUL handle when `handle::register` fails. Repeated failures leak kernel handles and can exhaust the process handle limit.
Close the NUL handle whenever registration fails before falling through.
</issue_to_address>
### Comment 9
<location path="packages/applechu/src/server_resources/download.rs" line_range="189-192" />
<code_context>
+ return Err(format!("unsupported url scheme: {url}"));
+ };
+
+ let (authority, path_and_query) = match rest.find('/') {
+ Some(index) => (&rest[..index], &rest[index..]),
+ None => (rest, "/"),
+ };
+ if authority.is_empty() {
+ return Err(format!("url has no host: {url}"));
</code_context>
<issue_to_address>
**Valid URLs fail to parse**
When the URL has a query without a path or uses an IPv6 host, `ParsedUrl::parse` includes a pathless query in the authority and splits IPv6 literals at their final colon as if it were a port. `WinHttpConnect` then receives an invalid host or port and the download fails.
Parse the URL authority, query, and IPv6 host according to their respective URL syntax.
Also at `packages/applechu/src/server_resources/download.rs:197-205`.
</issue_to_address>
### Comment 10
<location path="packages/applechu/src/server_resources/download.rs" line_range="53" />
<code_context>
+ let capacity = u32::from_le_bytes(buffer[0x14..0x18].try_into().ok()?);
+ let data = if capacity >= SSO_CAPACITY {
+ u32::from_le_bytes(buffer[0..4].try_into().ok()?) as usize as *const u8
+ } else {
+ buffer.as_ptr()
+ };
</code_context>
<issue_to_address>
**HTTPS downloads fail**
When the selected resource URL uses HTTPS, `download` passes `WINHTTP_FLAG_SECURE_DEFAULTS` to `WinHttpOpenRequest`, although that flag belongs to `WinHttpOpen`. WinHTTP rejects the request before sending it.
Remove `WINHTTP_FLAG_SECURE_DEFAULTS` from the request flags and apply it only when creating the session, if needed.
</issue_to_address>
### Comment 11
<location path="packages/applechu/src/server_resources/vfs_bridge.rs" line_range="89-103" />
<code_context>
+ bind(api, "FindFirstFileW", hooked_find_first_file_w as *const (), &NEXT_FIND_FIRST_FILE_W)?;
</code_context>
<issue_to_address>
**Virtual file existence checks fail**
When the game checks attributes for a file that exists only in the downloaded package, `platform::path_hook` redirects `GetFileAttributesW` to the physical option directory, but the VFS bridge does not intercept that API or check the in-memory package. A game loader that checks a virtual file’s attributes before opening it sees the file as missing and skips it.
Add package-aware `GetFileAttributesW` handling (and any corresponding attribute APIs the game uses) before falling through to the path hook.
</issue_to_address>
### Comment 12
<location path="packages/applechu/src/server_resources/net_packet.rs" line_range="380-381" />
<code_context>
+ fn dispatch_ignores_non_json_bodies() {
+ // Given: 一段非 JSON 响应。
+ // When/Then: 派发不 panic 且不触发任何处理(无订阅者时也安全)。
+ dispatch("/x/GetGameSettingApi", b"not json at all");
+ dispatch("/x/GetGameSettingApi", &[0xff, 0xfe, 0x00]);
+ }
+}
</code_context>
<issue_to_address>
**Malformed responses still pass**
When `dispatch` incorrectly invokes subscribers for a non-JSON body, `dispatch_ignores_non_json_bodies` calls `dispatch` at `net_packet.rs:380-381` but makes no assertion and registers no subscriber, so a broken dispatcher that sends malformed responses to subscribers still passes.
Register a handler that records invocations and assert that dispatching malformed bodies leaves the count at zero.
</issue_to_address>
### Comment 13
<location path="packages/applechu/src/server_resources/power_on_gate.rs" line_range="186-191" />
<code_context>
+ #[test]
+ fn stale_start_time_opens_the_gate() {
+ // Given: 冻结但开始时刻被设为 0(异常状态)。
+ BLOCKING.store(true, Ordering::Release);
+ BLOCK_STARTED.store(0, Ordering::Release);
+
+ // Then: 判定为不阻塞,避免永久挂起。
+ assert!(!blocking());
+ BLOCKING.store(false, Ordering::Release);
+ }
+}
</code_context>
<issue_to_address>
**Gate tests race on shared state**
When rust runs the gate tests concurrently, `stale_start_time_opens_the_gate` mutates the global gate state at `power_on_gate.rs:186-191`, while `gate_is_open_by_default` assumes it is clear at line 169; parallel test execution lets either test change the state during the other's assertion, so correct code produces intermittent failures.
Serialize these tests or give each test isolated gate state.
Also at `packages/applechu/src/server_resources/power_on_gate.rs:169`.
</issue_to_address>
### Comment 14
<location path="packages/applechu/src/util/crypto.rs" line_range="143-148" />
<code_context>
+ ("Zm8=", "fo"),
+ ("Zm9v", "foo"),
+ ("Zm9vYmFy", "foobar"),
+ ("+/8=", "\u{ff}\u{fe}"),
+ ];
+
+ // When/Then: 解码结果与原文一致。
+ for (encoded, plain) in cases {
+ assert_eq!(base64_decode(encoded).unwrap(), plain.as_bytes());
+ }
+ }
</code_context>
<issue_to_address>
**Base64 vector fails its test**
When the `base64_round_trips_known_vectors` test runs, `base64_round_trips_known_vectors` compares the two decoded bytes for `+/8=` at `crypto.rs:148` with the four UTF-8 bytes of `"\u{ff}\u{fe}"` at line 143, so this test fails with a correct decoder.
Use a byte-slice expected value for the decoded binary vector, or replace the vector with a Base64 encoding of the expected string's UTF-8 bytes.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 14 findings to address first, and if the signature verification, native hooks, or virtual-file handling is wrong, an enabled installation could accept or serve unintended resource contents, or freeze the boot state machine and cause an outage. Reverting stops the feature on future runs, but downloaded cache data and any already-served resources outlive the revert and require cleanup or restart.
Blocking findings: packages/applechu/src/server_resources/handle.rs:79, packages/applechu/src/server_resources/vfs_bridge.rs:220, packages/applechu/src/server_resources/vfs_bridge.rs:181, packages/applechu/src/server_resources/vfs_bridge.rs:377, packages/applechu/src/server_resources/mod.rs:195, and 9 more
| if DOWNLOAD_STARTED.swap(true, Ordering::AcqRel) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟠 高优先级 · 下载失败会阻止重试
当首个选定的包下载或加载失败,随后又收到另一个匹配响应时,fetch_and_load 失败后 DOWNLOAD_STARTED 仍为 true,因此 start_download 会忽略后续匹配响应。该包在本次进程剩余时间内都不可用。
下载或加载失败时应清除 DOWNLOAD_STARTED,以便后续匹配响应能够重试。
给 AI agent 的提示
**下载失败会阻止重试**
下载或加载失败时应清除 `DOWNLOAD_STARTED`,以便后续匹配响应能够重试。Original comment in English
🟠 High · Failed downloads block retries
When the first selected package fails to download or load and another matching response arrives, DOWNLOAD_STARTED remains true after fetch_and_load fails, so start_download ignores later matching responses. The package stays unavailable for the rest of the process.
Clear DOWNLOAD_STARTED when the download or load fails so a later matching response can retry.
Prompt for AI agents
In `packages/applechu/src/server_resources/mod.rs` at lines 193-195:
**Failed downloads block retries**
When the first selected package fails to download or load and another matching response arrives, `DOWNLOAD_STARTED` remains true after `fetch_and_load` fails, so `start_download` ignores later matching responses. The package stays unavailable for the rest of the process.
Clear `DOWNLOAD_STARTED` when the download or load fails so a later matching response can retry.| let request_result = (|| -> Result<Vec<u8>, String> { | ||
| let flags = if parsed.secure { | ||
| WINHTTP_FLAG_SECURE | WINHTTP_FLAG_SECURE_DEFAULTS | ||
| } else { |
There was a problem hiding this comment.
🟠 高优先级 · HTTPS 下载失败
当选定资源 URL 使用 HTTPS 时,download 将 WINHTTP_FLAG_SECURE_DEFAULTS 传给 WinHttpOpenRequest,但该标志属于 WinHttpOpen。WinHTTP 会在发送请求前拒绝它。
应从请求标志中移除 WINHTTP_FLAG_SECURE_DEFAULTS,如有需要,仅在创建会话时使用它。
给 AI agent 的提示
**HTTPS 下载失败**
应从请求标志中移除 `WINHTTP_FLAG_SECURE_DEFAULTS`,如有需要,仅在创建会话时使用它。Original comment in English
🟠 High · HTTPS downloads fail
When the selected resource URL uses HTTPS, download passes WINHTTP_FLAG_SECURE_DEFAULTS to WinHttpOpenRequest, although that flag belongs to WinHttpOpen. WinHTTP rejects the request before sending it.
Remove WINHTTP_FLAG_SECURE_DEFAULTS from the request flags and apply it only when creating the session, if needed.
Prompt for AI agents
In `packages/applechu/src/server_resources/download.rs` at line 53:
**HTTPS downloads fail**
When the selected resource URL uses HTTPS, `download` passes `WINHTTP_FLAG_SECURE_DEFAULTS` to `WinHttpOpenRequest`, although that flag belongs to `WinHttpOpen`. WinHTTP rejects the request before sending it.
Remove `WINHTTP_FLAG_SECURE_DEFAULTS` from the request flags and apply it only when creating the session, if needed.| bind(api, "FindFirstFileW", hooked_find_first_file_w as *const (), &NEXT_FIND_FIRST_FILE_W)?; | ||
| bind(api, "FindNextFileW", hooked_find_next_file_w as *const (), &NEXT_FIND_NEXT_FILE_W)?; | ||
| bind(api, "FindClose", hooked_find_close as *const (), &NEXT_FIND_CLOSE)?; | ||
| bind(api, "CreateFileW", hooked_create_file_w as *const (), &NEXT_CREATE_FILE_W)?; | ||
| bind(api, "ReadFile", hooked_read_file as *const (), &NEXT_READ_FILE)?; | ||
| bind(api, "GetFileSize", hooked_get_file_size as *const (), &NEXT_GET_FILE_SIZE)?; | ||
| bind(api, "GetFileSizeEx", hooked_get_file_size_ex as *const (), &NEXT_GET_FILE_SIZE_EX)?; | ||
| bind(api, "SetFilePointer", hooked_set_file_pointer as *const (), &NEXT_SET_FILE_POINTER)?; | ||
| bind( | ||
| api, | ||
| "SetFilePointerEx", | ||
| hooked_set_file_pointer_ex as *const (), | ||
| &NEXT_SET_FILE_POINTER_EX, | ||
| )?; | ||
| bind(api, "CloseHandle", hooked_close_handle as *const (), &NEXT_CLOSE_HANDLE)?; |
There was a problem hiding this comment.
🟡 中优先级 · 虚拟文件存在性检查失败
当游戏检查一个只存在于已下载包中的文件属性时,路径钩子会将请求重定向到物理选项目录,导致加载器认为文件不存在并跳过它。
应添加支持包查询的 GetFileAttributesW 处理,并在落回路径钩子之前处理对应属性 API。
给 AI agent 的提示
**虚拟文件存在性检查失败**
应添加支持包查询的 `GetFileAttributesW` 处理。Original comment in English
🟡 Medium · Virtual file existence checks fail
When the game checks attributes for a file that exists only in the downloaded package, platform::path_hook redirects GetFileAttributesW to the physical option directory, but the VFS bridge does not intercept that API or check the in-memory package. A game loader that checks a virtual file’s attributes before opening it sees the file as missing and skips it.
Add package-aware GetFileAttributesW handling (and any corresponding attribute APIs the game uses) before falling through to the path hook.
Prompt for AI agents
In `packages/applechu/src/server_resources/vfs_bridge.rs` at lines 89-103:
**Virtual file existence checks fail**
When the game checks attributes for a file that exists only in the downloaded package, `platform::path_hook` redirects `GetFileAttributesW` to the physical option directory, but the VFS bridge does not intercept that API or check the in-memory package. A game loader that checks a virtual file’s attributes before opening it sees the file as missing and skips it.
Add package-aware `GetFileAttributesW` handling (and any corresponding attribute APIs the game uses) before falling through to the path hook.| dispatch("/x/GetGameSettingApi", b"not json at all"); | ||
| dispatch("/x/GetGameSettingApi", &[0xff, 0xfe, 0x00]); |
There was a problem hiding this comment.
🟡 中优先级 · 格式错误的响应仍能通过测试
dispatch_ignores_non_json_bodies 没有断言,也没有注册订阅者,因此错误的 dispatcher 仍可能将格式错误的响应发送给订阅者而通过测试。
应注册记录调用次数的处理器,并断言派发格式错误正文后调用次数为零。
给 AI agent 的提示
**格式错误的响应仍能通过测试**
应注册记录调用次数的处理器,并断言派发格式错误正文后调用次数为零。Original comment in English
🟡 Medium · Malformed responses still pass
When dispatch incorrectly invokes subscribers for a non-JSON body, dispatch_ignores_non_json_bodies calls dispatch at net_packet.rs:380-381 but makes no assertion and registers no subscriber, so a broken dispatcher that sends malformed responses to subscribers still passes.
Register a handler that records invocations and assert that dispatching malformed bodies leaves the count at zero.
Prompt for AI agents
In `packages/applechu/src/server_resources/net_packet.rs` at lines 380-381:
**Malformed responses still pass**
When `dispatch` incorrectly invokes subscribers for a non-JSON body, `dispatch_ignores_non_json_bodies` calls `dispatch` at `net_packet.rs:380-381` but makes no assertion and registers no subscriber, so a broken dispatcher that sends malformed responses to subscribers still passes.
Register a handler that records invocations and assert that dispatching malformed bodies leaves the count at zero.| BLOCKING.store(true, Ordering::Release); | ||
| BLOCK_STARTED.store(0, Ordering::Release); | ||
|
|
||
| // Then: 判定为不阻塞,避免永久挂起。 | ||
| assert!(!blocking()); | ||
| BLOCKING.store(false, Ordering::Release); |
There was a problem hiding this comment.
🟡 中优先级 · 闸门测试会在共享状态上发生竞态
并行运行闸门测试时,一个测试会修改全局闸门状态,而另一个测试假定状态已清除,导致间歇性失败。
应串行运行这些测试,或为每个测试提供隔离的闸门状态。
另见 packages/applechu/src/server_resources/power_on_gate.rs:169。
给 AI agent 的提示
**闸门测试会在共享状态上发生竞态**
应串行运行这些测试,或为每个测试提供隔离的闸门状态。Original comment in English
🟡 Medium · Gate tests race on shared state
When rust runs the gate tests concurrently, stale_start_time_opens_the_gate mutates the global gate state at power_on_gate.rs:186-191, while gate_is_open_by_default assumes it is clear at line 169; parallel test execution lets either test change the state during the other's assertion, so correct code produces intermittent failures.
Serialize these tests or give each test isolated gate state.
Also at packages/applechu/src/server_resources/power_on_gate.rs:169.
Prompt for AI agents
In `packages/applechu/src/server_resources/power_on_gate.rs` at lines 186-191:
**Gate tests race on shared state**
When rust runs the gate tests concurrently, `stale_start_time_opens_the_gate` mutates the global gate state at `power_on_gate.rs:186-191`, while `gate_is_open_by_default` assumes it is clear at line 169; parallel test execution lets either test change the state during the other's assertion, so correct code produces intermittent failures.
Serialize these tests or give each test isolated gate state.
Also at `packages/applechu/src/server_resources/power_on_gate.rs:169`.| ("+/8=", "\u{ff}\u{fe}"), | ||
| ]; | ||
|
|
||
| // When/Then: 解码结果与原文一致。 | ||
| for (encoded, plain) in cases { | ||
| assert_eq!(base64_decode(encoded).unwrap(), plain.as_bytes()); |
There was a problem hiding this comment.
🔴 严重 · Base64 测试向量无法通过测试
测试将 +/8= 解码得到的两个字节与字符串 "\u{ff}\u{fe}" 的四个 UTF-8 字节比较,因此即使解码器正确也会失败。
应为解码后的二进制向量使用字节切片作为期望值,或替换为期望字符串 UTF-8 字节对应的 Base64 编码。
给 AI agent 的提示
**Base64 测试向量无法通过测试**
应为解码后的二进制向量使用字节切片作为期望值,或替换为期望字符串 UTF-8 字节对应的 Base64 编码。Original comment in English
🔴 Critical · Base64 vector fails its test
When the base64_round_trips_known_vectors test runs, base64_round_trips_known_vectors compares the two decoded bytes for +/8= at crypto.rs:148 with the four UTF-8 bytes of "\u{ff}\u{fe}" at line 143, so this test fails with a correct decoder.
Use a byte-slice expected value for the decoded binary vector, or replace the vector with a Base64 encoding of the expected string's UTF-8 bytes.
Prompt for AI agents
In `packages/applechu/src/util/crypto.rs` at lines 143-148:
**Base64 vector fails its test**
When the `base64_round_trips_known_vectors` test runs, `base64_round_trips_known_vectors` compares the two decoded bytes for `+/8=` at `crypto.rs:148` with the four UTF-8 bytes of `"\u{ff}\u{fe}"` at line 143, so this test fails with a correct decoder.
Use a byte-slice expected value for the decoded binary vector, or replace the vector with a Base64 encoding of the expected string's UTF-8 bytes.There was a problem hiding this comment.
26 issues found across 18 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/applechu/src/util/api.rs">
<violation number="1" location="packages/applechu/src/util/api.rs:278">
P2: 当前把 `&[u8]` 强转为可写指针传给 ABI;订阅回调也收到 `*mut c_void`,因此回调写入时会修改共享借用所指数据并触发未定义行为。请先复制到可变缓冲区,再将其指针传给 `func`。</violation>
<violation number="2" location="packages/applechu/src/util/api.rs:278">
P2: 新增的 `publish` 会直接进入 `api_publish`,而 `api_publish`(packages/applechu/src/proxy/api_impl/ipc.rs:47)在持有 `SUBSCRIBERS` 锁的同时遍历并调用每个订阅回调。`std::sync::Mutex` 不可重入:若某个订阅回调在本线程内再次调用 `publish` 或 `subscribe`(例如转发、回包或取消订阅),会再次尝试锁住同一把锁,游戏线程因此死锁挂起。建议在 `api_publish` 中先收集匹配的回调、释放锁后再逐一调用。</violation>
</file>
<file name="packages/applechu/src/util/hash.rs">
<violation number="1" location="packages/applechu/src/util/hash.rs:16">
P2: 公开接口接受任意长度切片,但 `data.len() as u32` 会截断超过 `u32::MAX` 的长度,导致哈希遗漏尾部数据(长度为 4 GiB 时会按空输入计算)。请分块提交完整切片,或明确拒绝超出 BCrypt 长度范围的输入。</violation>
</file>
<file name="packages/applechu/src/server_resources/entry.rs">
<violation number="1" location="packages/applechu/src/server_resources/entry.rs:71">
P2: 未知游戏版本(0)与「不限制」语义冲突。`mod.rs` 的 `game_version_from` 注释明确「缺失或异常时返回 0(不限制)」,但此处把 0 当作真实版本参与区间比较:romVersion 缺失/无法解析时 `game_version == 0`,任何带 `minimumGameVersion > 0` 的条目都会被 `0 < minimum` 排除,导致服务器正常下发但永远选不中条目。当无法确定游戏版本时,应当对版本区间不做限制(跳过区间过滤),而不是把 0 当最低版本。现有测试 `zero_game_version_means_unrestricted` 只覆盖了条目的 min/max 均为 0 的情况,未覆盖「条目有下限 + 版本未知」组合,因此未暴露此矛盾。</violation>
<violation number="2" location="packages/applechu/src/server_resources/entry.rs:129">
P2: `unwrap_or(0)` 会把非法版本段(如 `2.x`)转换为 `2.0.0`,导致当前版本 `2.1.0` 被错误上限排除;请让版本段解析失败时返回 `None`,以遵循不可解析约束不限制的约定。</violation>
</file>
<file name="packages/applechu/src/server_resources/dispatcher.rs">
<violation number="1" location="packages/applechu/src/server_resources/dispatcher.rs:46">
P1: 启用 detour 后才写入 `ORIGINAL`;若此时线程进入 `hooked_dispatcher`,它会直接返回 `0` 并跳过游戏原始响应分发。请在 `hook_enable` 前发布 trampoline,并在启用失败时清零。</violation>
<violation number="2" location="packages/applechu/src/server_resources/dispatcher.rs:113">
P2: 响应以 UTF-8 BOM 开头时,`on_net_response` 的 `json::parse` 会失败,资源条目处理和响应发布都会被跳过;WinHTTP 路径已明确剥除 BOM。请在此处也剥除 BOM。</violation>
</file>
<file name="packages/applechu/src/server_resources/net_packet.rs">
<violation number="1" location="packages/applechu/src/server_resources/net_packet.rs:290">
P1: 若游戏使用 `WINHTTP_FLAG_ASYNC` 创建 session,这里不会累积或派发响应,WinHTTP 回调完成的数据也不会被读取。请接管并转发相应状态回调,或确保回退桥只用于同步请求。</violation>
</file>
<file name="packages/applechu/src/server_resources/mod.rs">
<violation number="1" location="packages/applechu/src/server_resources/mod.rs:170">
P2: `GetGameSettingApi` 返回 `gameSetting.dataVersion` 时,这里会将游戏版本读成 0,导致版本条件筛选错误。请在 `romVersion` 缺失时回退读取 `dataVersion`。</violation>
</file>
<file name="packages/applechu/src/server_resources/power_on_gate.rs">
<violation number="1" location="packages/applechu/src/server_resources/power_on_gate.rs:126">
P2: `GetTickCount` 回绕时,这里的 `u64::wrapping_sub` 会得到极大的差值,闸门因此立即放行,资源尚未下载完成时开机流程就会继续。请用 `u32` 的 `wrapping_sub` 计算经过时间。</violation>
<violation number="2" location="packages/applechu/src/server_resources/power_on_gate.rs:175">
P2: 这三个测试共享全局原子状态,却没有串行化;默认并行运行时,它们会互相改写冻结状态并导致断言偶发失败。请用测试锁串行化这些测试。</violation>
</file>
<file name="packages/applechu/src/server_resources/deflate.rs">
<violation number="1" location="packages/applechu/src/server_resources/deflate.rs:73">
P2: `read_string` 拒绝大于 64 MiB 的输出时,这里的 `?` 会在释放堆缓冲前提前返回,导致该解压缓冲泄漏。请确保所有返回路径都释放堆输出。</violation>
<violation number="2" location="packages/applechu/src/server_resources/deflate.rs:84">
P1: 解压失败但留下部分输出时,这里仍会返回 `Ok(result)`,使截断数据进入 ZIP 包并被挂载。请对所有 `status != 1` 的结果报错。</violation>
</file>
<file name="packages/applechu/src/util/crypto.rs">
<violation number="1" location="packages/applechu/src/util/crypto.rs:37">
P2: 遇到第一个 `=` 后解码器不检查后续字符、填充数量或剩余位,因此 `Zg==garbage` 和 `Zg` 都被接受;请校验完整输入的标准 Base64 填充,符合函数文档约定。</violation>
<violation number="2" location="packages/applechu/src/util/crypto.rs:143">
P2: 此用例把 `+/8=` 的二进制结果与 UTF-8 编码的 `ÿþ` 比较,导致 `base64_round_trips_known_vectors` 必然失败;请改用对应的 UTF-8 Base64 向量 `w7/Dvg==`,或直接断言字节数组。</violation>
</file>
<file name="packages/applechu/src/server_resources/json.rs">
<violation number="1" location="packages/applechu/src/server_resources/json.rs:179">
P1: `parse_value` 对每个嵌套数组或对象递归调用自身,没有深度上限;`dispatcher::hooked_dispatcher` 在原始分发前同步解析最多 1 MiB 的响应,深层嵌套可耗尽线程栈并导致进程崩溃。请增加最大嵌套深度,超限时返回 `None`。</violation>
<violation number="2" location="packages/applechu/src/server_resources/json.rs:278">
P2: `other < 0x80` 也包含未转义的 U+0000–U+001F,因此带原始换行或控制字节的非法 JSON 字符串会通过 `parse`,并进入响应发布流程。将该分支限制为 `0x20..0x80`,让控制字节走失败路径。</violation>
<violation number="3" location="packages/applechu/src/server_resources/json.rs:309">
P2: 未验证 `low` 是否为低代理区;例如 `\uD800\uE000` 会被错误合成为 U+10400。请仅在 `0xDC00..0xE000` 范围内接受 `low`,否则返回 `None`。</violation>
<violation number="4" location="packages/applechu/src/server_resources/json.rs:319">
P2: `parse_number` 仅按字符集合扫描,`01` 会被 `parse` 接受,且 `as_i64` 可将其读成 `1`,导致非法 JSON 数值仍参与资源版本筛选。请按 JSON 数字语法校验完整 token 后再构造 `Number`。</violation>
</file>
<file name="packages/applechu/src/server_resources/package.rs">
<violation number="1" location="packages/applechu/src/server_resources/package.rs:213">
P1: `inflate` 会先分配完整解压结果,`deflate.rs` 的 64 MiB 检查在调用返回后才执行;高压缩比条目可能耗尽游戏进程内存。请在解压过程中强制执行输出上限。</violation>
</file>
<file name="packages/applechu/src/server_resources/download.rs">
<violation number="1" location="packages/applechu/src/server_resources/download.rs:43">
P2: `apply_timeout` 的注释说明设置失败不致命、应继续下载,但这里的 `?` 会立即终止请求;任一超时选项设置失败都会使资源包下载失败。请按注释忽略或记录该错误后继续。</violation>
<violation number="2" location="packages/applechu/src/server_resources/download.rs:52">
P1: `WINHTTP_FLAG_SECURE_DEFAULTS` 是 `WinHttpOpen` 的会话标志,却被传入 `WinHttpOpenRequest`;这个请求级参数不支持该标志,可能导致 HTTPS 请求创建失败。此处只传 `WINHTTP_FLAG_SECURE`,需要安全默认值时应在 `WinHttpOpen` 设置。</violation>
<violation number="3" location="packages/applechu/src/server_resources/download.rs:91">
P1: `MAX_DOWNLOAD` 检查发生在读取之后,但这里先按 `available` 分配缓冲区;单次可用数据超过 256 MiB 时,程序会在拒绝响应前分配超限内存,可能耗尽进程内存。请先按 `MAX_DOWNLOAD - body.len()` 检查 `available`,再分配和读取。</violation>
<violation number="4" location="packages/applechu/src/server_resources/download.rs:189">
P2: `rest.find('/')` 无法识别仅带查询串的合法 URL(如 `https://host?token=x`),于是把 `host?token=x` 当成主机传给 `WinHttpConnect`,导致下载失败。请同时识别 `/`、`?` 和 `#` 分隔符,并为 query-only 地址补上 `/`。</violation>
</file>
<file name="packages/applechu/src/server_resources/vfs_bridge.rs">
<violation number="1" location="packages/applechu/src/server_resources/vfs_bridge.rs:377">
P2: `FILE_END` 也被当作 `FILE_CURRENT`,因此从文件末尾定位会按当前位置计算偏移,导致后续读取错误。请在两个指针钩子中分别按 `FILE_BEGIN`、`FILE_CURRENT`、`FILE_END` 计算基准。</violation>
</file>
<file name="packages/applechu/src/platform/vfs.rs">
<violation number="1" location="packages/applechu/src/platform/vfs.rs:212">
P3: `option_source_prefix()` 返回 `String`,每次调用都会对编译期常量 `VFS_OPTION`(`&'static str`)做一次堆分配。两个调用点 `option_root_glob` 和 `package_key` 位于 `FindFirstFileW`/`CreateFileW` 的 IAT detour 路径上,游戏每个文件操作都会走到这里,只为取一个常量而反复分配不值当。返回 `&'static str` 即可,调用方的 `trim_end_matches`/`format!` 用法都不受影响。</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
View guided diff | Re-trigger cubic
| if !api.hook_enable(target) { | ||
| api.hook_remove(target); | ||
| return Err("client dispatcher hook could not be enabled".to_owned()); | ||
| } | ||
| ORIGINAL.store(trampoline, std::sync::atomic::Ordering::Release); | ||
| TARGET.store(target, std::sync::atomic::Ordering::Release); |
There was a problem hiding this comment.
P1: 启用 detour 后才写入 ORIGINAL;若此时线程进入 hooked_dispatcher,它会直接返回 0 并跳过游戏原始响应分发。请在 hook_enable 前发布 trampoline,并在启用失败时清零。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/dispatcher.rs, line 46:
<comment>启用 detour 后才写入 `ORIGINAL`;若此时线程进入 `hooked_dispatcher`,它会直接返回 `0` 并跳过游戏原始响应分发。请在 `hook_enable` 前发布 trampoline,并在启用失败时清零。</comment>
<file context>
@@ -0,0 +1,117 @@
+ let Some(trampoline) = api.hook_create(target, hooked_dispatcher as *const () as usize) else {
+ return Err("client dispatcher hook could not be created".to_owned());
+ };
+ if !api.hook_enable(target) {
+ api.hook_remove(target);
+ return Err("client dispatcher hook could not be enabled".to_owned());
</file context>
| if !api.hook_enable(target) { | |
| api.hook_remove(target); | |
| return Err("client dispatcher hook could not be enabled".to_owned()); | |
| } | |
| ORIGINAL.store(trampoline, std::sync::atomic::Ordering::Release); | |
| TARGET.store(target, std::sync::atomic::Ordering::Release); | |
| ORIGINAL.store(trampoline, std::sync::atomic::Ordering::Release); | |
| if !api.hook_enable(target) { | |
| ORIGINAL.store(0, std::sync::atomic::Ordering::Release); | |
| api.hook_remove(target); | |
| return Err("client dispatcher hook could not be enabled".to_owned()); | |
| } | |
| TARGET.store(target, std::sync::atomic::Ordering::Release); |
| } | ||
| let original: WinHttpQueryDataAvailableFn = std::mem::transmute(original); | ||
| let result = original(request, available); | ||
| if result != 0 && !available.is_null() && *available == 0 { |
There was a problem hiding this comment.
P1: 若游戏使用 WINHTTP_FLAG_ASYNC 创建 session,这里不会累积或派发响应,WinHTTP 回调完成的数据也不会被读取。请接管并转发相应状态回调,或确保回退桥只用于同步请求。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/net_packet.rs, line 290:
<comment>若游戏使用 `WINHTTP_FLAG_ASYNC` 创建 session,这里不会累积或派发响应,WinHTTP 回调完成的数据也不会被读取。请接管并转发相应状态回调,或确保回退桥只用于同步请求。</comment>
<file context>
@@ -0,0 +1,383 @@
+ }
+ let original: WinHttpQueryDataAvailableFn = std::mem::transmute(original);
+ let result = original(request, available);
+ if result != 0 && !available.is_null() && *available == 0 {
+ // 可用字节为 0 表示响应体结束,此时游戏不会再调用 ReadData
+ finish_request(request as usize);
</file context>
| } | ||
| } | ||
|
|
||
| if result.is_empty() && status != 1 { |
There was a problem hiding this comment.
P1: 解压失败但留下部分输出时,这里仍会返回 Ok(result),使截断数据进入 ZIP 包并被挂载。请对所有 status != 1 的结果报错。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/deflate.rs, line 84:
<comment>解压失败但留下部分输出时,这里仍会返回 `Ok(result)`,使截断数据进入 ZIP 包并被挂载。请对所有 `status != 1` 的结果报错。</comment>
<file context>
@@ -0,0 +1,174 @@
+ }
+ }
+
+ if result.is_empty() && status != 1 {
+ return Err(format!("deflate failed with status {status}"));
+ }
</file context>
| fn parse_value(&mut self) -> Option<JsonValue> { | ||
| match self.peek()? { | ||
| b'{' => self.parse_object(), | ||
| b'[' => self.parse_array(), |
There was a problem hiding this comment.
P1: parse_value 对每个嵌套数组或对象递归调用自身,没有深度上限;dispatcher::hooked_dispatcher 在原始分发前同步解析最多 1 MiB 的响应,深层嵌套可耗尽线程栈并导致进程崩溃。请增加最大嵌套深度,超限时返回 None。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/json.rs, line 179:
<comment>`parse_value` 对每个嵌套数组或对象递归调用自身,没有深度上限;`dispatcher::hooked_dispatcher` 在原始分发前同步解析最多 1 MiB 的响应,深层嵌套可耗尽线程栈并导致进程崩溃。请增加最大嵌套深度,超限时返回 `None`。</comment>
<file context>
@@ -0,0 +1,415 @@
+ fn parse_value(&mut self) -> Option<JsonValue> {
+ match self.peek()? {
+ b'{' => self.parse_object(),
+ b'[' => self.parse_array(),
+ b'"' => self.parse_string().map(JsonValue::String),
+ b't' => self.parse_literal("true", JsonValue::Bool(true)),
</file context>
| } | ||
| } | ||
|
|
||
| if self.minimum_game_version != 0 && game_version < self.minimum_game_version { |
There was a problem hiding this comment.
P2: 未知游戏版本(0)与「不限制」语义冲突。mod.rs 的 game_version_from 注释明确「缺失或异常时返回 0(不限制)」,但此处把 0 当作真实版本参与区间比较:romVersion 缺失/无法解析时 game_version == 0,任何带 minimumGameVersion > 0 的条目都会被 0 < minimum 排除,导致服务器正常下发但永远选不中条目。当无法确定游戏版本时,应当对版本区间不做限制(跳过区间过滤),而不是把 0 当最低版本。现有测试 zero_game_version_means_unrestricted 只覆盖了条目的 min/max 均为 0 的情况,未覆盖「条目有下限 + 版本未知」组合,因此未暴露此矛盾。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/entry.rs, line 71:
<comment>未知游戏版本(0)与「不限制」语义冲突。`mod.rs` 的 `game_version_from` 注释明确「缺失或异常时返回 0(不限制)」,但此处把 0 当作真实版本参与区间比较:romVersion 缺失/无法解析时 `game_version == 0`,任何带 `minimumGameVersion > 0` 的条目都会被 `0 < minimum` 排除,导致服务器正常下发但永远选不中条目。当无法确定游戏版本时,应当对版本区间不做限制(跳过区间过滤),而不是把 0 当最低版本。现有测试 `zero_game_version_means_unrestricted` 只覆盖了条目的 min/max 均为 0 的情况,未覆盖「条目有下限 + 版本未知」组合,因此未暴露此矛盾。</comment>
<file context>
@@ -0,0 +1,252 @@
+ }
+ }
+
+ if self.minimum_game_version != 0 && game_version < self.minimum_game_version {
+ return false;
+ }
</file context>
| method: u32, | ||
| ) -> u32 { | ||
| if let Some((_, position)) = handle::size_and_position(file as usize) { | ||
| let base = if method == FILE_BEGIN { 0isize } else { position as isize }; |
There was a problem hiding this comment.
P2: FILE_END 也被当作 FILE_CURRENT,因此从文件末尾定位会按当前位置计算偏移,导致后续读取错误。请在两个指针钩子中分别按 FILE_BEGIN、FILE_CURRENT、FILE_END 计算基准。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/vfs_bridge.rs, line 377:
<comment>`FILE_END` 也被当作 `FILE_CURRENT`,因此从文件末尾定位会按当前位置计算偏移,导致后续读取错误。请在两个指针钩子中分别按 `FILE_BEGIN`、`FILE_CURRENT`、`FILE_END` 计算基准。</comment>
<file context>
@@ -0,0 +1,451 @@
+ method: u32,
+) -> u32 {
+ if let Some((_, position)) = handle::size_and_position(file as usize) {
+ let base = if method == FILE_BEGIN { 0isize } else { position as isize };
+ let target = base.saturating_add(distance as isize).max(0) as usize;
+ if handle::seek(file as usize, target).is_some() {
</file context>
| self.raw() | ||
| .and_then(|api| api.publish) | ||
| .is_some_and(|func| unsafe { | ||
| func(topic.as_ptr(), data.as_ptr().cast_mut().cast(), size) == 0 |
There was a problem hiding this comment.
P2: 新增的 publish 会直接进入 api_publish,而 api_publish(packages/applechu/src/proxy/api_impl/ipc.rs:47)在持有 SUBSCRIBERS 锁的同时遍历并调用每个订阅回调。std::sync::Mutex 不可重入:若某个订阅回调在本线程内再次调用 publish 或 subscribe(例如转发、回包或取消订阅),会再次尝试锁住同一把锁,游戏线程因此死锁挂起。建议在 api_publish 中先收集匹配的回调、释放锁后再逐一调用。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/util/api.rs, line 278:
<comment>新增的 `publish` 会直接进入 `api_publish`,而 `api_publish`(packages/applechu/src/proxy/api_impl/ipc.rs:47)在持有 `SUBSCRIBERS` 锁的同时遍历并调用每个订阅回调。`std::sync::Mutex` 不可重入:若某个订阅回调在本线程内再次调用 `publish` 或 `subscribe`(例如转发、回包或取消订阅),会再次尝试锁住同一把锁,游戏线程因此死锁挂起。建议在 `api_publish` 中先收集匹配的回调、释放锁后再逐一调用。</comment>
<file context>
@@ -262,6 +262,23 @@ impl Api {
+ self.raw()
+ .and_then(|api| api.publish)
+ .is_some_and(|func| unsafe {
+ func(topic.as_ptr(), data.as_ptr().cast_mut().cast(), size) == 0
+ })
+ }
</file context>
| pub fn option_source_prefix() -> String { | ||
| VFS_OPTION.to_owned() |
There was a problem hiding this comment.
P3: option_source_prefix() 返回 String,每次调用都会对编译期常量 VFS_OPTION(&'static str)做一次堆分配。两个调用点 option_root_glob 和 package_key 位于 FindFirstFileW/CreateFileW 的 IAT detour 路径上,游戏每个文件操作都会走到这里,只为取一个常量而反复分配不值当。返回 &'static str 即可,调用方的 trim_end_matches/format! 用法都不受影响。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/platform/vfs.rs, line 212:
<comment>`option_source_prefix()` 返回 `String`,每次调用都会对编译期常量 `VFS_OPTION`(`&'static str`)做一次堆分配。两个调用点 `option_root_glob` 和 `package_key` 位于 `FindFirstFileW`/`CreateFileW` 的 IAT detour 路径上,游戏每个文件操作都会走到这里,只为取一个常量而反复分配不值当。返回 `&'static str` 即可,调用方的 `trim_end_matches`/`format!` 用法都不受影响。</comment>
<file context>
@@ -205,6 +205,14 @@ pub fn amfs_path() -> Option<String> {
+///
+/// 游戏访问 option 时传入的是 `c:\mount\option\...`,`resolve_path` 之后才被
+/// 改写为真实目录;虚拟包匹配必须对源前缀做判断。
+pub fn option_source_prefix() -> String {
+ VFS_OPTION.to_owned()
+}
</file context>
| pub fn option_source_prefix() -> String { | |
| VFS_OPTION.to_owned() | |
| pub fn option_source_prefix() -> &'static str { | |
| VFS_OPTION | |
| } |
There was a problem hiding this comment.
1 existing issue remains and 2 new issues found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/applechu/src/server_resources/mod.rs">
<violation number="1" location="packages/applechu/src/server_resources/mod.rs:166">
P3: 新增的 `locate_resources`(根级字段 → `gameSetting` 嵌套字段回退,以及根级优先的并存语义)没有对应测试。mod.rs 现有测试只覆盖 `version_to_int`、`game_version_from`、`resolve_cache_path`。建议补用例覆盖四种组合:根级命中、仅嵌在 `gameSetting`、两处同时存在(根级优先)、两处都不存在(返回空、流程不误报)。</violation>
</file>
<file name="packages/applechu/src/server_resources/entry.rs">
<violation number="1" location="packages/applechu/src/server_resources/entry.rs:121">
P3: 「取第一个非空字符串」的语义在混合命名的响应里会静默跨命名空间泄漏约束:当服务端写了空的 `minimumAppleChuVersion: ""`(意为 AppleChu 不限)同时又写了有值的 `minimumAquaMaiVersion: "9.0.0"`(本意只约束 AquaMai 客户端)时,AppleChu 客户端会被套上 9.0.0 的 AquaMai 版本约束——旧实现只读 AppleChu 键时不会出现(空值 → `None` → 不限)。若「AppleChu 优先」的意图是键出现即停,建议按键存在性(而非非空)决定是否回退。</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
View guided diff | Re-trigger cubic
| /// | ||
| /// 根级优先;为兼容把字段嵌进 `gameSetting` 的服务器实现,再往里找一层。 | ||
| /// 字段名 `_appleChuResources` 与 AquaMai 的 `_aquaMaiResources` 结构一致。 | ||
| fn locate_resources(response: &JsonValue) -> Option<&JsonValue> { |
There was a problem hiding this comment.
P3: 新增的 locate_resources(根级字段 → gameSetting 嵌套字段回退,以及根级优先的并存语义)没有对应测试。mod.rs 现有测试只覆盖 version_to_int、game_version_from、resolve_cache_path。建议补用例覆盖四种组合:根级命中、仅嵌在 gameSetting、两处同时存在(根级优先)、两处都不存在(返回空、流程不误报)。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/mod.rs, line 166:
<comment>新增的 `locate_resources`(根级字段 → `gameSetting` 嵌套字段回退,以及根级优先的并存语义)没有对应测试。mod.rs 现有测试只覆盖 `version_to_int`、`game_version_from`、`resolve_cache_path`。建议补用例覆盖四种组合:根级命中、仅嵌在 `gameSetting`、两处同时存在(根级优先)、两处都不存在(返回空、流程不误报)。</comment>
<file context>
@@ -133,34 +133,45 @@ fn on_response(api_name: &str, response: &mut JsonValue) -> bool {
+///
+/// 根级优先;为兼容把字段嵌进 `gameSetting` 的服务器实现,再往里找一层。
+/// 字段名 `_appleChuResources` 与 AquaMai 的 `_aquaMaiResources` 结构一致。
+fn locate_resources(response: &JsonValue) -> Option<&JsonValue> {
+ response
+ .get(RESOURCES_FIELD)
</file context>
| } | ||
|
|
||
| /// 按候选键顺序取第一个非空字符串(AppleChu 键优先,AquaMai 键兜底) | ||
| fn optional_string_any(value: &JsonValue, keys: &[&str]) -> Option<String> { |
There was a problem hiding this comment.
P3: 「取第一个非空字符串」的语义在混合命名的响应里会静默跨命名空间泄漏约束:当服务端写了空的 minimumAppleChuVersion: ""(意为 AppleChu 不限)同时又写了有值的 minimumAquaMaiVersion: "9.0.0"(本意只约束 AquaMai 客户端)时,AppleChu 客户端会被套上 9.0.0 的 AquaMai 版本约束——旧实现只读 AppleChu 键时不会出现(空值 → None → 不限)。若「AppleChu 优先」的意图是键出现即停,建议按键存在性(而非非空)决定是否回退。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/entry.rs, line 121:
<comment>「取第一个非空字符串」的语义在混合命名的响应里会静默跨命名空间泄漏约束:当服务端写了空的 `minimumAppleChuVersion: ""`(意为 AppleChu 不限)同时又写了有值的 `minimumAquaMaiVersion: "9.0.0"`(本意只约束 AquaMai 客户端)时,AppleChu 客户端会被套上 9.0.0 的 AquaMai 版本约束——旧实现只读 AppleChu 键时不会出现(空值 → `None` → 不限)。若「AppleChu 优先」的意图是键出现即停,建议按键存在性(而非非空)决定是否回退。</comment>
<file context>
@@ -99,13 +117,16 @@ pub fn select_entry<'a>(
- .filter(|text| !text.is_empty())
- .map(ToOwned::to_owned)
+/// 按候选键顺序取第一个非空字符串(AppleChu 键优先,AquaMai 键兜底)
+fn optional_string_any(value: &JsonValue, keys: &[&str]) -> Option<String> {
+ keys.iter().find_map(|key| {
+ value
</file context>
| fn optional_string_any(value: &JsonValue, keys: &[&str]) -> Option<String> { | |
| fn optional_string_any(value: &JsonValue, keys: &[&str]) -> Option<String> { | |
| keys.iter().find_map(|key| { | |
| if value.get(key).is_some() { | |
| return value | |
| .get(key) | |
| .and_then(JsonValue::as_str) | |
| .map(str::trim) | |
| .filter(|text| !text.is_empty()) | |
| .map(ToOwned::to_owned); | |
| } | |
| None | |
| }) | |
| } |
- schema 测试的 section 顺序表补上 ServerResources(order 355, 排在 PCBID 与 VFS 之间) - rustfmt:规范化 server_resources 与 util/crypto 的换行缩进 - clippy:去掉 vfs_bridge 中 usize -> usize 的冗余 cast
5a359a3 to
ad41b0b
Compare
Option 不再只存在于内存:解压到缓存目录 tmp/ServerResources.d/<AXXX>/, 由 platform/vfs 把 c:\mount\option\<AXXX>\... 重定向过去(源路径与 mount 根两种拼法都覆盖),读取与子目录枚举全部交回游戏原有的文件 API。 - vfs_bridge 只保留 option 根那一级的枚举注入(包内根不在游戏真实 option 目录里,扫不到),语义为叠加:真实枚举先跑并原样返回,自定义根追加其后, 磁盘原有 A001 等的顺序与内容不变 - 补齐 ANSI 变体:游戏 option 扫描走 FindFirstFileExA + FindNextFileA, 原先只挂 W 变体导致注入完全不触发;A/W 各自记自己的下一跳 - 删除 handle.rs 与 CreateFile/ReadFile/GetFileSize/SetFilePointer/CloseHandle /GetFileAttributes 等读取类 hook - package.rs 新增 extract_to,条目路径做穿越校验(拒绝绝对路径、盘符、..) - 未注册覆盖时 option_override 返回 None,原有 resolve_path 分支逐条保留
There was a problem hiding this comment.
8 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/applechu/src/server_resources/mod.rs">
<violation number="1" location="packages/applechu/src/server_resources/mod.rs:62">
P3: `MountedOption.directory` 是只写不读的字段:全库只有 `mount_package` 写入,`mounted_roots` 与 `shutdown` 都只用 `root`,没有任何读取方。同时解压目录 `tmp/ServerResources.d/<AXXX>` 也没有任何清理逻辑,shutdown 只注销了重定向,目录会一直留在缓存里。
要么删掉这个字段(连同 `directory: directory.clone()` 的 clone),要么让它真正发挥作用:shutdown 注销重定向后用该字段 `remove_dir_all` 清掉解压目录。</violation>
<violation number="2" location="packages/applechu/src/server_resources/mod.rs:294">
P1: `package.extract_to` 保留 ZIP 条目中的 `A200/...` 根路径,因此传入已包含根名的 `directory` 会将文件写到 `.d/A200/A200/...`;VFS 映射到 `.d/A200` 后,游戏无法找到包内文件。请改为解压到 `directory` 的父目录,或在解压时剥除条目的首段根名。</violation>
</file>
<file name="packages/applechu/src/server_resources/package.rs">
<violation number="1" location="packages/applechu/src/server_resources/package.rs:149">
P1: 这里只拒绝完全等于 `..` 的路径段;Windows 会规范化普通路径组件末尾的空格或点,因此 `.. ` 可绕过检查并让 `extract_to` 写出目标目录。还需拒绝这类尾随字符,或按 Windows 路径规则规范化后再校验。</violation>
<violation number="2" location="packages/applechu/src/server_resources/package.rs:163">
P2: `to_ascii_lowercase()` 不会折叠非 ASCII 大小写;例如 `A200/Ä.xml` 和 `A200/ä.xml` 会成为两个条目,但 Windows 将它们视为同一路径,解压时会互相覆盖。请使用 Unicode 大小写归一化,并在归一化后处理重复路径。</violation>
</file>
<file name="packages/applechu/src/server_resources/vfs_bridge.rs">
<violation number="1" location="packages/applechu/src/server_resources/vfs_bridge.rs:176">
P2: 根目录使用 `*.txt` 等带后缀的通配符时,这里仍注入全部自定义根,导致 `FindFirstFile*` 返回不符合调用方筛选条件的条目。请仅在模式能匹配根名时注入,或限定为 `*` / `*.*`。</violation>
<violation number="2" location="packages/applechu/src/server_resources/vfs_bridge.rs:182">
P2: `is_option_root_glob` 对 `*.*` 会误判为 false,与本次新增的单测自相矛盾。`winapi::normalize_path` 只做小写和斜杠转换,不处理 `*.*`:对 `C:\mount\option\*.*`,`rfind('*')` 取到的是第二个星号(下标 18),`normalized[..star]` 为 `c:\mount\option\*.`,`trim_end_matches('\\')` 剥不掉尾部点号,与 `option_source()` 永不相等。因此 `only_the_option_root_glob_is_claimed` 里的 `assert!(is_option_root_glob("C:\\mount\\option\\*.*"))` 必然失败;若游戏以 `*.*` 枚举 option 根(与 `*` 同样常见的写法),虚拟根将完全不可见,整个注入静默失效。</violation>
<violation number="3" location="packages/applechu/src/server_resources/vfs_bridge.rs:313">
P2: `FindClose(INVALID_HANDLE_VALUE)` 会被当作合成句柄并返回成功。请仅在句柄确实存在于 `SYNTHETIC` 时接管,否则转交下一跳。</violation>
</file>
<file name="packages/applechu/src/platform/vfs/tests.rs">
<violation number="1" location="packages/applechu/src/platform/vfs/tests.rs:60">
P3: 新增测试只直接调用 `option_target`/`option_override`,没有覆盖生产入口 `resolve_path`。`resolve_path` 里 `option_override` 与盘符映射(`strip_drive('e'/'y')`)、`VFS_OPTION` 分支的判断顺序正是这次改动最关键的接缝(改动注释反复强调“优先于盘符映射”),而 `c:\mount\option\a9z1\...` 源拼法只在 `resolve_path` 里经 `option_target` 生效;这两点都没有端到端断言,如果顺序被调整或分支被遗漏,现有测试不会失败。建议补一组通过 `resolve_path` 的断言(例如注册后 `resolve_path("c:\\mount\\option\\a9z1\\music\\*")` 与 `resolve_path("d:\\game\\option\\a9z1\\music")` 分别指向解压目录、注销后恢复原有行为)。</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| let _ = std::fs::remove_dir_all(&directory); | ||
| std::fs::create_dir_all(&directory) | ||
| .map_err(|error| format!("cannot create {}: {error}", directory.display()))?; | ||
| let written = package.extract_to(&directory)?; |
There was a problem hiding this comment.
P1: package.extract_to 保留 ZIP 条目中的 A200/... 根路径,因此传入已包含根名的 directory 会将文件写到 .d/A200/A200/...;VFS 映射到 .d/A200 后,游戏无法找到包内文件。请改为解压到 directory 的父目录,或在解压时剥除条目的首段根名。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/mod.rs, line 294:
<comment>`package.extract_to` 保留 ZIP 条目中的 `A200/...` 根路径,因此传入已包含根名的 `directory` 会将文件写到 `.d/A200/A200/...`;VFS 映射到 `.d/A200` 后,游戏无法找到包内文件。请改为解压到 `directory` 的父目录,或在解压时剥除条目的首段根名。</comment>
<file context>
@@ -253,16 +278,34 @@ fn fetch_and_load(url: &str, sign: &str) -> Result<(), String> {
+ let _ = std::fs::remove_dir_all(&directory);
+ std::fs::create_dir_all(&directory)
+ .map_err(|error| format!("cannot create {}: {error}", directory.display()))?;
+ let written = package.extract_to(&directory)?;
+
+ // 挂载:`c:\mount\option\<root>\...` 指向解压目录
</file context>
| let written = package.extract_to(&directory)?; | |
| let written = package.extract_to( | |
| directory.parent().ok_or_else(|| "option package extraction directory has no parent".to_owned())?, | |
| )?; |
| fn safe_relative(name: &str) -> Result<String, String> { | ||
| let mut normalized = String::new(); | ||
| for segment in name.split('/') { | ||
| if segment.is_empty() || segment == "." || segment == ".." { |
There was a problem hiding this comment.
P1: 这里只拒绝完全等于 .. 的路径段;Windows 会规范化普通路径组件末尾的空格或点,因此 .. 可绕过检查并让 extract_to 写出目标目录。还需拒绝这类尾随字符,或按 Windows 路径规则规范化后再校验。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/package.rs, line 149:
<comment>这里只拒绝完全等于 `..` 的路径段;Windows 会规范化普通路径组件末尾的空格或点,因此 `.. ` 可绕过检查并让 `extract_to` 写出目标目录。还需拒绝这类尾随字符,或按 Windows 路径规则规范化后再校验。</comment>
<file context>
@@ -106,58 +116,61 @@ impl OptionPackage {
+fn safe_relative(name: &str) -> Result<String, String> {
+ let mut normalized = String::new();
+ for segment in name.split('/') {
+ if segment.is_empty() || segment == "." || segment == ".." {
+ return Err(format!("option package entry has an unsafe path: {name}"));
+ }
</file context>
| if normalized.is_empty() { | ||
| return Err("option package has an empty entry name".to_owned()); | ||
| } | ||
| Ok(normalized.to_ascii_lowercase()) |
There was a problem hiding this comment.
P2: to_ascii_lowercase() 不会折叠非 ASCII 大小写;例如 A200/Ä.xml 和 A200/ä.xml 会成为两个条目,但 Windows 将它们视为同一路径,解压时会互相覆盖。请使用 Unicode 大小写归一化,并在归一化后处理重复路径。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/package.rs, line 163:
<comment>`to_ascii_lowercase()` 不会折叠非 ASCII 大小写;例如 `A200/Ä.xml` 和 `A200/ä.xml` 会成为两个条目,但 Windows 将它们视为同一路径,解压时会互相覆盖。请使用 Unicode 大小写归一化,并在归一化后处理重复路径。</comment>
<file context>
@@ -106,58 +116,61 @@ impl OptionPackage {
+ if normalized.is_empty() {
+ return Err("option package has an empty entry name".to_owned());
}
+ Ok(normalized.to_ascii_lowercase())
}
</file context>
|
|
||
| /// 合成句柄的下一项;不是合成句柄时返回 `None`(调用方转交真实 API) | ||
| unsafe fn synthetic_next(handle: HANDLE, data: *mut c_void) -> Option<i32> { | ||
| if (handle as usize) & SYNTHETIC_FLAG == 0 { |
There was a problem hiding this comment.
P2: FindClose(INVALID_HANDLE_VALUE) 会被当作合成句柄并返回成功。请仅在句柄确实存在于 SYNTHETIC 时接管,否则转交下一跳。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/vfs_bridge.rs, line 313:
<comment>`FindClose(INVALID_HANDLE_VALUE)` 会被当作合成句柄并返回成功。请仅在句柄确实存在于 `SYNTHETIC` 时接管,否则转交下一跳。</comment>
<file context>
@@ -192,288 +167,317 @@ unsafe fn restore(name: &str, slot: &AtomicUsize) {
+
+/// 合成句柄的下一项;不是合成句柄时返回 `None`(调用方转交真实 API)
+unsafe fn synthetic_next(handle: HANDLE, data: *mut c_void) -> Option<i32> {
+ if (handle as usize) & SYNTHETIC_FLAG == 0 {
+ return None;
+ }
</file context>
| /// 是 option 根——更深一层已由 `platform/vfs` 重定向到真实目录,游戏自己能枚举。 | ||
| fn is_option_root_glob(path: &str) -> bool { | ||
| let normalized = winapi::normalize_path(path); | ||
| let Some(star) = normalized.rfind('*') else { |
There was a problem hiding this comment.
P2: 根目录使用 *.txt 等带后缀的通配符时,这里仍注入全部自定义根,导致 FindFirstFile* 返回不符合调用方筛选条件的条目。请仅在模式能匹配根名时注入,或限定为 * / *.*。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/vfs_bridge.rs, line 176:
<comment>根目录使用 `*.txt` 等带后缀的通配符时,这里仍注入全部自定义根,导致 `FindFirstFile*` 返回不符合调用方筛选条件的条目。请仅在模式能匹配根名时注入,或限定为 `*` / `*.*`。</comment>
<file context>
@@ -192,288 +167,317 @@ unsafe fn restore(name: &str, slot: &AtomicUsize) {
- let directory = normalized.trim_end_matches('\\').trim_end_matches("*");
- if directory.trim_end_matches('\\') != source.trim_end_matches('\\') {
- return None;
+ let Some(star) = normalized.rfind('*') else {
+ return false;
+ };
</file context>
| if normalized[star..].contains('\\') { | ||
| return false; | ||
| } | ||
| normalized[..star].trim_end_matches('\\') == option_source() |
There was a problem hiding this comment.
P2: is_option_root_glob 对 *.* 会误判为 false,与本次新增的单测自相矛盾。winapi::normalize_path 只做小写和斜杠转换,不处理 *.*:对 C:\mount\option\*.*,rfind('*') 取到的是第二个星号(下标 18),normalized[..star] 为 c:\mount\option\*.,trim_end_matches('\\') 剥不掉尾部点号,与 option_source() 永不相等。因此 only_the_option_root_glob_is_claimed 里的 assert!(is_option_root_glob("C:\\mount\\option\\*.*")) 必然失败;若游戏以 *.* 枚举 option 根(与 * 同样常见的写法),虚拟根将完全不可见,整个注入静默失效。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/vfs_bridge.rs, line 182:
<comment>`is_option_root_glob` 对 `*.*` 会误判为 false,与本次新增的单测自相矛盾。`winapi::normalize_path` 只做小写和斜杠转换,不处理 `*.*`:对 `C:\mount\option\*.*`,`rfind('*')` 取到的是第二个星号(下标 18),`normalized[..star]` 为 `c:\mount\option\*.`,`trim_end_matches('\\')` 剥不掉尾部点号,与 `option_source()` 永不相等。因此 `only_the_option_root_glob_is_claimed` 里的 `assert!(is_option_root_glob("C:\\mount\\option\\*.*"))` 必然失败;若游戏以 `*.*` 枚举 option 根(与 `*` 同样常见的写法),虚拟根将完全不可见,整个注入静默失效。</comment>
<file context>
@@ -192,288 +167,317 @@ unsafe fn restore(name: &str, slot: &AtomicUsize) {
+ return false;
}
- crate::server_resources::package().map(|package| package.root().to_owned())
+ normalized[..star].trim_end_matches('\\') == option_source()
}
</file context>
| normalized[..star].trim_end_matches('\\') == option_source() | |
| normalized[..star] | |
| .trim_end_matches("*.") | |
| .trim_end_matches('\\') | |
| == option_source() |
| } | ||
|
|
||
| #[test] | ||
| fn option_override_redirects_only_the_mounted_subtree() { |
There was a problem hiding this comment.
P3: 新增测试只直接调用 option_target/option_override,没有覆盖生产入口 resolve_path。resolve_path 里 option_override 与盘符映射(strip_drive('e'/'y'))、VFS_OPTION 分支的判断顺序正是这次改动最关键的接缝(改动注释反复强调“优先于盘符映射”),而 c:\mount\option\a9z1\... 源拼法只在 resolve_path 里经 option_target 生效;这两点都没有端到端断言,如果顺序被调整或分支被遗漏,现有测试不会失败。建议补一组通过 resolve_path 的断言(例如注册后 resolve_path("c:\\mount\\option\\a9z1\\music\\*") 与 resolve_path("d:\\game\\option\\a9z1\\music") 分别指向解压目录、注销后恢复原有行为)。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/platform/vfs/tests.rs, line 60:
<comment>新增测试只直接调用 `option_target`/`option_override`,没有覆盖生产入口 `resolve_path`。`resolve_path` 里 `option_override` 与盘符映射(`strip_drive('e'/'y')`)、`VFS_OPTION` 分支的判断顺序正是这次改动最关键的接缝(改动注释反复强调“优先于盘符映射”),而 `c:\mount\option\a9z1\...` 源拼法只在 `resolve_path` 里经 `option_target` 生效;这两点都没有端到端断言,如果顺序被调整或分支被遗漏,现有测试不会失败。建议补一组通过 `resolve_path` 的断言(例如注册后 `resolve_path("c:\\mount\\option\\a9z1\\music\\*")` 与 `resolve_path("d:\\game\\option\\a9z1\\music")` 分别指向解压目录、注销后恢复原有行为)。</comment>
<file context>
@@ -56,6 +56,71 @@ fn missing_candidates_keep_configured_option_path() {
}
+#[test]
+fn option_override_redirects_only_the_mounted_subtree() {
+ // Given: 一个注册了重定向的自定义 option 根。
+ let config = test_config("override-only-mounted");
</file context>
| struct MountedOption { | ||
| /// 根目录名(`AXXX`,保留包内原始大小写) | ||
| root: String, | ||
| /// 解压目录绝对路径 |
There was a problem hiding this comment.
P3: MountedOption.directory 是只写不读的字段:全库只有 mount_package 写入,mounted_roots 与 shutdown 都只用 root,没有任何读取方。同时解压目录 tmp/ServerResources.d/<AXXX> 也没有任何清理逻辑,shutdown 只注销了重定向,目录会一直留在缓存里。
要么删掉这个字段(连同 directory: directory.clone() 的 clone),要么让它真正发挥作用:shutdown 注销重定向后用该字段 remove_dir_all 清掉解压目录。
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/applechu/src/server_resources/mod.rs, line 62:
<comment>`MountedOption.directory` 是只写不读的字段:全库只有 `mount_package` 写入,`mounted_roots` 与 `shutdown` 都只用 `root`,没有任何读取方。同时解压目录 `tmp/ServerResources.d/<AXXX>` 也没有任何清理逻辑,shutdown 只注销了重定向,目录会一直留在缓存里。
要么删掉这个字段(连同 `directory: directory.clone()` 的 clone),要么让它真正发挥作用:shutdown 注销重定向后用该字段 `remove_dir_all` 清掉解压目录。</comment>
<file context>
@@ -50,17 +55,37 @@ crate::config_section! {
+struct MountedOption {
+ /// 根目录名(`AXXX`,保留包内原始大小写)
+ root: String,
+ /// 解压目录绝对路径
+ directory: PathBuf,
+}
</file context>
服务器下发自定义资源功能
Sourcery 总结
实现由服务器驱动的自定义 Option 资源,并支持安全交付、缓存和透明的虚拟文件系统集成。
新功能:
增强功能:
构建:
测试:
Original summary in English
Sourcery 摘要
实现由服务器驱动的安全自定义 Option 资源,并将其集成到游戏的虚拟文件系统中。
新功能:
增强功能:
构建:
测试:
Original summary in English
Sourcery 摘要
实现由服务器驱动的安全自定义 Option 资源交付,并通过游戏的虚拟文件系统公开生成的软件包。
新功能:
增强功能:
构建:
测试:
Original summary in English
Sourcery 摘要
实现由服务器驱动的自定义 Option 资源交付,并通过游戏的虚拟文件系统挂载生成的软件包。
新功能:
增强功能:
构建:
测试:
Original summary in English
Summary by Sourcery
Implement secure server-driven custom Option resource delivery and mount the resulting packages through the game's virtual filesystem.
New Features:
Enhancements:
Build:
Tests: