105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import { readU8, readU16, readU32, readNullTermString } from '../shared/binary-reader';
|
|
import type {
|
|
AppImageInfo, ImageHeader, ExtendedHeader, SegmentHeader, AppDescription,
|
|
} from './types';
|
|
import {
|
|
IMAGE_MAGIC, IMAGE_HEADER_SIZE, EXTENDED_HEADER_SIZE,
|
|
SEGMENT_HEADER_SIZE, APP_DESC_MAGIC, APP_DESC_SIZE, CHIP_ID_NAMES,
|
|
} from './constants';
|
|
|
|
/**
|
|
* Parse an ESP32 app image binary.
|
|
* Read-only parser — extracts headers, segment info, and app description.
|
|
*
|
|
* @param data Raw binary data of the app image
|
|
*/
|
|
export function parseAppImage(data: Uint8Array): AppImageInfo {
|
|
if (data.length < IMAGE_HEADER_SIZE + EXTENDED_HEADER_SIZE) {
|
|
throw new Error(`Image too short: ${data.length}B`);
|
|
}
|
|
|
|
// ── Image Header (8 bytes: magic + segments + spi_mode + spi_speed_size + entry_addr) ──
|
|
const magic = readU8(data, 0);
|
|
if (magic !== IMAGE_MAGIC) {
|
|
throw new Error(`Invalid image magic: 0x${magic.toString(16)}`);
|
|
}
|
|
|
|
const segmentCount = readU8(data, 1);
|
|
const spiModeRaw = readU8(data, 2);
|
|
const spiSpeedSize = readU8(data, 3);
|
|
const entryPoint = readU32(data, 4);
|
|
|
|
const header: ImageHeader = {
|
|
magic,
|
|
segmentCount,
|
|
spiMode: spiModeRaw,
|
|
spiSpeed: spiSpeedSize & 0x0F,
|
|
spiSize: (spiSpeedSize >> 4) & 0x0F,
|
|
entryPoint,
|
|
};
|
|
|
|
// ── Extended Header (16 bytes at offset 8) ──
|
|
const extOff = IMAGE_HEADER_SIZE;
|
|
const wpPin = readU8(data, extOff); // +0 = offset 8
|
|
const chipId = readU16(data, extOff + 4); // +4 = offset 12
|
|
const minChipRev = readU8(data, extOff + 6); // +6 = offset 14
|
|
const minChipRevFull = readU16(data, extOff + 7); // +7 = offset 15 (packed)
|
|
const maxChipRevFull = readU16(data, extOff + 9); // +9 = offset 17 (packed)
|
|
const hashAppended = readU8(data, extOff + 15) === 1; // +15 = offset 23
|
|
|
|
const extendedHeader: ExtendedHeader = {
|
|
wpPin,
|
|
spiPinDrv: [readU8(data, extOff + 1), readU8(data, extOff + 2), readU8(data, extOff + 3)],
|
|
chipId,
|
|
minChipRev,
|
|
minChipRevFull,
|
|
maxChipRevFull,
|
|
hashAppended,
|
|
};
|
|
|
|
// ── Segment Headers ──
|
|
const segments: SegmentHeader[] = [];
|
|
// Track each segment's data start offset internally (not exposed in public API)
|
|
const segDataOffsets: number[] = [];
|
|
let segOff = IMAGE_HEADER_SIZE + EXTENDED_HEADER_SIZE;
|
|
|
|
for (let i = 0; i < segmentCount && segOff + SEGMENT_HEADER_SIZE <= data.length; i++) {
|
|
const loadAddr = readU32(data, segOff);
|
|
const dataLen = readU32(data, segOff + 4);
|
|
const dataOffset = segOff + SEGMENT_HEADER_SIZE;
|
|
if (dataOffset + dataLen > data.length) break; // truncated image
|
|
segments.push({ loadAddr, dataLen });
|
|
segDataOffsets.push(dataOffset);
|
|
segOff = dataOffset + dataLen; // advance past segment data
|
|
}
|
|
|
|
// ── App Description — scan all segments for APP_DESC_MAGIC ──
|
|
let appDescription: AppDescription | null = null;
|
|
for (const off of segDataOffsets) {
|
|
if (off + 4 > data.length) continue;
|
|
const descMagic = readU32(data, off);
|
|
if (descMagic === APP_DESC_MAGIC && off + APP_DESC_SIZE <= data.length) {
|
|
appDescription = {
|
|
magicWord: descMagic,
|
|
secureVersion: readU32(data, off + 4),
|
|
version: readNullTermString(data, off + 16, 32),
|
|
projectName: readNullTermString(data, off + 48, 32),
|
|
compileTime: readNullTermString(data, off + 80, 16),
|
|
compileDate: readNullTermString(data, off + 96, 16),
|
|
idfVersion: readNullTermString(data, off + 112, 32),
|
|
appElfSha256: new Uint8Array(data.subarray(off + 144, off + 176)),
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
header,
|
|
extendedHeader,
|
|
segments,
|
|
appDescription,
|
|
valid: segments.length === segmentCount, // false if image was truncated mid-segment
|
|
chipName: CHIP_ID_NAMES[chipId] ?? `Unknown (0x${chipId.toString(16)})`,
|
|
};
|
|
}
|