Files

221 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('文件为空')
})
})