chore: base

This commit is contained in:
xHyroM
2022-07-11 09:45:23 +02:00
parent b04d87b14c
commit e187173d21
425 changed files with 1080881 additions and 5 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Ethan Arrowood
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+402
View File
@@ -0,0 +1,402 @@
'use strict'
const util = require('../core/util')
const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
const { Blob } = require('buffer')
const { kBodyUsed } = require('../core/symbols')
const assert = require('assert')
const { NotSupportedError } = require('../core/errors')
const { isErrored } = require('../core/util')
const { isUint8Array, isArrayBuffer } = require('util/types')
let ReadableStream
async function * blobGen (blob) {
if (blob.stream) {
yield * blob.stream()
} else {
// istanbul ignore next: node < 16.7
yield await blob.arrayBuffer()
}
}
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
if (!ReadableStream) {
ReadableStream = require('stream/web').ReadableStream
}
// 1. Let stream be object if object is a ReadableStream object.
// Otherwise, let stream be a new ReadableStream, and set up stream.
let stream = null
// 2. Let action be null.
let action = null
// 3. Let source be null.
let source = null
// 4. Let length be null.
let length = null
// 5. Let Content-Type be null.
let contentType = null
// 6. Switch on object:
if (object == null) {
// Note: The IDL processor cannot handle this situation. See
// https://crbug.com/335871.
} else if (object instanceof URLSearchParams) {
// URLSearchParams
// spec says to run application/x-www-form-urlencoded on body.list
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
source = object.toString()
// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
// BufferSource
if (object instanceof DataView) {
// TODO: Blob doesn't seem to work with DataView?
object = object.buffer
}
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object)
} else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random()
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
const escape = (str) =>
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
// Set action to this step: run the multipart/form-data
// encoding algorithm, with object’s entry list and UTF-8.
action = async function * (object) {
const enc = new TextEncoder()
for (const [name, value] of object) {
if (typeof value === 'string') {
yield enc.encode(
prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`
)
} else {
yield enc.encode(
prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') +
'\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`
)
yield * blobGen(value)
yield enc.encode('\r\n')
}
}
yield enc.encode(`--${boundary}--`)
}
// Set source to object.
source = object
// Set length to unclear, see html/6424 for improving this.
// TODO
// Set Content-Type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
contentType = 'multipart/form-data; boundary=' + boundary
} else if (isBlobLike(object)) {
// Blob
// Set action to this step: read object.
action = blobGen
// Set source to object.
source = object
// Set length to object’s size.
length = object.size
// If object’s type attribute is not the empty byte sequence, set
// Content-Type to its value.
if (object.type) {
contentType = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('keepalive')
}
// If object is disturbed or locked, then throw a TypeError.
if (util.isDisturbed(object) || object.locked) {
throw new TypeError(
'Response body object should not be disturbed or locked'
)
}
stream =
object instanceof ReadableStream ? object : ReadableStreamFrom(object)
} else {
// TODO: byte sequence?
// TODO: scalar value string?
// TODO: else?
source = toUSVString(object)
contentType = 'text/plain;charset=UTF-8'
}
// 7. If source is a byte sequence, then set action to a
// step that returns source and length to source’s length.
// TODO: What is a "byte sequence?"
if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source)
}
// 8. If action is non-null, then run these steps in in parallel:
if (action != null) {
// Run action.
let iterator
stream = new ReadableStream({
async start () {
iterator = action(object)[Symbol.asyncIterator]()
},
async pull (controller) {
const { value, done } = await iterator.next()
if (done) {
// When running action is done, close stream.
queueMicrotask(() => {
controller.close()
})
} else {
// Whenever one or more bytes are available and stream is not errored,
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
// bytes into stream.
if (!isErrored(stream)) {
controller.enqueue(new Uint8Array(value))
}
}
return controller.desiredSize > 0
},
async cancel (reason) {
await iterator.return()
}
})
} else if (!stream) {
// TODO: Spec doesn't say anything about this?
stream = new ReadableStream({
async pull (controller) {
controller.enqueue(
typeof source === 'string' ? new TextEncoder().encode(source) : source
)
queueMicrotask(() => {
controller.close()
})
}
})
}
// 9. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
// 10. Return body and Content-Type.
return [body, contentType]
}
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
function safelyExtractBody (object, keepalive = false) {
if (!ReadableStream) {
// istanbul ignore next
ReadableStream = require('stream/web').ReadableStream
}
// To safely extract a body and a `Content-Type` value from
// a byte sequence or BodyInit object object, run these steps:
// 1. If object is a ReadableStream object, then:
if (object instanceof ReadableStream) {
// Assert: object is neither disturbed nor locked.
// istanbul ignore next
assert(!util.isDisturbed(object), 'disturbed')
// istanbul ignore next
assert(!object.locked, 'locked')
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
}
function cloneBody (body) {
// To clone a body body, run these steps:
// https://fetch.spec.whatwg.org/#concept-body-clone
// 1. Let « out1, out2 » be the result of teeing body’s stream.
const [out1, out2] = body.stream.tee()
// 2. Set body’s stream to out1.
body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body.
return {
stream: out2,
length: body.length,
source: body.source
}
}
function bodyMixinMethods (instance) {
const methods = {
async blob () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}
const chunks = []
if (this[kState].body) {
if (isUint8Array(this[kState].body)) {
chunks.push(this[kState].body)
} else {
const stream = this[kState].body.stream
if (util.isDisturbed(stream)) {
throw new TypeError('disturbed')
}
if (stream.locked) {
throw new TypeError('locked')
}
// Compat.
stream[kBodyUsed] = true
for await (const chunk of stream) {
chunks.push(chunk)
}
}
}
return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
},
async arrayBuffer () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}
const blob = await this.blob()
return await blob.arrayBuffer()
},
async text () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}
const blob = await this.blob()
return toUSVString(await blob.text())
},
async json () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}
return JSON.parse(await this.text())
},
async formData () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}
const contentType = this.headers.get('Content-Type')
// If mimeType’s essence is "multipart/form-data", then:
if (/multipart\/form-data/.test(contentType)) {
throw new NotSupportedError('multipart/form-data not supported')
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
// 1. Let entries be the result of parsing bytes.
let entries
try {
entries = new URLSearchParams(await this.text())
} catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
// 2. If entries is failure, then throw a TypeError.
throw Object.assign(new TypeError(), { cause: err })
}
// 3. Return a new FormData object whose entries are entries.
const formData = new FormData()
for (const [name, value] of entries) {
formData.append(name, value)
}
return formData
} else {
// Otherwise, throw a TypeError.
webidl.errors.exception({
header: `${instance.name}.formData`,
value: 'Could not parse content as FormData.'
})
}
}
}
return methods
}
const properties = {
body: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}
return this[kState].body ? this[kState].body.stream : null
}
},
bodyUsed: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
}
}
}
function mixinBody (prototype) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
Object.defineProperties(prototype.prototype, properties)
}
module.exports = {
extractBody,
safelyExtractBody,
cloneBody,
mixinBody
}
+88
View File
@@ -0,0 +1,88 @@
'use strict'
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
const nullBodyStatus = [101, 204, 205, 304]
const redirectStatus = [301, 302, 303, 307, 308]
const referrerPolicy = [
'',
'no-referrer',
'no-referrer-when-downgrade',
'same-origin',
'origin',
'strict-origin',
'origin-when-cross-origin',
'strict-origin-when-cross-origin',
'unsafe-url'
]
const requestRedirect = ['follow', 'manual', 'error']
const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
const requestCredentials = ['omit', 'same-origin', 'include']
const requestCache = [
'default',
'no-store',
'reload',
'no-cache',
'force-cache',
'only-if-cached'
]
const requestBodyHeader = [
'content-encoding',
'content-language',
'content-location',
'content-type'
]
// http://fetch.spec.whatwg.org/#forbidden-method
const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
const subresource = [
'audio',
'audioworklet',
'font',
'image',
'manifest',
'paintworklet',
'script',
'style',
'track',
'video',
'xslt',
''
]
/** @type {globalThis['DOMException']} */
const DOMException = globalThis.DOMException ?? (() => {
// DOMException was only made a global in Node v17.0.0,
// but fetch supports >= v16.5.
try {
atob('~')
} catch (err) {
return Object.getPrototypeOf(err).constructor
}
})()
module.exports = {
DOMException,
subresource,
forbiddenMethods,
requestBodyHeader,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache,
redirectStatus,
corsSafeListedMethods,
nullBodyStatus,
safeMethods
}
+557
View File
@@ -0,0 +1,557 @@
const assert = require('assert')
const { atob } = require('buffer')
const encoder = new TextEncoder()
// https://fetch.spec.whatwg.org/#data-url-processor
/** @param {URL} dataURL */
function dataURLProcessor (dataURL) {
// 1. Assert: dataURL’s scheme is "data".
assert(dataURL.protocol === 'data:')
// 2. Let input be the result of running the URL
// serializer on dataURL with exclude fragment
// set to true.
let input = URLSerializer(dataURL, true)
// 3. Remove the leading "data:" string from input.
input = input.slice(5)
// 4. Let position point at the start of input.
const position = { position: 0 }
// 5. Let mimeType be the result of collecting a
// sequence of code points that are not equal
// to U+002C (,), given position.
let mimeType = collectASequenceOfCodePoints(
(char) => char !== ',',
input,
position
)
// 6. Strip leading and trailing ASCII whitespace
// from mimeType.
// Note: This will only remove U+0020 SPACE code
// points, if any.
// Undici implementation note: we need to store the
// length because if the mimetype has spaces removed,
// the wrong amount will be sliced from the input in
// step #9
const mimeTypeLength = mimeType.length
mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '')
// 7. If position is past the end of input, then
// return failure
if (position.position >= input.length) {
return 'failure'
}
// 8. Advance position by 1.
position.position++
// 9. Let encodedBody be the remainder of input.
const encodedBody = input.slice(mimeTypeLength + 1)
// 10. Let body be the percent-decoding of encodedBody.
/** @type {Uint8Array|string} */
let body = stringPercentDecode(encodedBody)
// 11. If mimeType ends with U+003B (;), followed by
// zero or more U+0020 SPACE, followed by an ASCII
// case-insensitive match for "base64", then:
if (/;(\u0020){0,}base64$/i.test(mimeType)) {
// 1. Let stringBody be the isomorphic decode of body.
const stringBody = decodeURIComponent(new TextDecoder('utf-8').decode(body))
// 2. Set body to the forgiving-base64 decode of
// stringBody.
body = forgivingBase64(stringBody)
// 3. If body is failure, then return failure.
if (body === 'failure') {
return 'failure'
}
// 4. Remove the last 6 code points from mimeType.
mimeType = mimeType.slice(0, -6)
// 5. Remove trailing U+0020 SPACE code points from mimeType,
// if any.
mimeType = mimeType.replace(/(\u0020)+$/, '')
// 6. Remove the last U+003B (;) code point from mimeType.
mimeType = mimeType.slice(0, -1)
}
// 12. If mimeType starts with U+003B (;), then prepend
// "text/plain" to mimeType.
if (mimeType.startsWith(';')) {
mimeType = 'text/plain' + mimeType
}
// 13. Let mimeTypeRecord be the result of parsing
// mimeType.
let mimeTypeRecord = parseMIMEType(mimeType)
// 14. If mimeTypeRecord is failure, then set
// mimeTypeRecord to text/plain;charset=US-ASCII.
if (mimeTypeRecord === 'failure') {
mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
}
// 15. Return a new data: URL struct whose MIME
// type is mimeTypeRecord and body is body.
// https://fetch.spec.whatwg.org/#data-url-struct
return { mimeType: mimeTypeRecord, body }
}
// https://url.spec.whatwg.org/#concept-url-serializer
/**
* @param {URL} url
* @param {boolean} excludeFragment
*/
function URLSerializer (url, excludeFragment = false) {
// 1. Let output be url’s scheme and U+003A (:) concatenated.
let output = url.protocol
// 2. If url’s host is non-null:
if (url.host.length > 0) {
// 1. Append "//" to output.
output += '//'
// 2. If url includes credentials, then:
if (url.username.length > 0 || url.password.length > 0) {
// 1. Append url’s username to output.
output += url.username
// 2. If url’s password is not the empty string, then append U+003A (:),
// followed by url’s password, to output.
if (url.password.length > 0) {
output += ':' + url.password
}
// 3. Append U+0040 (@) to output.
output += '@'
}
// 3. Append url’s host, serialized, to output.
output += decodeURIComponent(url.host)
// 4. If url’s port is non-null, append U+003A (:) followed by url’s port,
// serialized, to output.
if (url.port.length > 0) {
output += ':' + url.port
}
}
// 3. If url’s host is null, url does not have an opaque path,
// url’s path’s size is greater than 1, and url’s path[0]
// is the empty string, then append U+002F (/) followed by
// U+002E (.) to output.
// Note: This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/,
// when parsed and then serialized, from ending up as web+demo://not-a-host/
// (they end up as web+demo:/.//not-a-host/).
// Undici implementation note: url's path[0] can never be an
// empty string, so we have to slightly alter what the spec says.
if (
url.host.length === 0 &&
url.pathname.length > 1 &&
url.href.slice(url.protocol.length + 1)[0] === '.'
) {
output += '/.'
}
// 4. Append the result of URL path serializing url to output.
output += url.pathname
// 5. If url’s query is non-null, append U+003F (?),
// followed by url’s query, to output.
if (url.search.length > 0) {
output += url.search
}
// 6. If exclude fragment is false and url’s fragment is non-null,
// then append U+0023 (#), followed by url’s fragment, to output.
if (excludeFragment === false && url.hash.length > 0) {
output += url.hash
}
// 7. Return output.
return output
}
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
/**
* @param {(char: string) => boolean} condition
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePoints (condition, input, position) {
// 1. Let result be the empty string.
let result = ''
// 2. While position doesn’t point past the end of input and the
// code point at position within input meets the condition condition:
while (position.position < input.length && condition(input[position.position])) {
// 1. Append that code point to the end of result.
result += input[position.position]
// 2. Advance position by 1.
position.position++
}
// 3. Return result.
return result
}
// https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */
function stringPercentDecode (input) {
// 1. Let bytes be the UTF-8 encoding of input.
const bytes = encoder.encode(input)
// 2. Return the percent-decoding of bytes.
return percentDecode(bytes)
}
// https://url.spec.whatwg.org/#percent-decode
/** @param {Uint8Array} input */
function percentDecode (input) {
// 1. Let output be an empty byte sequence.
/** @type {number[]} */
const output = []
// 2. For each byte byte in input:
for (let i = 0; i < input.length; i++) {
const byte = input[i]
// 1. If byte is not 0x25 (%), then append byte to output.
if (byte !== 0x25) {
output.push(byte)
// 2. Otherwise, if byte is 0x25 (%) and the next two bytes
// after byte in input are not in the ranges
// 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
// and 0x61 (a) to 0x66 (f), all inclusive, append byte
// to output.
} else if (
byte === 0x25 &&
!/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2]))
) {
output.push(0x25)
// 3. Otherwise:
} else {
// 1. Let bytePoint be the two bytes after byte in input,
// decoded, and then interpreted as hexadecimal number.
const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2])
const bytePoint = Number.parseInt(nextTwoBytes, 16)
// 2. Append a byte whose value is bytePoint to output.
output.push(bytePoint)
// 3. Skip the next two bytes in input.
i += 2
}
}
// 3. Return output.
return Uint8Array.of(...output)
}
// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
/** @param {string} input */
function parseMIMEType (input) {
// 1. Remove any leading and trailing HTTP whitespace
// from input.
input = input.trim()
// 2. Let position be a position variable for input,
// initially pointing at the start of input.
const position = { position: 0 }
// 3. Let type be the result of collecting a sequence
// of code points that are not U+002F (/) from
// input, given position.
const type = collectASequenceOfCodePoints(
(char) => char !== '/',
input,
position
)
// 4. If type is the empty string or does not solely
// contain HTTP token code points, then return failure.
// https://mimesniff.spec.whatwg.org/#http-token-code-point
if (type.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(type)) {
return 'failure'
}
// 5. If position is past the end of input, then return
// failure
if (position.position > input.length) {
return 'failure'
}
// 6. Advance position by 1. (This skips past U+002F (/).)
position.position++
// 7. Let subtype be the result of collecting a sequence of
// code points that are not U+003B (;) from input, given
// position.
let subtype = collectASequenceOfCodePoints(
(char) => char !== ';',
input,
position
)
// 8. Remove any trailing HTTP whitespace from subtype.
subtype = subtype.trim()
// 9. If subtype is the empty string or does not solely
// contain HTTP token code points, then return failure.
if (subtype.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(subtype)) {
return 'failure'
}
// 10. Let mimeType be a new MIME type record whose type
// is type, in ASCII lowercase, and subtype is subtype,
// in ASCII lowercase.
// https://mimesniff.spec.whatwg.org/#mime-type
const mimeType = {
type: type.toLowerCase(),
subtype: subtype.toLowerCase(),
/** @type {Map<string, string>} */
parameters: new Map()
}
// 11. While position is not past the end of input:
while (position.position < input.length) {
// 1. Advance position by 1. (This skips past U+003B (;).)
position.position++
// 2. Collect a sequence of code points that are HTTP
// whitespace from input given position.
collectASequenceOfCodePoints(
// https://fetch.spec.whatwg.org/#http-whitespace
(char) => /(\u000A|\u000D|\u0009|\u0020)/.test(char), // eslint-disable-line
input,
position
)
// 3. Let parameterName be the result of collecting a
// sequence of code points that are not U+003B (;)
// or U+003D (=) from input, given position.
let parameterName = collectASequenceOfCodePoints(
(char) => char !== ';' && char !== '=',
input,
position
)
// 4. Set parameterName to parameterName, in ASCII
// lowercase.
parameterName = parameterName.toLowerCase()
// 5. If position is not past the end of input, then:
if (position.position < input.length) {
// 1. If the code point at position within input is
// U+003B (;), then continue.
if (input[position.position] === ';') {
continue
}
// 2. Advance position by 1. (This skips past U+003D (=).)
position.position++
}
// 6. If position is past the end of input, then break.
if (position.position > input.length) {
break
}
// 7. Let parameterValue be null.
let parameterValue = null
// 8. If the code point at position within input is
// U+0022 ("), then:
if (input[position.position] === '"') {
// 1. Set parameterValue to the result of collecting
// an HTTP quoted string from input, given position
// and the extract-value flag.
// Undici implementation note: extract-value is never
// defined or mentioned anywhere.
parameterValue = collectAnHTTPQuotedString(input, position/*, extractValue */)
// 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position.
collectASequenceOfCodePoints(
(char) => char !== ';',
input,
position
)
// 9. Otherwise:
} else {
// 1. Set parameterValue to the result of collecting
// a sequence of code points that are not U+003B (;)
// from input, given position.
parameterValue = collectASequenceOfCodePoints(
(char) => char !== ';',
input,
position
)
// 2. Remove any trailing HTTP whitespace from parameterValue.
parameterValue = parameterValue.trim()
// 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) {
continue
}
}
// 10. If all of the following are true
// - parameterName is not the empty string
// - parameterName solely contains HTTP token code points
// - parameterValue solely contains HTTP quoted-string token code points
// - mimeType’s parameters[parameterName] does not exist
// then set mimeType’s parameters[parameterName] to parameterValue.
if (
parameterName.length !== 0 &&
/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(parameterName) &&
// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
!/^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/.test(parameterValue) && // eslint-disable-line
!mimeType.parameters.has(parameterName)
) {
mimeType.parameters.set(parameterName, parameterValue)
}
}
// 12. Return mimeType.
return mimeType
}
// https://infra.spec.whatwg.org/#forgiving-base64-decode
/** @param {string} data */
function forgivingBase64 (data) {
// 1. Remove all ASCII whitespace from data.
data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line
// 2. If data’s code point length divides by 4 leaving
// no remainder, then:
if (data.length % 4 === 0) {
// 1. If data ends with one or two U+003D (=) code points,
// then remove them from data.
data = data.replace(/=?=$/, '')
}
// 3. If data’s code point length divides by 4 leaving
// a remainder of 1, then return failure.
if (data.length % 4 === 1) {
return 'failure'
}
// 4. If data contains a code point that is not one of
// U+002B (+)
// U+002F (/)
// ASCII alphanumeric
// then return failure.
if (/[^+/0-9A-Za-z]/.test(data)) {
return 'failure'
}
const binary = atob(data)
const bytes = new Uint8Array(binary.length)
for (let byte = 0; byte < binary.length; byte++) {
bytes[byte] = binary.charCodeAt(byte)
}
return bytes
}
// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
/**
* @param {string} input
* @param {{ position: number }} position
* @param {boolean?} extractValue
*/
function collectAnHTTPQuotedString (input, position, extractValue) {
// 1. Let positionStart be position.
const positionStart = position.position
// 2. Let value be the empty string.
let value = ''
// 3. Assert: the code point at position within input
// is U+0022 (").
assert(input[position.position] === '"')
// 4. Advance position by 1.
position.position++
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points
// that are not U+0022 (") or U+005C (\) from input, given
// position, to value.
value += collectASequenceOfCodePoints(
(char) => char !== '"' && char !== '\\',
input,
position
)
// 2. If position is past the end of input, then break.
if (position.position >= input.length) {
break
}
// 3. Let quoteOrBackslash be the code point at position within
// input.
const quoteOrBackslash = input[position.position]
// 4. Advance position by 1.
position.position++
// 5. If quoteOrBackslash is U+005C (\), then:
if (quoteOrBackslash === '\\') {
// 1. If position is past the end of input, then append
// U+005C (\) to value and break.
if (position.position >= input.length) {
value += '\\'
break
}
// 2. Append the code point at position within input to value.
value += input[position.position]
// 3. Advance position by 1.
position.position++
// 6. Otherwise:
} else {
// 1. Assert: quoteOrBackslash is U+0022 (").
assert(quoteOrBackslash === '"')
// 2. Break.
break
}
}
// 6. If the extract-value flag is set, then return value.
if (extractValue) {
return value
}
// 7. Return the code points from positionStart to position,
// inclusive, within input.
return input.slice(positionStart, position.position)
}
module.exports = {
dataURLProcessor,
URLSerializer,
collectASequenceOfCodePoints,
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString
}
+313
View File
@@ -0,0 +1,313 @@
'use strict'
const { Blob } = require('buffer')
const { types } = require('util')
const { kState } = require('./symbols')
const { isBlobLike } = require('./util')
const { webidl } = require('./webidl')
class File extends Blob {
constructor (fileBits, fileName, options = {}) {
// The File constructor is invoked with two or three parameters, depending
// on whether the optional dictionary parameter is used. When the File()
// constructor is invoked, user agents must run the following steps:
if (arguments.length < 2) {
throw new TypeError('2 arguments required')
}
fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
fileName = webidl.converters.USVString(fileName)
options = webidl.converters.FilePropertyBag(options)
// 1. Let bytes be the result of processing blob parts given fileBits and
// options.
// Note: Blob handles this for us
// 2. Let n be the fileName argument to the constructor.
const n = fileName
// 3. Process FilePropertyBag dictionary argument by running the following
// substeps:
// 1. If the type member is provided and is not the empty string, let t
// be set to the type dictionary member. If t contains any characters
// outside the range U+0020 to U+007E, then set t to the empty string
// and return from these substeps.
// 2. Convert every character in t to ASCII lowercase.
// Note: Blob handles both of these steps for us
// 3. If the lastModified member is provided, let d be set to the
// lastModified dictionary member. If it is not provided, set d to the
// current date and time represented as the number of milliseconds since
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
const d = options.lastModified
// 4. Return a new File object F such that:
// F refers to the bytes byte sequence.
// F.size is set to the number of total bytes in bytes.
// F.name is set to n.
// F.type is set to t.
// F.lastModified is set to d.
super(processBlobParts(fileBits, options), { type: options.type })
this[kState] = {
name: n,
lastModified: d
}
}
get name () {
if (!(this instanceof File)) {
throw new TypeError('Illegal invocation')
}
return this[kState].name
}
get lastModified () {
if (!(this instanceof File)) {
throw new TypeError('Illegal invocation')
}
return this[kState].lastModified
}
get [Symbol.toStringTag] () {
return this.constructor.name
}
}
class FileLike {
constructor (blobLike, fileName, options = {}) {
// TODO: argument idl type check
// The File constructor is invoked with two or three parameters, depending
// on whether the optional dictionary parameter is used. When the File()
// constructor is invoked, user agents must run the following steps:
// 1. Let bytes be the result of processing blob parts given fileBits and
// options.
// 2. Let n be the fileName argument to the constructor.
const n = fileName
// 3. Process FilePropertyBag dictionary argument by running the following
// substeps:
// 1. If the type member is provided and is not the empty string, let t
// be set to the type dictionary member. If t contains any characters
// outside the range U+0020 to U+007E, then set t to the empty string
// and return from these substeps.
// TODO
const t = options.type
// 2. Convert every character in t to ASCII lowercase.
// TODO
// 3. If the lastModified member is provided, let d be set to the
// lastModified dictionary member. If it is not provided, set d to the
// current date and time represented as the number of milliseconds since
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
const d = options.lastModified ?? Date.now()
// 4. Return a new File object F such that:
// F refers to the bytes byte sequence.
// F.size is set to the number of total bytes in bytes.
// F.name is set to n.
// F.type is set to t.
// F.lastModified is set to d.
this[kState] = {
blobLike,
name: n,
type: t,
lastModified: d
}
}
stream (...args) {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.stream(...args)
}
arrayBuffer (...args) {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.arrayBuffer(...args)
}
slice (...args) {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.slice(...args)
}
text (...args) {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.text(...args)
}
get size () {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.size
}
get type () {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.type
}
get name () {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].name
}
get lastModified () {
if (!(this instanceof FileLike)) {
throw new TypeError('Illegal invocation')
}
return this[kState].lastModified
}
get [Symbol.toStringTag] () {
return 'File'
}
}
webidl.converters.Blob = webidl.interfaceConverter(Blob)
webidl.converters.BlobPart = function (V, opts) {
if (webidl.util.Type(V) === 'Object') {
if (isBlobLike(V)) {
return webidl.converters.Blob(V, { strict: false })
}
return webidl.converters.BufferSource(V, opts)
} else {
return webidl.converters.USVString(V, opts)
}
}
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
webidl.converters.BlobPart
)
// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
{
key: 'lastModified',
converter: webidl.converters['long long'],
get defaultValue () {
return Date.now()
}
},
{
key: 'type',
converter: webidl.converters.DOMString,
defaultValue: ''
},
{
key: 'endings',
converter: (value) => {
value = webidl.converters.DOMString(value)
value = value.toLowerCase()
if (value !== 'native') {
value = 'transparent'
}
return value
},
defaultValue: 'transparent'
}
])
/**
* @see https://www.w3.org/TR/FileAPI/#process-blob-parts
* @param {(NodeJS.TypedArray|Blob|string)[]} parts
* @param {{ type: string, endings: string }} options
*/
function processBlobParts (parts, options) {
// 1. Let bytes be an empty sequence of bytes.
/** @type {NodeJS.TypedArray[]} */
const bytes = []
// 2. For each element in parts:
for (const element of parts) {
// 1. If element is a USVString, run the following substeps:
if (typeof element === 'string') {
// 1. Let s be element.
let s = element
// 2. If the endings member of options is "native", set s
// to the result of converting line endings to native
// of element.
if (options.endings === 'native') {
s = convertLineEndingsNative(s)
}
// 3. Append the result of UTF-8 encoding s to bytes.
bytes.push(new TextEncoder().encode(s))
} else if (
types.isAnyArrayBuffer(element) ||
types.isTypedArray(element)
) {
// 2. If element is a BufferSource, get a copy of the
// bytes held by the buffer source, and append those
// bytes to bytes.
if (!element.buffer) { // ArrayBuffer
bytes.push(new Uint8Array(element))
} else {
bytes.push(element.buffer)
}
} else if (isBlobLike(element)) {
// 3. If element is a Blob, append the bytes it represents
// to bytes.
bytes.push(element)
}
}
// 3. Return bytes.
return bytes
}
/**
* @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
* @param {string} s
*/
function convertLineEndingsNative (s) {
// 1. Let native line ending be be the code point U+000A LF.
let nativeLineEnding = '\n'
// 2. If the underlying platform’s conventions are to
// represent newlines as a carriage return and line feed
// sequence, set native line ending to the code point
// U+000D CR followed by the code point U+000A LF.
if (process.platform === 'win32') {
nativeLineEnding = '\r\n'
}
return s.replace(/\r?\n/g, nativeLineEnding)
}
module.exports = { File, FileLike }
+321
View File
@@ -0,0 +1,321 @@
'use strict'
const { isBlobLike, isFileLike, toUSVString, makeIterator } = require('./util')
const { kState } = require('./symbols')
const { File, FileLike } = require('./file')
const { webidl } = require('./webidl')
const { Blob } = require('buffer')
// https://xhr.spec.whatwg.org/#formdata
class FormData {
static name = 'FormData'
constructor (form) {
if (arguments.length > 0 && form != null) {
webidl.errors.conversionFailed({
prefix: 'FormData constructor',
argument: 'Argument 1',
types: ['null']
})
}
this[kState] = []
}
append (name, value, filename = undefined) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) {
throw new TypeError(
`Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
)
}
if (arguments.length === 3 && !isBlobLike(value)) {
throw new TypeError(
"Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'"
)
}
// 1. Let value be value if given; otherwise blobValue.
name = webidl.converters.USVString(name)
value = isBlobLike(value)
? webidl.converters.Blob(value, { strict: false })
: webidl.converters.USVString(value)
filename = arguments.length === 3
? webidl.converters.USVString(filename)
: undefined
// 2. Let entry be the result of creating an entry with
// name, value, and filename if given.
const entry = makeEntry(name, value, filename)
// 3. Append entry to this’s entry list.
this[kState].push(entry)
}
delete (name) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name)
// The delete(name) method steps are to remove all entries whose name
// is name from this’s entry list.
const next = []
for (const entry of this[kState]) {
if (entry.name !== name) {
next.push(entry)
}
}
this[kState] = next
}
get (name) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in this’s entry list,
// then return null.
const idx = this[kState].findIndex((entry) => entry.name === name)
if (idx === -1) {
return null
}
// 2. Return the value of the first entry whose name is name from
// this’s entry list.
return this[kState][idx].value
}
getAll (name) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name)
// 1. If there is no entry whose name is name in this’s entry list,
// then return the empty list.
// 2. Return the values of all entries whose name is name, in order,
// from this’s entry list.
return this[kState]
.filter((entry) => entry.name === name)
.map((entry) => entry.value)
}
has (name) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name)
// The has(name) method steps are to return true if there is an entry
// whose name is name in this’s entry list; otherwise false.
return this[kState].findIndex((entry) => entry.name === name) !== -1
}
set (name, value, filename = undefined) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) {
throw new TypeError(
`Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
)
}
if (arguments.length === 3 && !isBlobLike(value)) {
throw new TypeError(
"Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'"
)
}
// The set(name, value) and set(name, blobValue, filename) method steps
// are:
// 1. Let value be value if given; otherwise blobValue.
name = webidl.converters.USVString(name)
value = isBlobLike(value)
? webidl.converters.Blob(value, { strict: false })
: webidl.converters.USVString(value)
filename = arguments.length === 3
? toUSVString(filename)
: undefined
// 2. Let entry be the result of creating an entry with name, value, and
// filename if given.
const entry = makeEntry(name, value, filename)
// 3. If there are entries in this’s entry list whose name is name, then
// replace the first such entry with entry and remove the others.
const idx = this[kState].findIndex((entry) => entry.name === name)
if (idx !== -1) {
this[kState] = [
...this[kState].slice(0, idx),
entry,
...this[kState].slice(idx + 1).filter((entry) => entry.name !== name)
]
} else {
// 4. Otherwise, append entry to this’s entry list.
this[kState].push(entry)
}
}
get [Symbol.toStringTag] () {
return this.constructor.name
}
entries () {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(
makeIterable(this[kState], 'entries'),
'FormData'
)
}
keys () {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(
makeIterable(this[kState], 'keys'),
'FormData'
)
}
values () {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(
makeIterable(this[kState], 'values'),
'FormData'
)
}
/**
* @param {(value: string, key: string, self: FormData) => void} callbackFn
* @param {unknown} thisArg
*/
forEach (callbackFn, thisArg = globalThis) {
if (!(this instanceof FormData)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.`
)
}
if (typeof callbackFn !== 'function') {
throw new TypeError(
"Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
)
}
for (const [key, value] of this) {
callbackFn.apply(thisArg, [value, key, this])
}
}
}
FormData.prototype[Symbol.iterator] = FormData.prototype.entries
/**
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
* @param {string} name
* @param {string|Blob} value
* @param {?string} filename
* @returns
*/
function makeEntry (name, value, filename) {
// 1. Set name to the result of converting name into a scalar value string.
// "To convert a string into a scalar value string, replace any surrogates
// with U+FFFD."
// see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end
name = Buffer.from(name).toString('utf8')
// 2. If value is a string, then set value to the result of converting
// value into a scalar value string.
if (typeof value === 'string') {
value = Buffer.from(value).toString('utf8')
} else {
// 3. Otherwise:
// 1. If value is not a File object, then set value to a new File object,
// representing the same bytes, whose name attribute value is "blob"
if (!isFileLike(value)) {
value = value instanceof Blob
? new File([value], 'blob', { type: value.type })
: new FileLike(value, 'blob', { type: value.type })
}
// 2. If filename is given, then set value to a new File object,
// representing the same bytes, whose name attribute is filename.
if (filename !== undefined) {
value = value instanceof File
? new File([value], filename, { type: value.type })
: new FileLike(value, filename, { type: value.type })
}
}
// 4. Return an entry whose name is name and whose value is value.
return { name, value }
}
function * makeIterable (entries, type) {
// The value pairs to iterate over are this’s entry list’s entries
// with the key being the name and the value being the value.
for (const { name, value } of entries) {
if (type === 'entries') {
yield [name, value]
} else if (type === 'values') {
yield value
} else {
yield name
}
}
}
module.exports = { FormData }
+501
View File
@@ -0,0 +1,501 @@
// https://github.com/Ethan-Arrowood/undici-fetch
'use strict'
const { kHeadersList } = require('../core/symbols')
const { kGuard } = require('./symbols')
const { kEnumerableProperty } = require('../core/util')
const {
makeIterator,
isValidHeaderName,
isValidHeaderValue
} = require('./util')
const { webidl } = require('./webidl')
const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')
/**
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
* @param {string} potentialValue
*/
function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from
// potentialValue.
return potentialValue.replace(
/^[\r\n\t ]+|[\r\n\t ]+$/g,
''
)
}
function fill (headers, object) {
// To fill a Headers object headers with a given object object, run these steps:
// 1. If object is a sequence, then for each header in object:
// Note: webidl conversion to array has already been done.
if (Array.isArray(object)) {
for (const header of object) {
// 1. If header does not contain exactly two items, then throw a TypeError.
if (header.length !== 2) {
webidl.errors.exception({
header: 'Headers constructor',
message: `expected name/value pair to be length 2, found ${header.length}.`
})
}
// 2. Append (header’s first item, header’s second item) to headers.
headers.append(header[0], header[1])
}
} else if (typeof object === 'object' && object !== null) {
// Note: null should throw
// 2. Otherwise, object is a record, then for each key → value in object,
// append (key, value) to headers
for (const [key, value] of Object.entries(object)) {
headers.append(key, value)
}
} else {
webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
})
}
}
class HeadersList {
constructor (init) {
if (init instanceof HeadersList) {
this[kHeadersMap] = new Map(init[kHeadersMap])
this[kHeadersSortedMap] = init[kHeadersSortedMap]
} else {
this[kHeadersMap] = new Map(init)
this[kHeadersSortedMap] = null
}
}
// https://fetch.spec.whatwg.org/#header-list-contains
contains (name) {
// A header list list contains a header name name if list
// contains a header whose name is a byte-case-insensitive
// match for name.
name = name.toLowerCase()
return this[kHeadersMap].has(name)
}
clear () {
this[kHeadersMap].clear()
this[kHeadersSortedMap] = null
}
// https://fetch.spec.whatwg.org/#concept-header-list-append
append (name, value) {
this[kHeadersSortedMap] = null
// 1. If list contains name, then set name to the first such
// header’s name.
name = name.toLowerCase()
const exists = this[kHeadersMap].get(name)
// 2. Append (name, value) to list.
if (exists) {
this[kHeadersMap].set(name, `${exists}, ${value}`)
} else {
this[kHeadersMap].set(name, `${value}`)
}
}
// https://fetch.spec.whatwg.org/#concept-header-list-set
set (name, value) {
this[kHeadersSortedMap] = null
// 1. If list contains name, then set the value of
// the first such header to value and remove the
// others.
// 2. Otherwise, append header (name, value) to list.
return this[kHeadersMap].set(name, value)
}
// https://fetch.spec.whatwg.org/#concept-header-list-delete
delete (name) {
this[kHeadersSortedMap] = null
name = name.toLowerCase()
return this[kHeadersMap].delete(name)
}
// https://fetch.spec.whatwg.org/#concept-header-list-get
get (name) {
name = name.toLowerCase()
// 1. If list does not contain name, then return null.
if (!this.contains(name)) {
return null
}
// 2. Return the values of all headers in list whose name
// is a byte-case-insensitive match for name,
// separated from each other by 0x2C 0x20, in order.
return this[kHeadersMap].get(name) ?? null
}
has (name) {
name = name.toLowerCase()
return this[kHeadersMap].has(name)
}
keys () {
return this[kHeadersMap].keys()
}
values () {
return this[kHeadersMap].values()
}
entries () {
return this[kHeadersMap].entries()
}
[Symbol.iterator] () {
return this[kHeadersMap][Symbol.iterator]()
}
}
// https://fetch.spec.whatwg.org/#headers-class
class Headers {
constructor (init = undefined) {
this[kHeadersList] = new HeadersList()
// The new Headers(init) constructor steps are:
// 1. Set this’s guard to "none".
this[kGuard] = 'none'
// 2. If init is given, then fill this with init.
if (init !== undefined) {
init = webidl.converters.HeadersInit(init)
fill(this, init)
}
}
get [Symbol.toStringTag] () {
return this.constructor.name
}
// https://fetch.spec.whatwg.org/#dom-headers-append
append (name, value) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) {
throw new TypeError(
`Failed to execute 'append' on 'Headers': 2 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name)
value = webidl.converters.ByteString(value)
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
}
// 3. If headers’s guard is "immutable", then throw a TypeError.
// 4. Otherwise, if headers’s guard is "request" and name is a
// forbidden header name, return.
// Note: undici does not implement forbidden header names
if (this[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (this[kGuard] === 'request-no-cors') {
// 5. Otherwise, if headers’s guard is "request-no-cors":
// TODO
}
// 6. Otherwise, if headers’s guard is "response" and name is a
// forbidden response-header name, return.
// 7. Append (name, value) to headers’s header list.
// 8. If headers’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
return this[kHeadersList].append(name, value)
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
delete (name) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'delete' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.delete',
value: name,
type: 'header name'
})
}
// 2. If this’s guard is "immutable", then throw a TypeError.
// 3. Otherwise, if this’s guard is "request" and name is a
// forbidden header name, return.
// 4. Otherwise, if this’s guard is "request-no-cors", name
// is not a no-CORS-safelisted request-header name, and
// name is not a privileged no-CORS request-header name,
// return.
// 5. Otherwise, if this’s guard is "response" and name is
// a forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (this[kGuard] === 'request-no-cors') {
// TODO
}
// 6. If this’s header list does not contain name, then
// return.
if (!this[kHeadersList].contains(name)) {
return
}
// 7. Delete name from this’s header list.
// 8. If this’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from this.
return this[kHeadersList].delete(name)
}
// https://fetch.spec.whatwg.org/#dom-headers-get
get (name) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'get' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.get',
value: name,
type: 'header name'
})
}
// 2. Return the result of getting name from this’s header
// list.
return this[kHeadersList].get(name)
}
// https://fetch.spec.whatwg.org/#dom-headers-has
has (name) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'has' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.has',
value: name,
type: 'header name'
})
}
// 2. Return true if this’s header list contains name;
// otherwise false.
return this[kHeadersList].contains(name)
}
// https://fetch.spec.whatwg.org/#dom-headers-set
set (name, value) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) {
throw new TypeError(
`Failed to execute 'set' on 'Headers': 2 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name)
value = webidl.converters.ByteString(value)
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.set',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
webidl.errors.invalidArgument({
prefix: 'Headers.set',
value,
type: 'header value'
})
}
// 3. If this’s guard is "immutable", then throw a TypeError.
// 4. Otherwise, if this’s guard is "request" and name is a
// forbidden header name, return.
// 5. Otherwise, if this’s guard is "request-no-cors" and
// name/value is not a no-CORS-safelisted request-header,
// return.
// 6. Otherwise, if this’s guard is "response" and name is a
// forbidden response-header name, return.
// Note: undici does not implement forbidden header names
if (this[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (this[kGuard] === 'request-no-cors') {
// TODO
}
// 7. Set (name, value) in this’s header list.
// 8. If this’s guard is "request-no-cors", then remove
// privileged no-CORS request headers from this
return this[kHeadersList].set(name, value)
}
get [kHeadersSortedMap] () {
this[kHeadersList][kHeadersSortedMap] ??= new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
return this[kHeadersList][kHeadersSortedMap]
}
keys () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(this[kHeadersSortedMap].keys(), 'Headers')
}
values () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(this[kHeadersSortedMap].values(), 'Headers')
}
entries () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return makeIterator(this[kHeadersSortedMap].entries(), 'Headers')
}
/**
* @param {(value: string, key: string, self: Headers) => void} callbackFn
* @param {unknown} thisArg
*/
forEach (callbackFn, thisArg = globalThis) {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'forEach' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
if (typeof callbackFn !== 'function') {
throw new TypeError(
"Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'."
)
}
for (const [key, value] of this) {
callbackFn.apply(thisArg, [value, key, this])
}
}
[Symbol.for('nodejs.util.inspect.custom')] () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}
return this[kHeadersList]
}
}
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
Object.defineProperties(Headers.prototype, {
append: kEnumerableProperty,
delete: kEnumerableProperty,
get: kEnumerableProperty,
has: kEnumerableProperty,
set: kEnumerableProperty,
keys: kEnumerableProperty,
values: kEnumerableProperty,
entries: kEnumerableProperty,
forEach: kEnumerableProperty
})
webidl.converters.HeadersInit = function (V) {
if (webidl.util.Type(V) === 'Object') {
if (V[Symbol.iterator]) {
return webidl.converters['sequence<sequence<ByteString>>'](V)
}
return webidl.converters['record<ByteString, ByteString>'](V)
}
webidl.errors.conversionFailed({
prefix: 'Headers constructor',
argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
})
}
module.exports = {
fill,
Headers,
HeadersList
}
+2039
View File
File diff suppressed because it is too large Load Diff
+926
View File
@@ -0,0 +1,926 @@
/* globals AbortController */
'use strict'
const { extractBody, mixinBody, cloneBody } = require('./body')
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
const util = require('../core/util')
const {
isValidHTTPToken,
sameOrigin,
normalizeMethod
} = require('./util')
const {
forbiddenMethods,
corsSafeListedMethods,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache
} = require('./constants')
const { kEnumerableProperty } = util
const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl')
const { kHeadersList } = require('../core/symbols')
const assert = require('assert')
let TransformStream
const kInit = Symbol('init')
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
signal.removeEventListener('abort', abort)
})
// https://fetch.spec.whatwg.org/#request-class
class Request {
// https://fetch.spec.whatwg.org/#dom-request
constructor (input, init = {}) {
if (input === kInit) {
return
}
if (arguments.length < 1) {
throw new TypeError(
`Failed to construct 'Request': 1 argument required, but only ${arguments.length} present.`
)
}
input = webidl.converters.RequestInfo(input)
init = webidl.converters.RequestInit(init)
// TODO
this[kRealm] = { settingsObject: {} }
// 1. Let request be null.
let request = null
// 2. Let fallbackMode be null.
let fallbackMode = null
// 3. Let baseURL be this’s relevant settings object’s API base URL.
const baseUrl = this[kRealm].settingsObject.baseUrl
// 4. Let signal be null.
let signal = null
// 5. If input is a string, then:
if (typeof input === 'string') {
// 1. Let parsedURL be the result of parsing input with baseURL.
// 2. If parsedURL is failure, then throw a TypeError.
let parsedURL
try {
parsedURL = new URL(input, baseUrl)
} catch (err) {
throw new TypeError('Failed to parse URL from ' + input, { cause: err })
}
// 3. If parsedURL includes credentials, then throw a TypeError.
if (parsedURL.username || parsedURL.password) {
throw new TypeError(
'Request cannot be constructed from a URL that includes credentials: ' +
input
)
}
// 4. Set request to a new request whose URL is parsedURL.
request = makeRequest({ urlList: [parsedURL] })
// 5. Set fallbackMode to "cors".
fallbackMode = 'cors'
} else {
// 6. Otherwise:
// 7. Assert: input is a Request object.
assert(input instanceof Request)
// 8. Set request to input’s request.
request = input[kState]
// 9. Set signal to input’s signal.
signal = input[kSignal]
}
// 7. Let origin be this’s relevant settings object’s origin.
const origin = this[kRealm].settingsObject.origin
// 8. Let window be "client".
let window = 'client'
// 9. If request’s window is an environment settings object and its origin
// is same origin with origin, then set window to request’s window.
if (
request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
sameOrigin(request.window, origin)
) {
window = request.window
}
// 10. If init["window"] exists and is non-null, then throw a TypeError.
if (init.window !== undefined && init.window != null) {
throw new TypeError(`'window' option '${window}' must be null`)
}
// 11. If init["window"] exists, then set window to "no-window".
if (init.window !== undefined) {
window = 'no-window'
}
// 12. Set request to a new request with the following properties:
request = makeRequest({
// URL request’s URL.
// undici implementation note: this is set as the first item in request's urlList in makeRequest
// method request’s method.
method: request.method,
// header list A copy of request’s header list.
// undici implementation note: headersList is cloned in makeRequest
headersList: request.headersList,
// unsafe-request flag Set.
unsafeRequest: request.unsafeRequest,
// client This’s relevant settings object.
client: this[kRealm].settingsObject,
// window window.
window,
// priority request’s priority.
priority: request.priority,
// origin request’s origin. The propagation of the origin is only significant for navigation requests
// being handled by a service worker. In this scenario a request can have an origin that is different
// from the current client.
origin: request.origin,
// referrer request’s referrer.
referrer: request.referrer,
// referrer policy request’s referrer policy.
referrerPolicy: request.referrerPolicy,
// mode request’s mode.
mode: request.mode,
// credentials mode request’s credentials mode.
credentials: request.credentials,
// cache mode request’s cache mode.
cache: request.cache,
// redirect mode request’s redirect mode.
redirect: request.redirect,
// integrity metadata request’s integrity metadata.
integrity: request.integrity,
// keepalive request’s keepalive.
keepalive: request.keepalive,
// reload-navigation flag request’s reload-navigation flag.
reloadNavigation: request.reloadNavigation,
// history-navigation flag request’s history-navigation flag.
historyNavigation: request.historyNavigation,
// URL list A clone of request’s URL list.
urlList: [...request.urlList]
})
// 13. If init is not empty, then:
if (Object.keys(init).length > 0) {
// 1. If request’s mode is "navigate", then set it to "same-origin".
if (request.mode === 'navigate') {
request.mode = 'same-origin'
}
// 2. Unset request’s reload-navigation flag.
request.reloadNavigation = false
// 3. Unset request’s history-navigation flag.
request.historyNavigation = false
// 4. Set request’s origin to "client".
request.origin = 'client'
// 5. Set request’s referrer to "client"
request.referrer = 'client'
// 6. Set request’s referrer policy to the empty string.
request.referrerPolicy = ''
// 7. Set request’s URL to request’s current URL.
request.url = request.urlList[request.urlList.length - 1]
// 8. Set request’s URL list to « request’s URL ».
request.urlList = [request.url]
}
// 14. If init["referrer"] exists, then:
if (init.referrer !== undefined) {
// 1. Let referrer be init["referrer"].
const referrer = init.referrer
// 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
if (referrer === '') {
request.referrer = 'no-referrer'
} else {
// 1. Let parsedReferrer be the result of parsing referrer with
// baseURL.
// 2. If parsedReferrer is failure, then throw a TypeError.
let parsedReferrer
try {
parsedReferrer = new URL(referrer, baseUrl)
} catch (err) {
throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
}
// 3. If one of the following is true
// parsedReferrer’s cannot-be-a-base-URL is true, scheme is "about",
// and path contains a single string "client"
// parsedReferrer’s origin is not same origin with origin
// then set request’s referrer to "client".
// TODO
// 4. Otherwise, set request’s referrer to parsedReferrer.
request.referrer = parsedReferrer
}
}
// 15. If init["referrerPolicy"] exists, then set request’s referrer policy
// to it.
if (init.referrerPolicy !== undefined) {
request.referrerPolicy = init.referrerPolicy
if (!referrerPolicy.includes(request.referrerPolicy)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.referrerPolicy}' is not a valid enum value of type ReferrerPolicy.`
)
}
}
// 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
let mode
if (init.mode !== undefined) {
mode = init.mode
if (!requestMode.includes(mode)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.mode}' is not a valid enum value of type RequestMode.`
)
}
} else {
mode = fallbackMode
}
// 17. If mode is "navigate", then throw a TypeError.
if (mode === 'navigate') {
webidl.errors.exception({
header: 'Request constructor',
message: 'invalid request mode navigate.'
})
}
// 18. If mode is non-null, set request’s mode to mode.
if (mode != null) {
request.mode = mode
}
// 19. If init["credentials"] exists, then set request’s credentials mode
// to it.
if (init.credentials !== undefined) {
request.credentials = init.credentials
if (!requestCredentials.includes(request.credentials)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.credentials}' is not a valid enum value of type RequestCredentials.`
)
}
}
// 18. If init["cache"] exists, then set request’s cache mode to it.
if (init.cache !== undefined) {
request.cache = init.cache
if (!requestCache.includes(request.cache)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.cache}' is not a valid enum value of type RequestCache.`
)
}
}
// 21. If request’s cache mode is "only-if-cached" and request’s mode is
// not "same-origin", then throw a TypeError.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
throw new TypeError(
"'only-if-cached' can be set only with 'same-origin' mode"
)
}
// 22. If init["redirect"] exists, then set request’s redirect mode to it.
if (init.redirect !== undefined) {
request.redirect = init.redirect
if (!requestRedirect.includes(request.redirect)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.redirect}' is not a valid enum value of type RequestRedirect.`
)
}
}
// 23. If init["integrity"] exists, then set request’s integrity metadata to it.
if (init.integrity !== undefined && init.integrity != null) {
request.integrity = String(init.integrity)
}
// 24. If init["keepalive"] exists, then set request’s keepalive to it.
if (init.keepalive !== undefined) {
request.keepalive = Boolean(init.keepalive)
}
// 25. If init["method"] exists, then:
if (init.method !== undefined) {
// 1. Let method be init["method"].
let method = init.method
// 2. If method is not a method or method is a forbidden method, then
// throw a TypeError.
if (!isValidHTTPToken(init.method)) {
throw TypeError(`'${init.method}' is not a valid HTTP method.`)
}
if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) {
throw TypeError(`'${init.method}' HTTP method is unsupported.`)
}
// 3. Normalize method.
method = normalizeMethod(init.method)
// 4. Set request’s method to method.
request.method = method
}
// 26. If init["signal"] exists, then set signal to it.
if (init.signal !== undefined) {
signal = init.signal
}
// 27. Set this’s request to request.
this[kState] = request
// 28. Set this’s signal to a new AbortSignal object with this’s relevant
// Realm.
const ac = new AbortController()
this[kSignal] = ac.signal
this[kSignal][kRealm] = this[kRealm]
// 29. If signal is not null, then make this’s signal follow signal.
if (signal != null) {
if (
!signal ||
typeof signal.aborted !== 'boolean' ||
typeof signal.addEventListener !== 'function'
) {
throw new TypeError(
"Failed to construct 'Request': member signal is not of type AbortSignal."
)
}
if (signal.aborted) {
ac.abort()
} else {
const abort = () => ac.abort()
signal.addEventListener('abort', abort, { once: true })
requestFinalizer.register(this, { signal, abort })
}
}
// 30. Set this’s headers to a new Headers object with this’s relevant
// Realm, whose header list is request’s header list and guard is
// "request".
this[kHeaders] = new Headers()
this[kHeaders][kHeadersList] = request.headersList
this[kHeaders][kGuard] = 'request'
this[kHeaders][kRealm] = this[kRealm]
// 31. If this’s request’s mode is "no-cors", then:
if (mode === 'no-cors') {
// 1. If this’s request’s method is not a CORS-safelisted method,
// then throw a TypeError.
if (!corsSafeListedMethods.includes(request.method)) {
throw new TypeError(
`'${request.method} is unsupported in no-cors mode.`
)
}
// 2. Set this’s headers’s guard to "request-no-cors".
this[kHeaders][kGuard] = 'request-no-cors'
}
// 32. If init is not empty, then:
if (Object.keys(init).length !== 0) {
// 1. Let headers be a copy of this’s headers and its associated header
// list.
let headers = new Headers(this[kHeaders])
// 2. If init["headers"] exists, then set headers to init["headers"].
if (init.headers !== undefined) {
headers = init.headers
}
// 3. Empty this’s headers’s header list.
this[kHeaders][kHeadersList].clear()
// 4. If headers is a Headers object, then for each header in its header
// list, append header’s name/header’s value to this’s headers.
if (headers.constructor.name === 'Headers') {
for (const [key, val] of headers) {
this[kHeaders].append(key, val)
}
} else {
// 5. Otherwise, fill this’s headers with headers.
fillHeaders(this[kHeaders], headers)
}
}
// 33. Let inputBody be input’s request’s body if input is a Request
// object; otherwise null.
const inputBody = input instanceof Request ? input[kState].body : null
// 34. If either init["body"] exists and is non-null or inputBody is
// non-null, and request’s method is `GET` or `HEAD`, then throw a
// TypeError.
if (
((init.body !== undefined && init.body != null) || inputBody != null) &&
(request.method === 'GET' || request.method === 'HEAD')
) {
throw new TypeError('Request with GET/HEAD method cannot have body.')
}
// 35. Let initBody be null.
let initBody = null
// 36. If init["body"] exists and is non-null, then:
if (init.body !== undefined && init.body != null) {
// 1. Let Content-Type be null.
// 2. Set initBody and Content-Type to the result of extracting
// init["body"], with keepalive set to request’s keepalive.
const [extractedBody, contentType] = extractBody(
init.body,
request.keepalive
)
initBody = extractedBody
// 3, If Content-Type is non-null and this’s headers’s header list does
// not contain `Content-Type`, then append `Content-Type`/Content-Type to
// this’s headers.
if (contentType && !this[kHeaders].has('content-type')) {
this[kHeaders].append('content-type', contentType)
}
}
// 37. Let inputOrInitBody be initBody if it is non-null; otherwise
// inputBody.
const inputOrInitBody = initBody ?? inputBody
// 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
// null, then:
if (inputOrInitBody != null && inputOrInitBody.source == null) {
// 1. If this’s request’s mode is neither "same-origin" nor "cors",
// then throw a TypeError.
if (request.mode !== 'same-origin' && request.mode !== 'cors') {
throw new TypeError(
'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
)
}
// 2. Set this’s request’s use-CORS-preflight flag.
request.useCORSPreflightFlag = true
}
// 39. Let finalBody be inputOrInitBody.
let finalBody = inputOrInitBody
// 40. If initBody is null and inputBody is non-null, then:
if (initBody == null && inputBody != null) {
// 1. If input is unusable, then throw a TypeError.
if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) {
throw new TypeError(
'Cannot construct a Request with a Request object that has already been used.'
)
}
// 2. Set finalBody to the result of creating a proxy for inputBody.
if (!TransformStream) {
TransformStream = require('stream/web').TransformStream
}
// https://streams.spec.whatwg.org/#readablestream-create-a-proxy
const identityTransform = new TransformStream()
inputBody.stream.pipeThrough(identityTransform)
finalBody = {
source: inputBody.source,
length: inputBody.length,
stream: identityTransform.readable
}
}
// 41. Set this’s request’s body to finalBody.
this[kState].body = finalBody
}
get [Symbol.toStringTag] () {
return this.constructor.name
}
// Returns request’s HTTP method, which is "GET" by default.
get method () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The method getter steps are to return this’s request’s method.
return this[kState].method
}
// Returns the URL of request as a string.
get url () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The url getter steps are to return this’s request’s URL, serialized.
return this[kState].url.toString()
}
// Returns a Headers object consisting of the headers associated with request.
// Note that headers added in the network layer by the user agent will not
// be accounted for in this object, e.g., the "Host" header.
get headers () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The headers getter steps are to return this’s headers.
return this[kHeaders]
}
// Returns the kind of resource requested by request, e.g., "document"
// or "script".
get destination () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The destination getter are to return this’s request’s destination.
return this[kState].destination
}
// Returns the referrer of request. Its value can be a same-origin URL if
// explicitly set in init, the empty string to indicate no referrer, and
// "about:client" when defaulting to the global’s default. This is used
// during fetching to determine the value of the `Referer` header of the
// request being made.
get referrer () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// 1. If this’s request’s referrer is "no-referrer", then return the
// empty string.
if (this[kState].referrer === 'no-referrer') {
return ''
}
// 2. If this’s request’s referrer is "client", then return
// "about:client".
if (this[kState].referrer === 'client') {
return 'about:client'
}
// Return this’s request’s referrer, serialized.
return this[kState].referrer.toString()
}
// Returns the referrer policy associated with request.
// This is used during fetching to compute the value of the request’s
// referrer.
get referrerPolicy () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The referrerPolicy getter steps are to return this’s request’s referrer policy.
return this[kState].referrerPolicy
}
// Returns the mode associated with request, which is a string indicating
// whether the request will use CORS, or will be restricted to same-origin
// URLs.
get mode () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The mode getter steps are to return this’s request’s mode.
return this[kState].mode
}
// Returns the credentials mode associated with request,
// which is a string indicating whether credentials will be sent with the
// request always, never, or only when sent to a same-origin URL.
get credentials () {
// The credentials getter steps are to return this’s request’s credentials mode.
return this[kState].credentials
}
// Returns the cache mode associated with request,
// which is a string indicating how the request will
// interact with the browser’s cache when fetching.
get cache () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The cache getter steps are to return this’s request’s cache mode.
return this[kState].cache
}
// Returns the redirect mode associated with request,
// which is a string indicating how redirects for the
// request will be handled during fetching. A request
// will follow redirects by default.
get redirect () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The redirect getter steps are to return this’s request’s redirect mode.
return this[kState].redirect
}
// Returns request’s subresource integrity metadata, which is a
// cryptographic hash of the resource being fetched. Its value
// consists of multiple hashes separated by whitespace. [SRI]
get integrity () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The integrity getter steps are to return this’s request’s integrity
// metadata.
return this[kState].integrity
}
// Returns a boolean indicating whether or not request can outlive the
// global in which it was created.
get keepalive () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The keepalive getter steps are to return this’s request’s keepalive.
return this[kState].keepalive
}
// Returns a boolean indicating whether or not request is for a reload
// navigation.
get isReloadNavigation () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The isReloadNavigation getter steps are to return true if this’s
// request’s reload-navigation flag is set; otherwise false.
return this[kState].reloadNavigation
}
// Returns a boolean indicating whether or not request is for a history
// navigation (a.k.a. back-foward navigation).
get isHistoryNavigation () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The isHistoryNavigation getter steps are to return true if this’s request’s
// history-navigation flag is set; otherwise false.
return this[kState].historyNavigation
}
// Returns the signal associated with request, which is an AbortSignal
// object indicating whether or not request has been aborted, and its
// abort event handler.
get signal () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// The signal getter steps are to return this’s signal.
return this[kSignal]
}
// Returns a clone of request.
clone () {
if (!(this instanceof Request)) {
throw new TypeError('Illegal invocation')
}
// 1. If this is unusable, then throw a TypeError.
if (this.bodyUsed || this.body?.locked) {
throw new TypeError('unusable')
}
// 2. Let clonedRequest be the result of cloning this’s request.
const clonedRequest = cloneRequest(this[kState])
// 3. Let clonedRequestObject be the result of creating a Request object,
// given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
const clonedRequestObject = new Request(kInit)
clonedRequestObject[kState] = clonedRequest
clonedRequestObject[kRealm] = this[kRealm]
clonedRequestObject[kHeaders] = new Headers()
clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList
clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]
clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]
// 4. Make clonedRequestObject’s signal follow this’s signal.
const ac = new AbortController()
if (this.signal.aborted) {
ac.abort()
} else {
this.signal.addEventListener(
'abort',
function () {
ac.abort()
},
{ once: true }
)
}
clonedRequestObject[kSignal] = ac.signal
// 4. Return clonedRequestObject.
return clonedRequestObject
}
}
mixinBody(Request)
function makeRequest (init) {
// https://fetch.spec.whatwg.org/#requests
const request = {
method: 'GET',
localURLsOnly: false,
unsafeRequest: false,
body: null,
client: null,
reservedClient: null,
replacesClientId: '',
window: 'client',
keepalive: false,
serviceWorkers: 'all',
initiator: '',
destination: '',
priority: null,
origin: 'client',
policyContainer: 'client',
referrer: 'client',
referrerPolicy: '',
mode: 'no-cors',
useCORSPreflightFlag: false,
credentials: 'same-origin',
useCredentials: false,
cache: 'default',
redirect: 'follow',
integrity: '',
cryptoGraphicsNonceMetadata: '',
parserMetadata: '',
reloadNavigation: false,
historyNavigation: false,
userActivation: false,
taintedOrigin: false,
redirectCount: 0,
responseTainting: 'basic',
preventNoCacheCacheControlHeaderModification: false,
done: false,
timingAllowFailed: false,
...init,
headersList: init.headersList
? new HeadersList(init.headersList)
: new HeadersList()
}
request.url = request.urlList[0]
return request
}
// https://fetch.spec.whatwg.org/#concept-request-clone
function cloneRequest (request) {
// To clone a request request, run these steps:
// 1. Let newRequest be a copy of request, except for its body.
const newRequest = makeRequest({ ...request, body: null })
// 2. If request’s body is non-null, set newRequest’s body to the
// result of cloning request’s body.
if (request.body != null) {
newRequest.body = cloneBody(request.body)
}
// 3. Return newRequest.
return newRequest
}
Object.defineProperties(Request.prototype, {
method: kEnumerableProperty,
url: kEnumerableProperty,
headers: kEnumerableProperty,
redirect: kEnumerableProperty,
clone: kEnumerableProperty,
signal: kEnumerableProperty
})
webidl.converters.Request = webidl.interfaceConverter(
Request
)
// https://fetch.spec.whatwg.org/#requestinfo
webidl.converters.RequestInfo = function (V) {
if (typeof V === 'string') {
return webidl.converters.USVString(V)
}
if (V instanceof Request) {
return webidl.converters.Request(V)
}
return webidl.converters.USVString(V)
}
webidl.converters.AbortSignal = webidl.interfaceConverter(
AbortSignal
)
// https://fetch.spec.whatwg.org/#requestinit
webidl.converters.RequestInit = webidl.dictionaryConverter([
{
key: 'method',
converter: webidl.converters.ByteString
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
},
{
key: 'body',
converter: webidl.nullableConverter(
webidl.converters.BodyInit
)
},
{
key: 'referrer',
converter: webidl.converters.USVString
},
{
key: 'referrerPolicy',
converter: webidl.converters.DOMString,
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
allowedValues: [
'', 'no-referrer', 'no-referrer-when-downgrade',
'same-origin', 'origin', 'strict-origin',
'origin-when-cross-origin', 'strict-origin-when-cross-origin',
'unsafe-url'
]
},
{
key: 'mode',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#concept-request-mode
allowedValues: [
'same-origin', 'cors', 'no-cors', 'navigate', 'websocket'
]
},
{
key: 'credentials',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcredentials
allowedValues: [
'omit', 'same-origin', 'include'
]
},
{
key: 'cache',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcache
allowedValues: [
'default', 'no-store', 'reload', 'no-cache', 'force-cache',
'only-if-cached'
]
},
{
key: 'redirect',
converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestredirect
allowedValues: [
'follow', 'error', 'manual'
]
},
{
key: 'integrity',
converter: webidl.converters.DOMString
},
{
key: 'keepalive',
converter: webidl.converters.boolean
},
{
key: 'signal',
converter: webidl.nullableConverter(
webidl.converters.AbortSignal
)
},
{
key: 'window',
converter: webidl.converters.any
}
])
module.exports = { Request, makeRequest }
+580
View File
@@ -0,0 +1,580 @@
'use strict'
const { Headers, HeadersList, fill } = require('./headers')
const { extractBody, cloneBody, mixinBody } = require('./body')
const util = require('../core/util')
const { kEnumerableProperty } = util
const {
responseURL,
isValidReasonPhrase,
isCancelled,
isAborted,
isBlobLike,
serializeJavascriptValueToJSONString
} = require('./util')
const {
redirectStatus,
nullBodyStatus,
DOMException
} = require('./constants')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl')
const { FormData } = require('./formdata')
const { kHeadersList } = require('../core/symbols')
const assert = require('assert')
const { types } = require('util')
const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
// https://fetch.spec.whatwg.org/#response-class
class Response {
// Creates network error Response.
static error () {
// TODO
const relevantRealm = { settingsObject: {} }
// The static error() method steps are to return the result of creating a
// Response object, given a new network error, "immutable", and this’s
// relevant Realm.
const responseObject = new Response()
responseObject[kState] = makeNetworkError()
responseObject[kRealm] = relevantRealm
responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList
responseObject[kHeaders][kGuard] = 'immutable'
responseObject[kHeaders][kRealm] = relevantRealm
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response-json
static json (data, init = {}) {
if (arguments.length === 0) {
throw new TypeError(
'Failed to execute \'json\' on \'Response\': 1 argument required, but 0 present.'
)
}
if (init !== null) {
init = webidl.converters.ResponseInit(init)
}
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
const bytes = new TextEncoder('utf-8').encode(
serializeJavascriptValueToJSONString(data)
)
// 2. Let body be the result of extracting bytes.
const body = extractBody(bytes)
// 3. Let responseObject be the result of creating a Response object, given a new response,
// "response", and this’s relevant Realm.
const relevantRealm = { settingsObject: {} }
const responseObject = new Response()
responseObject[kRealm] = relevantRealm
responseObject[kHeaders][kGuard] = 'response'
responseObject[kHeaders][kRealm] = relevantRealm
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
// 5. Return responseObject.
return responseObject
}
// Creates a redirect Response that redirects to url with status status.
static redirect (url, status = 302) {
const relevantRealm = { settingsObject: {} }
if (arguments.length < 1) {
throw new TypeError(
`Failed to execute 'redirect' on 'Response': 1 argument required, but only ${arguments.length} present.`
)
}
url = webidl.converters.USVString(url)
status = webidl.converters['unsigned short'](status)
// 1. Let parsedURL be the result of parsing url with current settings
// object’s API base URL.
// 2. If parsedURL is failure, then throw a TypeError.
// TODO: base-URL?
let parsedURL
try {
parsedURL = new URL(url)
} catch (err) {
throw Object.assign(new TypeError('Failed to parse URL from ' + url), {
cause: err
})
}
// 3. If status is not a redirect status, then throw a RangeError.
if (!redirectStatus.includes(status)) {
throw new RangeError('Invalid status code')
}
// 4. Let responseObject be the result of creating a Response object,
// given a new response, "immutable", and this’s relevant Realm.
const responseObject = new Response()
responseObject[kRealm] = relevantRealm
responseObject[kHeaders][kGuard] = 'immutable'
responseObject[kHeaders][kRealm] = relevantRealm
// 5. Set responseObject’s response’s status to status.
responseObject[kState].status = status
// 6. Let value be parsedURL, serialized and isomorphic encoded.
// TODO: isomorphic encoded?
const value = parsedURL.toString()
// 7. Append `Location`/value to responseObject’s response’s header list.
responseObject[kState].headersList.append('location', value)
// 8. Return responseObject.
return responseObject
}
// https://fetch.spec.whatwg.org/#dom-response
constructor (body = null, init = {}) {
if (body !== null) {
body = webidl.converters.BodyInit(body)
}
init = webidl.converters.ResponseInit(init)
// TODO
this[kRealm] = { settingsObject: {} }
// 1. Set this’s response to a new response.
this[kState] = makeResponse({})
// 2. Set this’s headers to a new Headers object with this’s relevant
// Realm, whose header list is this’s response’s header list and guard
// is "response".
this[kHeaders] = new Headers()
this[kHeaders][kGuard] = 'response'
this[kHeaders][kHeadersList] = this[kState].headersList
this[kHeaders][kRealm] = this[kRealm]
// 3. Let bodyWithType be null.
let bodyWithType = null
// 4. If body is non-null, then set bodyWithType to the result of extracting body.
if (body != null) {
const [extractedBody, type] = extractBody(body)
bodyWithType = { body: extractedBody, type }
}
// 5. Perform initialize a response given this, init, and bodyWithType.
initializeResponse(this, init, bodyWithType)
}
get [Symbol.toStringTag] () {
return this.constructor.name
}
// Returns response’s type, e.g., "cors".
get type () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The type getter steps are to return this’s response’s type.
return this[kState].type
}
// Returns response’s URL, if it has one; otherwise the empty string.
get url () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The url getter steps are to return the empty string if this’s
// response’s URL is null; otherwise this’s response’s URL,
// serialized with exclude fragment set to true.
let url = responseURL(this[kState])
if (url == null) {
return ''
}
if (url.hash) {
url = new URL(url)
url.hash = ''
}
return url.toString()
}
// Returns whether response was obtained through a redirect.
get redirected () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The redirected getter steps are to return true if this’s response’s URL
// list has more than one item; otherwise false.
return this[kState].urlList.length > 1
}
// Returns response’s status.
get status () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The status getter steps are to return this’s response’s status.
return this[kState].status
}
// Returns whether response’s status is an ok status.
get ok () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The ok getter steps are to return true if this’s response’s status is an
// ok status; otherwise false.
return this[kState].status >= 200 && this[kState].status <= 299
}
// Returns response’s status message.
get statusText () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The statusText getter steps are to return this’s response’s status
// message.
return this[kState].statusText
}
// Returns response’s headers as Headers.
get headers () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// The headers getter steps are to return this’s headers.
return this[kHeaders]
}
// Returns a clone of response.
clone () {
if (!(this instanceof Response)) {
throw new TypeError('Illegal invocation')
}
// 1. If this is unusable, then throw a TypeError.
if (this.bodyUsed || (this.body && this.body.locked)) {
webidl.errors.exception({
header: 'Response.clone',
message: 'Body has already been consumed.'
})
}
// 2. Let clonedResponse be the result of cloning this’s response.
const clonedResponse = cloneResponse(this[kState])
// 3. Return the result of creating a Response object, given
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
const clonedResponseObject = new Response()
clonedResponseObject[kState] = clonedResponse
clonedResponseObject[kRealm] = this[kRealm]
clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList
clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard]
clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm]
return clonedResponseObject
}
}
mixinBody(Response)
Object.defineProperties(Response.prototype, {
type: kEnumerableProperty,
url: kEnumerableProperty,
status: kEnumerableProperty,
ok: kEnumerableProperty,
redirected: kEnumerableProperty,
statusText: kEnumerableProperty,
headers: kEnumerableProperty,
clone: kEnumerableProperty
})
// https://fetch.spec.whatwg.org/#concept-response-clone
function cloneResponse (response) {
// To clone a response response, run these steps:
// 1. If response is a filtered response, then return a new identical
// filtered response whose internal response is a clone of response’s
// internal response.
if (response.internalResponse) {
return filterResponse(
cloneResponse(response.internalResponse),
response.type
)
}
// 2. Let newResponse be a copy of response, except for its body.
const newResponse = makeResponse({ ...response, body: null })
// 3. If response’s body is non-null, then set newResponse’s body to the
// result of cloning response’s body.
if (response.body != null) {
newResponse.body = cloneBody(response.body)
}
// 4. Return newResponse.
return newResponse
}
function makeResponse (init) {
return {
aborted: false,
rangeRequested: false,
timingAllowPassed: false,
requestIncludesCredentials: false,
type: 'default',
status: 200,
timingInfo: null,
cacheState: '',
statusText: '',
...init,
headersList: init.headersList
? new HeadersList(init.headersList)
: new HeadersList(),
urlList: init.urlList ? [...init.urlList] : []
}
}
function makeNetworkError (reason) {
return makeResponse({
type: 'error',
status: 0,
error:
reason instanceof Error
? reason
: new Error(reason ? String(reason) : reason, {
cause: reason instanceof Error ? reason : undefined
}),
aborted: reason && reason.name === 'AbortError'
})
}
function makeFilteredResponse (response, state) {
state = {
internalResponse: response,
...state
}
return new Proxy(response, {
get (target, p) {
return p in state ? state[p] : target[p]
},
set (target, p, value) {
assert(!(p in state))
target[p] = value
return true
}
})
}
// https://fetch.spec.whatwg.org/#concept-filtered-response
function filterResponse (response, type) {
// Set response to the following filtered response with response as its
// internal response, depending on request’s response tainting:
if (type === 'basic') {
// A basic filtered response is a filtered response whose type is "basic"
// and header list excludes any headers in internal response’s header list
// whose name is a forbidden response-header name.
// Note: undici does not implement forbidden response-header names
return makeFilteredResponse(response, {
type: 'basic',
headersList: response.headersList
})
} else if (type === 'cors') {
// A CORS filtered response is a filtered response whose type is "cors"
// and header list excludes any headers in internal response’s header
// list whose name is not a CORS-safelisted response-header name, given
// internal response’s CORS-exposed header-name list.
// Note: undici does not implement CORS-safelisted response-header names
return makeFilteredResponse(response, {
type: 'cors',
headersList: response.headersList
})
} else if (type === 'opaque') {
// An opaque filtered response is a filtered response whose type is
// "opaque", URL list is the empty list, status is 0, status message
// is the empty byte sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaque',
urlList: Object.freeze([]),
status: 0,
statusText: '',
body: null
})
} else if (type === 'opaqueredirect') {
// An opaque-redirect filtered response is a filtered response whose type
// is "opaqueredirect", status is 0, status message is the empty byte
// sequence, header list is empty, and body is null.
return makeFilteredResponse(response, {
type: 'opaqueredirect',
status: 0,
statusText: '',
headersList: [],
body: null
})
} else {
assert(false)
}
}
// https://fetch.spec.whatwg.org/#appropriate-network-error
function makeAppropriateNetworkError (fetchParams) {
// 1. Assert: fetchParams is canceled.
assert(isCancelled(fetchParams))
// 2. Return an aborted network error if fetchParams is aborted;
// otherwise return a network error.
return isAborted(fetchParams)
? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError'))
: makeNetworkError(fetchParams.controller.terminated.reason)
}
// https://whatpr.org/fetch/1392.html#initialize-a-response
function initializeResponse (response, init, body) {
// 1. If init["status"] is not in the range 200 to 599, inclusive, then
// throw a RangeError.
if (init.status !== null && (init.status < 200 || init.status > 599)) {
throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
}
// 2. If init["statusText"] does not match the reason-phrase token production,
// then throw a TypeError.
if ('statusText' in init && init.statusText != null) {
// See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
if (!isValidReasonPhrase(String(init.statusText))) {
throw new TypeError('Invalid statusText')
}
}
// 3. Set response’s response’s status to init["status"].
if ('status' in init && init.status != null) {
response[kState].status = init.status
}
// 4. Set response’s response’s status message to init["statusText"].
if ('statusText' in init && init.statusText != null) {
response[kState].statusText = init.statusText
}
// 5. If init["headers"] exists, then fill response’s headers with init["headers"].
if ('headers' in init && init.headers != null) {
fill(response[kState].headersList, init.headers)
}
// 6. If body was given, then:
if (body) {
// 1. If response's status is a null body status, then throw a TypeError.
if (nullBodyStatus.includes(response.status)) {
webidl.errors.exception({
header: 'Response constructor',
message: 'Invalid response status code.'
})
}
// 2. Set response's body to body's body.
response[kState].body = body.body
// 3. If body's type is non-null and response's header list does not contain
// `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
if (body.type != null && !response[kState].headersList.has('Content-Type')) {
response[kState].headersList.append('content-type', body.type)
}
}
}
webidl.converters.ReadableStream = webidl.interfaceConverter(
ReadableStream
)
webidl.converters.FormData = webidl.interfaceConverter(
FormData
)
webidl.converters.URLSearchParams = webidl.interfaceConverter(
URLSearchParams
)
// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
webidl.converters.XMLHttpRequestBodyInit = function (V) {
if (typeof V === 'string') {
return webidl.converters.USVString(V)
}
if (isBlobLike(V)) {
return webidl.converters.Blob(V)
}
if (
types.isAnyArrayBuffer(V) ||
types.isTypedArray(V) ||
types.isDataView(V)
) {
return webidl.converters.BufferSource(V)
}
if (V instanceof FormData) {
return webidl.converters.FormData(V)
}
if (V instanceof URLSearchParams) {
return webidl.converters.URLSearchParams(V)
}
return webidl.converters.DOMString(V)
}
// https://fetch.spec.whatwg.org/#bodyinit
webidl.converters.BodyInit = function (V) {
if (V instanceof ReadableStream) {
return webidl.converters.ReadableStream(V)
}
// Note: the spec doesn't include async iterables,
// this is an undici extension.
if (V?.[Symbol.asyncIterator]) {
return V
}
return webidl.converters.XMLHttpRequestBodyInit(V)
}
webidl.converters.ResponseInit = webidl.dictionaryConverter([
{
key: 'status',
converter: webidl.converters['unsigned short'],
defaultValue: 200
},
{
key: 'statusText',
converter: webidl.converters.ByteString,
defaultValue: ''
},
{
key: 'headers',
converter: webidl.converters.HeadersInit
}
])
module.exports = {
makeNetworkError,
makeResponse,
makeAppropriateNetworkError,
filterResponse,
Response
}
+10
View File
@@ -0,0 +1,10 @@
'use strict'
module.exports = {
kUrl: Symbol('url'),
kHeaders: Symbol('headers'),
kSignal: Symbol('signal'),
kState: Symbol('state'),
kGuard: Symbol('guard'),
kRealm: Symbol('realm')
}
+467
View File
@@ -0,0 +1,467 @@
'use strict'
const { redirectStatus } = require('./constants')
const { performance } = require('perf_hooks')
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
const assert = require('assert')
let File
// https://fetch.spec.whatwg.org/#block-bad-port
const badPorts = [
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
'2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
'10080'
]
function responseURL (response) {
// https://fetch.spec.whatwg.org/#responses
// A response has an associated URL. It is a pointer to the last URL
// in response’s URL list and null if response’s URL list is empty.
const urlList = response.urlList
const length = urlList.length
return length === 0 ? null : urlList[length - 1].toString()
}
// https://fetch.spec.whatwg.org/#concept-response-location-url
function responseLocationURL (response, requestFragment) {
// 1. If response’s status is not a redirect status, then return null.
if (!redirectStatus.includes(response.status)) {
return null
}
// 2. Let location be the result of extracting header list values given
// `Location` and response’s header list.
let location = response.headersList.get('location')
// 3. If location is a value, then set location to the result of parsing
// location with response’s URL.
location = location ? new URL(location, responseURL(response)) : null
// 4. If location is a URL whose fragment is null, then set location’s
// fragment to requestFragment.
if (location && !location.hash) {
location.hash = requestFragment
}
// 5. Return location.
return location
}
/** @returns {URL} */
function requestCurrentURL (request) {
return request.urlList[request.urlList.length - 1]
}
function requestBadPort (request) {
// 1. Let url be request’s current URL.
const url = requestCurrentURL(request)
// 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
// then return blocked.
if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) {
return 'blocked'
}
// 3. Return allowed.
return 'allowed'
}
function isFileLike (object) {
if (!File) {
File = require('./file').File
}
return object instanceof File || (
object &&
(typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function') &&
/^(File)$/.test(object[Symbol.toStringTag])
)
}
// Check whether |statusText| is a ByteString and
// matches the Reason-Phrase token production.
// RFC 2616: https://tools.ietf.org/html/rfc2616
// RFC 7230: https://tools.ietf.org/html/rfc7230
// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
function isValidReasonPhrase (statusText) {
for (let i = 0; i < statusText.length; ++i) {
const c = statusText.charCodeAt(i)
if (
!(
(
c === 0x09 || // HTAB
(c >= 0x20 && c <= 0x7e) || // SP / VCHAR
(c >= 0x80 && c <= 0xff)
) // obs-text
)
) {
return false
}
}
return true
}
function isTokenChar (c) {
return !(
c >= 0x7f ||
c <= 0x20 ||
c === '(' ||
c === ')' ||
c === '<' ||
c === '>' ||
c === '@' ||
c === ',' ||
c === ';' ||
c === ':' ||
c === '\\' ||
c === '"' ||
c === '/' ||
c === '[' ||
c === ']' ||
c === '?' ||
c === '=' ||
c === '{' ||
c === '}'
)
}
// See RFC 7230, Section 3.2.6.
// https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/platform/network/http_parsers.cc#L321
function isValidHTTPToken (characters) {
if (!characters || typeof characters !== 'string') {
return false
}
for (let i = 0; i < characters.length; ++i) {
const c = characters.charCodeAt(i)
if (c > 0x7f || !isTokenChar(c)) {
return false
}
}
return true
}
// https://fetch.spec.whatwg.org/#header-name
// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342
function isValidHeaderName (potentialValue) {
if (potentialValue.length === 0) {
return false
}
for (const char of potentialValue) {
if (!isValidHTTPToken(char)) {
return false
}
}
return true
}
/**
* @see https://fetch.spec.whatwg.org/#header-value
* @param {string} potentialValue
*/
function isValidHeaderValue (potentialValue) {
// - Has no leading or trailing HTTP tab or space bytes.
// - Contains no 0x00 (NUL) or HTTP newline bytes.
if (
potentialValue.startsWith('\t') ||
potentialValue.startsWith(' ') ||
potentialValue.endsWith('\t') ||
potentialValue.endsWith(' ')
) {
return false
}
if (
potentialValue.includes('\0') ||
potentialValue.includes('\r') ||
potentialValue.includes('\n')
) {
return false
}
return true
}
// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
// Given a request request and a response actualResponse, this algorithm
// updates request’s referrer policy according to the Referrer-Policy
// header (if any) in actualResponse.
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy
// from a Referrer-Policy header on actualResponse.
// TODO: https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
const policy = ''
// 2. If policy is not the empty string, then set request’s referrer policy to policy.
if (policy !== '') {
request.referrerPolicy = policy
}
}
// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
function crossOriginResourcePolicyCheck () {
// TODO
return 'allowed'
}
// https://fetch.spec.whatwg.org/#concept-cors-check
function corsCheck () {
// TODO
return 'success'
}
// https://fetch.spec.whatwg.org/#concept-tao-check
function TAOCheck () {
// TODO
return 'success'
}
function appendFetchMetadata (httpRequest) {
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
// TODO
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
// 1. Assert: r’s url is a potentially trustworthy URL.
// TODO
// 2. Let header be a Structured Header whose value is a token.
let header = null
// 3. Set header’s value to r’s mode.
header = httpRequest.mode
// 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
httpRequest.headersList.set('sec-fetch-mode', header)
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
// TODO
// https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
// TODO
}
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
function appendRequestOriginHeader (request) {
// 1. Let serializedOrigin be the result of byte-serializing a request origin with request.
let serializedOrigin = request.origin
// 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
if (request.responseTainting === 'cors' || request.mode === 'websocket') {
if (serializedOrigin) {
request.headersList.append('Origin', serializedOrigin)
}
// 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
} else if (request.method !== 'GET' && request.method !== 'HEAD') {
// 1. Switch on request’s referrer policy:
switch (request.referrerPolicy) {
case 'no-referrer':
// Set serializedOrigin to `null`.
serializedOrigin = null
break
case 'no-referrer-when-downgrade':
case 'strict-origin':
case 'strict-origin-when-cross-origin':
// If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) {
serializedOrigin = null
}
break
case 'same-origin':
// If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`.
if (!sameOrigin(request, requestCurrentURL(request))) {
serializedOrigin = null
}
break
default:
// Do nothing.
}
if (serializedOrigin) {
// 2. Append (`Origin`, serializedOrigin) to request’s header list.
request.headersList.append('Origin', serializedOrigin)
}
}
}
function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
// TODO
return performance.now()
}
// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
function createOpaqueTimingInfo (timingInfo) {
return {
startTime: timingInfo.startTime ?? 0,
redirectStartTime: 0,
redirectEndTime: 0,
postRedirectStartTime: timingInfo.startTime ?? 0,
finalServiceWorkerStartTime: 0,
finalNetworkResponseStartTime: 0,
finalNetworkRequestStartTime: 0,
endTime: 0,
encodedBodySize: 0,
decodedBodySize: 0,
finalConnectionTimingInfo: null
}
}
// https://html.spec.whatwg.org/multipage/origin.html#policy-container
function makePolicyContainer () {
// TODO
return {}
}
// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
function clonePolicyContainer () {
// TODO
return {}
}
// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
function determineRequestsReferrer (request) {
// TODO
return 'no-referrer'
}
function matchRequestIntegrity (request, bytes) {
return false
}
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
// TODO
}
/**
* @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
* @param {URL} A
* @param {URL} B
*/
function sameOrigin (A, B) {
// 1. If A and B are the same opaque origin, then return true.
// "opaque origin" is an internal value we cannot access, ignore.
// 2. If A and B are both tuple origins and their schemes,
// hosts, and port are identical, then return true.
if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
return true
}
// 3. Return false.
return false
}
function createDeferredPromise () {
let res
let rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
return { promise, resolve: res, reject: rej }
}
function isAborted (fetchParams) {
return fetchParams.controller.state === 'aborted'
}
function isCancelled (fetchParams) {
return fetchParams.controller.state === 'aborted' ||
fetchParams.controller.state === 'terminated'
}
// https://fetch.spec.whatwg.org/#concept-method-normalize
function normalizeMethod (method) {
return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method)
? method.toUpperCase()
: method
}
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
function serializeJavascriptValueToJSONString (value) {
// 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
const result = JSON.stringify(value)
// 2. If result is undefined, then throw a TypeError.
if (result === undefined) {
throw new TypeError('Value is not JSON serializable')
}
// 3. Assert: result is a string.
assert(typeof result === 'string')
// 4. Return result.
return result
}
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
function makeIterator (iterator, name) {
const i = {
next () {
if (Object.getPrototypeOf(this) !== i) {
throw new TypeError(
`'next' called on an object that does not implement interface ${name} Iterator.`
)
}
return iterator.next()
},
// The class string of an iterator prototype object for a given interface is the
// result of concatenating the identifier of the interface and the string " Iterator".
[Symbol.toStringTag]: `${name} Iterator`
}
// The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%.
Object.setPrototypeOf(i, esIteratorPrototype)
// esIteratorPrototype needs to be the prototype of i
// which is the prototype of an empty object. Yes, it's confusing.
return Object.setPrototypeOf({}, i)
}
module.exports = {
isAborted,
isCancelled,
createDeferredPromise,
ReadableStreamFrom,
toUSVString,
tryUpgradeRequestToAPotentiallyTrustworthyURL,
coarsenedSharedCurrentTime,
matchRequestIntegrity,
determineRequestsReferrer,
makePolicyContainer,
clonePolicyContainer,
appendFetchMetadata,
appendRequestOriginHeader,
TAOCheck,
corsCheck,
crossOriginResourcePolicyCheck,
createOpaqueTimingInfo,
setRequestReferrerPolicyOnRedirect,
isValidHTTPToken,
requestBadPort,
requestCurrentURL,
responseURL,
responseLocationURL,
isBlobLike,
isFileLike,
isValidReasonPhrase,
sameOrigin,
normalizeMethod,
serializeJavascriptValueToJSONString,
makeIterator,
isValidHeaderName,
isValidHeaderValue
}
+594
View File
@@ -0,0 +1,594 @@
'use strict'
const { toUSVString, types } = require('util')
const webidl = {}
webidl.converters = {}
webidl.util = {}
webidl.errors = {}
/**
*
* @param {{
* header: string
* message: string
* }} message
*/
webidl.errors.exception = function (message) {
throw new TypeError(`${message.header}: ${message.message}`)
}
/**
* Throw an error when conversion from one type to another has failed
* @param {{
* prefix: string
* argument: string
* types: string[]
* }} context
*/
webidl.errors.conversionFailed = function (context) {
const plural = context.types.length === 1 ? '' : ' one of'
const message =
`${context.argument} could not be converted to` +
`${plural}: ${context.types.join(', ')}.`
return webidl.errors.exception({
header: context.prefix,
message
})
}
/**
* Throw an error when an invalid argument is provided
* @param {{
* prefix: string
* value: string
* type: string
* }} context
*/
webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({
header: context.prefix,
message: `"${context.value}" is an invalid ${context.type}.`
})
}
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) {
switch (typeof V) {
case 'undefined': return 'Undefined'
case 'boolean': return 'Boolean'
case 'string': return 'String'
case 'symbol': return 'Symbol'
case 'number': return 'Number'
case 'bigint': return 'BigInt'
case 'function':
case 'object': {
if (V === null) {
return 'Null'
}
return 'Object'
}
}
}
// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
let upperBound
let lowerBound
// 1. If bitLength is 64, then:
if (bitLength === 64) {
// 1. Let upperBound be 2^53 − 1.
upperBound = Math.pow(2, 53) - 1
// 2. If signedness is "unsigned", then let lowerBound be 0.
if (signedness === 'unsigned') {
lowerBound = 0
} else {
// 3. Otherwise let lowerBound be −2^53 + 1.
lowerBound = Math.pow(-2, 53) + 1
}
} else if (signedness === 'unsigned') {
// 2. Otherwise, if signedness is "unsigned", then:
// 1. Let lowerBound be 0.
lowerBound = 0
// 2. Let upperBound be 2^bitLength − 1.
upperBound = Math.pow(2, bitLength) - 1
} else {
// 3. Otherwise:
// 1. Let lowerBound be -2^bitLength − 1.
lowerBound = Math.pow(-2, bitLength) - 1
// 2. Let upperBound be 2^bitLength − 1 − 1.
upperBound = Math.pow(2, bitLength - 1) - 1
}
// 4. Let x be ? ToNumber(V).
let x = Number(V)
// 5. If x is −0, then set x to +0.
if (Object.is(-0, x)) {
x = 0
}
// 6. If the conversion is to an IDL type associated
// with the [EnforceRange] extended attribute, then:
if (opts.enforceRange === true) {
// 1. If x is NaN, +∞, or −∞, then throw a TypeError.
if (
Number.isNaN(x) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
webidl.errors.exception({
header: 'Integer conversion',
message: `Could not convert ${V} to an integer.`
})
}
// 2. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 3. If x < lowerBound or x > upperBound, then
// throw a TypeError.
if (x < lowerBound || x > upperBound) {
webidl.errors.exception({
header: 'Integer conversion',
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
})
}
// 4. Return x.
return x
}
// 7. If x is not NaN and the conversion is to an IDL
// type associated with the [Clamp] extended
// attribute, then:
if (!Number.isNaN(x) && opts.clamp === true) {
// 1. Set x to min(max(x, lowerBound), upperBound).
x = Math.min(Math.max(x, lowerBound), upperBound)
// 2. Round x to the nearest integer, choosing the
// even integer if it lies halfway between two,
// and choosing +0 rather than −0.
if (Math.floor(x) % 2 === 0) {
x = Math.floor(x)
} else {
x = Math.ceil(x)
}
// 3. Return x.
return x
}
// 8. If x is NaN, +0, +∞, or −∞, then return +0.
if (
Number.isNaN(x) ||
Object.is(0, x) ||
x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY
) {
return 0
}
// 9. Set x to IntegerPart(x).
x = webidl.util.IntegerPart(x)
// 10. Set x to x modulo 2^bitLength.
x = x % Math.pow(2, bitLength)
// 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
// then return x − 2^bitLength.
if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
return x - Math.pow(2, bitLength)
}
// 12. Otherwise, return x.
return x
}
// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
webidl.util.IntegerPart = function (n) {
// 1. Let r be floor(abs(n)).
const r = Math.floor(Math.abs(n))
// 2. If n < 0, then return -1 × r.
if (n < 0) {
return -1 * r
}
// 3. Otherwise, return r.
return r
}
// https://webidl.spec.whatwg.org/#es-sequence
webidl.sequenceConverter = function (converter) {
return (V) => {
// 1. If Type(V) is not Object, throw a TypeError.
if (webidl.util.Type(V) !== 'Object') {
webidl.errors.exception({
header: 'Sequence',
message: `Value of type ${webidl.util.Type(V)} is not an Object.`
})
}
// 2. Let method be ? GetMethod(V, @@iterator).
/** @type {Generator} */
const method = V?.[Symbol.iterator]?.()
const seq = []
// 3. If method is undefined, throw a TypeError.
if (
method === undefined ||
typeof method.next !== 'function'
) {
webidl.errors.exception({
header: 'Sequence',
message: 'Object is not an iterator.'
})
}
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
while (true) {
const { done, value } = method.next()
if (done) {
break
}
seq.push(converter(value))
}
return seq
}
}
webidl.recordConverter = function (keyConverter, valueConverter) {
return (V) => {
const record = {}
const type = webidl.util.Type(V)
if (type === 'Undefined' || type === 'Null') {
return record
}
if (type !== 'Object') {
webidl.errors.exception({
header: 'Record',
message: `Expected ${V} to be an Object type.`
})
}
for (let [key, value] of Object.entries(V)) {
key = keyConverter(key)
value = valueConverter(value)
record[key] = value
}
return record
}
}
webidl.interfaceConverter = function (i) {
return (V, opts = {}) => {
if (opts.strict !== false && !(V instanceof i)) {
webidl.errors.exception({
header: i.name,
message: `Expected ${V} to be an instance of ${i.name}.`
})
}
return V
}
}
/**
* @param {{
* key: string,
* defaultValue?: any,
* required?: boolean,
* converter: (...args: unknown[]) => unknown,
* allowedValues?: any[]
* }[]} converters
* @returns
*/
webidl.dictionaryConverter = function (converters) {
return (dictionary) => {
const type = webidl.util.Type(dictionary)
const dict = {}
if (type !== 'Null' && type !== 'Undefined' && type !== 'Object') {
webidl.errors.exception({
header: 'Dictionary',
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
})
}
for (const options of converters) {
const { key, defaultValue, required, converter } = options
if (required === true) {
if (!Object.hasOwn(dictionary, key)) {
webidl.errors.exception({
header: 'Dictionary',
message: `Missing required key "${key}".`
})
}
}
let value = dictionary[key]
const hasDefault = Object.hasOwn(options, 'defaultValue')
// Only use defaultValue if value is undefined and
// a defaultValue options was provided.
if (hasDefault && value !== null) {
value = value ?? defaultValue
}
// A key can be optional and have no default value.
// When this happens, do not perform a conversion,
// and do not assign the key a value.
if (required || hasDefault || value !== undefined) {
value = converter(value)
if (
options.allowedValues &&
!options.allowedValues.includes(value)
) {
webidl.errors.exception({
header: 'Dictionary',
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
})
}
dict[key] = value
}
}
return dict
}
}
webidl.nullableConverter = function (converter) {
return (V) => {
if (V === null) {
return V
}
return converter(V)
}
}
// https://webidl.spec.whatwg.org/#es-DOMString
webidl.converters.DOMString = function (V, opts = {}) {
// 1. If V is null and the conversion is to an IDL type
// associated with the [LegacyNullToEmptyString]
// extended attribute, then return the DOMString value
// that represents the empty string.
if (V === null && opts.legacyNullToEmptyString) {
return ''
}
// 2. Let x be ? ToString(V).
if (typeof V === 'symbol') {
throw new TypeError('Could not convert argument of type symbol to string.')
}
// 3. Return the IDL DOMString value that represents the
// same sequence of code units as the one the
// ECMAScript String value x represents.
return String(V)
}
// eslint-disable-next-line no-control-regex
const isNotLatin1 = /[^\u0000-\u00ff]/
// https://webidl.spec.whatwg.org/#es-ByteString
webidl.converters.ByteString = function (V) {
// 1. Let x be ? ToString(V).
// Note: DOMString converter perform ? ToString(V)
const x = webidl.converters.DOMString(V)
// 2. If the value of any element of x is greater than
// 255, then throw a TypeError.
if (isNotLatin1.test(x)) {
throw new TypeError('Argument is not a ByteString')
}
// 3. Return an IDL ByteString value whose length is the
// length of x, and where the value of each element is
// the value of the corresponding element of x.
return x
}
// https://webidl.spec.whatwg.org/#es-USVString
// TODO: ensure that util.toUSVString follows webidl spec
webidl.converters.USVString = toUSVString
// https://webidl.spec.whatwg.org/#es-boolean
webidl.converters.boolean = function (V) {
// 1. Let x be the result of computing ToBoolean(V).
const x = Boolean(V)
// 2. Return the IDL boolean value that is the one that represents
// the same truth value as the ECMAScript Boolean value x.
return x
}
// https://webidl.spec.whatwg.org/#es-any
webidl.converters.any = function (V) {
return V
}
// https://webidl.spec.whatwg.org/#es-long-long
webidl.converters['long long'] = function (V, opts) {
// 1. Let x be ? ConvertToInt(V, 64, "signed").
const x = webidl.util.ConvertToInt(V, 64, 'signed', opts)
// 2. Return the IDL long long value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-short
webidl.converters['unsigned short'] = function (V) {
// 1. Let x be ? ConvertToInt(V, 16, "unsigned").
const x = webidl.util.ConvertToInt(V, 16, 'unsigned')
// 2. Return the IDL unsigned short value that represents
// the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
webidl.converters.ArrayBuffer = function (V, opts = {}) {
// 1. If Type(V) is not Object, or V does not have an
// [[ArrayBufferData]] internal slot, then throw a
// TypeError.
// see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
// see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
if (
webidl.util.Type(V) !== 'Object' ||
!types.isAnyArrayBuffer(V)
) {
webidl.errors.conversionFailed({
prefix: `${V}`,
argument: `${V}`,
types: ['ArrayBuffer']
})
}
// 2. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V) is true, then throw a
// TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V)) {
webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V) is true, then throw a
// TypeError.
// Note: resizable ArrayBuffers are currently a proposal.
// 4. Return the IDL ArrayBuffer value that is a
// reference to the same object as V.
return V
}
webidl.converters.TypedArray = function (V, T, opts = {}) {
// 1. Let T be the IDL type V is being converted to.
// 2. If Type(V) is not Object, or V does not have a
// [[TypedArrayName]] internal slot with a value
// equal to T’s name, then throw a TypeError.
if (
webidl.util.Type(V) !== 'Object' ||
!types.isTypedArray(V) ||
V.constructor.name !== T.name
) {
webidl.errors.conversionFailed({
prefix: `${T.name}`,
argument: `${V}`,
types: [T.name]
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 4. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
// Note: resizable array buffers are currently a proposal
// 5. Return the IDL value of type T that is a reference
// to the same object as V.
return V
}
webidl.converters.DataView = function (V, opts = {}) {
// 1. If Type(V) is not Object, or V does not have a
// [[DataView]] internal slot, then throw a TypeError.
if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) {
webidl.errors.exception({
header: 'DataView',
message: 'Object is not a DataView.'
})
}
// 2. If the conversion is not to an IDL type associated
// with the [AllowShared] extended attribute, and
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
// then throw a TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
webidl.errors.exception({
header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.'
})
}
// 3. If the conversion is not to an IDL type associated
// with the [AllowResizable] extended attribute, and
// IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError.
// Note: resizable ArrayBuffers are currently a proposal
// 4. Return the IDL DataView value that is a reference
// to the same object as V.
return V
}
// https://webidl.spec.whatwg.org/#BufferSource
webidl.converters.BufferSource = function (V, opts = {}) {
if (types.isAnyArrayBuffer(V)) {
return webidl.converters.ArrayBuffer(V, opts)
}
if (types.isTypedArray(V)) {
return webidl.converters.TypedArray(V, V.constructor)
}
if (types.isDataView(V)) {
return webidl.converters.DataView(V, opts)
}
throw new TypeError(`Could not convert ${V} to a BufferSource.`)
}
webidl.converters['sequence<ByteString>'] = webidl.sequenceConverter(
webidl.converters.ByteString
)
webidl.converters['sequence<sequence<ByteString>>'] = webidl.sequenceConverter(
webidl.converters['sequence<ByteString>']
)
webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
webidl.converters.ByteString,
webidl.converters.ByteString
)
module.exports = {
webidl
}