mirror of
https://github.com/denoland/deno
synced 2024-08-25 11:07:24 +00:00
450 lines
10 KiB
JavaScript
450 lines
10 KiB
JavaScript
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
// @ts-check
|
|
/// <reference path="../../core/internal.d.ts" />
|
|
/// <reference path="../../core/lib.deno_core.d.ts" />
|
|
/// <reference path="../web/internal.d.ts" />
|
|
/// <reference path="../web/lib.deno_web.d.ts" />
|
|
|
|
import { primordials } from "ext:core/mod.js";
|
|
const {
|
|
ArrayPrototypeIncludes,
|
|
MapPrototypeGet,
|
|
MapPrototypeHas,
|
|
MapPrototypeSet,
|
|
RegExpPrototypeTest,
|
|
RegExpPrototypeExec,
|
|
SafeMap,
|
|
SafeMapIterator,
|
|
StringPrototypeReplaceAll,
|
|
StringPrototypeToLowerCase,
|
|
StringPrototypeEndsWith,
|
|
Uint8Array,
|
|
TypedArrayPrototypeGetLength,
|
|
TypedArrayPrototypeIncludes,
|
|
} = primordials;
|
|
|
|
import {
|
|
assert,
|
|
collectHttpQuotedString,
|
|
collectSequenceOfCodepoints,
|
|
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
|
|
HTTP_TOKEN_CODE_POINT_RE,
|
|
HTTP_WHITESPACE,
|
|
HTTP_WHITESPACE_PREFIX_RE,
|
|
HTTP_WHITESPACE_SUFFIX_RE,
|
|
} from "ext:deno_web/00_infra.js";
|
|
|
|
/**
|
|
* @typedef MimeType
|
|
* @property {string} type
|
|
* @property {string} subtype
|
|
* @property {Map<string,string>} parameters
|
|
*/
|
|
|
|
/**
|
|
* @param {string} input
|
|
* @returns {MimeType | null}
|
|
*/
|
|
function parseMimeType(input) {
|
|
// 1.
|
|
input = StringPrototypeReplaceAll(input, HTTP_WHITESPACE_PREFIX_RE, "");
|
|
input = StringPrototypeReplaceAll(input, HTTP_WHITESPACE_SUFFIX_RE, "");
|
|
|
|
// 2.
|
|
let position = 0;
|
|
const endOfInput = input.length;
|
|
|
|
// 3.
|
|
const res1 = collectSequenceOfCodepoints(
|
|
input,
|
|
position,
|
|
(c) => c != "\u002F",
|
|
);
|
|
const type = res1.result;
|
|
position = res1.position;
|
|
|
|
// 4.
|
|
if (type === "" || !RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, type)) {
|
|
return null;
|
|
}
|
|
|
|
// 5.
|
|
if (position >= endOfInput) return null;
|
|
|
|
// 6.
|
|
position++;
|
|
|
|
// 7.
|
|
const res2 = collectSequenceOfCodepoints(
|
|
input,
|
|
position,
|
|
(c) => c != "\u003B",
|
|
);
|
|
let subtype = res2.result;
|
|
position = res2.position;
|
|
|
|
// 8.
|
|
subtype = StringPrototypeReplaceAll(subtype, HTTP_WHITESPACE_SUFFIX_RE, "");
|
|
|
|
// 9.
|
|
if (
|
|
subtype === "" || !RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, subtype)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// 10.
|
|
const mimeType = {
|
|
type: StringPrototypeToLowerCase(type),
|
|
subtype: StringPrototypeToLowerCase(subtype),
|
|
/** @type {Map<string, string>} */
|
|
parameters: new SafeMap(),
|
|
};
|
|
|
|
// 11.
|
|
while (position < endOfInput) {
|
|
// 11.1.
|
|
position++;
|
|
|
|
// 11.2.
|
|
const res1 = collectSequenceOfCodepoints(
|
|
input,
|
|
position,
|
|
(c) => ArrayPrototypeIncludes(HTTP_WHITESPACE, c),
|
|
);
|
|
position = res1.position;
|
|
|
|
// 11.3.
|
|
const res2 = collectSequenceOfCodepoints(
|
|
input,
|
|
position,
|
|
(c) => c !== "\u003B" && c !== "\u003D",
|
|
);
|
|
let parameterName = res2.result;
|
|
position = res2.position;
|
|
|
|
// 11.4.
|
|
parameterName = StringPrototypeToLowerCase(parameterName);
|
|
|
|
// 11.5.
|
|
if (position < endOfInput) {
|
|
if (input[position] == "\u003B") continue;
|
|
position++;
|
|
}
|
|
|
|
// 11.6.
|
|
if (position >= endOfInput) break;
|
|
|
|
// 11.7.
|
|
let parameterValue = null;
|
|
|
|
// 11.8.
|
|
if (input[position] === "\u0022") {
|
|
// 11.8.1.
|
|
const res = collectHttpQuotedString(input, position, true);
|
|
parameterValue = res.result;
|
|
position = res.position;
|
|
|
|
// 11.8.2.
|
|
position++;
|
|
} else { // 11.9.
|
|
// 11.9.1.
|
|
const res = collectSequenceOfCodepoints(
|
|
input,
|
|
position,
|
|
(c) => c !== "\u003B",
|
|
);
|
|
parameterValue = res.result;
|
|
position = res.position;
|
|
|
|
// 11.9.2.
|
|
parameterValue = StringPrototypeReplaceAll(
|
|
parameterValue,
|
|
HTTP_WHITESPACE_SUFFIX_RE,
|
|
"",
|
|
);
|
|
|
|
// 11.9.3.
|
|
if (parameterValue === "") continue;
|
|
}
|
|
|
|
// 11.10.
|
|
if (
|
|
parameterName !== "" &&
|
|
RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, parameterName) &&
|
|
RegExpPrototypeTest(
|
|
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
|
|
parameterValue,
|
|
) &&
|
|
!MapPrototypeHas(mimeType.parameters, parameterName)
|
|
) {
|
|
MapPrototypeSet(mimeType.parameters, parameterName, parameterValue);
|
|
}
|
|
}
|
|
|
|
// 12.
|
|
return mimeType;
|
|
}
|
|
|
|
/**
|
|
* @param {MimeType} mimeType
|
|
* @returns {string}
|
|
*/
|
|
function essence(mimeType) {
|
|
return `${mimeType.type}/${mimeType.subtype}`;
|
|
}
|
|
|
|
/**
|
|
* @param {MimeType} mimeType
|
|
* @returns {string}
|
|
*/
|
|
function serializeMimeType(mimeType) {
|
|
let serialization = essence(mimeType);
|
|
for (const param of new SafeMapIterator(mimeType.parameters)) {
|
|
serialization += `;${param[0]}=`;
|
|
let value = param[1];
|
|
if (RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, value) === null) {
|
|
value = StringPrototypeReplaceAll(value, "\\", "\\\\");
|
|
value = StringPrototypeReplaceAll(value, '"', '\\"');
|
|
value = `"${value}"`;
|
|
}
|
|
serialization += value;
|
|
}
|
|
return serialization;
|
|
}
|
|
|
|
/**
|
|
* Part of the Fetch spec's "extract a MIME type" algorithm
|
|
* (https://fetch.spec.whatwg.org/#concept-header-extract-mime-type).
|
|
* @param {string[] | null} headerValues The result of getting, decoding and
|
|
* splitting the "Content-Type" header.
|
|
* @returns {MimeType | null}
|
|
*/
|
|
function extractMimeType(headerValues) {
|
|
if (headerValues === null) return null;
|
|
|
|
let charset = null;
|
|
let essence_ = null;
|
|
let mimeType = null;
|
|
for (let i = 0; i < headerValues.length; ++i) {
|
|
const value = headerValues[i];
|
|
const temporaryMimeType = parseMimeType(value);
|
|
if (
|
|
temporaryMimeType === null ||
|
|
essence(temporaryMimeType) == "*/*"
|
|
) {
|
|
continue;
|
|
}
|
|
mimeType = temporaryMimeType;
|
|
if (essence(mimeType) !== essence_) {
|
|
charset = null;
|
|
const newCharset = MapPrototypeGet(mimeType.parameters, "charset");
|
|
if (newCharset !== undefined) {
|
|
charset = newCharset;
|
|
}
|
|
essence_ = essence(mimeType);
|
|
} else {
|
|
if (
|
|
!MapPrototypeHas(mimeType.parameters, "charset") &&
|
|
charset !== null
|
|
) {
|
|
MapPrototypeSet(mimeType.parameters, "charset", charset);
|
|
}
|
|
}
|
|
}
|
|
return mimeType;
|
|
}
|
|
|
|
/**
|
|
* Ref: https://mimesniff.spec.whatwg.org/#xml-mime-type
|
|
* @param {MimeType} mimeType
|
|
* @returns {boolean}
|
|
*/
|
|
function isXML(mimeType) {
|
|
return StringPrototypeEndsWith(mimeType.subtype, "+xml") ||
|
|
essence(mimeType) === "text/xml" || essence(mimeType) === "application/xml";
|
|
}
|
|
|
|
/**
|
|
* Ref: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm
|
|
* @param {Uint8Array} input
|
|
* @param {Uint8Array} pattern
|
|
* @param {Uint8Array} mask
|
|
* @param {Uint8Array} ignored
|
|
* @returns {boolean}
|
|
*/
|
|
function patternMatchingAlgorithm(input, pattern, mask, ignored) {
|
|
assert(
|
|
TypedArrayPrototypeGetLength(pattern) ===
|
|
TypedArrayPrototypeGetLength(mask),
|
|
);
|
|
|
|
if (
|
|
TypedArrayPrototypeGetLength(input) < TypedArrayPrototypeGetLength(pattern)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let s = 0;
|
|
for (; s < TypedArrayPrototypeGetLength(input); s++) {
|
|
if (!TypedArrayPrototypeIncludes(ignored, input[s])) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let p = 0;
|
|
for (; p < TypedArrayPrototypeGetLength(pattern); p++, s++) {
|
|
const maskedData = input[s] & mask[p];
|
|
if (maskedData !== pattern[p]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const ImageTypePatternTable = [
|
|
// A Windows Icon signature.
|
|
[
|
|
new Uint8Array([0x00, 0x00, 0x01, 0x00]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/x-icon",
|
|
],
|
|
// A Windows Cursor signature.
|
|
[
|
|
new Uint8Array([0x00, 0x00, 0x02, 0x00]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/x-icon",
|
|
],
|
|
// The string "BM", a BMP signature.
|
|
[
|
|
new Uint8Array([0x42, 0x4D]),
|
|
new Uint8Array([0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/bmp",
|
|
],
|
|
// The string "GIF87a", a GIF signature.
|
|
[
|
|
new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/gif",
|
|
],
|
|
// The string "GIF89a", a GIF signature.
|
|
[
|
|
new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/gif",
|
|
],
|
|
// The string "RIFF" followed by four bytes followed by the string "WEBPVP".
|
|
[
|
|
new Uint8Array([
|
|
0x52,
|
|
0x49,
|
|
0x46,
|
|
0x46,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x57,
|
|
0x45,
|
|
0x42,
|
|
0x50,
|
|
0x56,
|
|
0x50,
|
|
]),
|
|
new Uint8Array([
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
0xFF,
|
|
]),
|
|
new Uint8Array(),
|
|
"image/webp",
|
|
],
|
|
// An error-checking byte followed by the string "PNG" followed by CR LF SUB LF, the PNG signature.
|
|
[
|
|
new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/png",
|
|
],
|
|
// The JPEG Start of Image marker followed by the indicator byte of another marker.
|
|
[
|
|
new Uint8Array([0xFF, 0xD8, 0xFF]),
|
|
new Uint8Array([0xFF, 0xFF, 0xFF]),
|
|
new Uint8Array(),
|
|
"image/jpeg",
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Ref: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm
|
|
* @param {Uint8Array} input
|
|
* @returns {string | undefined}
|
|
*/
|
|
function imageTypePatternMatchingAlgorithm(input) {
|
|
for (let i = 0; i < ImageTypePatternTable.length; i++) {
|
|
const row = ImageTypePatternTable[i];
|
|
const patternMatched = patternMatchingAlgorithm(
|
|
input,
|
|
row[0],
|
|
row[1],
|
|
row[2],
|
|
);
|
|
if (patternMatched) {
|
|
return row[3];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Ref: https://mimesniff.spec.whatwg.org/#rules-for-sniffing-images-specifically
|
|
* @param {string} mimeTypeString
|
|
* @returns {string}
|
|
*/
|
|
function sniffImage(mimeTypeString) {
|
|
const mimeType = parseMimeType(mimeTypeString);
|
|
if (mimeType === null) {
|
|
return mimeTypeString;
|
|
}
|
|
|
|
if (isXML(mimeType)) {
|
|
return mimeTypeString;
|
|
}
|
|
|
|
const imageTypeMatched = imageTypePatternMatchingAlgorithm(
|
|
new TextEncoder().encode(mimeTypeString),
|
|
);
|
|
if (imageTypeMatched !== undefined) {
|
|
return imageTypeMatched;
|
|
}
|
|
|
|
return mimeTypeString;
|
|
}
|
|
|
|
export {
|
|
essence,
|
|
extractMimeType,
|
|
parseMimeType,
|
|
serializeMimeType,
|
|
sniffImage,
|
|
};
|