All files / media_types / parse_media_type.ts

100.00% Branches 25/25
100.00% Lines 79/79
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
 
 
 
x14
 
x14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x14
x14
 
x131
x131
 
x131
 
 
x131
 
x131
x131
x180
x180
x181
x181
x228
x180
x186
 
x192
x192
x191
x191
 
x191
 
x222
x222
x180
x190
x196
x196
x190
x190
x180
x181
x181
x221
x221
x221
 
 
 
x242
x131
x137
x137
x137
x140
x140
x142
x142
x140
x140
 
x140
x140
x137
x147
x147
x147
x152
x152
x152
x152
x152
x152
x147
x150
x150
x149
x147
x148
x148
x148
x148
x148
x148
x148
x148
x147
x140
x140
x140
x137
 
x746
x131



























































































































// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

import { consumeMediaParam, decode2331Encoding } from "./_util.ts";

const SEMICOLON_REGEXP = /^\s*;\s*$/;
/**
 * Parses the media type and any optional parameters, per
 * {@link https://www.rfc-editor.org/rfc/rfc1521.html | RFC 1521}.
 *
 * Media types are the values in `Content-Type` and `Content-Disposition`
 * headers. On success the function returns a tuple where the first element is
 * the media type and the second element is the optional parameters or
 * `undefined` if there are none.
 *
 * The function will throw if the parsed value is invalid.
 *
 * The returned media type will be normalized to be lower case, and returned
 * params keys will be normalized to lower case, but preserves the casing of
 * the value.
 *
 * @param type The media type to parse.
 *
 * @returns A tuple where the first element is the media type and the second
 * element is the optional parameters or `undefined` if there are none.
 *
 * @example Usage
 * ```ts
 * import { parseMediaType } from "@std/media-types/parse-media-type";
 * import { assertEquals } from "@std/assert";
 *
 * assertEquals(parseMediaType("application/JSON"), ["application/json", undefined]);
 * assertEquals(parseMediaType("text/html; charset=UTF-8"), ["text/html", { charset: "UTF-8" }]);
 * ```
 */
export function parseMediaType(
  type: string,
): [mediaType: string, params: Record<string, string> | undefined] {
  const [base] = type.split(";") as [string];
  const mediaType = base.toLowerCase().trim();

  const params: Record<string, string> = {};
  // Map of base parameter name -> parameter name -> value
  // for parameters containing a '*' character.
  const continuation = new Map<string, Record<string, string>>();

  type = type.slice(base.length);
  while (type.length) {
    type = type.trimStart();
    if (type.length === 0) {
      break;
    }
    const [key, value, rest] = consumeMediaParam(type);
    if (!key) {
      if (SEMICOLON_REGEXP.test(rest)) {
        // ignore trailing semicolons
        break;
      }
      throw new TypeError(
        `Cannot parse media type: invalid parameter "${type}"`,
      );
    }

    let pmap = params;
    const [baseName, rest2] = key.split("*");
    if (baseName && rest2 !== undefined) {
      if (!continuation.has(baseName)) {
        continuation.set(baseName, {});
      }
      pmap = continuation.get(baseName)!;
    }
    if (key in pmap) {
      throw new TypeError("Cannot parse media type: duplicate key");
    }
    pmap[key] = value;
    type = rest;
  }

  // Stitch together any continuations or things with stars
  // (i.e. RFC 2231 things with stars: "foo*0" or "foo*")
  let str = "";
  for (const [key, pieceMap] of continuation) {
    const singlePartKey = `${key}*`;
    const type = pieceMap[singlePartKey];
    if (type) {
      const decv = decode2331Encoding(type);
      if (decv) {
        params[key] = decv;
      }
      continue;
    }

    str = "";
    let valid = false;
    for (let n = 0;; n++) {
      const simplePart = `${key}*${n}`;
      let type = pieceMap[simplePart];
      if (type) {
        valid = true;
        str += type;
        continue;
      }
      const encodedPart = `${simplePart}*`;
      type = pieceMap[encodedPart];
      if (!type) {
        break;
      }
      valid = true;
      if (n === 0) {
        const decv = decode2331Encoding(type);
        if (decv) {
          str += decv;
        }
      } else {
        const decv = decodeURI(type);
        str += decv;
      }
    }
    if (valid) {
      params[key] = str;
    }
  }

  return [mediaType, Object.keys(params).length ? params : undefined];
}