yunsi-toolbox-vue/lib/partition-table/csv-parser.ts

146 lines
4.6 KiB
TypeScript

import type { PartitionEntry, PartitionTable } from './types';
import { PartitionFlags, PartitionType, NAME_TO_TYPE, subtypeFromName } from './types';
const U32_MAX = 0xFFFF_FFFF;
/** Parse partition flags: handles numeric values, single names, and combined forms like "encrypted readonly" */
function parseFlags(str: string): number {
if (!str) return 0;
const normalized = str.trim().toLowerCase();
// Numeric values (hex or decimal) take priority
if (/^0x[\da-f]+$/i.test(normalized) || /^\d+$/.test(normalized)) {
return parseSize(str);
}
// Split on whitespace, pipe, or comma to support combined flags
let result = 0;
for (const part of normalized.split(/[\s|,]+/).filter(Boolean)) {
if (part === 'encrypted') result |= PartitionFlags.ENCRYPTED;
else if (part === 'readonly') result |= PartitionFlags.READONLY;
else throw new Error(`Unknown flag: "${part}"`);
}
return result;
}
/** Parse a size string like "0x6000", "1M", "32K", "4096" */
function parseSize(str: string): number {
str = str.trim();
if (!str) return 0;
if (str.startsWith('0x') || str.startsWith('0X')) {
if (!/^0x[0-9a-f]+$/i.test(str)) throw new Error(`Invalid size/offset value: "${str}"`);
const v = parseInt(str, 16);
if (isNaN(v) || v < 0 || v > U32_MAX) throw new Error(`Invalid size/offset value: "${str}"`);
return v;
}
const match = str.match(/^(\d+(?:\.\d+)?)\s*([KkMm])?$/);
if (match) {
const num = parseFloat(match[1]);
const unit = (match[2] || '').toUpperCase();
let result: number;
if (unit === 'K') result = Math.floor(num * 1024);
else if (unit === 'M') result = Math.floor(num * 1024 * 1024);
else result = Math.floor(num);
if (result > U32_MAX) throw new Error(`Invalid size/offset value: "${str}" (exceeds 32-bit range)`);
return result;
}
throw new Error(`Invalid size/offset value: "${str}"`);
}
/**
* Split one CSV line into trimmed fields, respecting RFC-4180 quoting and
* treating an unquoted '#' as the start of an inline comment (discards rest).
*/
function splitCsvLine(line: string): string[] {
const fields: string[] = [];
let current = '';
let inQuotes = false;
let i = 0;
while (i < line.length) {
const ch = line[i];
if (inQuotes) {
if (ch === '"' && line[i + 1] === '"') { // escaped quote ""
current += '"';
i += 2;
} else if (ch === '"') {
inQuotes = false;
i++;
} else {
current += ch;
i++;
}
} else {
if (ch === '"') {
inQuotes = true;
i++;
} else if (ch === ',') {
fields.push(current.trim());
current = '';
i++;
} else if (ch === '#') {
break; // inline comment — discard rest of line
} else {
current += ch;
i++;
}
}
}
if (inQuotes) throw new Error('Unclosed quote');
fields.push(current.trim());
return fields;
}
/**
* Parse ESP-IDF partition table CSV format.
*
* Format:
* # Name, Type, SubType, Offset, Size, Flags
* nvs, data, nvs, 0x9000, 0x6000,
* phy_init, data, phy, 0xf000, 0x1000,
* factory, app, factory, 0x10000, 1M,
*/
export function parseCsv(text: string, onWarning?: (line: number, message: string) => void): PartitionTable {
const lines = text.split(/\r?\n/);
const entries: PartitionEntry[] = [];
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx].trim();
if (!line || line.startsWith('#')) continue;
try {
const fields = splitCsvLine(line);
if (fields.length < 5) {
onWarning?.(lineIdx + 1, `Not enough fields (need 5, got ${fields.length}): "${line}"`);
continue;
}
// Skip header line
if (fields[0].toLowerCase() === 'name' && fields[1].toLowerCase() === 'type') continue;
const name = fields[0];
const typeName = fields[1].toLowerCase();
const subtypeName = fields[2];
const offsetStr = fields[3];
const sizeStr = fields[4];
const flagsStr = fields[5] || '';
const type = NAME_TO_TYPE[typeName];
if (type === undefined) {
onWarning?.(lineIdx + 1, `Unknown partition type: "${typeName}"`);
continue;
}
const subtype = subtypeFromName(type, subtypeName);
const offset = parseSize(offsetStr);
const size = parseSize(sizeStr);
const flags = parseFlags(flagsStr);
entries.push({ name, type, subtype, offset, size, flags });
} catch (e) {
onWarning?.(lineIdx + 1, `Parse failed: ${(e as Error).message}`);
}
}
return { entries, md5Valid: false };
}