111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import "../setup.ts";
|
|
import { PNG } from "../helpers.ts";
|
|
import { assertEquals, assertThrows } from "@std/assert";
|
|
import {
|
|
detectImageMime,
|
|
scalableImage,
|
|
validateImageUpload,
|
|
} from "../../lib/upload.ts";
|
|
import { APIException } from "../../lib/api-exception.ts";
|
|
|
|
Deno.test("detectImageMime: sniffs magic bytes, not names", () => {
|
|
assertEquals(detectImageMime(PNG), "image/png");
|
|
assertEquals(
|
|
detectImageMime(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])),
|
|
"image/jpeg",
|
|
);
|
|
assertEquals(
|
|
detectImageMime(new TextEncoder().encode("GIF89a")),
|
|
"image/gif",
|
|
);
|
|
assertEquals(
|
|
detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")),
|
|
"image/webp",
|
|
);
|
|
assertEquals(
|
|
detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WAVEfmt ")),
|
|
null,
|
|
);
|
|
assertEquals(
|
|
detectImageMime(new TextEncoder().encode("<svg xmlns=...>")),
|
|
null,
|
|
);
|
|
assertEquals(detectImageMime(new Uint8Array()), null);
|
|
});
|
|
|
|
Deno.test("validateImageUpload: rejects non-images and anything over 5 MB", () => {
|
|
assertEquals(validateImageUpload(PNG), "image/png");
|
|
assertThrows(
|
|
() => validateImageUpload(new TextEncoder().encode("<html>")),
|
|
APIException,
|
|
"not a recognised image",
|
|
);
|
|
const big = new Uint8Array(5 * 1024 * 1024 + 1);
|
|
big.set(PNG);
|
|
assertThrows(() => validateImageUpload(big), APIException, "too large");
|
|
});
|
|
|
|
/** A JPEG's first bytes: SOI, then an EXIF segment stating `orientation`. */
|
|
function jpegWithOrientation(orientation: number, little: boolean): Uint8Array {
|
|
const u16 = (n: number) => little ? [n & 0xff, n >> 8] : [n >> 8, n & 0xff];
|
|
const u32 = (n: number) => little ? [...u16(n), 0, 0] : [0, 0, ...u16(n)];
|
|
const tiff = [
|
|
...(little ? [0x49, 0x49] : [0x4d, 0x4d]),
|
|
...u16(42),
|
|
...u32(8),
|
|
...u16(1),
|
|
...u16(0x0112),
|
|
...u16(3),
|
|
...u32(1),
|
|
...u16(orientation),
|
|
...[0, 0],
|
|
...u32(0),
|
|
];
|
|
const exif = [0x45, 0x78, 0x69, 0x66, 0, 0, ...tiff];
|
|
const length = exif.length + 2;
|
|
return new Uint8Array([
|
|
...[0xff, 0xd8],
|
|
// A JFIF segment first, as a camera's file may have.
|
|
...[0xff, 0xe0, 0x00, 0x04, 0x00, 0x00],
|
|
...[0xff, 0xe1, length >> 8, length & 0xff],
|
|
...exif,
|
|
...[0xff, 0xda],
|
|
]);
|
|
}
|
|
|
|
Deno.test("scalableImage: a JPEG's EXIF orientation, upright without one", () => {
|
|
const turn = (data: Uint8Array) => scalableImage(data)?.orientation;
|
|
assertEquals(turn(jpegWithOrientation(6, false)), 6);
|
|
assertEquals(turn(jpegWithOrientation(8, true)), 8);
|
|
assertEquals(turn(jpegWithOrientation(42, true)), 1);
|
|
assertEquals(turn(new Uint8Array([0xff, 0xd8, 0xff, 0xda, 0, 2])), 1);
|
|
// Cut short of what it announces: upright, not an error.
|
|
assertEquals(turn(jpegWithOrientation(6, false).subarray(0, 20)), 1);
|
|
assertEquals(turn(PNG), 1);
|
|
});
|
|
|
|
Deno.test("scalableImage: an animated WebP and what isn't an image are served as they are", () => {
|
|
const webp = (flags: number) =>
|
|
new Uint8Array([
|
|
...new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8X"),
|
|
...[10, 0, 0, 0],
|
|
flags,
|
|
]);
|
|
assertEquals(scalableImage(webp(0x02)), null);
|
|
// A reader without WebP gets a PNG of what may be transparent, a JPEG of
|
|
// the rest.
|
|
assertEquals(scalableImage(webp(0x10)), { orientation: 1, compat: "png" });
|
|
assertEquals(
|
|
scalableImage(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")),
|
|
{ orientation: 1, compat: "png" },
|
|
);
|
|
assertEquals(scalableImage(PNG), { orientation: 1, compat: "png" });
|
|
assertEquals(scalableImage(jpegWithOrientation(1, true))?.compat, "jpg");
|
|
assertEquals(scalableImage(new TextEncoder().encode("BM-a-bitmap")), {
|
|
orientation: 1,
|
|
compat: "jpg",
|
|
});
|
|
assertEquals(scalableImage(new TextEncoder().encode("<svg/>")), null);
|
|
assertEquals(scalableImage(new Uint8Array()), null);
|
|
});
|