You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
12 KiB
406 lines
12 KiB
'use strict' |
|
|
|
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils') |
|
const { SCHEMES, getSchemeHandler } = require('./lib/schemes') |
|
|
|
/** |
|
* @template {import('./types/index').URIComponent|string} T |
|
* @param {T} uri |
|
* @param {import('./types/index').Options} [options] |
|
* @returns {T} |
|
*/ |
|
function normalize (uri, options) { |
|
if (typeof uri === 'string') { |
|
uri = /** @type {T} */ (normalizeString(uri, options)) |
|
} else if (typeof uri === 'object') { |
|
uri = /** @type {T} */ (parse(serialize(uri, options), options)) |
|
} |
|
return uri |
|
} |
|
|
|
/** |
|
* @param {string} baseURI |
|
* @param {string} relativeURI |
|
* @param {import('./types/index').Options} [options] |
|
* @returns {string} |
|
*/ |
|
function resolve (baseURI, relativeURI, options) { |
|
const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' } |
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true) |
|
schemelessOptions.skipEscape = true |
|
return serialize(resolved, schemelessOptions) |
|
} |
|
|
|
/** |
|
* @param {import ('./types/index').URIComponent} base |
|
* @param {import ('./types/index').URIComponent} relative |
|
* @param {import('./types/index').Options} [options] |
|
* @param {boolean} [skipNormalization=false] |
|
* @returns {import ('./types/index').URIComponent} |
|
*/ |
|
function resolveComponent (base, relative, options, skipNormalization) { |
|
/** @type {import('./types/index').URIComponent} */ |
|
const target = {} |
|
if (!skipNormalization) { |
|
base = parse(serialize(base, options), options) // normalize base component |
|
relative = parse(serialize(relative, options), options) // normalize relative component |
|
} |
|
options = options || {} |
|
|
|
if (!options.tolerant && relative.scheme) { |
|
target.scheme = relative.scheme |
|
// target.authority = relative.authority; |
|
target.userinfo = relative.userinfo |
|
target.host = relative.host |
|
target.port = relative.port |
|
target.path = removeDotSegments(relative.path || '') |
|
target.query = relative.query |
|
} else { |
|
if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) { |
|
// target.authority = relative.authority; |
|
target.userinfo = relative.userinfo |
|
target.host = relative.host |
|
target.port = relative.port |
|
target.path = removeDotSegments(relative.path || '') |
|
target.query = relative.query |
|
} else { |
|
if (!relative.path) { |
|
target.path = base.path |
|
if (relative.query !== undefined) { |
|
target.query = relative.query |
|
} else { |
|
target.query = base.query |
|
} |
|
} else { |
|
if (relative.path[0] === '/') { |
|
target.path = removeDotSegments(relative.path) |
|
} else { |
|
if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) { |
|
target.path = '/' + relative.path |
|
} else if (!base.path) { |
|
target.path = relative.path |
|
} else { |
|
target.path = base.path.slice(0, base.path.lastIndexOf('/') + 1) + relative.path |
|
} |
|
target.path = removeDotSegments(target.path) |
|
} |
|
target.query = relative.query |
|
} |
|
// target.authority = base.authority; |
|
target.userinfo = base.userinfo |
|
target.host = base.host |
|
target.port = base.port |
|
} |
|
target.scheme = base.scheme |
|
} |
|
|
|
target.fragment = relative.fragment |
|
|
|
return target |
|
} |
|
|
|
/** |
|
* @param {import ('./types/index').URIComponent|string} uriA |
|
* @param {import ('./types/index').URIComponent|string} uriB |
|
* @param {import ('./types/index').Options} options |
|
* @returns {boolean} |
|
*/ |
|
function equal (uriA, uriB, options) { |
|
const normalizedA = normalizeComparableURI(uriA, options) |
|
const normalizedB = normalizeComparableURI(uriB, options) |
|
|
|
return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase() |
|
} |
|
|
|
/** |
|
* @param {Readonly<import('./types/index').URIComponent>} cmpts |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns {string} |
|
*/ |
|
function serialize (cmpts, opts) { |
|
const component = { |
|
host: cmpts.host, |
|
scheme: cmpts.scheme, |
|
userinfo: cmpts.userinfo, |
|
port: cmpts.port, |
|
path: cmpts.path, |
|
query: cmpts.query, |
|
nid: cmpts.nid, |
|
nss: cmpts.nss, |
|
uuid: cmpts.uuid, |
|
fragment: cmpts.fragment, |
|
reference: cmpts.reference, |
|
resourceName: cmpts.resourceName, |
|
secure: cmpts.secure, |
|
error: '' |
|
} |
|
const options = Object.assign({}, opts) |
|
const uriTokens = [] |
|
|
|
// find scheme handler |
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme) |
|
|
|
// perform scheme specific serialization |
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options) |
|
|
|
if (component.path !== undefined) { |
|
if (!options.skipEscape) { |
|
component.path = escapePreservingEscapes(component.path) |
|
|
|
if (component.scheme !== undefined) { |
|
component.path = component.path.split('%3A').join(':') |
|
} |
|
} else { |
|
component.path = normalizePercentEncoding(component.path) |
|
} |
|
} |
|
|
|
if (options.reference !== 'suffix' && component.scheme) { |
|
uriTokens.push(component.scheme, ':') |
|
} |
|
|
|
const authority = recomposeAuthority(component) |
|
if (authority !== undefined) { |
|
if (options.reference !== 'suffix') { |
|
uriTokens.push('//') |
|
} |
|
|
|
uriTokens.push(authority) |
|
|
|
if (component.path && component.path[0] !== '/') { |
|
uriTokens.push('/') |
|
} |
|
} |
|
if (component.path !== undefined) { |
|
let s = component.path |
|
|
|
if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { |
|
s = removeDotSegments(s) |
|
} |
|
|
|
if ( |
|
authority === undefined && |
|
s[0] === '/' && |
|
s[1] === '/' |
|
) { |
|
// don't allow the path to start with "//" |
|
s = '/%2F' + s.slice(2) |
|
} |
|
|
|
uriTokens.push(s) |
|
} |
|
|
|
if (component.query !== undefined) { |
|
uriTokens.push('?', component.query) |
|
} |
|
|
|
if (component.fragment !== undefined) { |
|
uriTokens.push('#', component.fragment) |
|
} |
|
return uriTokens.join('') |
|
} |
|
|
|
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u |
|
|
|
/** |
|
* @param {import('./types/index').URIComponent} parsed |
|
* @param {RegExpMatchArray} matches |
|
* @returns {string|undefined} |
|
*/ |
|
function getParseError (parsed, matches) { |
|
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') { |
|
return 'URI path must start with "/" when authority is present.' |
|
} |
|
|
|
if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) { |
|
return 'URI port is malformed.' |
|
} |
|
|
|
return undefined |
|
} |
|
|
|
/** |
|
* @param {string} uri |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }} |
|
*/ |
|
function parseWithStatus (uri, opts) { |
|
const options = Object.assign({}, opts) |
|
/** @type {import('./types/index').URIComponent} */ |
|
const parsed = { |
|
scheme: undefined, |
|
userinfo: undefined, |
|
host: '', |
|
port: undefined, |
|
path: '', |
|
query: undefined, |
|
fragment: undefined |
|
} |
|
|
|
let malformedAuthorityOrPort = false |
|
|
|
let isIP = false |
|
if (options.reference === 'suffix') { |
|
if (options.scheme) { |
|
uri = options.scheme + ':' + uri |
|
} else { |
|
uri = '//' + uri |
|
} |
|
} |
|
|
|
const matches = uri.match(URI_PARSE) |
|
|
|
if (matches) { |
|
// store each component |
|
parsed.scheme = matches[1] |
|
parsed.userinfo = matches[3] |
|
parsed.host = matches[4] |
|
parsed.port = parseInt(matches[5], 10) |
|
parsed.path = matches[6] || '' |
|
parsed.query = matches[7] |
|
parsed.fragment = matches[8] |
|
|
|
// fix port number |
|
if (isNaN(parsed.port)) { |
|
parsed.port = matches[5] |
|
} |
|
|
|
const parseError = getParseError(parsed, matches) |
|
if (parseError !== undefined) { |
|
parsed.error = parsed.error || parseError |
|
malformedAuthorityOrPort = true |
|
} |
|
|
|
if (parsed.host) { |
|
const ipv4result = isIPv4(parsed.host) |
|
if (ipv4result === false) { |
|
const ipv6result = normalizeIPv6(parsed.host) |
|
parsed.host = ipv6result.host.toLowerCase() |
|
isIP = ipv6result.isIPV6 |
|
} else { |
|
isIP = true |
|
} |
|
} |
|
if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && parsed.query === undefined && !parsed.path) { |
|
parsed.reference = 'same-document' |
|
} else if (parsed.scheme === undefined) { |
|
parsed.reference = 'relative' |
|
} else if (parsed.fragment === undefined) { |
|
parsed.reference = 'absolute' |
|
} else { |
|
parsed.reference = 'uri' |
|
} |
|
|
|
// check for reference errors |
|
if (options.reference && options.reference !== 'suffix' && options.reference !== parsed.reference) { |
|
parsed.error = parsed.error || 'URI is not a ' + options.reference + ' reference.' |
|
} |
|
|
|
// find scheme handler |
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme) |
|
|
|
// check if scheme can't handle IRIs |
|
if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) { |
|
// if host component is a domain name |
|
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) { |
|
// convert Unicode IDN -> ASCII IDN |
|
try { |
|
parsed.host = URL.domainToASCII(parsed.host.toLowerCase()) |
|
} catch (e) { |
|
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e |
|
} |
|
} |
|
// convert IRI -> URI |
|
} |
|
|
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) { |
|
if (uri.indexOf('%') !== -1) { |
|
if (parsed.scheme !== undefined) { |
|
parsed.scheme = unescape(parsed.scheme) |
|
} |
|
if (parsed.host !== undefined) { |
|
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP) |
|
} |
|
} |
|
if (parsed.path) { |
|
parsed.path = normalizePathEncoding(parsed.path) |
|
} |
|
if (parsed.fragment) { |
|
try { |
|
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)) |
|
} catch { |
|
parsed.error = parsed.error || 'URI malformed' |
|
} |
|
} |
|
} |
|
|
|
// perform scheme specific parsing |
|
if (schemeHandler && schemeHandler.parse) { |
|
schemeHandler.parse(parsed, options) |
|
} |
|
} else { |
|
parsed.error = parsed.error || 'URI can not be parsed.' |
|
} |
|
return { parsed, malformedAuthorityOrPort } |
|
} |
|
|
|
/** |
|
* @param {string} uri |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns |
|
*/ |
|
function parse (uri, opts) { |
|
return parseWithStatus(uri, opts).parsed |
|
} |
|
|
|
/** |
|
* @param {string} uri |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns {string} |
|
*/ |
|
function normalizeString (uri, opts) { |
|
return normalizeStringWithStatus(uri, opts).normalized |
|
} |
|
|
|
/** |
|
* @param {string} uri |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }} |
|
*/ |
|
function normalizeStringWithStatus (uri, opts) { |
|
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts) |
|
return { |
|
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts), |
|
malformedAuthorityOrPort |
|
} |
|
} |
|
|
|
/** |
|
* @param {import ('./types/index').URIComponent|string} uri |
|
* @param {import('./types/index').Options} [opts] |
|
* @returns {string|undefined} |
|
*/ |
|
function normalizeComparableURI (uri, opts) { |
|
if (typeof uri === 'string') { |
|
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts) |
|
return malformedAuthorityOrPort ? undefined : normalized |
|
} |
|
|
|
if (typeof uri === 'object') { |
|
return serialize(uri, opts) |
|
} |
|
} |
|
|
|
const fastUri = { |
|
SCHEMES, |
|
normalize, |
|
resolve, |
|
resolveComponent, |
|
equal, |
|
serialize, |
|
parse |
|
} |
|
|
|
module.exports = fastUri |
|
module.exports.default = fastUri |
|
module.exports.fastUri = fastUri
|
|
|