// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import { consumeMediaParam, decode2331Encoding } from "./_util.ts";
/**
* Parses the media type and any optional parameters, per
* [RFC 1521](https://datatracker.ietf.org/doc/html/rfc1521). 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.
*
* @example
* ```ts
* import { parseMediaType } from "https://deno.land/std@$STD_VERSION/media_types/parse_media_type.ts";
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
*
* assertEquals(
* parseMediaType("application/JSON"),
* [
* "application/json",
* undefined
* ]
* );
*
* assertEquals(
* parseMediaType("text/html; charset=UTF-8"),
* [
* "application/json",
* { charset: "UTF-8" },
* ]
* );
* ```
*/
export function parseMediaType(
v: string,
): [mediaType: string, params: Record | undefined] {
const [base] = v.split(";");
const mediaType = base.toLowerCase().trim();
const params: Record = {};
// Map of base parameter name -> parameter name -> value
// for parameters containing a '*' character.
const continuation = new Map>();
v = v.slice(base.length);
while (v.length) {
v = v.trimStart();
if (v.length === 0) {
break;
}
const [key, value, rest] = consumeMediaParam(v);
if (!key) {
if (rest.trim() === ";") {
// ignore trailing semicolons
break;
}
throw new TypeError("Invalid media parameter.");
}
let pmap = params;
const [baseName, rest2] = key.split("*");
if (baseName && rest2 != null) {
if (!continuation.has(baseName)) {
continuation.set(baseName, {});
}
pmap = continuation.get(baseName)!;
}
if (key in pmap) {
throw new TypeError("Duplicate key parsed.");
}
pmap[key] = value;
v = 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 v = pieceMap[singlePartKey];
if (v) {
const decv = decode2331Encoding(v);
if (decv) {
params[key] = decv;
}
continue;
}
str = "";
let valid = false;
for (let n = 0;; n++) {
const simplePart = `${key}*${n}`;
let v = pieceMap[simplePart];
if (v) {
valid = true;
str += v;
continue;
}
const encodedPart = `${simplePart}*`;
v = pieceMap[encodedPart];
if (!v) {
break;
}
valid = true;
if (n === 0) {
const decv = decode2331Encoding(v);
if (decv) {
str += decv;
}
} else {
const decv = decodeURI(v);
str += decv;
}
}
if (valid) {
params[key] = str;
}
}
return Object.keys(params).length
? [mediaType, params]
: [mediaType, undefined];
}