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 // @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 * @typedef {object} SupportedFileType
* @prop {string} name The English name of a file type. * @prop {string} name The English name of a file type.
* @prop {string[]} extensions File extension for this type, with the leading dot. * @prop {string[]} extensions File extension for this type, with the leading dot.
* @prop {string} mimeType MIME type for this file type. * @prop {string} mimeType MIME type for this file type.
* @prop {string} route The route to the file type's page. * @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 {MimeSniffPattern[]} mimeSniffPatterns MIME sniffing patterns.
* @prop {ArrayLike<number>} mimeSniffPatternMask The pattern mask to use for MIME sniffing, lifted from the "MIME Sniffing" spec.
*/ */
/** @type {SupportedFileType[]} */ /** @type {SupportedFileType[]} */
@ -17,7 +22,9 @@ export const SUPPORTED_FILE_TYPES = [
extensions: [".png", ".apng"], extensions: [".png", ".apng"],
mimeType: "image/png", mimeType: "image/png",
route: "/png", route: "/png",
mimeSniffBytePattern: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], mimeSniffPatterns: [{
mimeSniffPatternMask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], 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]. * See the ["MIME Sniffing" spec][0].
* [0]: https://mimesniff.spec.whatwg.org * [0]: https://mimesniff.spec.whatwg.org
@ -40,28 +53,23 @@ const fineFileTypeUsingFileName = (name) => {
*/ */
const findFileTypeWithMimeSniffing = async (file) => { const findFileTypeWithMimeSniffing = async (file) => {
// We only need to sample the first few bytes. // We only need to sample the first few bytes.
const sampleLength = SUPPORTED_FILE_TYPES const sampleLength = getLongestMimeSniffPattern();
.map((t) => t.mimeSniffBytePattern.length)
.reduce((a, b) => Math.max(a, b), 0);
const sampleAsBlob = file.slice(0, sampleLength); const sampleAsBlob = file.slice(0, sampleLength);
const sampleAsBuffer = await sampleAsBlob.arrayBuffer(); const sampleAsBuffer = await sampleAsBlob.arrayBuffer();
const sample = new Uint8Array(sampleAsBuffer); const sample = new Uint8Array(sampleAsBuffer);
return SUPPORTED_FILE_TYPES.find((fileType) => { return SUPPORTED_FILE_TYPES.find(({ mimeSniffPatterns }) => (
// Roughly follows the [pattern matching algorithm from the spec][1]. mimeSniffPatterns.some(({ bytes, mask }) => {
// [1]: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm // 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; for (let p = 0; p < bytes.length; p++) {
const maskedData = sample[p] & mask[p];
if (maskedData !== bytes[p]) return false;
}
console.assert(mimeSniffBytePattern.length === mimeSniffPatternMask.length); return true;
})
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;
}
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", () => { Deno.test("MIME sniffing patterns are the same size as their masks", () => {
for (const fileType of SUPPORTED_FILE_TYPES) { for (const fileType of SUPPORTED_FILE_TYPES) {
const { name, mimeSniffBytePattern, mimeSniffPatternMask } = fileType; const { name, mimeSniffPatterns } = fileType;
assertEquals( for (const { bytes, mask } of mimeSniffPatterns) {
mimeSniffBytePattern.length, assertEquals(
mimeSniffPatternMask.length, bytes.length,
`Pattern and mask for ${name} are not the same size`, mask.length,
); `Pattern and mask for ${name} are not the same size`,
);
}
} }
}); });