fix: Temporal 日历注解白名单化,u-ca 校验扩到 18 项内置日历
This commit is contained in:
@@ -2683,7 +2683,7 @@ fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
|||||||
if critical || saw_critical_calendar {
|
if critical || saw_critical_calendar {
|
||||||
return Err("invalid calendar annotation".into());
|
return Err("invalid calendar annotation".into());
|
||||||
}
|
}
|
||||||
} else if !value.eq_ignore_ascii_case("iso8601") {
|
} else if is_builtin_calendar_id(value).is_none() {
|
||||||
return Err("invalid calendar annotation".into());
|
return Err("invalid calendar annotation".into());
|
||||||
} else {
|
} else {
|
||||||
calendar_seen = true;
|
calendar_seen = true;
|
||||||
@@ -2885,22 +2885,60 @@ fn parse_temporal_string_impl(input: &str, enforce_date_time_range: bool) -> Res
|
|||||||
Ok((year, month, day, total_ns))
|
Ok((year, month, day, total_ns))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ParseTemporalCalendarString:日历标识符 = "iso8601"(ASCII 大小写不敏感)或合法 ISO
|
/// 规范 BuiltinCalendarID 全集(18 项)。匹配用 eq_ignore_ascii_case(ASCII 折叠,
|
||||||
/// 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
|
/// 禁 to_lowercase:U+0130 点 I 会被 Unicode 折叠成 i+组合符,误判为 iso8601)。
|
||||||
fn parse_temporal_calendar_string(input: &str) -> Result<(), String> {
|
const CALENDAR_ID_WHITELIST: [&str; 18] = [
|
||||||
|
"buddhist",
|
||||||
|
"chinese",
|
||||||
|
"coptic",
|
||||||
|
"dangi",
|
||||||
|
"ethioaa",
|
||||||
|
"ethiopic",
|
||||||
|
"gregory",
|
||||||
|
"hebrew",
|
||||||
|
"indian",
|
||||||
|
"islamic",
|
||||||
|
"islamic-civil",
|
||||||
|
"islamic-rcy",
|
||||||
|
"islamic-tbla",
|
||||||
|
"islamic-umalqura",
|
||||||
|
"iso8601",
|
||||||
|
"japanese",
|
||||||
|
"persian",
|
||||||
|
"roc",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 严格白名单匹配:命中返回规范小写形式,未命中返回 None。
|
||||||
|
fn is_builtin_calendar_id(value: &str) -> Option<&'static str> {
|
||||||
|
CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 严格日历 ID 解析:只走 18 项白名单,拒绝 ISO 串(含注解/compact/extended/空串)。
|
||||||
|
/// 供构造器日历参数与注解值判定使用。
|
||||||
|
fn parse_temporal_calendar_id_strict(input: &str) -> Result<String, String> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
is_builtin_calendar_id(trimmed)
|
||||||
|
.map(str::to_string)
|
||||||
|
.ok_or_else(|| "invalid calendar".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ParseTemporalCalendarString:日历标识符 = 18 项内置日历 ID(ASCII 大小写不敏感)
|
||||||
|
/// 或合法 ISO 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
|
||||||
|
/// 返回规范化日历 ID:白名单命中返回规范小写,ISO 串路径恒为 "iso8601"。
|
||||||
|
fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
|
||||||
let trimmed = input.trim();
|
let trimmed = input.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return Err("invalid calendar".into());
|
return Err("invalid calendar".into());
|
||||||
}
|
}
|
||||||
if trimmed.eq_ignore_ascii_case("iso8601") {
|
if let Some(id) = is_builtin_calendar_id(trimmed) {
|
||||||
return Ok(());
|
return Ok(id.to_string());
|
||||||
}
|
}
|
||||||
if trimmed.contains('\u{2212}') {
|
if trimmed.contains('\u{2212}') {
|
||||||
return Err("variant minus sign is not valid for calendar".into());
|
return Err("variant minus sign is not valid for calendar".into());
|
||||||
}
|
}
|
||||||
// 完整日期时间字符串(含注解校验),例如 2020-01-01T00:00:00.000000000[u-ca=iso8601]
|
// 完整日期时间字符串(含注解校验),例如 2020-01-01T00:00:00.000000000[u-ca=iso8601]
|
||||||
if parse_plain_date_time_string(trimmed).is_ok() {
|
if parse_plain_date_time_string(trimmed).is_ok() {
|
||||||
return Ok(());
|
return Ok("iso8601".to_string());
|
||||||
}
|
}
|
||||||
// 部分日期:YYYY-MM 或 MM-DD(可带注解)
|
// 部分日期:YYYY-MM 或 MM-DD(可带注解)
|
||||||
let text = trimmed.to_owned();
|
let text = trimmed.to_owned();
|
||||||
@@ -2910,7 +2948,8 @@ fn parse_temporal_calendar_string(input: &str) -> Result<(), String> {
|
|||||||
if text.contains(['Z', 'z']) {
|
if text.contains(['Z', 'z']) {
|
||||||
return Err("UTC designator is not valid for calendar".into());
|
return Err("UTC designator is not valid for calendar".into());
|
||||||
}
|
}
|
||||||
parse_partial_calendar_date(text)
|
parse_partial_calendar_date(text)?;
|
||||||
|
Ok("iso8601".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 部分 ISO 日期(无时间):YYYY[-MM[-DD]] 或 MM-DD;校验月份/日期基本范围并拒绝负零年。
|
/// 部分 ISO 日期(无时间):YYYY[-MM[-DD]] 或 MM-DD;校验月份/日期基本范围并拒绝负零年。
|
||||||
@@ -2981,25 +3020,38 @@ fn parse_partial_calendar_date(input: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ToTemporalCalendar 校验:非 string 抛 TypeError;字符串必须通过 ParseTemporalCalendarString。
|
/// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None;
|
||||||
fn temporal_calendar_check<H: VmHost>(vm: &mut H, value: JsValue) -> Result<(), JsValue> {
|
/// string → 宽松解析(白名单 ID 或 ISO 串);其他类型 → TypeError。
|
||||||
|
/// 日历对象的内部槽 fast path 在日历槽接入后补充。
|
||||||
|
fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
||||||
if value.is_undefined() {
|
if value.is_undefined() {
|
||||||
return Ok(());
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if !value.is_string() {
|
if value.is_string() {
|
||||||
return Err(crate::error::create_type_error(vm, "invalid calendar"));
|
return parse_temporal_calendar_string(&to_string(value))
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
||||||
}
|
}
|
||||||
if parse_temporal_calendar_string(&to_string(value)).is_err() {
|
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
||||||
return Err(crate::error::create_range_error(vm, "invalid calendar"));
|
}
|
||||||
|
|
||||||
|
/// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串;
|
||||||
|
/// undefined / 其他分支与宽松版一致。
|
||||||
|
#[expect(dead_code)] // 构造器日历参数接入前的预留入口
|
||||||
|
fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
||||||
|
if value.is_string() {
|
||||||
|
return parse_temporal_calendar_id_strict(&to_string(value))
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
||||||
}
|
}
|
||||||
Ok(())
|
temporal_calendar_id(vm, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plain_date_time_object_parts<H: VmHost>(
|
fn plain_date_time_object_parts<H: VmHost>(
|
||||||
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool, ignore_time: bool,
|
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool, ignore_time: bool,
|
||||||
) -> Result<(i32, u32, u32, f64), JsValue> {
|
) -> Result<(i32, u32, u32, f64, Option<String>), JsValue> {
|
||||||
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
|
let calendar_raw = temporal_option_value(vm, obj, value, "calendar")?;
|
||||||
temporal_calendar_check(vm, calendar)?;
|
let calendar = temporal_calendar_id(vm, calendar_raw)?;
|
||||||
|
|
||||||
// 先按规范顺序读取全部原始字段,暂不转换类型;PlainDate 路径忽略时间字段。
|
// 先按规范顺序读取全部原始字段,暂不转换类型;PlainDate 路径忽略时间字段。
|
||||||
let (
|
let (
|
||||||
@@ -3151,7 +3203,7 @@ fn plain_date_time_object_parts<H: VmHost>(
|
|||||||
if !ignore_time && !valid_plain_date_time_range(year, month, day, total_ns) {
|
if !ignore_time && !valid_plain_date_time_range(year, month, day, total_ns) {
|
||||||
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
}
|
}
|
||||||
Ok((year, month, day, total_ns))
|
Ok((year, month, day, total_ns, calendar))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
|
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
|
||||||
@@ -3176,9 +3228,10 @@ fn zoned_date_time_plain_parts<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<
|
|||||||
|
|
||||||
fn plain_date_time_like_parts<H: VmHost>(
|
fn plain_date_time_like_parts<H: VmHost>(
|
||||||
vm: &mut H, value: JsValue, constrain: bool,
|
vm: &mut H, value: JsValue, constrain: bool,
|
||||||
) -> Result<(i32, u32, u32, f64), JsValue> {
|
) -> Result<(i32, u32, u32, f64, Option<String>), JsValue> {
|
||||||
if value.is_string() {
|
if value.is_string() {
|
||||||
return parse_plain_date_time_string(&to_string(value))
|
return parse_plain_date_time_string(&to_string(value))
|
||||||
|
.map(|(year, month, day, total_ns)| (year, month, day, total_ns, None))
|
||||||
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"));
|
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"));
|
||||||
}
|
}
|
||||||
if !value.is_object() {
|
if !value.is_object() {
|
||||||
@@ -3195,6 +3248,7 @@ fn plain_date_time_like_parts<H: VmHost>(
|
|||||||
get_double_prop(obj, 1) as u32,
|
get_double_prop(obj, 1) as u32,
|
||||||
get_double_prop(obj, 2) as u32,
|
get_double_prop(obj, 2) as u32,
|
||||||
get_double_prop(obj, 3),
|
get_double_prop(obj, 3),
|
||||||
|
None, // 实例日历传播在日历槽接入后补齐
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if obj.is_plain_date_obj() {
|
if obj.is_plain_date_obj() {
|
||||||
@@ -3203,10 +3257,12 @@ fn plain_date_time_like_parts<H: VmHost>(
|
|||||||
get_double_prop(obj, 1) as u32,
|
get_double_prop(obj, 1) as u32,
|
||||||
get_double_prop(obj, 2) as u32,
|
get_double_prop(obj, 2) as u32,
|
||||||
0.0,
|
0.0,
|
||||||
|
None, // 实例日历传播在日历槽接入后补齐
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if obj.is_zoned_date_time_obj() {
|
if obj.is_zoned_date_time_obj() {
|
||||||
return zoned_date_time_plain_parts(vm, obj);
|
return zoned_date_time_plain_parts(vm, obj)
|
||||||
|
.map(|(year, month, day, total_ns)| (year, month, day, total_ns, None));
|
||||||
}
|
}
|
||||||
plain_date_time_object_parts(vm, value, obj, constrain, false)
|
plain_date_time_object_parts(vm, value, obj, constrain, false)
|
||||||
}
|
}
|
||||||
@@ -3250,7 +3306,7 @@ pub fn plain_date_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
|||||||
make_plain_date_time(vm, year, month, day, total_ns)
|
make_plain_date_time(vm, year, month, day, total_ns)
|
||||||
} else {
|
} else {
|
||||||
let constrain = native_try!(temporal_overflow(vm, args));
|
let constrain = native_try!(temporal_overflow(vm, args));
|
||||||
let (year, month, day, total_ns) = native_try!(plain_date_time_like_parts(vm, value, constrain));
|
let (year, month, day, total_ns, _) = native_try!(plain_date_time_like_parts(vm, value, constrain));
|
||||||
make_plain_date_time(vm, year, month, day, total_ns)
|
make_plain_date_time(vm, year, month, day, total_ns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4324,7 +4380,7 @@ fn plain_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -
|
|||||||
Err(error) => return NativeResult::Err(error),
|
Err(error) => return NativeResult::Err(error),
|
||||||
};
|
};
|
||||||
let other = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
let other = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||||
let (oy, om, od, ot) = match plain_date_time_like_parts(vm, other, true) {
|
let (oy, om, od, ot, _) = match plain_date_time_like_parts(vm, other, true) {
|
||||||
Ok(parts) => parts,
|
Ok(parts) => parts,
|
||||||
Err(error) => return NativeResult::Err(error),
|
Err(error) => return NativeResult::Err(error),
|
||||||
};
|
};
|
||||||
@@ -4419,7 +4475,7 @@ fn object_date_ymd<H: VmHost>(vm: &mut H, val: JsValue, constrain: bool) -> Resu
|
|||||||
let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?;
|
let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?;
|
||||||
return Ok((year, month, day));
|
return Ok((year, month, day));
|
||||||
}
|
}
|
||||||
let (year, month, day, _) = plain_date_time_object_parts(vm, val, obj, constrain, true)?;
|
let (year, month, day, _, _) = plain_date_time_object_parts(vm, val, obj, constrain, true)?;
|
||||||
Ok((year, month, day))
|
Ok((year, month, day))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4744,3 +4800,69 @@ pub fn plain_date_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_calendar_id_whitelist() {
|
||||||
|
// 18 项白名单全部命中且返回规范小写形式。
|
||||||
|
for id in CALENDAR_ID_WHITELIST {
|
||||||
|
assert_eq!(is_builtin_calendar_id(id), Some(id), "白名单项 {id} 应命中自身");
|
||||||
|
}
|
||||||
|
// ASCII 大小写折叠:仅首字母大写与全大写变体命中。
|
||||||
|
assert_eq!(is_builtin_calendar_id("IsO8601"), Some("iso8601"));
|
||||||
|
assert_eq!(is_builtin_calendar_id("HEBREW"), Some("hebrew"));
|
||||||
|
assert_eq!(is_builtin_calendar_id("Gregory"), Some("gregory"));
|
||||||
|
// 点 I(U+0130)不属于 ASCII 折叠域,必须拒绝,否则误判为 iso8601。
|
||||||
|
assert_eq!(is_builtin_calendar_id("\u{0130}SO8601"), None);
|
||||||
|
// 白名单外与类日期串拒绝。
|
||||||
|
assert_eq!(is_builtin_calendar_id("notacal"), None);
|
||||||
|
assert_eq!(is_builtin_calendar_id("1111-11-11"), None);
|
||||||
|
assert_eq!(is_builtin_calendar_id("11111111"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_calendar_id_parser() {
|
||||||
|
// 严格解析:只收白名单 ID,ISO 串 / 未知值 / 空串全拒。
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict("hebrew"), Ok("hebrew".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict("IsO8601"), Ok("iso8601".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict(" gregory "), Ok("gregory".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
parse_temporal_calendar_id_strict("1997-12-04[u-ca=iso8601]"),
|
||||||
|
Err("invalid calendar".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict("11111111"), Err("invalid calendar".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict("\u{0130}SO8601"), Err("invalid calendar".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_id_strict(""), Err("invalid calendar".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loose_calendar_string_parser() {
|
||||||
|
// 宽松解析(property bag 路径):白名单 ID 返回规范小写,ISO 串路径恒为 iso8601。
|
||||||
|
assert_eq!(parse_temporal_calendar_string("gregory"), Ok("gregory".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_string("iSo8601"), Ok("iso8601".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_string("1997-12-04"), Ok("iso8601".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_string("1997-12-04[u-ca=iso8601]"), Ok("iso8601".to_string()));
|
||||||
|
assert_eq!(parse_temporal_calendar_string(""), Err("invalid calendar".to_string()));
|
||||||
|
// 注解值未过白名单 / 裸未知标识符,两种路径都拒绝。
|
||||||
|
assert!(parse_temporal_calendar_string("[u-ca=notacal]").is_err());
|
||||||
|
assert!(parse_temporal_calendar_string("notacal").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotation_suffix_calendar_whitelist() {
|
||||||
|
// 首个 u-ca 注解值过 18 项白名单:白名单内放行(含关键标记),白名单外拒绝。
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=hebrew]").is_ok());
|
||||||
|
assert!(validate_temporal_annotation_suffix("[!u-ca=hebrew]").is_ok());
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=iSo8601]").is_ok());
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=notacal]").is_err());
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=1111-11-11]").is_err());
|
||||||
|
// 点 I 变体不做 Unicode 折叠,拒绝。
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=\u{0130}SO8601]").is_err());
|
||||||
|
// 第二及后续 u-ca 注解被忽略,不参与校验;含关键标记的重复日历报错。
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][u-ca=discord]").is_ok());
|
||||||
|
assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][!u-ca=iso8601]").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user