Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The WebP EXIF chunk scan reads chunk sizes as unsigned, so a crafted or corrupt image can no longer hang the process in an infinite scan loop.
5 changes: 3 additions & 2 deletions packages/coding-agent/src/utils/exif-orientation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function readOrientationFromTiff(bytes: Uint8Array, tiffStart: number): number {
};

const read32 = (pos: number): number => {
if (le) return bytes[pos] | (bytes[pos + 1] << 8) | (bytes[pos + 2] << 16) | (bytes[pos + 3] << 24);
if (le) return (bytes[pos] | (bytes[pos + 1] << 8) | (bytes[pos + 2] << 16) | (bytes[pos + 3] << 24)) >>> 0;
return ((bytes[pos] << 24) | (bytes[pos + 1] << 16) | (bytes[pos + 2] << 8) | bytes[pos + 3]) >>> 0;
};

Expand Down Expand Up @@ -66,8 +66,9 @@ function findWebpTiffOffset(bytes: Uint8Array): number {
let offset = 12;
while (offset + 8 <= bytes.length) {
const chunkId = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
// Unsigned: a high-bit chunk size read as negative would walk the scan backward forever.
const chunkSize =
bytes[offset + 4] | (bytes[offset + 5] << 8) | (bytes[offset + 6] << 16) | (bytes[offset + 7] << 24);
(bytes[offset + 4] | (bytes[offset + 5] << 8) | (bytes[offset + 6] << 16) | (bytes[offset + 7] << 24)) >>> 0;
const dataStart = offset + 8;

if (chunkId === "EXIF") {
Expand Down
18 changes: 18 additions & 0 deletions packages/coding-agent/test/exif-orientation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { applyExifOrientation } from "../src/utils/exif-orientation.js";
import type { PhotonImageType } from "../src/utils/photon.js";

type Photon = typeof import("@silvia-odwyer/photon-node");

describe("exif orientation", () => {
it("terminates the WebP chunk scan when a chunk size has the high bit set", () => {
const bytes = new Uint8Array(20);
bytes.set([0x52, 0x49, 0x46, 0x46], 0); // RIFF
bytes.set([0x57, 0x45, 0x42, 0x50], 8); // WEBP
bytes.set([0x4a, 0x55, 0x4e, 0x4b], 12); // JUNK
bytes.set([0xf8, 0xff, 0xff, 0xff], 16); // chunk size 0xFFFFFFF8: read signed (-8), the scan re-visits this chunk forever
// Orientation resolves to 1, so neither photon nor the image is ever touched.
const image = {} as PhotonImageType;
expect(applyExifOrientation({} as Photon, image, bytes)).toBe(image);
});
});
1 change: 1 addition & 0 deletions packages/tui/.changes/selection-marker-lone-surrogates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Table cell selection markers no longer crash the renderer when cell content contains a lone UTF-16 surrogate; the surrogate is replaced before encoding.
4 changes: 3 additions & 1 deletion packages/tui/src/selection-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ interface TableBounds {
}

function cellMarker(kind: CellMarker["kind"], row: number, column: number, segment: number, content?: string): string {
const encodedContent = content === undefined ? "" : `:${encodeURIComponent(content)}`;
// Lone surrogates would throw in encodeURIComponent; toWellFormed exists on Node>=20, lib is ES2022.
const wellFormed = content as undefined | (string & { toWellFormed(): string });
const encodedContent = wellFormed === undefined ? "" : `:${encodeURIComponent(wellFormed.toWellFormed())}`;
return `${TABLE_MARKER_PREFIX}${kind}:${row}:${column}:${segment}${encodedContent}\x07`;
}

Expand Down
17 changes: 17 additions & 0 deletions packages/tui/test/selection-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import {
extractTableCellSelectionRegions,
markTableCell,
markTableEnd,
markTableStart,
} from "../src/selection-metadata.js";

describe("selection metadata", () => {
it("marks cells containing lone surrogates without throwing", () => {
// The lone surrogate is replaced with U+FFFD, not crashed on.
const line = markTableEnd(markTableStart("") + markTableCell("cell text", 0, 0, 0, "broken \uD800 surrogate"));
const { regions } = extractTableCellSelectionRegions([line], () => ({}));
assert.equal(regions[0]?.content, "broken \uFFFD surrogate");
});
});
Loading