diff --git a/packages/opencode/src/util/media.ts b/packages/opencode/src/util/media.ts index 566ac843..580f3ad6 100644 --- a/packages/opencode/src/util/media.ts +++ b/packages/opencode/src/util/media.ts @@ -12,15 +12,25 @@ export function isImageAttachment(mime: string) { return mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" } -export function sniffAttachmentMime(bytes: Uint8Array, fallback: string) { - if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png" - if (startsWith(bytes, [0xff, 0xd8, 0xff])) return "image/jpeg" - if (startsWith(bytes, [0x47, 0x49, 0x46, 0x38])) return "image/gif" - if (startsWith(bytes, [0x42, 0x4d])) return "image/bmp" - if (startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) return "application/pdf" - if (startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50])) { - return "image/webp" - } +interface MimeSignature { + mime: string + matches: (bytes: Uint8Array) => boolean +} - return fallback +const MIME_SIGNATURES: MimeSignature[] = [ + { mime: "image/png", matches: (bytes) => startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) }, + { mime: "image/jpeg", matches: (bytes) => startsWith(bytes, [0xff, 0xd8, 0xff]) }, + { mime: "image/gif", matches: (bytes) => startsWith(bytes, [0x47, 0x49, 0x46, 0x38]) }, + { mime: "image/bmp", matches: (bytes) => startsWith(bytes, [0x42, 0x4d]) }, + { mime: "application/pdf", matches: (bytes) => startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]) }, + { + mime: "image/webp", + matches: (bytes) => + startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50]), + }, +] + +export function sniffAttachmentMime(bytes: Uint8Array, fallback: string) { + const signature = MIME_SIGNATURES.find((candidate) => candidate.matches(bytes)) + return signature?.mime ?? fallback } diff --git a/packages/opencode/test/util/media.test.ts b/packages/opencode/test/util/media.test.ts new file mode 100644 index 00000000..8f3d4932 --- /dev/null +++ b/packages/opencode/test/util/media.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test" +import { + isImageAttachment, + isMedia, + isPdfAttachment, + sniffAttachmentMime, +} from "../../src/util/media" + +describe("util.media", () => { + test("detects PNG from magic bytes", () => { + const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("image/png") + }) + + test("detects JPEG from magic bytes", () => { + const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0x00, 0x00]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("image/jpeg") + }) + + test("detects GIF from magic bytes", () => { + const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x00, 0x00]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("image/gif") + }) + + test("detects BMP from magic bytes", () => { + const bytes = new Uint8Array([0x42, 0x4d, 0x00, 0x00]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("image/bmp") + }) + + test("detects PDF from magic bytes", () => { + const bytes = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x00]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("application/pdf") + }) + + test("detects WEBP from RIFF + WEBP markers", () => { + const bytes = new Uint8Array([0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("image/webp") + }) + + test("does not detect WEBP from RIFF alone without WEBP marker", () => { + const bytes = new Uint8Array([0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]) + expect(sniffAttachmentMime(bytes, "application/octet-stream")).toBe("application/octet-stream") + }) + + test("returns fallback for unrecognized bytes", () => { + const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]) + expect(sniffAttachmentMime(bytes, "text/plain")).toBe("text/plain") + }) + + test("isPdfAttachment matches only application/pdf", () => { + expect(isPdfAttachment("application/pdf")).toBe(true) + expect(isPdfAttachment("image/png")).toBe(false) + }) + + test("isMedia matches images and pdf", () => { + expect(isMedia("image/png")).toBe(true) + expect(isMedia("application/pdf")).toBe(true) + expect(isMedia("text/plain")).toBe(false) + }) + + test("isImageAttachment excludes svg and fastbidsheet", () => { + expect(isImageAttachment("image/png")).toBe(true) + expect(isImageAttachment("image/svg+xml")).toBe(false) + expect(isImageAttachment("image/vnd.fastbidsheet")).toBe(false) + }) +}) \ No newline at end of file