Files
onenote-tools/src/onenote/image-safety.test.ts

160 lines
4.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
inspectBrowserImage,
} from './image-safety.js';
describe('browser image safety inspection', () => {
it('accepts bounded PNG and JPEG dimensions', () => {
expect(inspectBrowserImage(png(640, 480))).toMatchObject({
format: 'png',
extension: 'png',
mediaType: 'image/png',
width: 640,
height: 480,
pixels: 307_200,
browserRenderable: true,
});
expect(inspectBrowserImage(jpeg(1_920, 1_080))).toMatchObject({
format: 'jpeg',
extension: 'jpg',
mediaType: 'image/jpeg',
width: 1_920,
height: 1_080,
pixels: 2_073_600,
browserRenderable: true,
});
});
it('rejects huge PNG and JPEG dimensions without losing their type', () => {
expect(inspectBrowserImage(png(0xffff_ffff, 1))).toMatchObject({
format: 'png',
mediaType: 'image/png',
width: 0xffff_ffff,
height: 1,
browserRenderable: false,
rejectionReason: 'dimension-limit',
});
expect(inspectBrowserImage(jpeg(0xffff, 0xffff))).toMatchObject({
format: 'jpeg',
mediaType: 'image/jpeg',
width: 0xffff,
height: 0xffff,
browserRenderable: false,
rejectionReason: 'dimension-limit',
});
});
it('enforces decoded pixel and compressed-byte limits independently', () => {
expect(
inspectBrowserImage(png(8_192, 8_193), {
...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
maxDimension: 10_000,
})
).toMatchObject({
pixels: 67_117_056,
browserRenderable: false,
rejectionReason: 'pixel-limit',
});
expect(
inspectBrowserImage(png(1, 1), {
...DEFAULT_BROWSER_IMAGE_SAFETY_LIMITS,
maxBytes: 32,
})
).toMatchObject({
width: 1,
height: 1,
browserRenderable: false,
rejectionReason: 'byte-limit',
});
});
it('rejects malformed PNG IHDR and JPEG marker streams', () => {
const malformedPng = png(16, 16);
malformedPng[11] = 12;
const malformedJpeg = jpeg(16, 16);
malformedJpeg[4] = 0;
malformedJpeg[5] = 1;
expect(inspectBrowserImage(malformedPng)).toMatchObject({
format: 'png',
browserRenderable: false,
rejectionReason: 'invalid-header',
});
expect(inspectBrowserImage(malformedJpeg)).toMatchObject({
format: 'jpeg',
browserRenderable: false,
rejectionReason: 'invalid-header',
});
expect(inspectBrowserImage(Uint8Array.of(0xff, 0xd8, 0xff))).toMatchObject({
format: 'jpeg',
browserRenderable: false,
rejectionReason: 'invalid-header',
});
});
it('caps a JPEG marker walk that never reaches a frame header', () => {
const bytes = new Uint8Array(2 + 5_000 * 2);
bytes.set([0xff, 0xd8], 0);
for (let offset = 2; offset < bytes.length; offset += 2) {
bytes[offset] = 0xff;
bytes[offset + 1] = 0xd0;
}
expect(inspectBrowserImage(bytes)).toMatchObject({
format: 'jpeg',
browserRenderable: false,
rejectionReason: 'invalid-header',
});
});
});
function png(width: number, height: number): Uint8Array {
const bytes = new Uint8Array(33);
bytes.set([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10], 0);
writeU32Be(bytes, 8, 13);
bytes.set([0x49, 0x48, 0x44, 0x52], 12);
writeU32Be(bytes, 16, width);
writeU32Be(bytes, 20, height);
bytes.set([8, 6, 0, 0, 0], 24);
return bytes;
}
function jpeg(width: number, height: number): Uint8Array {
return Uint8Array.of(
0xff,
0xd8,
0xff,
0xe0,
0x00,
0x02,
0xff,
0xc0,
0x00,
0x11,
0x08,
(height >>> 8) & 0xff,
height & 0xff,
(width >>> 8) & 0xff,
width & 0xff,
0x03,
0x01,
0x11,
0x00,
0x02,
0x11,
0x00,
0x03,
0x11,
0x00
);
}
function writeU32Be(bytes: Uint8Array, offset: number, value: number): void {
bytes[offset] = (value >>> 24) & 0xff;
bytes[offset + 1] = (value >>> 16) & 0xff;
bytes[offset + 2] = (value >>> 8) & 0xff;
bytes[offset + 3] = value & 0xff;
}