From a4cd722a30ee186eff7f5ab65e0f6d237a87815d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 9 Oct 2026 12:03:42 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20setYear=20=E5=AE=8C=E6=95=B4=E5=BC=BA?= =?UTF-8?q?=E8=BD=AC=E5=8A=A0=20TimeClip=20=E6=94=B6=E5=B0=BE=EF=BC=8Cesca?= =?UTF-8?q?pe/unescape=20=E7=A0=81=E5=85=83=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 38 ++--- Cargo.toml | 2 +- crates/oxide_builtins/src/date.rs | 53 +++--- crates/oxide_builtins/src/global.rs | 11 +- crates/oxide_builtins/src/global/annex_b.rs | 161 +++++++++---------- crates/oxide_vm/tests/builtins_date_tests.rs | 47 ++++++ 6 files changed, 177 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 600e99e7..1aaa3133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.101" +version = "0.8.102" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.101" +version = "0.8.102" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.101" +version = "0.8.102" dependencies = [ "ansi_term", "bumpalo", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.101" +version = "0.8.102" dependencies = [ "lru", "oxide_bytecode", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.101" +version = "0.8.102" dependencies = [ "num-bigint", "oxc_allocator", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.101" +version = "0.8.102" dependencies = [ "bumpalo", "dashmap", @@ -1319,7 +1319,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.101" +version = "0.8.102" dependencies = [ "tracing", "tracing-appender", @@ -1340,7 +1340,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1371,7 +1371,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.101" +version = "0.8.102" dependencies = [ "num-bigint", "num-traits", @@ -1384,7 +1384,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.101" +version = "0.8.102" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.101" +version = "0.8.102" dependencies = [ "bumpalo", "dashmap", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.101" +version = "0.8.102" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 010ad7d3..1a6bb881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.101" +version = "0.8.102" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/date.rs b/crates/oxide_builtins/src/date.rs index 3a85ae2a..abc87611 100644 --- a/crates/oxide_builtins/src/date.rs +++ b/crates/oxide_builtins/src/date.rs @@ -1018,37 +1018,40 @@ pub fn date_get_year(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(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 格式)。 diff --git a/crates/oxide_builtins/src/global.rs b/crates/oxide_builtins/src/global.rs index f8a327c6..5c7b15d0 100644 --- a/crates/oxide_builtins/src/global.rs +++ b/crates/oxide_builtins/src/global.rs @@ -51,14 +51,15 @@ pub fn global_is_finite(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(vm: &mut H, args: &[u8]) -> Result { +fn units_arg_full(vm: &mut H, args: &[u8]) -> Result, 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(vm: &mut H, args: &[u8]) -> Result Option { - std::str::from_utf8(slice).ok().and_then(|s| u16::from_str_radix(s, 16).ok()) +/// 单个码元的十六进制值(逐位严格匹配,不带符号位);非十六进制数字返回 None。 +fn hex_val(c: u16) -> Option { + 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 { + 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 { + 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(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(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 { + 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]); } } diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index 6e059b9d..0e9b1743 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -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]