fix: 字符串索引体系改 UTF-16 code unit 语义(astral 字符正确)

This commit is contained in:
2026-08-15 19:34:55 +08:00
parent ffe1d6fc02
commit f63b41454f
2 changed files with 270 additions and 65 deletions
+146 -61
View File
@@ -306,7 +306,7 @@ fn call_replacer<H: VmHost>(vm: &mut H, replacer: JsValue, cb_args: &[JsValue])
} }
/// 函数 replacer 的回调参数:匹配串、各捕获组(未匹配为 undefined)、position、原字符串。 /// 函数 replacer 的回调参数:匹配串、各捕获组(未匹配为 undefined)、position、原字符串。
/// position 按字符索引(非字节偏移)。原字符串参数由调用方预构传入(`text_arg`), /// position 按 UTF-16 单元计数(非字节偏移)。原字符串参数由调用方预构传入(`text_arg`),
/// 避免每匹配复制整个源串——字符串不可变,同一 `JsValue` 可安全复用。 /// 避免每匹配复制整个源串——字符串不可变,同一 `JsValue` 可安全复用。
fn replacer_cb_args<H: VmHost>(vm: &mut H, text: &str, m: &regress::Match, text_arg: JsValue) -> Vec<JsValue> { fn replacer_cb_args<H: VmHost>(vm: &mut H, text: &str, m: &regress::Match, text_arg: JsValue) -> Vec<JsValue> {
let range = m.range(); let range = m.range();
@@ -318,7 +318,7 @@ fn replacer_cb_args<H: VmHost>(vm: &mut H, text: &str, m: &regress::Match, text_
None => cb_args.push(JsValue::undefined()), None => cb_args.push(JsValue::undefined()),
} }
} }
cb_args.push(JsValue::int(text[..range.start].chars().count() as i32)); cb_args.push(JsValue::int(byte_to_unit(text, range.start) as i32));
cb_args.push(text_arg); cb_args.push(text_arg);
cb_args cb_args
} }
@@ -382,7 +382,7 @@ fn string_replace_fn<H: VmHost>(
out.push_str(&text[last_end..p]); out.push_str(&text[last_end..p]);
let cb_args = [ let cb_args = [
vm.new_string(&text[p..p + search_length]), vm.new_string(&text[p..p + search_length]),
JsValue::int(text[..p].chars().count() as i32), JsValue::int(byte_to_unit(text, p) as i32),
text_arg, text_arg,
]; ];
let repl = try_string!(call_replacer(vm, replacer, &cb_args)); let repl = try_string!(call_replacer(vm, replacer, &cb_args));
@@ -393,27 +393,94 @@ fn string_replace_fn<H: VmHost>(
NativeResult::Ok(vm.new_string_owned(out)) NativeResult::Ok(vm.new_string_owned(out))
} }
/// Unicode 标量数(供 pad 族按字符计数;索引/切片族须用 [`utf16_len`])。
fn char_len(s: &str) -> usize { fn char_len(s: &str) -> usize {
s.chars().count() s.chars().count()
} }
fn byte_index_at_char(s: &str, char_pos: usize) -> usize { /// 取串头 count 个 Unicode 标量(供 pad 族填充计数)。
if char_pos == 0 {
return 0;
}
s.char_indices().nth(char_pos).map(|(idx, _)| idx).unwrap_or(s.len())
}
fn char_slice(s: &str, start: usize, end: usize) -> &str {
let start_byte = byte_index_at_char(s, start);
let end_byte = byte_index_at_char(s, end);
&s[start_byte..end_byte]
}
fn take_chars(s: &str, count: usize) -> String { fn take_chars(s: &str, count: usize) -> String {
s.chars().take(count).collect() s.chars().take(count).collect()
} }
/// UTF-16 code unit 数:astral 字符计 2 单元;ASCII 串与字节数一致直接短路。
fn utf16_len(s: &str) -> usize {
if s.is_ascii() {
s.len()
} else {
s.encode_utf16().count()
}
}
/// 字节偏移前缀的 UTF-16 单元数(索引型返回值统一换算入口)。
/// 调用方须保证 `byte` 落在字符边界(well-formed 匹配恒对齐边界)。
fn byte_to_unit(s: &str, byte: usize) -> usize {
if s.is_ascii() {
byte
} else {
s[..byte].encode_utf16().count()
}
}
/// UTF-16 单元位置所在字符的起始字节偏移(字符访问/切片起点定位:位置在
/// 字符边界时即该边界,落在代理对中间时取所在 astral 字符起始)。
fn unit_to_char_start(s: &str, unit: usize) -> usize {
if s.is_ascii() {
return unit.min(s.len());
}
let mut acc = 0usize;
for (byte, ch) in s.char_indices() {
if unit < acc + ch.len_utf16() {
return byte;
}
acc += ch.len_utf16();
}
s.len()
}
/// UTF-16 单元位置起最近的字符边界字节偏移(查找窗口起点:位置在边界时
/// 即该边界,落在代理对中间时取所在字符末尾——well-formed 匹配不可能从
/// 代理对内部起始)。
fn unit_to_next_boundary(s: &str, unit: usize) -> usize {
if s.is_ascii() {
return unit.min(s.len());
}
let mut acc = 0usize;
for (byte, ch) in s.char_indices() {
let next = acc + ch.len_utf16();
if unit <= next {
return if unit <= acc { byte } else { byte + ch.len_utf8() };
}
acc = next;
}
s.len()
}
/// UTF-16 单元位置是否为字符边界(返回该边界的字节偏移);落在代理对
/// 中间时返回 None(该位置无法对齐任何 well-formed 子串边界)。
fn unit_to_boundary(s: &str, unit: usize) -> Option<usize> {
if s.is_ascii() {
return Some(unit.min(s.len()));
}
let mut acc = 0usize;
for (byte, ch) in s.char_indices() {
if unit == acc {
return Some(byte);
}
acc += ch.len_utf16();
}
(unit == acc).then_some(s.len())
}
/// 按 UTF-16 单元区间取子串:起点取所在字符起始、终点取最近字符边界,
/// 保证结果恒为 well-formed(代理对整体保留)。
fn unit_slice(s: &str, start: usize, end: usize) -> &str {
if start >= end {
return "";
}
&s[unit_to_char_start(s, start)..unit_to_next_boundary(s, end)]
}
fn is_regexp_obj<H: VmHost>(val: JsValue, vm: &H) -> bool { fn is_regexp_obj<H: VmHost>(val: JsValue, vm: &H) -> bool {
if !val.is_object() { if !val.is_object() {
return false; return false;
@@ -435,7 +502,7 @@ fn is_regexp_obj<H: VmHost>(val: JsValue, vm: &H) -> bool {
std::ptr::eq(proto_ptr, rp) std::ptr::eq(proto_ptr, rp)
} }
/// `String.prototype.indexOf(searchString, position)`:按字符索引查找首次出现位置。 /// `String.prototype.indexOf(searchString, position)`:按 UTF-16 单元查找首次出现位置。
pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.indexOf called with {} args", args.len()); builtins_debug!("String.prototype.indexOf called with {} args", args.len());
// 参数转换先行:search 与 position 均可能触发对象 ToString/ToNumber(&mut 路径)。 // 参数转换先行:search 与 position 均可能触发对象 ToString/ToNumber(&mut 路径)。
@@ -454,23 +521,23 @@ pub fn string_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::int(-1)); return NativeResult::Ok(JsValue::int(-1));
} }
let n = char_len(&s); let n = utf16_len(&s);
let pos = pos_raw.min(n); let pos = pos_raw.min(n);
if search.is_empty() { if search.is_empty() {
return NativeResult::Ok(JsValue::int(pos as i32)); return NativeResult::Ok(JsValue::int(pos as i32));
} }
let start_byte = byte_index_at_char(&s, pos); let start_byte = unit_to_next_boundary(&s, pos);
let haystack = &s[start_byte..]; let haystack = &s[start_byte..];
if search.len() == 1 { if search.len() == 1 {
if let Some(idx) = memchr(search.as_bytes()[0], haystack.as_bytes()) { if let Some(idx) = memchr(search.as_bytes()[0], haystack.as_bytes()) {
let matched_byte = start_byte + idx; let matched_byte = start_byte + idx;
return NativeResult::Ok(JsValue::int(char_len(&s[..matched_byte]) as i32)); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, matched_byte) as i32));
} }
} else if let Some(idx) = haystack.find(&search) { } else if let Some(idx) = haystack.find(&search) {
let matched_byte = start_byte + idx; let matched_byte = start_byte + idx;
return NativeResult::Ok(JsValue::int(char_len(&s[..matched_byte]) as i32)); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, matched_byte) as i32));
} }
NativeResult::Ok(JsValue::int(-1)) NativeResult::Ok(JsValue::int(-1))
} }
@@ -493,14 +560,14 @@ pub fn string_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false)); return NativeResult::Ok(JsValue::bool(false));
} }
let n = char_len(&s); let n = utf16_len(&s);
let pos = pos_raw.min(n); let pos = pos_raw.min(n);
if search.is_empty() { if search.is_empty() {
return NativeResult::Ok(JsValue::bool(true)); return NativeResult::Ok(JsValue::bool(true));
} }
let haystack = &s[byte_index_at_char(&s, pos)..]; let haystack = &s[unit_to_next_boundary(&s, pos)..];
if search.len() == 1 { if search.len() == 1 {
NativeResult::Ok(JsValue::bool(memchr(search.as_bytes()[0], haystack.as_bytes()).is_some())) NativeResult::Ok(JsValue::bool(memchr(search.as_bytes()[0], haystack.as_bytes()).is_some()))
} else { } else {
@@ -508,7 +575,7 @@ pub fn string_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} }
} }
/// `String.prototype.charAt(index)`:返回指定位置的单字符;越界返回空串。 /// `String.prototype.charAt(index)`:返回指定 UTF-16 单元位置的单字符;越界返回空串。
pub fn string_char_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_char_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.charAt called with {} args", args.len()); builtins_debug!("String.prototype.charAt called with {} args", args.len());
// index 可能触发对象 ToNumber(&mut 路径),先行转换。 // index 可能触发对象 ToNumber(&mut 路径),先行转换。
@@ -529,20 +596,20 @@ pub fn string_char_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
None => NativeResult::Ok(vm.new_string(&first.to_string())), None => NativeResult::Ok(vm.new_string(&first.to_string())),
}; };
} }
if idx < 0 || idx as usize >= char_len(&s) { if idx < 0 || idx as usize >= utf16_len(&s) {
return NativeResult::Ok(vm.new_string("")); return NativeResult::Ok(vm.new_string(""));
} }
let ch = s.chars().nth(idx as usize); // 按 UTF-16 单元定位;落在代理对中间时返回所在 astral 字符(孤立代理
match ch { // 无法在 UTF-8 表示,见架构限制)。
Some(c) => match vm.single_char(c) { let byte = unit_to_char_start(&s, idx as usize);
Some(v) => NativeResult::Ok(v), let ch = s[byte..].chars().next().unwrap();
None => NativeResult::Ok(vm.new_string(&c.to_string())), match vm.single_char(ch) {
}, Some(v) => NativeResult::Ok(v),
None => NativeResult::Ok(vm.new_string("")), None => NativeResult::Ok(vm.new_string(&ch.to_string())),
} }
} }
/// `String.prototype.charCodeAt(index)`:返回指定位置字符的 UTF-16 code unit;越界返回 NaN。 /// `String.prototype.charCodeAt(index)`:返回指定 UTF-16 单元位置的 code unit;越界返回 NaN。
pub fn string_char_code_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_char_code_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.charCodeAt called with {} args", args.len()); builtins_debug!("String.prototype.charCodeAt called with {} args", args.len());
// index 可能触发对象 ToNumber(&mut 路径),先行转换。 // index 可能触发对象 ToNumber(&mut 路径),先行转换。
@@ -556,12 +623,15 @@ pub fn string_char_code_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if s.is_empty() { if s.is_empty() {
return NativeResult::Ok(JsValue::float(f64::NAN)); return NativeResult::Ok(JsValue::float(f64::NAN));
} }
return NativeResult::Ok(JsValue::int(s.chars().next().unwrap() as i32)); // 缺省 index=0:返回首单元(astral 首字符为高代理)。
return NativeResult::Ok(JsValue::int(s.encode_utf16().next().unwrap() as i32));
} }
if idx < 0 || idx as usize >= char_len(&s) { if idx < 0 || idx as usize >= utf16_len(&s) {
return NativeResult::Ok(JsValue::float(f64::NAN)); return NativeResult::Ok(JsValue::float(f64::NAN));
} }
NativeResult::Ok(JsValue::int(s.chars().nth(idx as usize).unwrap() as i32)) // 按 UTF-16 单元展开取码元:astral 字符的高/低代理分别精确返回。
let unit = s.encode_utf16().nth(idx as usize).unwrap();
NativeResult::Ok(JsValue::int(unit as i32))
} }
/// `String.prototype.concat(...strings)`:拼接 this 与各参数返回新字符串。 /// `String.prototype.concat(...strings)`:拼接 this 与各参数返回新字符串。
@@ -583,7 +653,7 @@ pub fn string_concat<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string_owned(result)) NativeResult::Ok(vm.new_string_owned(result))
} }
/// `String.prototype.slice(start, end)`:按字符区间(支持负索引)取子串。 /// `String.prototype.slice(start, end)`:按 UTF-16 单元区间(支持负索引)取子串。
pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.slice called with {} args", args.len()); builtins_debug!("String.prototype.slice called with {} args", args.len());
// 位置参数先行(&mut 转换),后借 this 取子串。 // 位置参数先行(&mut 转换),后借 this 取子串。
@@ -598,7 +668,7 @@ pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
None None
}; };
let s = try_string!(this_string(vm, args)); let s = try_string!(this_string(vm, args));
let n = char_len(&s) as i32; let n = utf16_len(&s) as i32;
let start = match start_raw { let start = match start_raw {
Some(v) => { Some(v) => {
if v < 0 { if v < 0 {
@@ -619,7 +689,7 @@ pub fn string_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} }
None => n, None => n,
}; };
let result = if start < end { char_slice(&s, start as usize, end as usize) } else { "" }; let result = if start < end { unit_slice(&s, start as usize, end as usize) } else { "" };
let owned = result.to_string(); let owned = result.to_string();
NativeResult::Ok(vm.new_string_owned(owned)) NativeResult::Ok(vm.new_string_owned(owned))
} }
@@ -631,7 +701,7 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None }; let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None };
let end_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None }; let end_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None };
let s = try_string!(this_string(vm, args)); let s = try_string!(this_string(vm, args));
let n = char_len(&s) as i32; let n = utf16_len(&s) as i32;
let mut start = match start_arg { let mut start = match start_arg {
Some(v) => { Some(v) => {
let v = oxide_runtime_api::to_integer_or_infinity(v); let v = oxide_runtime_api::to_integer_or_infinity(v);
@@ -657,19 +727,20 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if start > end { if start > end {
std::mem::swap(&mut start, &mut end); std::mem::swap(&mut start, &mut end);
} }
let result = char_slice(&s, start as usize, end as usize); let result = unit_slice(&s, start as usize, end as usize);
let owned = result.to_string(); let owned = result.to_string();
NativeResult::Ok(vm.new_string_owned(owned)) NativeResult::Ok(vm.new_string_owned(owned))
} }
/// `String.prototype.substr(start, length)`:从 start 取 length 个字符(Annex B,支持负 start)。 /// `String.prototype.substr(start, length)`:从 start 起取 length 个 UTF-16 单元
/// (Annex B,支持负 start)。
pub fn string_substr<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_substr<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.substr called with {} args", args.len()); builtins_debug!("String.prototype.substr called with {} args", args.len());
// 寄存器取值先行(纯函数),后借 this 截取。 // 寄存器取值先行(纯函数),后借 this 截取。
let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None }; let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None };
let length_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None }; let length_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None };
let s = try_string!(this_string(vm, args)); let s = try_string!(this_string(vm, args));
let len = char_len(&s) as isize; let len = utf16_len(&s) as isize;
let start = match start_arg { let start = match start_arg {
Some(v) => { Some(v) => {
let n = oxide_runtime_api::to_integer_or_infinity(v) as isize; let n = oxide_runtime_api::to_integer_or_infinity(v) as isize;
@@ -685,11 +756,15 @@ pub fn string_substr<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Some(v) => (oxide_runtime_api::to_integer_or_infinity(v) as isize).max(0) as usize, Some(v) => (oxide_runtime_api::to_integer_or_infinity(v) as isize).max(0) as usize,
None => len as usize - start, None => len as usize - start,
}; };
let result = take_chars(&s[byte_index_at_char(&s, start)..], length.min(len as usize - start)); // 起点按 UTF-16 单元定位取窗口(起点落在代理对中间时取所在 astral 字符,
NativeResult::Ok(vm.new_string_owned(result)) // 孤立代理无法在 UTF-8 表示,见架构限制)。
let count = length.min(len as usize - start);
let result = unit_slice(&s, start, start + count);
let owned = result.to_string();
NativeResult::Ok(vm.new_string_owned(owned))
} }
/// `String.prototype.at(index)`:按字符索引取字符(支持负索引);越界返回 undefined。 /// `String.prototype.at(index)`:按 UTF-16 单元索引取字符(支持负索引);越界返回 undefined。
pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.at called with {} args", args.len()); builtins_debug!("String.prototype.at called with {} args", args.len());
let idx = if args.len() > 1 { let idx = if args.len() > 1 {
@@ -698,12 +773,14 @@ pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
0 0
}; };
let s = try_string!(this_string(vm, args)); let s = try_string!(this_string(vm, args));
let len = char_len(&s) as i32; let len = utf16_len(&s) as i32;
let idx = if idx < 0 { len + idx } else { idx }; let idx = if idx < 0 { len + idx } else { idx };
if idx < 0 || idx >= len { if idx < 0 || idx >= len {
return NativeResult::Ok(JsValue::undefined()); return NativeResult::Ok(JsValue::undefined());
} }
let ch = s.chars().nth(idx as usize).unwrap().to_string(); // 按 UTF-16 单元定位;落在代理对中间时返回所在 astral 字符(架构限制)。
let byte = unit_to_char_start(&s, idx as usize);
let ch = s[byte..].chars().next().unwrap().to_string();
NativeResult::Ok(vm.new_string(&ch)) NativeResult::Ok(vm.new_string(&ch))
} }
@@ -730,7 +807,7 @@ pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::int(-1)); return NativeResult::Ok(JsValue::int(-1));
} }
let n = char_len(&s); let n = utf16_len(&s);
let pos = match pos_raw { let pos = match pos_raw {
Some(p) => p.min(n), Some(p) => p.min(n),
None => n, None => n,
@@ -740,17 +817,15 @@ pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
return NativeResult::Ok(JsValue::int(pos as i32)); return NativeResult::Ok(JsValue::int(pos as i32));
} }
let end_byte = byte_index_at_char(&s, (pos + 1).min(n)); let end_byte = unit_to_next_boundary(&s, (pos + 1).min(n));
let haystack = &s[..end_byte]; let haystack = &s[..end_byte];
if search.len() == 1 { if search.len() == 1 {
if let Some(idx) = memchr::memrchr(search.as_bytes()[0], haystack.as_bytes()) { if let Some(idx) = memchr::memrchr(search.as_bytes()[0], haystack.as_bytes()) {
let result = char_len(&s[..idx]); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, idx) as i32));
return NativeResult::Ok(JsValue::int(result as i32));
} }
} else if let Some(idx) = haystack.rfind(&search) { } else if let Some(idx) = haystack.rfind(&search) {
let result = char_len(&s[..idx]); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, idx) as i32));
return NativeResult::Ok(JsValue::int(result as i32));
} }
NativeResult::Ok(JsValue::int(-1)) NativeResult::Ok(JsValue::int(-1))
} }
@@ -877,9 +952,14 @@ pub fn string_starts_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false)); return NativeResult::Ok(JsValue::bool(false));
} }
let n = char_len(&s); let n = utf16_len(&s);
let pos = pos_raw.min(n); let pos = pos_raw.min(n);
NativeResult::Ok(JsValue::bool(s[byte_index_at_char(&s, pos)..].starts_with(&search))) // 位置须对齐字符边界才有 well-formed 前缀可比;落在代理对中间时仅空串可匹配。
let result = match unit_to_boundary(&s, pos) {
Some(byte) => s[byte..].starts_with(&search),
None => search.is_empty(),
};
NativeResult::Ok(JsValue::bool(result))
} }
/// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。 /// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。
@@ -900,9 +980,14 @@ pub fn string_ends_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.len() < 2 { if args.len() < 2 {
return NativeResult::Ok(JsValue::bool(false)); return NativeResult::Ok(JsValue::bool(false));
} }
let n = char_len(&s); let n = utf16_len(&s);
let end_pos = end_pos_raw.min(n); let end_pos = end_pos_raw.min(n);
NativeResult::Ok(JsValue::bool(s[..byte_index_at_char(&s, end_pos)].ends_with(&search))) // 截断位置须对齐字符边界才有 well-formed 后缀可比;落在代理对中间时仅空串可匹配。
let result = match unit_to_boundary(&s, end_pos) {
Some(byte) => s[..byte].ends_with(&search),
None => search.is_empty(),
};
NativeResult::Ok(JsValue::bool(result))
} }
/// `String.prototype.split(separator, limit)`:按分隔符拆分为字符串数组; /// `String.prototype.split(separator, limit)`:按分隔符拆分为字符串数组;
@@ -1242,7 +1327,7 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::null()) NativeResult::Ok(JsValue::null())
} }
/// `String.prototype.search(pattern)`:返回首个匹配位置,无匹配返回 -1。 /// `String.prototype.search(pattern)`:返回首个匹配位置(UTF-16 单元),无匹配返回 -1。
pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.search called with {} args", args.len()); builtins_debug!("String.prototype.search called with {} args", args.len());
// 参数读取守卫先行:无参调用缺省 searchString 为 undefined(args 仅含 this 槽)。 // 参数读取守卫先行:无参调用缺省 searchString 为 undefined(args 仅含 this 槽)。
@@ -1269,12 +1354,12 @@ pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。 // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
if let Some(m) = regex.find(&s) { if let Some(m) = regex.find(&s) {
return NativeResult::Ok(JsValue::int(m.range().start as i32)); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, m.range().start) as i32));
} }
return NativeResult::Ok(JsValue::int(-1)); return NativeResult::Ok(JsValue::int(-1));
} }
if let Some(pos) = s.find(&pattern) { if let Some(pos) = s.find(&pattern) {
return NativeResult::Ok(JsValue::int(pos as i32)); return NativeResult::Ok(JsValue::int(byte_to_unit(&s, pos) as i32));
} }
NativeResult::Ok(JsValue::int(-1)) NativeResult::Ok(JsValue::int(-1))
} }
+124 -4
View File
@@ -873,10 +873,11 @@ fn string_methods_null_receiver_throw_type_error() {
#[test] #[test]
fn string_slice_astral_character_indices() { fn string_slice_astral_character_indices() {
// 字符索引语义:astral 字符按单字符计位,切片不截断代理对。 // UTF-16 单元索引语义:astral 字符占 2 单元,切片边界落在代理对中间时
// 保留所在 astral 字符(孤立代理无法在 UTF-8 表示,见架构限制)。
let mut vm = Vm::new(); let mut vm = Vm::new();
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(1, 3)").unwrap(); let s = eval(&mut vm, "'\\u{1F600}ab'.slice(1, 3)").unwrap();
assert_eq!(to_str(&vm, s), "ab"); assert_eq!(to_str(&vm, s), "\u{1F600}a");
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(0, 1)").unwrap(); let s = eval(&mut vm, "'\\u{1F600}ab'.slice(0, 1)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}"); assert_eq!(to_str(&vm, s), "\u{1F600}");
let s = eval(&mut vm, "'\\u{1F600}'.padStart(3, 'x')").unwrap(); let s = eval(&mut vm, "'\\u{1F600}'.padStart(3, 'x')").unwrap();
@@ -885,8 +886,8 @@ fn string_slice_astral_character_indices() {
#[test] #[test]
fn string_char_at_ascii_and_astral() { fn string_char_at_ascii_and_astral() {
// charAt 产出:ASCII 走单字符缓存,astral 按标量整体返回,越界空串, // charAt 产出:ASCII 走单字符缓存,astral 按 UTF-16 单元定位(落在代理对
// 缺省参数取首字符。 // 中间时返回所在 astral 字符,架构限制),越界空串,缺省参数取首字符。
let mut vm = Vm::new(); let mut vm = Vm::new();
let s = eval(&mut vm, "'abc'.charAt(1)").unwrap(); let s = eval(&mut vm, "'abc'.charAt(1)").unwrap();
assert_eq!(to_str(&vm, s), "b"); assert_eq!(to_str(&vm, s), "b");
@@ -933,3 +934,122 @@ fn string_rest_destructure_chars() {
let result = eval(&mut vm, "const {...r} = 'ab'; r['0'] + r['1']").unwrap(); let result = eval(&mut vm, "const {...r} = 'ab'; r['0'] + r['1']").unwrap();
assert_eq!(to_str(&vm, result), "ab"); assert_eq!(to_str(&vm, result), "ab");
} }
// ── UTF-16 索引体系回归(astral 字符)──
#[test]
fn string_utf16_length_char_at_code_at() {
// astral 字符(U+1F600)占 2 个 UTF-16 单元:length/charCodeAt 精确,
// charAt 按单元定位(代理对中间返回所在 astral 字符)。
let mut vm = Vm::new();
let r = eval(&mut vm, "'\\u{1F600}ab'.length").unwrap();
assert_num_eq(r, 4.0);
let r = eval(&mut vm, "'\\u{1F600}'.charCodeAt(0)").unwrap();
assert_eq!(r.as_int(), 0xD83D);
let r = eval(&mut vm, "'\\u{1F600}'.charCodeAt(1)").unwrap();
assert_eq!(r.as_int(), 0xDE00);
let r = eval(&mut vm, "'\\u{1F600}a'.charCodeAt(2)").unwrap();
assert_eq!(r.as_int(), 0x61);
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2)").unwrap();
assert_eq!(to_str(&vm, r), "a");
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(1)").unwrap();
assert_eq!(to_str(&vm, r), "\u{1F600}");
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(4)").unwrap();
assert_eq!(to_str(&vm, r), "");
}
#[test]
fn string_utf16_at() {
// at 按 UTF-16 单元定位:正/负索引与越界。
let mut vm = Vm::new();
let r = eval(&mut vm, "'\\u{1F600}ab'.at(2)").unwrap();
assert_eq!(to_str(&vm, r), "a");
let r = eval(&mut vm, "'\\u{1F600}ab'.at(-1)").unwrap();
assert_eq!(to_str(&vm, r), "b");
let r = eval(&mut vm, "'\\u{1F600}ab'.at(-2)").unwrap();
assert_eq!(to_str(&vm, r), "a");
let r = eval(&mut vm, "'\\u{1F600}ab'.at(4)").unwrap();
assert!(r.is_undefined());
}
#[test]
fn string_utf16_slice_substring_substr() {
// 切片族按 UTF-16 单元计数:边界落在代理对中间时保留所在 astral 字符。
let mut vm = Vm::new();
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(0, 2)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}");
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(-2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
let s = eval(&mut vm, "'\\u{1F600}ab'.substring(2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
let s = eval(&mut vm, "'\\u{1F600}ab'.substring(0, 2)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}");
let s = eval(&mut vm, "'\\u{1F600}ab'.substr(2, 2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
let s = eval(&mut vm, "'\\u{1F600}ab'.substr(1, 2)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}a");
}
#[test]
fn string_utf16_index_of_last_index_of() {
// 查找返回值与 position 参数均按 UTF-16 单元计数。
let mut vm = Vm::new();
let r = eval(&mut vm, "'\\u{1F600}ab'.indexOf('a')").unwrap();
assert_eq!(r.as_int(), 2);
let r = eval(&mut vm, "'\\u{1F600}ab'.indexOf('b', 1)").unwrap();
assert_eq!(r.as_int(), 3);
let r = eval(&mut vm, "'\\u{1F600}ab'.indexOf('\\u{1F600}', 1)").unwrap();
assert_eq!(r.as_int(), -1);
let r = eval(&mut vm, "'\\u{1F600}ab'.lastIndexOf('a')").unwrap();
assert_eq!(r.as_int(), 2);
let r = eval(&mut vm, "'\\u{1F600}ab'.lastIndexOf('b', 2)").unwrap();
assert_eq!(r.as_int(), -1);
let r = eval(&mut vm, "'\\u{1F600}ab'.lastIndexOf('')").unwrap();
assert_eq!(r.as_int(), 4);
}
#[test]
fn string_utf16_includes_starts_ends() {
// position/endPosition 按 UTF-16 单元;代理对中间的位置无 well-formed 子串可匹配。
let mut vm = Vm::new();
let r = eval(&mut vm, "'\\u{1F600}ab'.includes('ab', 2)").unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.includes('\\u{1F600}', 1)").unwrap();
assert!(!r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.startsWith('a', 2)").unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.startsWith('\\u{1F600}', 1)").unwrap();
assert!(!r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.endsWith('ab')").unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.endsWith('\\u{1F600}', 1)").unwrap();
assert!(!r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.endsWith('', 1)").unwrap();
assert!(r.as_bool());
}
#[test]
fn string_utf16_search_offset() {
// search 返回值按 UTF-16 单元(正则与字符串路径均非字节偏移)。
let mut vm = Vm::new();
let r = eval(&mut vm, "'\\u{1F600}ab'.search('b')").unwrap();
assert_eq!(r.as_int(), 3);
let r = eval(&mut vm, "'\\u{1F600}ab'.search(/b/)").unwrap();
assert_eq!(r.as_int(), 3);
let r = eval(&mut vm, "'\\u{1F600}ab'.search('\\u{1F600}')").unwrap();
assert_eq!(r.as_int(), 0);
}
#[test]
fn string_utf16_replace_position_callback() {
// 函数 replacer 的 position 参数按 UTF-16 单元计数(正则与字符串模式)。
let mut vm = Vm::new();
let s = eval(&mut vm, "'\\u{1F600}ab'.replace('b', function(m, o){ return o })").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}a3");
let s = eval(&mut vm, "'\\u{1F600}ab'.replace(/b/, function(m, o){ return o })").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}a3");
let s = eval(&mut vm, "'\\u{1F600}ab'.replaceAll('a', function(m, o){ return o })").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}2b");
}