import { describe, expect, it } from 'vitest' import { calculateCrc, calculateCrcChunks, CRC_PRESETS, DEFAULT_CRC_PRESET, formatCrc, type CrcPreset, } from '../src/lib/crc' import { calculateCrcHexInput, calculateCrcReport, MAX_INLINE_CRC_DATA_BYTES, MAX_INLINE_CRC_INPUT_CHARS, } from '../src/lib/crcWorker' import { getFirmwareFormat, parseFirmwareContent } from '../src/lib/firmware' import { concatenateBlocks, parseIntelHex } from '../src/lib/intelHex' import { textToBytes } from '../src/lib/bytes' function intelHexRecord(address: number, data: number[]): string { const bytes = [data.length, address >> 8, address & 0xff, 0, ...data] const checksum = (-bytes.reduce((sum, byte) => sum + byte, 0)) & 0xff return `:${[...bytes, checksum].map((byte) => byte.toString(16).padStart(2, '0')).join('').toUpperCase()}` } const STANDARD_CRC_PRESETS = [ { id: 'crc-32-iso-hdlc', name: 'CRC-32/ISO-HDLC', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xcbf43926n }, { id: 'crc-32-iscsi', name: 'CRC-32/ISCSI (CRC-32C)', width: 32, polynomial: 0x1edc6f41n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xe3069283n }, { id: 'crc-32-mpeg-2', name: 'CRC-32/MPEG-2', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x0376e6e7n }, { id: 'crc-16-arc', name: 'CRC-16/ARC', width: 16, polynomial: 0x8005n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xbb3dn }, { id: 'crc-16-dnp', name: 'CRC-16/DNP', width: 16, polynomial: 0x3d65n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xea82n }, { id: 'crc-16-ccitt-false', name: 'CRC-16/IBM-3740 (CCITT-FALSE)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x29b1n }, { id: 'crc-16-ibm-sdlc', name: 'CRC-16/IBM-SDLC (X-25)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0x906en }, { id: 'crc-16-kermit', name: 'CRC-16/KERMIT (CCITT)', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x2189n }, { id: 'crc-16-modbus', name: 'CRC-16/MODBUS', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x4b37n }, { id: 'crc-16-usb', name: 'CRC-16/USB', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xb4c8n }, { id: 'crc-16-xmodem', name: 'CRC-16/XMODEM', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x31c3n }, { id: 'crc-8', name: 'CRC-8/SMBUS (CRC-8)', width: 8, polynomial: 0x07n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0xf4n }, { id: 'crc-8-maxim-dow', name: 'CRC-8/MAXIM-DOW', width: 8, polynomial: 0x31n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xa1n }, { id: 'crc-8-sae-j1850', name: 'CRC-8/SAE-J1850', width: 8, polynomial: 0x1dn, initialValue: 0xffn, inputReflected: false, outputReflected: false, xorOut: 0xffn, check: 0x4bn }, { id: 'crc-64-ecma-182', name: 'CRC-64/ECMA-182', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x6c40df5f0b497347n }, { id: 'crc-64-xz', name: 'CRC-64/XZ', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0xffffffffffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffffffffffn, check: 0x995dc9bbdf1939fan }, ] as const satisfies readonly CrcPreset[] function presetById(id: string): CrcPreset { const preset = CRC_PRESETS.find((candidate) => candidate.id === id) if (!preset) throw new Error(`Missing test preset: ${id}`) return preset } describe('CRC', () => { it('publishes the reviewed canonical preset names and parameters', () => { expect(CRC_PRESETS).toEqual(STANDARD_CRC_PRESETS) expect(DEFAULT_CRC_PRESET).toEqual(STANDARD_CRC_PRESETS[0]) expect(new Set(CRC_PRESETS.map((preset) => preset.id)).size).toBe(CRC_PRESETS.length) expect(new Set(CRC_PRESETS.map((preset) => preset.name)).size).toBe(CRC_PRESETS.length) expect(new Set(CRC_PRESETS.map((preset) => preset.width))).toEqual(new Set([8, 16, 32, 64])) const parameterSignatures = CRC_PRESETS.map((preset) => [ preset.width, preset.polynomial, preset.initialValue, preset.inputReflected, preset.outputReflected, preset.xorOut, ].join(':')) expect(new Set(parameterSignatures).size).toBe(CRC_PRESETS.length) }) it('matches standard check values for 123456789', () => { const input = textToBytes('123456789') for (const preset of STANDARD_CRC_PRESETS) { expect(calculateCrc(input, preset), preset.name).toBe(preset.check) expect(formatCrc(preset.check, preset.width)).toHaveLength(Math.ceil(preset.width / 4)) } }) it('handles empty input and fixed-width result boundaries', () => { for (const preset of STANDARD_CRC_PRESETS) { const mask = (1n << BigInt(preset.width)) - 1n expect(calculateCrc(new Uint8Array(), preset), preset.name).toBe((preset.initialValue ^ preset.xorOut) & mask) } expect(formatCrc(0n, 8)).toBe('00') expect(formatCrc(0xffffffffffffffffn, 64)).toBe('FFFFFFFFFFFFFFFF') }) it('calculates chunked data as one continuous byte stream', () => { const chunks = [textToBytes('123'), textToBytes('456'), textToBytes('789')] for (const preset of [DEFAULT_CRC_PRESET, presetById('crc-64-xz')]) { expect(calculateCrcChunks(chunks, preset)).toBe(calculateCrc(textToBytes('123456789'), preset)) } }) it('calculates inline Hex with the same separators used by other tools', () => { const preset = DEFAULT_CRC_PRESET const inputs = [ 'A51F7ADD', 'A5 1F 7A DD', 'A5,1F,7A,DD', 'A5,1F,7A,DD', '0xA5 0x1F 0x7A 0xDD', '0xa5, 1F\n0x7a,DD', ] for (const input of inputs) { expect(calculateCrcHexInput(input, preset)).toEqual({ combinedResult: '6139ED73', totalBytes: 4, }) } }) it('calculates the CCITT/KERMIT preset through the inline Hex path', () => { expect(calculateCrcHexInput('31 32 33 34 35 36 37 38 39', presetById('crc-16-kermit'))).toEqual({ combinedResult: '2189', totalBytes: 9, }) }) it('rejects empty, malformed, and oversized inline Hex', () => { const preset = DEFAULT_CRC_PRESET expect(() => calculateCrcHexInput(' ', preset)).toThrow('请输入 Hex 数据') expect(() => calculateCrcHexInput('ABC', preset)).toThrow('偶数') expect(() => calculateCrcHexInput('A5,,1F', preset)).toThrow('片段缺少') expect(() => calculateCrcHexInput('GG', preset)).toThrow('无效') expect(() => calculateCrcHexInput('AA'.repeat(MAX_INLINE_CRC_DATA_BYTES + 1), preset)).toThrow('Hex 数据不能超过 1 MiB') expect(() => calculateCrcHexInput(' '.repeat(MAX_INLINE_CRC_INPUT_CHARS + 1), preset)).toThrow('输入不能超过') }) it('rejects malformed runtime parameters instead of coercing them', () => { const preset = DEFAULT_CRC_PRESET expect(() => calculateCrc(new Uint8Array(), { ...preset, inputReflected: undefined as never })).toThrow('布尔值') expect(() => calculateCrc(new Uint8Array(), { ...preset, polynomial: 7 as never })).toThrow('BigInt') expect(() => formatCrc(0x100n, 8)).toThrow('超出当前位宽') expect(() => formatCrc(0n, 7)).toThrow('位宽') expect(() => calculateCrc(new Uint8Array(), { ...preset, width: 65 })).toThrow('位宽') expect(() => calculateCrc(new Uint8Array(), { ...preset, polynomial: 0n })).toThrow('多项式') }) it('reports both the combined CRC and each block CRC', () => { const parameters = DEFAULT_CRC_PRESET const blocks = [ { startAddress: 0x1000, data: textToBytes('1234') }, { startAddress: 0x2000, data: textToBytes('56789') }, ] const report = calculateCrcReport(blocks, parameters) expect(report.combinedResult).toBe(formatCrc(parameters.check, parameters.width)) expect(report.blockResults).toEqual([ { startAddress: 0x1000, endAddress: 0x1003, length: 4, crc: formatCrc(calculateCrc(blocks[0].data, parameters), 32) }, { startAddress: 0x2000, endAddress: 0x2004, length: 5, crc: formatCrc(calculateCrc(blocks[1].data, parameters), 32) }, ]) }) it('preserves full 64-bit CRC values in block reports', () => { const parameters = presetById('crc-64-xz') const report = calculateCrcReport([{ startAddress: 0, data: textToBytes('123456789') }], parameters) expect(report.combinedResult).toBe('995DC9BBDF1939FA') expect(report.blockResults).toEqual([ { startAddress: 0, endAddress: 8, length: 9, crc: '995DC9BBDF1939FA' }, ]) }) }) describe('Intel HEX', () => { it('validates records and separates non-contiguous data into blocks', () => { const parsed = parseIntelHex(':0400000001020304F2\n:02001000AABB89\n:00000001FF\n') expect(parsed.totalBytes).toBe(6) expect(parsed.blocks).toHaveLength(2) expect(parsed.blocks[0]).toEqual({ startAddress: 0, data: Uint8Array.from([1, 2, 3, 4]) }) expect(parsed.blocks[1]).toEqual({ startAddress: 0x10, data: Uint8Array.from([0xaa, 0xbb]) }) expect(concatenateBlocks(parsed.blocks)).toEqual(Uint8Array.from([1, 2, 3, 4, 0xaa, 0xbb])) }) it('supports extended addresses and reports a missing EOF record', () => { const parsed = parseIntelHex(':020000040001F9\n:040000001122334452\n') expect(parsed.blocks[0].startAddress).toBe(0x10000) expect(parsed.warnings).toEqual(['文件缺少 EOF 记录,已按现有数据计算']) }) it('rejects invalid checksums and conflicting overlapping data', () => { expect(() => parseIntelHex(':0400000001020304F3\n:00000001FF')).toThrow('校验和错误') expect(() => parseIntelHex(':020000000102FB\n:02000100FF03FB\n:00000001FF')).toThrow('数据冲突') }) it('merges out-of-order and identical overlapping records without changing bytes', () => { const parsed = parseIntelHex([ intelHexRecord(2, [3, 4]), intelHexRecord(0, [1, 2, 3]), ':00000001FF', ].join('\n')) expect(parsed.blocks).toEqual([{ startAddress: 0, data: Uint8Array.from([1, 2, 3, 4]) }]) }) it('enforces configurable data-record and block limits', () => { const records = [intelHexRecord(0, [1]), intelHexRecord(2, [2]), ':00000001FF'].join('\n') expect(() => parseIntelHex(records, { maxDataRecords: 1 })).toThrow('数据记录不能超过 1 条') expect(() => parseIntelHex(records, { maxBlocks: 1 })).toThrow('数据块不能超过 1 个') expect(() => parseIntelHex(records, { maxBlocks: 0 })).toThrow('必须是正整数') }) }) describe('firmware parsing', () => { it('detects supported extensions case-insensitively', () => { expect(getFirmwareFormat('firmware.HEX')).toBe('Intel HEX') expect(getFirmwareFormat('firmware.ihx')).toBe('Intel HEX') expect(getFirmwareFormat('firmware.bin')).toBe('BIN') expect(() => getFirmwareFormat('firmware.txt')).toThrow('请选择') }) it('parses Intel HEX and BIN content with strict content types', () => { const hex = parseFirmwareContent('firmware.hex', `${intelHexRecord(0, [0xaa, 0xbb])}\n:00000001FF`) expect(hex.totalBytes).toBe(2) expect(hex.blocks[0].data).toEqual(Uint8Array.from([0xaa, 0xbb])) const bin = parseFirmwareContent('firmware.bin', Uint8Array.from([1, 2, 3])) expect(bin).toMatchObject({ format: 'BIN', totalBytes: 3, warnings: [] }) expect(() => parseFirmwareContent('firmware.hex', Uint8Array.of(1))).toThrow('必须是文本') expect(() => parseFirmwareContent('firmware.bin', '')).toThrow('必须是二进制') expect(() => parseFirmwareContent('firmware.bin', new Uint8Array())).toThrow('文件为空') }) })