1import { describe, expect, it } from "vitest";
2import {
3 assertMime,
4 jpegWidthHeight,
5 MimeMismatchError,
6 pdfPageCount,
7 pngWidthHeight,
8 sniffMagicBytes,
9 webpWidthHeight,
10 imageDimensions
11} from "../lib/mime.js";
12
13
14
15const JPEG = Buffer.concat([
16 Buffer.from([0xff, 0xd8, 0xff, 0xe0]),
17 Buffer.alloc(16)
18]);
19
20const PNG = Buffer.concat([
21 Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
22 Buffer.from([0, 0, 0, 13]),
23 Buffer.from("IHDR"),
24 Buffer.from([0, 0, 0, 0x78]),
25 Buffer.from([0, 0, 0, 0x80])
26]);
27
28const WEBP = Buffer.concat([
29 Buffer.from("RIFF"),
30 Buffer.from([0, 0, 0, 0]),
31 Buffer.from("WEBP"),
32 Buffer.from("VP8X"),
33 Buffer.from([0]),
34 Buffer.alloc(3),
35 Buffer.from([0x3f, 0, 0]),
36 Buffer.from([0x2f, 0, 0])
37]);
38
39const GIF = Buffer.concat([Buffer.from("GIF89a"), Buffer.alloc(16)]);
40const PDF = Buffer.from("%PDF-1.7\n1 0 obj\n<< /Type /Page >>\nendobj\n<< /Type /Pages >>\n");
41const ZIP = Buffer.concat([Buffer.from([0x50, 0x4b, 0x03, 0x04]), Buffer.alloc(16)]);
42const WEBM = Buffer.concat([Buffer.from([0x1a, 0x45, 0xdf, 0xa3]), Buffer.alloc(16)]);
43const MP4 = Buffer.concat([
44 Buffer.from([0, 0, 0, 24]),
45 Buffer.from("ftypmp42"),
46 Buffer.alloc(8)
47]);
48const UTF16BE = Buffer.concat([Buffer.from([0xfe, 0xff]), Buffer.from([0x00, 0x68, 0x00, 0x69])]);
49const JSONBUF = Buffer.from('{"a":1}');
50const XMLBUF = Buffer.from('<?xml version="1.0"?>');
51const HTMLBUF = Buffer.from("<html><body></body></html>");
52
53
54const JPEG_DIMS = Buffer.concat([
55 Buffer.from([0xff, 0xd8]),
56 Buffer.from([0xff, 0xe0, 0x00, 0x10]),
57 Buffer.alloc(14),
58 Buffer.from([0xff, 0xc0, 0x00, 0x0b, 0x08, 0, 100, 0, 200, 1, 1, 1, 0])
59]);
60
61describe("sniffMagicBytes", () => {
62 it("image/jpeg", () => expect(sniffMagicBytes(JPEG)).toBe("image/jpeg"));
63 it("image/png", () => expect(sniffMagicBytes(PNG)).toBe("image/png"));
64 it("image/webp", () => expect(sniffMagicBytes(WEBP)).toBe("image/webp"));
65 it("image/gif", () => expect(sniffMagicBytes(GIF)).toBe("image/gif"));
66 it("application/pdf", () => expect(sniffMagicBytes(PDF)).toBe("application/pdf"));
67 it("application/zip", () => expect(sniffMagicBytes(ZIP)).toBe("application/zip"));
68 it("video/webm", () => expect(sniffMagicBytes(WEBM)).toBe("video/webm"));
69 it("video/mp4", () => expect(sniffMagicBytes(MP4)).toBe("video/mp4"));
70 it("text/utf16 (BOM)", () => expect(sniffMagicBytes(UTF16BE)).toBe("text/utf16"));
71 it("application/json heuristic", () => expect(sniffMagicBytes(JSONBUF)).toBe("application/json"));
72 it("text/xml", () => expect(sniffMagicBytes(XMLBUF)).toBe("text/xml"));
73 it("text/html", () => expect(sniffMagicBytes(HTMLBUF)).toBe("text/html"));
74 it("plain text → null", () => expect(sniffMagicBytes(Buffer.from("just some text"))).toBe(null));
75 it("empty buffer → null", () => expect(sniffMagicBytes(Buffer.alloc(0))).toBe(null));
76 it("truncated buffer (2 bytes) → null, no throw", () =>
77 expect(sniffMagicBytes(Buffer.from([0x50, 0x4b]))).toBe(null));
78});
79
80describe("assertMime", () => {
81 it("exact match passes", () => {
82 expect(() => assertMime("image/png", "image/png")).not.toThrow();
83 });
84 it("declared octet-stream always passes", () => {
85 expect(() => assertMime("application/octet-stream", "application/pdf")).not.toThrow();
86 });
87 it("text/* family leniency: declared text/plain, sniffed null", () => {
88 expect(() => assertMime("text/plain", null)).not.toThrow();
89 });
90 it("declared text/csv vs sniffed text/csv (family) passes", () => {
91 expect(() => assertMime("text/csv", "text/csv")).not.toThrow();
92 });
93 it("json/xml declared with no magic passes", () => {
94 expect(() => assertMime("application/json", null)).not.toThrow();
95 expect(() => assertMime("application/xml", null)).not.toThrow();
96 });
97 it("mismatch throws MimeMismatchError", () => {
98 expect(() => assertMime("image/png", "application/zip")).toThrow(MimeMismatchError);
99 try {
100 assertMime("image/jpeg", "application/pdf");
101 expect.unreachable();
102 } catch (err) {
103 expect(err).toBeInstanceOf(MimeMismatchError);
104 expect((err as MimeMismatchError).declared).toBe("image/jpeg");
105 expect((err as MimeMismatchError).actual).toBe("application/pdf");
106 }
107 });
108 it("binary declared but content is plain text → throws", () => {
109 expect(() => assertMime("image/png", null)).toThrow(MimeMismatchError);
110 });
111});
112
113describe("image dimension parsers", () => {
114 it("pngWidthHeight reads IHDR", () => {
115 expect(pngWidthHeight(PNG)).toEqual({ width: 120, height: 128 });
116 });
117 it("pngWidthHeight rejects non-PNG", () => {
118 expect(pngWidthHeight(JPEG)).toBeNull();
119 });
120 it("jpegWidthHeight walks to SOF0", () => {
121 expect(jpegWidthHeight(JPEG_DIMS)).toEqual({ width: 200, height: 100 });
122 });
123 it("jpegWidthHeight rejects non-JPEG", () => {
124 expect(jpegWidthHeight(PNG)).toBeNull();
125 });
126 it("webpWidthHeight (VP8X extended)", () => {
127 expect(webpWidthHeight(WEBP)).toEqual({ width: 64, height: 48 });
128 });
129 it("imageDimensions dispatches by mime", () => {
130 expect(imageDimensions(PNG)).toEqual({ width: 120, height: 128 });
131 expect(imageDimensions(JPEG_DIMS)).toEqual({ width: 200, height: 100 });
132 expect(imageDimensions(WEBP)).toEqual({ width: 64, height: 48 });
133 expect(imageDimensions(GIF)).toBeNull();
134 expect(imageDimensions(Buffer.from("text"))).toBeNull();
135 });
136});
137
138describe("pdfPageCount (approximate)", () => {
139 it("counts /Type /Page but not /Type /Pages", () => {
140 const pdf = Buffer.from(
141 "%PDF-1.7\n<< /Type /Page >>\n<< /Type /Page >>\n<< /Type /Pages /Count 2 >>\n"
142 );
143 expect(pdfPageCount(pdf)).toBe(2);
144 });
145 it("returns null when no pages found", () => {
146 expect(pdfPageCount(Buffer.from("%PDF-1.7\nnothing here"))).toBeNull();
147 });
148});
149