From 0d75e901ff14252a32088b9b0f6081bd2488f835 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 11 Aug 2026 13:11:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Temporal=20=E6=97=B6=E9=95=BF=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 200 ++++++++++++++---- crates/oxide_vm/src/bindings/bind_temporal.rs | 2 + .../oxide_vm/tests/builtins_temporal_tests.rs | 38 ++++ 3 files changed, 201 insertions(+), 39 deletions(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 7699c75a..581a70f8 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -264,7 +264,7 @@ fn parse_duration_string(input: &str) -> Option<[f64; 10]> { } _ => false, }; - if bytes.get(cursor) != Some(&b'P') { + if bytes.get(cursor).copied().map(|byte| byte.to_ascii_uppercase()) != Some(b'P') { return None; } cursor += 1; @@ -276,7 +276,7 @@ fn parse_duration_string(input: &str) -> Option<[f64; 10]> { let mut last_order = 0usize; let mut fraction_seen = false; while cursor < bytes.len() { - if bytes[cursor] == b'T' { + if bytes[cursor].eq_ignore_ascii_case(&b'T') { if in_time { return None; } @@ -312,7 +312,7 @@ fn parse_duration_string(input: &str) -> Option<[f64; 10]> { fraction = Some((numerator, 10_u64.pow(digits as u32))); } - let unit = *bytes.get(cursor)?; + let unit = bytes.get(cursor)?.to_ascii_uppercase(); cursor += 1; let (index, order, unit_nanos) = match (in_time, unit) { (false, b'Y') => (0, 1, None), @@ -324,7 +324,7 @@ fn parse_duration_string(input: &str) -> Option<[f64; 10]> { (true, b'S') => (6, 7, Some(1_000_000_000_u64)), _ => return None, }; - if order <= last_order || (fraction_seen && (whole != 0.0 || fraction.is_some_and(|f| f.0 != 0))) { + if order <= last_order || fraction_seen { return None; } last_order = order; @@ -363,46 +363,122 @@ fn parse_duration_string(input: &str) -> Option<[f64; 10]> { Some(values) } -fn duration_like_values(vm: &mut H, val: JsValue) -> Result<[f64; 10], JsValue> { - if val.is_string() { - return parse_duration_string(&to_string(val)) - .ok_or_else(|| crate::error::create_range_error(vm, "invalid duration string")); +const DURATION_FIELD_ORDER: [(&str, usize); 10] = [ + ("days", 3), + ("hours", 4), + ("microseconds", 8), + ("milliseconds", 7), + ("minutes", 5), + ("months", 1), + ("nanoseconds", 9), + ("seconds", 6), + ("weeks", 2), + ("years", 0), +]; + +fn duration_engine_error(vm: &mut H, error: &str) -> JsValue { + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_from_text(vm, error)) +} + +fn duration_field_number( + vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str, +) -> Result, JsValue> { + let key_val = vm.new_string(name); + let si = vm.property_key_si(key_val); + let raw = match vm.ordinary_get(obj, si, receiver) { + Ok(value) => value, + Err(error) => return Err(duration_engine_error(vm, &error)), + }; + if raw.is_undefined() { + return Ok(None); } + let primitive = match oxide_runtime_api::to_primitive(raw, oxide_runtime_api::ToPrimitiveHint::Number, vm) { + Ok(value) => value, + Err(error) => return Err(duration_engine_error(vm, &error)), + }; + if primitive.is_symbol() || primitive.is_bigint() { + return Err(crate::error::create_type_error(vm, "cannot convert duration field to number")); + } + Ok(Some(to_number(primitive))) +} + +fn duration_partial_values(vm: &mut H, val: JsValue) -> Result<[Option; 10], JsValue> { if !val.is_object() { - return Err(crate::error::create_type_error(vm, "cannot convert value to Duration")); + return Err(crate::error::create_type_error(vm, "duration-like value must be an object")); } let ptr = val.as_js_object_ptr(); if ptr.is_null() { - return Err(crate::error::create_type_error(vm, "cannot convert value to Duration")); + return Err(crate::error::create_type_error(vm, "duration-like value must be an object")); } let obj = unsafe { &*ptr }; - let names = [ - "years", - "months", - "weeks", - "days", - "hours", - "minutes", - "seconds", - "milliseconds", - "microseconds", - "nanoseconds", - ]; - let mut values = [0.0; 10]; - for (index, name) in names.iter().enumerate() { - let value = if obj.is_duration_obj() { - get_double_prop(obj, index) - } else { - read_prop_number(vm, obj, val, name) - }; - if value.is_nan() { - continue; - } - if !value.is_finite() || value.fract() != 0.0 { + if obj.is_duration_obj() { + return Ok(std::array::from_fn(|index| Some(get_double_prop(obj, index)))); + } + + let mut values = [None; 10]; + for (name, index) in DURATION_FIELD_ORDER { + values[index] = duration_field_number(vm, obj, val, name)?; + } + if values.iter().all(Option::is_none) { + return Err(crate::error::create_type_error(vm, "duration-like object has no duration fields")); + } + Ok(values) +} + +fn duration_component_integer(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 || value.abs() >= i128::MAX as f64 { + return None; + } + Some(value as i128) +} + +fn duration_time_nanoseconds(values: &[f64; 10]) -> Option { + const SCALES: [i128; 7] = + [86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; + let mut total = 0_i128; + for (value, scale) in values[3..].iter().zip(SCALES) { + total = total.checked_add(duration_component_integer(*value)?.checked_mul(scale)?)?; + } + Some(total) +} + +fn validate_duration_values(vm: &mut H, values: &[f64; 10]) -> Result<(), JsValue> { + let mut sign = 0_i8; + for value in values { + if duration_component_integer(*value).is_none() { return Err(crate::error::create_range_error(vm, "invalid duration")); } - values[index] = value; + if *value != 0.0 { + let current = if value.is_sign_negative() { -1 } else { 1 }; + if sign != 0 && sign != current { + return Err(crate::error::create_range_error(vm, "duration fields must have the same sign")); + } + sign = current; + } } + if values[..3].iter().any(|value| value.abs() > u32::MAX as f64) { + return Err(crate::error::create_range_error(vm, "duration date field is out of range")); + } + const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000; + let total = duration_time_nanoseconds(values) + .ok_or_else(|| crate::error::create_range_error(vm, "duration time fields are out of range"))?; + if total.abs() >= MAX_TIME_NANOSECONDS { + return Err(crate::error::create_range_error(vm, "duration time fields are out of range")); + } + Ok(()) +} + +fn duration_like_values(vm: &mut H, val: JsValue) -> Result<[f64; 10], JsValue> { + if val.is_string() { + let values = parse_duration_string(&to_string(val)) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid duration string"))?; + validate_duration_values(vm, &values)?; + return Ok(values); + } + let partial = duration_partial_values(vm, val)?; + let values = std::array::from_fn(|index| partial[index].unwrap_or(0.0)); + validate_duration_values(vm, &values)?; Ok(values) } @@ -426,6 +502,7 @@ pub fn duration_constructor(vm: &mut H, args: &[u8]) -> NativeResult } *value = number; } + native_try!(validate_duration_values(vm, &values)); make_duration(vm, values) } @@ -445,13 +522,18 @@ fn format_duration_number(value: f64) -> String { } fn format_duration_seconds(seconds: f64, milliseconds: f64, microseconds: f64, nanoseconds: f64) -> Option { - let subsecond = milliseconds * 1_000_000.0 + microseconds * 1_000.0 + nanoseconds; - if seconds == 0.0 && subsecond == 0.0 { + let total = duration_component_integer(seconds)? + .checked_mul(1_000_000_000)? + .checked_add(duration_component_integer(milliseconds)?.checked_mul(1_000_000)?)? + .checked_add(duration_component_integer(microseconds)?.checked_mul(1_000)?)? + .checked_add(duration_component_integer(nanoseconds)?)?; + if total == 0 { return None; } - let negative = seconds < 0.0 || (seconds == 0.0 && subsecond < 0.0); - let whole = seconds.abs() as i64; - let fraction = subsecond.abs() as u64; + let negative = total < 0; + let magnitude = total.abs(); + let whole = magnitude / 1_000_000_000; + let fraction = magnitude % 1_000_000_000; let mut result = format!("{}", whole); if fraction != 0 { let mut digits = format!("{fraction:09}"); @@ -467,6 +549,46 @@ fn format_duration_seconds(seconds: f64, milliseconds: f64, microseconds: f64, n Some(result) } +/// `Temporal.Duration.prototype.with(partial)`,以给定字段替换当前时长分量。 +pub fn duration_with(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_duration(vm, obj)); + let partial_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let partial = native_try!(duration_partial_values(vm, partial_value)); + let mut values = duration_values(obj); + for (index, value) in partial.into_iter().enumerate() { + if let Some(value) = value { + values[index] = value; + } + } + native_try!(validate_duration_values(vm, &values)); + make_duration(vm, values) +} + +/// `Temporal.Duration.prototype.total("seconds")`,汇总不含日历大单位的总秒数。 +pub fn duration_total(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_duration(vm, obj)); + let unit_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !unit_value.is_string() || !to_string(unit_value).eq_ignore_ascii_case("seconds") { + return NativeResult::Err(crate::error::create_range_error(vm, "only seconds total is supported")); + } + let values = duration_values(obj); + if values[..3].iter().any(|value| *value != 0.0) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "a relativeTo option is required for calendar units", + )); + } + let total = match duration_time_nanoseconds(&values) { + Some(value) => value, + None => return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")), + }; + NativeResult::Ok(JsValue::float(total as f64 / 1_000_000_000.0)) +} + /// `Temporal.Duration.prototype.toString()` 的 ISO 8601 表示。 pub fn duration_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index 7c9b8ea6..0e2961ca 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -338,6 +338,8 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m &[ ("abs", oxide_builtins::temporal::duration_abs:: as *const (), 0), ("negated", oxide_builtins::temporal::duration_negated:: as *const (), 0), + ("with", oxide_builtins::temporal::duration_with:: as *const (), 1), + ("total", oxide_builtins::temporal::duration_total:: as *const (), 1), ("toString", oxide_builtins::temporal::duration_to_string:: as *const (), 0), ("valueOf", oxide_builtins::temporal::duration_value_of:: as *const (), 0), ], diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 5c4295bf..2de29018 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -234,6 +234,44 @@ fn duration_constructor_exposes_components() { assert_eq!(str_val(&vm, r), "P1Y2M3W4D"); } +#[test] +fn duration_from_validates_property_bags_and_preserves_precision() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "try { Temporal.Duration.from({}) } catch (e) { e.constructor.name }").unwrap(); + assert_eq!(str_val(&vm, r), "TypeError"); + let r = eval( + &mut vm, + "try { Temporal.Duration.from({hours:1,minutes:-1}) } catch (e) { e.constructor.name }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + let r = eval( + &mut vm, + "Temporal.Duration.from({milliseconds:4503599627370497000,microseconds:4503599627370495000000}).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "PT9007199254740991.975424S"); +} + +#[test] +fn duration_from_propagates_getter_errors_and_checks_range() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "try { Temporal.Duration.from({get years(){throw new TypeError('sentinel')}}) } catch (e) { e.message }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "sentinel"); + let r = eval( + &mut vm, + "try { Temporal.Duration.from({seconds:9007199254740992}) } catch (e) { e.constructor.name }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + let r = eval(&mut vm, "Temporal.Duration.from('p1y1m1dt1h1m1s').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "P1Y1M1DT1H1M1S"); +} + #[test] fn plain_date_balances_duration_time_units_into_days() { let mut vm = Vm::new();