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
|
// @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],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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 }) => (
|
||||||
|
mimeSniffPatterns.some(({ bytes, mask }) => {
|
||||||
// Roughly follows the [pattern matching algorithm from the spec][1].
|
// Roughly follows the [pattern matching algorithm from the spec][1].
|
||||||
// [1]: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm
|
// [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];
|
||||||
console.assert(mimeSniffBytePattern.length === mimeSniffPatternMask.length);
|
if (maskedData !== bytes[p]) return false;
|
||||||
|
|
||||||
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", () => {
|
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;
|
||||||
|
for (const { bytes, mask } of mimeSniffPatterns) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
mimeSniffBytePattern.length,
|
bytes.length,
|
||||||
mimeSniffPatternMask.length,
|
mask.length,
|
||||||
`Pattern and mask for ${name} are not the same size`,
|
`Pattern and mask for ${name} are not the same size`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue