/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const { extname } = require("path"); const db = require("mime-db"); /** * RegExp to match type in RFC 6838 * * type-name = restricted-name * subtype-name = restricted-name * restricted-name = restricted-name-first *126restricted-name-chars * restricted-name-first = ALPHA / DIGIT * restricted-name-chars = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "-" / "^" / "_" * restricted-name-chars =/ "." ; Characters before first dot always specify a facet name * restricted-name-chars =/ "+" ; Characters after last plus always specify a structured syntax suffix * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z * DIGIT = %x30-39 ; 0-9 */ const TYPE_REGEXP = /^ *(([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126})) *(?:;.*)?$/; const extensions = Object.create(null); const types = Object.create(null); // Score RFC facets (see https://tools.ietf.org/html/rfc6838#section-3) /** @type {Record} */ const FACET_SCORES = { "prs.": 100, "x-": 200, "x.": 300, "vnd.": 400, default: 900 }; /** @typedef {"nginx" | "apache" | "iana" | "default"} SourceScore */ // Score mime source (Logic originally from `jshttp/mime-types` module) /** @type {Record} */ const SOURCE_SCORES = { nginx: 10, apache: 20, iana: 40, default: 30 }; /** @type {Record} */ const TYPE_SCORES = { // prefer application/xml over text/xml // prefer application/rtf over text/rtf application: 1, // prefer font/woff over application/font-woff font: 2, // prefer video/mp4 over audio/mp4 over application/mp4 // See https://www.rfc-editor.org/rfc/rfc4337.html#section-2 audio: 2, video: 3, default: 0 }; /** * @param {string} mimeType mime type * @param {SourceScore=} source source * @returns {number} min score */ function mimeScore(mimeType, source = "default") { if (mimeType === "application/octet-stream") { return 0; } const [type, subtype] = mimeType.split("/"); const facet = subtype.replace(/(\.|x-).*/, "$1"); const facetScore = FACET_SCORES[facet] || FACET_SCORES.default; const sourceScore = SOURCE_SCORES[source] || SOURCE_SCORES.default; const typeScore = TYPE_SCORES[type] || TYPE_SCORES.default; // All else being equal prefer shorter types const lengthScore = 1 - mimeType.length / 100; return facetScore + sourceScore + typeScore + lengthScore; } /** * @param {string} ext extension * @param {string} type0 the first type * @param {string} type1 the second type * @returns {string} preferred type */ const preferredType = (ext, type0, type1) => { const score0 = type0 ? mimeScore(type0, db[type0].source) : 0; const score1 = type1 ? mimeScore(type1, db[type1].source) : 0; return score0 > score1 ? type0 : type1; }; /** * @param {Record} extensions extensions * @param {Record} types types */ const populate = (extensions, types) => { for (const type of Object.keys(db)) { const mime = db[type]; const foundExtensions = mime.extensions; if (!foundExtensions || foundExtensions.length === 0) { continue; } // mime -> extensions extensions[type] = foundExtensions; // extension -> mime for (let i = 0; i < foundExtensions.length; i++) { const extension = foundExtensions[i]; types[extension] = preferredType(extension, types[extension], type); } } }; populate(extensions, types); /** * Get the default extension for a MIME type. * @param {string} type type * @returns {undefined | string} resolve extension */ const extension = (type) => { if (!type) { return; } const match = TYPE_REGEXP.exec(type); if (!match) { return; } const possibleExtensions = extensions[match[1].toLowerCase()]; if (!possibleExtensions || possibleExtensions.length === 0) { return; } return possibleExtensions[0]; }; /** * Lookup the MIME type for a file path/extension. * @param {string} path path * @returns {undefined | string} resolved MIME type */ const lookup = (path) => { if (!path) { return; } // get the extension ("ext" or ".ext" or full path) const extension = extname(`x.${path}`).toLowerCase().slice(1); if (!extension) { return; } return types[extension]; }; module.exports = { extension, lookup };