Files
WebTools/tests/crc.test.ts
T

165 lines
7.3 KiB
TypeScript
Raw 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, formatCrc } 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()}`
}
describe('CRC', () => {
it('matches standard check values for 123456789', () => {
const input = textToBytes('123456789')
for (const preset of CRC_PRESETS) {
expect(calculateCrc(input, preset), preset.name).toBe(preset.check)
expect(formatCrc(preset.check, preset.width)).toHaveLength(Math.ceil(preset.width / 4))
}
})
it('supports CRC-64 / ECMA-182 parameters', () => {
expect(calculateCrc(textToBytes('123456789'), {
width: 64,
polynomial: 0x42f0e1eba9ea3693n,
initialValue: 0n,
inputReflected: false,
outputReflected: false,
xorOut: 0n,
})).toBe(0x6c40df5f0b497347n)
})
it('supports reflected CRC-64 / XZ parameters', () => {
expect(calculateCrc(textToBytes('123456789'), {
width: 64,
polynomial: 0x42f0e1eba9ea3693n,
initialValue: 0xffffffffffffffffn,
inputReflected: true,
outputReflected: true,
xorOut: 0xffffffffffffffffn,
})).toBe(0x995dc9bbdf1939fan)
})
it('calculates chunked data as one continuous byte stream', () => {
const preset = CRC_PRESETS[0]
const chunks = [textToBytes('123'), textToBytes('456'), textToBytes('789')]
expect(calculateCrcChunks(chunks, preset)).toBe(calculateCrc(textToBytes('123456789'), preset))
})
it('calculates inline Hex with the same separators used by other tools', () => {
const preset = CRC_PRESETS[0]
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('rejects empty, malformed, and oversized inline Hex', () => {
const preset = CRC_PRESETS[0]
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 = CRC_PRESETS[0]
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('位宽')
})
it('reports both the combined CRC and each block CRC', () => {
const parameters = CRC_PRESETS.find((preset) => preset.id === 'crc-32-iso-hdlc')!
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) },
])
})
})
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('文件为空')
})
})