From c46bb512483457d65e545b60106aea87c2fb6568 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Fri, 11 Aug 2023 14:58:47 -0500 Subject: [PATCH] Support multiple mime sniff patterns Not useful for PNG, but useful for GIFs (upcoming). --- public/index/constants.js | 15 ++++++++---- public/index/routeFile.js | 44 +++++++++++++++++++++--------------- test/index/constants.test.ts | 14 +++++++----- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/public/index/constants.js b/public/index/constants.js index 66316e9..b2a6970 100644 --- a/public/index/constants.js +++ b/public/index/constants.js @@ -1,13 +1,18 @@ // @ts-check +/** + * @typedef {object} MimeSniffPattern + * @prop {ArrayLike} bytes The byte pattern to use for MIME sniffing, lifted from the "MIME Sniffing" spec. + * @prop {ArrayLike} 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} mimeSniffBytePattern The byte pattern to use for MIME sniffing, lifted from the "MIME Sniffing" spec. - * @prop {ArrayLike} 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], + }], }, ]; diff --git a/public/index/routeFile.js b/public/index/routeFile.js index 9cd6e98..1f00329 100644 --- a/public/index/routeFile.js +++ b/public/index/routeFile.js @@ -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) => { - // Roughly follows the [pattern matching algorithm from the spec][1]. - // [1]: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm + 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; + 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); - - 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; - }); + return true; + }) + )); }; diff --git a/test/index/constants.test.ts b/test/index/constants.test.ts index 6c981b6..2eb6798 100644 --- a/test/index/constants.test.ts +++ b/test/index/constants.test.ts @@ -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; - assertEquals( - mimeSniffBytePattern.length, - mimeSniffPatternMask.length, - `Pattern and mask for ${name} are not the same size`, - ); + const { name, mimeSniffPatterns } = fileType; + for (const { bytes, mask } of mimeSniffPatterns) { + assertEquals( + bytes.length, + mask.length, + `Pattern and mask for ${name} are not the same size`, + ); + } } });