mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-25 19:21:11 +08:00
159 lines
5.2 KiB
Rust
159 lines
5.2 KiB
Rust
use oxide_runtime_api::{NativeResult, VmHost};
|
||
|
||
const URI_UNESCAPED: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
|
||
const URI_RESERVED: &str = ";/?:@&=+$,#";
|
||
const URI_SAFE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'();/?:@&=+$,#";
|
||
const URI_ERROR_MESSAGE: &str = "malformed URI sequence";
|
||
|
||
fn encode_uri_string(input: &str, safe: &str) -> String {
|
||
let mut out = String::with_capacity(input.len());
|
||
for ch in input.chars() {
|
||
if ch.is_ascii() && safe.contains(ch) {
|
||
out.push(ch);
|
||
continue;
|
||
}
|
||
|
||
let mut buf = [0u8; 4];
|
||
for byte in ch.encode_utf8(&mut buf).as_bytes() {
|
||
out.push_str(&format!("%{byte:02X}"));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn read_percent_byte(bytes: &[u8], pos: usize) -> Result<u8, ()> {
|
||
if pos + 3 > bytes.len() || bytes[pos] != b'%' {
|
||
return Err(());
|
||
}
|
||
super::parse_hex_u8(&bytes[pos + 1..pos + 3]).ok_or(())
|
||
}
|
||
|
||
fn utf8_sequence_len(first: u8) -> Result<usize, ()> {
|
||
match first {
|
||
0x00..=0x7F => Ok(1),
|
||
0xC2..=0xDF => Ok(2),
|
||
0xE0..=0xEF => Ok(3),
|
||
0xF0..=0xF4 => Ok(4),
|
||
_ => Err(()),
|
||
}
|
||
}
|
||
|
||
fn decode_uri_string(input: &str, preserve_reserved: bool) -> Result<String, ()> {
|
||
let bytes = input.as_bytes();
|
||
let mut out = String::with_capacity(input.len());
|
||
let mut i = 0;
|
||
|
||
while i < bytes.len() {
|
||
if bytes[i] != b'%' {
|
||
let ch = input[i..].chars().next().expect("valid utf-8 char boundary");
|
||
out.push(ch);
|
||
i += ch.len_utf8();
|
||
continue;
|
||
}
|
||
|
||
let first = read_percent_byte(bytes, i)?;
|
||
let sequence_len = utf8_sequence_len(first)?;
|
||
let raw_start = i;
|
||
let mut encoded = Vec::with_capacity(sequence_len);
|
||
encoded.push(first);
|
||
i += 3;
|
||
|
||
for _ in 1..sequence_len {
|
||
let byte = read_percent_byte(bytes, i)?;
|
||
if byte & 0b1100_0000 != 0b1000_0000 {
|
||
return Err(());
|
||
}
|
||
encoded.push(byte);
|
||
i += 3;
|
||
}
|
||
|
||
let decoded = std::str::from_utf8(&encoded).map_err(|_| ())?;
|
||
if preserve_reserved && decoded.len() == 1 {
|
||
let ch = decoded.chars().next().expect("single decoded char");
|
||
if URI_RESERVED.contains(ch) {
|
||
out.push_str(&input[raw_start..i]);
|
||
continue;
|
||
}
|
||
}
|
||
out.push_str(decoded);
|
||
}
|
||
|
||
Ok(out)
|
||
}
|
||
|
||
fn uri_error<H: VmHost>(vm: &mut H) -> NativeResult {
|
||
NativeResult::Err(crate::error::create_uri_error(vm, URI_ERROR_MESSAGE))
|
||
}
|
||
|
||
/// `encodeURI`:编码输入为 URI,保留未转义字符与保留字符 `;/?:@&=+$,#`,
|
||
/// 其余按 UTF-8 字节转成 `%XX`。
|
||
pub fn encode_uri<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||
let input = super::string_arg(vm, args);
|
||
NativeResult::Ok(vm.new_string(&encode_uri_string(&input, URI_SAFE)))
|
||
}
|
||
|
||
/// `encodeURIComponent`:编码输入为 URI component,仅保留未转义字符,
|
||
/// 保留字符(如 `=&`)也会被转成 `%XX`。
|
||
pub fn encode_uri_component<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||
let input = super::string_arg(vm, args);
|
||
NativeResult::Ok(vm.new_string(&encode_uri_string(&input, URI_UNESCAPED)))
|
||
}
|
||
|
||
/// `decodeURI`:解码 `%XX` 序列,但保留字符的转义形式原样保留(不还原),
|
||
/// 保证 decode(encode(uri)) 不变。非法序列抛 URIError。
|
||
pub fn decode_uri<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||
let input = super::string_arg(vm, args);
|
||
match decode_uri_string(&input, true) {
|
||
Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)),
|
||
Err(()) => uri_error(vm),
|
||
}
|
||
}
|
||
|
||
/// `decodeURIComponent`:解码所有 `%XX` 序列(含保留字符),
|
||
/// 非法或截断的 UTF-8 序列抛 URIError。
|
||
pub fn decode_uri_component<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||
let input = super::string_arg(vm, args);
|
||
match decode_uri_string(&input, false) {
|
||
Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)),
|
||
Err(()) => uri_error(vm),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::{decode_uri_string, encode_uri_string, URI_UNESCAPED};
|
||
|
||
#[test]
|
||
fn encode_uri_keeps_reserved_and_encodes_space() {
|
||
assert_eq!(
|
||
encode_uri_string("https://example.com/path?q=hello world", super::URI_SAFE),
|
||
"https://example.com/path?q=hello%20world"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn encode_uri_component_encodes_reserved() {
|
||
assert_eq!(encode_uri_string("a=1&b=2", URI_UNESCAPED), "a%3D1%26b%3D2");
|
||
}
|
||
|
||
#[test]
|
||
fn decode_uri_preserves_reserved_escapes() {
|
||
assert_eq!(
|
||
decode_uri_string("https://example.com/path%3Fq=hello%20world", true).unwrap(),
|
||
"https://example.com/path%3Fq=hello world"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn decode_uri_component_decodes_reserved() {
|
||
assert_eq!(decode_uri_string("a%3D1%26b%3D2", false).unwrap(), "a=1&b=2");
|
||
}
|
||
|
||
#[test]
|
||
fn decode_uri_rejects_malformed_sequences() {
|
||
assert!(decode_uri_string("%", false).is_err());
|
||
assert!(decode_uri_string("%E0%A4", false).is_err());
|
||
assert!(decode_uri_string("%ED%A0%80", false).is_err());
|
||
}
|
||
}
|