From a62376d09f2ac04b3b917c890805d2c5e9afcd9c Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 10 May 2026 06:55:39 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20improve=20pack=20string=20overfl?= =?UTF-8?q?ow=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops.ts | 24 +++++++++++++++---- specs/renderer-spec.md | 7 ++++++ test/pack.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 test/pack.test.ts diff --git a/ops.ts b/ops.ts index 3344eea..c76d584 100644 --- a/ops.ts +++ b/ops.ts @@ -52,11 +52,27 @@ function packAxis(view: DataView, offset: number, axis: SizingAxis): number { return o; } -function packString(view: DataView, bytes: Uint8Array, o: number): number { +function packString( + view: DataView, + bytes: Uint8Array, + o: number, + end: number, + context: string, +): number { + let paddedLength = Math.ceil(bytes.length / 4) * 4; + let next = o + 4 + paddedLength; + if (next > end) { + throw new RangeError( + `clayterm transfer buffer capacity exceeded while packing ${context} ` + + `(${next} byte offset, ${end} byte limit). ` + + `Render a smaller visible slice or reduce frame content.`, + ); + } + view.setUint32(o, bytes.length, true); o += 4; new Uint8Array(view.buffer).set(bytes, o); - o += Math.ceil(bytes.length / 4) * 4; + o += paddedLength; return o; } @@ -82,7 +98,7 @@ export function pack( o += 4; let bytes = encoder.encode(op.id); - o = packString(view, bytes, o); + o = packString(view, bytes, o, end, "element id"); let mask = 0; if (op.layout) mask |= PROP_LAYOUT; @@ -192,7 +208,7 @@ export function pack( o += 4; let str = encoder.encode(op.content); - o = packString(view, str, o); + o = packString(view, str, o, end, "text content"); break; } } diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..b78848c 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -466,6 +466,13 @@ form that the WASM module can process. This transfer is handled internally by the renderer and is not an operation the caller performs or observes. The transfer mechanism is an implementation detail described in Section 12.1. +If a frame exceeds transfer-buffer capacity while packing string content, the +renderer MUST throw a descriptive `RangeError` that identifies the condition as +a transfer-buffer, frame-capacity, or packing overflow. The renderer MUST NOT +expose only the raw host-level TypedArray message `"offset is out of bounds"` +for this condition. The error message SHOULD direct callers to render a smaller +visible slice or reduce frame content. + ### 9.3 Directive identity Each element directive carries an `id` provided by the caller via `open()`. diff --git a/test/pack.test.ts b/test/pack.test.ts new file mode 100644 index 0000000..9b3bee8 --- /dev/null +++ b/test/pack.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "./suite.ts"; +import { close, open, pack, text } from "../ops.ts"; + +describe("pack", () => { + it("throws a descriptive RangeError when text exceeds the transfer buffer", () => { + let memory = new ArrayBuffer(64); + let error: unknown; + + try { + pack( + [ + open("root"), + text("x".repeat(128)), + close(), + ], + memory, + 0, + memory.byteLength, + ); + } catch (caught) { + error = caught; + } + + expect(error).toBeInstanceOf(RangeError); + expect((error as Error).message).toMatch( + /transfer buffer|capacity|packing/, + ); + expect((error as Error).message).toContain("text content"); + expect((error as Error).message).not.toBe("offset is out of bounds"); + expect((error as Error).message).toMatch( + /smaller visible slice|reduce frame content/, + ); + }); + + it("throws a descriptive RangeError when an element id exceeds the transfer buffer", () => { + let memory = new ArrayBuffer(16); + let error: unknown; + + try { + pack([open("x".repeat(64)), close()], memory, 0, memory.byteLength); + } catch (caught) { + error = caught; + } + + expect(error).toBeInstanceOf(RangeError); + expect((error as Error).message).toMatch( + /transfer buffer|capacity|packing/, + ); + expect((error as Error).message).toContain("element id"); + expect((error as Error).message).not.toBe("offset is out of bounds"); + }); +});