repo_description: Node.js JavaScript runtime ✨🐢🚀✨
repo_topics: javascript js linux macos mit node nodejs runtime windows
code: 'use strict';
  
  const {
    FunctionPrototypeCall,
    ObjectDefineProperty,
    RegExpPrototypeExec,
    SafeMap,
    SafeStringPrototypeSearch,
    StringPrototypeCharAt,
    StringPrototypeIndexOf,
    StringPrototypeSlice,
    StringPrototypeToLowerCase,
    SymbolIterator,
  } = primordials;
  const {
    ERR_INVALID_MIME_SYNTAX,
  } = require('internal/errors').codes;
  
  const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g;
  const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g;
  
  const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/;
  const START_ENDING_WHITESPACE = /[\r\n\t ]*$/;
  
  function toASCIILower(str) {
    // eslint-disable-next-line no-control-regex
    if (!/[^\x00-\x7f]/.test(str)) return StringPrototypeToLowerCase(str);
    let result = '';
    for (let i = 0; i < str.length; i++) {
      const char = str[i];
  
      result += char >= 'A' && char <= 'Z' ?
        StringPrototypeToLowerCase(char) :
        char;
    }
    return result;
  }
  
  const SOLIDUS = '/';
  const SEMICOLON = ';';
  
  function parseTypeAndSubtype(str) {
    // Skip only HTTP whitespace from start
    let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
    // read until '/'
    const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position);
    const trimmedType = typeEnd === -1 ?
      StringPrototypeSlice(str, position) :
      StringPrototypeSlice(str, position, typeEnd);
    const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType,
                                                       NOT_HTTP_TOKEN_CODE_POINT);
    if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) {
      throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
    }
    // skip type and '/'
    position = typeEnd + 1;
    const type = toASCIILower(trimmedType);
    // read until ';'
    const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
    const rawSubtype = subtypeEnd === -1 ?
      StringPrototypeSlice(str, position) :
      StringPrototypeSlice(str, position, subtypeEnd);
    position += rawSubtype.length;
    if (subtypeEnd !== -1) {
      // skip ';'
      position += 1;
    }
    const trimmedSubtype = StringPrototypeSlice(
      rawSubtype,
      0,
      SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
    const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
                                                          NOT_HTTP_TOKEN_CODE_POINT);
    if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
      throw new ERR_INVALID_MIME_SYNTAX('subtype', str, invalidSubtypeIndex);
    }
    const subtype = toASCIILower(trimmedSubtype);
    return [
      type,
      subtype,
      position,
    ];
  }
  
  const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
  const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u;
  
  function removeBackslashes(str) {
    let ret = '';
    // We stop at str.length - 1 because we want to look ahead one character.
    let i;
    for (i = 0; i < str.length - 1; i++) {
      const c = str[i];
      if (c === '\\') {
        i++;
        ret += str[i];
      } else {
        ret += c;
      }
    }
    // We add the last character if we didn't skip to it.
    if (i === str.length - 1) {
      ret += str[i];
    }
    return ret;
  }
  
  
  function escapeQuoteOrSolidus(str) {
    let result = '';
    for (let i = 0; i < str.length; i++) {
      const char = str[i];
      result += (char === '"' || char === '\\') ? `\\${char}` : char;
    }
    return result;
  }
  
  const encode = (value) => {
    if (value.length === 0) return '""';
    const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1;
    if (!encode) return value;
    const escaped = escapeQuoteOrSolidus(value);
    return `"${escaped}"`;
  };
  
  class MIMEParams {
    #data = new SafeMap();
    // We set the flag the MIMEParams instance as processed on initialization
    // to defer the parsing of a potentially large string.
    #processed = true;
    #string = null;
  
    /**
     * Used to instantiate a MIMEParams object within the MIMEType class and
     * to allow it to be parsed lazily.
     * @returns {MIMEParams}
     */
    static instantiateMimeParams(str) {
      const instance = new MIMEParams();
      instance.#string = str;
      instance.#processed = false;
      return instance;
    }
  
    /**
     * @param {string} name
     * @returns {void}
     */
    delete(name) {
      this.#parse();
      this.#data.delete(name);
    }
  
    get(name) {
      this.#parse();
      const data = this.#data;
      if (data.has(name)) {
        return data.get(name);
      }
      return null;
    }
  
    has(name) {
      this.#parse();
      return this.#data.has(name);
    }
  
    set(name, value) {
      this.#parse();
      const data = this.#data;
      name = `${name}`;
      value = `${value}`;
      const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
      if (name.length === 0 || invalidNameIndex !== -1) {
        throw new ERR_INVALID_MIME_SYNTAX(
          'parameter name',
          name,
          invalidNameIndex,
        );
      }
      const invalidValueIndex = SafeStringPrototypeSearch(
        value,
        NOT_HTTP_QUOTED_STRING_CODE_POINT);
      if (invalidValueIndex !== -1) {
        throw new ERR_INVALID_MIME_SYNTAX(
          'parameter value',
          value,
          invalidValueIndex,
        );
      }
      data.set(name, value);
    }
  
    *entries() {
      this.#parse();
      yield* this.#data.entries();
    }
  
    *keys() {
      this.#parse();
      yield* this.#data.keys();
    }
  
    *values() {
      this.#parse();
      yield* this.#data.values();
    }
  
    toString() {
      this.#parse();
      let ret = '';
      for (const { 0: key, 1: value } of this.#data) {
        const encoded = encode(value);
        // Ensure they are separated
        if (ret.length) ret += ';';
        ret += `${key}=${encoded}`;
      }
      return ret;
    }
  
    // Used to act as a friendly class to stringifying stuff
    // not meant to be exposed to users, could inject invalid values
    #parse() {
      if (this.#processed) return;  // already parsed
      const paramsMap = this.#data;
      let position = 0;
      const str = this.#string;
      const endOfSource = SafeStringPrototypeSearch(
        StringPrototypeSlice(str, position),
        START_ENDING_WHITESPACE,
      ) + position;
      while (position < endOfSource) {
        // Skip any whitespace before parameter
        position += SafeStringPrototypeSearch(
          StringPrototypeSlice(str, position),
          END_BEGINNING_WHITESPACE,
        );
        // Read until ';' or '='
        const afterParameterName = SafeStringPrototypeSearch(
          StringPrototypeSlice(str, position),
          EQUALS_SEMICOLON_OR_END,
        ) + position;
        const parameterString = toASCIILower(
          StringPrototypeSlice(str, position, afterParameterName),
        );
        position = afterParameterName;
        // If we found a terminating character
        if (position < endOfSource) {
          // Safe to use because we never do special actions for surrogate pairs
          const char = StringPrototypeCharAt(str, position);
          // Skip the terminating character
          position += 1;
          // Ignore parameters without values
          if (char === ';') {
            continue;
          }
        }
        // If we are at end of the string, it cannot have a value
        if (position >= endOfSource) break;
        // Safe to use because we never do special actions for surrogate pairs
        const char = StringPrototypeCharAt(str, position);
        let parameterValue = null;
        if (char === '"') {
          // Handle quoted-string form of values
          // skip '"'
          position += 1;
          // Find matching closing '"' or end of string
          //   use $1 to see if we terminated on unmatched '\'
          //   use $2 to see if we terminated on a matching '"'
          //   so we can skip the last char in either case
          const insideMatch = RegExpPrototypeExec(
            QUOTED_VALUE_PATTERN,
            StringPrototypeSlice(str, position));
          position += insideMatch[0].length;
          // Skip including last character if an unmatched '\' or '"' during
          // unescape
          const inside = insideMatch[1] || insideMatch[2] ?
            StringPrototypeSlice(insideMatch[0], 0, -1) :
            insideMatch[0];
          // Unescape '\' quoted characters
          parameterValue = removeBackslashes(inside);
          // If we did have an unmatched '\' add it back to the end
          if (insideMatch[1]) parameterValue += '\\';
        } else {
          // Handle the normal parameter value form
          const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
          const rawValue = valueEnd === -1 ?
            StringPrototypeSlice(str, position) :
            StringPrototypeSlice(str, position, valueEnd);
          position += rawValue.length;
          const trimmedValue = StringPrototypeSlice(
            rawValue,
            0,
            SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE),
          );
          // Ignore parameters without values
          if (trimmedValue === '') continue;
          parameterValue = trimmedValue;
        }
        if (
          parameterString !== '' &&
          SafeStringPrototypeSearch(parameterString,
                                    NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
          SafeStringPrototypeSearch(parameterValue,
                                    NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
          paramsMap.has(parameterString) === false
        ) {
          paramsMap.set(parameterString, parameterValue);
        }
        position++;
      }
      this.#data = paramsMap;
      this.#processed = true;
    }
  }
  const MIMEParamsStringify = MIMEParams.prototype.toString;
  ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, {
    __proto__: null,
    configurable: true,
    value: MIMEParams.prototype.entries,
    writable: true,
  });
  ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
    __proto__: null,
    configurable: true,
    value: MIMEParamsStringify,
    writable: true,
  });
  
  const { instantiateMimeParams } = MIMEParams;
  delete MIMEParams.instantiateMimeParams;
  
  class MIMEType {
    #type;
    #subtype;
    #parameters;
    constructor(string) {
      string = `${string}`;
      const data = parseTypeAndSubtype(string);
      this.#type = data[0];
      this.#subtype = data[1];
      this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
    }
  
    get type() {
      return this.#type;
    }
  
    set type(v) {
      v = `${v}`;
      const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
      if (v.length === 0 || invalidTypeIndex !== -1) {
        throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
      }
      this.#type = toASCIILower(v);
    }
  
    get subtype() {
      return this.#subtype;
    }
  
    set subtype(v) {
      v = `${v}`;
      const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
      if (v.length === 0 || invalidSubtypeIndex !== -1) {
        throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
      }
      this.#subtype = toASCIILower(v);
    }
  
    get essence() {
      return `${this.#type}/${this.#subtype}`;
    }
  
    get params() {
      return this.#parameters;
    }
  
    toString() {
      let ret = `${this.#type}/${this.#subtype}`;
      const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters);
      if (paramStr.length) ret += `;${paramStr}`;
      return ret;
    }
  }
  ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
    __proto__: null,
    configurable: true,
    value: MIMEType.prototype.toString,
    writable: true,
  });
  
  module.exports = {
    MIMEParams,
    MIMEType,
  };
  