Add firmware SHA hash tool

This commit is contained in:
mid
2026-08-08 02:45:44 +08:00
parent 55bc6ba15d
commit 290da46567
18 changed files with 821 additions and 18 deletions
+133
View File
@@ -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)
})
})