Support multiple mime sniff patterns

Not useful for PNG, but useful for GIFs (upcoming).
This commit is contained in:
Evan Hahn 2023-08-11 14:58:47 -05:00
parent c0ed212937
commit c46bb51248
3 changed files with 45 additions and 28 deletions

View File

@ -1,13 +1,18 @@
// @ts-check
/**
* @typedef {object} MimeSniffPattern
* @prop {ArrayLike<number>} bytes The byte pattern to use for MIME sniffing, lifted from the "MIME Sniffing" spec.
* @prop {ArrayLike<number>} mask The pattern mask to use for MIME sniffing, lifted from the "MIME Sniffing" spec.
*/
/**
* @typedef {object} SupportedFileType
* @prop {string} name The English name of a file type.
* @prop {string[]} extensions File extension for this type, with the leading dot.
* @prop {string} mimeType MIME type for this file type.
* @prop {string} route The route to the file type's page.
* @prop {ArrayLike<number>} mimeSniffBytePattern The byte pattern to use for MIME sniffing, lifted from the "MIME Sniffing" spec.
* @prop {ArrayLike<number>} mimeSniffPatternMask The pattern mask to use for MIME sniffing, lifted from the "MIME Sniffing" spec.
* @prop {MimeSniffPattern[]} mimeSniffPatterns MIME sniffing patterns.
*/
/** @type {SupportedFileType[]} */
@ -17,7 +22,9 @@ export const SUPPORTED_FILE_TYPES = [
extensions: [".png", ".apng"],
mimeType: "image/png",
route: "/png",
mimeSniffBytePattern: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
mimeSniffPatternMask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
mimeSniffPatterns: [{
bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
}],
},
];

View File

@ -33,6 +33,19 @@ const fineFileTypeUsingFileName = (name) => {
);
};
/**
* @returns {number}
*/
const getLongestMimeSniffPattern = () => {
let result = 0;
for (const fileType of SUPPORTED_FILE_TYPES) {
for (const pattern of fileType.mimeSniffPatterns) {
result = Math.max(result, pattern.bytes.length);
}
}
return result;
};
/**
* See the ["MIME Sniffing" spec][0].
* [0]: https://mimesniff.spec.whatwg.org
@ -40,28 +53,23 @@ const fineFileTypeUsingFileName = (name) => {
*/
const findFileTypeWithMimeSniffing = async (file) => {
// We only need to sample the first few bytes.
const sampleLength = SUPPORTED_FILE_TYPES
.map((t) => t.mimeSniffBytePattern.length)
.reduce((a, b) => Math.max(a, b), 0);
const sampleLength = getLongestMimeSniffPattern();
const sampleAsBlob = file.slice(0, sampleLength);
const sampleAsBuffer = await sampleAsBlob.arrayBuffer();
const sample = new Uint8Array(sampleAsBuffer);
return SUPPORTED_FILE_TYPES.find((fileType) => {
return SUPPORTED_FILE_TYPES.find(({ mimeSniffPatterns }) => (
mimeSniffPatterns.some(({ bytes, mask }) => {
// Roughly follows the [pattern matching algorithm from the spec][1].
// [1]: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm
if (sample.length < bytes.length) return false;
const { mimeSniffBytePattern, mimeSniffPatternMask } = fileType;
console.assert(mimeSniffBytePattern.length === mimeSniffPatternMask.length);
if (sample.length < mimeSniffBytePattern.length) return false;
for (let p = 0; p < mimeSniffBytePattern.length; p++) {
const maskedData = sample[p] & mimeSniffPatternMask[p];
if (maskedData !== mimeSniffBytePattern[p]) return false;
for (let p = 0; p < bytes.length; p++) {
const maskedData = sample[p] & mask[p];
if (maskedData !== bytes[p]) return false;
}
return true;
});
})
));
};

View File

@ -12,11 +12,13 @@ Deno.test("no duplicate extensions in supported file types", () => {
Deno.test("MIME sniffing patterns are the same size as their masks", () => {
for (const fileType of SUPPORTED_FILE_TYPES) {
const { name, mimeSniffBytePattern, mimeSniffPatternMask } = fileType;
const { name, mimeSniffPatterns } = fileType;
for (const { bytes, mask } of mimeSniffPatterns) {
assertEquals(
mimeSniffBytePattern.length,
mimeSniffPatternMask.length,
bytes.length,
mask.length,
`Pattern and mask for ${name} are not the same size`,
);
}
}
});