boa_engine/builtins/uri/mod.rs
1//! Boa's implementation of ECMAScript's URI Handling Functions.
2//!
3//! Uniform Resource Identifiers, or URIs, are Strings that identify resources (e.g. web pages or
4//! files) and transport protocols by which to access them (e.g. HTTP or FTP) on the Internet. The
5//! ECMAScript language itself does not provide any support for using URIs except for functions
6//! that encode and decode URIs as described in 19.2.6.2, 19.2.6.3, 19.2.6.4 and 19.2.6.5
7//!
8//! More information:
9//! - [ECMAScript reference][spec]
10//!
11//! [spec]: https://tc39.es/ecma262/#sec-uri-handling-functions
12
13mod consts;
14
15use boa_gc::{Finalize, Trace};
16
17use self::consts::{
18 is_uri_reserved_or_number_sign, is_uri_reserved_or_uri_unescaped_or_number_sign,
19 is_uri_unescaped,
20};
21
22use super::{BuiltInBuilder, BuiltInObject, IntrinsicObject};
23use crate::{
24 Context, JsArgs, JsExpect, JsNativeError, JsResult, JsString, JsValue,
25 context::intrinsics::Intrinsics,
26 js_string,
27 object::{JsFunction, JsObject},
28 realm::Realm,
29 string::{CodePoint, StaticJsStrings},
30};
31
32/// Intrinsics for the [`URI Handling Functions`][spec].
33///
34/// [spec]: https://tc39.es/ecma262/multipage/global-object.html#sec-uri-handling-functions
35#[derive(Debug, Trace, Finalize)]
36pub struct UriFunctions {
37 /// %decodeURI%
38 decode_uri: JsFunction,
39
40 /// %decodeURI%
41 decode_uri_component: JsFunction,
42
43 /// %encodeURI%
44 encode_uri: JsFunction,
45
46 /// %encodeURIcomponent%
47 encode_uri_component: JsFunction,
48}
49
50impl Default for UriFunctions {
51 fn default() -> Self {
52 Self {
53 decode_uri: JsFunction::empty_intrinsic_function(false),
54 decode_uri_component: JsFunction::empty_intrinsic_function(false),
55 encode_uri: JsFunction::empty_intrinsic_function(false),
56 encode_uri_component: JsFunction::empty_intrinsic_function(false),
57 }
58 }
59}
60
61impl UriFunctions {
62 pub(crate) fn decode_uri(&self) -> JsFunction {
63 self.decode_uri.clone()
64 }
65
66 pub(crate) fn decode_uri_component(&self) -> JsFunction {
67 self.decode_uri_component.clone()
68 }
69
70 pub(crate) fn encode_uri(&self) -> JsFunction {
71 self.encode_uri.clone()
72 }
73
74 pub(crate) fn encode_uri_component(&self) -> JsFunction {
75 self.encode_uri_component.clone()
76 }
77}
78
79/// URI Handling Functions
80#[derive(Debug, Clone, Copy)]
81pub(crate) struct DecodeUri;
82
83impl IntrinsicObject for DecodeUri {
84 fn init(realm: &Realm) {
85 BuiltInBuilder::callable_with_intrinsic::<Self>(realm, decode_uri)
86 .name(Self::NAME)
87 .length(1)
88 .build();
89 }
90 fn get(intrinsics: &Intrinsics) -> JsObject {
91 intrinsics.objects().uri_functions().decode_uri().into()
92 }
93}
94
95impl BuiltInObject for DecodeUri {
96 const NAME: JsString = StaticJsStrings::DECODE_URI;
97}
98
99pub(crate) struct DecodeUriComponent;
100
101impl IntrinsicObject for DecodeUriComponent {
102 fn init(realm: &Realm) {
103 BuiltInBuilder::callable_with_intrinsic::<Self>(realm, decode_uri_component)
104 .name(Self::NAME)
105 .length(1)
106 .build();
107 }
108 fn get(intrinsics: &Intrinsics) -> JsObject {
109 intrinsics
110 .objects()
111 .uri_functions()
112 .decode_uri_component()
113 .into()
114 }
115}
116
117impl BuiltInObject for DecodeUriComponent {
118 const NAME: JsString = StaticJsStrings::DECODE_URI_COMPONENT;
119}
120
121pub(crate) struct EncodeUri;
122
123impl IntrinsicObject for EncodeUri {
124 fn init(realm: &Realm) {
125 BuiltInBuilder::callable_with_intrinsic::<Self>(realm, encode_uri)
126 .name(Self::NAME)
127 .length(1)
128 .build();
129 }
130 fn get(intrinsics: &Intrinsics) -> JsObject {
131 intrinsics.objects().uri_functions().encode_uri().into()
132 }
133}
134
135impl BuiltInObject for EncodeUri {
136 const NAME: JsString = StaticJsStrings::ENCODE_URI;
137}
138pub(crate) struct EncodeUriComponent;
139
140impl IntrinsicObject for EncodeUriComponent {
141 fn init(realm: &Realm) {
142 BuiltInBuilder::callable_with_intrinsic::<Self>(realm, encode_uri_component)
143 .name(Self::NAME)
144 .length(1)
145 .build();
146 }
147 fn get(intrinsics: &Intrinsics) -> JsObject {
148 intrinsics
149 .objects()
150 .uri_functions()
151 .encode_uri_component()
152 .into()
153 }
154}
155
156impl BuiltInObject for EncodeUriComponent {
157 const NAME: JsString = StaticJsStrings::ENCODE_URI_COMPONENT;
158}
159
160/// Builtin JavaScript `decodeURI ( encodedURI )` function.
161///
162/// This function computes a new version of a URI in which each escape sequence and UTF-8
163/// encoding of the sort that might be introduced by the `encodeURI` function is replaced with
164/// the UTF-16 encoding of the code points that it represents. Escape sequences that could not
165/// have been introduced by `encodeURI` are not replaced.
166///
167/// More information:
168/// - [ECMAScript reference][spec]
169/// - [MDN documentation][mdn]
170///
171/// [spec]: https://tc39.es/ecma262/#sec-decodeuri-encodeduri
172/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI
173pub(crate) fn decode_uri(
174 _: &JsValue,
175 args: &[JsValue],
176 context: &mut Context,
177) -> JsResult<JsValue> {
178 let encoded_uri = args.get_or_undefined(0);
179
180 // 1. Let uriString be ? ToString(encodedURI).
181 let uri_string = encoded_uri.to_string(context)?;
182
183 // 2. Let reservedURISet be a String containing one instance of each code unit valid in uriReserved plus "#".
184 let reserved_uri_set = is_uri_reserved_or_number_sign;
185
186 // 3. Return ? Decode(uriString, reservedURISet).
187 Ok(JsValue::from(decode(&uri_string, reserved_uri_set)?))
188}
189/// Builtin JavaScript `decodeURIComponent ( encodedURIComponent )` function.
190///
191/// This function computes a new version of a URI in which each escape sequence and UTF-8
192/// encoding of the sort that might be introduced by the `encodeURIComponent` function is
193/// replaced with the UTF-16 encoding of the code points that it represents.
194///
195/// More information:
196/// - [ECMAScript reference][spec]
197/// - [MDN documentation][mdn]
198///
199/// [spec]: https://tc39.es/ecma262/#sec-decodeuricomponent-encodeduricomponent
200/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent
201pub(crate) fn decode_uri_component(
202 _: &JsValue,
203 args: &[JsValue],
204 context: &mut Context,
205) -> JsResult<JsValue> {
206 let encoded_uri_component = args.get_or_undefined(0);
207
208 // 1. Let componentString be ? ToString(encodedURIComponent).
209 let component_string = encoded_uri_component.to_string(context)?;
210
211 // 2. Let reservedURIComponentSet be the empty String.
212 let reserved_uri_component_set = |_: u16| false;
213
214 // 3. Return ? Decode(componentString, reservedURIComponentSet).
215 Ok(JsValue::from(decode(
216 &component_string,
217 reserved_uri_component_set,
218 )?))
219}
220
221/// Builtin JavaScript `encodeURI ( uri )` function.
222///
223/// This function computes a new version of a UTF-16 encoded (6.1.4) URI in which each instance
224/// of certain code points is replaced by one, two, three, or four escape sequences
225/// representing the UTF-8 encoding of the code points.
226///
227/// More information:
228/// - [ECMAScript reference][spec]
229/// - [MDN documentation][mdn]
230///
231/// [spec]: https://tc39.es/ecma262/#sec-encodeuri-uri
232/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
233pub(crate) fn encode_uri(
234 _: &JsValue,
235 args: &[JsValue],
236 context: &mut Context,
237) -> JsResult<JsValue> {
238 let uri = args.get_or_undefined(0);
239
240 // 1. Let uriString be ? ToString(uri).
241 let uri_string = uri.to_string(context)?;
242
243 // 2. Let unescapedURISet be a String containing one instance of each code unit valid in uriReserved and uriUnescaped plus "#".
244 let unescaped_uri_set = is_uri_reserved_or_uri_unescaped_or_number_sign;
245
246 // 3. Return ? Encode(uriString, unescapedURISet).
247 Ok(JsValue::from(encode(&uri_string, unescaped_uri_set)?))
248}
249
250/// Builtin JavaScript `encodeURIComponent ( uriComponent )` function.
251///
252/// This function computes a new version of a UTF-16 encoded (6.1.4) URI in which each instance
253/// of certain code points is replaced by one, two, three, or four escape sequences
254/// representing the UTF-8 encoding of the code point.
255///
256/// More information:
257/// - [ECMAScript reference][spec]
258/// - [MDN documentation][mdn]
259///
260/// [spec]: https://tc39.es/ecma262/#sec-encodeuricomponent-uricomponent
261/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
262pub(crate) fn encode_uri_component(
263 _: &JsValue,
264 args: &[JsValue],
265 context: &mut Context,
266) -> JsResult<JsValue> {
267 let uri_component = args.get_or_undefined(0);
268
269 // 1. Let componentString be ? ToString(uriComponent).
270 let component_string = uri_component.to_string(context)?;
271
272 // 2. Let unescapedURIComponentSet be a String containing one instance of each code unit valid in uriUnescaped.
273 let unescaped_uri_component_set = is_uri_unescaped;
274
275 // 3. Return ? Encode(componentString, unescapedURIComponentSet).
276 Ok(JsValue::from(encode(
277 &component_string,
278 unescaped_uri_component_set,
279 )?))
280}
281
282/// The `Encode ( string, unescapedSet )` abstract operation
283///
284/// The abstract operation Encode takes arguments `string` (a String) and `unescapedSet` (a String)
285/// and returns either a normal completion containing a String or a throw completion. It performs
286/// URI encoding and escaping.
287///
288/// More information:
289/// - [ECMAScript reference][spec]
290///
291/// [spec]: https://tc39.es/ecma262/#sec-encode
292fn encode<F>(string: &JsString, unescaped_set: F) -> JsResult<JsString>
293where
294 F: Fn(u16) -> bool,
295{
296 // 1. Let strLen be the length of string.
297 let str_len = string.len();
298
299 // 2. Let R be the empty String.
300 let mut r = Vec::with_capacity(str_len);
301
302 // 3. Let k be 0.
303 let mut k = 0;
304 // 4. Repeat,
305 loop {
306 // a. If k = strLen, return R.
307 if k == str_len {
308 return Ok(js_string!(&r[..]));
309 }
310
311 // b. Let C be the code unit at index k within string.
312 let c = string.code_unit_at(k).js_expect("Bounds were verified")?;
313
314 // c. If C is in unescapedSet, then
315 if unescaped_set(c) {
316 // i. Set k to k + 1.
317 k += 1;
318
319 // ii. Set R to the string-concatenation of R and C.
320 r.push(c);
321 } else {
322 // d. Else,
323 // i. Let cp be CodePointAt(string, k).
324 let cp = string.code_point_at(k);
325
326 // ii. If cp.[[IsUnpairedSurrogate]] is true, throw a URIError exception.
327 let CodePoint::Unicode(ch) = cp else {
328 return Err(JsNativeError::uri()
329 .with_message("trying to encode an invalid string")
330 .into());
331 };
332
333 // iii. Set k to k + cp.[[CodeUnitCount]].
334 k += cp.code_unit_count();
335
336 // iv. Let Octets be the List of octets resulting by applying the UTF-8 transformation
337 // to cp.[[CodePoint]].
338 let mut buff = [0_u8; 4]; // Will never be more than 4 bytes
339
340 let octets = ch.encode_utf8(&mut buff);
341
342 // v. For each element octet of Octets, do
343 for octet in octets.bytes() {
344 // 1. Set R to the string-concatenation of:
345 // R
346 // "%"
347 // the String representation of octet, formatted as a two-digit uppercase
348 // hexadecimal number, padded to the left with a zero if necessary
349 r.extend(format!("%{octet:0>2X}").encode_utf16());
350 }
351 }
352 }
353}
354
355/// The `Decode ( string, reservedSet )` abstract operation.
356///
357/// The abstract operation Decode takes arguments `string` (a String) and `reservedSet` (a String)
358/// and returns either a normal completion containing a String or a throw completion. It performs
359/// URI unescaping and decoding.
360///
361/// More information:
362/// - [ECMAScript reference][spec]
363///
364/// [spec]: https://tc39.es/ecma262/#sec-decode
365#[allow(clippy::many_single_char_names)]
366fn decode<F>(string: &JsString, reserved_set: F) -> JsResult<JsString>
367where
368 F: Fn(u16) -> bool,
369{
370 // 1. Let strLen be the length of string.
371 let str_len = string.len();
372 // 2. Let R be the empty String.
373 let mut r = Vec::with_capacity(str_len);
374
375 let mut octets = Vec::with_capacity(4);
376
377 // 3. Let k be 0.
378 let mut k = 0;
379 // 4. Repeat,
380 loop {
381 // a. If k = strLen, return R.
382 // b. Let C be the code unit at index k within string.
383 let Some(c) = string.code_unit_at(k) else {
384 return Ok(js_string!(&r[..]));
385 };
386
387 // c. If C is not the code unit 0x0025 (PERCENT SIGN), then
388 #[allow(clippy::if_not_else)]
389 let s = if c != 0x0025_u16 {
390 // i. Let S be the String value containing only the code unit C.
391 Vec::from([c])
392 } else {
393 // d. Else,
394 // i. Let start be k.
395 let start = k;
396
397 // ii. If k + 2 ≥ strLen, throw a URIError exception.
398 if k + 2 >= str_len {
399 return Err(JsNativeError::uri()
400 .with_message("invalid escape character found")
401 .into());
402 }
403
404 // iii. If the code units at index (k + 1) and (k + 2) within string do not represent
405 // hexadecimal digits, throw a URIError exception.
406 // iv. Let B be the 8-bit value represented by the two hexadecimal digits at index (k + 1) and (k + 2).
407
408 // SAFETY: the indices have been verified as valid already.
409 let (high, low) = unsafe {
410 (
411 string.code_unit_at(k + 1).unwrap_unchecked(),
412 string.code_unit_at(k + 2).unwrap_unchecked(),
413 )
414 };
415 let b = decode_hex_byte(high, low).ok_or_else(|| {
416 JsNativeError::uri().with_message("invalid hexadecimal digit found")
417 })?;
418
419 // v. Set k to k + 2.
420 k += 2;
421
422 // vi. Let n be the number of leading 1 bits in B.
423 let n = b.leading_ones() as usize;
424
425 // vii. If n = 0, then
426 if n == 0 {
427 // 1. Let C be the code unit whose value is B.
428 let c = u16::from(b);
429
430 // 2. If C is not in reservedSet, then
431 if !reserved_set(c) {
432 // a. Let S be the String value containing only the code unit C.
433 Vec::from([c])
434 } else {
435 // 3. Else,
436 // a. Let S be the substring of string from start to k + 1.
437 string.get_expect(start..=k).to_vec()
438 }
439 } else {
440 // viii. Else,
441 // 1. If n = 1 or n > 4, throw a URIError exception.
442 if n == 1 || n > 4 {
443 return Err(JsNativeError::uri()
444 .with_message("invalid escaped character found")
445 .into());
446 }
447
448 // 2. If k + (3 × (n - 1)) ≥ strLen, throw a URIError exception.
449 if k + (3 * (n - 1)) >= str_len {
450 return Err(JsNativeError::uri()
451 .with_message("non-terminated escape character found")
452 .into());
453 }
454
455 // 3. Let Octets be « B ».
456 octets.push(b);
457
458 // 4. Let j be 1.
459 // 5. Repeat, while j < n,
460 for _j in 1..n {
461 // a. Set k to k + 1.
462 k += 1;
463
464 // b. If the code unit at index k within string is not the code unit 0x0025 (PERCENT SIGN), throw a URIError exception.
465 if string.code_unit_at(k) != Some(0x0025) {
466 return Err(JsNativeError::uri()
467 .with_message("escape characters must be preceded with a % sign")
468 .into());
469 }
470
471 // c. If the code units at index (k + 1) and (k + 2) within string do not represent hexadecimal digits, throw a URIError exception.
472 // d. Let B be the 8-bit value represented by the two hexadecimal digits at index (k + 1) and (k + 2).
473 // SAFETY: the indices have been verified as valid already.
474 let (high, low) = unsafe {
475 (
476 string.code_unit_at(k + 1).unwrap_unchecked(),
477 string.code_unit_at(k + 2).unwrap_unchecked(),
478 )
479 };
480 let b = decode_hex_byte(high, low).ok_or_else(|| {
481 JsNativeError::uri().with_message("invalid hexadecimal digit found")
482 })?;
483
484 // e. Set k to k + 2.
485 k += 2;
486
487 // f. Append B to Octets.
488 octets.push(b);
489
490 // g. Set j to j + 1.
491 }
492
493 // 6. Assert: The length of Octets is n.
494 assert_eq!(octets.len(), n);
495
496 // 7. If Octets does not contain a valid UTF-8 encoding of a Unicode code point, throw a URIError exception.
497 match std::str::from_utf8(&octets) {
498 Err(_) => {
499 return Err(JsNativeError::uri()
500 .with_message("invalid UTF-8 encoding found")
501 .into());
502 }
503 Ok(v) => {
504 // 8. Let V be the code point obtained by applying the UTF-8 transformation to Octets, that is, from a List of octets into a 21-bit value.
505
506 // 9. Let S be UTF16EncodeCodePoint(V).
507 // utf16_encode_codepoint(v)
508 let s = v.encode_utf16().collect::<Vec<_>>();
509 octets.clear();
510 s
511 }
512 }
513 }
514 };
515
516 // e. Set R to the string-concatenation of R and S.
517 r.extend_from_slice(&s);
518
519 // f. Set k to k + 1.
520 k += 1;
521 }
522}
523
524/// Decodes a byte from two unicode code units.
525fn decode_hex_byte(high: u16, low: u16) -> Option<u8> {
526 match (
527 char::from_u32(u32::from(high)),
528 char::from_u32(u32::from(low)),
529 ) {
530 (Some(high), Some(low)) => match (high.to_digit(16), low.to_digit(16)) {
531 (Some(high), Some(low)) => Some(((high as u8) << 4) + low as u8),
532 _ => None,
533 },
534 _ => None,
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::JsNativeErrorKind;
542
543 /// Checks that the `decode_byte()` function works as expected.
544 #[test]
545 fn decode_byte() {
546 // Sunny day tests
547 assert_eq!(
548 decode_hex_byte(u16::from(b'2'), u16::from(b'0')).unwrap(),
549 0x20
550 );
551 assert_eq!(
552 decode_hex_byte(u16::from(b'2'), u16::from(b'A')).unwrap(),
553 0x2A
554 );
555 assert_eq!(
556 decode_hex_byte(u16::from(b'3'), u16::from(b'C')).unwrap(),
557 0x3C
558 );
559 assert_eq!(
560 decode_hex_byte(u16::from(b'4'), u16::from(b'0')).unwrap(),
561 0x40
562 );
563 assert_eq!(
564 decode_hex_byte(u16::from(b'7'), u16::from(b'E')).unwrap(),
565 0x7E
566 );
567 assert_eq!(
568 decode_hex_byte(u16::from(b'0'), u16::from(b'0')).unwrap(),
569 0x00
570 );
571
572 // Rainy day tests
573 assert!(decode_hex_byte(u16::from(b'-'), u16::from(b'0')).is_none());
574 assert!(decode_hex_byte(u16::from(b'f'), u16::from(b'~')).is_none());
575 assert!(decode_hex_byte(u16::from(b'A'), 0_u16).is_none());
576 assert!(decode_hex_byte(u16::from(b'%'), u16::from(b'&')).is_none());
577
578 assert!(decode_hex_byte(0xFACD_u16, u16::from(b'-')).is_none());
579 assert!(decode_hex_byte(u16::from(b'-'), 0xA0FD_u16).is_none());
580 }
581
582 #[test]
583 fn decode_uri_reports_uri_error_on_incomplete_sequence() {
584 // "%E7%9A%8" is an incomplete 3-byte UTF-8 sequence and must error with Uri kind
585 let s = js_string!("%E7%9A%8");
586 let err = decode(&s, |_| false).expect_err("should error on incomplete escape");
587 let native = err.as_native().expect("error should be native");
588 assert!(matches!(native.kind(), JsNativeErrorKind::Uri));
589 }
590
591 #[test]
592 fn decode_preserves_non_bmp_characters() {
593 let s = js_string!("\u{1F600}");
594 let decoded = decode(&s, |_| false).expect("decode should succeed");
595 assert_eq!(decoded, s);
596 }
597
598 #[test]
599 fn decode_preserves_non_bmp_characters_for_decode_uri_set() {
600 let s = js_string!("\u{1F600}");
601 let decoded = decode(&s, is_uri_reserved_or_number_sign).expect("decode should succeed");
602 assert_eq!(decoded, s);
603 }
604}