Complete TypeScript frontend migration #22
@@ -265,6 +265,93 @@
|
|||||||
return `${(bytes / 1048576).toFixed(1)} MB`;
|
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
|
// src/app.js
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
var authRole = null;
|
var authRole = null;
|
||||||
@@ -690,7 +777,6 @@
|
|||||||
connLostOverlayEl.classList.toggle("conn-lost-fullscreen", fullscreen);
|
connLostOverlayEl.classList.toggle("conn-lost-fullscreen", fullscreen);
|
||||||
connLostOverlayEl.classList.toggle("is-hidden", !visible);
|
connLostOverlayEl.classList.toggle("is-hidden", !visible);
|
||||||
}
|
}
|
||||||
var decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
|
||||||
var decodeHistoryReplayActive = false;
|
var decodeHistoryReplayActive = false;
|
||||||
var decodeMapSyncPending = false;
|
var decodeMapSyncPending = false;
|
||||||
function markDecodeMapSyncPending() {
|
function markDecodeMapSyncPending() {
|
||||||
@@ -712,124 +798,6 @@
|
|||||||
function decodeHistoryMapRenderingDeferred() {
|
function decodeHistoryMapRenderingDeferred() {
|
||||||
return decodeHistoryReplayActive || !window.trx?.map?.aprsMap;
|
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;
|
var lastSpectrumData = null;
|
||||||
window.lastSpectrumData = null;
|
window.lastSpectrumData = null;
|
||||||
var lastControl;
|
var lastControl;
|
||||||
@@ -5893,7 +5861,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
const payload = await resp.arrayBuffer();
|
const payload = await resp.arrayBuffer();
|
||||||
if (!payload || payload.byteLength === 0) return {};
|
if (!payload || payload.byteLength === 0) return {};
|
||||||
setDecodeHistoryOverlayVisible(true, "Loading decode history…", "Decoding compressed history payload");
|
setDecodeHistoryOverlayVisible(true, "Loading decode history…", "Decoding compressed history payload");
|
||||||
return decodeCborPayload(payload);
|
return decodeCbor(payload);
|
||||||
}).then((groups) => {
|
}).then((groups) => {
|
||||||
if (typeof onReady === "function") onReady(groups && typeof groups === "object" ? groups : {});
|
if (typeof onReady === "function") onReady(groups && typeof groups === "object" ? groups : {});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
formatWavelength,
|
formatWavelength,
|
||||||
parseFrequencyInput,
|
parseFrequencyInput,
|
||||||
} from "./core/format.js";
|
} from "./core/format.js";
|
||||||
|
import { decodeCbor as decodeCborPayload } from "./core/cbor.js";
|
||||||
|
|
||||||
// --- Decoder registry (fetched from /decoders on load) ---
|
// --- Decoder registry (fetched from /decoders on load) ---
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
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("conn-lost-fullscreen", fullscreen);
|
||||||
connLostOverlayEl.classList.toggle("is-hidden", !visible);
|
connLostOverlayEl.classList.toggle("is-hidden", !visible);
|
||||||
}
|
}
|
||||||
const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
|
||||||
let decodeHistoryReplayActive = false;
|
let decodeHistoryReplayActive = false;
|
||||||
let decodeMapSyncPending = false;
|
let decodeMapSyncPending = false;
|
||||||
|
|
||||||
@@ -552,128 +552,6 @@ function decodeHistoryMapRenderingDeferred() {
|
|||||||
return decodeHistoryReplayActive || !window.trx?.map?.aprsMap;
|
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;
|
let lastSpectrumData = null;
|
||||||
window.lastSpectrumData = null;
|
window.lastSpectrumData = null;
|
||||||
let lastControl;
|
let lastControl;
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// 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<Record<number, number>> = { 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<string, CborValue> = {};
|
||||||
|
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<Record<number, number>> = { 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user