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
30 changes: 20 additions & 10 deletions packages/opencode/src/util/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
66 changes: 66 additions & 0 deletions packages/opencode/test/util/media.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading