feat: 实现 URI 全局编码解码函数

This commit is contained in:
2026-06-14 17:41:07 +08:00
parent 111c61d4d7
commit 4ce8fa75dc
6 changed files with 337 additions and 122 deletions
@@ -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),
],
);
}
+5
View File
@@ -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]))
+10 -121
View File
@@ -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;
}
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));
}
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()
}
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");
}
}
+151
View File
@@ -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());
}