diff --git a/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.i18n.yaml new file mode 100644 index 0000000000..bf02c9b7dd --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.md +2026-08-23-win32-utf16-nul-truncation.md: 3962730c66d72b9927e5ce6dabde0f50101c5787 +2026-08-23-win32-utf16-nul-truncation.zh.md: 23df80053b23c5e2fbb810c95668bbeef7c91fe9 diff --git a/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.md b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.md new file mode 100644 index 0000000000..3962730c66 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.md @@ -0,0 +1,29 @@ +# Agent Note: Win32 folder-picker paths stop truncating at U+XX00 code units + +Status: implemented + +English | [中文](2026-08-23-win32-utf16-nul-truncation.zh.md) + +## Problem + +`readUtf16` in `packages/host/directory-picker-native/src/win32-dialog-bindings.ts` translated the `IFileOpenDialog` result buffer by scanning for a zero byte with `bytes[end] !== 0`. UTF-16LE encodes NUL as two zero bytes, so any BMP code unit whose low byte is zero — U+XX00, such as 开 (U+5F00) — ended the scan early. Selecting a folder like `C:\Users\XIAOPAN\Desktop\安卓开发` returned `C:\Users\XIAOPAN\Desktop\安卓`, and the workspace-creation call failed with `workspace-invalid-path ... ENOENT`. + +## Decision + +The scan ends only when both bytes of a code unit are zero, still advancing two bytes at a time over the same 32KiB `koffi.view` buffer. A regression test drives `readUtf16` through the existing fake koffi COM world with a path containing 安卓开发 (U+5F00), so the termination rule is proven without a real Windows host. + +The fix is adopted verbatim from the community patch series on the `fix/win32-utf16-nul-truncation` branch of the ericcaiwx-star fork — [c8aac14703](https://github.com/ericcaiwx-star/deepseek-harness/commit/c8aac14703a517b8db1573f9ca4ed94dc58e276b) for the scan fix and [e1d6265cb9](https://github.com/ericcaiwx-star/deepseek-harness/commit/e1d6265cb930a0a74cba03c40e73ed872a83575f) for the fixture cleanup — reported in [discussion #580](https://github.com/deepseek-ai/deepseek-harness/discussions/580) (earlier reported in [discussion #563](https://github.com/deepseek-ai/deepseek-harness/discussions/563)). Both cherry-picks retain the original author, ericcaiwx-star; the upstream fork is the source of record for the patch. + +## Alternatives considered + +**Reject the community patch and rewrite the scan locally.** Rejected: the patch is minimal, fits the dialog's existing test approach, and a byte-identical cherry-pick preserves provenance and credit. + +**Decode the whole buffer with `toString('utf16le')` and split at `\0`.** Rejected: it copies the entire buffer instead of scanning, and the split would still depend on the same two-zero-byte rule. + +**Ask COM or koffi for a string length.** Rejected: the binding surface provides no length; the double-zero scan is the standard UTF-16LE NUL test. + +## Consequences + +- Any path containing a U+XX00 code unit survives the picker translation; paths with such characters (for example Chinese folder names) can be selected and used to create workspaces. +- The fix changes no ABI usage, buffer size, or dialog flow; the COM child-process architecture in the [Win32 folder dialog note](../feature/2026-08-02-win32-in-process-folder-dialog.md) is untouched. +- Real-dialog rendering and selection remain a manual Windows check; this change's regression test exercises only the byte-to-string translation against the fake COM world. The fixture path is synthetic (`C:\fixture\安卓开发`) so no real user path appears in the repository. diff --git a/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.zh.md new file mode 100644 index 0000000000..23df80053b --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-23-win32-utf16-nul-truncation.zh.md @@ -0,0 +1,29 @@ +# Agent Note: Win32 目录选择器路径不再在 U+XX00 码元处截断 + +Status: implemented + +[English](2026-08-23-win32-utf16-nul-truncation.md) | 中文 + +## 问题 + +`packages/host/directory-picker-native/src/win32-dialog-bindings.ts` 的 `readUtf16` 用 `bytes[end] !== 0` 扫描 `IFileOpenDialog` 结果缓冲区来寻找零字节。UTF-16LE 真正的 NUL 是两个零字节,因此任何低字节为 0 的 BMP 码元——U+XX00,例如「开」(U+5F00)——都会提前结束扫描。选择 `C:\Users\XIAOPAN\Desktop\安卓开发` 这类目录会得到 `C:\Users\XIAOPAN\Desktop\安卓`,随后创建工作区的调用以 `workspace-invalid-path ... ENOENT` 失败。 + +## 决策 + +扫描只有在一个码元的两个字节都为零时才结束,仍按每次两个字节在同一个 32KiB `koffi.view` 缓冲区上推进。回归测试通过既有的假 koffi COM 世界驱动 `readUtf16`,路径包含「安卓开发」(U+5F00),从而不依赖真实 Windows 主机验证终止规则。 + +修复逐字采用 ericcaiwx-star fork 的 `fix/win32-utf16-nul-truncation` 分支上的社区补丁系列——[c8aac14703](https://github.com/ericcaiwx-star/deepseek-harness/commit/c8aac14703a517b8db1573f9ca4ed94dc58e276b) 是扫描修复,[e1d6265cb9](https://github.com/ericcaiwx-star/deepseek-harness/commit/e1d6265cb930a0a74cba03c40e73ed872a83575f) 是 fixture 清理——在 [discussion #580](https://github.com/deepseek-ai/deepseek-harness/discussions/580) 报告(更早在 [discussion #563](https://github.com/deepseek-ai/deepseek-harness/discussions/563) 报告)。两次 cherry-pick 均保留原作者 ericcaiwx-star;上游 fork 是补丁的记录来源。 + +## 考虑过的替代方案 + +**拒绝社区补丁,本地重写扫描。** 拒绝:补丁极小,与目录选择器现有测试方式一致;逐字节一致的 cherry-pick 保留来源与署名。 + +**用 `toString('utf16le')` 解码整个缓冲区再按 `\0` 切分。** 拒绝:复制整个缓冲区而非扫描,且切分仍依赖同一「双零字节」规则。 + +**向 COM 或 koffi 索取字符串长度。** 拒绝:绑定面不提供长度;双零扫描是标准的 UTF-16LE NUL 判定。 + +## 后果 + +- 任何含 U+XX00 码元的路径组件都能通过选择器转译;含这类字符的路径(例如中文目录名)可以选中并用于创建工作区。 +- 修复不改变 ABI 用法、缓冲区大小或对话框流程;[Win32 目录选择器 note](../feature/2026-08-02-win32-in-process-folder-dialog.zh.md) 中的 COM 子进程架构不受影响。 +- 真实对话框渲染与选择仍是手动 Windows 检查;本次回归测试只针对假 COM 世界中的字节到字符串转译。fixture 路径为合成路径(`C:\fixture\安卓开发`),仓库中不出现真实用户路径。 diff --git a/packages/host/directory-picker-native/src/win32-dialog-bindings.ts b/packages/host/directory-picker-native/src/win32-dialog-bindings.ts index 654bbc5a74..ef7795cf95 100644 --- a/packages/host/directory-picker-native/src/win32-dialog-bindings.ts +++ b/packages/host/directory-picker-native/src/win32-dialog-bindings.ts @@ -37,7 +37,9 @@ interface Koffi { function readUtf16(koffi: Koffi, address: unknown): string { const bytes = Buffer.from(koffi.view(address, 32768)) let end = 0 - while (end + 1 < bytes.length && bytes[end] !== 0) end += 2 + // UTF-16LE NUL is two zero bytes. A single zero low byte is a valid BMP + // code unit (U+XX00, e.g. 开 = U+5F00) and must not terminate the scan. + while (end + 1 < bytes.length && !(bytes[end] === 0 && bytes[end + 1] === 0)) end += 2 return bytes.toString('utf16le', 0, end) } diff --git a/packages/host/directory-picker-native/tests/win32-dialog-bindings.spec.ts b/packages/host/directory-picker-native/tests/win32-dialog-bindings.spec.ts index b8ff4c3f1a..85d4aed48b 100644 --- a/packages/host/directory-picker-native/tests/win32-dialog-bindings.spec.ts +++ b/packages/host/directory-picker-native/tests/win32-dialog-bindings.spec.ts @@ -180,6 +180,15 @@ describe('loadWin32DialogBindings over the fake COM world', () => { expect(world.uninitialized).toBe(1) }) + it('reads a UTF-16 path whose BMP code unit has a zero low byte (U+5F00 开)', async () => { + // 开 = U+5F00 → UTF-16LE bytes 00 5F. A scan that treats any zero low + // byte as NUL truncates here and returns the nonexistent ...\安卓. + const world = comWorld({ path: 'C:\\fixture\\安卓开发' }) + installFakeKoffi(world) + const bindings = await (await loadBindingsModule()).loadWin32DialogBindings() + expect(runFolderDialog(bindings, 'Pick', vi.fn())).toBe('C:\\fixture\\安卓开发') + }) + it('maps dismissal and the S_FALSE CoInitializeEx', async () => { const world = comWorld({ showHr: HRESULT_CANCELLED, coInitHr: 1 }) installFakeKoffi(world)