123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 'use strict'
- /** @type {(value: string) => boolean} */
- const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu)
- /** @type {(value: string) => boolean} */
- const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
- /**
- * @param {Array<string>} input
- * @returns {string}
- */
- function stringArrayToHexStripped (input) {
- let acc = ''
- let code = 0
- let i = 0
- for (i = 0; i < input.length; i++) {
- code = input[i].charCodeAt(0)
- if (code === 48) {
- continue
- }
- if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
- return ''
- }
- acc += input[i]
- break
- }
- for (i += 1; i < input.length; i++) {
- code = input[i].charCodeAt(0)
- if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
- return ''
- }
- acc += input[i]
- }
- return acc
- }
- /**
- * @typedef {Object} GetIPV6Result
- * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
- * @property {string} address - The parsed IPv6 address.
- * @property {string} [zone] - The zone identifier, if present.
- */
- /**
- * @param {string} value
- * @returns {boolean}
- */
- const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
- /**
- * @param {Array<string>} buffer
- * @returns {boolean}
- */
- function consumeIsZone (buffer) {
- buffer.length = 0
- return true
- }
- /**
- * @param {Array<string>} buffer
- * @param {Array<string>} address
- * @param {GetIPV6Result} output
- * @returns {boolean}
- */
- function consumeHextets (buffer, address, output) {
- if (buffer.length) {
- const hex = stringArrayToHexStripped(buffer)
- if (hex !== '') {
- address.push(hex)
- } else {
- output.error = true
- return false
- }
- buffer.length = 0
- }
- return true
- }
- /**
- * @param {string} input
- * @returns {GetIPV6Result}
- */
- function getIPV6 (input) {
- let tokenCount = 0
- const output = { error: false, address: '', zone: '' }
- /** @type {Array<string>} */
- const address = []
- /** @type {Array<string>} */
- const buffer = []
- let endipv6Encountered = false
- let endIpv6 = false
- let consume = consumeHextets
- for (let i = 0; i < input.length; i++) {
- const cursor = input[i]
- if (cursor === '[' || cursor === ']') { continue }
- if (cursor === ':') {
- if (endipv6Encountered === true) {
- endIpv6 = true
- }
- if (!consume(buffer, address, output)) { break }
- if (++tokenCount > 7) {
- // not valid
- output.error = true
- break
- }
- if (i > 0 && input[i - 1] === ':') {
- endipv6Encountered = true
- }
- address.push(':')
- continue
- } else if (cursor === '%') {
- if (!consume(buffer, address, output)) { break }
- // switch to zone detection
- consume = consumeIsZone
- } else {
- buffer.push(cursor)
- continue
- }
- }
- if (buffer.length) {
- if (consume === consumeIsZone) {
- output.zone = buffer.join('')
- } else if (endIpv6) {
- address.push(buffer.join(''))
- } else {
- address.push(stringArrayToHexStripped(buffer))
- }
- }
- output.address = address.join('')
- return output
- }
- /**
- * @typedef {Object} NormalizeIPv6Result
- * @property {string} host - The normalized host.
- * @property {string} [escapedHost] - The escaped host.
- * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
- */
- /**
- * @param {string} host
- * @returns {NormalizeIPv6Result}
- */
- function normalizeIPv6 (host) {
- if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
- const ipv6 = getIPV6(host)
- if (!ipv6.error) {
- let newHost = ipv6.address
- let escapedHost = ipv6.address
- if (ipv6.zone) {
- newHost += '%' + ipv6.zone
- escapedHost += '%25' + ipv6.zone
- }
- return { host: newHost, isIPV6: true, escapedHost }
- } else {
- return { host, isIPV6: false }
- }
- }
- /**
- * @param {string} str
- * @param {string} token
- * @returns {number}
- */
- function findToken (str, token) {
- let ind = 0
- for (let i = 0; i < str.length; i++) {
- if (str[i] === token) ind++
- }
- return ind
- }
- /**
- * @param {string} path
- * @returns {string}
- *
- * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
- */
- function removeDotSegments (path) {
- let input = path
- const output = []
- let nextSlash = -1
- let len = 0
- // eslint-disable-next-line no-cond-assign
- while (len = input.length) {
- if (len === 1) {
- if (input === '.') {
- break
- } else if (input === '/') {
- output.push('/')
- break
- } else {
- output.push(input)
- break
- }
- } else if (len === 2) {
- if (input[0] === '.') {
- if (input[1] === '.') {
- break
- } else if (input[1] === '/') {
- input = input.slice(2)
- continue
- }
- } else if (input[0] === '/') {
- if (input[1] === '.' || input[1] === '/') {
- output.push('/')
- break
- }
- }
- } else if (len === 3) {
- if (input === '/..') {
- if (output.length !== 0) {
- output.pop()
- }
- output.push('/')
- break
- }
- }
- if (input[0] === '.') {
- if (input[1] === '.') {
- if (input[2] === '/') {
- input = input.slice(3)
- continue
- }
- } else if (input[1] === '/') {
- input = input.slice(2)
- continue
- }
- } else if (input[0] === '/') {
- if (input[1] === '.') {
- if (input[2] === '/') {
- input = input.slice(2)
- continue
- } else if (input[2] === '.') {
- if (input[3] === '/') {
- input = input.slice(3)
- if (output.length !== 0) {
- output.pop()
- }
- continue
- }
- }
- }
- }
- // Rule 2E: Move normal path segment to output
- if ((nextSlash = input.indexOf('/', 1)) === -1) {
- output.push(input)
- break
- } else {
- output.push(input.slice(0, nextSlash))
- input = input.slice(nextSlash)
- }
- }
- return output.join('')
- }
- /**
- * @param {import('../types/index').URIComponent} component
- * @param {boolean} esc
- * @returns {import('../types/index').URIComponent}
- */
- function normalizeComponentEncoding (component, esc) {
- const func = esc !== true ? escape : unescape
- if (component.scheme !== undefined) {
- component.scheme = func(component.scheme)
- }
- if (component.userinfo !== undefined) {
- component.userinfo = func(component.userinfo)
- }
- if (component.host !== undefined) {
- component.host = func(component.host)
- }
- if (component.path !== undefined) {
- component.path = func(component.path)
- }
- if (component.query !== undefined) {
- component.query = func(component.query)
- }
- if (component.fragment !== undefined) {
- component.fragment = func(component.fragment)
- }
- return component
- }
- /**
- * @param {import('../types/index').URIComponent} component
- * @returns {string|undefined}
- */
- function recomposeAuthority (component) {
- const uriTokens = []
- if (component.userinfo !== undefined) {
- uriTokens.push(component.userinfo)
- uriTokens.push('@')
- }
- if (component.host !== undefined) {
- let host = unescape(component.host)
- if (!isIPv4(host)) {
- const ipV6res = normalizeIPv6(host)
- if (ipV6res.isIPV6 === true) {
- host = `[${ipV6res.escapedHost}]`
- } else {
- host = component.host
- }
- }
- uriTokens.push(host)
- }
- if (typeof component.port === 'number' || typeof component.port === 'string') {
- uriTokens.push(':')
- uriTokens.push(String(component.port))
- }
- return uriTokens.length ? uriTokens.join('') : undefined
- };
- module.exports = {
- nonSimpleDomain,
- recomposeAuthority,
- normalizeComponentEncoding,
- removeDotSegments,
- isIPv4,
- isUUID,
- normalizeIPv6,
- stringArrayToHexStripped
- }
|