// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // @ts-check /// /// /// /// /// /// /// /// "use strict"; ((window) => { const webidl = window.__bootstrap.webidl; const { HTTP_TAB_OR_SPACE_PREFIX_RE, HTTP_TAB_OR_SPACE_SUFFIX_RE, HTTP_WHITESPACE_PREFIX_RE, HTTP_WHITESPACE_SUFFIX_RE, HTTP_TOKEN_CODE_POINT_RE, byteLowerCase, collectSequenceOfCodepoints, collectHttpQuotedString, } = window.__bootstrap.infra; const _headerList = Symbol("header list"); const _iterableHeaders = Symbol("iterable headers"); const _guard = Symbol("guard"); /** * @typedef Header * @type {[string, string]} */ /** * @typedef HeaderList * @type {Header[]} */ /** * @param {string} potentialValue * @returns {string} */ function normalizeHeaderValue(potentialValue) { potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, ""); potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, ""); return potentialValue; } /** * @param {Headers} headers * @param {HeadersInit} object */ function fillHeaders(headers, object) { if (Array.isArray(object)) { for (const header of object) { if (header.length !== 2) { throw new TypeError( `Invalid header. Length must be 2, but is ${header.length}`, ); } appendHeader(headers, header[0], header[1]); } } else { for (const key of Object.keys(object)) { appendHeader(headers, key, object[key]); } } } /** * https://fetch.spec.whatwg.org/#concept-headers-append * @param {Headers} headers * @param {string} name * @param {string} value */ function appendHeader(headers, name, value) { // 1. value = normalizeHeaderValue(value); // 2. if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if ( value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D") ) { throw new TypeError("Header value is not valid."); } // 3. if (headers[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } // 7. const list = headers[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { name = list[i][0]; break; } } list.push([name, value]); } /** * https://fetch.spec.whatwg.org/#concept-header-list-get * @param {HeaderList} list * @param {string} name */ function getHeader(list, name) { const lowercaseName = byteLowerCase(name); const entries = list.filter((entry) => byteLowerCase(entry[0]) === lowercaseName ).map((entry) => entry[1]); if (entries.length === 0) { return null; } else { return entries.join("\x2C\x20"); } } /** * https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split * @param {HeaderList} list * @param {string} name * @returns {string[] | null} */ function getDecodeSplitHeader(list, name) { const initialValue = getHeader(list, name); if (initialValue === null) return null; const input = initialValue; let position = 0; const values = []; let value = ""; while (position < initialValue.length) { // 7.1. collect up to " or , const res = collectSequenceOfCodepoints( initialValue, position, (c) => c !== "\u0022" && c !== "\u002C", ); value += res.result; position = res.position; if (position < initialValue.length) { if (input[position] === "\u0022") { const res = collectHttpQuotedString(input, position, false); value += res.result; position = res.position; if (position < initialValue.length) { continue; } } else { if (input[position] !== "\u002C") throw new TypeError("Unreachable"); position += 1; } } value = value.replaceAll(HTTP_TAB_OR_SPACE_PREFIX_RE, ""); value = value.replaceAll(HTTP_TAB_OR_SPACE_SUFFIX_RE, ""); values.push(value); value = ""; } return values; } class Headers { /** @type {HeaderList} */ [_headerList] = []; /** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ [_guard]; get [_iterableHeaders]() { const list = this[_headerList]; const headers = []; const headerNamesSet = new Set(); for (const entry of list) { headerNamesSet.add(byteLowerCase(entry[0])); } const names = [...headerNamesSet].sort(); for (const name of names) { // The following if statement, and if block of the following statement // are not spec compliant. `set-cookie` is the only header that can not // be concatentated, so must be given to the user as multiple headers. // The else block of the if statement is spec compliant again. if (name == "set-cookie") { const setCookie = list.filter((entry) => byteLowerCase(entry[0]) === "set-cookie" ); if (setCookie.length === 0) throw new TypeError("Unreachable"); for (const entry of setCookie) { headers.push([name, entry[1]]); } } else { const value = getHeader(list, name); if (value === null) throw new TypeError("Unreachable"); headers.push([name, value]); } } return headers; } /** @param {HeadersInit} [init] */ constructor(init = undefined) { const prefix = "Failed to construct 'Event'"; if (init !== undefined) { init = webidl.converters["HeadersInit"](init, { prefix, context: "Argument 1", }); } this[webidl.brand] = webidl.brand; this[_guard] = "none"; if (init !== undefined) { fillHeaders(this, init); } } /** * @param {string} name * @param {string} value */ append(name, value) { webidl.assertBranded(this, Headers); const prefix = "Failed to execute 'append' on 'Headers'"; webidl.requiredArguments(arguments.length, 2, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); value = webidl.converters["ByteString"](value, { prefix, context: "Argument 2", }); appendHeader(this, name, value); } /** * @param {string} name */ delete(name) { const prefix = "Failed to execute 'delete' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if (this[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { list.splice(i, 1); i--; } } } /** * @param {string} name */ get(name) { const prefix = "Failed to execute 'get' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } const list = this[_headerList]; return getHeader(list, name); } /** * @param {string} name */ has(name) { const prefix = "Failed to execute 'has' on 'Headers'"; webidl.requiredArguments(arguments.length, 1, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { return true; } } return false; } /** * @param {string} name * @param {string} value */ set(name, value) { webidl.assertBranded(this, Headers); const prefix = "Failed to execute 'set' on 'Headers'"; webidl.requiredArguments(arguments.length, 2, { prefix }); name = webidl.converters["ByteString"](name, { prefix, context: "Argument 1", }); value = webidl.converters["ByteString"](value, { prefix, context: "Argument 2", }); value = normalizeHeaderValue(value); // 2. if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) { throw new TypeError("Header name is not valid."); } if ( value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D") ) { throw new TypeError("Header value is not valid."); } if (this[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } const list = this[_headerList]; const lowercaseName = byteLowerCase(name); let added = false; for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { if (!added) { list[i][1] = value; added = true; } else { list.splice(i, 1); i--; } } } if (!added) { list.push([name, value]); } } [Symbol.for("Deno.customInspect")](inspect) { const headers = {}; for (const header of this) { headers[header[0]] = header[1]; } return `Headers ${inspect(headers)}`; } get [Symbol.toStringTag]() { return "Headers"; } } webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1); webidl.converters["sequence"] = webidl .createSequenceConverter(webidl.converters["ByteString"]); webidl.converters["sequence>"] = webidl .createSequenceConverter(webidl.converters["sequence"]); webidl.converters["record"] = webidl .createRecordConverter( webidl.converters["ByteString"], webidl.converters["ByteString"], ); webidl.converters["HeadersInit"] = (V, opts) => { // Union for (sequence> or record) if (typeof V === "object" && V !== null) { if (V[Symbol.iterator] !== undefined) { return webidl.converters["sequence>"](V, opts); } return webidl.converters["record"](V, opts); } throw webidl.makeException( TypeError, "The provided value is not of type '(sequence> or record)'", opts, ); }; webidl.converters["Headers"] = webidl.createInterfaceConverter( "Headers", Headers, ); /** * @param {HeaderList} list * @param {"immutable" | "request" | "request-no-cors" | "response" | "none"} guard * @returns {Headers} */ function headersFromHeaderList(list, guard) { const headers = webidl.createBranded(Headers); headers[_headerList] = list; headers[_guard] = guard; return headers; } /** * @param {Headers} * @returns {HeaderList} */ function headerListFromHeaders(headers) { return headers[_headerList]; } /** * @param {Headers} * @returns {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ function guardFromHeaders(headers) { return headers[_guard]; } window.__bootstrap.headers = { Headers, headersFromHeaderList, headerListFromHeaders, fillHeaders, getDecodeSplitHeader, guardFromHeaders, }; })(this);