104 lines
3.9 KiB
JavaScript
104 lines
3.9 KiB
JavaScript
// Hex dump with an inspector: hover or click a byte to decode it.
|
|
|
|
import { h, pad } from './dom.js';
|
|
|
|
const hex2 = (b) => pad(b.toString(16).toUpperCase(), 2);
|
|
const printable = (b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : '.');
|
|
|
|
/** Decode the bytes at offset i as little-endian integers. */
|
|
export function inspect(bytes, i) {
|
|
const b = (k) => (i + k < bytes.length ? bytes[i + k] : null);
|
|
const have = (n) => i + n <= bytes.length;
|
|
const out = { offset: i, u8: bytes[i] };
|
|
if (have(2)) {
|
|
const v = b(0) | (b(1) << 8);
|
|
out.u16 = v;
|
|
out.i16 = (v << 16) >> 16;
|
|
}
|
|
if (have(4)) {
|
|
const v = (b(0) | (b(1) << 8) | (b(2) << 16) | (b(3) << 24));
|
|
out.i32 = v;
|
|
out.u32 = v >>> 0;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function describe(bytes, i) {
|
|
const d = inspect(bytes, i);
|
|
const parts = [`OFFSET 0x${pad(d.offset.toString(16).toUpperCase(), 4)} (${d.offset})`, `U8 ${d.u8}`];
|
|
if (d.i16 !== undefined) parts.push(`I16 ${d.i16}`);
|
|
if (d.i32 !== undefined) parts.push(`I32 ${d.i32}`, `U32 ${d.u32}`);
|
|
return parts.join(' | ');
|
|
}
|
|
|
|
/**
|
|
* Render a hex dump. Returns { el, inspector } where inspector is the element
|
|
* that shows the decoded value of the hovered/selected byte.
|
|
*/
|
|
export function hexView(bytes, { rows: maxRows = Infinity } = {}) {
|
|
const inspector = h('div', { class: 'hx-inspector', 'aria-live': 'polite' }, 'HOVER OR CLICK A BYTE TO DECODE IT');
|
|
const body = h('div', { class: 'hx-body', tabindex: '0', role: 'group', 'aria-label': 'Hex dump' });
|
|
let pinned = null;
|
|
const cells = new Map();
|
|
|
|
const rowCount = Math.min(Math.ceil(bytes.length / 16), maxRows);
|
|
for (let r = 0; r < rowCount; r++) {
|
|
const start = r * 16;
|
|
const hexCells = [];
|
|
const ascCells = [];
|
|
for (let k = 0; k < 16; k++) {
|
|
const i = start + k;
|
|
if (i >= bytes.length) {
|
|
hexCells.push(h('span', { class: 'hx-b pad' }, ' '));
|
|
continue;
|
|
}
|
|
const v = bytes[i];
|
|
const cls = v === 0 ? 'hx-b z' : 'hx-b';
|
|
const a = h('span', { class: `${cls} a`, 'data-i': i }, printable(v));
|
|
const x = h('span', { class: cls, 'data-i': i }, hex2(v));
|
|
cells.set(i, [x, a]);
|
|
hexCells.push(x);
|
|
ascCells.push(a);
|
|
if (k === 7) hexCells.push(h('span', { class: 'hx-gap' }, ' '));
|
|
}
|
|
body.append(
|
|
h('div', { class: 'hx-row' },
|
|
h('span', { class: 'hx-off' }, pad(start.toString(16).toUpperCase(), 4)),
|
|
h('span', { class: 'hx-hex' }, hexCells),
|
|
h('span', { class: 'hx-asc' }, ascCells),
|
|
),
|
|
);
|
|
}
|
|
|
|
const select = (i, pin) => {
|
|
if (pin) {
|
|
if (pinned !== null) cells.get(pinned)?.forEach((c) => c.classList.remove('sel'));
|
|
pinned = pinned === i ? null : i;
|
|
if (pinned !== null) cells.get(pinned).forEach((c) => c.classList.add('sel'));
|
|
}
|
|
const shown = pinned ?? i;
|
|
inspector.textContent = shown === null ? 'HOVER OR CLICK A BYTE TO DECODE IT' : describe(bytes, shown);
|
|
};
|
|
const target = (e) => {
|
|
const t = e.target.closest('[data-i]');
|
|
return t ? Number(t.dataset.i) : null;
|
|
};
|
|
body.addEventListener('mouseover', (e) => { const i = target(e); if (i !== null && pinned === null) select(i, false); });
|
|
body.addEventListener('mouseleave', () => { if (pinned === null) select(null, false); });
|
|
body.addEventListener('click', (e) => { const i = target(e); if (i !== null) select(i, true); });
|
|
|
|
return { el: h('div', { class: 'hex' }, body, inspector), inspector };
|
|
}
|
|
|
|
/** Parse hex text ("de ad 0xBE, ef") into bytes; throws on bad input. */
|
|
export function parseHex(text) {
|
|
const cleaned = text.replace(/0x/gi, ' ').replace(/[,\s]+/g, ' ').trim();
|
|
if (!cleaned) return new Uint8Array(0);
|
|
const out = [];
|
|
for (const tok of cleaned.split(' ')) {
|
|
if (!/^[0-9a-fA-F]+$/.test(tok) || tok.length % 2) throw new Error(`"${tok}" is not whole bytes of hex`);
|
|
for (let i = 0; i < tok.length; i += 2) out.push(parseInt(tok.slice(i, i + 2), 16));
|
|
}
|
|
return Uint8Array.from(out);
|
|
}
|