Skip to main content

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}