Add firmware SHA hash tool
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user