Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
7 changes: 7 additions & 0 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
52 changes: 52 additions & 0 deletions test/pack.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading