mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: with reject 分支负分量先校验范围再截断
This commit is contained in:
@@ -2321,6 +2321,9 @@ pub fn zoned_date_time_with<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<Option<f64>, 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]`。
|
||||
///
|
||||
/// # 边界与前提
|
||||
|
||||
@@ -722,6 +722,7 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
||||
("add", oxide_builtins::temporal::plain_time_add::<crate::vm::Vm> as *const (), 1),
|
||||
("subtract", oxide_builtins::temporal::plain_time_subtract::<crate::vm::Vm> as *const (), 1),
|
||||
("round", oxide_builtins::temporal::plain_time_round::<crate::vm::Vm> as *const (), 1),
|
||||
("with", oxide_builtins::temporal::plain_time_with::<crate::vm::Vm> as *const (), 1),
|
||||
],
|
||||
);
|
||||
bind_well_known_data_property(
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user