From c7ebcea305d0684426e771360b8efaf4b9988a12 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 17:41:05 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20with=20reject=20=E5=88=86=E6=94=AF?= =?UTF-8?q?=E8=B4=9F=E5=88=86=E9=87=8F=E5=85=88=E6=A0=A1=E9=AA=8C=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E5=86=8D=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 98 +++++++++++++++++-- crates/oxide_vm/src/bindings/bind_temporal.rs | 1 + .../oxide_vm/tests/builtins_temporal_tests.rs | 52 ++++++++++ 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index d2066f58..23cfef21 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -2321,6 +2321,9 @@ pub fn zoned_date_time_with(vm: &mut H, args: &[u8]) -> NativeResult merged.nanosecond.clamp(0.0, 999.0), ) } else { + // reject 语义须先做范围校验再截断:负分量或超上限分量立即抛错, + // 不允许 f64→u32 饱和把负值静默归 0 而逃过范围检查。 + let limits = [23.0, 59.0, 59.0, 999.0, 999.0, 999.0]; let values = [ merged.hour, merged.minute, @@ -2329,14 +2332,7 @@ pub fn zoned_date_time_with(vm: &mut H, args: &[u8]) -> NativeResult merged.microsecond, merged.nanosecond, ]; - if !valid_plain_time( - values[0] as u32, - values[1] as u32, - values[2] as u32, - values[3] as u32, - values[4] as u32, - values[5] as u32, - ) { + if values.iter().zip(limits.iter()).any(|(value, limit)| *value < 0.0 || *value > *limit) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); } (values[0], values[1], values[2], values[3], values[4], values[5]) @@ -4725,6 +4721,92 @@ pub fn plain_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult plain_time_get(vm, args, |_, _, _, _, _, ns| ns) } +/// `Temporal.PlainTime.prototype.with(temporalTimeLike [, options])`:替换 +/// receiver 的指定时间分量,返回新 PlainTime。 +/// +/// # 步骤 +/// 1. branding + ToTemporalTimeLike(非对象 / Temporal 实例 / calendar、 +/// timeZone 有定义 → TypeError;字段按字典序读 hour → microsecond → +/// millisecond → minute → nanosecond → second,非数字 → TypeError, +/// NaN/±Inf → RangeError)。 +/// 2. 分量全 undefined → TypeError。 +/// 3. options → overflow(缺省 constrain)。 +/// 4. RegulateTime:constrain 钳制;reject 先校验范围再截断(负 / 超界分量 +/// 立即抛 RangeError)。 +/// 5. 合并分量算午夜后纳秒,构造新 PlainTime。 +/// +/// # 边界与前提 +/// - 字段读取先于选项解析(options-wrong-type 先报字段错误,与 ZDT with 同口径)。 +/// - receiver 分量取自午夜后纳秒槽;partial 中 undefined 的分量保留 receiver 值。 +pub fn plain_time_with(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_time(vm, obj)); + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value)); + + // 字段读取按字典序,undefined 不覆盖;number-only 路径(Symbol/BigInt → + // TypeError,NaN/±Inf → RangeError)。 + let bag = unsafe { &*value.as_js_object_ptr() }; + let read = |vm: &mut H, name: &str| -> Result, JsValue> { + let raw = temporal_option_value(vm, bag, value, name)?; + if raw.is_undefined() { + Ok(None) + } else { + Ok(Some(temporal_number_component(vm, raw)?)) + } + }; + let hour = native_try!(read(vm, "hour")); + let microsecond = native_try!(read(vm, "microsecond")); + let millisecond = native_try!(read(vm, "millisecond")); + let minute = native_try!(read(vm, "minute")); + let nanosecond = native_try!(read(vm, "nanosecond")); + let second = native_try!(read(vm, "second")); + if [hour, microsecond, millisecond, minute, nanosecond, second].iter().all(|c| c.is_none()) { + return NativeResult::Err(crate::error::create_type_error(vm, "no properties present")); + } + + // options:字段读取完成后解析 overflow。 + let constrain = native_try!(temporal_overflow(vm, args)); + + // RegulateTime:partial 有定义的分量覆盖 receiver 分量。 + let (recv_hour, recv_minute, recv_second, recv_ms, recv_us, recv_ns) = + plain_time_components(get_double_prop(obj, 0)); + let merged = [ + hour.unwrap_or(recv_hour as f64), + minute.unwrap_or(recv_minute as f64), + second.unwrap_or(recv_second as f64), + millisecond.unwrap_or(recv_ms as f64), + microsecond.unwrap_or(recv_us as f64), + nanosecond.unwrap_or(recv_ns as f64), + ]; + let values = if constrain { + [ + merged[0].clamp(0.0, 23.0), + merged[1].clamp(0.0, 59.0), + merged[2].clamp(0.0, 59.0), + merged[3].clamp(0.0, 999.0), + merged[4].clamp(0.0, 999.0), + merged[5].clamp(0.0, 999.0), + ] + } else { + // reject 语义须先做范围校验再截断:负分量或超上限分量立即抛错, + // 不允许 f64→u32 饱和把负值静默归 0 而逃过范围检查。 + let limits = [23.0, 59.0, 59.0, 999.0, 999.0, 999.0]; + if merged.iter().zip(limits.iter()).any(|(v, limit)| *v < 0.0 || *v > *limit) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); + } + merged + }; + let total_ns = values[0] * 3_600_000_000_000.0 + + values[1] * 60_000_000_000.0 + + values[2] * 1_000_000_000.0 + + values[3] * 1_000_000.0 + + values[4] * 1_000.0 + + values[5]; + make_plain_time(vm, total_ns) +} + /// 按是否含秒与小数位格式化当日纳秒为 `HH:MM:SS[.frac]`。 /// /// # 边界与前提 diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index e51a0345..c2ffc51d 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -722,6 +722,7 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m ("add", oxide_builtins::temporal::plain_time_add:: as *const (), 1), ("subtract", oxide_builtins::temporal::plain_time_subtract:: as *const (), 1), ("round", oxide_builtins::temporal::plain_time_round:: as *const (), 1), + ("with", oxide_builtins::temporal::plain_time_with:: as *const (), 1), ], ); bind_well_known_data_property( diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 31f68219..348498f5 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -3843,6 +3843,58 @@ fn plain_time_ctor_negative_time_components_throw_range_error() { ); } +#[test] +fn plain_time_with_reject_invalid_components_throw_range_error() { + let mut vm = Vm::new(); + // 红形钉:with 的 reject 语义对越界分量先查范围——负值与上界越界均抛 + // RangeError(红形为 with 未绑定或负值被饱和吞掉时不抛/抛错类型不符); + // constrain 钳制臂与 undefined 字段不覆盖面同时锁定。 + let r = eval( + &mut vm, + "(() => { + const t = new Temporal.PlainTime(12, 34, 56, 123, 456, 789); + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const val = (fn) => { try { return String(fn()); } catch (e) { return e.constructor.name; } }; + return [ + kind(() => t.with({ hour: -1 }, { overflow: 'reject' })), + kind(() => t.with({ nanosecond: -1 }, { overflow: 'reject' })), + kind(() => t.with({ hour: 24 }, { overflow: 'reject' })), + kind(() => t.with({ minute: 60 }, { overflow: 'reject' })), + val(() => t.with({ hour: 25, minute: 90 }, { overflow: 'constrain' }).toString()), + val(() => t.with({ hour: 18, second: undefined }).toString()), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "RangeError|RangeError|RangeError|RangeError|23:59:56.123456789|18:34:56.123456789" + ); +} + +#[test] +fn zoned_date_time_with_reject_negative_time_component_throws_range_error() { + let mut vm = Vm::new(); + // 红形钉:zdt.with 的 reject 臂负合并分量须先查范围后转换(截断后为负的 + // 分量直接 RangeError),红形为负值经饱和归 0 逃过检查静默产出错位对象; + // constrain 臂钳制到 0 的规范语义同钉锁定。 + let r = eval( + &mut vm, + "(() => { + const z = new Temporal.ZonedDateTime(1577880000_000_000_000n, 'UTC'); + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const val = (fn) => { try { return String(fn()); } catch (e) { return e.constructor.name; } }; + return [ + kind(() => z.with({ hour: -1 }, { overflow: 'reject' })), + kind(() => z.with({ nanosecond: -1 }, { overflow: 'reject' })), + val(() => z.with({ hour: -1 }, { overflow: 'constrain' }).hour), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError|0"); +} + #[test] fn plain_date_time_ctor_undefined_time_components_default() { let mut vm = Vm::new();