spiegel-keyman/common/tools/hextobin/index.ts
Marc Durdin ac6eb0b767 chore: use explicit node: prefix for node imports
This makes it easier for us to enumerate modules that have node
dependencies. There may be others I have not yet picked up.
2025-04-03 08:01:08 +07:00

233 lines
7.3 KiB
TypeScript

import * as fs from 'node:fs';
import * as rd from 'node:readline';
export default async function hextobin(inputFilename: string, outputFilename?: string, options?: {silent?: boolean}): Promise<Uint8Array> {
options = {...options};
options.silent = !!options.silent;
let reader = rd.createInterface(fs.createReadStream(inputFilename));
type HexBlockRefType =
'sizeof' | // gets the size of a block, optionally divided by divisor: sizeof(block,divisor)
'offset' | // gets the offset of a block in bytes from start of file: offset(block)
'diff' | // gets the offset of a block in bytes from start of a base block: offset(baseBlock,block)
'index'; // gets the index of a block from a base block, optionally divided by divisor: index(baseBlock,block,divisor)
interface HexBlockRef {
type: HexBlockRefType;
blockName: string;
blockName2?: string; // used only by diff, index
divisor?: number; // used only by sizeof, index
offset: number; // actual byte offset in hex data (i.e. offset in string is * 2)
};
interface HexBlock {
name: string;
offset: number; // calculated during reconciliation phase
hex: string;
refs: HexBlockRef[];
};
let blocks: HexBlock[] = [];
let currentLine = '';
let currentLineNumber = 0;
if(!await load()) {
return null;
}
if(!reconciliation()) {
return null;
}
return save();
function reportError(message: string) {
if(!options.silent) {
console.error(`Invalid input file: ${message} on line #${currentLineNumber}: "${currentLine}"`);
}
}
function currentBlock(): HexBlock {
return blocks.length ? blocks[blocks.length-1] : null;
}
function parseToken(token: string): { command: string, parameters: string[] } {
let m = /^([a-z]+)\((.+)\)$/.exec(token);
if(!m) {
if(!token.match(/^[a-fA-F0-9]{2}$/)) {
return null;
}
// hex byte
return { command: 'data', parameters: [token] };
}
// processing command
return { command: m[1], parameters: m[2].split(',') };
}
function token(token: string) {
const t = parseToken(token);
if(!t) {
reportError(`expected command or hex at token "${token}"`);
return false;
}
if(t.command == 'block') {
blocks.push({name: t.parameters[0], offset: 0, hex: '', refs: []});
return true;
}
const b = currentBlock();
if(!b) {
reportError(`expected block() before data`);
return false;
}
switch(t.command) {
case 'offset':
b.refs.push({type: 'offset', blockName: t.parameters[0], offset: b.hex.length / 2});
b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase
break;
case 'sizeof':
// sizeof can take a second parameter, divisor
{
let divisor = t.parameters.length > 1 ? parseInt(t.parameters[1],10) : 1;
b.refs.push({type: 'sizeof', blockName: t.parameters[0], divisor: divisor, offset: b.hex.length / 2});
b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase
}
break;
case 'diff':
// diff can also take a divisor, defaults to 1
{
const divisor = t.parameters.length > 2 ? parseInt(t.parameters[2],10) : 1;
b.refs.push({type: 'diff', blockName: t.parameters[0], blockName2: t.parameters[1], divisor: divisor, offset: b.hex.length / 2});
b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase
}
break;
case 'index':
// index can take a third parameter, divisor
{
let divisor = t.parameters.length > 2 ? parseInt(t.parameters[2],10) : 1;
b.refs.push({type: 'index', blockName: t.parameters[0], blockName2: t.parameters[1], divisor: divisor, offset: b.hex.length / 2});
b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase
}
break;
case 'data':
b.hex += t.parameters[0];
break;
default:
reportError(`unknown command ${t.command}`);
return false;
}
return true;
}
async function load() {
for await (const l of reader) {
// Error reporting variables
currentLine = l;
currentLineNumber++;
const tokens = l.split(/[ \t]+/);
for(let t of tokens) {
if(t.startsWith('#')) {
// comment, ignore all subsequent tokens to EOL
break;
}
if(t == '') {
continue;
}
if(!token(t)) {
return false;
}
}
}
return true;
}
function dwordLeToHex(v: number): string {
// hacky but who cares
let h = v.toString(16);
h = "0".repeat(8-h.length) + h;
return h.substring(6,8) + h.substring(4, 6) + h.substring(2, 4) + h.substring(0, 2);
}
function fillBlockPlaceholder(block: HexBlock, offset: number, value: number): void {
const hexvalue = dwordLeToHex(value);
block.hex = block.hex.substring(0, offset * 2) + hexvalue + block.hex.substring(offset * 2 + 8);
}
function reconciliation(): boolean {
let offset = 0;
// calculate block sizes
for(let b of blocks) {
b.offset = offset;
offset += b.hex.length / 2;
}
// reconcile block offsets and sizes
for(let b of blocks) {
for(let r of b.refs) {
const v = blocks.find(q => q.name == r.blockName);
if(!v) {
reportError(`Could not find block ${r.blockName} when reconciling ${b.name}`);
return false;
}
switch(r.type) {
case 'diff':
{
const v2 = blocks.find(q => q.name == r.blockName2);
if(!v2) {
reportError(`Could not find block ${r.blockName2} when reconciling ${b.name}`);
return false;
}
fillBlockPlaceholder(b, r.offset, (v2.offset - v.offset) / r.divisor);
}
break;
case 'offset':
fillBlockPlaceholder(b, r.offset, v.offset);
break;
case 'sizeof':
fillBlockPlaceholder(b, r.offset, v.hex.length / 2 / r.divisor);
break;
case 'index':
{
const v2 = blocks.find(q => q.name == r.blockName2);
if(!v2) {
reportError(`Could not find block ${r.blockName2} when reconciling ${b.name}`);
return false;
}
fillBlockPlaceholder(b, r.offset, (blocks.indexOf(v2) - blocks.indexOf(v)) / r.divisor);
}
break;
default:
reportError(`Invalid ref ${r.type}`);
return false;
}
}
}
return true;
}
function save(): Uint8Array {
let total = blocks.reduce((total: number, item: HexBlock) => Math.max(item.offset + item.hex.length/2, total), 0);
if(!options.silent) {
console.log(`${currentLineNumber} lines read; ${blocks.length} sections to write. Total file size = ${total} bytes.` );
}
let buffer = new Uint8Array(total);
blocks.forEach(item => {
let buf = Buffer.from(item.hex, 'hex');
buffer.set(buf, item.offset);
});
if(outputFilename) {
fs.writeFileSync(outputFilename, buffer);
}
return buffer;
}
}