index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. 'use strict'
  2. const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
  3. const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
  4. const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
  5. const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.'
  6. /**
  7. * @param {string} scheme
  8. * @returns {string}
  9. */
  10. function decodeValidScheme (scheme) {
  11. const decodedScheme = unescape(String(scheme))
  12. if (!VALID_SCHEME.test(decodedScheme)) {
  13. throw new TypeError(MALFORMED_SCHEME_ERROR)
  14. }
  15. return decodedScheme
  16. }
  17. /**
  18. * @template {import('./types/index').URIComponent|string} T
  19. * @param {T} uri
  20. * @param {import('./types/index').Options} [options]
  21. * @returns {T}
  22. */
  23. function normalize (uri, options) {
  24. if (typeof uri === 'string') {
  25. uri = /** @type {T} */ (normalizeString(uri, options))
  26. } else if (typeof uri === 'object') {
  27. uri = /** @type {T} */ (parse(serialize(uri, options), options))
  28. }
  29. return uri
  30. }
  31. /**
  32. * @param {string} baseURI
  33. * @param {string} relativeURI
  34. * @param {import('./types/index').Options} [options]
  35. * @returns {string}
  36. */
  37. function resolve (baseURI, relativeURI, options) {
  38. const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
  39. const {
  40. parsed: baseParsed,
  41. malformedAuthorityOrPort: baseMalformed,
  42. malformedPercentEncoding: baseMalformedPercentEncoding,
  43. malformedSchemeSpecific: baseMalformedSchemeSpecific,
  44. malformedHost: baseMalformedHost,
  45. malformedScheme: baseMalformedScheme
  46. } = parseWithStatus(baseURI, schemelessOptions)
  47. const {
  48. parsed: relativeParsed,
  49. malformedAuthorityOrPort: relativeMalformed,
  50. malformedPercentEncoding: relativeMalformedPercentEncoding,
  51. malformedSchemeSpecific: relativeMalformedSchemeSpecific,
  52. malformedHost: relativeMalformedHost,
  53. malformedScheme: relativeMalformedScheme
  54. } = parseWithStatus(relativeURI, schemelessOptions)
  55. if (
  56. baseMalformed ||
  57. relativeMalformed ||
  58. baseMalformedPercentEncoding ||
  59. relativeMalformedPercentEncoding ||
  60. baseMalformedSchemeSpecific ||
  61. relativeMalformedSchemeSpecific ||
  62. baseMalformedHost ||
  63. relativeMalformedHost ||
  64. baseMalformedScheme ||
  65. relativeMalformedScheme
  66. ) {
  67. throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
  68. }
  69. const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
  70. const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
  71. const resolvedHost = resolved.host
  72. const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
  73. (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
  74. canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
  75. // Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
  76. // parser can reject them even though fast-uri preserves them safely as RFC
  77. // 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
  78. const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
  79. !/\P{ASCII}/u.test(resolvedHost)
  80. if (resolved.error && !encodedASCIIHost) {
  81. throw new Error(resolved.error)
  82. }
  83. schemelessOptions.skipEscape = true
  84. return serialize(resolved, schemelessOptions)
  85. }
  86. /**
  87. * @param {import ('./types/index').URIComponent} base
  88. * @param {import ('./types/index').URIComponent} relative
  89. * @param {import('./types/index').Options} [options]
  90. * @param {boolean} [skipNormalization=false]
  91. * @returns {import ('./types/index').URIComponent}
  92. */
  93. function resolveComponent (base, relative, options, skipNormalization) {
  94. /** @type {import('./types/index').URIComponent} */
  95. const target = {}
  96. if (!skipNormalization) {
  97. base = parse(serialize(base, options), options) // normalize base component
  98. relative = parse(serialize(relative, options), options) // normalize relative component
  99. }
  100. options = options || {}
  101. if (!options.tolerant && relative.scheme) {
  102. target.scheme = relative.scheme
  103. // target.authority = relative.authority;
  104. target.userinfo = relative.userinfo
  105. target.host = relative.host
  106. target.port = relative.port
  107. target.path = removeDotSegments(relative.path || '')
  108. target.query = relative.query
  109. } else {
  110. if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) {
  111. // target.authority = relative.authority;
  112. target.userinfo = relative.userinfo
  113. target.host = relative.host
  114. target.port = relative.port
  115. target.path = removeDotSegments(relative.path || '')
  116. target.query = relative.query
  117. } else {
  118. if (!relative.path) {
  119. target.path = base.path
  120. if (relative.query !== undefined) {
  121. target.query = relative.query
  122. } else {
  123. target.query = base.query
  124. }
  125. } else {
  126. if (relative.path[0] === '/') {
  127. target.path = removeDotSegments(relative.path)
  128. } else {
  129. if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) {
  130. target.path = '/' + relative.path
  131. } else if (!base.path) {
  132. target.path = relative.path
  133. } else {
  134. target.path = base.path.slice(0, base.path.lastIndexOf('/') + 1) + relative.path
  135. }
  136. target.path = removeDotSegments(target.path)
  137. }
  138. target.query = relative.query
  139. }
  140. // target.authority = base.authority;
  141. target.userinfo = base.userinfo
  142. target.host = base.host
  143. target.port = base.port
  144. }
  145. target.scheme = base.scheme
  146. }
  147. target.fragment = relative.fragment
  148. return target
  149. }
  150. /**
  151. * @param {import ('./types/index').URIComponent|string} uriA
  152. * @param {import ('./types/index').URIComponent|string} uriB
  153. * @param {import ('./types/index').Options} options
  154. * @returns {boolean}
  155. */
  156. function equal (uriA, uriB, options) {
  157. const normalizedA = normalizeComparableURI(uriA, options)
  158. const normalizedB = normalizeComparableURI(uriB, options)
  159. return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
  160. }
  161. /**
  162. * @param {Readonly<import('./types/index').URIComponent>} cmpts
  163. * @param {import('./types/index').Options} [opts]
  164. * @returns {string}
  165. */
  166. function serialize (cmpts, opts) {
  167. const component = {
  168. host: cmpts.host,
  169. scheme: cmpts.scheme,
  170. userinfo: cmpts.userinfo,
  171. port: cmpts.port,
  172. path: cmpts.path,
  173. query: cmpts.query,
  174. nid: cmpts.nid,
  175. nss: cmpts.nss,
  176. uuid: cmpts.uuid,
  177. fragment: cmpts.fragment,
  178. reference: cmpts.reference,
  179. resourceName: cmpts.resourceName,
  180. secure: cmpts.secure,
  181. error: ''
  182. }
  183. const options = Object.assign({}, opts)
  184. const uriTokens = []
  185. if (component.scheme) {
  186. component.scheme = decodeValidScheme(component.scheme)
  187. }
  188. // find scheme handler
  189. const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
  190. // perform scheme specific serialization
  191. if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
  192. const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
  193. const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
  194. if (component.path !== undefined) {
  195. if (!options.skipEscape) {
  196. component.path = serializePathEncoding(component.path, pathNoScheme)
  197. } else {
  198. component.path = normalizePercentEncoding(component.path)
  199. }
  200. }
  201. if (options.reference !== 'suffix' && component.scheme) {
  202. // Scheme handlers may replace the scheme during serialization.
  203. component.scheme = decodeValidScheme(component.scheme)
  204. uriTokens.push(component.scheme, ':')
  205. }
  206. const authority = recomposeAuthority(component)
  207. if (authority !== undefined) {
  208. if (options.reference !== 'suffix') {
  209. uriTokens.push('//')
  210. }
  211. uriTokens.push(authority)
  212. if (component.path && component.path[0] !== '/') {
  213. uriTokens.push('/')
  214. }
  215. }
  216. if (component.path !== undefined) {
  217. let s = component.path
  218. if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
  219. s = removeDotSegments(s)
  220. }
  221. // Dot-segment removal can expose a colon that was not originally in the
  222. // first segment (for example, "./a:b"). Reapply path-noscheme encoding so
  223. // the serialized relative reference cannot be reparsed as a URI scheme.
  224. if (pathNoScheme) {
  225. s = serializePathEncoding(s, true)
  226. }
  227. if (
  228. authority === undefined &&
  229. s[0] === '/' &&
  230. s[1] === '/'
  231. ) {
  232. // don't allow the path to start with "//"
  233. s = '/%2F' + s.slice(2)
  234. }
  235. uriTokens.push(s)
  236. }
  237. if (component.query !== undefined) {
  238. uriTokens.push('?', encodeQuery(component.query))
  239. }
  240. if (component.fragment !== undefined) {
  241. uriTokens.push('#', encodeFragment(component.fragment))
  242. }
  243. return uriTokens.join('')
  244. }
  245. const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
  246. // Captures the authority component (between "//" and the next "/", "?" or "#"),
  247. // with or without a scheme prefix, for the literal-backslash rejection below.
  248. const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
  249. // Captures the leading authority-introducer region after an optional scheme: a
  250. // run of forward slashes, backslashes, and the characters the WHATWG URL parser
  251. // removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
  252. // is exactly "//". Node treats "\" as "/" on special schemes and strips those
  253. // characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
  254. // "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
  255. // the path group (host confusion / SSRF / redirect bypass).
  256. const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
  257. /**
  258. * @param {import('./types/index').URIComponent} parsed
  259. * @param {RegExpMatchArray} matches
  260. * @returns {string|undefined}
  261. */
  262. function getParseError (parsed, matches) {
  263. if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
  264. return 'URI path must start with "/" when authority is present.'
  265. }
  266. if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
  267. return 'URI port is malformed.'
  268. }
  269. return undefined
  270. }
  271. /**
  272. * Checks percent syntax without decoding the represented octets. RFC 3986
  273. * percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
  274. * though they are not independently valid UTF-8.
  275. *
  276. * @param {string|undefined} component
  277. * @returns {boolean}
  278. */
  279. function hasMalformedPercentEncoding (component) {
  280. if (component === undefined) return false
  281. let percent = component.indexOf('%')
  282. while (percent !== -1) {
  283. if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
  284. return true
  285. }
  286. percent = component.indexOf('%', percent + 3)
  287. }
  288. return false
  289. }
  290. /**
  291. * @param {RegExpMatchArray} matches
  292. * @returns {boolean}
  293. */
  294. function hasMalformedComponentPercentEncoding (matches) {
  295. // Bracketed IP literals use a raw "%" as the zone separator for historical
  296. // compatibility. Their parsing is intentionally left to normalizeIPv6.
  297. const host = matches[4]
  298. return hasMalformedPercentEncoding(matches[3]) ||
  299. (host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
  300. hasMalformedPercentEncoding(matches[6]) ||
  301. hasMalformedPercentEncoding(matches[7]) ||
  302. hasMalformedPercentEncoding(matches[8])
  303. }
  304. /**
  305. * @param {import('./types/index').URIComponent} parsed
  306. * @param {import('./types/index').Options} options
  307. * @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
  308. * @param {boolean} isIP
  309. * @returns {boolean} whether host conversion failed
  310. */
  311. function canonicalizeHost (parsed, options, schemeHandler, isIP) {
  312. if (
  313. !options.unicodeSupport &&
  314. (!schemeHandler || !schemeHandler.unicodeSupport) &&
  315. parsed.host &&
  316. parsed.host[0] !== '[' &&
  317. (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
  318. isIP === false &&
  319. nonSimpleDomain(parsed.host)
  320. ) {
  321. try {
  322. parsed.host = new URL('http://' + parsed.host).hostname
  323. } catch (e) {
  324. parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
  325. return true
  326. }
  327. }
  328. return false
  329. }
  330. /**
  331. * @param {string} uri
  332. * @param {import('./types/index').Options} [opts]
  333. * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
  334. */
  335. function parseWithStatus (uri, opts) {
  336. const options = Object.assign({}, opts)
  337. /** @type {import('./types/index').URIComponent} */
  338. const parsed = {
  339. scheme: undefined,
  340. userinfo: undefined,
  341. host: '',
  342. port: undefined,
  343. path: '',
  344. query: undefined,
  345. fragment: undefined
  346. }
  347. let malformedAuthorityOrPort = false
  348. let malformedPercentEncoding = false
  349. let malformedSchemeSpecific = false
  350. let malformedHost = false
  351. let malformedIPLiteral = false
  352. let malformedScheme = false
  353. let isIP = false
  354. if (options.reference === 'suffix') {
  355. if (options.scheme) {
  356. uri = options.scheme + ':' + uri
  357. } else {
  358. uri = '//' + uri
  359. }
  360. }
  361. // A literal backslash (U+005C) is not a valid RFC 3986 URI character and is
  362. // not an authority delimiter. Reject it in the authority rather than
  363. // rewriting it: normalizing "\" -> "/" (WHATWG error recovery) could silently
  364. // change the resource identified by an otherwise-invalid input, and lets "\"
  365. // act as a host delimiter here while Node's native URL parses a different
  366. // host (SSRF / redirect / origin-allowlist bypass). Percent-encoded %5C is
  367. // untouched and remains valid encoded data.
  368. const authorityMatch = uri.match(AUTHORITY_PREFIX)
  369. if (authorityMatch !== null && authorityMatch[1].indexOf('\\') !== -1) {
  370. parsed.error = 'URI authority must not contain a literal backslash.'
  371. malformedAuthorityOrPort = true
  372. }
  373. // Reject a malformed or whitespace-smuggled authority introducer. fast-uri
  374. // only recognizes a literal "//"; anything else in the leading separator run
  375. // (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
  376. // Node strips) means the authority fast-uri parses differs from the one Node's
  377. // URL resolves. Reject rather than rewrite, mirroring the literal-backslash
  378. // guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
  379. const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
  380. if (introducerMatch !== null) {
  381. const region = introducerMatch[1]
  382. const normalizedRegion = region.replace(/[\t\n\r]/g, '')
  383. // Two or more leading separators introduce an authority.
  384. if (normalizedRegion.length >= 2) {
  385. if (normalizedRegion.slice(0, 2) !== '//') {
  386. parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
  387. malformedAuthorityOrPort = true
  388. } else if (region.length !== normalizedRegion.length) {
  389. parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
  390. malformedAuthorityOrPort = true
  391. }
  392. }
  393. }
  394. const matches = uri.match(URI_PARSE)
  395. if (matches) {
  396. // store each component
  397. parsed.scheme = matches[1]
  398. parsed.userinfo = matches[3]
  399. parsed.host = matches[4]
  400. parsed.port = parseInt(matches[5], 10)
  401. parsed.path = matches[6] || ''
  402. parsed.query = matches[7]
  403. parsed.fragment = matches[8]
  404. if (parsed.scheme !== undefined) {
  405. const decodedScheme = unescape(parsed.scheme)
  406. if (VALID_SCHEME.test(decodedScheme)) {
  407. parsed.scheme = decodedScheme.toLowerCase()
  408. } else {
  409. parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
  410. malformedScheme = true
  411. }
  412. }
  413. malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
  414. if (malformedPercentEncoding) {
  415. parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
  416. }
  417. // fix port number
  418. if (isNaN(parsed.port)) {
  419. parsed.port = matches[5]
  420. }
  421. const parseError = getParseError(parsed, matches)
  422. if (parseError !== undefined) {
  423. parsed.error = parsed.error || parseError
  424. malformedAuthorityOrPort = true
  425. }
  426. if (parsed.host) {
  427. const ipv4result = isIPv4(parsed.host)
  428. if (ipv4result === false) {
  429. const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
  430. const ipv6result = normalizeIPv6(parsed.host)
  431. isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
  432. malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
  433. parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
  434. if (malformedIPLiteral) {
  435. parsed.error = parsed.error || 'URI host is malformed.'
  436. malformedAuthorityOrPort = true
  437. }
  438. } else {
  439. isIP = true
  440. }
  441. }
  442. if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && parsed.query === undefined && !parsed.path) {
  443. parsed.reference = 'same-document'
  444. } else if (parsed.scheme === undefined) {
  445. parsed.reference = 'relative'
  446. } else if (parsed.fragment === undefined) {
  447. parsed.reference = 'absolute'
  448. } else {
  449. parsed.reference = 'uri'
  450. }
  451. // check for reference errors
  452. if (options.reference && options.reference !== 'suffix' && options.reference !== parsed.reference) {
  453. parsed.error = parsed.error || 'URI is not a ' + options.reference + ' reference.'
  454. }
  455. // find scheme handler
  456. const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
  457. // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
  458. malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
  459. if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
  460. if (uri.indexOf('%') !== -1) {
  461. if (parsed.host !== undefined && !malformedIPLiteral) {
  462. const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
  463. parsed.host = reescapeHostDelimiters(host, isIP)
  464. }
  465. }
  466. if (parsed.path) {
  467. parsed.path = normalizePathEncoding(parsed.path)
  468. }
  469. if (parsed.query) {
  470. parsed.query = normalizeQueryFragmentEncoding(parsed.query)
  471. }
  472. if (parsed.fragment) {
  473. parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
  474. }
  475. }
  476. // perform scheme specific parsing
  477. if (schemeHandler && schemeHandler.parse) {
  478. schemeHandler.parse(parsed, options)
  479. if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
  480. malformedSchemeSpecific = true
  481. }
  482. }
  483. } else {
  484. parsed.error = parsed.error || 'URI can not be parsed.'
  485. }
  486. return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
  487. }
  488. /**
  489. * @param {string} uri
  490. * @param {import('./types/index').Options} [opts]
  491. * @returns
  492. */
  493. function parse (uri, opts) {
  494. return parseWithStatus(uri, opts).parsed
  495. }
  496. /**
  497. * @param {string} uri
  498. * @param {import('./types/index').Options} [opts]
  499. * @returns {string}
  500. */
  501. function normalizeString (uri, opts) {
  502. return normalizeStringWithStatus(uri, opts).normalized
  503. }
  504. /**
  505. * @param {string} uri
  506. * @param {import('./types/index').Options} [opts]
  507. * @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
  508. */
  509. function normalizeStringWithStatus (uri, opts) {
  510. const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
  511. return {
  512. normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
  513. malformedAuthorityOrPort,
  514. malformedPercentEncoding,
  515. malformedSchemeSpecific,
  516. malformedHost,
  517. malformedScheme
  518. }
  519. }
  520. /**
  521. * @param {import ('./types/index').URIComponent|string} uri
  522. * @param {import('./types/index').Options} [opts]
  523. * @returns {string|undefined}
  524. */
  525. function normalizeComparableURI (uri, opts) {
  526. if (typeof uri !== 'string' && typeof uri !== 'object') {
  527. return undefined
  528. }
  529. let value
  530. try {
  531. value = typeof uri === 'string' ? uri : serialize(uri, opts)
  532. } catch {
  533. return undefined
  534. }
  535. const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
  536. return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
  537. }
  538. const fastUri = {
  539. SCHEMES,
  540. normalize,
  541. resolve,
  542. resolveComponent,
  543. equal,
  544. serialize,
  545. parse
  546. }
  547. module.exports = fastUri
  548. module.exports.default = fastUri
  549. module.exports.fastUri = fastUri