Files
wifi-densepose/vendor/ruvector/examples/pwa-loader/app.js

1211 lines
36 KiB
JavaScript

/**
* RVF Seed Decoder - Main Application
*
* Loads the rvf-wasm module for segment-level operations on .rvf files.
* Parses RVQS cognitive seed headers in pure JS (matching the 64-byte
* binary layout from rvf-types/src/qr_seed.rs).
*/
'use strict';
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const WASM_PATH = window.RVF_WASM_PATH || './rvf_wasm_bg.wasm';
// RVQS seed constants (from rvf-types/src/qr_seed.rs)
const SEED_MAGIC = 0x52565153; // "RVQS"
const SEED_HEADER_SIZE = 64;
const FLAG_HAS_MICROKERNEL = 0x0001;
const FLAG_HAS_DOWNLOAD = 0x0002;
const FLAG_SIGNED = 0x0004;
const FLAG_OFFLINE_CAPABLE = 0x0008;
const FLAG_ENCRYPTED = 0x0010;
const FLAG_COMPRESSED = 0x0020;
const FLAG_HAS_VECTORS = 0x0040;
const FLAG_STREAM_UPGRADE = 0x0080;
// Download manifest TLV tags
const DL_TAG_HOST_PRIMARY = 0x0001;
const DL_TAG_HOST_FALLBACK = 0x0002;
const DL_TAG_CONTENT_HASH = 0x0003;
const DL_TAG_TOTAL_SIZE = 0x0004;
const DL_TAG_LAYER_MANIFEST = 0x0005;
const DL_TAG_SESSION_TOKEN = 0x0006;
const DL_TAG_TTL = 0x0007;
const DL_TAG_CERT_PIN = 0x0008;
// Layer ID names
const LAYER_NAMES = {
0: 'Level 0 Manifest',
1: 'Hot Cache',
2: 'HNSW Layer A',
3: 'Quant Dict',
4: 'HNSW Layer B',
5: 'Full Vectors',
6: 'HNSW Layer C',
};
// RVF segment magic (from rvf-types constants)
const SEGMENT_MAGIC = 0x52564653; // "RVFS"
// Data type names
const DTYPE_NAMES = {
0: 'F32',
1: 'F16',
2: 'BF16',
3: 'I8',
4: 'Binary',
};
// Signature algorithm names
const SIG_ALGO_NAMES = {
0: 'Ed25519',
1: 'ML-DSA-65',
2: 'HMAC-SHA256',
};
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let wasmInstance = null;
let wasmMemory = null;
let wasmReady = false;
let scannerStream = null;
let scannerAnimFrame = null;
// ---------------------------------------------------------------------------
// WASM Loader
// ---------------------------------------------------------------------------
async function loadWasm() {
try {
setStatus('Loading WASM module...');
const response = await fetch(WASM_PATH);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const bytes = await response.arrayBuffer();
const importObject = { env: {} };
const result = await WebAssembly.instantiate(bytes, importObject);
wasmInstance = result.instance;
wasmMemory = wasmInstance.exports.memory;
wasmReady = true;
setStatus('WASM loaded -- ready', 'success');
return true;
} catch (err) {
wasmReady = false;
setStatus(`WASM unavailable: ${err.message}. Falling back to JS-only parsing.`, 'error');
return false;
}
}
// ---------------------------------------------------------------------------
// WASM Helpers
// ---------------------------------------------------------------------------
/** Allocate bytes in WASM memory and copy data in. Returns ptr. */
function wasmWrite(data) {
const ptr = wasmInstance.exports.rvf_alloc(data.length);
if (ptr === 0) throw new Error('WASM allocation failed');
const mem = new Uint8Array(wasmMemory.buffer, ptr, data.length);
mem.set(data);
return ptr;
}
/** Free WASM memory. */
function wasmFree(ptr, size) {
wasmInstance.exports.rvf_free(ptr, size);
}
/** Read bytes from WASM memory. */
function wasmRead(ptr, len) {
return new Uint8Array(wasmMemory.buffer, ptr, len).slice();
}
// ---------------------------------------------------------------------------
// Binary Read Helpers
// ---------------------------------------------------------------------------
function readU16LE(buf, off) {
return buf[off] | (buf[off + 1] << 8);
}
function readU32LE(buf, off) {
return (buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0;
}
function readU64LE(buf, off) {
const lo = readU32LE(buf, off);
const hi = readU32LE(buf, off + 4);
return lo + hi * 0x100000000;
}
function toHex(bytes, maxLen) {
const arr = maxLen ? bytes.slice(0, maxLen) : bytes;
let hex = '';
for (let i = 0; i < arr.length; i++) {
hex += arr[i].toString(16).padStart(2, '0');
}
if (maxLen && bytes.length > maxLen) {
hex += '...';
}
return hex;
}
function formatBytes(n) {
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(2) + ' MB';
return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
function formatTimestamp(ns) {
if (ns === 0) return '(not set)';
try {
const ms = ns / 1e6;
return new Date(ms).toISOString();
} catch {
return '(invalid)';
}
}
// ---------------------------------------------------------------------------
// Seed Header Parser (Pure JS - matches rvf-types/src/qr_seed.rs layout)
// ---------------------------------------------------------------------------
/**
* Parse RVQS seed header from raw bytes.
* @param {Uint8Array} data - Full seed payload (>= 64 bytes)
* @returns {object} Parsed seed header and manifest
*/
function parseSeedHeader(data) {
if (data.length < SEED_HEADER_SIZE) {
throw new Error(`Seed too small: ${data.length} bytes (need >= ${SEED_HEADER_SIZE})`);
}
const magic = readU32LE(data, 0x00);
if (magic !== SEED_MAGIC) {
throw new Error(
`Bad magic: 0x${magic.toString(16).padStart(8, '0')} (expected 0x${SEED_MAGIC.toString(16).padStart(8, '0')} "RVQS")`
);
}
const header = {
magic,
version: readU16LE(data, 0x04),
flags: readU16LE(data, 0x06),
fileId: data.slice(0x08, 0x10),
totalVectorCount: readU32LE(data, 0x10),
dimension: readU16LE(data, 0x14),
baseDtype: data[0x16],
profileId: data[0x17],
createdNs: readU64LE(data, 0x18),
microkernelOffset: readU32LE(data, 0x20),
microkernelSize: readU32LE(data, 0x24),
downloadManifestOffset: readU32LE(data, 0x28),
downloadManifestSize: readU32LE(data, 0x2c),
sigAlgo: readU16LE(data, 0x30),
sigLength: readU16LE(data, 0x32),
totalSeedSize: readU32LE(data, 0x34),
contentHash: data.slice(0x38, 0x40),
};
// Decode flags
header.flagNames = [];
const flagDefs = [
[FLAG_HAS_MICROKERNEL, 'MICROKERNEL'],
[FLAG_HAS_DOWNLOAD, 'DOWNLOAD'],
[FLAG_SIGNED, 'SIGNED'],
[FLAG_OFFLINE_CAPABLE, 'OFFLINE'],
[FLAG_ENCRYPTED, 'ENCRYPTED'],
[FLAG_COMPRESSED, 'COMPRESSED'],
[FLAG_HAS_VECTORS, 'VECTORS'],
[FLAG_STREAM_UPGRADE, 'STREAM_UPGRADE'],
];
for (const [bit, name] of flagDefs) {
if (header.flags & bit) header.flagNames.push(name);
}
return header;
}
/**
* Parse download manifest TLV from seed payload.
* @param {Uint8Array} data - Full seed payload
* @param {object} header - Parsed header from parseSeedHeader
* @returns {object} Manifest with hosts, layers, etc.
*/
function parseManifest(data, header) {
const manifest = {
hosts: [],
contentHash: null,
totalFileSize: null,
layers: [],
sessionToken: null,
tokenTtl: null,
certPin: null,
};
if (!(header.flags & FLAG_HAS_DOWNLOAD) || header.downloadManifestSize === 0) {
return manifest;
}
const start = header.downloadManifestOffset;
const end = start + header.downloadManifestSize;
if (end > data.length) return manifest;
let pos = start;
while (pos + 4 <= end) {
const tag = readU16LE(data, pos);
const length = readU16LE(data, pos + 2);
pos += 4;
if (pos + length > end) break;
const value = data.slice(pos, pos + length);
switch (tag) {
case DL_TAG_HOST_PRIMARY:
case DL_TAG_HOST_FALLBACK: {
if (length >= 150) {
const urlLength = readU16LE(value, 0);
const urlBytes = value.slice(2, 2 + Math.min(urlLength, 128));
let url = '';
try { url = new TextDecoder().decode(urlBytes); } catch { /* ignore */ }
const priority = readU16LE(value, 130);
const region = readU16LE(value, 132);
const hostKeyHash = value.slice(134, 150);
manifest.hosts.push({
url,
priority,
region,
hostKeyHash,
isPrimary: tag === DL_TAG_HOST_PRIMARY,
});
}
break;
}
case DL_TAG_CONTENT_HASH: {
if (length >= 32) {
manifest.contentHash = value.slice(0, 32);
}
break;
}
case DL_TAG_TOTAL_SIZE: {
if (length >= 8) {
manifest.totalFileSize = readU64LE(value, 0);
}
break;
}
case DL_TAG_LAYER_MANIFEST: {
if (length > 0) {
const layerCount = value[0];
let lpos = 1;
for (let i = 0; i < layerCount; i++) {
if (lpos + 27 > value.length) break;
const layerId = value[lpos];
const priority = value[lpos + 1];
const offset = readU32LE(value, lpos + 2);
const size = readU32LE(value, lpos + 6);
const contentHash = value.slice(lpos + 10, lpos + 26);
const required = value[lpos + 26];
manifest.layers.push({
layerId,
name: LAYER_NAMES[layerId] || `Layer ${layerId}`,
priority,
offset,
size,
contentHash,
required: required === 1,
});
lpos += 27;
}
}
break;
}
case DL_TAG_SESSION_TOKEN: {
if (length >= 16) {
manifest.sessionToken = value.slice(0, 16);
}
break;
}
case DL_TAG_TTL: {
if (length >= 4) {
manifest.tokenTtl = readU32LE(value, 0);
}
break;
}
case DL_TAG_CERT_PIN: {
if (length >= 32) {
manifest.certPin = value.slice(0, 32);
}
break;
}
default:
// Unknown tags are forward-compatible, skip.
break;
}
pos += length;
}
return manifest;
}
/**
* Extract signature bytes from seed payload.
* @param {Uint8Array} data - Full seed payload
* @param {object} header - Parsed header
* @returns {Uint8Array|null} Signature bytes
*/
function extractSignature(data, header) {
if (!(header.flags & FLAG_SIGNED) || header.sigLength === 0) return null;
const sigStart = header.totalSeedSize - header.sigLength;
const sigEnd = header.totalSeedSize;
if (sigEnd > data.length) return null;
return data.slice(sigStart, sigEnd);
}
/**
* Extract microkernel bytes from seed payload.
* @param {Uint8Array} data - Full seed payload
* @param {object} header - Parsed header
* @returns {Uint8Array|null} Microkernel bytes (compressed)
*/
function extractMicrokernel(data, header) {
if (!(header.flags & FLAG_HAS_MICROKERNEL) || header.microkernelSize === 0) return null;
const start = header.microkernelOffset;
const end = start + header.microkernelSize;
if (end > data.length) return null;
return data.slice(start, end);
}
// ---------------------------------------------------------------------------
// RVF Segment Parser (uses WASM when available, pure JS fallback)
// ---------------------------------------------------------------------------
/**
* Parse .rvf file segments.
* @param {Uint8Array} data - Raw .rvf file bytes
* @returns {object} Parsed segment info
*/
function parseRvfSegments(data) {
const result = {
segmentCount: 0,
segments: [],
storeHandle: -1,
headerValid: false,
};
// Try WASM path first
if (wasmReady) {
try {
return parseRvfSegmentsWasm(data);
} catch (err) {
// Fall through to JS fallback
}
}
// JS fallback: scan for segment headers
return parseRvfSegmentsJS(data);
}
function parseRvfSegmentsWasm(data) {
const bufPtr = wasmWrite(data);
try {
// Header verification
const headerResult = wasmInstance.exports.rvf_verify_header(bufPtr);
// Segment count
const segCount = wasmInstance.exports.rvf_segment_count(bufPtr, data.length);
// Segment info (28 bytes per segment)
const segments = [];
const infoSize = 28;
const infoPtr = wasmInstance.exports.rvf_alloc(infoSize);
try {
for (let i = 0; i < segCount; i++) {
const rc = wasmInstance.exports.rvf_segment_info(bufPtr, data.length, i, infoPtr);
if (rc === 0) {
const info = wasmRead(infoPtr, infoSize);
const segId = readU64LE(info, 0);
const segType = info[8];
const payloadLength = readU64LE(info, 12);
const offset = readU64LE(info, 20);
segments.push({ segId, segType, payloadLength, offset });
}
}
} finally {
wasmFree(infoPtr, infoSize);
}
// CRC32C verification
let checksumValid = null;
if (data.length >= 4) {
checksumValid = wasmInstance.exports.rvf_verify_checksum(bufPtr, data.length) === 1;
}
// Try to open as a store
let storeHandle = -1;
try {
storeHandle = wasmInstance.exports.rvf_store_open(bufPtr, data.length);
} catch { /* ignore */ }
return {
segmentCount: segCount,
segments,
headerValid: headerResult === 0,
checksumValid,
storeHandle,
};
} finally {
wasmFree(bufPtr, data.length);
}
}
function parseRvfSegmentsJS(data) {
const MAGIC_BYTES = [
SEGMENT_MAGIC & 0xff,
(SEGMENT_MAGIC >> 8) & 0xff,
(SEGMENT_MAGIC >> 16) & 0xff,
(SEGMENT_MAGIC >> 24) & 0xff,
];
const HEADER_SIZE = 64; // rvf-types SEGMENT_HEADER_SIZE
const segments = [];
if (data.length < HEADER_SIZE) {
return { segmentCount: 0, segments, headerValid: false, storeHandle: -1 };
}
let i = 0;
const last = data.length - HEADER_SIZE;
while (i <= last) {
if (
data[i] === MAGIC_BYTES[0] &&
data[i + 1] === MAGIC_BYTES[1] &&
data[i + 2] === MAGIC_BYTES[2] &&
data[i + 3] === MAGIC_BYTES[3]
) {
const version = data[i + 4];
if (version === 1) {
const segType = data[i + 5];
const segId = readU64LE(data, i + 8);
const payloadLength = readU64LE(data, i + 16);
segments.push({
segId,
segType,
payloadLength,
offset: i,
});
const total = HEADER_SIZE + payloadLength;
const next = i + total;
if (next > i && next <= data.length) {
i = next;
continue;
}
}
}
i++;
}
// Check first 4 bytes for magic
const headerValid =
data.length >= 4 &&
readU32LE(data, 0) === SEGMENT_MAGIC;
return {
segmentCount: segments.length,
segments,
headerValid,
storeHandle: -1,
};
}
// Segment type names
const SEG_TYPE_NAMES = {
0x01: 'Vec',
0x02: 'HNSW',
0x03: 'IVF',
0x04: 'PQ',
0x05: 'Manifest',
0x06: 'Metadata',
0x10: 'WASM',
};
// ---------------------------------------------------------------------------
// Decode Entry Points
// ---------------------------------------------------------------------------
/**
* Decode a seed (RVQS) or RVF file from raw bytes.
* Detects type by magic number.
*/
async function decodeSeed(bytes) {
const data = new Uint8Array(bytes);
if (data.length < 4) {
throw new Error('File too small to contain valid data');
}
const magic = readU32LE(data, 0);
if (magic === SEED_MAGIC) {
// RVQS cognitive seed
const header = parseSeedHeader(data);
const manifest = parseManifest(data, header);
const signature = extractSignature(data, header);
const microkernel = extractMicrokernel(data, header);
return {
type: 'seed',
header,
manifest,
signature,
microkernel,
raw: data,
};
}
if (magic === SEGMENT_MAGIC) {
// Raw .rvf file
const segInfo = parseRvfSegments(data);
return {
type: 'rvf',
segInfo,
raw: data,
};
}
// Try as witness bundle (check for witness-specific patterns)
return decodeWitness(data);
}
/**
* Decode a witness bundle header.
* Witness bundles are envelope structures containing evidence chains.
* We parse the outer framing to show whatever structure is present.
*/
function decodeWitness(data) {
if (data.length < 4) {
throw new Error('File too small for witness bundle');
}
const magic = readU32LE(data, 0);
// If this is actually a seed or RVF segment, redirect
if (magic === SEED_MAGIC || magic === SEGMENT_MAGIC) {
throw new Error('Not a witness bundle (detected seed or segment magic)');
}
// Generic binary inspection for unknown formats
return {
type: 'witness',
size: data.length,
magic: '0x' + magic.toString(16).padStart(8, '0'),
raw: data,
preview: data.slice(0, Math.min(256, data.length)),
};
}
// ---------------------------------------------------------------------------
// UI Rendering
// ---------------------------------------------------------------------------
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status-bar' + (cls ? ' ' + cls : '');
}
function renderResults(result) {
const container = document.getElementById('results');
container.innerHTML = '';
if (result.type === 'seed') {
renderSeedResult(container, result);
} else if (result.type === 'rvf') {
renderRvfResult(container, result);
} else if (result.type === 'witness') {
renderWitnessResult(container, result);
}
}
function renderSeedResult(container, result) {
const { header, manifest, signature, microkernel, raw } = result;
// -- Header Card --
const headerCard = createCard('Seed Header');
const grid = document.createElement('dl');
grid.className = 'info-grid';
addInfoRow(grid, 'Magic', `0x${header.magic.toString(16).padStart(8, '0')} (RVQS)`);
addInfoRow(grid, 'Version', header.version.toString());
addInfoRow(grid, 'File ID', toHex(header.fileId));
addInfoRow(grid, 'Total Vectors', header.totalVectorCount.toLocaleString());
addInfoRow(grid, 'Dimension', header.dimension.toString());
addInfoRow(grid, 'Data Type', DTYPE_NAMES[header.baseDtype] || `0x${header.baseDtype.toString(16)}`);
addInfoRow(grid, 'Profile ID', header.profileId.toString());
addInfoRow(grid, 'Created', formatTimestamp(header.createdNs));
addInfoRow(grid, 'Seed Size', formatBytes(header.totalSeedSize));
addInfoRow(grid, 'Content Hash', toHex(header.contentHash));
if (header.flags & FLAG_HAS_MICROKERNEL) {
addInfoRow(grid, 'Microkernel', `${formatBytes(header.microkernelSize)} @ offset ${header.microkernelOffset}`);
}
if (header.flags & FLAG_SIGNED) {
addInfoRow(grid, 'Signature', `${SIG_ALGO_NAMES[header.sigAlgo] || `algo ${header.sigAlgo}`}, ${header.sigLength} bytes`);
}
headerCard.appendChild(grid);
// Flags badges
const flagsWrap = document.createElement('div');
flagsWrap.style.marginTop = '0.75rem';
const flagsLabel = document.createElement('dt');
flagsLabel.style.color = 'var(--text-muted)';
flagsLabel.style.fontSize = '0.85rem';
flagsLabel.style.marginBottom = '0.35rem';
flagsLabel.textContent = 'Flags';
flagsWrap.appendChild(flagsLabel);
const flagsList = document.createElement('ul');
flagsList.className = 'flags-list';
const allFlags = [
[FLAG_HAS_MICROKERNEL, 'MICROKERNEL'],
[FLAG_HAS_DOWNLOAD, 'DOWNLOAD'],
[FLAG_SIGNED, 'SIGNED'],
[FLAG_OFFLINE_CAPABLE, 'OFFLINE'],
[FLAG_ENCRYPTED, 'ENCRYPTED'],
[FLAG_COMPRESSED, 'COMPRESSED'],
[FLAG_HAS_VECTORS, 'VECTORS'],
[FLAG_STREAM_UPGRADE, 'STREAM_UPGRADE'],
];
for (const [bit, name] of allFlags) {
const li = document.createElement('li');
const badge = document.createElement('span');
badge.className = 'badge ' + (header.flags & bit ? 'badge-on' : 'badge-off');
badge.textContent = name;
li.appendChild(badge);
flagsList.appendChild(li);
}
flagsWrap.appendChild(flagsList);
headerCard.appendChild(flagsWrap);
container.appendChild(headerCard);
// -- Hosts Card --
if (manifest.hosts.length > 0) {
const hostsCard = createCard('Download Hosts');
const table = document.createElement('table');
table.className = 'data-table';
table.innerHTML = `
<thead><tr>
<th>Type</th><th>URL</th><th>Priority</th><th>Region</th>
</tr></thead>
`;
const tbody = document.createElement('tbody');
for (const host of manifest.hosts) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><span class="badge ${host.isPrimary ? 'badge-on' : 'badge-off'}">${host.isPrimary ? 'PRIMARY' : 'FALLBACK'}</span></td>
<td>${escapeHtml(host.url)}</td>
<td>${host.priority}</td>
<td>${host.region}</td>
`;
tbody.appendChild(tr);
}
table.appendChild(tbody);
hostsCard.appendChild(table);
container.appendChild(hostsCard);
}
// -- Layers Card --
if (manifest.layers.length > 0) {
const layersCard = createCard('Progressive Layers');
const table = document.createElement('table');
table.className = 'data-table';
table.innerHTML = `
<thead><tr>
<th>ID</th><th>Name</th><th>Priority</th><th>Size</th><th>Offset</th><th>Required</th><th>Hash</th>
</tr></thead>
`;
const tbody = document.createElement('tbody');
for (const layer of manifest.layers) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${layer.layerId}</td>
<td>${escapeHtml(layer.name)}</td>
<td>${layer.priority}</td>
<td>${formatBytes(layer.size)}</td>
<td>${layer.offset}</td>
<td><span class="badge ${layer.required ? 'badge-warn' : 'badge-off'}">${layer.required ? 'YES' : 'no'}</span></td>
<td class="hex">${toHex(layer.contentHash, 8)}</td>
`;
tbody.appendChild(tr);
}
table.appendChild(tbody);
layersCard.appendChild(table);
if (manifest.totalFileSize !== null) {
const note = document.createElement('p');
note.style.cssText = 'margin-top:0.5rem;font-size:0.8rem;color:var(--text-muted)';
note.textContent = `Total RVF file size: ${formatBytes(manifest.totalFileSize)}`;
layersCard.appendChild(note);
}
container.appendChild(layersCard);
}
// -- Manifest extras --
if (manifest.contentHash || manifest.certPin || manifest.sessionToken || manifest.tokenTtl !== null) {
const extrasCard = createCard('Manifest Details');
const grid2 = document.createElement('dl');
grid2.className = 'info-grid';
if (manifest.contentHash) {
addInfoRow(grid2, 'Full Content Hash', toHex(manifest.contentHash));
}
if (manifest.certPin) {
addInfoRow(grid2, 'TLS Cert Pin', toHex(manifest.certPin));
}
if (manifest.sessionToken) {
addInfoRow(grid2, 'Session Token', toHex(manifest.sessionToken));
}
if (manifest.tokenTtl !== null) {
addInfoRow(grid2, 'Token TTL', `${manifest.tokenTtl}s`);
}
extrasCard.appendChild(grid2);
container.appendChild(extrasCard);
}
// -- Evidence (signature + microkernel hex) --
if (signature || microkernel) {
const evidenceCard = createCard('Evidence');
if (signature) {
const details = document.createElement('details');
details.className = 'evidence-section';
const summary = document.createElement('summary');
summary.textContent = `Signature (${SIG_ALGO_NAMES[header.sigAlgo] || 'unknown'}, ${signature.length} bytes)`;
details.appendChild(summary);
const pre = document.createElement('div');
pre.className = 'evidence-hex';
pre.textContent = formatHexDump(signature);
details.appendChild(pre);
evidenceCard.appendChild(details);
}
if (microkernel) {
const details = document.createElement('details');
details.className = 'evidence-section';
const summary = document.createElement('summary');
summary.textContent = `Microkernel (${formatBytes(microkernel.length)}, ${header.flags & FLAG_COMPRESSED ? 'compressed' : 'raw'})`;
details.appendChild(summary);
const pre = document.createElement('div');
pre.className = 'evidence-hex';
pre.textContent = formatHexDump(microkernel.slice(0, 512));
if (microkernel.length > 512) {
const note = document.createElement('p');
note.style.cssText = 'font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem';
note.textContent = `Showing first 512 of ${microkernel.length} bytes`;
details.appendChild(note);
}
details.appendChild(pre);
evidenceCard.appendChild(details);
}
container.appendChild(evidenceCard);
}
// -- Raw Hex Dump --
{
const rawCard = createCard('Raw Payload');
const details = document.createElement('details');
details.className = 'evidence-section';
const summary = document.createElement('summary');
summary.textContent = `Full hex dump (${formatBytes(raw.length)})`;
details.appendChild(summary);
const pre = document.createElement('div');
pre.className = 'evidence-hex';
pre.textContent = formatHexDump(raw.slice(0, 1024));
if (raw.length > 1024) {
const note = document.createElement('p');
note.style.cssText = 'font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem';
note.textContent = `Showing first 1024 of ${raw.length} bytes`;
details.appendChild(note);
}
details.appendChild(pre);
rawCard.appendChild(details);
container.appendChild(rawCard);
}
}
function renderRvfResult(container, result) {
const { segInfo, raw } = result;
const overviewCard = createCard('RVF File');
const grid = document.createElement('dl');
grid.className = 'info-grid';
addInfoRow(grid, 'File Size', formatBytes(raw.length));
addInfoRow(grid, 'Header Valid', segInfo.headerValid ? 'Yes' : 'No');
addInfoRow(grid, 'Segment Count', segInfo.segmentCount.toString());
if (segInfo.checksumValid !== undefined && segInfo.checksumValid !== null) {
addInfoRow(grid, 'Checksum', segInfo.checksumValid ? 'Valid' : 'Invalid');
}
overviewCard.appendChild(grid);
container.appendChild(overviewCard);
if (segInfo.segments.length > 0) {
const segsCard = createCard('Segments');
const table = document.createElement('table');
table.className = 'data-table';
table.innerHTML = `
<thead><tr>
<th>#</th><th>Type</th><th>Segment ID</th><th>Payload Size</th><th>Offset</th>
</tr></thead>
`;
const tbody = document.createElement('tbody');
for (let i = 0; i < segInfo.segments.length; i++) {
const seg = segInfo.segments[i];
const typeName = SEG_TYPE_NAMES[seg.segType] || `0x${seg.segType.toString(16).padStart(2, '0')}`;
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i}</td>
<td><span class="badge badge-on">${escapeHtml(typeName)}</span></td>
<td class="hex">${seg.segId}</td>
<td>${formatBytes(seg.payloadLength)}</td>
<td>${seg.offset}</td>
`;
tbody.appendChild(tr);
}
table.appendChild(tbody);
segsCard.appendChild(table);
container.appendChild(segsCard);
}
// Raw hex
const rawCard = createCard('Raw Data');
const details = document.createElement('details');
details.className = 'evidence-section';
const summary = document.createElement('summary');
summary.textContent = `Hex dump (${formatBytes(raw.length)})`;
details.appendChild(summary);
const pre = document.createElement('div');
pre.className = 'evidence-hex';
pre.textContent = formatHexDump(raw.slice(0, 1024));
details.appendChild(pre);
rawCard.appendChild(details);
container.appendChild(rawCard);
}
function renderWitnessResult(container, result) {
const card = createCard('Witness Bundle');
const grid = document.createElement('dl');
grid.className = 'info-grid';
addInfoRow(grid, 'Size', formatBytes(result.size));
addInfoRow(grid, 'Magic', result.magic);
card.appendChild(grid);
const details = document.createElement('details');
details.className = 'evidence-section';
const summary = document.createElement('summary');
summary.textContent = `Hex preview (${Math.min(256, result.size)} bytes)`;
details.appendChild(summary);
const pre = document.createElement('div');
pre.className = 'evidence-hex';
pre.textContent = formatHexDump(result.preview);
details.appendChild(pre);
card.appendChild(details);
container.appendChild(card);
}
// ---------------------------------------------------------------------------
// DOM Helpers
// ---------------------------------------------------------------------------
function createCard(title) {
const card = document.createElement('div');
card.className = 'result-card';
const h2 = document.createElement('h2');
h2.textContent = title;
card.appendChild(h2);
return card;
}
function addInfoRow(grid, label, value) {
const dt = document.createElement('dt');
dt.textContent = label;
const dd = document.createElement('dd');
dd.textContent = value;
grid.appendChild(dt);
grid.appendChild(dd);
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatHexDump(bytes) {
const lines = [];
for (let i = 0; i < bytes.length; i += 16) {
const offset = i.toString(16).padStart(8, '0');
const hexParts = [];
let ascii = '';
for (let j = 0; j < 16; j++) {
if (i + j < bytes.length) {
hexParts.push(bytes[i + j].toString(16).padStart(2, '0'));
const ch = bytes[i + j];
ascii += (ch >= 0x20 && ch <= 0x7e) ? String.fromCharCode(ch) : '.';
} else {
hexParts.push(' ');
ascii += ' ';
}
}
const hex = hexParts.slice(0, 8).join(' ') + ' ' + hexParts.slice(8).join(' ');
lines.push(`${offset} ${hex} |${ascii}|`);
}
return lines.join('\n');
}
// ---------------------------------------------------------------------------
// File Input & Drag-and-Drop
// ---------------------------------------------------------------------------
function setupFileHandlers() {
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const browseBtn = document.getElementById('btn-browse');
browseBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
// Click anywhere in drop zone opens file picker
dropZone.addEventListener('click', (e) => {
if (e.target === dropZone || e.target.tagName === 'P') {
fileInput.click();
}
});
}
async function handleFile(file) {
try {
setStatus(`Reading ${file.name} (${formatBytes(file.size)})...`);
const buffer = await file.arrayBuffer();
const result = await decodeSeed(buffer);
setStatus(`Decoded ${file.name} as ${result.type.toUpperCase()}`, 'success');
renderResults(result);
} catch (err) {
setStatus(`Error: ${err.message}`, 'error');
document.getElementById('results').innerHTML = '';
}
}
// ---------------------------------------------------------------------------
// QR Scanner
// ---------------------------------------------------------------------------
function setupScanner() {
const scanBtn = document.getElementById('btn-scan');
const stopBtn = document.getElementById('btn-scan-stop');
const scannerContainer = document.getElementById('scanner');
const video = document.getElementById('scanner-video');
const canvas = document.getElementById('scanner-canvas');
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
scanBtn.disabled = true;
scanBtn.title = 'Camera not available in this browser';
return;
}
scanBtn.addEventListener('click', async () => {
try {
scannerContainer.classList.add('active');
scanBtn.disabled = true;
scannerStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
});
video.srcObject = scannerStream;
await video.play();
setStatus('Scanning for QR codes... (point camera at an RVQS QR code)');
startScanLoop(video, canvas);
} catch (err) {
setStatus(`Camera error: ${err.message}`, 'error');
stopScanner();
}
});
stopBtn.addEventListener('click', stopScanner);
}
function stopScanner() {
const scanBtn = document.getElementById('btn-scan');
const scannerContainer = document.getElementById('scanner');
const video = document.getElementById('scanner-video');
if (scannerAnimFrame) {
cancelAnimationFrame(scannerAnimFrame);
scannerAnimFrame = null;
}
if (scannerStream) {
scannerStream.getTracks().forEach((t) => t.stop());
scannerStream = null;
}
video.srcObject = null;
scannerContainer.classList.remove('active');
scanBtn.disabled = false;
}
function startScanLoop(video, canvas) {
const ctx = canvas.getContext('2d', { willReadFrequently: true });
function tick() {
if (!scannerStream) return;
if (video.readyState >= video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
// Use BarcodeDetector API if available (Chrome/Edge)
if ('BarcodeDetector' in window) {
const detector = new BarcodeDetector({ formats: ['qr_code'] });
detector.detect(canvas).then((barcodes) => {
if (barcodes.length > 0) {
handleQrResult(barcodes[0].rawValue);
return;
}
}).catch(() => { /* no detection, keep scanning */ });
}
// Try ImageData approach for manual decode
// (In production, you'd use a QR decode library here.
// For this minimal PWA, we rely on BarcodeDetector or raw binary.)
try {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Binary QR codes encode raw bytes. BarcodeDetector handles this
// on supported browsers. Without it, the user can export from
// their QR reader app and load the binary file directly.
void imageData;
} catch { /* ignore */ }
}
scannerAnimFrame = requestAnimationFrame(tick);
}
tick();
}
function handleQrResult(rawValue) {
stopScanner();
// QR could contain raw binary or base64-encoded seed
let bytes;
try {
// Try base64 first
const decoded = atob(rawValue);
bytes = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) {
bytes[i] = decoded.charCodeAt(i);
}
} catch {
// Try as raw string bytes
const encoder = new TextEncoder();
bytes = encoder.encode(rawValue);
}
decodeSeed(bytes.buffer)
.then((result) => {
setStatus('Decoded QR seed', 'success');
renderResults(result);
})
.catch((err) => {
setStatus(`QR decode error: ${err.message}`, 'error');
});
}
// ---------------------------------------------------------------------------
// Theme Toggle
// ---------------------------------------------------------------------------
function setupTheme() {
const btn = document.getElementById('btn-theme');
const stored = localStorage.getItem('rvf-theme');
if (stored) {
document.documentElement.setAttribute('data-theme', stored);
updateThemeLabel(btn, stored);
}
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('rvf-theme', next);
updateThemeLabel(btn, next);
});
}
function updateThemeLabel(btn, theme) {
btn.textContent = theme === 'light' ? 'Dark Mode' : 'Light Mode';
}
// ---------------------------------------------------------------------------
// PWA Registration
// ---------------------------------------------------------------------------
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(() => {
// Service worker registration failed (e.g., file:// protocol)
});
}
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
document.addEventListener('DOMContentLoaded', () => {
setupTheme();
setupFileHandlers();
setupScanner();
registerServiceWorker();
loadWasm();
});