mod consts;
use boa_gc::{Finalize, Trace};
use self::consts::{
is_uri_reserved_or_number_sign, is_uri_reserved_or_uri_unescaped_or_number_sign,
is_uri_unescaped,
};
use super::{BuiltInBuilder, BuiltInObject, IntrinsicObject};
use crate::{
context::intrinsics::Intrinsics,
js_string,
object::{JsFunction, JsObject},
realm::Realm,
string::CodePoint,
Context, JsArgs, JsNativeError, JsResult, JsString, JsValue,
};
#[derive(Debug, Trace, Finalize)]
pub struct UriFunctions {
decode_uri: JsFunction,
decode_uri_component: JsFunction,
encode_uri: JsFunction,
encode_uri_component: JsFunction,
}
impl Default for UriFunctions {
fn default() -> Self {
Self {
decode_uri: JsFunction::empty_intrinsic_function(false),
decode_uri_component: JsFunction::empty_intrinsic_function(false),
encode_uri: JsFunction::empty_intrinsic_function(false),
encode_uri_component: JsFunction::empty_intrinsic_function(false),
}
}
}
impl UriFunctions {
pub(crate) fn decode_uri(&self) -> JsFunction {
self.decode_uri.clone()
}
pub(crate) fn decode_uri_component(&self) -> JsFunction {
self.decode_uri_component.clone()
}
pub(crate) fn encode_uri(&self) -> JsFunction {
self.encode_uri.clone()
}
pub(crate) fn encode_uri_component(&self) -> JsFunction {
self.encode_uri_component.clone()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct DecodeUri;
impl IntrinsicObject for DecodeUri {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, decode_uri)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().uri_functions().decode_uri().into()
}
}
impl BuiltInObject for DecodeUri {
const NAME: &'static str = "decodeURI";
}
pub(crate) struct DecodeUriComponent;
impl IntrinsicObject for DecodeUriComponent {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, decode_uri_component)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics
.objects()
.uri_functions()
.decode_uri_component()
.into()
}
}
impl BuiltInObject for DecodeUriComponent {
const NAME: &'static str = "decodeURIComponent";
}
pub(crate) struct EncodeUri;
impl IntrinsicObject for EncodeUri {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, encode_uri)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().uri_functions().encode_uri().into()
}
}
impl BuiltInObject for EncodeUri {
const NAME: &'static str = "encodeURI";
}
pub(crate) struct EncodeUriComponent;
impl IntrinsicObject for EncodeUriComponent {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, encode_uri_component)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics
.objects()
.uri_functions()
.encode_uri_component()
.into()
}
}
impl BuiltInObject for EncodeUriComponent {
const NAME: &'static str = "encodeURIComponent";
}
pub(crate) fn decode_uri(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let encoded_uri = args.get_or_undefined(0);
let uri_string = encoded_uri.to_string(context)?;
let reserved_uri_set = is_uri_reserved_or_number_sign;
Ok(JsValue::from(decode(&uri_string, reserved_uri_set)?))
}
pub(crate) fn decode_uri_component(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let encoded_uri_component = args.get_or_undefined(0);
let component_string = encoded_uri_component.to_string(context)?;
let reserved_uri_component_set = |_: u16| false;
Ok(JsValue::from(decode(
&component_string,
reserved_uri_component_set,
)?))
}
pub(crate) fn encode_uri(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let uri = args.get_or_undefined(0);
let uri_string = uri.to_string(context)?;
let unescaped_uri_set = is_uri_reserved_or_uri_unescaped_or_number_sign;
Ok(JsValue::from(encode(&uri_string, unescaped_uri_set)?))
}
pub(crate) fn encode_uri_component(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let uri_component = args.get_or_undefined(0);
let component_string = uri_component.to_string(context)?;
let unescaped_uri_component_set = is_uri_unescaped;
Ok(JsValue::from(encode(
&component_string,
unescaped_uri_component_set,
)?))
}
fn encode<F>(string: &JsString, unescaped_set: F) -> JsResult<JsString>
where
F: Fn(u16) -> bool,
{
let str_len = string.len();
let mut r = Vec::new();
let mut k = 0;
loop {
if k == str_len {
return Ok(js_string!(r));
}
let c = string[k];
if unescaped_set(c) {
k += 1;
r.push(c);
} else {
let cp = string.code_point_at(k);
let CodePoint::Unicode(ch) = cp else {
return Err(JsNativeError::uri()
.with_message("trying to encode an invalid string")
.into());
};
k += cp.code_unit_count();
let mut buff = [0_u8; 4];
let octets = ch.encode_utf8(&mut buff);
for octet in octets.bytes() {
r.extend(format!("%{octet:0>2X}").encode_utf16());
}
}
}
}
#[allow(clippy::many_single_char_names)]
fn decode<F>(string: &JsString, reserved_set: F) -> JsResult<JsString>
where
F: Fn(u16) -> bool,
{
let str_len = string.len();
let mut r = Vec::new();
let mut octets = Vec::with_capacity(4);
let mut k = 0;
loop {
if k == str_len {
return Ok(js_string!(r));
}
let c = string[k];
#[allow(clippy::if_not_else)]
let s = if c != 0x0025_u16 {
Vec::from([c])
} else {
let start = k;
if k + 2 >= str_len {
return Err(JsNativeError::uri()
.with_message("invalid escape character found")
.into());
}
let b = decode_hex_byte(string[k + 1], string[k + 2]).ok_or_else(|| {
JsNativeError::uri().with_message("invalid hexadecimal digit found")
})?;
k += 2;
let n = b.leading_ones() as usize;
if n == 0 {
let c = u16::from(b);
if !reserved_set(c) {
Vec::from([c])
} else {
Vec::from(&string[start..=k])
}
} else {
if n == 1 || n > 4 {
return Err(JsNativeError::uri()
.with_message("invalid escaped character found")
.into());
}
if k + (3 * (n - 1)) > str_len {
return Err(JsNativeError::uri()
.with_message("non-terminated escape character found")
.into());
}
octets.push(b);
for _j in 1..n {
k += 1;
if string[k] != 0x0025 {
return Err(JsNativeError::uri()
.with_message("escape characters must be preceded with a % sign")
.into());
}
let b = decode_hex_byte(string[k + 1], string[k + 2]).ok_or_else(|| {
JsNativeError::uri().with_message("invalid hexadecimal digit found")
})?;
k += 2;
octets.push(b);
}
assert_eq!(octets.len(), n);
match std::str::from_utf8(&octets) {
Err(_) => {
return Err(JsNativeError::uri()
.with_message("invalid UTF-8 encoding found")
.into())
}
Ok(v) => {
let s = v.encode_utf16().collect::<Vec<_>>();
octets.clear();
s
}
}
}
};
r.extend_from_slice(&s);
k += 1;
}
}
fn decode_hex_byte(high: u16, low: u16) -> Option<u8> {
match (
char::from_u32(u32::from(high)),
char::from_u32(u32::from(low)),
) {
(Some(high), Some(low)) => match (high.to_digit(16), low.to_digit(16)) {
(Some(high), Some(low)) => Some(((high as u8) << 4) + low as u8),
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_byte() {
assert_eq!(
decode_hex_byte(u16::from(b'2'), u16::from(b'0')).unwrap(),
0x20
);
assert_eq!(
decode_hex_byte(u16::from(b'2'), u16::from(b'A')).unwrap(),
0x2A
);
assert_eq!(
decode_hex_byte(u16::from(b'3'), u16::from(b'C')).unwrap(),
0x3C
);
assert_eq!(
decode_hex_byte(u16::from(b'4'), u16::from(b'0')).unwrap(),
0x40
);
assert_eq!(
decode_hex_byte(u16::from(b'7'), u16::from(b'E')).unwrap(),
0x7E
);
assert_eq!(
decode_hex_byte(u16::from(b'0'), u16::from(b'0')).unwrap(),
0x00
);
assert!(decode_hex_byte(u16::from(b'-'), u16::from(b'0')).is_none());
assert!(decode_hex_byte(u16::from(b'f'), u16::from(b'~')).is_none());
assert!(decode_hex_byte(u16::from(b'A'), 0_u16).is_none());
assert!(decode_hex_byte(u16::from(b'%'), u16::from(b'&')).is_none());
assert!(decode_hex_byte(0xFACD_u16, u16::from(b'-')).is_none());
assert!(decode_hex_byte(u16::from(b'-'), 0xA0FD_u16).is_none());
}
}