mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: String 方法 Symbol 参数抛 TypeError 且缺参按 undefined 查找
as_string 由兜底转换改 Result 化:Symbol 与其余转换失败抛 TypeError,对象 ToString 抛出的原生异常经 take_uncaught_value 原样传播,不再静默降级为空串/占位文本。17 处调用点统一经 try_string! 抛错。 indexOf/includes/startsWith/endsWith/lastIndexOf 删除缺参短路: searchString 缺省按 ToString(undefined)="undefined" 参与查找, 'undefined'.indexOf() 现得 0。 类正则对象(Object.create(RegExp.prototype))默认 toString 抛 TypeError 的 replace/split 用例随异常传播行为更新断言。
This commit is contained in:
@@ -222,12 +222,27 @@ pub(crate) fn make_string_array_values<H: VmHost>(vm: &mut H, parts: Vec<JsValue
|
||||
}
|
||||
|
||||
/// 取参数字符串内容:原始字符串零拷贝借用,其余经完整 ToString(对象 ToPrimitive)。
|
||||
/// Symbol 与其余转换失败按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 失败返回异常值,调用方经 `try_string!` 原样抛出。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// 返回借用绑定本次 `&mut` 借用;需与 `this_string` 借用并存时先 `into_owned` 落地。
|
||||
fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Cow<'a, str> {
|
||||
fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result<Cow<'a, str>, JsValue> {
|
||||
if val.is_string() {
|
||||
Cow::Borrowed(vm.string_ref(val))
|
||||
} else {
|
||||
Cow::Owned(oxide_runtime_api::to_string_full(val, vm).unwrap_or_else(|_| oxide_runtime_api::to_string(val)))
|
||||
return Ok(Cow::Borrowed(vm.string_ref(val)));
|
||||
}
|
||||
match oxide_runtime_api::to_string_full(val, vm) {
|
||||
Ok(s) => Ok(Cow::Owned(s)),
|
||||
Err(_) => {
|
||||
// ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播,
|
||||
// 否则会被展平为普通 Error 丢失原始异常对象。
|
||||
if let Some(exc) = vm.take_uncaught_value() {
|
||||
return Err(exc);
|
||||
}
|
||||
Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,12 +523,10 @@ fn is_regexp_obj<H: VmHost>(val: JsValue, vm: &H) -> bool {
|
||||
/// `String.prototype.indexOf(searchString, position)`:按 UTF-16 单元查找首次出现位置。
|
||||
pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.indexOf called with {} args", args.len());
|
||||
// 参数转换先行:search 与 position 均可能触发对象 ToString/ToNumber(&mut 路径)。
|
||||
let search = if args.len() >= 2 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// 参数转换先行:search 缺省为 undefined(经 ToString 得 "undefined" 参与查找),
|
||||
// position 缺省 0;均可能触发对象 ToString/ToNumber(&mut 路径)。
|
||||
let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let search = try_string!(as_string(vm, search_val)).into_owned();
|
||||
let pos_raw = if args.len() > 2 {
|
||||
vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize
|
||||
} else {
|
||||
@@ -521,9 +534,6 @@ pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
};
|
||||
// 借用 this 原始字符串零拷贝,扫描期内不再有 &mut 调用。
|
||||
let s = try_string!(this_string(vm, args));
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::int(-1));
|
||||
}
|
||||
let n = utf16_len(&s);
|
||||
let pos = pos_raw.min(n);
|
||||
|
||||
@@ -548,21 +558,15 @@ pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
/// `String.prototype.includes(searchString, position)`:是否包含子串。
|
||||
pub fn string_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.includes called with {} args", args.len());
|
||||
// 参数转换先行(&mut 路径),后借 this 扫描。
|
||||
let search = if args.len() >= 2 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。
|
||||
let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let search = try_string!(as_string(vm, search_val)).into_owned();
|
||||
let pos_raw = if args.len() > 2 {
|
||||
vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let s = try_string!(this_string(vm, args));
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::bool(false));
|
||||
}
|
||||
let n = utf16_len(&s);
|
||||
let pos = pos_raw.min(n);
|
||||
|
||||
@@ -790,12 +794,9 @@ pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
/// `String.prototype.lastIndexOf(searchString, position)`:从后往前查找首次出现位置。
|
||||
pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len());
|
||||
// 参数转换先行(&mut 路径),后借 this 扫描。
|
||||
let search = if args.len() >= 2 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。
|
||||
let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let search = try_string!(as_string(vm, search_val)).into_owned();
|
||||
let pos_raw = if args.len() > 2 {
|
||||
let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
|
||||
if p.is_nan() {
|
||||
@@ -807,9 +808,6 @@ pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
||||
None
|
||||
};
|
||||
let s = try_string!(this_string(vm, args));
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::int(-1));
|
||||
}
|
||||
let n = utf16_len(&s);
|
||||
let pos = match pos_raw {
|
||||
Some(p) => p.min(n),
|
||||
@@ -881,7 +879,7 @@ pub fn string_pad_start<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
None
|
||||
};
|
||||
let pad = if args.len() > 2 {
|
||||
as_string(vm, vm.reg(args[2])).into_owned()
|
||||
try_string!(as_string(vm, vm.reg(args[2]))).into_owned()
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
@@ -914,7 +912,7 @@ pub fn string_pad_end<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
None
|
||||
};
|
||||
let pad = if args.len() > 2 {
|
||||
as_string(vm, vm.reg(args[2])).into_owned()
|
||||
try_string!(as_string(vm, vm.reg(args[2]))).into_owned()
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
@@ -940,21 +938,15 @@ pub fn string_pad_end<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
/// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头。
|
||||
pub fn string_starts_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.startsWith called with {} args", args.len());
|
||||
// 参数转换先行(&mut 路径),后借 this 扫描。
|
||||
let search = if args.len() >= 2 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。
|
||||
let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let search = try_string!(as_string(vm, search_val)).into_owned();
|
||||
let pos_raw = if args.len() > 2 {
|
||||
vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let s = try_string!(this_string(vm, args));
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::bool(false));
|
||||
}
|
||||
let n = utf16_len(&s);
|
||||
let pos = pos_raw.min(n);
|
||||
// 位置须对齐字符边界才有 well-formed 前缀可比;落在代理对中间时仅空串可匹配。
|
||||
@@ -968,21 +960,15 @@ pub fn string_starts_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
/// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。
|
||||
pub fn string_ends_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.endsWith called with {} args", args.len());
|
||||
// 参数转换先行(&mut 路径),后借 this 扫描。
|
||||
let search = if args.len() >= 2 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。
|
||||
let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let search = try_string!(as_string(vm, search_val)).into_owned();
|
||||
let end_pos_raw = if args.len() > 2 {
|
||||
vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
let s = try_string!(this_string(vm, args));
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::bool(false));
|
||||
}
|
||||
let n = utf16_len(&s);
|
||||
let end_pos = end_pos_raw.min(n);
|
||||
// 截断位置须对齐字符边界才有 well-formed 后缀可比;落在代理对中间时仅空串可匹配。
|
||||
@@ -1017,7 +1003,7 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let sep = if is_undefined_sep || has_native_re {
|
||||
String::new()
|
||||
} else {
|
||||
as_string(vm, sep_val.unwrap()).into_owned()
|
||||
try_string!(as_string(vm, sep_val.unwrap())).into_owned()
|
||||
};
|
||||
// ToUint32(limit),缺省为 2^32-1。
|
||||
let limit = if args.len() > 2 {
|
||||
@@ -1178,7 +1164,7 @@ fn string_replace_impl<H: VmHost>(vm: &mut H, args: &[u8], all: bool) -> NativeR
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
|
||||
return regex_replace_fn(vm, regex, &s, replacer_val, is_global, text_arg);
|
||||
}
|
||||
let pattern = as_string(vm, pattern_val).into_owned();
|
||||
let pattern = try_string!(as_string(vm, pattern_val)).into_owned();
|
||||
return string_replace_fn(vm, &s, &pattern, replacer_val, all, text_arg);
|
||||
}
|
||||
|
||||
@@ -1228,10 +1214,10 @@ fn string_replace_impl<H: VmHost>(vm: &mut H, args: &[u8], all: bool) -> NativeR
|
||||
} else if args.len() < 2 {
|
||||
"undefined".to_string()
|
||||
} else {
|
||||
as_string(vm, pattern_val).into_owned()
|
||||
try_string!(as_string(vm, pattern_val)).into_owned()
|
||||
};
|
||||
let replacement = if args.len() > 2 {
|
||||
as_string(vm, replacement_val).into_owned()
|
||||
try_string!(as_string(vm, replacement_val)).into_owned()
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
@@ -1244,10 +1230,10 @@ fn string_replace_impl<H: VmHost>(vm: &mut H, args: &[u8], all: bool) -> NativeR
|
||||
} else if args.len() < 2 {
|
||||
"undefined".to_string()
|
||||
} else {
|
||||
as_string(vm, pattern_val).into_owned()
|
||||
try_string!(as_string(vm, pattern_val)).into_owned()
|
||||
};
|
||||
let replacement = if args.len() > 2 {
|
||||
as_string(vm, replacement_val).into_owned()
|
||||
try_string!(as_string(vm, replacement_val)).into_owned()
|
||||
} else {
|
||||
"undefined".to_string()
|
||||
};
|
||||
@@ -1287,7 +1273,7 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let pattern_val = if args.len() >= 2 { Some(vm.reg(args[1])) } else { None };
|
||||
let is_re = pattern_val.map(|v| is_regexp_obj(v, vm)).unwrap_or(false);
|
||||
let pattern = match pattern_val {
|
||||
Some(v) if !is_re => as_string(vm, v).into_owned(),
|
||||
Some(v) if !is_re => try_string!(as_string(vm, v)).into_owned(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let s = try_string!(this_string(vm, args));
|
||||
@@ -1339,7 +1325,7 @@ pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
// 正则判定与参数转换先行(&mut 路径),后借 this 扫描。
|
||||
let is_re = args.len() >= 2 && is_regexp_obj(pattern_val, vm);
|
||||
let pattern = if args.len() >= 2 && !is_re {
|
||||
as_string(vm, pattern_val).into_owned()
|
||||
try_string!(as_string(vm, pattern_val)).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -1471,7 +1457,7 @@ pub fn string_normalize<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.normalize called with {} args", args.len());
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
let form = if args.len() > 1 {
|
||||
as_string(vm, vm.reg(args[1])).into_owned()
|
||||
try_string!(as_string(vm, vm.reg(args[1]))).into_owned()
|
||||
} else {
|
||||
"NFC".to_string()
|
||||
};
|
||||
@@ -1512,7 +1498,7 @@ pub fn string_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
pattern_val
|
||||
} else {
|
||||
let pattern_str = as_string(vm, pattern_val);
|
||||
let pattern_str = try_string!(as_string(vm, pattern_val));
|
||||
let escaped = regress::escape(pattern_str.as_ref());
|
||||
let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) };
|
||||
let compiled = match regress::Regex::new(&rx_str) {
|
||||
|
||||
@@ -222,8 +222,10 @@ fn string_search_empty_pattern_returns_zero() {
|
||||
|
||||
#[test]
|
||||
fn string_no_arg_guard_family_consistent() {
|
||||
// 家族无参守卫对照:search/indexOf 返回 -1,includes/startsWith/endsWith 返回 false,
|
||||
// charAt 取首字符——search 与同批方法守卫位置一致、行为家族化。
|
||||
// 缺参时 searchString 按 ToString(undefined)="undefined" 参与查找——'abc'
|
||||
// 不含 "undefined",indexOf 得 -1、includes/startsWith/endsWith 得 false
|
||||
// ("undefined" 命中的判别断言见 string_missing_arg_searches_undefined);
|
||||
// search 缺参仍走旧守卫返回 -1。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'abc'.search()").unwrap();
|
||||
assert_eq!(result.as_int(), -1);
|
||||
@@ -237,6 +239,53 @@ fn string_no_arg_guard_family_consistent() {
|
||||
assert!(!result.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_missing_arg_searches_undefined() {
|
||||
// 缺参 searchString 按 ToString(undefined)="undefined" 参与查找(非固定结果短路)。
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(&mut vm, "'undefined'.indexOf()").unwrap();
|
||||
assert_eq!(r.as_int(), 0);
|
||||
let r = eval(&mut vm, "'abc'.indexOf()").unwrap();
|
||||
assert_eq!(r.as_int(), -1);
|
||||
let r = eval(&mut vm, "'undefined'.lastIndexOf()").unwrap();
|
||||
assert_eq!(r.as_int(), 0);
|
||||
let r = eval(&mut vm, "'undefined'.includes()").unwrap();
|
||||
assert!(r.as_bool());
|
||||
let r = eval(&mut vm, "''.includes()").unwrap();
|
||||
assert!(!r.as_bool());
|
||||
let r = eval(&mut vm, "'undefined'.startsWith()").unwrap();
|
||||
assert!(r.as_bool());
|
||||
let r = eval(&mut vm, "'undefined'.endsWith()").unwrap();
|
||||
assert!(r.as_bool());
|
||||
// 显式传 undefined 与缺参等价(同走 "undefined" 查找)。
|
||||
let r = eval(&mut vm, "'undefined'.indexOf(undefined)").unwrap();
|
||||
assert_eq!(r.as_int(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_symbol_argument_throws_type_error() {
|
||||
// Symbol 参数经 as_string 抛 TypeError(不静默降级为文本查找)。
|
||||
let mut vm = Vm::new();
|
||||
for src in [
|
||||
"'abc'.indexOf(Symbol('x'))",
|
||||
"'abc'.lastIndexOf(Symbol('x'))",
|
||||
"'abc'.includes(Symbol('x'))",
|
||||
"'abc'.startsWith(Symbol('x'))",
|
||||
"'abc'.endsWith(Symbol('x'))",
|
||||
"'abc'.replace('a', Symbol('x'))",
|
||||
"'abc'.replaceAll('a', Symbol('x'))",
|
||||
"'abc'.split(Symbol('x'))",
|
||||
"'abc'.search(Symbol('x'))",
|
||||
"'abc'.match(Symbol('x'))",
|
||||
"'abc'.padStart(5, Symbol('x'))",
|
||||
"'abc'.padEnd(5, Symbol('x'))",
|
||||
"'abc'.normalize(Symbol('x'))",
|
||||
] {
|
||||
let err = eval(&mut vm, src).unwrap_err();
|
||||
assert!(err.contains("TypeError"), "{src} 应抛 TypeError,实际: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_trim_start() {
|
||||
let mut vm = Vm::new();
|
||||
@@ -438,21 +487,19 @@ fn string_replace_all_nonglobal_regex_throws() {
|
||||
|
||||
#[test]
|
||||
fn string_replace_regex_proto_subclass_fallback() {
|
||||
// 类正则对象(proto 恒等 RegExp.prototype 但无编译正则):
|
||||
// replace/replaceAll 统一按 ToString 文本走字符串路径;默认 toString
|
||||
// 不命中源串,故此处断言结果与原串一致(判别性用例见
|
||||
// string_replace_regex_proto_subclass_custom_tostring)。
|
||||
// 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)的默认 toString 是
|
||||
// RegExp.prototype.toString,对非 RegExp receiver 抛 TypeError——对象 ToString
|
||||
// 异常按规范原样传播,不吞错降级为文本替换;自定义 toString 的判别性用例见
|
||||
// string_replace_regex_proto_subclass_custom_tostring。
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, 'Y')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "aXb");
|
||||
let s = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.replaceAll(sp, 'Y')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "aXb");
|
||||
let s = eval(
|
||||
&mut vm,
|
||||
for src in [
|
||||
"var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, 'Y')",
|
||||
"var sp = Object.create(RegExp.prototype); 'aXb'.replaceAll(sp, 'Y')",
|
||||
"var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, function(m){ return 'Z' })",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(to_str(&vm, s), "aXb");
|
||||
] {
|
||||
let err = eval(&mut vm, src).unwrap_err();
|
||||
assert!(err.contains("TypeError"), "{src} 应抛 TypeError,实际: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -581,13 +628,11 @@ fn string_split_string_separator() {
|
||||
|
||||
#[test]
|
||||
fn string_split_regex_proto_subclass_fallback() {
|
||||
// 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)回退字符串路径,
|
||||
// 按 ToString 文本("[object]")切分,不落入逐字符切分。
|
||||
// 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)的默认 toString 抛
|
||||
// TypeError(RegExp.prototype.toString 校验 receiver),split 按规范传播异常。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.split(sp)").unwrap();
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 1);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "aXb");
|
||||
let err = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.split(sp)").unwrap_err();
|
||||
assert!(err.contains("TypeError"), "类正则分隔符应抛 TypeError,实际: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user