From 4231ea8172f12dcb53b058ce0f9cdb49d42c5592 Mon Sep 17 00:00:00 2001 From: ttmx Date: Thu, 25 Jun 2026 12:29:34 +0100 Subject: [PATCH 1/4] feat: Support buffer views serialization --- .changeset/fix-arraybuffer-serialization.md | 5 ++ README.md | 3 +- __tests__/index.test.ts | 77 ++++++++++++++++++ protocol.md | 6 +- src/core.ts | 12 +++ src/serialize.ts | 90 +++++++++++++++++++-- 6 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 .changeset/fix-arraybuffer-serialization.md diff --git a/.changeset/fix-arraybuffer-serialization.md b/.changeset/fix-arraybuffer-serialization.md new file mode 100644 index 00000000..71399cc6 --- /dev/null +++ b/.changeset/fix-arraybuffer-serialization.md @@ -0,0 +1,5 @@ +--- +"capnweb": patch +--- + +Support exact ArrayBuffer, DataView, and typed array serialization over RPC. diff --git a/README.md b/README.md index 963e4327..bfe4ae98 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ The following types can be passed over RPC (in arguments or return values), and * Arrays * `bigint` * `Date` -* `Uint8Array` +* `ArrayBuffer`, `DataView`, and typed arrays * `Error` and its well-known subclasses * `Blob` * `ReadableStream` and `WritableStream`, with automatic flow control. @@ -207,7 +207,6 @@ The following types can be passed over RPC (in arguments or return values), and The following types are not supported as of this writing, but may be added in the future: * `Map` and `Set` -* `ArrayBuffer` and typed arrays other than `Uint8Array` * `RegExp` The following are intentionally NOT supported: diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index fdf41b33..3123da5b 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -160,6 +160,16 @@ describe("simple serialization", () => { expect(new Uint8Array(deserialized)).toStrictEqual(bytes); }) + it("can serialize Uint8Array as legacy bytes without a type marker", () => { + let bytes = new Uint8Array([72, 101, 108, 108, 111]); + let serialized = serialize(bytes); + expect(serialized).toBe('["bytes","SGVsbG8"]'); + + let deserialized = deserialize(serialized) as Uint8Array; + expect(deserialized).toBeInstanceOf(Uint8Array); + expect(new Uint8Array(deserialized)).toStrictEqual(bytes); + }) + it("can serialize Node.js Buffer as bytes", () => { if (typeof Buffer === "undefined") return; // skip in browsers let buf = Buffer.from("hello!"); @@ -170,6 +180,73 @@ describe("simple serialization", () => { expect(new Uint8Array(deserialized)).toStrictEqual(new Uint8Array(buf)); }) + it("can serialize ArrayBuffer as bytes with an ArrayBuffer marker", () => { + let bytes = new Uint8Array([72, 101, 108, 108, 111]); + let serialized = serialize(bytes.buffer); + expect(serialized).toBe('["bytes","SGVsbG8","ArrayBuffer"]'); + + let deserialized = deserialize(serialized); + expect(deserialized).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(deserialized as ArrayBuffer)).toStrictEqual(bytes); + }) + + it("can serialize typed array views as bytes with type markers", () => { + let cases = [ + { + name: "DataView", + elementSize: 1, + makeView: (buffer: ArrayBuffer, offset: number, byteLength: number) => + new DataView(buffer, offset, byteLength), + }, + ...[ + Int8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + ].map(Type => ({ + name: Type.name, + elementSize: Type.BYTES_PER_ELEMENT, + makeView: (buffer: ArrayBuffer, offset: number, byteLength: number) => + new Type(buffer, offset, byteLength / Type.BYTES_PER_ELEMENT), + })), + ]; + + for (let {name, elementSize, makeView} of cases) { + // Use a non-zero offset and extra trailing byte to verify only the view's visible byte range + // is serialized, not the whole backing buffer. + let byteLength = elementSize * 2; + let offset = elementSize; + let backing = new ArrayBuffer(offset + byteLength + 1); + let bytes = new Uint8Array(byteLength); + for (let i = 0; i < bytes.length; i++) bytes[i] = i + 1; + new Uint8Array(backing, offset, byteLength).set(bytes); + + let view = makeView(backing, offset, byteLength); + let serialized = serialize(view); + let parsed = JSON.parse(serialized) as [string, string, string]; + expect(parsed[0]).toBe("bytes"); + expect(parsed[2]).toBe(name); + + let deserialized = deserialize(serialized) as ArrayBufferView; + expect(Object.getPrototypeOf(deserialized)).toBe(Object.getPrototypeOf(view)); + expect(new Uint8Array( + deserialized.buffer, deserialized.byteOffset, deserialized.byteLength)).toStrictEqual(bytes); + } + }) + + it("throws for unknown bytes type markers", () => { + expect(() => deserialize('["bytes","SGVsbG8","invalidUint8Array"]')).toThrowError( + "Unknown bytes type marker: invalidUint8Array"); + expect(() => deserialize('["bytes","SGVsbG8",123]')).toThrowError( + "Unknown bytes type marker type: number"); + }) + it("preserves Invalid Date values through serialization", () => { let invalidDate = new Date(NaN); let serialized = serialize(invalidDate); diff --git a/protocol.md b/protocol.md index b1797954..02542d6c 100644 --- a/protocol.md +++ b/protocol.md @@ -168,9 +168,11 @@ The literal value `undefined`. The values Infinity, -Infinity, and NaN. -`["bytes", base64]` +`["bytes", base64]`, `["bytes", base64, type]` -A `Uint8Array`, represented as a base64-encoded string. +A `Uint8Array`, `ArrayBuffer`, `DataView`, or typed array, represented as a base64-encoded string. +The optional third element preserves the byte container type across the wire. If it is +omitted, the receiver should deserialize bytes as its default `Uint8Array` for backwards compatibility. `["blob", type, readableExpression]` diff --git a/src/core.ts b/src/core.ts index 2ea587e6..d163a20a 100644 --- a/src/core.ts +++ b/src/core.ts @@ -95,6 +95,18 @@ export function typeForRpc(value: unknown): TypeForRpc { case Uint8Array.prototype: case BUFFER_PROTOTYPE: + case ArrayBuffer.prototype: + case DataView.prototype: + case Int8Array.prototype: + case Uint8ClampedArray.prototype: + case Int16Array.prototype: + case Uint16Array.prototype: + case Int32Array.prototype: + case Uint32Array.prototype: + case BigInt64Array.prototype: + case BigUint64Array.prototype: + case Float32Array.prototype: + case Float64Array.prototype: return "bytes"; case WritableStream.prototype: diff --git a/src/serialize.ts b/src/serialize.ts index 97074c0c..9bb0dc00 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -162,12 +162,59 @@ export class Devaluator { } case "bytes": { - let bytes = value as Uint8Array; - if (bytes.toBase64) { - return ["bytes", bytes.toBase64({omitPadding: true})]; + let alternateTypeName: string | undefined; + let bytes: Uint8Array | undefined; + switch (Object.getPrototypeOf(value)) { + case ArrayBuffer.prototype: + alternateTypeName = "ArrayBuffer"; + bytes = new Uint8Array(value as ArrayBuffer); + break; + case DataView.prototype: + alternateTypeName = "DataView"; + break; + case Int8Array.prototype: + alternateTypeName = "Int8Array"; + break; + case Uint8ClampedArray.prototype: + alternateTypeName = "Uint8ClampedArray"; + break; + case Int16Array.prototype: + alternateTypeName = "Int16Array"; + break; + case Uint16Array.prototype: + alternateTypeName = "Uint16Array"; + break; + case Int32Array.prototype: + alternateTypeName = "Int32Array"; + break; + case Uint32Array.prototype: + alternateTypeName = "Uint32Array"; + break; + case BigInt64Array.prototype: + alternateTypeName = "BigInt64Array"; + break; + case BigUint64Array.prototype: + alternateTypeName = "BigUint64Array"; + break; + case Float32Array.prototype: + alternateTypeName = "Float32Array"; + break; + case Float64Array.prototype: + alternateTypeName = "Float64Array"; + break; + default: + bytes = value as Uint8Array; + break; + } + if (bytes === undefined) { + let view = value as ArrayBufferView; + bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } + let b64: string; - if (typeof Buffer !== "undefined") { + if (bytes.toBase64) { + b64 = bytes.toBase64({omitPadding: true}); + } else if (typeof Buffer !== "undefined") { let buf = bytes instanceof Buffer ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); b64 = buf.toString("base64"); @@ -178,7 +225,8 @@ export class Devaluator { } b64 = btoa(binary); } - return ["bytes", b64.replace(/=+$/, "")]; + b64 = b64.replace(/=+$/, ""); + return alternateTypeName === undefined ? ["bytes", b64] : ["bytes", b64, alternateTypeName]; } case "headers": @@ -601,19 +649,45 @@ export class Evaluator { break; case "bytes": { if (typeof value[1] == "string") { + let bytes: Uint8Array; if (typeof Buffer !== "undefined") { - return Buffer.from(value[1], "base64"); + bytes = Buffer.from(value[1], "base64"); } else if (Uint8Array.fromBase64) { - return Uint8Array.fromBase64(value[1]); + bytes = Uint8Array.fromBase64(value[1]); } else { let bs = atob(value[1]); let len = bs.length; - let bytes = new Uint8Array(len); + bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = bs.charCodeAt(i); } + } + if (value.length === 2) { return bytes; } + if (typeof value[2] !== "string") { + throw new TypeError(`Unknown bytes type marker type: ${typeof value[2]}`); + } + + let buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + switch (value[2]) { + case "ArrayBuffer": return buffer; + case "DataView": return new DataView(buffer); + case "Int8Array": return new Int8Array(buffer); + case "Uint8ClampedArray": return new Uint8ClampedArray(buffer); + case "Int16Array": return new Int16Array(buffer); + case "Uint16Array": return new Uint16Array(buffer); + case "Int32Array": return new Int32Array(buffer); + case "Uint32Array": return new Uint32Array(buffer); + case "BigInt64Array": return new BigInt64Array(buffer); + case "BigUint64Array": return new BigUint64Array(buffer); + case "Float32Array": return new Float32Array(buffer); + case "Float64Array": return new Float64Array(buffer); + default: { + let marker = value[2].slice(0, 64); + throw new TypeError(`Unknown bytes type marker: ${marker}`); + } + } } break; } From 282158470e73b28f71e820c41afff72ffb04de90 Mon Sep 17 00:00:00 2001 From: ttmx Date: Thu, 25 Jun 2026 19:17:18 +0100 Subject: [PATCH 2/4] docs: list bytes type markers --- protocol.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/protocol.md b/protocol.md index 02542d6c..b21bfa7d 100644 --- a/protocol.md +++ b/protocol.md @@ -170,9 +170,12 @@ The values Infinity, -Infinity, and NaN. `["bytes", base64]`, `["bytes", base64, type]` -A `Uint8Array`, `ArrayBuffer`, `DataView`, or typed array, represented as a base64-encoded string. -The optional third element preserves the byte container type across the wire. If it is -omitted, the receiver should deserialize bytes as its default `Uint8Array` for backwards compatibility. +A byte container, represented as a base64-encoded string. If `type` is omitted, the receiver +should deserialize bytes as its default `Uint8Array` for backwards compatibility. Otherwise, +`type` preserves the byte container type across the wire. The supported `type` values are +`ArrayBuffer`, `DataView`, `Int8Array`, `Uint8ClampedArray`, +`Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, +`Float32Array`, and `Float64Array`. `["blob", type, readableExpression]` From 4543561543d2b3f156ad7d6c0733d46b93a1f60a Mon Sep 17 00:00:00 2001 From: ttmx Date: Wed, 15 Jul 2026 15:45:48 +0100 Subject: [PATCH 3/4] Always serialize in little endian, check length multiples. --- .changeset/fix-arraybuffer-serialization.md | 2 +- __tests__/index.test.ts | 35 +++++++++++++++ protocol.md | 3 +- src/serialize.ts | 48 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/.changeset/fix-arraybuffer-serialization.md b/.changeset/fix-arraybuffer-serialization.md index 71399cc6..31028901 100644 --- a/.changeset/fix-arraybuffer-serialization.md +++ b/.changeset/fix-arraybuffer-serialization.md @@ -1,5 +1,5 @@ --- -"capnweb": patch +"capnweb": minor --- Support exact ArrayBuffer, DataView, and typed array serialization over RPC. diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 3123da5b..942caa20 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -6,6 +6,7 @@ import { expect, it, describe, inject } from "vitest" import { deserialize, serialize, RpcSession, type RpcSessionOptions, RpcTransport, RpcTarget, RpcStub, newWebSocketRpcSession, newMessagePortRpcSession, newHttpBatchRpcSession} from "../src/index.js" +import { swapByteOrder } from "../src/serialize.js" import { Counter, TestTarget } from "./test-util.js"; let SERIALIZE_TEST_CASES: Record = { @@ -240,6 +241,40 @@ describe("simple serialization", () => { } }) + it("serializes multi-byte numbers in little-endian wire order", () => { + let serialized = serialize(new Uint32Array([0x01020304])); + let parsed = JSON.parse(serialized) as [string, string, string]; + expect(parsed[2]).toBe("Uint32Array"); + let wireBytes = Uint8Array.from(atob(parsed[1]), c => c.charCodeAt(0)); + expect([...wireBytes]).toStrictEqual([0x04, 0x03, 0x02, 0x01]); + }) + + it("can swap each multi-byte element's byte order", () => { + for (let elementSize of [2, 4, 8]) { + let bytes = Uint8Array.from( + { length: elementSize * 3 }, (_, index) => (index * 37 + 1) & 0xff); + let expected = bytes.slice(); + for (let offset = 0; offset < expected.length; offset += elementSize) { + expected.subarray(offset, offset + elementSize).reverse(); + } + + swapByteOrder(bytes, elementSize); + expect(bytes).toStrictEqual(expected); + } + }) + + it("rejects byte lengths that are misaligned for typed arrays", () => { + for (let [type, byteLength, elementSize] of [ + ["Int16Array", 1, 2], + ["Float32Array", 3, 4], + ["BigUint64Array", 7, 8], + ] as const) { + let base64 = btoa("\0".repeat(byteLength)); + expect(() => deserialize(`["bytes","${base64}","${type}"]`)).toThrowError( + `Invalid byte length ${byteLength} for ${type}; expected a multiple of ${elementSize}`); + } + }) + it("throws for unknown bytes type markers", () => { expect(() => deserialize('["bytes","SGVsbG8","invalidUint8Array"]')).toThrowError( "Unknown bytes type marker: invalidUint8Array"); diff --git a/protocol.md b/protocol.md index b21bfa7d..9fda5f39 100644 --- a/protocol.md +++ b/protocol.md @@ -175,7 +175,8 @@ should deserialize bytes as its default `Uint8Array` for backwards compatibility `type` preserves the byte container type across the wire. The supported `type` values are `ArrayBuffer`, `DataView`, `Int8Array`, `Uint8ClampedArray`, `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, -`Float32Array`, and `Float64Array`. +`Float32Array`, and `Float64Array`. Multi-byte typed array elements are encoded in little-endian +byte order. `["blob", type, readableExpression]` diff --git a/src/serialize.ts b/src/serialize.ts index 9bb0dc00..06bf6e8f 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -9,6 +9,39 @@ export type ExportId = number; // ======================================================================================= +const NATIVE_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1; + +const TYPED_ARRAY_ELEMENT_SIZE: Record = { + Int16Array: 2, + Uint16Array: 2, + Int32Array: 4, + Uint32Array: 4, + Float32Array: 4, + BigInt64Array: 8, + BigUint64Array: 8, + Float64Array: 8, +}; + +// Reverse each element's bytes in place. Production callers only need this on +// big-endian hosts; it is exported solely so the behavior can be tested on +// little-endian hosts. +export function swapByteOrder(bytes: Uint8Array, elementSize: number): void { + let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + for (let offset = 0; offset < bytes.byteLength; offset += elementSize) { + switch (elementSize) { + case 2: + view.setUint16(offset, view.getUint16(offset, false), true); + break; + case 4: + view.setUint32(offset, view.getUint32(offset, false), true); + break; + case 8: + view.setBigUint64(offset, view.getBigUint64(offset, false), true); + break; + } + } +} + export interface Exporter { exportStub(hook: StubHook): ExportId; exportPromise(hook: StubHook): ExportId; @@ -209,6 +242,11 @@ export class Devaluator { if (bytes === undefined) { let view = value as ArrayBufferView; bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + let elementSize = alternateTypeName && TYPED_ARRAY_ELEMENT_SIZE[alternateTypeName]; + if (!NATIVE_LITTLE_ENDIAN && elementSize) { + bytes = bytes.slice(); + swapByteOrder(bytes, elementSize); + } } let b64: string; @@ -669,7 +707,17 @@ export class Evaluator { throw new TypeError(`Unknown bytes type marker type: ${typeof value[2]}`); } + let elementSize = TYPED_ARRAY_ELEMENT_SIZE[value[2]]; + if (elementSize !== undefined && bytes.byteLength % elementSize !== 0) { + throw new TypeError( + `Invalid byte length ${bytes.byteLength} for ${value[2]}; ` + + `expected a multiple of ${elementSize}`); + } + let buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + if (!NATIVE_LITTLE_ENDIAN && elementSize !== undefined) { + swapByteOrder(new Uint8Array(buffer), elementSize); + } switch (value[2]) { case "ArrayBuffer": return buffer; case "DataView": return new DataView(buffer); From ca13b89cd537c02b113aa6895edce3bc15dd415d Mon Sep 17 00:00:00 2001 From: ttmx Date: Wed, 15 Jul 2026 17:14:45 +0100 Subject: [PATCH 4/4] feat: Enhance byte container support and error handling in serialization --- __tests__/index.test.ts | 9 +++ protocol.md | 2 +- src/serialize.ts | 132 +++++++++++++++++++++++----------------- 3 files changed, 87 insertions(+), 56 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 942caa20..404e2fe8 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -169,6 +169,12 @@ describe("simple serialization", () => { let deserialized = deserialize(serialized) as Uint8Array; expect(deserialized).toBeInstanceOf(Uint8Array); expect(new Uint8Array(deserialized)).toStrictEqual(bytes); + + // Accept the explicit marker from other implementations, while continuing + // to emit the markerless form for backwards compatibility. + let explicitlyTyped = deserialize('["bytes","SGVsbG8","Uint8Array"]'); + expect(Object.getPrototypeOf(explicitlyTyped)).toBe(Uint8Array.prototype); + expect(explicitlyTyped).toStrictEqual(bytes); }) it("can serialize Node.js Buffer as bytes", () => { @@ -261,6 +267,9 @@ describe("simple serialization", () => { swapByteOrder(bytes, elementSize); expect(bytes).toStrictEqual(expected); } + + expect(() => swapByteOrder(new Uint8Array(), 3)).toThrowError( + "Unsupported element size: 3"); }) it("rejects byte lengths that are misaligned for typed arrays", () => { diff --git a/protocol.md b/protocol.md index 9fda5f39..52ce5ac3 100644 --- a/protocol.md +++ b/protocol.md @@ -173,7 +173,7 @@ The values Infinity, -Infinity, and NaN. A byte container, represented as a base64-encoded string. If `type` is omitted, the receiver should deserialize bytes as its default `Uint8Array` for backwards compatibility. Otherwise, `type` preserves the byte container type across the wire. The supported `type` values are -`ArrayBuffer`, `DataView`, `Int8Array`, `Uint8ClampedArray`, +`ArrayBuffer`, `DataView`, `Int8Array`, `Uint8Array`, `Uint8ClampedArray`, `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, `Float32Array`, and `Float64Array`. Multi-byte typed array elements are encoded in little-endian byte order. diff --git a/src/serialize.ts b/src/serialize.ts index 06bf6e8f..d446ab3b 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -11,21 +11,76 @@ export type ExportId = number; const NATIVE_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1; -const TYPED_ARRAY_ELEMENT_SIZE: Record = { +const BYTE_CONTAINER_TYPE_NAMES = [ + "ArrayBuffer", + "DataView", + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "BigInt64Array", + "BigUint64Array", + "Float32Array", + "Float64Array", +] as const; + +type ByteContainerTypeName = typeof BYTE_CONTAINER_TYPE_NAMES[number]; +type MarkedByteContainerTypeName = Exclude; + +function isValidByteContainerName(value: string): value is ByteContainerTypeName { + return (BYTE_CONTAINER_TYPE_NAMES as readonly string[]).includes(value); +} + +// Listing every type, including those without multi-byte elements, makes adding +// a new ByteContainerTypeName a compile error until its element size is considered. +const TYPED_ARRAY_ELEMENT_SIZE: Record = { + ArrayBuffer: undefined, + DataView: undefined, + Int8Array: undefined, + Uint8Array: undefined, + Uint8ClampedArray: undefined, Int16Array: 2, Uint16Array: 2, Int32Array: 4, Uint32Array: 4, - Float32Array: 4, BigInt64Array: 8, BigUint64Array: 8, + Float32Array: 4, Float64Array: 8, }; +// Uint8Array intentionally isn't included because it uses the markerless legacy form. +const BYTE_CONTAINER_PROTOTYPES: Record = { + ArrayBuffer: ArrayBuffer.prototype, + DataView: DataView.prototype, + Int8Array: Int8Array.prototype, + Uint8ClampedArray: Uint8ClampedArray.prototype, + Int16Array: Int16Array.prototype, + Uint16Array: Uint16Array.prototype, + Int32Array: Int32Array.prototype, + Uint32Array: Uint32Array.prototype, + BigInt64Array: BigInt64Array.prototype, + BigUint64Array: BigUint64Array.prototype, + Float32Array: Float32Array.prototype, + Float64Array: Float64Array.prototype, +}; + +const BYTE_CONTAINER_TYPE_BY_PROTOTYPE = new Map(); +for (let type of Object.keys(BYTE_CONTAINER_PROTOTYPES) as MarkedByteContainerTypeName[]) { + BYTE_CONTAINER_TYPE_BY_PROTOTYPE.set(BYTE_CONTAINER_PROTOTYPES[type], type); +} + // Reverse each element's bytes in place. Production callers only need this on // big-endian hosts; it is exported solely so the behavior can be tested on // little-endian hosts. export function swapByteOrder(bytes: Uint8Array, elementSize: number): void { + if (elementSize !== 2 && elementSize !== 4 && elementSize !== 8) { + throw new RangeError(`Unsupported element size: ${elementSize}`); + } + let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); for (let offset = 0; offset < bytes.byteLength; offset += elementSize) { switch (elementSize) { @@ -195,54 +250,16 @@ export class Devaluator { } case "bytes": { - let alternateTypeName: string | undefined; - let bytes: Uint8Array | undefined; - switch (Object.getPrototypeOf(value)) { - case ArrayBuffer.prototype: - alternateTypeName = "ArrayBuffer"; - bytes = new Uint8Array(value as ArrayBuffer); - break; - case DataView.prototype: - alternateTypeName = "DataView"; - break; - case Int8Array.prototype: - alternateTypeName = "Int8Array"; - break; - case Uint8ClampedArray.prototype: - alternateTypeName = "Uint8ClampedArray"; - break; - case Int16Array.prototype: - alternateTypeName = "Int16Array"; - break; - case Uint16Array.prototype: - alternateTypeName = "Uint16Array"; - break; - case Int32Array.prototype: - alternateTypeName = "Int32Array"; - break; - case Uint32Array.prototype: - alternateTypeName = "Uint32Array"; - break; - case BigInt64Array.prototype: - alternateTypeName = "BigInt64Array"; - break; - case BigUint64Array.prototype: - alternateTypeName = "BigUint64Array"; - break; - case Float32Array.prototype: - alternateTypeName = "Float32Array"; - break; - case Float64Array.prototype: - alternateTypeName = "Float64Array"; - break; - default: - bytes = value as Uint8Array; - break; - } - if (bytes === undefined) { + let alternateTypeName = BYTE_CONTAINER_TYPE_BY_PROTOTYPE.get(Object.getPrototypeOf(value)); + let bytes: Uint8Array; + if (alternateTypeName === "ArrayBuffer") { + bytes = new Uint8Array(value as ArrayBuffer); + } else if (alternateTypeName === undefined) { + bytes = value as Uint8Array; + } else { let view = value as ArrayBufferView; bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength); - let elementSize = alternateTypeName && TYPED_ARRAY_ELEMENT_SIZE[alternateTypeName]; + let elementSize = TYPED_ARRAY_ELEMENT_SIZE[alternateTypeName]; if (!NATIVE_LITTLE_ENDIAN && elementSize) { bytes = bytes.slice(); swapByteOrder(bytes, elementSize); @@ -707,21 +724,29 @@ export class Evaluator { throw new TypeError(`Unknown bytes type marker type: ${typeof value[2]}`); } - let elementSize = TYPED_ARRAY_ELEMENT_SIZE[value[2]]; + if (!isValidByteContainerName(value[2])) { + let marker = value[2].slice(0, 64); + throw new TypeError(`Unknown bytes type marker: ${marker}`); + } + + let marker = value[2]; + let elementSize = TYPED_ARRAY_ELEMENT_SIZE[marker]; if (elementSize !== undefined && bytes.byteLength % elementSize !== 0) { throw new TypeError( - `Invalid byte length ${bytes.byteLength} for ${value[2]}; ` + + `Invalid byte length ${bytes.byteLength} for ${marker}; ` + `expected a multiple of ${elementSize}`); } + // Copy exactly the decoded range rather than exposing or aliasing a pooled Buffer. let buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); if (!NATIVE_LITTLE_ENDIAN && elementSize !== undefined) { swapByteOrder(new Uint8Array(buffer), elementSize); } - switch (value[2]) { + switch (marker) { case "ArrayBuffer": return buffer; case "DataView": return new DataView(buffer); case "Int8Array": return new Int8Array(buffer); + case "Uint8Array": return new Uint8Array(buffer); case "Uint8ClampedArray": return new Uint8ClampedArray(buffer); case "Int16Array": return new Int16Array(buffer); case "Uint16Array": return new Uint16Array(buffer); @@ -731,10 +756,7 @@ export class Evaluator { case "BigUint64Array": return new BigUint64Array(buffer); case "Float32Array": return new Float32Array(buffer); case "Float64Array": return new Float64Array(buffer); - default: { - let marker = value[2].slice(0, 64); - throw new TypeError(`Unknown bytes type marker: ${marker}`); - } + default: marker satisfies never; } } break;