mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 实现 URI 全局编码解码函数
This commit is contained in:
@@ -13,6 +13,10 @@ pub fn bind_global(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
|
||||
&[
|
||||
("escape", crate::builtins::global::js_escape as *const (), 1),
|
||||
("unescape", crate::builtins::global::js_unescape as *const (), 1),
|
||||
("encodeURI", crate::builtins::global::encode_uri as *const (), 1),
|
||||
("decodeURI", crate::builtins::global::decode_uri as *const (), 1),
|
||||
("encodeURIComponent", crate::builtins::global::encode_uri_component as *const (), 1),
|
||||
("decodeURIComponent", crate::builtins::global::decode_uri_component as *const (), 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ pub fn create_syntax_error(vm: &mut Vm, msg: &str) -> JsValue {
|
||||
create_error_object(vm, proto_ptr, "SyntaxError", msg)
|
||||
}
|
||||
|
||||
pub fn create_uri_error(vm: &mut Vm, msg: &str) -> JsValue {
|
||||
let proto_ptr = P::as_ptr(&vm.session().builtin_world().uri_error_proto) as *mut JsObject;
|
||||
create_error_object(vm, proto_ptr, "URIError", msg)
|
||||
}
|
||||
|
||||
fn get_msg(vm: &mut Vm, args: &[u8]) -> String {
|
||||
if args.len() > 1 {
|
||||
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1]))
|
||||
|
||||
@@ -1,131 +1,20 @@
|
||||
mod annex_b;
|
||||
mod uri;
|
||||
|
||||
use crate::coercion;
|
||||
use crate::native::NativeResult;
|
||||
use crate::vm::Vm;
|
||||
|
||||
const ESCAPE_SAFE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
|
||||
pub use annex_b::{js_escape, js_unescape};
|
||||
pub use uri::{decode_uri, decode_uri_component, encode_uri, encode_uri_component};
|
||||
|
||||
fn escape_string(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
for ch in input.chars() {
|
||||
if ESCAPE_SAFE.contains(ch) {
|
||||
out.push(ch);
|
||||
continue;
|
||||
fn string_arg(vm: &mut Vm, args: &[u8]) -> String {
|
||||
if args.len() > 1 {
|
||||
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1]))
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
}
|
||||
|
||||
let code = ch as u32;
|
||||
if code <= 0xFF {
|
||||
out.push('%');
|
||||
out.push_str(&format!("{code:02X}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut buf = [0u16; 2];
|
||||
for unit in ch.encode_utf16(&mut buf).iter() {
|
||||
out.push_str(&format!("%u{:04X}", *unit));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_hex_u8(slice: &[u8]) -> Option<u8> {
|
||||
std::str::from_utf8(slice).ok().and_then(|s| u8::from_str_radix(s, 16).ok())
|
||||
}
|
||||
|
||||
fn parse_hex_u16(slice: &[u8]) -> Option<u16> {
|
||||
std::str::from_utf8(slice).ok().and_then(|s| u16::from_str_radix(s, 16).ok())
|
||||
}
|
||||
|
||||
fn unescape_string(input: &str) -> 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'%' {
|
||||
if i + 6 <= bytes.len() && bytes[i + 1] == b'u' {
|
||||
if let Some(unit) = parse_hex_u16(&bytes[i + 2..i + 6]) {
|
||||
if (0xD800..=0xDBFF).contains(&unit)
|
||||
&& i + 12 <= bytes.len()
|
||||
&& bytes[i + 6] == b'%'
|
||||
&& bytes[i + 7] == b'u'
|
||||
{
|
||||
if let Some(low) = parse_hex_u16(&bytes[i + 8..i + 12]) {
|
||||
if (0xDC00..=0xDFFF).contains(&low) {
|
||||
let pair = [unit, low];
|
||||
let decoded = char::decode_utf16(pair).next();
|
||||
match decoded {
|
||||
Some(Ok(ch)) => out.push(ch),
|
||||
_ => {
|
||||
out.push_str(&input[i..i + 12]);
|
||||
}
|
||||
}
|
||||
i += 12;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ch) = char::from_u32(unit as u32) {
|
||||
out.push(ch);
|
||||
} else {
|
||||
out.push_str(&input[i..i + 6]);
|
||||
}
|
||||
i += 6;
|
||||
continue;
|
||||
}
|
||||
} else if i + 3 <= bytes.len() {
|
||||
if let Some(value) = parse_hex_u8(&bytes[i + 1..i + 3]) {
|
||||
out.push(value as char);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ch = input[i..].chars().next().expect("valid utf-8 char boundary");
|
||||
out.push(ch);
|
||||
i += ch.len_utf8();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn js_escape(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = if args.len() > 1 {
|
||||
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1]))
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
NativeResult::Ok(vm.intern(&escape_string(&input)))
|
||||
}
|
||||
|
||||
pub fn js_unescape(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = if args.len() > 1 {
|
||||
coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1]))
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
NativeResult::Ok(vm.intern(&unescape_string(&input)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{escape_string, unescape_string};
|
||||
|
||||
#[test]
|
||||
fn escape_encodes_spaces_and_unicode() {
|
||||
assert_eq!(escape_string("hello world"), "hello%20world");
|
||||
assert_eq!(escape_string("AΩ你"), "A%u03A9%u4F60");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unescape_decodes_percent_sequences() {
|
||||
assert_eq!(unescape_string("hello%20world"), "hello world");
|
||||
assert_eq!(unescape_string("%u03A9%u4F60"), "Ω你");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unescape_keeps_invalid_sequences() {
|
||||
assert_eq!(unescape_string("%uXYZ1%2G"), "%uXYZ1%2G");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::native::NativeResult;
|
||||
use crate::vm::Vm;
|
||||
|
||||
const ESCAPE_SAFE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
|
||||
|
||||
fn parse_hex_u16(slice: &[u8]) -> Option<u16> {
|
||||
std::str::from_utf8(slice).ok().and_then(|s| u16::from_str_radix(s, 16).ok())
|
||||
}
|
||||
|
||||
fn escape_string(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
for ch in input.chars() {
|
||||
if ESCAPE_SAFE.contains(ch) {
|
||||
out.push(ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
let code = ch as u32;
|
||||
if code <= 0xFF {
|
||||
out.push('%');
|
||||
out.push_str(&format!("{code:02X}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut buf = [0u16; 2];
|
||||
for unit in ch.encode_utf16(&mut buf).iter() {
|
||||
out.push_str(&format!("%u{:04X}", *unit));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn unescape_string(input: &str) -> 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'%' {
|
||||
if i + 6 <= bytes.len() && bytes[i + 1] == b'u' {
|
||||
if let Some(unit) = parse_hex_u16(&bytes[i + 2..i + 6]) {
|
||||
if (0xD800..=0xDBFF).contains(&unit)
|
||||
&& i + 12 <= bytes.len()
|
||||
&& bytes[i + 6] == b'%'
|
||||
&& bytes[i + 7] == b'u'
|
||||
{
|
||||
if let Some(low) = parse_hex_u16(&bytes[i + 8..i + 12]) {
|
||||
if (0xDC00..=0xDFFF).contains(&low) {
|
||||
let pair = [unit, low];
|
||||
let decoded = char::decode_utf16(pair).next();
|
||||
match decoded {
|
||||
Some(Ok(ch)) => out.push(ch),
|
||||
_ => {
|
||||
out.push_str(&input[i..i + 12]);
|
||||
}
|
||||
}
|
||||
i += 12;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ch) = char::from_u32(unit as u32) {
|
||||
out.push(ch);
|
||||
} else {
|
||||
out.push_str(&input[i..i + 6]);
|
||||
}
|
||||
i += 6;
|
||||
continue;
|
||||
}
|
||||
} else if i + 3 <= bytes.len() {
|
||||
if let Some(value) = super::parse_hex_u8(&bytes[i + 1..i + 3]) {
|
||||
out.push(value as char);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ch = input[i..].chars().next().expect("valid utf-8 char boundary");
|
||||
out.push(ch);
|
||||
i += ch.len_utf8();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn js_escape(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
NativeResult::Ok(vm.intern(&escape_string(&input)))
|
||||
}
|
||||
|
||||
pub fn js_unescape(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
NativeResult::Ok(vm.intern(&unescape_string(&input)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{escape_string, unescape_string};
|
||||
|
||||
#[test]
|
||||
fn escape_encodes_spaces_and_unicode() {
|
||||
assert_eq!(escape_string("hello world"), "hello%20world");
|
||||
assert_eq!(escape_string("AΩ你"), "A%u03A9%u4F60");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unescape_decodes_percent_sequences() {
|
||||
assert_eq!(unescape_string("hello%20world"), "hello world");
|
||||
assert_eq!(unescape_string("%u03A9%u4F60"), "Ω你");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unescape_keeps_invalid_sequences() {
|
||||
assert_eq!(unescape_string("%uXYZ1%2G"), "%uXYZ1%2G");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
use crate::native::NativeResult;
|
||||
use crate::vm::Vm;
|
||||
|
||||
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(vm: &mut Vm) -> NativeResult {
|
||||
NativeResult::Err(crate::builtins::error::create_uri_error(vm, URI_ERROR_MESSAGE))
|
||||
}
|
||||
|
||||
pub fn encode_uri(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
NativeResult::Ok(vm.intern(&encode_uri_string(&input, URI_SAFE)))
|
||||
}
|
||||
|
||||
pub fn encode_uri_component(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
NativeResult::Ok(vm.intern(&encode_uri_string(&input, URI_UNESCAPED)))
|
||||
}
|
||||
|
||||
pub fn decode_uri(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
match decode_uri_string(&input, true) {
|
||||
Ok(decoded) => NativeResult::Ok(vm.intern(&decoded)),
|
||||
Err(()) => uri_error(vm),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_uri_component(vm: &mut Vm, args: &[u8]) -> NativeResult {
|
||||
let input = super::string_arg(vm, args);
|
||||
match decode_uri_string(&input, false) {
|
||||
Ok(decoded) => NativeResult::Ok(vm.intern(&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());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<oxide_types::value::JsValue, String> {
|
||||
fn eval(source: &str) -> Result<JsValue, String> {
|
||||
let allocator = Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
|
||||
@@ -10,6 +11,22 @@ fn eval(source: &str) -> Result<oxide_types::value::JsValue, String> {
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
fn eval_with_vm(source: &str) -> (Vm, Result<JsValue, String>) {
|
||||
let allocator = Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).expect("parse");
|
||||
let module = Compiler::new().compile(&program).expect("compile");
|
||||
let mut vm = Vm::new();
|
||||
let result = vm.run(&module);
|
||||
(vm, result)
|
||||
}
|
||||
|
||||
fn str_val(vm: &Vm, val: JsValue) -> String {
|
||||
vm.kernel_core()
|
||||
.string_forge()
|
||||
.lookup(val.as_string_index())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nan_returns_nan() {
|
||||
let result = eval("NaN").unwrap();
|
||||
@@ -83,3 +100,34 @@ fn global_this_points_to_global_object() {
|
||||
assert!(result.is_bool());
|
||||
assert!(result.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_uri_preserves_reserved_chars() {
|
||||
let (vm, result) = eval_with_vm("encodeURI('https://example.com/path?q=hello world')");
|
||||
assert_eq!(str_val(&vm, result.unwrap()), "https://example.com/path?q=hello%20world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_uri_component_encodes_reserved_chars() {
|
||||
let (vm, result) = eval_with_vm("encodeURIComponent('a=1&b=2')");
|
||||
assert_eq!(str_val(&vm, result.unwrap()), "a%3D1%26b%3D2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_uri_preserves_escaped_reserved_chars() {
|
||||
let (vm, result) = eval_with_vm("decodeURI('https://example.com/path%3Fq=hello%20world')");
|
||||
assert_eq!(str_val(&vm, result.unwrap()), "https://example.com/path%3Fq=hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_uri_component_decodes_reserved_chars() {
|
||||
let (vm, result) = eval_with_vm("decodeURIComponent('a%3D1%26b%3D2')");
|
||||
assert_eq!(str_val(&vm, result.unwrap()), "a=1&b=2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_uri_component_malformed_sequence_throws_uri_error() {
|
||||
let result = eval("try { decodeURIComponent('%') } catch (e) { e instanceof URIError }").unwrap();
|
||||
assert!(result.is_bool());
|
||||
assert!(result.as_bool());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user