75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import { readU16, readU32, readNullTermString } from '../shared/binary-reader';
|
|
import type { PartitionEntry, PartitionTable } from './types';
|
|
import { PartitionType } from './types';
|
|
import { ENTRY_SIZE, ENTRY_MAGIC, MD5_MAGIC, NAME_FIELD_SIZE, MAX_ENTRIES } from './constants';
|
|
import { md5 } from './md5';
|
|
|
|
/**
|
|
* Parse ESP32 partition table binary.
|
|
* @param data Raw binary data (the partition table region, typically 3KB at offset 0x8000)
|
|
*/
|
|
export function parseBinary(data: Uint8Array): PartitionTable {
|
|
const entries: PartitionEntry[] = [];
|
|
let md5Valid = false;
|
|
let corrupted = false;
|
|
let offset = 0;
|
|
|
|
for (let i = 0; i < MAX_ENTRIES && offset + ENTRY_SIZE <= data.length; i++) {
|
|
const magic = readU16(data, offset);
|
|
|
|
if (magic === 0xFFFF) {
|
|
// Empty entry — end of table (erased flash)
|
|
break;
|
|
}
|
|
|
|
if (magic === MD5_MAGIC) {
|
|
// MD5 checksum entry
|
|
// Bytes [16..31] contain the stored MD5 hash of all preceding bytes
|
|
const storedMd5 = data.subarray(offset + 16, offset + 32);
|
|
const computedMd5 = md5(data.subarray(0, offset));
|
|
|
|
md5Valid = storedMd5.length === 16 && computedMd5.length === 16 &&
|
|
storedMd5.every((v, j) => v === computedMd5[j]);
|
|
break;
|
|
}
|
|
|
|
if (magic !== ENTRY_MAGIC) {
|
|
// Unknown magic — binary is corrupted
|
|
corrupted = true;
|
|
break;
|
|
}
|
|
|
|
// Parse 32-byte entry:
|
|
// [0..1] magic (already read)
|
|
// [2] type
|
|
// [3] subtype
|
|
// [4..7] offset (LE uint32)
|
|
// [8..11] size (LE uint32)
|
|
// [12..27] name (null-terminated, 16 bytes)
|
|
// [28..31] flags (LE uint32)
|
|
const type = data[offset + 2] as PartitionType;
|
|
const subtype = data[offset + 3];
|
|
const partOffset = readU32(data, offset + 4);
|
|
const size = readU32(data, offset + 8);
|
|
const name = readNullTermString(data, offset + 12, NAME_FIELD_SIZE);
|
|
const flags = readU32(data, offset + 28);
|
|
|
|
entries.push({ name, type, subtype, offset: partOffset, size, flags });
|
|
offset += ENTRY_SIZE;
|
|
}
|
|
|
|
// When the table is exactly full (MAX_ENTRIES entries), the loop exits on
|
|
// i >= MAX_ENTRIES without ever seeing the MD5 slot. Check it explicitly.
|
|
if (!md5Valid && offset + ENTRY_SIZE <= data.length) {
|
|
const magic = readU16(data, offset);
|
|
if (magic === MD5_MAGIC) {
|
|
const storedMd5 = data.subarray(offset + 16, offset + 32);
|
|
const computedMd5 = md5(data.subarray(0, offset));
|
|
md5Valid = storedMd5.length === 16 && computedMd5.length === 16 &&
|
|
storedMd5.every((v, j) => v === computedMd5[j]);
|
|
}
|
|
}
|
|
|
|
return { entries, md5Valid, ...(corrupted ? { corrupted } : {}) };
|
|
}
|