[代码质量/鲁棒性] 底层二进制解码过度依赖非空断言 !:缺失防御性越界校验与异常统一 #11

Closed
opened 2026-09-14 07:01:32 +00:00 by troytt · 1 comment
Owner

Parent Issue: #7 ([Code Review] 整体代码质量、架构缺陷与技术债务全景审查报告)

模块位置

src/formats/bitstream.ts, pcx.ts, cel.ts, cof.ts, dcc.ts, pal.ts, dt1.ts 等

问题背景与现象

  • 在解包解析二进制流时,底层格式解析模块中存在超过 500+ 处密集使用 data[i]! 压制 TypeScript 空安全检查。

造成的影响

  1. 当读取被截断、损坏的 MPQ 扇区或异常数据包时,缺少边界检查会导致运行时抛出原生的 TypeError: Cannot read properties of undefined;
  2. 导致异常脱离了项目自定义的 CelError、MpqError、PcxError 领域异常体系,丢失了具体的偏移量(Offset)和损坏位置信息,给故障排查带来困难。

解决建议

  1. 在 bitstream.ts 与格式解析基础库中提供安全的切片与步进读取函数(如 safeByte(data, offset)、requireExtent(offset, length, limit));
  2. 针对所有的动态下标访问增加前置长度校验;
  3. 在越界或非法结构时统一包装并抛出对应的领域异常类,附带具体的文件偏移、字段名称与预期尺寸。
> **Parent Issue**: #7 ([Code Review] 整体代码质量、架构缺陷与技术债务全景审查报告) ## 模块位置 `src/formats/bitstream.ts`, `pcx.ts`, `cel.ts`, `cof.ts`, `dcc.ts`, `pal.ts`, `dt1.ts` 等 ## 问题背景与现象 - 在解包解析二进制流时,底层格式解析模块中存在超过 500+ 处密集使用 `data[i]!` 压制 TypeScript 空安全检查。 ## 造成的影响 1. 当读取被截断、损坏的 MPQ 扇区或异常数据包时,缺少边界检查会导致运行时抛出原生的 `TypeError: Cannot read properties of undefined`; 2. 导致异常脱离了项目自定义的 `CelError`、`MpqError`、`PcxError` 领域异常体系,丢失了具体的偏移量(Offset)和损坏位置信息,给故障排查带来困难。 ## 解决建议 1. 在 `bitstream.ts` 与格式解析基础库中提供安全的切片与步进读取函数(如 `safeByte(data, offset)`、`requireExtent(offset, length, limit)`); 2. 针对所有的动态下标访问增加前置长度校验; 3. 在越界或非法结构时统一包装并抛出对应的领域异常类,附带具体的文件偏移、字段名称与预期尺寸。
Author
Owner

已修复 — 边界检查读取器与领域错误 (a6ca337 → f52edd8)

新增 src/formats/reader.ts:FormatError / TruncatedDataError / InvalidFieldError 错误层次 + ByteReader 游标 + requireBytes()。错误携带结构化字段(format / field / offset / needed / available),消息形如:

dcc: truncated reading header.framesPerDirection at offset 0x1A4: need 4 bytes, only 2 available (buffer length 420)

按 pal → bitstream → pcx → dc6 → cel → pl2 → ds1 → dt1 → cof → dcc → crypt → archive → implode 的顺序逐个改造,除字节可用性外还校验结构不变量:magic / 版本 / 用作循环上界或分配尺寸的计数(在分配之前拒绝荒谬值,避免一个损坏的 u32 触发 GB 级分配)/ 必须落在缓冲区内的偏移。对合法输入的解码行为逐字节不变,这是加固而非重写。

]! 的收敛情况:

指标 改造前 改造后
formats/ + mpq/ 中 ]! 总数 126 29
其中作用于原始不可信缓冲区的 41 0

剩余 29 处全部是编译期可证安全的结构化访问(常量表、已校验长度的内部数组、预建哈希表),例如 dt1.ts 的 ISO_JUMP[row]! 受 row < ISO_JUMP.length 约束,pcx.ts 的 palette[index]! 中 palette 恒为 768 字节而 index 上界为 767。这些逐一复核过。

验证(冷装): tsc --noEmit 0 错误;全仓 grep ": any\|as any" src/ 干净;npm run verify:formats 通过(已接入 verify:all)。

变异测试(我方独立选定): 将 reader.ts 中 requireBytes 的边界判断置为 false → reader.test.ts > requireBytes > validates direct offsets 转红并给出精确期望消息;还原后转绿。

一处如实说明: 复核中我发现 formats-malformed.test.ts 原先大量使用 toThrow(FormatError) 这种粗粒度断言 —— 我删掉 pcx.ts 的 geometry 守卫后该用例依然通过(因为调色板 marker 守卫抢先抛错),属于「因错误的原因而通过」。我已把该用例收紧为断言具体的错误子类与 field 名,并重新做了变异验证:绿 → 红 → 绿。其余同类断言粒度偏粗但不掩盖缺陷(失败仍会响亮抛出),可作为后续改进项。

### 已修复 — 边界检查读取器与领域错误 (a6ca337 → f52edd8) 新增 `src/formats/reader.ts`:`FormatError` / `TruncatedDataError` / `InvalidFieldError` 错误层次 + `ByteReader` 游标 + `requireBytes()`。错误携带结构化字段(`format` / `field` / `offset` / `needed` / `available`),消息形如: `dcc: truncated reading header.framesPerDirection at offset 0x1A4: need 4 bytes, only 2 available (buffer length 420)` 按 `pal → bitstream → pcx → dc6 → cel → pl2 → ds1 → dt1 → cof → dcc → crypt → archive → implode` 的顺序逐个改造,除字节可用性外还校验结构不变量:magic / 版本 / 用作循环上界或分配尺寸的计数(**在分配之前**拒绝荒谬值,避免一个损坏的 u32 触发 GB 级分配)/ 必须落在缓冲区内的偏移。**对合法输入的解码行为逐字节不变**,这是加固而非重写。 **`]!` 的收敛情况:** | 指标 | 改造前 | 改造后 | | :-- | --: | --: | | `formats/` + `mpq/` 中 `]!` 总数 | 126 | 29 | | 其中作用于**原始不可信缓冲区**的 | 41 | **0** | 剩余 29 处全部是编译期可证安全的结构化访问(常量表、已校验长度的内部数组、预建哈希表),例如 `dt1.ts` 的 `ISO_JUMP[row]!` 受 `row < ISO_JUMP.length` 约束,`pcx.ts` 的 `palette[index]!` 中 palette 恒为 768 字节而 `index` 上界为 767。这些逐一复核过。 **验证(冷装):** `tsc --noEmit` 0 错误;全仓 `grep ": any\|as any" src/` 干净;`npm run verify:formats` 通过(已接入 `verify:all`)。 **变异测试(我方独立选定):** 将 `reader.ts` 中 `requireBytes` 的边界判断置为 `false` → `reader.test.ts > requireBytes > validates direct offsets` 转红并给出精确期望消息;还原后转绿。 > **一处如实说明:** 复核中我发现 `formats-malformed.test.ts` 原先大量使用 `toThrow(FormatError)` 这种粗粒度断言 —— 我删掉 `pcx.ts` 的 geometry 守卫后该用例**依然通过**(因为调色板 marker 守卫抢先抛错),属于「因错误的原因而通过」。我已把该用例收紧为断言具体的错误子类与 `field` 名,并重新做了变异验证:绿 → 红 → 绿。其余同类断言粒度偏粗但不掩盖缺陷(失败仍会响亮抛出),可作为后续改进项。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Reference: troytt/diablo2-web#11
No description provided.