From c8cc235e88628d3d010ee8eb907b20e5a6db7858 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 17:04:13 +0200 Subject: [PATCH] refactor: extract strict CBOR decoder --- .../assets/web/generated/app.js | 208 ++++++++---------- .../trx-frontend-http/frontend/src/app.js | 124 +---------- .../frontend/src/core/cbor.ts | 104 +++++++++ 3 files changed, 193 insertions(+), 243 deletions(-) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/cbor.ts diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index f0d794fb..c4f29569 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -265,6 +265,93 @@ return `${(bytes / 1048576).toFixed(1)} MB`; } + // src/core/cbor.ts + var textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null; + function decodeUint(view, bytes, state, additional) { + const offset = state.offset; + if (additional < 24) return additional; + const widths = { 24: 1, 25: 2, 26: 4, 27: 8 }; + const width = widths[additional]; + if (width === void 0) throw new Error("Unsupported CBOR additional info"); + if (offset + width > bytes.length) throw new Error("CBOR payload truncated"); + state.offset += width; + if (additional === 24) return bytes[offset] ?? 0; + if (additional === 25) return view.getUint16(offset); + if (additional === 26) return view.getUint32(offset); + const numeric = Number(view.getBigUint64(offset)); + if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range"); + return numeric; + } + function decodeFloat16(bits) { + const sign = bits & 32768 ? -1 : 1; + const exponent = bits >> 10 & 31; + const fraction = bits & 1023; + if (exponent === 0) return fraction === 0 ? sign * 0 : sign * 2 ** -14 * (fraction / 1024); + if (exponent === 31) return fraction === 0 ? sign * Infinity : Number.NaN; + return sign * 2 ** (exponent - 15) * (1 + fraction / 1024); + } + function decodeItem(view, bytes, state) { + if (state.offset >= bytes.length) throw new Error("CBOR payload truncated"); + const initial = bytes[state.offset++]; + if (initial === void 0) throw new Error("CBOR payload truncated"); + const major = initial >> 5; + const additional = initial & 31; + if (major === 0) return decodeUint(view, bytes, state, additional); + if (major === 1) return -1 - decodeUint(view, bytes, state, additional); + if (major === 2 || major === 3) { + const length = decodeUint(view, bytes, state, additional); + if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); + const chunk = bytes.subarray(state.offset, state.offset + length); + state.offset += length; + if (major === 2) return Array.from(chunk); + return textDecoder ? textDecoder.decode(chunk) : String.fromCharCode(...chunk); + } + if (major === 4) { + const length = decodeUint(view, bytes, state, additional); + return Array.from({ length }, () => decodeItem(view, bytes, state)); + } + if (major === 5) { + const length = decodeUint(view, bytes, state, additional); + const value = {}; + for (let index = 0; index < length; index += 1) { + const key = decodeItem(view, bytes, state); + if (typeof key !== "string" && typeof key !== "number" && typeof key !== "boolean" && key !== null) { + throw new Error("Unsupported composite CBOR map key"); + } + value[String(key)] = decodeItem(view, bytes, state); + } + return value; + } + if (major === 6) { + decodeUint(view, bytes, state, additional); + return decodeItem(view, bytes, state); + } + if (major === 7) { + if (additional === 20) return false; + if (additional === 21) return true; + if (additional === 22) return null; + if (additional === 23) return void 0; + const widths = { 25: 2, 26: 4, 27: 8 }; + const width = widths[additional]; + if (width === void 0) throw new Error("Unsupported CBOR major type"); + if (state.offset + width > bytes.length) throw new Error("CBOR payload truncated"); + const offset = state.offset; + state.offset += width; + if (additional === 25) return decodeFloat16(view.getUint16(offset)); + if (additional === 26) return view.getFloat32(offset); + return view.getFloat64(offset); + } + throw new Error("Unsupported CBOR major type"); + } + function decodeCbor(buffer) { + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const state = { offset: 0 }; + const value = decodeItem(view, bytes, state); + if (state.offset !== bytes.length) throw new Error("Unexpected trailing bytes in CBOR payload"); + return value; + } + // src/app.js void loadDecoderRegistry(refreshOperatorLayoutCapabilities); var authRole = null; @@ -690,7 +777,6 @@ connLostOverlayEl.classList.toggle("conn-lost-fullscreen", fullscreen); connLostOverlayEl.classList.toggle("is-hidden", !visible); } - var decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null; var decodeHistoryReplayActive = false; var decodeMapSyncPending = false; function markDecodeMapSyncPending() { @@ -712,124 +798,6 @@ function decodeHistoryMapRenderingDeferred() { return decodeHistoryReplayActive || !window.trx?.map?.aprsMap; } - function decodeCborUint(view, bytes, state, additional) { - const offset = state.offset; - if (additional < 24) return additional; - if (additional === 24) { - if (offset + 1 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 1; - return bytes[offset]; - } - if (additional === 25) { - if (offset + 2 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 2; - return view.getUint16(offset); - } - if (additional === 26) { - if (offset + 4 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 4; - return view.getUint32(offset); - } - if (additional === 27) { - if (offset + 8 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getBigUint64(offset); - state.offset += 8; - const numeric = Number(value); - if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range"); - return numeric; - } - throw new Error("Unsupported CBOR additional info"); - } - function decodeCborFloat16(bits) { - const sign = bits & 32768 ? -1 : 1; - const exponent = bits >> 10 & 31; - const fraction = bits & 1023; - if (exponent === 0) { - return fraction === 0 ? sign * 0 : sign * Math.pow(2, -14) * (fraction / 1024); - } - if (exponent === 31) { - return fraction === 0 ? sign * Infinity : Number.NaN; - } - return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024); - } - function decodeCborItem(view, bytes, state) { - if (state.offset >= bytes.length) throw new Error("CBOR payload truncated"); - const initial = bytes[state.offset++]; - const major = initial >> 5; - const additional = initial & 31; - if (major === 0) return decodeCborUint(view, bytes, state, additional); - if (major === 1) return -1 - decodeCborUint(view, bytes, state, additional); - if (major === 2) { - const length = decodeCborUint(view, bytes, state, additional); - if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); - const chunk = bytes.slice(state.offset, state.offset + length); - state.offset += length; - return Array.from(chunk); - } - if (major === 3) { - const length = decodeCborUint(view, bytes, state, additional); - if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); - const chunk = bytes.subarray(state.offset, state.offset + length); - state.offset += length; - return decodeHistoryTextDecoder ? decodeHistoryTextDecoder.decode(chunk) : String.fromCharCode(...chunk); - } - if (major === 4) { - const length = decodeCborUint(view, bytes, state, additional); - const items = new Array(length); - for (let i = 0; i < length; i += 1) { - items[i] = decodeCborItem(view, bytes, state); - } - return items; - } - if (major === 5) { - const length = decodeCborUint(view, bytes, state, additional); - const value = {}; - for (let i = 0; i < length; i += 1) { - const key = decodeCborItem(view, bytes, state); - value[String(key)] = decodeCborItem(view, bytes, state); - } - return value; - } - if (major === 6) { - decodeCborUint(view, bytes, state, additional); - return decodeCborItem(view, bytes, state); - } - if (major === 7) { - if (additional === 20) return false; - if (additional === 21) return true; - if (additional === 22) return null; - if (additional === 23) return void 0; - if (additional === 25) { - if (state.offset + 2 > bytes.length) throw new Error("CBOR payload truncated"); - const bits = view.getUint16(state.offset); - state.offset += 2; - return decodeCborFloat16(bits); - } - if (additional === 26) { - if (state.offset + 4 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getFloat32(state.offset); - state.offset += 4; - return value; - } - if (additional === 27) { - if (state.offset + 8 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getFloat64(state.offset); - state.offset += 8; - return value; - } - } - throw new Error("Unsupported CBOR major type"); - } - function decodeCborPayload(buffer) { - const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); - const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - const state = { offset: 0 }; - const value = decodeCborItem(view, bytes, state); - if (state.offset !== bytes.length) { - throw new Error("Unexpected trailing bytes in CBOR payload"); - } - return value; - } var lastSpectrumData = null; window.lastSpectrumData = null; var lastControl; @@ -5893,7 +5861,7 @@ ${unsupportedBandSummary()}`; const payload = await resp.arrayBuffer(); if (!payload || payload.byteLength === 0) return {}; setDecodeHistoryOverlayVisible(true, "Loading decode history…", "Decoding compressed history payload"); - return decodeCborPayload(payload); + return decodeCbor(payload); }).then((groups) => { if (typeof onReady === "function") onReady(groups && typeof groups === "object" ? groups : {}); }).catch((err) => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js index e595e268..122737fb 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js @@ -29,6 +29,7 @@ import { formatWavelength, parseFrequencyInput, } from "./core/format.js"; +import { decodeCbor as decodeCborPayload } from "./core/cbor.js"; // --- Decoder registry (fetched from /decoders on load) --- void loadDecoderRegistry(refreshOperatorLayoutCapabilities); @@ -525,7 +526,6 @@ function setConnLostOverlay(visible, title = "Connection lost", sub = "Retrying\ connLostOverlayEl.classList.toggle("conn-lost-fullscreen", fullscreen); connLostOverlayEl.classList.toggle("is-hidden", !visible); } -const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null; let decodeHistoryReplayActive = false; let decodeMapSyncPending = false; @@ -552,128 +552,6 @@ function decodeHistoryMapRenderingDeferred() { return decodeHistoryReplayActive || !window.trx?.map?.aprsMap; } -function decodeCborUint(view, bytes, state, additional) { - const offset = state.offset; - if (additional < 24) return additional; - if (additional === 24) { - if (offset + 1 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 1; - return bytes[offset]; - } - if (additional === 25) { - if (offset + 2 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 2; - return view.getUint16(offset); - } - if (additional === 26) { - if (offset + 4 > bytes.length) throw new Error("CBOR payload truncated"); - state.offset += 4; - return view.getUint32(offset); - } - if (additional === 27) { - if (offset + 8 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getBigUint64(offset); - state.offset += 8; - const numeric = Number(value); - if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range"); - return numeric; - } - throw new Error("Unsupported CBOR additional info"); -} - -function decodeCborFloat16(bits) { - const sign = (bits & 0x8000) ? -1 : 1; - const exponent = (bits >> 10) & 0x1f; - const fraction = bits & 0x03ff; - if (exponent === 0) { - return fraction === 0 ? sign * 0 : sign * Math.pow(2, -14) * (fraction / 1024); - } - if (exponent === 0x1f) { - return fraction === 0 ? sign * Infinity : Number.NaN; - } - return sign * Math.pow(2, exponent - 15) * (1 + (fraction / 1024)); -} - -function decodeCborItem(view, bytes, state) { - if (state.offset >= bytes.length) throw new Error("CBOR payload truncated"); - const initial = bytes[state.offset++]; - const major = initial >> 5; - const additional = initial & 0x1f; - if (major === 0) return decodeCborUint(view, bytes, state, additional); - if (major === 1) return -1 - decodeCborUint(view, bytes, state, additional); - if (major === 2) { - const length = decodeCborUint(view, bytes, state, additional); - if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); - const chunk = bytes.slice(state.offset, state.offset + length); - state.offset += length; - return Array.from(chunk); - } - if (major === 3) { - const length = decodeCborUint(view, bytes, state, additional); - if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); - const chunk = bytes.subarray(state.offset, state.offset + length); - state.offset += length; - return decodeHistoryTextDecoder ? decodeHistoryTextDecoder.decode(chunk) : String.fromCharCode(...chunk); - } - if (major === 4) { - const length = decodeCborUint(view, bytes, state, additional); - const items = new Array(length); - for (let i = 0; i < length; i += 1) { - items[i] = decodeCborItem(view, bytes, state); - } - return items; - } - if (major === 5) { - const length = decodeCborUint(view, bytes, state, additional); - const value = {}; - for (let i = 0; i < length; i += 1) { - const key = decodeCborItem(view, bytes, state); - value[String(key)] = decodeCborItem(view, bytes, state); - } - return value; - } - if (major === 6) { - decodeCborUint(view, bytes, state, additional); - return decodeCborItem(view, bytes, state); - } - if (major === 7) { - if (additional === 20) return false; - if (additional === 21) return true; - if (additional === 22) return null; - if (additional === 23) return undefined; - if (additional === 25) { - if (state.offset + 2 > bytes.length) throw new Error("CBOR payload truncated"); - const bits = view.getUint16(state.offset); - state.offset += 2; - return decodeCborFloat16(bits); - } - if (additional === 26) { - if (state.offset + 4 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getFloat32(state.offset); - state.offset += 4; - return value; - } - if (additional === 27) { - if (state.offset + 8 > bytes.length) throw new Error("CBOR payload truncated"); - const value = view.getFloat64(state.offset); - state.offset += 8; - return value; - } - } - throw new Error("Unsupported CBOR major type"); -} - -function decodeCborPayload(buffer) { - const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); - const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - const state = { offset: 0 }; - const value = decodeCborItem(view, bytes, state); - if (state.offset !== bytes.length) { - throw new Error("Unexpected trailing bytes in CBOR payload"); - } - return value; -} - let lastSpectrumData = null; window.lastSpectrumData = null; let lastControl; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/cbor.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/cbor.ts new file mode 100644 index 00000000..ed85ddae --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/cbor.ts @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +export type CborValue = number | string | boolean | null | undefined + | CborValue[] | { [key: string]: CborValue }; + +interface DecodeState { offset: number } + +const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null; + +function decodeUint( + view: DataView, + bytes: Uint8Array, + state: DecodeState, + additional: number, +): number { + const offset = state.offset; + if (additional < 24) return additional; + const widths: Partial> = { 24: 1, 25: 2, 26: 4, 27: 8 }; + const width = widths[additional]; + if (width === undefined) throw new Error("Unsupported CBOR additional info"); + if (offset + width > bytes.length) throw new Error("CBOR payload truncated"); + state.offset += width; + if (additional === 24) return bytes[offset] ?? 0; + if (additional === 25) return view.getUint16(offset); + if (additional === 26) return view.getUint32(offset); + const numeric = Number(view.getBigUint64(offset)); + if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range"); + return numeric; +} + +function decodeFloat16(bits: number): number { + const sign = (bits & 0x8000) ? -1 : 1; + const exponent = (bits >> 10) & 0x1f; + const fraction = bits & 0x03ff; + if (exponent === 0) return fraction === 0 ? sign * 0 : sign * 2 ** -14 * (fraction / 1024); + if (exponent === 0x1f) return fraction === 0 ? sign * Infinity : Number.NaN; + return sign * 2 ** (exponent - 15) * (1 + fraction / 1024); +} + +function decodeItem(view: DataView, bytes: Uint8Array, state: DecodeState): CborValue { + if (state.offset >= bytes.length) throw new Error("CBOR payload truncated"); + const initial = bytes[state.offset++]; + if (initial === undefined) throw new Error("CBOR payload truncated"); + const major = initial >> 5; + const additional = initial & 0x1f; + if (major === 0) return decodeUint(view, bytes, state, additional); + if (major === 1) return -1 - decodeUint(view, bytes, state, additional); + if (major === 2 || major === 3) { + const length = decodeUint(view, bytes, state, additional); + if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated"); + const chunk = bytes.subarray(state.offset, state.offset + length); + state.offset += length; + if (major === 2) return Array.from(chunk); + return textDecoder ? textDecoder.decode(chunk) : String.fromCharCode(...chunk); + } + if (major === 4) { + const length = decodeUint(view, bytes, state, additional); + return Array.from({ length }, () => decodeItem(view, bytes, state)); + } + if (major === 5) { + const length = decodeUint(view, bytes, state, additional); + const value: Record = {}; + for (let index = 0; index < length; index += 1) { + const key = decodeItem(view, bytes, state); + if (typeof key !== "string" && typeof key !== "number" && typeof key !== "boolean" + && key !== null) { + throw new Error("Unsupported composite CBOR map key"); + } + value[String(key)] = decodeItem(view, bytes, state); + } + return value; + } + if (major === 6) { + decodeUint(view, bytes, state, additional); + return decodeItem(view, bytes, state); + } + if (major === 7) { + if (additional === 20) return false; + if (additional === 21) return true; + if (additional === 22) return null; + if (additional === 23) return undefined; + const widths: Partial> = { 25: 2, 26: 4, 27: 8 }; + const width = widths[additional]; + if (width === undefined) throw new Error("Unsupported CBOR major type"); + if (state.offset + width > bytes.length) throw new Error("CBOR payload truncated"); + const offset = state.offset; + state.offset += width; + if (additional === 25) return decodeFloat16(view.getUint16(offset)); + if (additional === 26) return view.getFloat32(offset); + return view.getFloat64(offset); + } + throw new Error("Unsupported CBOR major type"); +} + +export function decodeCbor(buffer: ArrayBuffer | Uint8Array): CborValue { + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const state = { offset: 0 }; + const value = decodeItem(view, bytes, state); + if (state.offset !== bytes.length) throw new Error("Unexpected trailing bytes in CBOR payload"); + return value; +}