From 601d231ba2100abc29071f8b324765112fa79fcf Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 25 Sep 2026 04:43:11 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20String=20=E4=B8=89=E6=96=B9=E6=B3=95=20R?= =?UTF-8?q?egExp=20=E6=8A=9B=E9=94=99=E3=80=81pad=20=E6=94=B9=E7=A0=81?= =?UTF-8?q?=E5=85=83=E5=8F=A3=E5=BE=84=E4=B8=8E=20legacy=20receiver=20?= =?UTF-8?q?=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/string/basic.rs | 133 ++++++++++++++------- crates/oxide_builtins/src/string/common.rs | 30 ----- crates/oxide_builtins/src/string/regex.rs | 3 +- 3 files changed, 94 insertions(+), 72 deletions(-) diff --git a/crates/oxide_builtins/src/string/basic.rs b/crates/oxide_builtins/src/string/basic.rs index 22589703..deffd7b5 100644 --- a/crates/oxide_builtins/src/string/basic.rs +++ b/crates/oxide_builtins/src/string/basic.rs @@ -7,10 +7,7 @@ use crate::builtins_debug; use crate::builtins_error; use super::common::try_string; -use super::{ - as_units, code_point_count, find_units, is_trim_unit, map_well_formed_segments, rfind_units, take_code_points, - this_units, -}; +use super::{as_units, find_units, is_trim_unit, map_well_formed_segments, rfind_units, this_units}; // ── 静态方法 / 构造 ───────────────────────────────────────────────────── @@ -216,12 +213,24 @@ pub fn string_index_of(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::int(-1)) } -/// `String.prototype.includes(searchString, position)`:是否包含子串。 +/// `String.prototype.includes(searchString, position)`:是否包含子串;search 为 +/// RegExp 抛 TypeError(普通对象带抛错 @@match getter 时传播原异常)。 pub fn string_includes(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.includes called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); + // IsRegExp 前置判:RegExp search 抛 TypeError;getter 抛错恢复原异常上抛。 + let is_regexp = match super::is_regexp_live(vm, search_val) { + Ok(b) => b, + Err(e) => return NativeResult::Err(e), + }; + if is_regexp { + return NativeResult::Err(crate::error::create_type_error( + vm, + "First argument to String.prototype.includes must not be a regular expression", + )); + } let s: Vec = try_string!(this_units(vm, args)).into_owned(); // position:ToIntegerOrInfinity 传播式,负值归 0、+Inf 归 len。 let pos = if args.len() > 2 { @@ -337,14 +346,18 @@ pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); let s: Vec = try_string!(this_units(vm, args)).into_owned(); let n = s.len(); - // position:ToIntegerOrInfinity 传播式(NaN → 0),负值归 0、+Inf 归 len; - // 缺参 → len。 + // position:ToIntegerOrInfinity 传播式(NaN → 全长,legacy 位置窗口), + // 负值归 0、+Inf 归 len;缺参 → len。 let pos = if args.len() > 2 { - let p = match to_integer_or_infinity_bounded(vm, vm.reg(args[2])) { + let p = match vm.coerce_number_bounded(vm.reg(args[2])) { Ok(p) => p, - Err(exc) => return NativeResult::Err(exc), + Err(msg) => return NativeResult::Err(crate::array::from_engine_error(vm, &msg)), }; - (p.max(0.0).min(n as f64)) as usize + if p.is_nan() { + n + } else { + (p.max(0.0).min(n as f64)) as usize + } } else { n }; @@ -352,8 +365,8 @@ pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult if search.is_empty() { return NativeResult::Ok(JsValue::int(pos as i32)); } - // 窗口 = 前 pos+1 个码元:起始位置 ≤ pos 的末次出现。 - let window = (pos + 1).min(n); + // 窗口 = 前 pos+searchLen 个码元:起始位置 ≤ pos 的末次出现。 + let window = (pos + search.len()).min(n); if let Some(idx) = rfind_units(&s, &search, window) { return NativeResult::Ok(JsValue::int(idx as i32)); } @@ -430,7 +443,8 @@ pub fn string_slice(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.substring(start, end)`:取子串,start/end 自动对调且取非负。 +/// `String.prototype.substring(start, end)`:取子串,start/end 自动对调且取非负; +/// 显式 undefined 端视缺参(end → len,node 口径)。 pub fn string_substring(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.substring called with {} args", args.len()); // 寄存器取值先行(纯函数),后借 this 取子串。 @@ -452,7 +466,10 @@ pub fn string_substring(vm: &mut H, args: &[u8]) -> NativeResult { } None => 0.0, }; + // 显式 undefined 端视缺参(node 口径:end → len),其余 ToIntegerOrInfinity + // 传播式:NaN/负 → 0、+Inf → len,取 min。 let mut end = match end_arg { + Some(v) if v.is_undefined() => s.len() as f64, Some(v) => { let p = match to_integer_or_infinity_bounded(vm, v) { Ok(p) => p, @@ -621,46 +638,51 @@ pub fn string_repeat(vm: &mut H, args: &[u8]) -> NativeResult { } /// `String.prototype.padStart(targetLength, padString)`:在头部补足 padString -/// 到目标码点数。 +/// 到目标码元数。 pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.padStart called with {} args", args.len()); - // 参数转换先行(&mut 路径):targetLength 与 padString 均可能触发对象转换。 + // 可观察操作序由规范钉死:receiver ToString 先行,后 targetLength 转换, + // 再 padString ToString。 + let s: Vec = try_string!(this_units(vm, args)).into_owned(); // targetLength:ToIntegerOrInfinity 传播式,负值归 0;+Inf 经既有 10000 // 上限检查自然落 RangeError。 - let target_arg = if args.len() > 1 { + let target = if args.len() > 1 { let p = match to_integer_or_infinity_bounded(vm, vm.reg(args[1])) { Ok(p) => p, Err(exc) => return NativeResult::Err(exc), }; - Some(p.max(0.0)) + p.max(0.0) } else { - None + s.len() as f64 }; - let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; // 规范口径:padString 为 undefined(含显式传入)回落空格填充。 + let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let pad: Vec = if pad_val.is_undefined() { vec![0x20] } else { try_string!(as_units(vm, pad_val)).into_owned() }; - let s: Vec = try_string!(this_units(vm, args)).into_owned(); - let s_len = code_point_count(&s); - let target = target_arg.map(|p| p as usize).unwrap_or(s_len); + // 目标长度按码元(非码点)口径:pad 亦按码元截断。 + let s_len = s.len(); + let target = target as usize; + if s_len >= target { + return NativeResult::Ok(vm.new_string_units_owned(s)); + } if target > 10000 { builtins_error!("String.prototype.padStart: invalid receiver"); return NativeResult::Err(crate::error::create_range_error(vm, "Invalid string length")); } - if s_len >= target || pad.is_empty() { - return NativeResult::Ok(vm.new_string_units_owned(s.to_vec())); + if pad.is_empty() { + return NativeResult::Ok(vm.new_string_units_owned(s)); } let needed = target - s_len; - let pad_len = code_point_count(&pad).max(1); + let pad_len = pad.len(); let reps = needed.div_ceil(pad_len); let mut pad_rep = Vec::with_capacity(pad.len() * reps); for _ in 0..reps { pad_rep.extend_from_slice(&pad); } - let prefix = take_code_points(&pad_rep, needed); + let prefix = &pad_rep[..needed]; let mut out = Vec::with_capacity(prefix.len() + s.len()); out.extend_from_slice(prefix); out.extend_from_slice(&s); @@ -668,46 +690,51 @@ pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { } /// `String.prototype.padEnd(targetLength, padString)`:在尾部补足 padString -/// 到目标码点数。 +/// 到目标码元数。 pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.padEnd called with {} args", args.len()); - // 参数转换先行(&mut 路径):targetLength 与 padString 均可能触发对象转换。 + // 可观察操作序由规范钉死:receiver ToString 先行,后 targetLength 转换, + // 再 padString ToString。 + let s: Vec = try_string!(this_units(vm, args)).into_owned(); // targetLength:ToIntegerOrInfinity 传播式,负值归 0;+Inf 经既有 10000 // 上限检查自然落 RangeError。 - let target_arg = if args.len() > 1 { + let target = if args.len() > 1 { let p = match to_integer_or_infinity_bounded(vm, vm.reg(args[1])) { Ok(p) => p, Err(exc) => return NativeResult::Err(exc), }; - Some(p.max(0.0)) + p.max(0.0) } else { - None + s.len() as f64 }; - let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; // 规范口径:padString 为 undefined(含显式传入)回落空格填充。 + let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let pad: Vec = if pad_val.is_undefined() { vec![0x20] } else { try_string!(as_units(vm, pad_val)).into_owned() }; - let s: Vec = try_string!(this_units(vm, args)).into_owned(); - let s_len = code_point_count(&s); - let target = target_arg.map(|p| p as usize).unwrap_or(s_len); + // 目标长度按码元(非码点)口径:pad 亦按码元截断。 + let s_len = s.len(); + let target = target as usize; + if s_len >= target { + return NativeResult::Ok(vm.new_string_units_owned(s)); + } if target > 10000 { builtins_error!("String.prototype.padEnd: invalid receiver"); return NativeResult::Err(crate::error::create_range_error(vm, "Invalid string length")); } - if s_len >= target || pad.is_empty() { - return NativeResult::Ok(vm.new_string_units_owned(s.to_vec())); + if pad.is_empty() { + return NativeResult::Ok(vm.new_string_units_owned(s)); } let needed = target - s_len; - let pad_len = code_point_count(&pad).max(1); + let pad_len = pad.len(); let reps = needed.div_ceil(pad_len); let mut pad_rep = Vec::with_capacity(pad.len() * reps); for _ in 0..reps { pad_rep.extend_from_slice(&pad); } - let suffix = take_code_points(&pad_rep, needed); + let suffix = &pad_rep[..needed]; let mut out = Vec::with_capacity(s.len() + suffix.len()); out.extend_from_slice(&s); out.extend_from_slice(suffix); @@ -715,12 +742,24 @@ pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { } /// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头 -/// (position 起的前缀码元比较,不做字符边界吸附——规格口径)。 +/// (position 起的前缀码元比较,不做字符边界吸附——规格口径);search 为 +/// RegExp 抛 TypeError(普通对象带抛错 @@match getter 时传播原异常)。 pub fn string_starts_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.startsWith called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); + // IsRegExp 前置判:RegExp search 抛 TypeError;getter 抛错恢复原异常上抛。 + let is_regexp = match super::is_regexp_live(vm, search_val) { + Ok(b) => b, + Err(e) => return NativeResult::Err(e), + }; + if is_regexp { + return NativeResult::Err(crate::error::create_type_error( + vm, + "First argument to String.prototype.startsWith must not be a regular expression", + )); + } let s: Vec = try_string!(this_units(vm, args)).into_owned(); // position:ToIntegerOrInfinity 传播式,负值归 0、+Inf 归 len。 let pos = if args.len() > 2 { @@ -737,12 +776,24 @@ pub fn string_starts_with(vm: &mut H, args: &[u8]) -> NativeResult { } /// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾 -/// (截断至 endPosition 的后缀码元比较,不做字符边界吸附——规格口径)。 +/// (截断至 endPosition 的后缀码元比较,不做字符边界吸附——规格口径);search +/// 为 RegExp 抛 TypeError(普通对象带抛错 @@match getter 时传播原异常)。 pub fn string_ends_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.endsWith called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); + // IsRegExp 前置判:RegExp search 抛 TypeError;getter 抛错恢复原异常上抛。 + let is_regexp = match super::is_regexp_live(vm, search_val) { + Ok(b) => b, + Err(e) => return NativeResult::Err(e), + }; + if is_regexp { + return NativeResult::Err(crate::error::create_type_error( + vm, + "First argument to String.prototype.endsWith must not be a regular expression", + )); + } let s: Vec = try_string!(this_units(vm, args)).into_owned(); // endPosition:缺参/显式 undefined → len(规范步),其余 ToIntegerOrInfinity // 传播式折叠,负值归 0、+Inf 归 len。 diff --git a/crates/oxide_builtins/src/string/common.rs b/crates/oxide_builtins/src/string/common.rs index bed07771..160de6e6 100644 --- a/crates/oxide_builtins/src/string/common.rs +++ b/crates/oxide_builtins/src/string/common.rs @@ -189,36 +189,6 @@ fn well_formed_segment_end(units: &[u16], start: usize) -> usize { i } -/// 码点数(代理对计 1,孤立 surrogate 计 1)——pad 族的目标长度口径。 -pub(crate) fn code_point_count(units: &[u16]) -> usize { - let mut n = 0; - let mut i = 0; - while i < units.len() { - if (0xD800..=0xDBFF).contains(&units[i]) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { - i += 2; - } else { - i += 1; - } - n += 1; - } - n -} - -/// 取序列头 count 个码点对应的单元前缀。 -pub(crate) fn take_code_points(units: &[u16], count: usize) -> &[u16] { - let mut n = 0; - let mut i = 0; - while i < units.len() && n < count { - if (0xD800..=0xDBFF).contains(&units[i]) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { - i += 2; - } else { - i += 1; - } - n += 1; - } - &units[..i] -} - /// ES 白空格判定的单元口径:Rust 空白表与 White_Space+LineTerminator 等价, /// 另补 U+FEFF(ES White_Space 含 BOM,Unicode White_Space 不含); /// 孤立 surrogate 非白空格。 diff --git a/crates/oxide_builtins/src/string/regex.rs b/crates/oxide_builtins/src/string/regex.rs index 0dd328db..a23d80a0 100644 --- a/crates/oxide_builtins/src/string/regex.rs +++ b/crates/oxide_builtins/src/string/regex.rs @@ -60,7 +60,8 @@ fn is_regexp_obj(val: JsValue, vm: &H) -> bool { /// IsRegExp 口径判定:非对象 false;对象一律 Get(@@match) 后 ToBoolean /// (真 RegExp 亦读,getter 副作用可观测;getter 抛错恢复原异常上抛)。 -fn is_regexp_live(vm: &mut H, val: JsValue) -> Result { +/// 供正则匹配族与 includes/startsWith/endsWith 的 IsRegExp 前置判共用。 +pub(crate) fn is_regexp_live(vm: &mut H, val: JsValue) -> Result { if !val.is_object() { return Ok(false); }