'use strict' const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority, normalizeComponentEncoding } = require('./lib/utils') const SCHEMES = require('./lib/schemes') function normalize (uri, options) { if (typeof uri === 'string') { uri = serialize(parse(uri, options), options) } else if (typeof uri === 'object') { uri = parse(serialize(uri, options), options) } return uri } function resolve (baseURI, relativeURI, options) { const schemelessOptions = Object.assign({ scheme: 'null' }, options) const resolved = resolveComponents(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true) return serialize(resolved, { ...schemelessOptions, skipEscape: true }) } function resolveComponents (base, relative, options, skipNormalization) { const target = {} if (!skipNormalization) { base = parse(serialize(base, options), options) // normalize base components relative = parse(serialize(relative, options), options) // normalize relative components } 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.charAt(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 } function equal (uriA, uriB, options) { if (typeof uriA === 'string') { uriA = unescape(uriA) uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true }) } else if (typeof uriA === 'object') { uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true }) } if (typeof uriB === 'string') { uriB = unescape(uriB) uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true }) } else if (typeof uriB === 'object') { uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true }) } return uriA.toLowerCase() === uriB.toLowerCase() } function serialize (cmpts, opts) { const components = { 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 = SCHEMES[(options.scheme || components.scheme || '').toLowerCase()] // perform scheme specific serialization if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(components, options) if (components.path !== undefined) { if (!options.skipEscape) { components.path = escape(components.path) if (components.scheme !== undefined) { components.path = components.path.split('%3A').join(':') } } else { components.path = unescape(components.path) } } if (options.reference !== 'suffix' && components.scheme) { uriTokens.push(components.scheme, ':') } const authority = recomposeAuthority(components, options) if (authority !== undefined) { if (options.reference !== 'suffix') { uriTokens.push('//') } uriTokens.push(authority) if (components.path && components.path.charAt(0) !== '/') { uriTokens.push('/') } } if (components.path !== undefined) { let s = components.path if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { s = removeDotSegments(s) } if (authority === undefined) { s = s.replace(/^\/\//u, '/%2F') // don't allow the path to start with "//" } uriTokens.push(s) } if (components.query !== undefined) { uriTokens.push('?', components.query) } if (components.fragment !== undefined) { uriTokens.push('#', components.fragment) } return uriTokens.join('') } const hexLookUp = Array.from({ length: 127 }, (v, k) => /[^!"$&'()*+,\-.;=_`a-z{}~]/u.test(String.fromCharCode(k))) function nonSimpleDomain (value) { let code = 0 for (let i = 0, len = value.length; i < len; ++i) { code = value.charCodeAt(i) if (code > 126 || hexLookUp[code]) { return true } } return false } const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u function parse (uri, opts) { const options = Object.assign({}, opts) const parsed = { scheme: undefined, userinfo: undefined, host: '', port: undefined, path: '', query: undefined, fragment: undefined } const gotEncoding = uri.indexOf('%') !== -1 let isIP = false if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + 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] } if (parsed.host) { const ipv4result = normalizeIPv4(parsed.host) if (ipv4result.isIPV4 === false) { const ipv6result = normalizeIPv6(ipv4result.host, { isIPV4: false }) parsed.host = ipv6result.host.toLowerCase() isIP = ipv6result.isIPV6 } else { parsed.host = ipv4result.host isIP = true } } if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && !parsed.path && parsed.query === undefined) { 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 = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()] // 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 (gotEncoding && parsed.scheme !== undefined) { parsed.scheme = unescape(parsed.scheme) } if (gotEncoding && parsed.host !== undefined) { parsed.host = unescape(parsed.host) } if (parsed.path !== undefined && parsed.path.length) { parsed.path = escape(unescape(parsed.path)) } if (parsed.fragment !== undefined && parsed.fragment.length) { parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)) } } // perform scheme specific parsing if (schemeHandler && schemeHandler.parse) { schemeHandler.parse(parsed, options) } } else { parsed.error = parsed.error || 'URI can not be parsed.' } return parsed } const fastUri = { SCHEMES, normalize, resolve, resolveComponents, equal, serialize, parse } module.exports = fastUri module.exports.default = fastUri module.exports.fastUri = fastUri