diff --git a/Cargo.lock b/Cargo.lock index 1d41b1bb..67d6851f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.17.4" +version = "0.17.5" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.17.4" +version = "0.17.5" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.17.4" +version = "0.17.5" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.17.4" +version = "0.17.5" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.17.4" +version = "0.17.5" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.17.4" +version = "0.17.5" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.17.4" +version = "0.17.5" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.17.4" +version = "0.17.5" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.17.4" +version = "0.17.5" [[package]] name = "oxide_parser" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.17.4" +version = "0.17.5" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.17.4" +version = "0.17.5" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.17.4" +version = "0.17.5" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.17.4" +version = "0.17.5" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 70e04956..1e163b59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.17.4" +version = "0.17.5" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 9ee422a8..b36a5c09 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -977,46 +977,79 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { let mut visited = HashSet::new(); let mut output: Vec = Vec::new(); - let indent_level: usize = 0; - match jsvalue_to_json( - vm, - value, - &mut visited, - &mut output, + let ctx = StringifyCtx { + visited: &mut visited, + out: &mut output, replacer_fn, - replacer_whitelist.as_ref(), - &space, - indent_level, - ) { + replacer_whitelist: replacer_whitelist.as_ref(), + space: &space, + indent_level: 0, + }; + match jsvalue_to_json(vm, value, ctx) { Ok(()) => NativeResult::Ok(vm.new_string_units_owned(output)), Err(exc) => NativeResult::Err(exc), } } +/// JSON 序列化上下文:环检表、输出流、replacer 与缩进状态。 +/// 递归传递时经 [`StringifyCtx::child`] 派生下一缩进层。 +struct StringifyCtx<'data, 'w> { + visited: &'w mut HashSet<*const JsObject>, + out: &'w mut Vec, + replacer_fn: Option, + replacer_whitelist: Option<&'data Vec>, + space: &'data [u16], + indent_level: usize, +} + +impl<'data, 'w> StringifyCtx<'data, 'w> { + /// 派生同缩进层上下文:共享同一环检表与输出流,indent_level 不变 + /// (值位序列化入口传给对象/数组展开时用)。 + fn same(&mut self) -> StringifyCtx<'data, '_> { + StringifyCtx { + visited: &mut *self.visited, + out: &mut *self.out, + replacer_fn: self.replacer_fn, + replacer_whitelist: self.replacer_whitelist, + space: self.space, + indent_level: self.indent_level, + } + } + + /// 派生下一缩进层上下文:共享同一环检表与输出流,indent_level 加一 + /// (对象/数组展开内递归回值位序列化时用)。 + fn child(&mut self) -> StringifyCtx<'data, '_> { + StringifyCtx { + visited: &mut *self.visited, + out: &mut *self.out, + replacer_fn: self.replacer_fn, + replacer_whitelist: self.replacer_whitelist, + space: self.space, + indent_level: self.indent_level + 1, + } + } +} + /// 值位序列化(SerializeValue 及对象/数组展开)。toJSON 钩子不在本入口应用—— /// 唯一应用点在调用方(顶层 `json_stringify` 与 `stringify_object`/`stringify_array` /// 的属性循环内,Get 之后、replacer 之前),保证每值位恰一次且与 replacer 顺序 /// 符合规范。`Err` 携带的原始异常值(含环检 TypeError)原样上抛。 -#[allow(clippy::too_many_arguments)] -fn jsvalue_to_json( - vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut Vec, replacer_fn: Option, - replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, -) -> Result<(), JsValue> { +fn jsvalue_to_json(vm: &mut H, val: JsValue, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> { if val.is_null() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); } else if val.is_undefined() { } else if val.is_symbol() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); } else if val.is_bool() { - push_ascii(out, if val.as_bool() { "true" } else { "false" }); + push_ascii(ctx.out, if val.as_bool() { "true" } else { "false" }); } else if val.is_int() { - push_ascii(out, &val.as_int().to_string()); + push_ascii(ctx.out, &val.as_int().to_string()); } else if val.is_double() { let n = val.as_double(); if !n.is_finite() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); } else { - push_number(out, n); + push_number(ctx.out, n); } } else if val.is_bigint() { // SerializeJSONProperty 步 10:BigInt 无 JSON 文本形态,抛 TypeError。 @@ -1024,11 +1057,11 @@ fn jsvalue_to_json( } else if val.is_string() { // SAFETY: val 已确认是字符串值;单元视图按码单元流序列化(见 stringify_string_units)。 let units = unsafe { (*val.as_string_ptr()).units() }; - stringify_string_units(&units, out); + stringify_string_units(&units, ctx.out); } else if val.is_object() { let obj_ptr = val.as_js_object_ptr(); if obj_ptr.is_null() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); return Ok(()); } @@ -1040,12 +1073,12 @@ fn jsvalue_to_json( let raw_val = obj.get_prop_at(0); // SAFETY: rawJSON 属性构造期即写入字符串值。 let raw_text = unsafe { (*raw_val.as_string_ptr()).to_owned_string() }; - push_ascii(out, &raw_text); + push_ascii(ctx.out, &raw_text); return Ok(()); } } - if !visited.insert(obj_ptr as *const JsObject) { + if !ctx.visited.insert(obj_ptr as *const JsObject) { return Err(crate::error::create_type_error(vm, "Converting circular structure to JSON")); } @@ -1058,23 +1091,23 @@ fn jsvalue_to_json( let exc = vm .take_uncaught_value() .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); - visited.remove(&(obj_ptr as *const JsObject)); + ctx.visited.remove(&(obj_ptr as *const JsObject)); return Err(exc); } }; if n.is_finite() { - push_number(out, n); + push_number(ctx.out, n); } else { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); } - visited.remove(&(obj_ptr as *const JsObject)); + ctx.visited.remove(&(obj_ptr as *const JsObject)); return Ok(()); } // 步 4b:装箱 Boolean → [[BooleanData]]。 if obj.is_boolean_obj() { let b = obj.boxed_value(); - push_ascii(out, if b.is_bool() && b.as_bool() { "true" } else { "false" }); - visited.remove(&(obj_ptr as *const JsObject)); + push_ascii(ctx.out, if b.is_bool() && b.as_bool() { "true" } else { "false" }); + ctx.visited.remove(&(obj_ptr as *const JsObject)); return Ok(()); } // 步 4d:装箱 BigInt 无条件解包 [[BigIntData]],后续步 10 抛 TypeError。 @@ -1090,21 +1123,21 @@ fn jsvalue_to_json( let exc = vm .take_uncaught_value() .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); - visited.remove(&(obj_ptr as *const JsObject)); + ctx.visited.remove(&(obj_ptr as *const JsObject)); return Err(exc); } }; let units = vm.string_units(s); - stringify_string_units(&units, out); + stringify_string_units(&units, ctx.out); } else if obj.is_typed_array_obj() { - stringify_typed_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; + stringify_typed_array(vm, obj, ctx.same())?; } else if obj.is_array() { - stringify_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; + stringify_array(vm, obj, ctx.same())?; } else { - stringify_object(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; + stringify_object(vm, obj, ctx.same())?; } - visited.remove(&(obj_ptr as *const JsObject)); + ctx.visited.remove(&(obj_ptr as *const JsObject)); } Ok(()) } @@ -1153,17 +1186,13 @@ fn stringify_string_units(units: &[u16], out: &mut Vec) { push_ascii(out, "\""); } -#[allow(clippy::too_many_arguments)] -fn stringify_object( - vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, - replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, -) -> Result<(), JsValue> { - let has_space = !space.is_empty(); - push_ascii(out, "{"); +fn stringify_object(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> { + let has_space = !ctx.space.is_empty(); + push_ascii(ctx.out, "{"); // 键集:白名单给定时 K = P(列表序,不限自身可枚举,值读走 Get); // 无白名单走自身可枚举序(EnumerableOwnPropertyNames)。 - let entries: Vec<(Vec, u32, Option)> = if let Some(whitelist) = replacer_whitelist { + let entries: Vec<(Vec, u32, Option)> = if let Some(whitelist) = ctx.replacer_whitelist { whitelist .iter() .map(|name| { @@ -1198,7 +1227,7 @@ fn stringify_object( let val = call_to_json(vm, val, &units)?; // replacer 函数回调。 - let val = if let Some(replacer) = replacer_fn { + let val = if let Some(replacer) = ctx.replacer_fn { let key_val = vm.new_string_units_owned(units.clone()); match vm.call_function_sync(replacer, obj_val, &[key_val, val]) { Ok(v) => { @@ -1227,55 +1256,51 @@ fn stringify_object( } if !first { - push_ascii(out, ","); + push_ascii(ctx.out, ","); } first = false; if has_space { - push_ascii(out, "\n"); - for _ in 0..indent_level + 1 { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level + 1 { + ctx.out.extend_from_slice(ctx.space); } - stringify_string_units(&units, out); - push_ascii(out, ":"); - push_ascii(out, " "); + stringify_string_units(&units, ctx.out); + push_ascii(ctx.out, ":"); + push_ascii(ctx.out, " "); } else { - stringify_string_units(&units, out); - push_ascii(out, ":"); + stringify_string_units(&units, ctx.out); + push_ascii(ctx.out, ":"); } - jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; + jsvalue_to_json(vm, val, ctx.child())?; } if has_space && !first { - push_ascii(out, "\n"); - for _ in 0..indent_level { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level { + ctx.out.extend_from_slice(ctx.space); } } - push_ascii(out, "}"); + push_ascii(ctx.out, "}"); Ok(()) } -#[allow(clippy::too_many_arguments)] -fn stringify_array( - vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, - replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, -) -> Result<(), JsValue> { - let has_space = !space.is_empty(); - push_ascii(out, "["); +fn stringify_array(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> { + let has_space = !ctx.space.is_empty(); + push_ascii(ctx.out, "["); let obj_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); let len = obj.prop_count() as usize; for i in 0..len { if i > 0 { - push_ascii(out, ","); + push_ascii(ctx.out, ","); } if has_space { - push_ascii(out, "\n"); - for _ in 0..indent_level + 1 { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level + 1 { + ctx.out.extend_from_slice(ctx.space); } } @@ -1287,12 +1312,12 @@ fn stringify_array( let val = call_to_json(vm, val, &index_units)?; // replacer 函数回调。 - let val = if let Some(replacer) = replacer_fn { + let val = if let Some(replacer) = ctx.replacer_fn { let key_val = vm.new_string(&index_str); match vm.call_function_sync(replacer, obj_val, &[key_val, val]) { Ok(v) => { if v.is_undefined() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); continue; } v @@ -1313,39 +1338,35 @@ fn stringify_array( !ptr.is_null() && unsafe { (*ptr).is_function() } }; if is_function || val.is_undefined() { - push_ascii(out, "null"); + push_ascii(ctx.out, "null"); } else { - jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; + jsvalue_to_json(vm, val, ctx.child())?; } } if has_space && len > 0 { - push_ascii(out, "\n"); - for _ in 0..indent_level { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level { + ctx.out.extend_from_slice(ctx.space); } } - push_ascii(out, "]"); + push_ascii(ctx.out, "]"); Ok(()) } /// TypedArray 序列化:整数索引是可枚举自身属性(元素存于原生载荷,不在形状链), /// 逐元素按对象臂规范序 Get → toJSON → replacer 处理,输出形态与普通对象一致。 -#[allow(clippy::too_many_arguments)] -fn stringify_typed_array( - vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, - replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, -) -> Result<(), JsValue> { - let has_space = !space.is_empty(); +fn stringify_typed_array(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> { + let has_space = !ctx.space.is_empty(); let this_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); let view = crate::typed_array::get_typed_array_data(vm, this_val)?; // live 口径:buffer 收缩/detach 后按当前 live 长度枚举(detached 空对象形态)。 let len = crate::typed_array::ta_live_length(view); - push_ascii(out, "{"); + push_ascii(ctx.out, "{"); // 键集:白名单给定时 K = P(列表序,整数索引名读元素、越界名跳过、 // 非索引名经 Get 读命名键如 length);无白名单走升序元素序。 - let entries: Vec<(Vec, JsValue)> = if let Some(whitelist) = replacer_whitelist { + let entries: Vec<(Vec, JsValue)> = if let Some(whitelist) = ctx.replacer_whitelist { let mut list: Vec<(Vec, JsValue)> = Vec::new(); for name in whitelist { let si = vm.string_key_si(name); @@ -1377,7 +1398,7 @@ fn stringify_typed_array( let val = call_to_json(vm, val, &index_units)?; // replacer 函数回调。 - let val = if let Some(replacer) = replacer_fn { + let val = if let Some(replacer) = ctx.replacer_fn { let key_val = vm.new_string_units_owned(index_units.clone()); match vm.call_function_sync(replacer, this_val, &[key_val, val]) { Ok(v) => { @@ -1406,32 +1427,32 @@ fn stringify_typed_array( } if !first { - push_ascii(out, ","); + push_ascii(ctx.out, ","); } first = false; if has_space { - push_ascii(out, "\n"); - for _ in 0..indent_level + 1 { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level + 1 { + ctx.out.extend_from_slice(ctx.space); } - stringify_string_units(&index_units, out); - push_ascii(out, ":"); - push_ascii(out, " "); + stringify_string_units(&index_units, ctx.out); + push_ascii(ctx.out, ":"); + push_ascii(ctx.out, " "); } else { - stringify_string_units(&index_units, out); - push_ascii(out, ":"); + stringify_string_units(&index_units, ctx.out); + push_ascii(ctx.out, ":"); } - jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; + jsvalue_to_json(vm, val, ctx.child())?; } if has_space && !first { - push_ascii(out, "\n"); - for _ in 0..indent_level { - out.extend_from_slice(space); + push_ascii(ctx.out, "\n"); + for _ in 0..ctx.indent_level { + ctx.out.extend_from_slice(ctx.space); } } - push_ascii(out, "}"); + push_ascii(ctx.out, "}"); Ok(()) } diff --git a/crates/oxide_builtins/src/ta_codec.rs b/crates/oxide_builtins/src/ta_codec.rs index 04495c1d..d773ae33 100644 --- a/crates/oxide_builtins/src/ta_codec.rs +++ b/crates/oxide_builtins/src/ta_codec.rs @@ -26,6 +26,14 @@ pub enum LastChunkHandling { StopBeforePartial, } +/// FromBase64 解码策略:字母表、尾块处理与长度上限三项。 +#[derive(Debug, Clone, Copy)] +pub struct DecodeConfig { + pub alphabet: Base64Alphabet, + pub handling: LastChunkHandling, + pub max_len: Option, +} + const B64_ALPHABET_STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const B64_ALPHABET_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; @@ -62,9 +70,9 @@ fn base64_value(unit: u16, alphabet: Base64Alphabet) -> Option { /// - `chunk` 恰 4 字符;调用方保证只在此处解码满块。 /// - 返回 `(24 位值, 解码字节数 1-3)`:24 位值按 4 槽位左对齐(padding 槽补 0), /// 字节数 = 3 - padding 数。 -fn decode_full_chunk( - chunk: [u16; 4], alphabet: Base64Alphabet, handling: LastChunkHandling, -) -> Result<(u32, usize), DecodeError> { +fn decode_full_chunk(chunk: [u16; 4], config: DecodeConfig) -> Result<(u32, usize), DecodeError> { + let alphabet = config.alphabet; + let handling = config.handling; let pad = match (chunk[2] == 0x3d, chunk[3] == 0x3d) { (false, false) => 0, (false, true) => 1, @@ -108,11 +116,13 @@ fn write_chunk_bytes(write: &mut dyn FnMut(usize, u8), written: usize, bits: u32 /// `clen - 1` 字节(2 字符 1 字节、3 字符 2 字节)并写出。 /// - 成功 `read`:解出字节的 loose 路径取块后消费位置;各"停在块前"路径 /// (sbp、余量不足)取块前位置 `chunk_start`。 -#[allow(clippy::too_many_arguments)] fn finish_partial( - chunk: [u16; 4], clen: usize, alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, - write: &mut dyn FnMut(usize, u8), mut written: usize, chunk_start: usize, read: usize, + chunk: [u16; 4], clen: usize, config: DecodeConfig, write: &mut dyn FnMut(usize, u8), mut written: usize, + chunk_start: usize, read: usize, ) -> Result<(usize, usize), DecodeError> { + let alphabet = config.alphabet; + let handling = config.handling; + let max_len = config.max_len; if (0..clen).any(|i| chunk[i] == 0x3d) { let trailing = (0..clen).rev().take_while(|&i| chunk[i] == 0x3d).count(); if clen == 3 && trailing == 1 && handling == LastChunkHandling::StopBeforePartial { @@ -166,9 +176,9 @@ fn finish_partial( /// - `Ok((read, written))` 中 `read` 为成功语义下的消费位置(含跳过空白, /// 停止路径为块前位置)。 pub fn decode_base64( - units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, - write: &mut dyn FnMut(usize, u8), + units: &[u16], config: DecodeConfig, write: &mut dyn FnMut(usize, u8), ) -> Result<(usize, usize), DecodeError> { + let max_len = config.max_len; if max_len == Some(0) { return Ok((0, 0)); } @@ -197,7 +207,7 @@ pub fn decode_base64( return Err(DecodeError); } } - let (bits, count) = decode_full_chunk(chunk, alphabet, handling)?; + let (bits, count) = decode_full_chunk(chunk, config)?; if let Some(m) = max_len { if written + count > m { return Ok((chunk_start, written)); @@ -211,7 +221,7 @@ pub fn decode_base64( } } 0 => return Ok((read, written)), - _ => return finish_partial(chunk, clen, alphabet, handling, max_len, write, written, chunk_start, read), + _ => return finish_partial(chunk, clen, config, write, written, chunk_start, read), } } } @@ -302,8 +312,16 @@ mod tests { units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, ) -> Result<(usize, Vec), DecodeError> { let mut bytes = Vec::new(); - decode_base64(units, alphabet, handling, max_len, &mut |_, b| bytes.push(b)) - .map(|(r, w)| (r, bytes.into_iter().take(w).collect())) + decode_base64( + units, + DecodeConfig { + alphabet, + handling, + max_len, + }, + &mut |_, b| bytes.push(b), + ) + .map(|(r, w)| (r, bytes.into_iter().take(w).collect())) } fn decode_hex_full(units: &[u16], max_len: Option) -> Result<(usize, Vec), DecodeError> { @@ -475,9 +493,11 @@ mod tests { let mut out = Vec::new(); let r = decode_base64( &units("MjYyZm.9v"), - Base64Alphabet::Standard, - LastChunkHandling::Loose, - None, + DecodeConfig { + alphabet: Base64Alphabet::Standard, + handling: LastChunkHandling::Loose, + max_len: None, + }, &mut |i, b| out.insert(i, b), ); assert!(r.is_err()); @@ -486,9 +506,11 @@ mod tests { let mut out = Vec::new(); let r = decode_base64( &units("MjYyZg==="), - Base64Alphabet::Standard, - LastChunkHandling::Loose, - None, + DecodeConfig { + alphabet: Base64Alphabet::Standard, + handling: LastChunkHandling::Loose, + max_len: None, + }, &mut |i, b| out.insert(i, b), ); assert!(r.is_err()); diff --git a/crates/oxide_builtins/src/temporal/difference.rs b/crates/oxide_builtins/src/temporal/difference.rs index cb30ef9f..5e281dd5 100644 --- a/crates/oxide_builtins/src/temporal/difference.rs +++ b/crates/oxide_builtins/src/temporal/difference.rs @@ -86,6 +86,21 @@ pub(crate) struct InternalDuration { pub(crate) time_ns: i128, } +/// ISO 日期时间:日期三元(年/月/日)加时间纳秒(不含日分量)。 +#[derive(Clone, Copy)] +pub(crate) struct IsoDateTime { + pub(crate) date: (i128, i128, i128), + pub(crate) time_ns: i128, +} + +/// 取整规格:增量、单位与舍入模式(吸收取整三参)。 +#[derive(Clone, Copy)] +pub(crate) struct RoundingSpec { + pub(crate) increment: i128, + pub(crate) unit: usize, + pub(crate) mode: InstantRoundingMode, +} + impl InternalDuration { /// 转十分量数组:时间部分按向零截断分解为时/分/秒/毫秒/微秒/纳秒(与时长同号)。 pub(crate) fn to_values(&self) -> [f64; 10] { @@ -394,11 +409,11 @@ pub(crate) struct NudgeCalendarResult { /// /// `epoch_of_date` 是窗口端点日期的纪元换算闭包:plain 面传墙钟换算(行为逐字节 /// 不变),zoned 面传 GetEpochNanosecondsFor(compatible) 换算。 -#[allow(clippy::too_many_arguments)] pub(crate) fn nudge_to_calendar_unit( - sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), increment: i128, - unit: usize, mode: InstantRoundingMode, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result, + sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), + spec: RoundingSpec, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result, ) -> Result { + let RoundingSpec { increment, unit, mode } = spec; let epoch_of = |dur: &[f64; 10]| -> Result { if date_duration_sign(dur) == 0 { return Ok(origin_epoch); @@ -744,12 +759,10 @@ pub(crate) fn parse_year_month_difference_settings( /// 的舍入模式并在最后整体取反(不调换两端,调换会改变 0.5 边界所在的年长)。 /// no_rounding = 单位索引空间中"最细单位"的索引:该单位且 increment=1 时 /// 舍入为恒等变换,跳过 nudge(边界处避免窗口端点的无效范围抛错)。 -#[allow(clippy::too_many_arguments)] pub(crate) fn difference_core( - vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, - settings: DifferenceSettings, since: bool, no_rounding: usize, + vm: &mut H, start: IsoDateTime, end: IsoDateTime, settings: DifferenceSettings, since: bool, no_rounding: usize, ) -> NativeResult { - let values = match nudge_iso_difference(vm, start, start_time_ns, end, end_time_ns, settings, since, no_rounding) { + let values = match nudge_iso_difference(vm, start, end, settings, since, no_rounding) { Ok(values) => values, Err(error) => return NativeResult::Err(error), }; @@ -769,10 +782,8 @@ pub(crate) fn difference_core( /// - Duration.round 的日历单位路径与本函数共用:round 把 duration 应用 relativeTo 后 /// 以同样的 start/end 差输入本函数,输出即 balance 后的舍入结果。 /// - 时间分量为带符号 i128;start/end 日与时间各自独立,符号由差值推导。 -#[allow(clippy::too_many_arguments)] pub(crate) fn nudge_iso_difference( - vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, - settings: DifferenceSettings, since: bool, no_rounding: usize, + vm: &mut H, start: IsoDateTime, end: IsoDateTime, settings: DifferenceSettings, since: bool, no_rounding: usize, ) -> Result<[f64; 10], JsValue> { let DifferenceSettings { largest_index, @@ -794,17 +805,17 @@ pub(crate) fn nudge_iso_difference( // 规范语义:internal = other - receiver;since 用 NegateRoundingMode 的舍入模式,最后整体取反。 // 不调换两端(调换会改变 0.5 边界所在的年长,破坏对称性)。 - let origin_epoch = days_from_civil(start.0, start.1, start.2) * DAY_NS + start_time_ns; - let dest_epoch = days_from_civil(end.0, end.1, end.2) * DAY_NS + end_time_ns; + let origin_epoch = days_from_civil(start.date.0, start.date.1, start.date.2) * DAY_NS + start.time_ns; + let dest_epoch = days_from_civil(end.date.0, end.date.1, end.date.2) * DAY_NS + end.time_ns; - let date1 = start; - let time1_ns = start_time_ns; + let date1 = start.date; + let time1_ns = start.time_ns; // plain 面纪元换算闭包:墙钟(日 × 24h + 时间),行为与旧内联算法逐字节一致。 let epoch_of_date = |date: (i128, i128, i128)| -> Result { Ok(days_from_civil(date.0, date.1, date.2) * DAY_NS + time1_ns) }; - let mut date2 = end; - let mut time_ns = end_time_ns - time1_ns; + let mut date2 = end.date; + let mut time_ns = end.time_ns - time1_ns; let time_sign = if time_ns > 0 { 1_i128 } else if time_ns < 0 { @@ -906,9 +917,11 @@ pub(crate) fn nudge_iso_difference( origin_epoch, dest_epoch, date1, - increment, - smallest_index, - mode, + RoundingSpec { + increment, + unit: smallest_index, + mode, + }, &epoch_of_date, ) { Ok(result) => result, diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index fa0f384b..9a6d14ca 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -15,14 +15,14 @@ use super::common::{ use super::difference::{ add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso, nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings, - InternalDuration, DAY_NS, MAX_ISO_DAY, + IsoDateTime, InternalDuration, DAY_NS, MAX_ISO_DAY, }; use super::instant::{instant_rounding_mode, InstantRoundingMode}; use super::time_zone::zone_offset_seconds; use super::zoned_date_time::{ add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm, disambiguate_possible_epoch_nanoseconds, extract_time_zone_annotation, get_possible_epoch_nanoseconds, - map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts, + map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts, ZonedPair, }; use super::{make_duration, parse_plain_date_time_string}; @@ -708,9 +708,13 @@ pub fn duration_total(vm: &mut H, args: &[u8]) -> NativeResult { // Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithTotal(门与单位无关)。 if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date { let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true)); - let total = native_try!(difference_zoned_datetime_with_total_vm( - vm, *epoch_ns, target, time_zone, calendar, unit_index - )); + let pair = ZonedPair { + ns1: *epoch_ns, + ns2: target, + time_zone, + calendar, + }; + let total = native_try!(difference_zoned_datetime_with_total_vm(vm, pair, unit_index)); return NativeResult::Ok(JsValue::float(total)); } @@ -1369,17 +1373,19 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { // Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithRounding(门与单位无关)。 if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date { let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true)); - let internal = native_try!(difference_zoned_datetime_with_rounding_vm( - vm, - *epoch_ns, - target, + let pair = ZonedPair { + ns1: *epoch_ns, + ns2: target, time_zone, calendar, - largest, - increment, + }; + let settings = DifferenceSettings { + largest_index: largest, smallest_index, + increment, mode, - )); + }; + let internal = native_try!(difference_zoned_datetime_with_rounding_vm(vm, pair, settings)); // largestUnit 为日期类时降为 hour。 let largest = if largest <= 3 { 4 } else { largest }; let result = internal_duration_to_values(&internal, largest); @@ -1460,7 +1466,15 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { increment, mode, }; - let result = match nudge_iso_difference(vm, rel, 0, end, target_time, settings, false, 9) { + let start = IsoDateTime { + date: rel, + time_ns: 0, + }; + let end_dt = IsoDateTime { + date: end, + time_ns: target_time, + }; + let result = match nudge_iso_difference(vm, start, end_dt, settings, false, 9) { Ok(values) => values, Err(error) => return NativeResult::Err(error), }; diff --git a/crates/oxide_builtins/src/temporal/plain_date.rs b/crates/oxide_builtins/src/temporal/plain_date.rs index 1f6c8870..bcd4fa91 100644 --- a/crates/oxide_builtins/src/temporal/plain_date.rs +++ b/crates/oxide_builtins/src/temporal/plain_date.rs @@ -11,7 +11,7 @@ use super::common::{ is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj, temporal_calendar_id_strict, temporal_number_component, temporal_overflow, zoned_date_time_plain_parts, }; -use super::difference::{difference_core, parse_difference_settings}; +use super::difference::{difference_core, parse_difference_settings, IsoDateTime}; use super::duration::duration_like_values; use super::make_plain_date; @@ -623,10 +623,14 @@ pub fn plain_date_until(vm: &mut H, args: &[u8]) -> NativeResult { }; difference_core( vm, - (i128::from(start.0), i128::from(start.1), i128::from(start.2)), - 0, - (i128::from(other.0), i128::from(other.1), i128::from(other.2)), - 0, + IsoDateTime { + date: (i128::from(start.0), i128::from(start.1), i128::from(start.2)), + time_ns: 0, + }, + IsoDateTime { + date: (i128::from(other.0), i128::from(other.1), i128::from(other.2)), + time_ns: 0, + }, settings, false, 9, @@ -651,10 +655,14 @@ pub fn plain_date_since(vm: &mut H, args: &[u8]) -> NativeResult { }; difference_core( vm, - (i128::from(start.0), i128::from(start.1), i128::from(start.2)), - 0, - (i128::from(other.0), i128::from(other.1), i128::from(other.2)), - 0, + IsoDateTime { + date: (i128::from(start.0), i128::from(start.1), i128::from(start.2)), + time_ns: 0, + }, + IsoDateTime { + date: (i128::from(other.0), i128::from(other.1), i128::from(other.2)), + time_ns: 0, + }, settings, true, 9, diff --git a/crates/oxide_builtins/src/temporal/plain_date_time.rs b/crates/oxide_builtins/src/temporal/plain_date_time.rs index e108f6f2..fa4270fe 100644 --- a/crates/oxide_builtins/src/temporal/plain_date_time.rs +++ b/crates/oxide_builtins/src/temporal/plain_date_time.rs @@ -14,7 +14,7 @@ use super::{ parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, receiver_obj, round_instant_ns, temporal_calendar_id_strict, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, valid_iso_date, valid_plain_date_time_range, valid_plain_time, zoned_date_time_plain_parts, - FractionalSecondDigitsInput, + FractionalSecondDigitsInput, IsoDateTime, }; // ───────────────────── PlainDateTime 基础方法 ───────────────────── @@ -589,10 +589,14 @@ fn plain_date_time_difference(vm: &mut H, args: &[u8], since: bool) - }; difference_core( vm, - (i128::from(sy), i128::from(sm), i128::from(sd)), - st as i128, - (i128::from(oy), i128::from(om), i128::from(od)), - ot as i128, + IsoDateTime { + date: (i128::from(sy), i128::from(sm), i128::from(sd)), + time_ns: st as i128, + }, + IsoDateTime { + date: (i128::from(oy), i128::from(om), i128::from(od)), + time_ns: ot as i128, + }, settings, since, 9, diff --git a/crates/oxide_builtins/src/temporal/plain_time.rs b/crates/oxide_builtins/src/temporal/plain_time.rs index a61d0d2a..1be7832d 100644 --- a/crates/oxide_builtins/src/temporal/plain_time.rs +++ b/crates/oxide_builtins/src/temporal/plain_time.rs @@ -11,7 +11,7 @@ use super::{ make_plain_time, parse_any_offset_seconds, parse_difference_settings, parse_fractional_second_digits, parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns, temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value, - temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, + temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, IsoDateTime, }; /// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。 @@ -701,7 +701,20 @@ fn plain_time_difference(vm: &mut H, args: &[u8], since: bool) -> Nat Ok(settings) => settings, Err(error) => return NativeResult::Err(error), }; - difference_core(vm, (0, 0, 0), receiver_ns as i128, (0, 0, 0), other_ns as i128, settings, since, 9) + difference_core( + vm, + IsoDateTime { + date: (0, 0, 0), + time_ns: receiver_ns as i128, + }, + IsoDateTime { + date: (0, 0, 0), + time_ns: other_ns as i128, + }, + settings, + since, + 9, + ) } /// `Temporal.PlainTime.prototype.until(other, options)`。 diff --git a/crates/oxide_builtins/src/temporal/plain_year_month.rs b/crates/oxide_builtins/src/temporal/plain_year_month.rs index 44042fda..b741c538 100644 --- a/crates/oxide_builtins/src/temporal/plain_year_month.rs +++ b/crates/oxide_builtins/src/temporal/plain_year_month.rs @@ -13,7 +13,7 @@ use super::{ read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, temporal_constructor_calendar_id, temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow, temporal_to_show_calendar, validate_month_day_annotation_suffix, validate_month_day_time_suffix, ShowCalendar, - MAX_ISO_DAY, + IsoDateTime, MAX_ISO_DAY, }; /// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。 @@ -884,5 +884,18 @@ fn plain_year_month_difference(vm: &mut H, args: &[u8], since: bool) { return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range")); } - difference_core(vm, (ry, rm, 1), 0, (i128::from(oy), i128::from(om), 1), 0, settings, since, 1) + difference_core( + vm, + IsoDateTime { + date: (ry, rm, 1), + time_ns: 0, + }, + IsoDateTime { + date: (i128::from(oy), i128::from(om), 1), + time_ns: 0, + }, + settings, + since, + 1, + ) } diff --git a/crates/oxide_builtins/src/temporal/tests.rs b/crates/oxide_builtins/src/temporal/tests.rs index c411cefa..a9002331 100644 --- a/crates/oxide_builtins/src/temporal/tests.rs +++ b/crates/oxide_builtins/src/temporal/tests.rs @@ -104,20 +104,79 @@ fn format_time_zone_annotation_normalizes_offset() { #[test] fn format_zoned_date_time_iso_annotations() { // 偏移/时区/日历注解各取值组合,验证段序 `{offset}[{tz}][{ca}]` 与 critical 前缀。 - let base = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "auto"); + let base = format_zoned_date_time_iso( + 0, + 3600, + "+01:00", + "iso8601", + true, + None, + FormatNames { + offset_name: "auto", + time_zone_name: "auto", + calendar_name: "auto", + }, + ); assert_eq!(base, Some("1970-01-01T01:00:00+01:00[+01:00]".to_string())); // offset never 省略偏移段,时区注解仍显示。 - let no_offset = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "never", "auto", "auto"); + let no_offset = format_zoned_date_time_iso( + 0, + 3600, + "+01:00", + "iso8601", + true, + None, + FormatNames { + offset_name: "never", + time_zone_name: "auto", + calendar_name: "auto", + }, + ); assert_eq!(no_offset, Some("1970-01-01T01:00:00[+01:00]".to_string())); // calendarName always 追加日历注解。 - let ca_always = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "always"); + let ca_always = format_zoned_date_time_iso( + 0, + 3600, + "+01:00", + "iso8601", + true, + None, + FormatNames { + offset_name: "auto", + time_zone_name: "auto", + calendar_name: "always", + }, + ); assert_eq!(ca_always, Some("1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]".to_string())); // timeZoneName/calendarName critical 均 `!` 置于括号内。 - let critical = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "critical", "critical"); + let critical = format_zoned_date_time_iso( + 0, + 3600, + "+01:00", + "iso8601", + true, + None, + FormatNames { + offset_name: "auto", + time_zone_name: "critical", + calendar_name: "critical", + }, + ); assert_eq!(critical, Some("1970-01-01T01:00:00+01:00[!+01:00][!u-ca=iso8601]".to_string())); // offset critical 偏移段前加 !。 - let offset_critical = - format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "critical", "auto", "auto"); + let offset_critical = format_zoned_date_time_iso( + 0, + 3600, + "+01:00", + "iso8601", + true, + None, + FormatNames { + offset_name: "critical", + time_zone_name: "auto", + calendar_name: "auto", + }, + ); assert_eq!(offset_critical, Some("1970-01-01T01:00:00!+01:00[+01:00]".to_string())); } @@ -125,7 +184,19 @@ fn format_zoned_date_time_iso_annotations() { fn format_zoned_date_time_iso_epoch_rounding_cross_midnight() { // 2000-01-01 前 1ns 已舍入到 2000-01-01 整点,8 位小数输出。 let rounded = round_instant_ns(946_684_799_999_999_999, 100, InstantRoundingMode::HalfExpand).unwrap(); - let output = format_zoned_date_time_iso(rounded, 0, "UTC", "iso8601", true, Some(8), "auto", "auto", "auto"); + let output = format_zoned_date_time_iso( + rounded, + 0, + "UTC", + "iso8601", + true, + Some(8), + FormatNames { + offset_name: "auto", + time_zone_name: "auto", + calendar_name: "auto", + }, + ); assert_eq!(output, Some("2000-01-01T00:00:00.00000000+00:00[UTC]".to_string())); } @@ -429,7 +500,19 @@ fn sub_minute_offset_getter_branch() { assert_eq!(format_offset_seconds_text(2670), "+00:44:30"); assert_eq!(format_offset_seconds_text(3600), "+01:00"); assert_eq!(format_time_zone_annotation("-00:44:30", false), "[-00:44:30]"); - let iso = format_zoned_date_time_iso(0, -2670, "-00:44:30", "iso8601", true, None, "auto", "auto", "auto"); + let iso = format_zoned_date_time_iso( + 0, + -2670, + "-00:44:30", + "iso8601", + true, + None, + FormatNames { + offset_name: "auto", + time_zone_name: "auto", + calendar_name: "auto", + }, + ); assert_eq!(iso, Some("1969-12-31T23:15:30-00:44:30[-00:44:30]".to_string())); assert_eq!( format_instant_iso(0, Some(-2670), true, None), @@ -614,10 +697,13 @@ fn calendar_date_add_iso_constrain_semantics() { use super::zoned_date_time::{calendar_date_add_iso, AddZonedError}; // 月末截断:constrain 钳到目标月末,reject 抛 DayOutOfRange。 - assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, true), Ok((2025, 2, 28))); - assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, false), Err(AddZonedError::DayOutOfRange)); + assert_eq!(calendar_date_add_iso((2025, 1, 31), [0, 1, 0, 0], true), Ok((2025, 2, 28))); + assert_eq!( + calendar_date_add_iso((2025, 1, 31), [0, 1, 0, 0], false), + Err(AddZonedError::DayOutOfRange) + ); // 闰年二月:constrain 钳到 29。 - assert_eq!(calendar_date_add_iso((2024, 1, 31), 0, 1, 0, 0, true), Ok((2024, 2, 29))); + assert_eq!(calendar_date_add_iso((2024, 1, 31), [0, 1, 0, 0], true), Ok((2024, 2, 29))); } #[test] @@ -627,25 +713,61 @@ fn zoned_difference_day_correction() { // 同日纯时间差:无日期部分,时间部分为纳秒差。 let ns1 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 43_200_000_000_000).unwrap(); let ns2 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 66_600_000_000_000).unwrap(); - let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap(); + let d = difference_zoned_datetime( + ZonedPair { + ns1, + ns2, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]); assert_eq!(d.time_ns, 23_400_000_000_000); // 跨 DST 间隙日(23 小时):墙历时刻相同,日期部分 1 天、时间部分 0。 let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap(); let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap(); - let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap(); + let d = difference_zoned_datetime( + ZonedPair { + ns1, + ns2, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert_eq!(d.date, [0.0, 0.0, 0.0, 1.0]); assert_eq!(d.time_ns, 0); // dayCorrection 循环推进:12 小时跨间隙日,日期部分 0、时间部分 12 小时。 let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap(); - let d = difference_zoned_datetime(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap(); + let d = difference_zoned_datetime( + ZonedPair { + ns1, + ns2: ns2b, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]); assert_eq!(d.time_ns, 43_200_000_000_000); // 反向(sign 为正):日期部分为负一天、时间部分 0。 - let d = difference_zoned_datetime(ns2, ns1, "America/New_York", "iso8601", 3).unwrap(); + let d = difference_zoned_datetime( + ZonedPair { + ns1: ns2, + ns2: ns1, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert_eq!(d.date, [0.0, 0.0, 0.0, -1.0]); assert_eq!(d.time_ns, 0); } @@ -657,12 +779,30 @@ fn zoned_total_dst_fraction() { // 整日差(23 小时间隙日):总量 1。 let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap(); let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap(); - let total = difference_zoned_datetime_with_total(ns1, ns2, "America/New_York", "iso8601", 3).unwrap(); + let total = difference_zoned_datetime_with_total( + ZonedPair { + ns1, + ns2, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert!((total - 1.0).abs() < 1e-12); // 12 小时跨 23 小时间隙日:总量 12/23(按实际日长折算,非 24 小时)。 let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap(); - let total = difference_zoned_datetime_with_total(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap(); + let total = difference_zoned_datetime_with_total( + ZonedPair { + ns1, + ns2: ns2b, + time_zone: "America/New_York", + calendar: "iso8601", + }, + 3, + ) + .unwrap(); assert!((total - 12.0 / 23.0).abs() < 1e-12); } @@ -681,14 +821,18 @@ fn zoned_round_nudge_to_zoned_time() { let p25h = [0.0_f64, 0.0, 0.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p25h, true).unwrap(); let internal = difference_zoned_datetime_with_rounding( - origin, - target, - "Pacific/Apia", - "iso8601", - 4, - 1, - 4, - InstantRoundingMode::Trunc, + ZonedPair { + ns1: origin, + ns2: target, + time_zone: "Pacific/Apia", + calendar: "iso8601", + }, + DifferenceSettings { + largest_index: 4, + smallest_index: 4, + increment: 1, + mode: InstantRoundingMode::Trunc, + }, ) .unwrap(); let values = internal_duration_to_values(&internal, 4); @@ -698,14 +842,18 @@ fn zoned_round_nudge_to_zoned_time() { let p1dt26h = [0.0_f64, 0.0, 0.0, 1.0, 26.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p1dt26h, true).unwrap(); let internal = difference_zoned_datetime_with_rounding( - origin, - target, - "Pacific/Apia", - "iso8601", - 3, - 1, - 4, - InstantRoundingMode::Trunc, + ZonedPair { + ns1: origin, + ns2: target, + time_zone: "Pacific/Apia", + calendar: "iso8601", + }, + DifferenceSettings { + largest_index: 3, + smallest_index: 4, + increment: 1, + mode: InstantRoundingMode::Trunc, + }, ) .unwrap(); let values = internal_duration_to_values(&internal, 4); diff --git a/crates/oxide_builtins/src/temporal/zoned_date_time.rs b/crates/oxide_builtins/src/temporal/zoned_date_time.rs index 40e0d101..b5af6fe2 100644 --- a/crates/oxide_builtins/src/temporal/zoned_date_time.rs +++ b/crates/oxide_builtins/src/temporal/zoned_date_time.rs @@ -19,7 +19,7 @@ use super::common::{ use super::difference::{ add_days_iso, bubble_relative_duration, compare_iso_date, date_duration_sign, date_until_iso, difference_core, internal_duration_sign, nudge_to_calendar_unit, parse_difference_settings, round_instant_difference, - InternalDuration, DAY_NS, MAX_ISO_DAY, TIME_UNIT_NS, + DifferenceSettings, IsoDateTime, InternalDuration, RoundingSpec, DAY_NS, MAX_ISO_DAY, TIME_UNIT_NS, }; use super::instant::{ instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint, @@ -533,17 +533,27 @@ pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) -> } } +/// toString 注解名选项:offset / timeZoneName / calendarName 三取值。 +#[derive(Clone, Copy)] +pub(crate) struct FormatNames<'a> { + pub(crate) offset_name: &'a str, + pub(crate) time_zone_name: &'a str, + pub(crate) calendar_name: &'a str, +} + /// ZDT 字符串化核心:epoch 域已舍入后按偏移与注解选项拼 `{date}T{time}.fff{offset}[{tz}][{ca}]`。 /// /// # 边界与前提 /// - epoch_ns 须已按 quantum 舍入(调用方保证),此处仅做本地墙钟分解。 /// - offset_name/time_zone_name 取 auto/never/critical,calendar_name 取 auto/never/always/critical。 /// - 偏移段 `±HH:MM` 带冒号(秒非零时 `±HH:MM:SS`);时区注解命名区原样、偏移区规范化。 -#[expect(clippy::too_many_arguments)] pub(crate) fn format_zoned_date_time_iso( epoch_ns: i128, offset_seconds: i64, time_zone_id: &str, calendar_id: &str, include_seconds: bool, - output_digits: Option, offset_name: &str, time_zone_name: &str, calendar_name: &str, + output_digits: Option, names: FormatNames, ) -> Option { + let offset_name = names.offset_name; + let time_zone_name = names.time_zone_name; + let calendar_name = names.calendar_name; const DAY_NS: i128 = 86_400_000_000_000; let offset_ns = i128::from(offset_seconds).checked_mul(1_000_000_000)?; let local_ns = epoch_ns.checked_add(offset_ns)?; @@ -716,9 +726,11 @@ pub fn zoned_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeRe &calendar_id, include_seconds, output_digits, - &offset_name, - &time_zone_name, - &calendar_name, + FormatNames { + offset_name: &offset_name, + time_zone_name: &time_zone_name, + calendar_name: &calendar_name, + }, ) { Some(output) => NativeResult::Ok(vm.new_string_owned(output)), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")), @@ -1350,10 +1362,14 @@ fn zoned_date_time_difference(vm: &mut H, args: &[u8], since: bool) - let settings = native_try!(parse_difference_settings(vm, options_value, false, 4)); difference_core( vm, - (i128::from(y_r), i128::from(m_r), i128::from(d_r)), - time_ns_r as i128, - (y_o, m_o, d_o), - time_ns_o, + IsoDateTime { + date: (i128::from(y_r), i128::from(m_r), i128::from(d_r)), + time_ns: time_ns_r as i128, + }, + IsoDateTime { + date: (y_o, m_o, d_o), + time_ns: time_ns_o, + }, settings, since, 9, @@ -1710,14 +1726,15 @@ pub(crate) fn add_instant(epoch_ns: i128, time_ns: i128) -> Result Result<(i128, i128, i128), AddZonedError> { + let (years, months, weeks, days) = (values[0], values[1], values[2], values[3]); let total = date.0 * 12 + (date.1 - 1) + years * 12 + months; let ny = total.div_euclid(12); let nm = total.rem_euclid(12) + 1; @@ -1837,17 +1854,16 @@ pub(crate) fn add_zoned_datetime_pure( return add_instant(epoch_ns, time_ns); } // GetISODateTimeFor:墙历分量。 - let ((year, month, day), time_ns) = get_iso_date_time_for(time_zone, epoch_ns)?; + let iso = get_iso_date_time_for(time_zone, epoch_ns)?; + let (year, month, day) = iso.date; + let time_ns = iso.time_ns; if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) { return Err(AddZonedError::OutOfDateTimeRange); } // CalendarDateAdd:日期分量(constrain 钳月末,reject 抛错)。 let added = calendar_date_add_iso( (year, month, day), - values[0] as i128, - values[1] as i128, - values[2] as i128, - values[3] as i128, + [values[0] as i128, values[1] as i128, values[2] as i128, values[3] as i128], constrain, )?; // 中间日期检查(ISODateTimeWithinLimits)。 @@ -1887,20 +1903,18 @@ pub(crate) fn map_add_zoned_error(vm: &mut H, error: AddZonedError) - } /// DifferenceZonedDateTimeWithRounding 的 VM 包装:错误映射为 RangeError。 -#[allow(clippy::too_many_arguments)] pub(crate) fn difference_zoned_datetime_with_rounding_vm( - vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, largest_unit: usize, increment: i128, - smallest_unit: usize, mode: InstantRoundingMode, + vm: &mut H, pair: ZonedPair, settings: DifferenceSettings, ) -> Result { - difference_zoned_datetime_with_rounding(ns1, ns2, time_zone, calendar, largest_unit, increment, smallest_unit, mode) + difference_zoned_datetime_with_rounding(pair, settings) .map_err(|error| map_add_zoned_error(vm, error)) } /// DifferenceZonedDateTimeWithTotal 的 VM 包装:错误映射为 RangeError。 pub(crate) fn difference_zoned_datetime_with_total_vm( - vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, unit: usize, + vm: &mut H, pair: ZonedPair, unit: usize, ) -> Result { - difference_zoned_datetime_with_total(ns1, ns2, time_zone, calendar, unit) + difference_zoned_datetime_with_total(pair, unit) .map_err(|error| map_add_zoned_error(vm, error)) } @@ -1912,16 +1926,17 @@ pub(crate) fn difference_zoned_datetime_with_total_vm( /// /// # 边界与前提 /// - 时区名非法(不在区表且非固定偏移串)返回 InvalidTimeZone。 -pub(crate) fn get_iso_date_time_for( - time_zone: &str, epoch_ns: i128, -) -> Result<((i128, i128, i128), i128), AddZonedError> { +pub(crate) fn get_iso_date_time_for(time_zone: &str, epoch_ns: i128) -> Result { let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64) .ok_or(AddZonedError::InvalidTimeZone)?; let local_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000; let days = local_ns.div_euclid(DAY_NS); let time_ns = local_ns.rem_euclid(DAY_NS); let (year, month, day) = civil_from_days(days); - Ok(((year, month, day), time_ns)) + Ok(IsoDateTime { + date: (year, month, day), + time_ns, + }) } /// GetEpochNanosecondsFor(规范 11.1.11,compatible 消歧):墙历日期时间的纪元。 @@ -1959,6 +1974,17 @@ fn time_duration_sign(ns: i128) -> i128 { } } +/// zoned 差值端点对:两个 epoch 加时区与日历(日历透传,引擎仅 iso8601)。 +#[derive(Clone, Copy)] +pub(crate) struct ZonedPair<'a> { + pub(crate) ns1: i128, + pub(crate) ns2: i128, + pub(crate) time_zone: &'a str, + // 透传字段:引擎仅 iso8601 日历,差值计算不消费,保留以对齐规范签名。 + #[allow(dead_code)] + pub(crate) calendar: &'a str, +} + /// DifferenceZonedDateTime(规范 6.5.6):两个 epoch 的差,平衡后结果中无大于 /// largestUnit 的非零单位,计入日历 reckoning 与时区偏移变化。 /// @@ -1972,13 +1998,20 @@ fn time_duration_sign(ns: i128) -> i128 { /// - 全程 i128 纳秒;中间点时间取起点墙历时间(规范 (intermediateDate, startDT.Time))。 /// - calendar 参数透传(引擎仅 iso8601 日历)。 pub(crate) fn difference_zoned_datetime( - ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize, + pair: ZonedPair, largest_unit: usize, ) -> Result { + let ns1 = pair.ns1; + let ns2 = pair.ns2; + let time_zone = pair.time_zone; if ns1 == ns2 { return Ok(InternalDuration { date: [0.0; 4], time_ns: 0 }); } - let (start_date, start_time) = get_iso_date_time_for(time_zone, ns1)?; - let (end_date, end_time) = get_iso_date_time_for(time_zone, ns2)?; + let start = get_iso_date_time_for(time_zone, ns1)?; + let end = get_iso_date_time_for(time_zone, ns2)?; + let start_date = start.date; + let start_time = start.time_ns; + let end_date = end.date; + let end_time = end.time_ns; if compare_iso_date(start_date, end_date) == 0 { return Ok(InternalDuration { date: [0.0; 4], @@ -2023,11 +2056,16 @@ pub(crate) fn difference_zoned_datetime( /// /// # 边界与前提 /// - smallestUnit 不大于 largestUnit(调用方已校验)。 -#[allow(clippy::too_many_arguments)] pub(crate) fn difference_zoned_datetime_with_rounding( - ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize, increment: i128, smallest_unit: usize, - mode: InstantRoundingMode, + pair: ZonedPair, settings: DifferenceSettings, ) -> Result { + let ns1 = pair.ns1; + let ns2 = pair.ns2; + let time_zone = pair.time_zone; + let largest_unit = settings.largest_index; + let increment = settings.increment; + let smallest_unit = settings.smallest_index; + let mode = settings.mode; if largest_unit >= 4 { let quantum = TIME_UNIT_NS[smallest_unit - 4] .checked_mul(increment) @@ -2038,22 +2076,12 @@ pub(crate) fn difference_zoned_datetime_with_rounding( time_ns: rounded, }); } - let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, largest_unit)?; + let difference = difference_zoned_datetime(pair, largest_unit)?; if smallest_unit == 9 && increment == 1 { return Ok(difference); } - let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?; - round_relative_duration_zoned( - difference, - ns1, - ns2, - (iso_date, iso_time), - time_zone, - largest_unit, - increment, - smallest_unit, - mode, - ) + let iso = get_iso_date_time_for(time_zone, ns1)?; + round_relative_duration_zoned(difference, ns1, ns2, iso, time_zone, settings) } /// RoundRelativeDuration(规范 7.5.38)的 zoned 路由:日历单位(或 day)走日历 @@ -2063,33 +2091,32 @@ pub(crate) fn difference_zoned_datetime_with_rounding( /// 1. 路由:smallestUnit 为日历单位或 day → NudgeToCalendarUnit(zoned 闭包); /// 否则 NudgeToZonedTime。 /// 2. 扩窗且 smallestUnit 非 week → BubbleRelativeDuration(startUnit 取 smallestUnit 与 day 的较大者)。 -#[allow(clippy::too_many_arguments)] fn round_relative_duration_zoned( - difference: InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str, - largest_unit: usize, increment: i128, smallest_unit: usize, mode: InstantRoundingMode, + difference: InternalDuration, origin_ns: i128, dest_ns: i128, iso: IsoDateTime, time_zone: &str, + settings: DifferenceSettings, ) -> Result { + let largest_unit = settings.largest_index; + let increment = settings.increment; + let smallest_unit = settings.smallest_index; + let mode = settings.mode; let values = difference.to_values(); let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 }; - let (iso_date, iso_time) = iso; + let iso_date = iso.date; + let iso_time = iso.time_ns; let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time); // zoned 面时区恒在场:irregular = smallestUnit 为日历单位或 day。 let irregular = smallest_unit <= 3; + let spec = RoundingSpec { + increment, + unit: smallest_unit, + mode, + }; let (mut result, nudged_epoch, did_expand) = if irregular { - let outcome = nudge_to_calendar_unit( - sign, - &values, - origin_ns, - dest_ns, - iso_date, - increment, - smallest_unit, - mode, - &epoch_of_date, - ) - .map_err(|_| AddZonedError::OutOfDateTimeRange)?; + let outcome = nudge_to_calendar_unit(sign, &values, origin_ns, dest_ns, iso_date, spec, &epoch_of_date) + .map_err(|_| AddZonedError::OutOfDateTimeRange)?; (outcome.duration, outcome.nudged_epoch, outcome.did_expand) } else { - nudge_to_zoned_time(sign, &values, iso_date, iso_time, time_zone, increment, smallest_unit, mode)? + nudge_to_zoned_time(sign, &values, iso, time_zone, spec)? }; if did_expand && smallest_unit != 2 { let start_unit = smallest_unit.min(3); @@ -2113,18 +2140,18 @@ fn round_relative_duration_zoned( /// /// # 边界与前提 /// - unit 为时间单位(时到纳秒);day 单位走日历窗口路径,不进本函数。 -#[allow(clippy::too_many_arguments)] fn nudge_to_zoned_time( - sign: i128, values: &[f64; 10], iso_date: (i128, i128, i128), iso_time: i128, time_zone: &str, increment: i128, - unit: usize, mode: InstantRoundingMode, + sign: i128, values: &[f64; 10], iso: IsoDateTime, time_zone: &str, spec: RoundingSpec, ) -> Result<([f64; 10], i128, bool), AddZonedError> { + let increment = spec.increment; + let unit = spec.unit; + let mode = spec.mode; + let iso_date = iso.date; + let iso_time = iso.time_ns; let time_ns = duration_time_only_nanoseconds(values).ok_or(AddZonedError::OutOfTimeRange)?; let start = calendar_date_add_iso( iso_date, - values[0] as i128, - values[1] as i128, - values[2] as i128, - values[3] as i128, + [values[0] as i128, values[1] as i128, values[2] as i128, values[3] as i128], true, )?; let start_epoch = get_epoch_nanos_for(time_zone, start, iso_time)?; @@ -2156,15 +2183,18 @@ fn nudge_to_zoned_time( /// 1. unit 为时间单位 → TotalTimeDuration 快路径(纳秒差直接除单位长度)。 /// 2. 否则 DifferenceZonedDateTime + TotalRelativeDuration。 pub(crate) fn difference_zoned_datetime_with_total( - ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, unit: usize, + pair: ZonedPair, unit: usize, ) -> Result { + let ns1 = pair.ns1; + let ns2 = pair.ns2; + let time_zone = pair.time_zone; if unit >= 4 { let difference = ns2 - ns1; return Ok(difference as f64 / TIME_UNIT_NS[unit - 4] as f64); } - let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, unit)?; - let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?; - total_relative_duration(&difference, ns1, ns2, (iso_date, iso_time), time_zone, unit) + let difference = difference_zoned_datetime(pair, unit)?; + let iso = get_iso_date_time_for(time_zone, ns1)?; + total_relative_duration(&difference, ns1, ns2, iso, time_zone, unit) } /// TotalRelativeDuration(规范 7.5.39):时长以 unit 计的总量,相对起点折算。 @@ -2174,27 +2204,23 @@ pub(crate) fn difference_zoned_datetime_with_total( /// (increment = 1、trunc)取 [[Total]]。 /// 2. 否则 TotalTimeDuration(24h 日折算,zoned 面不可达)。 fn total_relative_duration( - difference: &InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str, + difference: &InternalDuration, origin_ns: i128, dest_ns: i128, iso: IsoDateTime, time_zone: &str, unit: usize, ) -> Result { let values = difference.to_values(); // zoned 面时区恒在场:unit ≤ day 恒走日历窗口路径。 if unit <= 3 { let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 }; - let (iso_date, iso_time) = iso; + let iso_date = iso.date; + let iso_time = iso.time_ns; let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time); - let outcome = nudge_to_calendar_unit( - sign, - &values, - origin_ns, - dest_ns, - iso_date, - 1, + let spec = RoundingSpec { + increment: 1, unit, - InstantRoundingMode::Trunc, - &epoch_of_date, - ) - .map_err(|_| AddZonedError::OutOfDateTimeRange)?; + mode: InstantRoundingMode::Trunc, + }; + let outcome = nudge_to_calendar_unit(sign, &values, origin_ns, dest_ns, iso_date, spec, &epoch_of_date) + .map_err(|_| AddZonedError::OutOfDateTimeRange)?; return Ok(outcome.total); } let time_ns = duration_time_only_nanoseconds(&values).ok_or(AddZonedError::OutOfTimeRange)?; diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index 20406822..54fab168 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -2701,7 +2701,15 @@ fn ta_decode_base64( handling = ta_handling_option(vm, obj, opts_val)?; } let units = vm.string_units(string).into_owned(); - match crate::ta_codec::decode_base64(&units, alphabet, handling, max_len, write) { + match crate::ta_codec::decode_base64( + &units, + crate::ta_codec::DecodeConfig { + alphabet, + handling, + max_len, + }, + write, + ) { Ok(r) => Ok(r), Err(_) => Err(crate::error::create_syntax_error(vm, "invalid base64 input")), }