Signal-Desktop/ts/util/sniffImageMimeType.ts

97 lines
2.4 KiB
TypeScript

// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MIMEType } from '../types/MIME';
import {
IMAGE_BMP,
IMAGE_GIF,
IMAGE_ICO,
IMAGE_JPEG,
IMAGE_PNG,
IMAGE_WEBP,
} from '../types/MIME';
/**
* This follows the [MIME Sniffing Standard for images][0].
*
* [0]: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
*/
export function sniffImageMimeType(bytes: Uint8Array): undefined | MIMEType {
for (let i = 0; i < TYPES.length; i += 1) {
const type = TYPES[i];
if (matchesType(bytes, type)) {
return type.mimeType;
}
}
return undefined;
}
type Type = {
mimeType: MIMEType;
bytePattern: Uint8Array;
patternMask?: Uint8Array;
};
const TYPES: Array<Type> = [
{
mimeType: IMAGE_ICO,
bytePattern: new Uint8Array([0x00, 0x00, 0x01, 0x00]),
},
{
mimeType: IMAGE_ICO,
bytePattern: new Uint8Array([0x00, 0x00, 0x02, 0x00]),
},
{
mimeType: IMAGE_BMP,
bytePattern: new Uint8Array([0x42, 0x4d]),
},
{
mimeType: IMAGE_GIF,
bytePattern: new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]),
},
{
mimeType: IMAGE_GIF,
bytePattern: new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]),
},
{
mimeType: IMAGE_WEBP,
bytePattern: new Uint8Array([
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
0x56, 0x50,
]),
patternMask: new Uint8Array([
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff,
]),
},
{
mimeType: IMAGE_PNG,
bytePattern: new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]),
},
{
mimeType: IMAGE_JPEG,
bytePattern: new Uint8Array([0xff, 0xd8, 0xff]),
},
];
// This follows the [pattern matching algorithm in the spec][1].
// [1]: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm
function matchesType(input: Uint8Array, type: Type): boolean {
if (input.byteLength < type.bytePattern.byteLength) {
return false;
}
for (let p = 0; p < type.bytePattern.length; p += 1) {
const mask = type.patternMask ? type.patternMask[p] : 0xff;
// We need to use a bitwise operator here, per the spec.
// eslint-disable-next-line no-bitwise
const maskedData = input[p] & mask;
if (maskedData !== type.bytePattern[p]) {
return false;
}
}
return true;
}