• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  FunctionPrototypeCall,
5  ObjectDefineProperty,
6  RegExpPrototypeExec,
7  SafeMap,
8  SafeStringPrototypeSearch,
9  StringPrototypeCharAt,
10  StringPrototypeIndexOf,
11  StringPrototypeSlice,
12  StringPrototypeToLowerCase,
13  SymbolIterator,
14} = primordials;
15const {
16  ERR_INVALID_MIME_SYNTAX,
17} = require('internal/errors').codes;
18
19const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g;
20const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g;
21
22const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/;
23const START_ENDING_WHITESPACE = /[\r\n\t ]*$/;
24
25function toASCIILower(str) {
26  let result = '';
27  for (let i = 0; i < str.length; i++) {
28    const char = str[i];
29
30    result += char >= 'A' && char <= 'Z' ?
31      StringPrototypeToLowerCase(char) :
32      char;
33  }
34  return result;
35}
36
37const SOLIDUS = '/';
38const SEMICOLON = ';';
39function parseTypeAndSubtype(str) {
40  // Skip only HTTP whitespace from start
41  let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
42  // read until '/'
43  const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position);
44  const trimmedType = typeEnd === -1 ?
45    StringPrototypeSlice(str, position) :
46    StringPrototypeSlice(str, position, typeEnd);
47  const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType,
48                                                     NOT_HTTP_TOKEN_CODE_POINT);
49  if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) {
50    throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
51  }
52  // skip type and '/'
53  position = typeEnd + 1;
54  const type = toASCIILower(trimmedType);
55  // read until ';'
56  const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
57  const rawSubtype = subtypeEnd === -1 ?
58    StringPrototypeSlice(str, position) :
59    StringPrototypeSlice(str, position, subtypeEnd);
60  position += rawSubtype.length;
61  if (subtypeEnd !== -1) {
62    // skip ';'
63    position += 1;
64  }
65  const trimmedSubtype = StringPrototypeSlice(
66    rawSubtype,
67    0,
68    SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
69  const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
70                                                        NOT_HTTP_TOKEN_CODE_POINT);
71  if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
72    throw new ERR_INVALID_MIME_SYNTAX('subtype', str, trimmedSubtype);
73  }
74  const subtype = toASCIILower(trimmedSubtype);
75  return {
76    __proto__: null,
77    type,
78    subtype,
79    parametersStringIndex: position,
80  };
81}
82
83const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
84const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u;
85
86function removeBackslashes(str) {
87  let ret = '';
88  // We stop at str.length - 1 because we want to look ahead one character.
89  let i;
90  for (i = 0; i < str.length - 1; i++) {
91    const c = str[i];
92    if (c === '\\') {
93      i++;
94      ret += str[i];
95    } else {
96      ret += c;
97    }
98  }
99  // We add the last character if we didn't skip to it.
100  if (i === str.length - 1) {
101    ret += str[i];
102  }
103  return ret;
104}
105
106
107function escapeQuoteOrSolidus(str) {
108  let result = '';
109  for (let i = 0; i < str.length; i++) {
110    const char = str[i];
111    result += (char === '"' || char === '\\') ? `\\${char}` : char;
112  }
113  return result;
114}
115
116const encode = (value) => {
117  if (value.length === 0) return '""';
118  const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1;
119  if (!encode) return value;
120  const escaped = escapeQuoteOrSolidus(value);
121  return `"${escaped}"`;
122};
123
124class MIMEParams {
125  #data = new SafeMap();
126
127  delete(name) {
128    this.#data.delete(name);
129  }
130
131  get(name) {
132    const data = this.#data;
133    if (data.has(name)) {
134      return data.get(name);
135    }
136    return null;
137  }
138
139  has(name) {
140    return this.#data.has(name);
141  }
142
143  set(name, value) {
144    const data = this.#data;
145    name = `${name}`;
146    value = `${value}`;
147    const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
148    if (name.length === 0 || invalidNameIndex !== -1) {
149      throw new ERR_INVALID_MIME_SYNTAX(
150        'parameter name',
151        name,
152        invalidNameIndex,
153      );
154    }
155    const invalidValueIndex = SafeStringPrototypeSearch(
156      value,
157      NOT_HTTP_QUOTED_STRING_CODE_POINT);
158    if (invalidValueIndex !== -1) {
159      throw new ERR_INVALID_MIME_SYNTAX(
160        'parameter value',
161        value,
162        invalidValueIndex,
163      );
164    }
165    data.set(name, value);
166  }
167
168  *entries() {
169    yield* this.#data.entries();
170  }
171
172  *keys() {
173    yield* this.#data.keys();
174  }
175
176  *values() {
177    yield* this.#data.values();
178  }
179
180  toString() {
181    let ret = '';
182    for (const { 0: key, 1: value } of this.#data) {
183      const encoded = encode(value);
184      // Ensure they are separated
185      if (ret.length) ret += ';';
186      ret += `${key}=${encoded}`;
187    }
188    return ret;
189  }
190
191  // Used to act as a friendly class to stringifying stuff
192  // not meant to be exposed to users, could inject invalid values
193  static parseParametersString(str, position, params) {
194    const paramsMap = params.#data;
195    const endOfSource = SafeStringPrototypeSearch(
196      StringPrototypeSlice(str, position),
197      START_ENDING_WHITESPACE,
198    ) + position;
199    while (position < endOfSource) {
200      // Skip any whitespace before parameter
201      position += SafeStringPrototypeSearch(
202        StringPrototypeSlice(str, position),
203        END_BEGINNING_WHITESPACE,
204      );
205      // Read until ';' or '='
206      const afterParameterName = SafeStringPrototypeSearch(
207        StringPrototypeSlice(str, position),
208        EQUALS_SEMICOLON_OR_END,
209      ) + position;
210      const parameterString = toASCIILower(
211        StringPrototypeSlice(str, position, afterParameterName),
212      );
213      position = afterParameterName;
214      // If we found a terminating character
215      if (position < endOfSource) {
216        // Safe to use because we never do special actions for surrogate pairs
217        const char = StringPrototypeCharAt(str, position);
218        // Skip the terminating character
219        position += 1;
220        // Ignore parameters without values
221        if (char === ';') {
222          continue;
223        }
224      }
225      // If we are at end of the string, it cannot have a value
226      if (position >= endOfSource) break;
227      // Safe to use because we never do special actions for surrogate pairs
228      const char = StringPrototypeCharAt(str, position);
229      let parameterValue = null;
230      if (char === '"') {
231        // Handle quoted-string form of values
232        // skip '"'
233        position += 1;
234        // Find matching closing '"' or end of string
235        //   use $1 to see if we terminated on unmatched '\'
236        //   use $2 to see if we terminated on a matching '"'
237        //   so we can skip the last char in either case
238        const insideMatch = RegExpPrototypeExec(
239          QUOTED_VALUE_PATTERN,
240          StringPrototypeSlice(str, position));
241        position += insideMatch[0].length;
242        // Skip including last character if an unmatched '\' or '"' during
243        // unescape
244        const inside = insideMatch[1] || insideMatch[2] ?
245          StringPrototypeSlice(insideMatch[0], 0, -1) :
246          insideMatch[0];
247        // Unescape '\' quoted characters
248        parameterValue = removeBackslashes(inside);
249        // If we did have an unmatched '\' add it back to the end
250        if (insideMatch[1]) parameterValue += '\\';
251      } else {
252        // Handle the normal parameter value form
253        const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
254        const rawValue = valueEnd === -1 ?
255          StringPrototypeSlice(str, position) :
256          StringPrototypeSlice(str, position, valueEnd);
257        position += rawValue.length;
258        const trimmedValue = StringPrototypeSlice(
259          rawValue,
260          0,
261          SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE),
262        );
263        // Ignore parameters without values
264        if (trimmedValue === '') continue;
265        parameterValue = trimmedValue;
266      }
267      if (
268        parameterString !== '' &&
269        SafeStringPrototypeSearch(parameterString,
270                                  NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
271        SafeStringPrototypeSearch(parameterValue,
272                                  NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
273        params.has(parameterString) === false
274      ) {
275        paramsMap.set(parameterString, parameterValue);
276      }
277      position++;
278    }
279    return paramsMap;
280  }
281}
282const MIMEParamsStringify = MIMEParams.prototype.toString;
283ObjectDefineProperty(MIMEParams.prototype, SymbolIterator, {
284  __proto__: null,
285  configurable: true,
286  value: MIMEParams.prototype.entries,
287  writable: true,
288});
289ObjectDefineProperty(MIMEParams.prototype, 'toJSON', {
290  __proto__: null,
291  configurable: true,
292  value: MIMEParamsStringify,
293  writable: true,
294});
295
296const { parseParametersString } = MIMEParams;
297delete MIMEParams.parseParametersString;
298
299class MIMEType {
300  #type;
301  #subtype;
302  #parameters;
303  constructor(string) {
304    string = `${string}`;
305    const data = parseTypeAndSubtype(string);
306    this.#type = data.type;
307    this.#subtype = data.subtype;
308    this.#parameters = new MIMEParams();
309    parseParametersString(
310      string,
311      data.parametersStringIndex,
312      this.#parameters,
313    );
314  }
315
316  get type() {
317    return this.#type;
318  }
319
320  set type(v) {
321    v = `${v}`;
322    const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
323    if (v.length === 0 || invalidTypeIndex !== -1) {
324      throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
325    }
326    this.#type = toASCIILower(v);
327  }
328
329  get subtype() {
330    return this.#subtype;
331  }
332
333  set subtype(v) {
334    v = `${v}`;
335    const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
336    if (v.length === 0 || invalidSubtypeIndex !== -1) {
337      throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
338    }
339    this.#subtype = toASCIILower(v);
340  }
341
342  get essence() {
343    return `${this.#type}/${this.#subtype}`;
344  }
345
346  get params() {
347    return this.#parameters;
348  }
349
350  toString() {
351    let ret = `${this.#type}/${this.#subtype}`;
352    const paramStr = FunctionPrototypeCall(MIMEParamsStringify, this.#parameters);
353    if (paramStr.length) ret += `;${paramStr}`;
354    return ret;
355  }
356}
357ObjectDefineProperty(MIMEType.prototype, 'toJSON', {
358  __proto__: null,
359  configurable: true,
360  value: MIMEType.prototype.toString,
361  writable: true,
362});
363
364module.exports = {
365  MIMEParams,
366  MIMEType,
367};
368