yunsi-toolbox-vue/lib/nvs/nvs-binary-parser.ts

376 lines
13 KiB
TypeScript

import type { NvsPartition, NvsEntry } from './types';
import { NvsType, NvsVersion, PageState, EntryState } from './types';
import {
PAGE_SIZE, PAGE_HEADER_SIZE, BITMAP_OFFSET, BITMAP_SIZE,
FIRST_ENTRY_OFFSET, ENTRY_SIZE, ENTRIES_PER_PAGE, KEY_FIELD_SIZE,
} from './constants';
import { crc32 } from '../shared/crc32';
import {
readU8, readU16, readU32, readI8, readI16, readI32, readU64, readI64,
readNullTermString,
} from '../shared/binary-reader';
import { generateEntryId } from './nvs-partition';
// ── Entry state bitmap ─────────────────────────────────────────────
function getEntryState(bitmap: Uint8Array, index: number): EntryState {
const bitPos = index * 2;
const byteIdx = Math.floor(bitPos / 8);
const bitOff = bitPos % 8;
return ((bitmap[byteIdx] >> bitOff) & 0x3) as EntryState;
}
// ── Entry CRC verification ─────────────────────────────────────────
/** Entry CRC is over bytes [0..3] + [8..31], skipping the CRC field [4..7] */
function computeEntryCrc(entryBytes: Uint8Array): number {
const crcData = new Uint8Array(28);
crcData.set(entryBytes.subarray(0, 4), 0); // nsIndex, type, span, chunkIndex
crcData.set(entryBytes.subarray(8, 32), 4); // key[16] + data[8]
return crc32(crcData);
}
// ── Page header CRC ────────────────────────────────────────────────
/** Page header CRC is over bytes [4..27] (seqNum, version, reserved) */
function computePageHeaderCrc(page: Uint8Array): number {
return crc32(page.subarray(4, 28));
}
// ── Raw parsed structures ──────────────────────────────────────────
interface RawEntry {
nsIndex: number;
type: number;
span: number;
chunkIndex: number;
crc: number;
key: string;
data: Uint8Array; // 8 bytes
// Additional data for multi-span entries
extraData: Uint8Array | null;
}
interface ParsedPage {
state: PageState;
seqNumber: number;
version: number;
entries: (RawEntry | null)[]; // null = EMPTY or ERASED
}
// ── Main parser ────────────────────────────────────────────────────
/**
* Parse an NVS binary partition into NvsPartition.
* @param data Raw binary data (must be multiple of 4096 bytes)
*/
export function parseBinary(data: Uint8Array): NvsPartition {
if (data.length % PAGE_SIZE !== 0) {
throw new Error(`二进制数据大小 (${data.length}) 不是页大小 (${PAGE_SIZE}) 的倍数`);
}
if (data.length === 0) {
throw new Error('二进制数据为空');
}
const pageCount = data.length / PAGE_SIZE;
const pages: ParsedPage[] = [];
// ── Phase 1: Parse all pages ──
for (let p = 0; p < pageCount; p++) {
const pageOff = p * PAGE_SIZE;
const pageData = data.subarray(pageOff, pageOff + PAGE_SIZE);
// Read page header
const state = readU32(pageData, 0) as PageState;
const seqNumber = readU32(pageData, 4);
const version = readU8(pageData, 8);
const storedCrc = readU32(pageData, 28);
// Skip EMPTY pages
if (state === PageState.EMPTY) continue;
// Verify page header CRC
const calcCrc = computePageHeaderCrc(pageData);
if (calcCrc !== storedCrc) {
// Corrupted page, skip
continue;
}
// Parse bitmap
const bitmap = pageData.subarray(BITMAP_OFFSET, BITMAP_OFFSET + BITMAP_SIZE);
// Parse entries
const rawEntries: (RawEntry | null)[] = [];
let entryIdx = 0;
while (entryIdx < ENTRIES_PER_PAGE) {
const entState = getEntryState(bitmap, entryIdx);
if (entState === EntryState.EMPTY) {
// All remaining entries are EMPTY
break;
}
if (entState === EntryState.ERASED) {
rawEntries.push(null);
entryIdx++;
continue;
}
// WRITTEN entry
const entOff = FIRST_ENTRY_OFFSET + entryIdx * ENTRY_SIZE;
const entryBytes = pageData.subarray(entOff, entOff + ENTRY_SIZE);
const nsIndex = readU8(entryBytes, 0);
const type = readU8(entryBytes, 1);
const span = readU8(entryBytes, 2);
const chunkIndex = readU8(entryBytes, 3);
const entryCrc = readU32(entryBytes, 4);
const key = readNullTermString(entryBytes, 8, KEY_FIELD_SIZE);
const entryData = new Uint8Array(entryBytes.subarray(24, 32));
// Reject nonsensical spans before CRC check
if (span < 1 || entryIdx + span > ENTRIES_PER_PAGE) {
entryIdx++; // skip this entry slot
continue;
}
// Verify entry CRC
const calcEntryCrc = computeEntryCrc(entryBytes);
if (calcEntryCrc !== entryCrc) {
// Corrupted entry, skip the span
entryIdx += span;
continue;
}
// Collect extra data for multi-span entries (SZ, BLOB, BLOB_DATA)
let extraData: Uint8Array | null = null;
if (span > 1) {
const extraLen = (span - 1) * ENTRY_SIZE;
const extraOff = FIRST_ENTRY_OFFSET + (entryIdx + 1) * ENTRY_SIZE;
if (extraOff + extraLen <= PAGE_SIZE) {
extraData = new Uint8Array(pageData.subarray(extraOff, extraOff + extraLen));
}
}
rawEntries.push({ nsIndex, type, span, chunkIndex, crc: entryCrc, key, data: entryData, extraData });
// Skip past the span (span >= 1 is guaranteed above)
entryIdx += span;
}
pages.push({ state, seqNumber, version, entries: rawEntries });
}
// Sort pages by sequence number (ascending) for proper deduplication
pages.sort((a, b) => a.seqNumber - b.seqNumber);
// Detect version from first valid page
const detectedVersion: NvsVersion = pages.length > 0 && pages[0].version === NvsVersion.V2
? NvsVersion.V2
: NvsVersion.V1;
// ── Phase 2: Build namespace map ──
const nsMap = new Map<number, string>(); // nsIndex → namespace name
const namespaces: string[] = [];
for (const page of pages) {
for (const entry of page.entries) {
if (!entry) continue;
// Namespace definitions have nsIndex=0 and type=U8
if (entry.nsIndex === 0 && entry.type === NvsType.U8) {
const assignedIdx = readU8(entry.data, 0);
nsMap.set(assignedIdx, entry.key);
if (!namespaces.includes(entry.key)) {
namespaces.push(entry.key);
}
}
}
}
// ── Phase 3: Resolve data entries (deduplication by last-write-wins) ──
// For V2 blobs, we need to collect BLOB_DATA and BLOB_IDX separately.
// Keys use \x00 as separator (NVS key/namespace names are C strings and cannot contain null bytes).
const blobDataChunks = new Map<string, Map<number, Uint8Array>>(); // "ns\x00key" → chunkIndex → data
const blobIdxEntries = new Map<string, { size: number; chunkCount: number; chunkStart: number }>();
const entryMap = new Map<string, NvsEntry>(); // "ns\x00key" → NvsEntry (last wins)
for (const page of pages) {
for (const entry of page.entries) {
if (!entry) continue;
if (entry.nsIndex === 0) continue; // Skip namespace definitions
const nsName = nsMap.get(entry.nsIndex);
if (!nsName) continue; // Unknown namespace, skip
const compositeKey = `${nsName}\x00${entry.key}`;
switch (entry.type) {
case NvsType.U8:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.U8, value: readU8(entry.data, 0),
});
break;
case NvsType.I8:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.I8, value: readI8(entry.data, 0),
});
break;
case NvsType.U16:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.U16, value: readU16(entry.data, 0),
});
break;
case NvsType.I16:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.I16, value: readI16(entry.data, 0),
});
break;
case NvsType.U32:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.U32, value: readU32(entry.data, 0),
});
break;
case NvsType.I32:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.I32, value: readI32(entry.data, 0),
});
break;
case NvsType.U64:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.U64, value: readU64(entry.data, 0),
});
break;
case NvsType.I64:
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.I64, value: readI64(entry.data, 0),
});
break;
case NvsType.SZ: {
// String: size at data[0..1], dataCrc at data[4..7]
const size = readU16(entry.data, 0);
if (entry.extraData && size > 0) {
const payload = entry.extraData.subarray(0, size);
const storedDataCrc = readU32(entry.data, 4);
if (crc32(payload) !== storedDataCrc) break; // corrupted payload, skip
// Decode string (remove null terminator)
const str = new TextDecoder('utf-8').decode(payload.subarray(0, size - 1));
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.SZ, value: str,
});
}
break;
}
case NvsType.BLOB: {
// Legacy V1 blob: size at data[0..1], dataCrc at data[4..7]
const size = readU16(entry.data, 0);
if (entry.extraData && size > 0) {
const payload = entry.extraData.subarray(0, size);
const storedDataCrc = readU32(entry.data, 4);
if (crc32(payload) !== storedDataCrc) break; // corrupted payload, skip
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.BLOB, value: new Uint8Array(payload),
});
} else {
entryMap.set(compositeKey, {
id: generateEntryId(), namespace: nsName, key: entry.key,
type: NvsType.BLOB, value: new Uint8Array(0),
});
}
break;
}
case NvsType.BLOB_DATA: {
// V2 blob data chunk
const size = readU16(entry.data, 0);
if (!blobDataChunks.has(compositeKey)) {
blobDataChunks.set(compositeKey, new Map());
}
if (entry.extraData && size > 0) {
const payload = entry.extraData.subarray(0, size);
const storedDataCrc = readU32(entry.data, 4);
if (crc32(payload) !== storedDataCrc) break; // corrupted chunk, skip
blobDataChunks.get(compositeKey)!.set(entry.chunkIndex, new Uint8Array(payload));
} else {
blobDataChunks.get(compositeKey)!.set(entry.chunkIndex, new Uint8Array(0));
}
break;
}
case NvsType.BLOB_IDX: {
// V2 blob index
const totalSize = readU32(entry.data, 0);
const chunkCount = readU8(entry.data, 4);
const chunkStart = readU8(entry.data, 5);
blobIdxEntries.set(compositeKey, { size: totalSize, chunkCount, chunkStart });
break;
}
}
}
}
// ── Phase 4: Reassemble V2 blobs ──
for (const [compositeKey, idxInfo] of blobIdxEntries) {
const chunks = blobDataChunks.get(compositeKey);
if (!chunks) continue;
const assembled = new Uint8Array(idxInfo.size);
let offset = 0;
let chunksValid = true;
for (let i = idxInfo.chunkStart; i < idxInfo.chunkStart + idxInfo.chunkCount; i++) {
const chunk = chunks.get(i);
if (!chunk) {
chunksValid = false; // missing chunk — cannot reassemble correctly
break;
}
assembled.set(chunk, offset);
offset += chunk.length;
}
if (!chunksValid) continue; // skip blob with missing chunks rather than return corrupted data
if (offset !== idxInfo.size) continue; // chunk sizes don't match declared total — zero tail would result
const sepIdx = compositeKey.indexOf('\x00');
const nsName = compositeKey.substring(0, sepIdx);
const key = compositeKey.substring(sepIdx + 1);
entryMap.set(compositeKey, {
id: generateEntryId(),
namespace: nsName,
key,
type: NvsType.BLOB_DATA,
value: assembled,
});
}
return {
entries: Array.from(entryMap.values()),
namespaces,
version: detectedVersion,
};
}