From 87cbe4ad2ae4347167379e03324549cb9201d60e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 27 Sep 2026 21:09:35 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20Duration.round=20?= =?UTF-8?q?=E7=BA=AF=E6=97=B6=E9=97=B4=E8=B7=AF=E5=BE=84=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E4=B8=BA=E5=AD=98=E5=82=A8=E5=88=86=E9=87=8F?= =?UTF-8?q?=E5=8A=A0=E6=9D=83=E5=92=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oxide_builtins/src/temporal/duration.rs | 33 ++++++++----------- .../oxide_vm/tests/builtins_temporal_tests.rs | 27 +++++++++++++++ 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index 9c1581ef..8eb79325 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -306,7 +306,9 @@ fn duration_relative_to_date( if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) { return Ok(Some((i128::from(year), i128::from(month), i128::from(day)))); } - // ZonedDateTime-like(含 Z 或时区注解):按 instant 解析 + 注解时区反推墙钟日期。 + // 回退支:plain 首支解析失败的串(含 Z 的串或非法注解)。命名区当前全部 RangeError + // (canonical_time_zone 拒 IANA),可解析的带注解串已被 plain 首支接受。 + // 按 instant 解析 + 注解时区反推墙钟日期。 let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?; let offset_minutes = instant_time_zone_offset(&time_zone_id).unwrap_or(0); let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; @@ -651,7 +653,7 @@ pub fn duration_add(vm: &mut H, args: &[u8]) -> NativeResult { /// 4. 平衡后存储时间分量(含 days,𝔽 舍入)的纳秒加权和校验 2^53 秒上限。 /// /// # 边界与前提 -/// - 任一侧含日历单位抛 RangeError(本批无 relativeTo 支持)。 +/// - add/subtract 无 relativeTo 选项,任一侧含 years/months/weeks 即抛 RangeError。 /// - 全零与 nanosecond 分量和为 0 时直接返回零时长。 fn add_duration_values(vm: &mut H, receiver: &[f64; 10], other: &[f64; 10]) -> Result<[f64; 10], JsValue> { if receiver[..3].iter().any(|value| *value != 0.0) || other[..3].iter().any(|value| *value != 0.0) { @@ -1088,24 +1090,15 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { unit -= 1; } } - // 范围校验:对每个时间分量按纳秒刻度检查是否达到 2^53 秒上限。 - const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0; - const UNIT_SCALES: [f64; 7] = [ - 86_400_000_000_000.0, - 3_600_000_000_000.0, - 60_000_000_000.0, - 1_000_000_000.0, - 1_000_000.0, - 1_000.0, - 1.0, - ]; - for (index, scale) in (3..10).zip(UNIT_SCALES) { - if result[index] != 0.0 && result[index].abs() * scale >= MAX_TIME_NANOSECONDS { - return NativeResult::Err(crate::error::create_range_error( - vm, - "duration time fields are out of range", - )); - } + // 范围校验:判据作用在存储分量(平衡后 𝔽 舍入结果)的纳秒加权和上,与 + // add/subtract 同式;加权和覆盖逐分量检查的盲区(总和落在 [2^53 s, 2^53 s + + // 86400 s) 而各分量均低于上限的缝隙带)。 + const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000; + let Some(stored_ns) = duration_time_nanoseconds(&result) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); + }; + if stored_ns.abs() >= MAX_TIME_NANOSECONDS { + return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range")); } return make_duration(vm, result); } diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 348498f5..1294bf0a 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -2629,6 +2629,33 @@ fn duration_total_relative_zdt() { assert_eq!(r.as_double(), 1.0); } +#[test] +fn duration_round_weighted_sum_range_boundary() { + let mut vm = Vm::new(); + // round 纯时间路径范围判据按存储分量加权和(spec IsValidDuration),非逐分量: + // 输入 2^53 s − 30 s 合法,expand 舍到 minute 后 2^53 s + 28 s 越界 → RangeError + // (平衡后各分量均低于上限);负镜像同抛;2^53 s − 1 ns 输入舍到 day 得 + // 104249991374 天(2^53 s − 27392 s)→ 接受。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + kind(() => new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 2) + .round({ smallestUnit: 'minute', roundingMode: 'expand' })), + kind(() => new Temporal.Duration(0, 0, 0, -104249991374, -7, -36, -2) + .round({ smallestUnit: 'minute', roundingMode: 'expand' })), + kind(() => new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 31, 999, 999, 999) + .round({ smallestUnit: 'day' })), + new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 31, 999, 999, 999) + .round({ smallestUnit: 'day' }).toString(), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError|no|P104249991374D"); +} + // -- Temporal.PlainMonthDay / PlainYearMonth(E 批)-- #[test]