Skip to content

Commit 0285f7f

Browse files
os-zhuangclaude
andauthored
chore(scripts): the raw-byte gate scans DEL (0x7f) too, not only the C0 set (#5460) (#5479)
#5157 drew this gate's scan surface as "the C0 controls minus tab/LF/CR". DEL (U+007F) is a control character but not a C0 one -- it sits alone past the printable range -- so a set expressed as a contiguous range could not reach it. That the gap was arbitrary rather than considered is shown by where the two remaining raw specimens sat: nine lines below the 0x03 #5157 had just escaped, in the same switch, in both login.ts and register.ts, as the Backspace key literal. The escaped case reads as a key; the raw one renders as nothing and reads as an empty-string case. All three harms #5157 argued for C0 land on DEL verbatim, and the decisive one is that the accident source -- an editing tool materialising an escape into its byte while an author writes about the byte -- does not pick byte values. The binary criterion widens with it, and is load-bearing there: E4 B8 7F AD is the character 中 with a stray 0x7f in it, which a C0-only strip reads as binary, making the byte its own alibi. Measured over all 5457 tracked regular files: zero movement between the text and binary verdicts. Both specimens are escaped to the U+007F escape sequence, which is byte-identical at runtime and already the spelling used by the sibling prompt in cloud/login.ts. Self-test: 34 -> 48 assertions, both directions pinned. The assertion that deliberately held DEL outside the set is inverted rather than deleted. Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE Co-authored-by: Claude <noreply@anthropic.com>
1 parent a3d9c67 commit 0285f7f

5 files changed

Lines changed: 195 additions & 32 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
---
3+
4+
chore(scripts): `check:nul-bytes` 的扫描面纳入 DEL(0x7f),并转义 login.ts / register.ts 各一枚裸字节 (#5460)
5+
6+
#5157 把这道门禁的扫描面从 NUL 扩到「C0 控制字符集去掉 tab/LF/CR」。**DEL(0x7f)不在那个区间里** —— 它不是 C0 控制符,而是单独坐在 ASCII 表末尾、印刷字符区之后的一枚控制字符,所以一个用连续区间表达的集合根本够不到它。
7+
8+
这个缺口是偶然而非深思熟虑,证据是剩下那两枚裸字节被发现的位置:#5157 转义了 CLI 密码输入里的一枚裸 0x03,而在 `login.ts``register.ts` **同一个 switch 里、往下九行**,各躺着一枚当 Backspace 键值的裸 0x7f,原封不动。转义后的 case 与仍是裸字节的 case 并排,前者读得出是个按键,后者渲染为空、读起来是 `case '':` —— 一个空串 case;区分二者的只有 ASCII 当年把这个字节放在了 0x1f 的哪一侧。
9+
10+
#5157 论证 C0 的三条危害,逐条原样落在 0x7f 上:同样渲染为空、同样两种拼写都搜不到(既 grep 不到 `\u007f` 这段文本,也没法把那个字节敲进搜索框)、同样出自「作者正在写关于这个字节的内容时,编辑工具把转义落成真字节」这一事故源 —— 而**该事故源不挑字节值**。本单 issue 正文的第一版自己就踩了这个坑(写着写着落进两枚真的 0x03),实施本 PR 的过程中又复现两次:一次落进临时脚本,一次落进本脚本头部那段正在描述该事故的注释里。多数语言对「控制字符」的定义(C 的 `iscntrl`、正则的 `\p{Cc}` 类)也都把 0x7f 算进去,「C0」是个更窄的读法。
11+
12+
扫描面现在是 `[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`
13+
14+
**二进制判据同步扩面,而且这一步同样是承重的。** 判据是「剔除全部扫描字节后整文件 UTF-8 严格解码」,现在 0x7f 也在剔除之列。理由与 #5157 对 0x01 的论证同构:`E4 B8 7F AD` 是「中」被塞进一枚 0x7f,只剔 C0 的话它解码失败 → 文件被判二进制 → 跳过 → **那枚 0x7f 成了自己的不在场证明**。自测里把这条钉住了:回退扩面后,该样本文件立刻从「文本」掉进「二进制」(断言 `exactly the 2 binary assets skip``got 3`)。反方向不会出错:扫描集全部 `<= 0x7f`,而合法 UTF-8 多字节序列只由 `>= 0x80` 的字节构成。
15+
16+
**实测全部 5456 个受追踪常规文件:扩面前后文本/二进制判定零变化**(仍是 4 个 PNG + 1 个 ICO 跳过)。那 4 个 PNG 和 1 个 ICO 里本就含大量裸 0x7f(其中一个 1317 枚),不受影响 —— 它们是整文件解码失败才判的二进制,与任何单个字节无关。
17+
18+
**行为不变,不发版。** `'\u007f'` 与那枚裸字节在运行时逐字节相同,两个 switch 的 Backspace 分支照旧命中。
19+
20+
`--self-test` 断言数 34 → 48。新增断言把两个方向都钉在代码旁边:正向(裸 0x7f 判红、报错报出 0x7f 与 `\u007f` 处方、多字节序列里的 0x7f 判红),反向(两个新样本**整个文件都不含 C0 字节**,所以前一版 C0-only 的扫描确实无事可做),外加一条「转义后的拼写保持绿」—— 门禁教作者写转义,那这条处方本身必须可测,否则只证明了什么会被拒、没证明照做会被接受。原先那条**故意**把 DEL 钉在扫描面之外的断言(`'tab / CR / LF / DEL are outside the scanned set and stay green'`)按裁定反转:它当年是用来说明这条边界是选出来的而不是漏掉的,这次边界被重新选择,选到了另一边。
21+
22+
脚本名与 `pnpm check:nul-bytes` 命令名依旧不变(理由见脚本头);语义变化仍写在脚本头、报错文案和 CI 步骤三处。工具链改动,不发版。

.github/workflows/lint.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ jobs:
7676
- name: Slot-lookup ratchet
7777
run: pnpm check:slot-lookup
7878

79-
# Raw control-byte guard (#3127 / #4890 / #5157). Scans every tracked TEXT
80-
# file for a raw C0 control byte — 0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f, i.e.
81-
# everything except tab/LF/CR. Two distinct harms, one gate:
79+
# Raw control-byte guard (#3127 / #4890 / #5157 / #5460). Scans every
80+
# tracked TEXT file for a raw ASCII control byte — 0x00-0x08, 0x0b, 0x0c,
81+
# 0x0e-0x1f and 0x7f, i.e. everything except tab/LF/CR. Two distinct
82+
# harms, one gate:
8283
# • A literal U+0000 makes grep/ripgrep treat the whole file as binary and
8384
# silently return ZERO matches — the file drops out of code search and
8485
# out of every grep-based lint, with no error saying so. Nothing else
@@ -92,6 +93,13 @@ jobs:
9293
# Four tracked source files carried those past the NUL-only gate until
9394
# #5157 widened the scan surface; PR #5140 is the case that found it,
9495
# when a 0x01 sitting 14 bytes from a caught NUL went unfixed.
96+
# • DEL (0x7f) is in the set for the same reason, added by #5460. It is
97+
# not a C0 control — it sits alone past the printable range — so the
98+
# C0-shaped set could not reach it, and two raw specimens survived in
99+
# the CLI's password prompts nine lines below a 0x03 #5157 had just
100+
# escaped, reading as `case ''`. The set is drawn by the accident
101+
# source (a tool materialising an escape into its byte), and that
102+
# source does not pick byte values.
95103
# The command name stays `check:nul-bytes` for continuity — see the script's
96104
# header for why. Authors must write the unicode escape instead of the byte.
97105
- name: Raw control-byte guard

packages/cli/src/commands/login.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async function promptPassword(promptText: string): Promise<string> {
4444
cleanup();
4545
resolve(chars.join(''));
4646
break;
47-
case '': // Backspace
47+
case '\u007f': // Backspace
4848
if (chars.length > 0) {
4949
chars.pop();
5050
process.stdout.clearLine(0);

packages/cli/src/commands/register.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async function promptPassword(promptText: string): Promise<string> {
4040
cleanup();
4141
resolve(chars.join(''));
4242
break;
43-
case '': // Backspace
43+
case '\u007f': // Backspace
4444
if (chars.length > 0) {
4545
chars.pop();
4646
process.stdout.clearLine(0);

0 commit comments

Comments
 (0)