import { describe, expect, it } from 'vitest' import { phpUnserialize, PhpLimitError, PhpParseError, type PhpValue, } from '../src/parse/php-unserialize.js' // 捕获抛出的错误实例(而非仅匹配),以便断言 offset / kind / message 细节 function capture(fn: () => unknown): unknown { try { fn() } catch (e) { return e } throw new Error('期望抛出但未抛出') } describe('phpUnserialize:标量与真实夹具', () => { it('真实夹具串(wppop_sections 形状):逐层逐值深度等值', () => { const input = 'a:1:{i:0;a:2:{s:5:"title";s:8:"SpecsTab";s:7:"content";s:10:"

Tab

";}}' // 外层 i:0 连续键 → 密集数组;内层 s: 键 → Map(保留插入序) expect(phpUnserialize(input)).toStrictEqual([ new Map([ ['title', 'SpecsTab'], ['content', '

Tab

'], ]), ]) }) it('标量四类型:s → string、i → number(冻结)、b → boolean、d → number', () => { expect(phpUnserialize('s:5:"hello";')).toBe('hello') expect(phpUnserialize('i:42;')).toBe(42) expect(phpUnserialize('i:-7;')).toBe(-7) expect(phpUnserialize('b:1;')).toBe(true) expect(phpUnserialize('b:0;')).toBe(false) expect(phpUnserialize('d:1.5;')).toBe(1.5) }) it('s: 长度前缀按 UTF-8 字节计:CJK(3 字节/字)与增补平面(4 字节)均准确', () => { expect(phpUnserialize('s:6:"中文";')).toBe('中文') expect(phpUnserialize('s:4:"😀";')).toBe('😀') expect(phpUnserialize('s:8:"a中😀";')).toBe('a中😀') }) }) describe('phpUnserialize:a: 形状推断(Map vs 密集数组)', () => { it('字符串键 → Map', () => { expect(phpUnserialize('a:1:{s:3:"foo";i:1;}')).toStrictEqual(new Map([['foo', 1]])) }) it('连续整数键(0..n-1 依序)→ 密集数组', () => { expect(phpUnserialize('a:3:{i:0;i:10;i:1;i:20;i:2;i:30;}')).toStrictEqual([10, 20, 30]) }) it('混合键(i: 与 s: 并存,真实 WP 数据两类都有)→ Map', () => { expect(phpUnserialize('a:2:{i:0;b:1;s:1:"k";s:1:"v";}')).toStrictEqual( new Map([ [0, true], ['k', 'v'], ]), ) }) it('稀疏/非 0 起始的整数键 → Map(键保留原值)', () => { expect(phpUnserialize('a:2:{i:1;s:1:"a";i:2;s:1:"b";}')).toStrictEqual( new Map([ [1, 'a'], [2, 'b'], ]), ) }) it('a:0:{} → 空数组', () => { expect(phpUnserialize('a:0:{}')).toStrictEqual([]) }) }) describe('phpUnserialize:长度前缀与结构错误(不静默,抛 PhpParseError 带 offset)', () => { it('长度前缀错误:声明 7 实际 8 字符 → 抛,offset 指向不符处,message 含位置', () => { const e = capture(() => phpUnserialize('s:7:"SpecsTab";')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e).not.toBeInstanceOf(PhpLimitError) expect(e.offset).toBe(12) // 's:7:"SpecsTa|b'——字节计与声明不符处 expect(e.message).toContain('12') }) it('多字节字符使字节计数超出声明 → 抛', () => { const e = capture(() => phpUnserialize('s:2:"中";')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e.offset).toBe(5) }) it('空串 → 抛,offset 0', () => { const e = capture(() => phpUnserialize('')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e.offset).toBe(0) }) it('截断串(字符串内容未达声明字节数)→ 抛,offset 指向扫描停点', () => { const e = capture(() => phpUnserialize('a:1:{i:0;s:5:"ab')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e.offset).toBe(16) }) it('尾随垃圾 → 抛(解析完成后必须到达串尾)', () => { const e = capture(() => phpUnserialize('i:1;junk')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e.offset).toBe(4) }) it('非法类型前缀 x: → 抛', () => { const e = capture(() => phpUnserialize('x:1:irrelevant;')) as PhpParseError expect(e).toBeInstanceOf(PhpParseError) expect(e.offset).toBe(0) }) it('不在支持范围的 PHP 类型(N;/O:)→ 抛(本解析器仅支持 i/b/d/s/a)', () => { expect(capture(() => phpUnserialize('N;'))).toBeInstanceOf(PhpParseError) expect(capture(() => phpUnserialize('O:8:"stdClass":0:{}'))).toBeInstanceOf(PhpParseError) }) it('d:INF / d:NAN → 抛(冻结:不引入非有限数值)', () => { expect(capture(() => phpUnserialize('d:INF;'))).toBeInstanceOf(PhpParseError) expect(capture(() => phpUnserialize('d:NAN;'))).toBeInstanceOf(PhpParseError) }) it('a: 声明元素数与实际不符 → 抛', () => { expect(capture(() => phpUnserialize('a:2:{i:0;s:1:"a";}'))).toBeInstanceOf(PhpParseError) }) it('数组键只支持 i:/s: → b: 作键抛', () => { expect(capture(() => phpUnserialize('a:1:{b:1;i:0;}'))).toBeInstanceOf(PhpParseError) }) }) describe('phpUnserialize:限额(默认取 DEFAULT_WXR_LIMITS:phpMaxDepth=8 / phpMaxElements=1000)', () => { const nestStr = (depth: number): string => (depth === 0 ? 'i:0;' : `a:1:{i:0;${nestStr(depth - 1)}}`) it('深度边界:默认限下嵌套 8 层通过,且形状逐层正确', () => { const result = phpUnserialize(nestStr(8)) expect(JSON.stringify(result)).toBe('['.repeat(8) + '0' + ']'.repeat(8)) }) it('深度超限:第 9 层 → 抛 PhpLimitError(kind=depth,区别于 PhpParseError)', () => { const e = capture(() => phpUnserialize(nestStr(9))) as PhpLimitError expect(e).toBeInstanceOf(PhpLimitError) expect(e).not.toBeInstanceOf(PhpParseError) expect(e.kind).toBe('depth') }) it('自定义 maxDepth:2 层过、3 层抛', () => { expect(JSON.stringify(phpUnserialize(nestStr(2), { maxDepth: 2 }))).toBe('[[0]]') const e = capture(() => phpUnserialize(nestStr(3), { maxDepth: 2 })) as PhpLimitError expect(e).toBeInstanceOf(PhpLimitError) expect(e.kind).toBe('depth') }) it('元素数超限(预检路径):扁平 a:1001:{} → 抛 PhpLimitError(kind=elements)', () => { const e = capture(() => phpUnserialize('a:1001:{}')) as PhpLimitError expect(e).toBeInstanceOf(PhpLimitError) expect(e).not.toBeInstanceOf(PhpParseError) expect(e.kind).toBe('elements') }) it('元素数超限(解析中计数路径):值节点计数超自定义上限抛,恰好达限放行', () => { // 节点数 = 2 键 + 2 值 + 1 数组自身 = 5 const e = capture(() => phpUnserialize('a:2:{i:0;s:1:"a";i:1;s:1:"b";}', { maxElements: 4 })) as PhpLimitError expect(e).toBeInstanceOf(PhpLimitError) expect(e.kind).toBe('elements') expect(phpUnserialize('a:2:{i:0;s:1:"a";i:1;s:1:"b";}', { maxElements: 5 })).toStrictEqual(['a', 'b']) }) })