utils.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. 'use strict'
  2. /** @type {(value: string) => boolean} */
  3. const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu)
  4. /** @type {(value: string) => boolean} */
  5. 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)
  6. /**
  7. * @param {Array<string>} input
  8. * @returns {string}
  9. */
  10. function stringArrayToHexStripped (input) {
  11. let acc = ''
  12. let code = 0
  13. let i = 0
  14. for (i = 0; i < input.length; i++) {
  15. code = input[i].charCodeAt(0)
  16. if (code === 48) {
  17. continue
  18. }
  19. if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
  20. return ''
  21. }
  22. acc += input[i]
  23. break
  24. }
  25. for (i += 1; i < input.length; i++) {
  26. code = input[i].charCodeAt(0)
  27. if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
  28. return ''
  29. }
  30. acc += input[i]
  31. }
  32. return acc
  33. }
  34. /**
  35. * @typedef {Object} GetIPV6Result
  36. * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
  37. * @property {string} address - The parsed IPv6 address.
  38. * @property {string} [zone] - The zone identifier, if present.
  39. */
  40. /**
  41. * @param {string} value
  42. * @returns {boolean}
  43. */
  44. const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
  45. /**
  46. * @param {Array<string>} buffer
  47. * @returns {boolean}
  48. */
  49. function consumeIsZone (buffer) {
  50. buffer.length = 0
  51. return true
  52. }
  53. /**
  54. * @param {Array<string>} buffer
  55. * @param {Array<string>} address
  56. * @param {GetIPV6Result} output
  57. * @returns {boolean}
  58. */
  59. function consumeHextets (buffer, address, output) {
  60. if (buffer.length) {
  61. const hex = stringArrayToHexStripped(buffer)
  62. if (hex !== '') {
  63. address.push(hex)
  64. } else {
  65. output.error = true
  66. return false
  67. }
  68. buffer.length = 0
  69. }
  70. return true
  71. }
  72. /**
  73. * @param {string} input
  74. * @returns {GetIPV6Result}
  75. */
  76. function getIPV6 (input) {
  77. let tokenCount = 0
  78. const output = { error: false, address: '', zone: '' }
  79. /** @type {Array<string>} */
  80. const address = []
  81. /** @type {Array<string>} */
  82. const buffer = []
  83. let endipv6Encountered = false
  84. let endIpv6 = false
  85. let consume = consumeHextets
  86. for (let i = 0; i < input.length; i++) {
  87. const cursor = input[i]
  88. if (cursor === '[' || cursor === ']') { continue }
  89. if (cursor === ':') {
  90. if (endipv6Encountered === true) {
  91. endIpv6 = true
  92. }
  93. if (!consume(buffer, address, output)) { break }
  94. if (++tokenCount > 7) {
  95. // not valid
  96. output.error = true
  97. break
  98. }
  99. if (i > 0 && input[i - 1] === ':') {
  100. endipv6Encountered = true
  101. }
  102. address.push(':')
  103. continue
  104. } else if (cursor === '%') {
  105. if (!consume(buffer, address, output)) { break }
  106. // switch to zone detection
  107. consume = consumeIsZone
  108. } else {
  109. buffer.push(cursor)
  110. continue
  111. }
  112. }
  113. if (buffer.length) {
  114. if (consume === consumeIsZone) {
  115. output.zone = buffer.join('')
  116. } else if (endIpv6) {
  117. address.push(buffer.join(''))
  118. } else {
  119. address.push(stringArrayToHexStripped(buffer))
  120. }
  121. }
  122. output.address = address.join('')
  123. return output
  124. }
  125. /**
  126. * @typedef {Object} NormalizeIPv6Result
  127. * @property {string} host - The normalized host.
  128. * @property {string} [escapedHost] - The escaped host.
  129. * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
  130. */
  131. /**
  132. * @param {string} host
  133. * @returns {NormalizeIPv6Result}
  134. */
  135. function normalizeIPv6 (host) {
  136. if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
  137. const ipv6 = getIPV6(host)
  138. if (!ipv6.error) {
  139. let newHost = ipv6.address
  140. let escapedHost = ipv6.address
  141. if (ipv6.zone) {
  142. newHost += '%' + ipv6.zone
  143. escapedHost += '%25' + ipv6.zone
  144. }
  145. return { host: newHost, isIPV6: true, escapedHost }
  146. } else {
  147. return { host, isIPV6: false }
  148. }
  149. }
  150. /**
  151. * @param {string} str
  152. * @param {string} token
  153. * @returns {number}
  154. */
  155. function findToken (str, token) {
  156. let ind = 0
  157. for (let i = 0; i < str.length; i++) {
  158. if (str[i] === token) ind++
  159. }
  160. return ind
  161. }
  162. /**
  163. * @param {string} path
  164. * @returns {string}
  165. *
  166. * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
  167. */
  168. function removeDotSegments (path) {
  169. let input = path
  170. const output = []
  171. let nextSlash = -1
  172. let len = 0
  173. // eslint-disable-next-line no-cond-assign
  174. while (len = input.length) {
  175. if (len === 1) {
  176. if (input === '.') {
  177. break
  178. } else if (input === '/') {
  179. output.push('/')
  180. break
  181. } else {
  182. output.push(input)
  183. break
  184. }
  185. } else if (len === 2) {
  186. if (input[0] === '.') {
  187. if (input[1] === '.') {
  188. break
  189. } else if (input[1] === '/') {
  190. input = input.slice(2)
  191. continue
  192. }
  193. } else if (input[0] === '/') {
  194. if (input[1] === '.' || input[1] === '/') {
  195. output.push('/')
  196. break
  197. }
  198. }
  199. } else if (len === 3) {
  200. if (input === '/..') {
  201. if (output.length !== 0) {
  202. output.pop()
  203. }
  204. output.push('/')
  205. break
  206. }
  207. }
  208. if (input[0] === '.') {
  209. if (input[1] === '.') {
  210. if (input[2] === '/') {
  211. input = input.slice(3)
  212. continue
  213. }
  214. } else if (input[1] === '/') {
  215. input = input.slice(2)
  216. continue
  217. }
  218. } else if (input[0] === '/') {
  219. if (input[1] === '.') {
  220. if (input[2] === '/') {
  221. input = input.slice(2)
  222. continue
  223. } else if (input[2] === '.') {
  224. if (input[3] === '/') {
  225. input = input.slice(3)
  226. if (output.length !== 0) {
  227. output.pop()
  228. }
  229. continue
  230. }
  231. }
  232. }
  233. }
  234. // Rule 2E: Move normal path segment to output
  235. if ((nextSlash = input.indexOf('/', 1)) === -1) {
  236. output.push(input)
  237. break
  238. } else {
  239. output.push(input.slice(0, nextSlash))
  240. input = input.slice(nextSlash)
  241. }
  242. }
  243. return output.join('')
  244. }
  245. /**
  246. * @param {import('../types/index').URIComponent} component
  247. * @param {boolean} esc
  248. * @returns {import('../types/index').URIComponent}
  249. */
  250. function normalizeComponentEncoding (component, esc) {
  251. const func = esc !== true ? escape : unescape
  252. if (component.scheme !== undefined) {
  253. component.scheme = func(component.scheme)
  254. }
  255. if (component.userinfo !== undefined) {
  256. component.userinfo = func(component.userinfo)
  257. }
  258. if (component.host !== undefined) {
  259. component.host = func(component.host)
  260. }
  261. if (component.path !== undefined) {
  262. component.path = func(component.path)
  263. }
  264. if (component.query !== undefined) {
  265. component.query = func(component.query)
  266. }
  267. if (component.fragment !== undefined) {
  268. component.fragment = func(component.fragment)
  269. }
  270. return component
  271. }
  272. /**
  273. * @param {import('../types/index').URIComponent} component
  274. * @returns {string|undefined}
  275. */
  276. function recomposeAuthority (component) {
  277. const uriTokens = []
  278. if (component.userinfo !== undefined) {
  279. uriTokens.push(component.userinfo)
  280. uriTokens.push('@')
  281. }
  282. if (component.host !== undefined) {
  283. let host = unescape(component.host)
  284. if (!isIPv4(host)) {
  285. const ipV6res = normalizeIPv6(host)
  286. if (ipV6res.isIPV6 === true) {
  287. host = `[${ipV6res.escapedHost}]`
  288. } else {
  289. host = component.host
  290. }
  291. }
  292. uriTokens.push(host)
  293. }
  294. if (typeof component.port === 'number' || typeof component.port === 'string') {
  295. uriTokens.push(':')
  296. uriTokens.push(String(component.port))
  297. }
  298. return uriTokens.length ? uriTokens.join('') : undefined
  299. };
  300. module.exports = {
  301. nonSimpleDomain,
  302. recomposeAuthority,
  303. normalizeComponentEncoding,
  304. removeDotSegments,
  305. isIPv4,
  306. isUUID,
  307. normalizeIPv6,
  308. stringArrayToHexStripped
  309. }