import { createHash } from 'node:crypto' import { describe, expect, it } from 'vitest' import { buildFirmwareHashInput, calculateFirmwareHashReport, digestFirmwareBytes, 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) }) }) describe('firmware digest provider', () => { it('falls back to the local implementation when SubtleCrypto is unavailable', async () => { const data = textToBytes('plain HTTP LAN fallback') expect(await digestFirmwareBytes('SHA-256', data, undefined)).toBe(hash('sha256', data)) expect(await digestFirmwareBytes('SHA-512', data, undefined)).toBe(hash('sha512', data)) }) })