source: frontend/node_modules/fast-uri/lib/utils.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 10.5 KB
Line 
1'use strict'
2
3/** @type {(value: string) => boolean} */
4const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu)
5
6/** @type {(value: string) => boolean} */
7const 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)
8
9/** @type {(value: string) => boolean} */
10const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
11
12/** @type {(value: string) => boolean} */
13const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
14
15/** @type {(value: string) => boolean} */
16const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
17
18/**
19 * @param {Array<string>} input
20 * @returns {string}
21 */
22function stringArrayToHexStripped (input) {
23 let acc = ''
24 let code = 0
25 let i = 0
26
27 for (i = 0; i < input.length; i++) {
28 code = input[i].charCodeAt(0)
29 if (code === 48) {
30 continue
31 }
32 if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
33 return ''
34 }
35 acc += input[i]
36 break
37 }
38
39 for (i += 1; i < input.length; i++) {
40 code = input[i].charCodeAt(0)
41 if (!((code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102))) {
42 return ''
43 }
44 acc += input[i]
45 }
46 return acc
47}
48
49/**
50 * @typedef {Object} GetIPV6Result
51 * @property {boolean} error - Indicates if there was an error parsing the IPv6 address.
52 * @property {string} address - The parsed IPv6 address.
53 * @property {string} [zone] - The zone identifier, if present.
54 */
55
56/**
57 * @param {string} value
58 * @returns {boolean}
59 */
60const nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u)
61
62/**
63 * @param {Array<string>} buffer
64 * @returns {boolean}
65 */
66function consumeIsZone (buffer) {
67 buffer.length = 0
68 return true
69}
70
71/**
72 * @param {Array<string>} buffer
73 * @param {Array<string>} address
74 * @param {GetIPV6Result} output
75 * @returns {boolean}
76 */
77function consumeHextets (buffer, address, output) {
78 if (buffer.length) {
79 const hex = stringArrayToHexStripped(buffer)
80 if (hex !== '') {
81 address.push(hex)
82 } else {
83 output.error = true
84 return false
85 }
86 buffer.length = 0
87 }
88 return true
89}
90
91/**
92 * @param {string} input
93 * @returns {GetIPV6Result}
94 */
95function getIPV6 (input) {
96 let tokenCount = 0
97 const output = { error: false, address: '', zone: '' }
98 /** @type {Array<string>} */
99 const address = []
100 /** @type {Array<string>} */
101 const buffer = []
102 let endipv6Encountered = false
103 let endIpv6 = false
104
105 let consume = consumeHextets
106
107 for (let i = 0; i < input.length; i++) {
108 const cursor = input[i]
109 if (cursor === '[' || cursor === ']') { continue }
110 if (cursor === ':') {
111 if (endipv6Encountered === true) {
112 endIpv6 = true
113 }
114 if (!consume(buffer, address, output)) { break }
115 if (++tokenCount > 7) {
116 // not valid
117 output.error = true
118 break
119 }
120 if (i > 0 && input[i - 1] === ':') {
121 endipv6Encountered = true
122 }
123 address.push(':')
124 continue
125 } else if (cursor === '%') {
126 if (!consume(buffer, address, output)) { break }
127 // switch to zone detection
128 consume = consumeIsZone
129 } else {
130 buffer.push(cursor)
131 continue
132 }
133 }
134 if (buffer.length) {
135 if (consume === consumeIsZone) {
136 output.zone = buffer.join('')
137 } else if (endIpv6) {
138 address.push(buffer.join(''))
139 } else {
140 address.push(stringArrayToHexStripped(buffer))
141 }
142 }
143 output.address = address.join('')
144 return output
145}
146
147/**
148 * @typedef {Object} NormalizeIPv6Result
149 * @property {string} host - The normalized host.
150 * @property {string} [escapedHost] - The escaped host.
151 * @property {boolean} isIPV6 - Indicates if the host is an IPv6 address.
152 */
153
154/**
155 * @param {string} host
156 * @returns {NormalizeIPv6Result}
157 */
158function normalizeIPv6 (host) {
159 if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
160 const ipv6 = getIPV6(host)
161
162 if (!ipv6.error) {
163 let newHost = ipv6.address
164 let escapedHost = ipv6.address
165 if (ipv6.zone) {
166 newHost += '%' + ipv6.zone
167 escapedHost += '%25' + ipv6.zone
168 }
169 return { host: newHost, isIPV6: true, escapedHost }
170 } else {
171 return { host, isIPV6: false }
172 }
173}
174
175/**
176 * @param {string} str
177 * @param {string} token
178 * @returns {number}
179 */
180function findToken (str, token) {
181 let ind = 0
182 for (let i = 0; i < str.length; i++) {
183 if (str[i] === token) ind++
184 }
185 return ind
186}
187
188/**
189 * @param {string} path
190 * @returns {string}
191 *
192 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
193 */
194function removeDotSegments (path) {
195 let input = path
196 const output = []
197 let nextSlash = -1
198 let len = 0
199
200 // eslint-disable-next-line no-cond-assign
201 while (len = input.length) {
202 if (len === 1) {
203 if (input === '.') {
204 break
205 } else if (input === '/') {
206 output.push('/')
207 break
208 } else {
209 output.push(input)
210 break
211 }
212 } else if (len === 2) {
213 if (input[0] === '.') {
214 if (input[1] === '.') {
215 break
216 } else if (input[1] === '/') {
217 input = input.slice(2)
218 continue
219 }
220 } else if (input[0] === '/') {
221 if (input[1] === '.' || input[1] === '/') {
222 output.push('/')
223 break
224 }
225 }
226 } else if (len === 3) {
227 if (input === '/..') {
228 if (output.length !== 0) {
229 output.pop()
230 }
231 output.push('/')
232 break
233 }
234 }
235 if (input[0] === '.') {
236 if (input[1] === '.') {
237 if (input[2] === '/') {
238 input = input.slice(3)
239 continue
240 }
241 } else if (input[1] === '/') {
242 input = input.slice(2)
243 continue
244 }
245 } else if (input[0] === '/') {
246 if (input[1] === '.') {
247 if (input[2] === '/') {
248 input = input.slice(2)
249 continue
250 } else if (input[2] === '.') {
251 if (input[3] === '/') {
252 input = input.slice(3)
253 if (output.length !== 0) {
254 output.pop()
255 }
256 continue
257 }
258 }
259 }
260 }
261
262 // Rule 2E: Move normal path segment to output
263 if ((nextSlash = input.indexOf('/', 1)) === -1) {
264 output.push(input)
265 break
266 } else {
267 output.push(input.slice(0, nextSlash))
268 input = input.slice(nextSlash)
269 }
270 }
271
272 return output.join('')
273}
274
275/**
276 * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
277 * After the URI regex parses, these characters cannot be literal in the host
278 * field, so any that appear after decoding came from percent-encoding and
279 * must be restored to prevent authority structure changes.
280 *
281 * @param {string} host
282 * @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
283 * @returns {string}
284 */
285const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
286const HOST_DELIM_RE = /[@/?#:]/g
287const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
288
289function reescapeHostDelimiters (host, isIP) {
290 const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
291 re.lastIndex = 0
292 return host.replace(re, (ch) => HOST_DELIMS[ch])
293}
294
295/**
296 * Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
297 * Reserved delimiters such as `%2F` and `%2E` stay escaped.
298 *
299 * @param {string} input
300 * @param {boolean} [decodeUnreserved=false]
301 * @returns {string}
302 */
303function normalizePercentEncoding (input, decodeUnreserved = false) {
304 if (input.indexOf('%') === -1) {
305 return input
306 }
307
308 let output = ''
309
310 for (let i = 0; i < input.length; i++) {
311 if (input[i] === '%' && i + 2 < input.length) {
312 const hex = input.slice(i + 1, i + 3)
313 if (isHexPair(hex)) {
314 const normalizedHex = hex.toUpperCase()
315 const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
316
317 if (decodeUnreserved && isUnreserved(decoded)) {
318 output += decoded
319 } else {
320 output += '%' + normalizedHex
321 }
322
323 i += 2
324 continue
325 }
326 }
327
328 output += input[i]
329 }
330
331 return output
332}
333
334/**
335 * Normalizes path data without turning reserved escapes into live path syntax.
336 * Valid escapes are uppercased, raw unsafe characters are escaped, and only
337 * unreserved bytes that are not `.` are decoded.
338 *
339 * @param {string} input
340 * @returns {string}
341 */
342function normalizePathEncoding (input) {
343 let output = ''
344
345 for (let i = 0; i < input.length; i++) {
346 if (input[i] === '%' && i + 2 < input.length) {
347 const hex = input.slice(i + 1, i + 3)
348 if (isHexPair(hex)) {
349 const normalizedHex = hex.toUpperCase()
350 const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
351
352 if (decoded !== '.' && isUnreserved(decoded)) {
353 output += decoded
354 } else {
355 output += '%' + normalizedHex
356 }
357
358 i += 2
359 continue
360 }
361 }
362
363 if (isPathCharacter(input[i])) {
364 output += input[i]
365 } else {
366 output += escape(input[i])
367 }
368 }
369
370 return output
371}
372
373/**
374 * Escapes a component while preserving existing valid percent escapes.
375 *
376 * @param {string} input
377 * @returns {string}
378 */
379function escapePreservingEscapes (input) {
380 let output = ''
381
382 for (let i = 0; i < input.length; i++) {
383 if (input[i] === '%' && i + 2 < input.length) {
384 const hex = input.slice(i + 1, i + 3)
385 if (isHexPair(hex)) {
386 output += '%' + hex.toUpperCase()
387 i += 2
388 continue
389 }
390 }
391
392 output += escape(input[i])
393 }
394
395 return output
396}
397
398/**
399 * @param {import('../types/index').URIComponent} component
400 * @returns {string|undefined}
401 */
402function recomposeAuthority (component) {
403 const uriTokens = []
404
405 if (component.userinfo !== undefined) {
406 uriTokens.push(component.userinfo)
407 uriTokens.push('@')
408 }
409
410 if (component.host !== undefined) {
411 let host = unescape(component.host)
412 if (!isIPv4(host)) {
413 const ipV6res = normalizeIPv6(host)
414 if (ipV6res.isIPV6 === true) {
415 host = `[${ipV6res.escapedHost}]`
416 } else {
417 host = reescapeHostDelimiters(host, false)
418 }
419 }
420 uriTokens.push(host)
421 }
422
423 if (typeof component.port === 'number' || typeof component.port === 'string') {
424 uriTokens.push(':')
425 uriTokens.push(String(component.port))
426 }
427
428 return uriTokens.length ? uriTokens.join('') : undefined
429};
430
431module.exports = {
432 nonSimpleDomain,
433 recomposeAuthority,
434 reescapeHostDelimiters,
435 normalizePercentEncoding,
436 normalizePathEncoding,
437 escapePreservingEscapes,
438 removeDotSegments,
439 isIPv4,
440 isUUID,
441 normalizeIPv6,
442 stringArrayToHexStripped
443}
Note: See TracBrowser for help on using the repository browser.