Support multiple mime sniff patterns
Not useful for PNG, but useful for GIFs (upcoming).
This commit is contained in:
parent
c0ed212937
commit
c46bb51248
|
@ -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],
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
));
|
||||
};
|
||||
|
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue