feat(adapter-wp): WXR→UniversalRow 映射器(status 值域/悬空保留/unsupported 分流)

- mapItem 纯函数:WxrItem → MappedResult 三分支(row/unsupported/error),
  DEFAULT_TYPE_ROUTES/STATUS_MAP/META_KEYS 三冻结表导出
- Plan 1 移交落实:status 值域表驱动(未知→unknown+issue);property 断言
  (isUniversalRow+refs 形状非空)测试钉死
- Task 3 审查移交:postId null→error(不产 row 不猜);-7 原样;-0 归一 '0'
- taxonomyRefs 锚点=category @nicename(=term slug,Task 8 实数据核对后冻结)
- 悬空 gallery 203/204 原样保留;wppop 解析失败→extras.phpRaw+issue(不崩不丢)
- Task 2 审查移交补测四例:d: 键拒绝/NUL 往返/s:1:"0"→Map/1MB<50ms 烟雾
- tsconfig.build.json:src 首次引 @sf/contracts 后 build 经 contracts dist
  声明解析(rootDir TS6059;typecheck/vitest/tsx 仍 src 直连,ADR-016 归口)
- 142→173 测试全绿;typecheck/lint 绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 09:45:34 +08:00
parent f4971a943b
commit c99bbbb29e
5 changed files with 592 additions and 1 deletions
@@ -213,6 +213,40 @@ describe('错误信息上下文摘录(offset ±20 码元窗口、控制字符
})
})
// Task 2 审查移交补测(Plan 2 Task 4 落实):四例边界/形状/性能钉死
describe('Task 2 审查移交补测:数组键类型、NUL、数字串键形状、1MB 性能烟雾', () => {
it('d: 作数组键 → 拒绝(数组键只认 i:/s:,浮点键是 PHP 序列化产物中不存在的形状)', () => {
const e = capture(() => phpUnserialize('a:1:{d:1.5;i:0;}')) as PhpParseError
expect(e).toBeInstanceOf(PhpParseError)
expect(e.message).toContain('数组键')
})
it('s: 内容含 NUL(\\u0000)原样往返,不截断不报错(单指针扫描无 C 式终止符语义)', () => {
expect(phpUnserialize('s:3:"a\0b";')).toBe('a\0b')
// NUL 作键亦不炸:Map 保留该键
expect(phpUnserialize('a:1:{s:3:"a\0b";i:1;}')).toStrictEqual(new Map([['a\0b', 1]]))
})
it('数字串键 s:1:"0" → Map(密集数组判定用严格 ===,字符串键永不伪装整数键 0)', () => {
expect(phpUnserialize('a:1:{s:1:"0";i:5;}')).toStrictEqual(new Map([['0', 5]]))
expect(JSON.stringify(phpValueToJson(phpUnserialize('a:1:{s:1:"0";i:5;}')))).toBe('{"0":5}')
})
it('1MB 输入性能烟雾:<50ms(防 O(n²) 回归——单指针扫描应线性)', () => {
const parts: string[] = []
for (let i = 0; i < 400; i++) parts.push(`i:${i};s:2500:"${'x'.repeat(2500)}";`)
const input = `a:400:{${parts.join('')}}`
expect(input.length).toBeGreaterThan(1_000_000) // 确证规模 ≥1MB(纯 ASCII:码元数=字节数)
// 元素数 = 400 键 + 400 值 + 1 数组自身 = 801 ≤ 默认 phpMaxElements=1000(不经限额捷径)
const t0 = performance.now()
const value = phpUnserialize(input)
const elapsed = performance.now() - t0
expect(Array.isArray(value)).toBe(true)
expect(value).toHaveLength(400)
expect(elapsed).toBeLessThan(50)
})
})
describe('错误类型层级:PhpError 公共基类', () => {
it('PhpParseError 与 PhpLimitError 均为 PhpError 子类,且互不为对方子类', () => {
const parse = capture(() => phpUnserialize('x:1:irrelevant;')) as PhpParseError