134 lines
6.2 KiB
TypeScript
134 lines
6.2 KiB
TypeScript
import { createHash } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildFirmwareHashInput,
|
|
calculateFirmwareHashReport,
|
|
encodeFirmwareBlockHeader,
|
|
type FirmwareHashByteOrder,
|
|
type FirmwareHashMode,
|
|
} from '../src/lib/firmwareHash'
|
|
import { textToBytes } from '../src/lib/bytes'
|
|
|
|
function hash(algorithm: 'sha256' | 'sha512', data: Uint8Array): string {
|
|
return createHash(algorithm).update(data).digest('hex').toUpperCase()
|
|
}
|
|
|
|
describe('firmware block headers', () => {
|
|
it('encodes two uint32 fields as big endian by default', () => {
|
|
expect(encodeFirmwareBlockHeader(0x08000000, 0x123)).toEqual(Uint8Array.from([
|
|
0x08, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x01, 0x23,
|
|
]))
|
|
})
|
|
|
|
it('supports explicit little endian serialization', () => {
|
|
expect(encodeFirmwareBlockHeader(0x08000000, 0x123, 'little-endian')).toEqual(Uint8Array.from([
|
|
0x00, 0x00, 0x00, 0x08,
|
|
0x23, 0x01, 0x00, 0x00,
|
|
]))
|
|
})
|
|
|
|
it('accepts uint32 boundaries and rejects invalid values', () => {
|
|
expect(encodeFirmwareBlockHeader(0xffffffff, 0xffffffff)).toEqual(Uint8Array.from([
|
|
0xff, 0xff, 0xff, 0xff,
|
|
0xff, 0xff, 0xff, 0xff,
|
|
]))
|
|
expect(() => encodeFirmwareBlockHeader(-1, 1)).toThrow('起始地址')
|
|
expect(() => encodeFirmwareBlockHeader(0x1_0000_0000, 1)).toThrow('起始地址')
|
|
expect(() => encodeFirmwareBlockHeader(0, 0)).toThrow('数据长度')
|
|
expect(() => encodeFirmwareBlockHeader(0, 0x1_0000_0000)).toThrow('数据长度')
|
|
expect(() => encodeFirmwareBlockHeader(0, 1, 'middle-endian' as FirmwareHashByteOrder)).toThrow('字节序')
|
|
})
|
|
})
|
|
|
|
describe('firmware hash input', () => {
|
|
const blocks = [
|
|
{ startAddress: 0x20, data: Uint8Array.from([0xaa, 0xbb]) },
|
|
{ startAddress: 0, data: Uint8Array.from([0x01, 0x02]) },
|
|
]
|
|
|
|
it('sorts blocks and concatenates only effective bytes without filling gaps', () => {
|
|
expect(buildFirmwareHashInput(blocks, 'data-only')).toEqual(Uint8Array.from([0x01, 0x02, 0xaa, 0xbb]))
|
|
})
|
|
|
|
it('prefixes every sorted block with its binary address and data length', () => {
|
|
expect(buildFirmwareHashInput(blocks, 'block-header')).toEqual(Uint8Array.from([
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02,
|
|
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0xaa, 0xbb,
|
|
]))
|
|
})
|
|
|
|
it('uses explicit little endian fields without changing block data', () => {
|
|
expect(buildFirmwareHashInput(blocks, 'block-header', 'little-endian')).toEqual(Uint8Array.from([
|
|
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x02,
|
|
0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xaa, 0xbb,
|
|
]))
|
|
})
|
|
|
|
it('accepts one byte at the highest uint32 address', () => {
|
|
expect(buildFirmwareHashInput([
|
|
{ startAddress: 0xffffffff, data: Uint8Array.of(0xaa) },
|
|
], 'block-header')).toEqual(Uint8Array.from([
|
|
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0xaa,
|
|
]))
|
|
})
|
|
|
|
it('rejects invalid, overlapping and overflowing blocks', () => {
|
|
expect(() => buildFirmwareHashInput([], 'data-only')).toThrow('有效数据块')
|
|
expect(() => buildFirmwareHashInput([{ startAddress: 0, data: new Uint8Array() }], 'data-only')).toThrow('数据长度')
|
|
expect(() => buildFirmwareHashInput([{ startAddress: 0xffffffff, data: Uint8Array.of(1, 2) }], 'data-only')).toThrow('32 位空间')
|
|
expect(() => buildFirmwareHashInput([
|
|
{ startAddress: 0, data: Uint8Array.of(1, 2) },
|
|
{ startAddress: 1, data: Uint8Array.of(2, 3) },
|
|
], 'data-only')).toThrow('重叠')
|
|
expect(() => buildFirmwareHashInput([
|
|
{ startAddress: 0, data: Uint8Array.of(1, 2) },
|
|
{ startAddress: 2, data: Uint8Array.of(3) },
|
|
], 'block-header')).toThrow('必须先合并')
|
|
expect(() => buildFirmwareHashInput([{ startAddress: 0, data: Uint8Array.of(1) }], 'unknown' as FirmwareHashMode)).toThrow('模式')
|
|
})
|
|
})
|
|
|
|
describe('firmware SHA report', () => {
|
|
const blocks = [
|
|
{ startAddress: 0x1000, data: textToBytes('1234') },
|
|
{ startAddress: 0x2000, data: textToBytes('56789') },
|
|
]
|
|
|
|
it('matches standard SHA-256 and SHA-512 values for data-only mode', async () => {
|
|
const report = await calculateFirmwareHashReport(blocks, 'data-only')
|
|
expect(report.combined.sha256).toBe('15E2B0D3C33891EBB0F1EF609EC419420C20E320CE94C65FBC8C3312448EB225')
|
|
expect(report.combined.sha512).toBe('D9E6762DD1C8EAF6D61B3C6192FC408D4D6D5F1176D0C29169BC24E71C3F274AD27FCD5811B313D681F7E55EC02D73D499C95455B6B5BB503ACF574FBA8FFE85')
|
|
expect(report).toMatchObject({ hashedBytes: 9, headerBytes: 0 })
|
|
expect(report.blocks[0].headerHex).toBe('')
|
|
expect(report.blocks[0].sha256).toBe(hash('sha256', textToBytes('1234')))
|
|
expect(report.blocks[1].sha512).toBe(hash('sha512', textToBytes('56789')))
|
|
})
|
|
|
|
it('hashes each binary block header with its data and reports exact byte counts', async () => {
|
|
const combinedInput = buildFirmwareHashInput(blocks, 'block-header', 'big-endian')
|
|
const report = await calculateFirmwareHashReport(blocks, 'block-header', 'big-endian')
|
|
expect(report.combined).toEqual({
|
|
sha256: hash('sha256', combinedInput),
|
|
sha512: hash('sha512', combinedInput),
|
|
})
|
|
expect(report).toMatchObject({ hashedBytes: 25, headerBytes: 16 })
|
|
expect(report.blocks.map((block) => block.headerHex)).toEqual([
|
|
'00 00 10 00 00 00 00 04',
|
|
'00 00 20 00 00 00 00 05',
|
|
])
|
|
expect(report.blocks[0].sha256).toBe(hash('sha256', buildFirmwareHashInput([blocks[0]], 'block-header')))
|
|
expect(report.blocks[1].sha512).toBe(hash('sha512', buildFirmwareHashInput([blocks[1]], 'block-header')))
|
|
})
|
|
|
|
it('binds addresses only in block-header mode', async () => {
|
|
const relocated = blocks.map((block) => ({ ...block, startAddress: block.startAddress + 0x10000 }))
|
|
const direct = await calculateFirmwareHashReport(blocks, 'data-only')
|
|
const relocatedDirect = await calculateFirmwareHashReport(relocated, 'data-only')
|
|
const withHeaders = await calculateFirmwareHashReport(blocks, 'block-header')
|
|
const relocatedWithHeaders = await calculateFirmwareHashReport(relocated, 'block-header')
|
|
expect(relocatedDirect.combined).toEqual(direct.combined)
|
|
expect(relocatedWithHeaders.combined).not.toEqual(withHeaders.combined)
|
|
})
|
|
})
|