feat: setYear 完整强转加 TimeClip 收尾,escape/unescape 码元口径

This commit is contained in:
2026-10-09 12:03:42 +00:00
parent 11903fc520
commit a4cd722a30
6 changed files with 177 additions and 135 deletions
+28 -25
View File
@@ -1018,37 +1018,40 @@ pub fn date_get_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// Annex B `Date.prototype.setYear(y)`:设置年份,0..99 自动加 1900;
/// 缺参/NaN 置为 Invalid Date。
///
/// 年份参数按完整 ToNumber 强转(对象走 valueOf/toString,Symbol 抛
/// TypeError);this 值无效时取 +0 基准分量(1970-01-01 全零)继续,
/// 结果经 TimeClip 包络收尾。
pub fn date_set_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) };
// this 值先于 ToNumber(year) 读取(规范步序)。
let ms = get_timestamp(obj);
// 缺省年份参数强制转为 NaN;按 Annex B 语义,NaN 年份把日期值置为
// NaN 并返回 NaN。
let arg = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let y_num = oxide_runtime_api::to_number(arg);
if y_num.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
}
if !ms.is_finite() {
return NativeResult::Ok(JsValue::float(f64::NAN));
}
let dt = match dt_from_ms_local(ms) {
Some(d) => d,
None => return NativeResult::Ok(JsValue::float(f64::NAN)),
};
let y_trunc = y_num.trunc();
// f64 截断后可能超出 i32 范围,先做有界转换。
let y = if y_trunc >= i32::MIN as f64 && y_trunc <= i32::MAX as f64 {
y_trunc as i32
let y = native_try!(date_arg_number(vm, if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }));
// 无效 this 值取 +0 基准分量(1970-01-01 时刻全零,与 setFullYear 同口径)。
let (_dy, dm, dd, dh, dmin, dsec, dms) = if ms.is_finite() {
local_components(ms).unwrap_or((1970, 0, 1, 0, 0, 0, 0))
} else {
(1970, 0, 1, 0, 0, 0, 0)
};
if y.is_nan() {
set_timestamp(obj, f64::NAN);
return NativeResult::Ok(JsValue::float(f64::NAN));
};
let full_year = if (0..=99).contains(&y) { y + 1900 } else { y };
let nd = dt.with_year(full_year).unwrap_or(dt);
let ts = nd.timestamp_millis() as f64;
set_timestamp(obj, ts);
NativeResult::Ok(JsValue::float(ts))
}
// MakeFullYear:ToIntegerOrInfinity 截断,0..99 映射到 1900..1999。
let y_int = y.trunc() as i64;
let full_year = if (0..=99).contains(&y_int) { y_int + 1900 } else { y_int };
finish_local_setter(
obj,
make_local_timestamp(
full_year as f64,
dm as f64,
dd as f64,
dh as f64,
dmin as f64,
dsec as f64,
dms as f64,
),
)
}
/// `Date.prototype.toGMTString()`:别名 `toUTCString`(GMT 格式)。
+6 -5
View File
@@ -51,14 +51,15 @@ pub fn global_is_finite<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::bool(n.is_finite()))
}
/// `escape`/`unescape` 的字符串参数:按规范 `? ToString` 完整转换。
/// `escape`/`unescape` 的字符串参数:按规范 `? ToString` 完整转换,
/// 取 UTF-16 单元序列(不 lossy,孤立 surrogate 保真)。
///
/// 对象经 ToPrimitive(string hint) 强制转换,Symbol 抛 TypeError,对象方法
/// 抛出的原生异常原样传播;缺省参数按 "undefined" 处理。
fn string_arg_full<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue> {
fn units_arg_full<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<Vec<u16>, JsValue> {
if args.len() > 1 {
match oxide_runtime_api::to_string_full(vm.reg(args[1]), vm) {
Ok(s) => Ok(s),
match oxide_runtime_api::to_units_full(vm.reg(args[1]), vm) {
Ok(u) => Ok(u),
Err(_) => {
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
@@ -67,7 +68,7 @@ fn string_arg_full<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue
}
}
} else {
Ok("undefined".to_string())
Ok("undefined".encode_utf16().collect())
}
}
+76 -85
View File
@@ -2,133 +2,124 @@ use oxide_runtime_api::{NativeResult, VmHost};
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())
/// 单个码元的十六进制值(逐位严格匹配,不带符号位);非十六进制数字返回 None。
fn hex_val(c: u16) -> Option<u16> {
match c {
48..=57 => Some(c - 48),
97..=102 => Some(c - 97 + 10),
65..=70 => Some(c - 65 + 10),
_ => None,
}
}
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);
/// escape 核(规范 B.2.1.1 逐码元迭代):安全集 `A-Za-z0-9@*_+-./` 直留,
/// <=0xFF 码元产 `%XX`,其余产 `%uXXXX`;孤立 surrogate 码元按码元保真
/// 输出(不替 U+FFFD)。
fn escape_units(input: &[u16]) -> Vec<u16> {
let mut out = Vec::with_capacity(input.len() * 6);
for &u in input {
if u < 0x80 && ESCAPE_SAFE.contains(char::from(u as u8)) {
out.push(u);
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));
}
// 转义序列恒为全 ASCII,按码元推入。
let text = if u <= 0xFF {
format!("%{u:02X}")
} else {
format!("%u{u:04X}")
};
out.extend(text.encode_utf16());
}
out
}
fn unescape_string(input: &str) -> String {
let bytes = input.as_bytes();
let mut out = String::with_capacity(input.len());
/// unescape 核(规范 B.2.1.2 逐码元扫描):`%uXXXX` 优先于 `%XX` 解码,
/// 解码值直接推码元(孤立 surrogate 码元按规范合法输出);无法解析的
/// 序列原样保留。
fn unescape_units(input: &[u16]) -> Vec<u16> {
let mut out = Vec::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;
}
while i < input.len() {
if input[i] == b'%' as u16 {
// %uXXXX:后跟 4 位十六进制。
if i + 6 <= input.len()
&& input[i + 1] == b'u' as u16
&& (2..6).all(|k| hex_val(input[i + k]).is_some())
{
let v = (0..4).fold(0u16, |acc, k| acc * 16 + hex_val(input[i + 2 + k]).unwrap());
out.push(v);
i += 6;
continue;
}
// %XX:后跟 2 位十六进制。
if i + 3 <= input.len() && (1..3).all(|k| hex_val(input[i + k]).is_some()) {
out.push(hex_val(input[i + 1]).unwrap() * 16 + hex_val(input[i + 2]).unwrap());
i += 3;
continue;
}
}
let ch = match input[i..].chars().next() {
Some(c) => c,
None => break,
};
out.push(ch);
i += ch.len_utf8();
out.push(input[i]);
i += 1;
}
out
}
/// Annex B 的全局 `escape(string)`:除 ASCII 字母数字与 `@*_+-./` 外全部编码。
/// <=0xFF 字符用 `%XX`,其它按 UTF-16 code unit 用 `%uXXXX` 转义。
/// <=0xFF 码元用 `%XX`,其它按 UTF-16 码元用 `%uXXXX` 转义。
/// 参数按 `? ToString` 完整转换:Symbol 抛 TypeError,对象方法抛出的原生
/// 异常原样传播。
pub fn js_escape<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let input = match super::string_arg_full(vm, args) {
Ok(s) => s,
let input = match super::units_arg_full(vm, args) {
Ok(u) => u,
Err(exc) => return NativeResult::Err(exc),
};
NativeResult::Ok(vm.new_string(&escape_string(&input)))
NativeResult::Ok(vm.new_string_units_owned(escape_units(&input)))
}
/// Annex B 的全局 `unescape(string)`:解码 `escape` 生成的 `%XX`/`%uXXXX` 序列,
/// 支持 surrogate pair 合并;无法解析的序列原样保留。
/// Annex B 的全局 `unescape(string)`:解码 `escape` 生成的 `%XX`/`%uXXXX`
/// 序列;无法解析的序列原样保留。
/// 参数按 `? ToString` 完整转换:Symbol 抛 TypeError,对象方法抛出的原生
/// 异常原样传播。
pub fn js_unescape<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let input = match super::string_arg_full(vm, args) {
Ok(s) => s,
let input = match super::units_arg_full(vm, args) {
Ok(u) => u,
Err(exc) => return NativeResult::Err(exc),
};
NativeResult::Ok(vm.new_string(&unescape_string(&input)))
NativeResult::Ok(vm.new_string_units_owned(unescape_units(&input)))
}
#[cfg(test)]
mod tests {
use super::{escape_string, unescape_string};
use super::{escape_units, unescape_units};
fn units(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}
#[test]
fn escape_encodes_spaces_and_unicode() {
assert_eq!(escape_string("hello world"), "hello%20world");
assert_eq!(escape_string("AΩ你"), "A%u03A9%u4F60");
assert_eq!(escape_units(&units("hello world")), units("hello%20world"));
assert_eq!(escape_units(&units("AΩ你")), units("A%u03A9%u4F60"));
}
#[test]
fn unescape_decodes_percent_sequences() {
assert_eq!(unescape_string("hello%20world"), "hello world");
assert_eq!(unescape_string("%u03A9%u4F60"), "Ω你");
assert_eq!(unescape_units(&units("hello%20world")), units("hello world"));
assert_eq!(unescape_units(&units("%u03A9%u4F60")), units("Ω你"));
}
#[test]
fn unescape_keeps_invalid_sequences() {
assert_eq!(unescape_string("%uXYZ1%2G"), "%uXYZ1%2G");
assert_eq!(unescape_units(&units("%uXYZ1%2G")), units("%uXYZ1%2G"));
}
#[test]
fn escape_preserves_lone_surrogates() {
assert_eq!(escape_units(&[0xD834, 0xDF06]), units("%uD834%uDF06"));
}
#[test]
fn unescape_outputs_lone_surrogate_unit() {
assert_eq!(unescape_units(&units("%uD834")), vec![0xD834]);
}
}
@@ -540,6 +540,53 @@ fn date_set_year_four_digit_kept() {
assert_eq!(r.as_double() as i64, 2020);
}
// setYear 语义钉:this 值无效取 +0 基准(1970-01-01 全零)不早退、
// 结果经 TimeClip 包络、年份参数走完整 ToNumber(valueOf 通道、
// Symbol 抛 TypeError)。
#[test]
fn date_set_year_invalid_this_uses_plus_zero_base() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(NaN); d.setYear(71); d.getTime() == new Date(1971, 0).valueOf() ? 1 : 0",
)
.unwrap();
assert_eq!(r.as_int(), 1);
}
#[test]
fn date_set_year_time_clip_returns_nan() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(1970, 8, 14); var ts = d.setYear(275760); Number.isNaN(ts) && Number.isNaN(d.valueOf()) ? 1 : 0",
)
.unwrap();
assert_eq!(r.as_int(), 1);
}
#[test]
fn date_set_year_valueof_channel() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(0); d.setYear({valueOf: function() { return 20; }}); d.getFullYear()",
)
.unwrap();
assert_eq!(r.as_double() as i64, 1920);
}
#[test]
fn date_set_year_symbol_throws_type_error() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"var d = new Date(0); try { d.setYear(Symbol()); 'no' } catch (e) { e instanceof TypeError ? 'yes' : 'no' }",
)
.unwrap();
assert_eq!(str_val(&vm, r), "yes");
}
// UTC setter 无参数调用此前会索引缺失的参数寄存器(panic / 进程中止)。
// 缺失的主参数现在产出 NaN 并把日期置为 NaN。
#[test]