/** * AriaEngine WAL — CRC-32 校验升级测试 * * 覆盖: * 1. 新算法记录往返(append → recover) * 2. 篡改记录字节 → CRC 不匹配跳过 * 3. 旧版弱校验记录兼容(双算法探测) * 4. appendBatch 组提交记录往返 * 5. 损坏记录跳过不影响后续完整记录 */ import { WAL } from '../../src/engine/aria/wal/log'; import { WALRecordType } from '../../src/engine/aria/types'; // =================================================================== // WAL 内存 Store(模拟 IStorageBackend 单记录语义) // =================================================================== class MemoryWALStore { chunks: Uint8Array[] = []; truncated = false; async append(data: Uint8Array): Promise { this.chunks.push(data); } async readAll(): Promise { const total = this.chunks.reduce((s, c) => s + c.byteLength, 0); const combined = new Uint8Array(total); let off = 0; for (const c of this.chunks) { combined.set(c, off); off += c.byteLength; } return combined; } async truncate(): Promise { this.chunks = []; this.truncated = true; } async exists(): Promise { return this.chunks.length > 0; } } function makeRecord(over: Partial = {}) { return { type: WALRecordType.INSERT, txnId: 0, tableName: 'users', key: 'u-1', data: { id: 'u-1', name: 'Alice' }, ...over, }; } describe('AriaEngine — WAL CRC-32 校验', () => { it('新算法记录 append → recover 往返完整', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); await wal.append(makeRecord({ key: 'u-1' })); await wal.append(makeRecord({ key: 'u-2', data: { id: 'u-2', name: 'Bob' } })); const records: import('../../src/engine/aria/types').WALRecord[] = []; await wal.recover((r) => records.push(r)); expect(records).toHaveLength(2); expect(records[0].key).toBe('u-1'); expect(records[1].data).toEqual({ id: 'u-2', name: 'Bob' }); }); it('appendBatch 组提交记录往返完整', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); await wal.appendBatch([ makeRecord({ key: 'a' }), makeRecord({ key: 'b' }), makeRecord({ key: 'c' }), ]); const records: import('../../src/engine/aria/types').WALRecord[] = []; await wal.recover((r) => records.push(r)); expect(records.map((r) => r.key)).toEqual(['a', 'b', 'c']); expect(records.map((r) => r.lsn)).toEqual([1, 2, 3]); }); it('篡改记录数据 → CRC 不匹配跳过', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); await wal.append(makeRecord({ key: 'good-1' })); await wal.append(makeRecord({ key: 'evil' })); await wal.append(makeRecord({ key: 'good-2' })); // 篡改第二条记录的 data 字节(LSN=2 的记录) const raw = await store.readAll(); const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); // 定位第二条记录起始:按记录格式顺序扫描直到 LSN=2 // [lsn:4][type:1][txnId:4][tblLen:2][table][keyLen:2][key][jsonLen:4][json][crc:4] let offset = 0; while (offset + 4 <= raw.byteLength && view.getUint32(offset, false) !== 2) { const tblLen = view.getUint16(offset + 9, false); const keyLenOff = offset + 11 + tblLen; const keyLen = view.getUint16(keyLenOff, false); const jsonLenOff = keyLenOff + 2 + keyLen; const jsonLen = view.getUint32(jsonLenOff, false); offset += 4 + 1 + 4 + 2 + tblLen + 2 + keyLen + 4 + jsonLen + 4; } expect(view.getUint32(offset, false)).toBe(2); // 篡改 data 区(json 起始处第 4 字节) const tblLen2 = view.getUint16(offset + 9, false); const jsonStart = offset + 4 + 1 + 4 + 2 + tblLen2 + 2 + view.getUint16(offset + 11 + tblLen2, false) + 4; raw[jsonStart + 4] ^= 0x01; // 写回 store(readAll 返回的是合并副本) store.chunks = [raw]; // 恢复:坏记录被跳过,前后好记录保留 const records: import('../../src/engine/aria/types').WALRecord[] = []; await wal.recover((r) => records.push(r)); expect(records.map((r) => r.key)).toEqual(['good-1', 'good-2']); }); it('旧版弱校验记录兼容(双算法探测)', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); // 手工构造一条"旧算法"记录:与 encodeRecord 布局一致但 CRC 用旧算法计算 const record = { lsn: 1, type: WALRecordType.INSERT as number, txnId: 0, tableName: 'legacy', key: 'old', data: { id: 'old', v: 1 }, }; const encoder = new TextEncoder(); const tblB = encoder.encode(record.tableName); const keyB = encoder.encode(record.key); const jsonB = encoder.encode(JSON.stringify(record.data)); const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + jsonB.length + 4; const buf = new ArrayBuffer(size); const v = new DataView(buf); let o = 0; v.setUint32(o, record.lsn, false); o += 4; v.setUint8(o, record.type); o += 1; v.setUint32(o, record.txnId, false); o += 4; v.setUint16(o, tblB.length, false); o += 2; new Uint8Array(buf).set(tblB, o); o += tblB.length; v.setUint16(o, keyB.length, false); o += 2; new Uint8Array(buf).set(keyB, o); o += keyB.length; v.setUint32(o, jsonB.length, false); o += 4; new Uint8Array(buf).set(jsonB, o); o += jsonB.length; // 旧算法 CRC let crc = 0; const u8 = new Uint8Array(buf, 0, o); for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0; v.setUint32(o, crc >>> 0, false); store.chunks.push(new Uint8Array(buf)); const records: import('../../src/engine/aria/types').WALRecord[] = []; await wal.recover((r) => records.push(r)); expect(records).toHaveLength(1); expect(records[0].tableName).toBe('legacy'); expect(records[0].data).toEqual({ id: 'old', v: 1 }); }); it('旧算法记录 + 新算法记录混合 → 全部正确恢复', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); // 先放一条旧算法记录 const record = { lsn: 1, type: WALRecordType.DELETE as number, txnId: 0, tableName: 't', key: 'old-key', data: undefined as Record | undefined, }; const encoder = new TextEncoder(); const tblB = encoder.encode(record.tableName); const keyB = encoder.encode(record.key); const size = 4 + 1 + 4 + 2 + tblB.length + 2 + keyB.length + 4 + 4; const buf = new ArrayBuffer(size); const v = new DataView(buf); let o = 0; v.setUint32(o, record.lsn, false); o += 4; v.setUint8(o, record.type); o += 1; v.setUint32(o, record.txnId, false); o += 4; v.setUint16(o, tblB.length, false); o += 2; new Uint8Array(buf).set(tblB, o); o += tblB.length; v.setUint16(o, keyB.length, false); o += 2; new Uint8Array(buf).set(keyB, o); o += keyB.length; v.setUint32(o, 0, false); o += 4; // jsonLen=0 let crc = 0; const u8 = new Uint8Array(buf, 0, o); for (let i = 0; i < u8.length; i++) crc = ((crc << 5) - crc + u8[i]) | 0; v.setUint32(o, crc >>> 0, false); store.chunks.push(new Uint8Array(buf)); // 再追加一条新算法记录(LSN=2) await wal.append(makeRecord({ key: 'new-key', lsn: 2 } as never)); const records: import('../../src/engine/aria/types').WALRecord[] = []; await wal.recover((r) => records.push(r)); expect(records.map((r) => r.key)).toEqual(['old-key', 'new-key']); expect(records[0].type).toBe(WALRecordType.DELETE); }); it('checkpoint 截断后 exists 返回 false', async () => { const store = new MemoryWALStore(); const wal = new WAL(store as never, true, 'full'); await wal.append(makeRecord({ key: 'x' })); expect(await store.exists()).toBe(true); await wal.checkpoint(); expect(store.truncated).toBe(true); expect(await store.exists()).toBe(false); }); });