diff --git a/Cargo.toml b/Cargo.toml index 7bd7ba38cd8..8d0b85bfb1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,7 @@ syn = { version = "2", default-features = false, features = ["clone-impls", "der synstructure = "0.13" thin-vec = "0.2.13" time = "0.1.41" -time_03 = { package = "time", version = "0.3", features = ["serde"] } +time_03 = { package = "time", version = "0.3", features = ["large-dates", "serde"] } to_shmem = { git = "https://github.com/servo/stylo", branch = "2024-07-16" } tokio = "1" tokio-rustls = "0.24" diff --git a/components/script/dom/bindings/str.rs b/components/script/dom/bindings/str.rs index 3489a2da617..ae948c843cd 100644 --- a/components/script/dom/bindings/str.rs +++ b/components/script/dom/bindings/str.rs @@ -12,13 +12,12 @@ use std::str::FromStr; use std::sync::LazyLock; use std::{fmt, ops, str}; -use chrono::prelude::{Utc, Weekday}; -use chrono::{Datelike, TimeZone}; use cssparser::CowRcStr; use html5ever::{LocalName, Namespace}; use num_traits::Zero; use regex::Regex; use servo_atoms::Atom; +use time_03::{Date, Month, OffsetDateTime, Time, Weekday}; /// Encapsulates the IDL `ByteString` type. #[derive(Clone, Debug, Default, Eq, JSTraceable, MallocSizeOf, PartialEq)] @@ -205,6 +204,11 @@ impl DOMString { DOMString(s, PhantomData) } + /// Get the internal `&str` value of this [`DOMString`]. + pub fn str(&self) -> &str { + &self.0 + } + /// Appends a given string slice onto the end of this String. pub fn push_str(&mut self, string: &str) { self.0.push_str(string) @@ -245,189 +249,6 @@ impl DOMString { self.0.replace_range(0..first_non_whitespace, ""); } - /// Validates this `DOMString` is a time string according to - /// . - pub fn is_valid_time_string(&self) -> bool { - enum State { - HourHigh, - HourLow09, - HourLow03, - MinuteColon, - MinuteHigh, - MinuteLow, - SecondColon, - SecondHigh, - SecondLow, - MilliStop, - MilliHigh, - MilliMiddle, - MilliLow, - Done, - Error, - } - let next_state = |valid: bool, next: State| -> State { - if valid { - next - } else { - State::Error - } - }; - - let state = self.chars().fold(State::HourHigh, |state, c| { - match state { - // Step 1 "HH" - State::HourHigh => match c { - '0' | '1' => State::HourLow09, - '2' => State::HourLow03, - _ => State::Error, - }, - State::HourLow09 => next_state(c.is_ascii_digit(), State::MinuteColon), - State::HourLow03 => next_state(c.is_digit(4), State::MinuteColon), - - // Step 2 ":" - State::MinuteColon => next_state(c == ':', State::MinuteHigh), - - // Step 3 "mm" - State::MinuteHigh => next_state(c.is_digit(6), State::MinuteLow), - State::MinuteLow => next_state(c.is_ascii_digit(), State::SecondColon), - - // Step 4.1 ":" - State::SecondColon => next_state(c == ':', State::SecondHigh), - // Step 4.2 "ss" - State::SecondHigh => next_state(c.is_digit(6), State::SecondLow), - State::SecondLow => next_state(c.is_ascii_digit(), State::MilliStop), - - // Step 4.3.1 "." - State::MilliStop => next_state(c == '.', State::MilliHigh), - // Step 4.3.2 "SSS" - State::MilliHigh => next_state(c.is_ascii_digit(), State::MilliMiddle), - State::MilliMiddle => next_state(c.is_ascii_digit(), State::MilliLow), - State::MilliLow => next_state(c.is_ascii_digit(), State::Done), - - _ => State::Error, - } - }); - - match state { - State::Done | - // Step 4 (optional) - State::SecondColon | - // Step 4.3 (optional) - State::MilliStop | - // Step 4.3.2 (only 1 digit required) - State::MilliMiddle | State::MilliLow => true, - _ => false - } - } - - /// A valid date string should be "YYYY-MM-DD" - /// YYYY must be four or more digits, MM and DD both must be two digits - /// - pub fn is_valid_date_string(&self) -> bool { - self.parse_date_string().is_some() - } - - /// - pub fn parse_date_string(&self) -> Option<(i32, u32, u32)> { - let value = &self.0; - // Step 1, 2, 3 - let (year_int, month_int, day_int) = parse_date_component(value)?; - - // Step 4 - if value.split('-').nth(3).is_some() { - return None; - } - - // Step 5, 6 - Some((year_int, month_int, day_int)) - } - - /// - pub fn parse_time_string(&self) -> Option<(u32, u32, f64)> { - let value = &self.0; - // Step 1, 2, 3 - let (hour_int, minute_int, second_float) = parse_time_component(value)?; - - // Step 4 - if value.split(':').nth(3).is_some() { - return None; - } - - // Step 5, 6 - Some((hour_int, minute_int, second_float)) - } - - /// A valid month string should be "YYYY-MM" - /// YYYY must be four or more digits, MM both must be two digits - /// - pub fn is_valid_month_string(&self) -> bool { - self.parse_month_string().is_some() - } - - /// - pub fn parse_month_string(&self) -> Option<(i32, u32)> { - let value = &self; - // Step 1, 2, 3 - let (year_int, month_int) = parse_month_component(value)?; - - // Step 4 - if value.split('-').nth(2).is_some() { - return None; - } - // Step 5 - Some((year_int, month_int)) - } - - /// A valid week string should be like {YYYY}-W{WW}, such as "2017-W52" - /// YYYY must be four or more digits, WW both must be two digits - /// - pub fn is_valid_week_string(&self) -> bool { - self.parse_week_string().is_some() - } - - /// - pub fn parse_week_string(&self) -> Option<(i32, u32)> { - let value = &self.0; - // Step 1, 2, 3 - let mut iterator = value.split('-'); - let year = iterator.next()?; - - // Step 4 - let year_int = year.parse::().ok()?; - if year.len() < 4 || year_int == 0 { - return None; - } - - // Step 5, 6 - let week = iterator.next()?; - let (week_first, week_last) = week.split_at(1); - if week_first != "W" { - return None; - } - - // Step 7 - let week_int = week_last.parse::().ok()?; - if week_last.len() != 2 { - return None; - } - - // Step 8 - let max_week = max_week_in_year(year_int); - - // Step 9 - if week_int < 1 || week_int > max_week { - return None; - } - - // Step 10 - if iterator.next().is_some() { - return None; - } - - // Step 11 - Some((year_int, week_int)) - } - /// pub fn is_valid_floating_point_number_string(&self) -> bool { static RE: LazyLock = LazyLock::new(|| { @@ -473,90 +294,6 @@ impl DOMString { self.0 = parsed_value.to_string() } } - - /// A valid normalized local date and time string should be "{date}T{time}" - /// where date and time are both valid, and the time string must be as short as possible - /// - pub fn convert_valid_normalized_local_date_and_time_string(&mut self) -> Option<()> { - let date = self.parse_local_date_and_time_string()?; - if date.seconds == 0.0 { - self.0 = format!( - "{:04}-{:02}-{:02}T{:02}:{:02}", - date.year, date.month, date.day, date.hour, date.minute - ); - } else if date.seconds < 10.0 { - // we need exactly one leading zero on the seconds, - // whatever their total string length might be - self.0 = format!( - "{:04}-{:02}-{:02}T{:02}:{:02}:0{}", - date.year, date.month, date.day, date.hour, date.minute, date.seconds - ); - } else { - // we need no leading zeroes on the seconds - self.0 = format!( - "{:04}-{:02}-{:02}T{:02}:{:02}:{}", - date.year, date.month, date.day, date.hour, date.minute, date.seconds - ); - } - Some(()) - } - - /// - pub(crate) fn parse_local_date_and_time_string(&self) -> Option { - let value = &self; - // Step 1, 2, 4 - let mut iterator = if value.contains('T') { - value.split('T') - } else { - value.split(' ') - }; - - // Step 3 - let date = iterator.next()?; - let (year, month, day) = parse_date_component(date)?; - - // Step 5 - let time = iterator.next()?; - let (hour, minute, seconds) = parse_time_component(time)?; - - // Step 6 - if iterator.next().is_some() { - return None; - } - - // Step 7, 8, 9 - Some(ParsedDate { - year, - month, - day, - hour, - minute, - seconds, - }) - } - - /// - pub fn is_valid_email_address_string(&self) -> bool { - static RE: LazyLock = LazyLock::new(|| { - Regex::new(concat!( - r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?", - r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" - )) - .unwrap() - }); - - RE.is_match(&self.0) - } - - /// - pub fn is_valid_simple_color_string(&self) -> bool { - let mut chars = self.0.chars(); - if self.0.len() == 7 && chars.next() == Some('#') { - chars.all(|c| c.is_ascii_hexdigit()) - } else { - false - } - } } impl Borrow for DOMString { @@ -731,52 +468,68 @@ fn parse_date_component(value: &str) -> Option<(i32, u32, u32)> { } /// -fn parse_time_component(value: &str) -> Option<(u32, u32, f64)> { - // Step 1 +fn parse_time_component(value: &str) -> Option<(u8, u8, u8, u16)> { + // Step 1: Collect a sequence of code points that are ASCII digits from input given + // position. If the collected sequence is not exactly two characters long, then fail. + // Otherwise, interpret the resulting sequence as a base-ten integer. Let that number + // be the hour. let mut iterator = value.split(':'); let hour = iterator.next()?; if hour.len() != 2 { return None; } - let hour_int = hour.parse::().ok()?; - - // Step 2 + // Step 2: If hour is not a number in the range 0 ≤ hour ≤ 23, then fail. + let hour_int = hour.parse::().ok()?; if hour_int > 23 { return None; } - // Step 3, 4 + // Step 3: If position is beyond the end of input or if the character at position is + // not a U+003A COLON character, then fail. Otherwise, move position forwards one + // character. + // Step 4: Collect a sequence of code points that are ASCII digits from input given + // position. If the collected sequence is not exactly two characters long, then fail. + // Otherwise, interpret the resulting sequence as a base-ten integer. Let that number + // be the minute. + // Step 5: If minute is not a number in the range 0 ≤ minute ≤ 59, then fail. let minute = iterator.next()?; if minute.len() != 2 { return None; } - let minute_int = minute.parse::().ok()?; - - // Step 5 + let minute_int = minute.parse::().ok()?; if minute_int > 59 { return None; } - // Step 6, 7 - let second_float = match iterator.next() { - Some(second) => { - let mut second_iterator = second.split('.'); - if second_iterator.next()?.len() != 2 { - return None; - } - if let Some(second_last) = second_iterator.next() { - if second_last.len() > 3 { - return None; - } - } - - second.parse::().ok()? - }, - None => 0.0, + // Step 6, 7: Asks us to parse the seconds as a floating point number, but below this + // is done as integral parts in order to avoid floating point precision issues. + let Some(seconds_and_milliseconds) = iterator.next() else { + return Some((hour_int, minute_int, 0, 0)); }; - // Step 8 - Some((hour_int, minute_int, second_float)) + // Parse the seconds portion. + let mut second_iterator = seconds_and_milliseconds.split('.'); + let second = second_iterator.next()?; + if second.len() != 2 { + return None; + } + let second_int = second.parse::().ok()?; + + // Parse the milliseconds portion as a u16 (milliseconds can be up to 1000) and + // make sure that it has the proper value based on how long the string is. + let Some(millisecond) = second_iterator.next() else { + return Some((hour_int, minute_int, second_int, 0)); + }; + let millisecond_length = millisecond.len() as u32; + if millisecond_length > 3 { + return None; + } + let millisecond_int = millisecond.parse::().ok()?; + let millisecond_int = millisecond_int * 10_u16.pow(3 - millisecond_length); + + // Step 8: Return hour, minute, and second (and in our case the milliseconds due to the note + // above about floating point precision). + Some((hour_int, minute_int, second_int, millisecond_int)) } fn max_day_in_month(year_num: i32, month_num: u32) -> Option { @@ -795,15 +548,22 @@ fn max_day_in_month(year_num: i32, month_num: u32) -> Option { } /// +/// +/// > A week-year with a number year has 53 weeks if it corresponds to either a year year +/// > in the proleptic Gregorian calendar that has a Thursday as its first day (January +/// > 1st), or a year year in the proleptic Gregorian calendar that has a Wednesday as its +/// > first day (January 1st) and where year is a number divisible by 400, or a number +/// > divisible by 4 but not by 100. All other week-years have 52 weeks. fn max_week_in_year(year: i32) -> u32 { - Utc.with_ymd_and_hms(year, 1, 1, 0, 0, 0) - .earliest() - .map(|date_time| match date_time.weekday() { - Weekday::Thu => 53, - Weekday::Wed if is_leap_year(year) => 53, - _ => 52, - }) - .unwrap_or(52) + let Ok(date) = Date::from_calendar_date(year, Month::January, 1) else { + return 52; + }; + + match OffsetDateTime::new_utc(date, Time::MIDNIGHT).weekday() { + Weekday::Thursday => 53, + Weekday::Wednesday if is_leap_year(year) => 53, + _ => 52, + } } #[inline] @@ -811,12 +571,340 @@ fn is_leap_year(year: i32) -> bool { year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) } -#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq)] -pub(crate) struct ParsedDate { - pub year: i32, - pub month: u32, - pub day: u32, - pub hour: u32, - pub minute: u32, - pub seconds: f64, +pub(crate) trait ToInputValueString { + fn to_date_string(&self) -> String; + fn to_month_string(&self) -> String; + fn to_week_string(&self) -> String; + fn to_time_string(&self) -> String; + + /// A valid normalized local date and time string should be "{date}T{time}" + /// where date and time are both valid, and the time string must be as short as possible + /// + fn to_local_date_time_string(&self) -> String; +} + +impl ToInputValueString for OffsetDateTime { + fn to_date_string(&self) -> String { + format!( + "{:04}-{:02}-{:02}", + self.year(), + self.month() as u8, + self.day() + ) + } + + fn to_month_string(&self) -> String { + format!("{:04}-{:02}", self.year(), self.month() as u8) + } + + fn to_week_string(&self) -> String { + // NB: The ISO week year might be different than the year of the day. + let (year, week, _) = self.to_iso_week_date(); + format!("{:04}-W{:02}", year, week) + } + + fn to_time_string(&self) -> String { + if self.second().is_zero() && self.millisecond().is_zero() { + format!("{:02}:{:02}", self.hour(), self.minute()) + } else { + // This needs to trim off the zero parts of the milliseconds. + format!( + "{:02}:{:02}:{:02}.{:03}", + self.hour(), + self.minute(), + self.second(), + self.millisecond() + ) + .trim_end_matches(['.', '0']) + .to_owned() + } + } + + fn to_local_date_time_string(&self) -> String { + format!("{}T{}", self.to_date_string(), self.to_time_string()) + } +} + +pub(crate) trait FromInputValueString { + /// + /// + /// Parse the date string and return an [`OffsetDateTime`] on midnight of the + /// given date in UTC. + /// + /// A valid date string should be "YYYY-MM-DD" + /// YYYY must be four or more digits, MM and DD both must be two digits + /// + fn parse_date_string(&self) -> Option; + + /// + /// + /// Parse the month and return an [`OffsetDate`] on midnight of UTC of the morning of + /// the first day of the parsed month. + /// + /// A valid month string should be "YYYY-MM" YYYY must be four or more digits, MM both + /// must be two digits + fn parse_month_string(&self) -> Option; + + /// + /// + /// Parse the week string, returning an [`OffsetDateTime`] on the Monday of the parsed + /// week. + /// + /// A valid week string should be like {YYYY}-W{WW}, such as "2017-W52" YYYY must be + /// four or more digits, WW both must be two digits + /// + fn parse_week_string(&self) -> Option; + + /// Parse this value as a time string according to + /// . + fn parse_time_string(&self) -> Option; + + /// + /// + /// Parse the local date and time, returning an [`OffsetDateTime`] in UTC or None. + fn parse_local_date_time_string(&self) -> Option; + + /// Validates whether or not this value is a valid date string according to + /// . + fn is_valid_date_string(&self) -> bool { + self.parse_date_string().is_some() + } + + /// Validates whether or not this value is a valid month string according to + /// . + fn is_valid_month_string(&self) -> bool { + self.parse_month_string().is_some() + } + /// Validates whether or not this value is a valid week string according to + /// . + fn is_valid_week_string(&self) -> bool { + self.parse_week_string().is_some() + } + /// Validates whether or not this value is a valid time string according to + /// . + fn is_valid_time_string(&self) -> bool; + + /// Validates whether or not this value is a valid local date time string according to + /// . + fn is_valid_local_date_time_string(&self) -> bool { + self.parse_local_date_time_string().is_some() + } + + /// + fn is_valid_simple_color_string(&self) -> bool; + + /// + fn is_valid_email_address_string(&self) -> bool; +} + +impl FromInputValueString for &str { + fn parse_date_string(&self) -> Option { + // Step 1, 2, 3 + let (year_int, month_int, day_int) = parse_date_component(self)?; + + // Step 4 + if self.split('-').nth(3).is_some() { + return None; + } + + // Step 5, 6 + let month = (month_int as u8).try_into().ok()?; + let date = Date::from_calendar_date(year_int, month, day_int as u8).ok()?; + Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT)) + } + + fn parse_month_string(&self) -> Option { + // Step 1, 2, 3 + let (year_int, month_int) = parse_month_component(self)?; + + // Step 4 + if self.split('-').nth(2).is_some() { + return None; + } + // Step 5 + let month = (month_int as u8).try_into().ok()?; + let date = Date::from_calendar_date(year_int, month, 1).ok()?; + Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT)) + } + + fn parse_week_string(&self) -> Option { + // Step 1, 2, 3 + let mut iterator = self.split('-'); + let year = iterator.next()?; + + // Step 4 + let year_int = year.parse::().ok()?; + if year.len() < 4 || year_int == 0 { + return None; + } + + // Step 5, 6 + let week = iterator.next()?; + let (week_first, week_last) = week.split_at(1); + if week_first != "W" { + return None; + } + + // Step 7 + let week_int = week_last.parse::().ok()?; + if week_last.len() != 2 { + return None; + } + + // Step 8 + let max_week = max_week_in_year(year_int); + + // Step 9 + if week_int < 1 || week_int > max_week { + return None; + } + + // Step 10 + if iterator.next().is_some() { + return None; + } + + // Step 11 + let date = Date::from_iso_week_date(year_int, week_int as u8, Weekday::Monday).ok()?; + Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT)) + } + + fn parse_time_string(&self) -> Option { + // Step 1, 2, 3 + let (hour, minute, second, millisecond) = parse_time_component(self)?; + + // Step 4 + if self.split(':').nth(3).is_some() { + return None; + } + + // Step 5, 6 + let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?; + Some(OffsetDateTime::new_utc( + OffsetDateTime::UNIX_EPOCH.date(), + time, + )) + } + + fn parse_local_date_time_string(&self) -> Option { + // Step 1, 2, 4 + let mut iterator = if self.contains('T') { + self.split('T') + } else { + self.split(' ') + }; + + // Step 3 + let date = iterator.next()?; + let (year, month, day) = parse_date_component(date)?; + + // Step 5 + let time = iterator.next()?; + let (hour, minute, second, millisecond) = parse_time_component(time)?; + + // Step 6 + if iterator.next().is_some() { + return None; + } + + // Step 7, 8, 9 + // TODO: Is this supposed to know the locale's daylight-savings-time rules? + let month = (month as u8).try_into().ok()?; + let date = Date::from_calendar_date(year, month, day as u8).ok()?; + let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?; + Some(OffsetDateTime::new_utc(date, time)) + } + + fn is_valid_time_string(&self) -> bool { + enum State { + HourHigh, + HourLow09, + HourLow03, + MinuteColon, + MinuteHigh, + MinuteLow, + SecondColon, + SecondHigh, + SecondLow, + MilliStop, + MilliHigh, + MilliMiddle, + MilliLow, + Done, + Error, + } + let next_state = |valid: bool, next: State| -> State { + if valid { + next + } else { + State::Error + } + }; + + let state = self.chars().fold(State::HourHigh, |state, c| { + match state { + // Step 1 "HH" + State::HourHigh => match c { + '0' | '1' => State::HourLow09, + '2' => State::HourLow03, + _ => State::Error, + }, + State::HourLow09 => next_state(c.is_ascii_digit(), State::MinuteColon), + State::HourLow03 => next_state(c.is_digit(4), State::MinuteColon), + + // Step 2 ":" + State::MinuteColon => next_state(c == ':', State::MinuteHigh), + + // Step 3 "mm" + State::MinuteHigh => next_state(c.is_digit(6), State::MinuteLow), + State::MinuteLow => next_state(c.is_ascii_digit(), State::SecondColon), + + // Step 4.1 ":" + State::SecondColon => next_state(c == ':', State::SecondHigh), + // Step 4.2 "ss" + State::SecondHigh => next_state(c.is_digit(6), State::SecondLow), + State::SecondLow => next_state(c.is_ascii_digit(), State::MilliStop), + + // Step 4.3.1 "." + State::MilliStop => next_state(c == '.', State::MilliHigh), + // Step 4.3.2 "SSS" + State::MilliHigh => next_state(c.is_ascii_digit(), State::MilliMiddle), + State::MilliMiddle => next_state(c.is_ascii_digit(), State::MilliLow), + State::MilliLow => next_state(c.is_ascii_digit(), State::Done), + + _ => State::Error, + } + }); + + match state { + State::Done | + // Step 4 (optional) + State::SecondColon | + // Step 4.3 (optional) + State::MilliStop | + // Step 4.3.2 (only 1 digit required) + State::MilliMiddle | State::MilliLow => true, + _ => false + } + } + + fn is_valid_simple_color_string(&self) -> bool { + let mut chars = self.chars(); + if self.len() == 7 && chars.next() == Some('#') { + chars.all(|c| c.is_ascii_hexdigit()) + } else { + false + } + } + + fn is_valid_email_address_string(&self) -> bool { + static RE: LazyLock = LazyLock::new(|| { + Regex::new(concat!( + r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?", + r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + )) + .unwrap() + }); + RE.is_match(&self) + } } diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 21e05340cb4..6906270db04 100755 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -4,12 +4,11 @@ use std::borrow::Cow; use std::cell::Cell; +use std::cmp::Ordering; use std::ops::Range; use std::ptr::NonNull; use std::{f64, ptr}; -use chrono::naive::{NaiveDate, NaiveDateTime}; -use chrono::{DateTime, Datelike, Weekday}; use dom_struct::dom_struct; use embedder_traits::{FilterPattern, InputMethodType}; use encoding_rs::Encoding; @@ -30,9 +29,11 @@ use servo_atoms::Atom; use style::attr::AttrValue; use style::str::{split_commas, str_join}; use style_dom::ElementState; +use time_03::{Month, OffsetDateTime, Time}; use unicode_bidi::{bidi_class, BidiClass}; use url::Url; +use super::bindings::str::{FromInputValueString, ToInputValueString}; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; @@ -808,11 +809,9 @@ impl HTMLInputElement { // https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-a-type-mismatch-2 InputType::Email => { if self.Multiple() { - !split_commas(value).all(|s| { - DOMString::from_string(s.to_string()).is_valid_email_address_string() - }) + !split_commas(value).all(|string| string.is_valid_email_address_string()) } else { - !value.is_valid_email_address_string() + !value.str().is_valid_email_address_string() } }, // Other input types don't suffer from type mismatch @@ -863,20 +862,20 @@ impl HTMLInputElement { false }, // https://html.spec.whatwg.org/multipage/#date-state-(type%3Ddate)%3Asuffering-from-bad-input - InputType::Date => !value.is_valid_date_string(), + InputType::Date => !value.str().is_valid_date_string(), // https://html.spec.whatwg.org/multipage/#month-state-(type%3Dmonth)%3Asuffering-from-bad-input - InputType::Month => !value.is_valid_month_string(), + InputType::Month => !value.str().is_valid_month_string(), // https://html.spec.whatwg.org/multipage/#week-state-(type%3Dweek)%3Asuffering-from-bad-input - InputType::Week => !value.is_valid_week_string(), + InputType::Week => !value.str().is_valid_week_string(), // https://html.spec.whatwg.org/multipage/#time-state-(type%3Dtime)%3Asuffering-from-bad-input - InputType::Time => !value.is_valid_time_string(), + InputType::Time => !value.str().is_valid_time_string(), // https://html.spec.whatwg.org/multipage/#local-date-and-time-state-(type%3Ddatetime-local)%3Asuffering-from-bad-input - InputType::DatetimeLocal => value.parse_local_date_and_time_string().is_none(), + InputType::DatetimeLocal => !value.str().is_valid_local_date_time_string(), // https://html.spec.whatwg.org/multipage/#number-state-(type%3Dnumber)%3Asuffering-from-bad-input // https://html.spec.whatwg.org/multipage/#range-state-(type%3Drange)%3Asuffering-from-bad-input InputType::Number | InputType::Range => !value.is_valid_floating_point_number_string(), // https://html.spec.whatwg.org/multipage/#color-state-(type%3Dcolor)%3Asuffering-from-bad-input - InputType::Color => !value.is_valid_simple_color_string(), + InputType::Color => !value.str().is_valid_simple_color_string(), // Other input types don't suffer from bad input _ => false, } @@ -1305,9 +1304,9 @@ impl HTMLInputElementMethods for HTMLInputElement { #[allow(unsafe_code)] fn GetValueAsDate(&self, cx: SafeJSContext) -> Option> { self.convert_string_to_naive_datetime(self.Value()) - .map(|dt| unsafe { + .map(|date_time| unsafe { let time = ClippedTime { - t: dt.and_utc().timestamp_millis() as f64, + t: (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64, }; NonNull::new_unchecked(NewDateObject(*cx, time)) }) @@ -1342,15 +1341,11 @@ impl HTMLInputElementMethods for HTMLInputElement { return self.SetValue(DOMString::from("")); } } - // now we make a Rust date out of it so we can use safe code for the - // actual conversion logic - match milliseconds_to_datetime(msecs) { - Ok(dt) => match self.convert_naive_datetime_to_string(dt) { - Ok(converted) => self.SetValue(converted), - _ => self.SetValue(DOMString::from("")), - }, - _ => self.SetValue(DOMString::from("")), - } + + let Ok(date_time) = OffsetDateTime::from_unix_timestamp_nanos((msecs * 1e6) as i128) else { + return self.SetValue(DOMString::from("")); + }; + self.SetValue(self.convert_datetime_to_dom_string(date_time)) } // https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber @@ -1367,14 +1362,13 @@ impl HTMLInputElementMethods for HTMLInputElement { Err(Error::InvalidState) } else if value.is_nan() { self.SetValue(DOMString::from("")) - } else if let Ok(converted) = self.convert_number_to_string(value) { + } else if let Some(converted) = self.convert_number_to_string(value) { self.SetValue(converted) } else { - // The most literal spec-compliant implementation would - // use bignum chrono types so overflow is impossible, - // but just setting an overflow to the empty string matches - // Firefox's behavior. - // (for example, try input.valueAsNumber=1e30 on a type="date" input) + // The most literal spec-compliant implementation would use bignum types so + // overflow is impossible, but just setting an overflow to the empty string + // matches Firefox's behavior. For example, try input.valueAsNumber=1e30 on + // a type="date" input. self.SetValue(DOMString::from("")) } } @@ -1918,38 +1912,40 @@ impl HTMLInputElement { value.strip_leading_and_trailing_ascii_whitespace(); }, InputType::Date => { - if !value.is_valid_date_string() { + if !value.str().is_valid_date_string() { value.clear(); } }, InputType::Month => { - if !value.is_valid_month_string() { + if !value.str().is_valid_month_string() { value.clear(); } }, InputType::Week => { - if !value.is_valid_week_string() { + if !value.str().is_valid_week_string() { value.clear(); } }, InputType::Color => { - if value.is_valid_simple_color_string() { + if value.str().is_valid_simple_color_string() { value.make_ascii_lowercase(); } else { *value = "#000000".into(); } }, InputType::Time => { - if !value.is_valid_time_string() { + if !value.str().is_valid_time_string() { value.clear(); } }, InputType::DatetimeLocal => { - if value - .convert_valid_normalized_local_date_and_time_string() - .is_none() + match value + .str() + .parse_local_date_time_string() + .map(|date_time| date_time.to_local_date_time_string()) { - value.clear(); + Some(normalized_string) => *value = DOMString::from_string(normalized_string), + None => value.clear(), } }, InputType::Number => { @@ -2112,44 +2108,55 @@ impl HTMLInputElement { } } - // https://html.spec.whatwg.org/multipage/#concept-input-value-string-number + /// fn convert_string_to_number(&self, value: &DOMString) -> Option { match self.input_type() { - InputType::Date => value - .parse_date_string() - .and_then(|(year, month, day)| NaiveDate::from_ymd_opt(year, month, day)) - .and_then(|date| date.and_hms_opt(0, 0, 0)) - .map(|time| time.and_utc().timestamp_millis() as f64), - InputType::Month => match value.parse_month_string() { - // This one returns number of months, not milliseconds - // (specification requires this, presumably because number of - // milliseconds is not consistent across months) - // the - 1.0 is because january is 1, not 0 - Some((year, month)) => Some(((year - 1970) * 12) as f64 + (month as f64 - 1.0)), - _ => None, - }, - InputType::Week => value - .parse_week_string() - .and_then(|(year, weeknum)| NaiveDate::from_isoywd_opt(year, weeknum, Weekday::Mon)) - .and_then(|date| date.and_hms_opt(0, 0, 0)) - .map(|time| time.and_utc().timestamp_millis() as f64), - InputType::Time => match value.parse_time_string() { - Some((hours, minutes, seconds)) => { - Some((seconds + 60.0 * minutes as f64 + 3600.0 * hours as f64) * 1000.0) - }, - _ => None, - }, + // > The algorithm to convert a string to a number, given a string input, is as + // > follows: If parsing a date from input results in an error, then return an + // > error; otherwise, return the number of milliseconds elapsed from midnight + // > UTC on the morning of 1970-01-01 (the time represented by the value + // > "1970-01-01T00:00:00.0Z") to midnight UTC on the morning of the parsed + // > date, ignoring leap seconds. + InputType::Date => value.str().parse_date_string().map(|date_time| { + (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64 + }), + // > The algorithm to convert a string to a number, given a string input, is as + // > follows: If parsing a month from input results in an error, then return an + // > error; otherwise, return the number of months between January 1970 and the + // > parsed month. + // + // This one returns number of months, not milliseconds (specification requires + // this, presumably because number of milliseconds is not consistent across + // months) the - 1.0 is because january is 1, not 0 + InputType::Month => value.str().parse_month_string().map(|date_time| { + ((date_time.year() - 1970) * 12) as f64 + (date_time.month() as u8 - 1) as f64 + }), + // > The algorithm to convert a string to a number, given a string input, is as + // > follows: If parsing a week string from input results in an error, then + // > return an error; otherwise, return the number of milliseconds elapsed from + // > midnight UTC on the morning of 1970-01-01 (the time represented by the + // > value "1970-01-01T00:00:00.0Z") to midnight UTC on the morning of the + // > Monday of the parsed week, ignoring leap seconds. + InputType::Week => value.str().parse_week_string().map(|date_time| { + (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64 + }), + // > The algorithm to convert a string to a number, given a string input, is as + // > follows: If parsing a time from input results in an error, then return an + // > error; otherwise, return the number of milliseconds elapsed from midnight to + // > the parsed time on a day with no time changes. + InputType::Time => value + .str() + .parse_time_string() + .map(|date_time| (date_time.time() - Time::MIDNIGHT).whole_milliseconds() as f64), + // > The algorithm to convert a string to a number, given a string input, is as + // > follows: If parsing a date and time from input results in an error, then + // > return an error; otherwise, return the number of milliseconds elapsed from + // > midnight on the morning of 1970-01-01 (the time represented by the value + // > "1970-01-01T00:00:00.0") to the parsed local date and time, ignoring leap + // > seconds. InputType::DatetimeLocal => { - // Is this supposed to know the locale's daylight-savings-time rules? - value.parse_local_date_and_time_string().and_then(|date| { - let seconds = date.seconds as u32; - let milliseconds = ((date.seconds - seconds as f64) * 1000.) as u32; - Some( - NaiveDate::from_ymd_opt(date.year, date.month, date.day)? - .and_hms_milli_opt(date.hour, date.minute, seconds, milliseconds)? - .and_utc() - .timestamp_millis() as f64, - ) + value.str().parse_local_date_time_string().map(|date_time| { + (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64 }) }, InputType::Number | InputType::Range => value.parse_floating_point_number(), @@ -2159,94 +2166,73 @@ impl HTMLInputElement { } } - // https://html.spec.whatwg.org/multipage/#concept-input-value-string-number - fn convert_number_to_string(&self, value: f64) -> Result { + /// + fn convert_number_to_string(&self, value: f64) -> Option { match self.input_type() { - InputType::Date => { - let datetime = milliseconds_to_datetime(value)?; - Ok(DOMString::from(datetime.format("%Y-%m-%d").to_string())) + InputType::Date | InputType::Week | InputType::Time | InputType::DatetimeLocal => { + OffsetDateTime::from_unix_timestamp_nanos((value * 1e6) as i128) + .ok() + .map(|value| self.convert_datetime_to_dom_string(value)) }, InputType::Month => { - // interpret value as months(not millis) in epoch, return monthstring - let year_from_1970 = (value / 12.0).floor(); - let month = (value - year_from_1970 * 12.0).floor() as u32 + 1; // january is 1, not 0 - let year = (year_from_1970 + 1970.0) as u64; - Ok(DOMString::from(format!("{:04}-{:02}", year, month))) - }, - InputType::Week => { - let datetime = milliseconds_to_datetime(value)?; - let year = datetime.iso_week().year(); // not necessarily the same as datetime.year() - let week = datetime.iso_week().week(); - Ok(DOMString::from(format!("{:04}-W{:02}", year, week))) - }, - InputType::Time => { - let datetime = milliseconds_to_datetime(value)?; - Ok(DOMString::from(datetime.format("%H:%M:%S%.3f").to_string())) - }, - InputType::DatetimeLocal => { - let datetime = milliseconds_to_datetime(value)?; - Ok(DOMString::from( - datetime.format("%Y-%m-%dT%H:%M:%S%.3f").to_string(), - )) + // > The algorithm to convert a number to a string, given a number input, + // > is as follows: Return a valid month string that represents the month + // > that has input months between it and January 1970. + let date = OffsetDateTime::UNIX_EPOCH; + let years = (value / 12.) as i32; + let year = date.year() + years; + + let months = value as i32 - (years * 12); + let months = match months.cmp(&0) { + Ordering::Less => (12 - months) as u8, + Ordering::Equal | Ordering::Greater => months as u8, + } + 1; + + let date = date + .replace_year(year) + .ok()? + .replace_month(Month::try_from(months).ok()?) + .ok()?; + Some(self.convert_datetime_to_dom_string(date)) }, InputType::Number | InputType::Range => { let mut value = DOMString::from(value.to_string()); - value.set_best_representation_of_the_floating_point_number(); - - Ok(value) + Some(value) }, - // this won't be called from other input types - _ => unreachable!(), + _ => unreachable!("Should not have called convert_number_to_string for non-Date types"), } } - // https://html.spec.whatwg.org/multipage/#concept-input-value-string-date + // // This does the safe Rust part of conversion; the unsafe JS Date part // is in GetValueAsDate - fn convert_string_to_naive_datetime(&self, value: DOMString) -> Option { + fn convert_string_to_naive_datetime(&self, value: DOMString) -> Option { match self.input_type() { - InputType::Date => value - .parse_date_string() - .and_then(|(y, m, d)| NaiveDate::from_ymd_opt(y, m, d)) - .and_then(|date| date.and_hms_opt(0, 0, 0)), - InputType::Time => value.parse_time_string().and_then(|(h, m, s)| { - let whole_seconds = s.floor(); - let nanos = ((s - whole_seconds) * 1e9).floor() as u32; - NaiveDate::from_ymd_opt(1970, 1, 1) - .and_then(|date| date.and_hms_nano_opt(h, m, whole_seconds as u32, nanos)) - }), - InputType::Week => value - .parse_week_string() - .and_then(|(iso_year, week)| { - NaiveDate::from_isoywd_opt(iso_year, week, Weekday::Mon) - }) - .and_then(|date| date.and_hms_opt(0, 0, 0)), - InputType::Month => value - .parse_month_string() - .and_then(|(y, m)| NaiveDate::from_ymd_opt(y, m, 1)) - .and_then(|date| date.and_hms_opt(0, 0, 0)), + InputType::Date => value.str().parse_date_string(), + InputType::Time => value.str().parse_time_string(), + InputType::Week => value.str().parse_week_string(), + InputType::Month => value.str().parse_month_string(), + InputType::DatetimeLocal => value.str().parse_local_date_time_string(), // does not apply to other types _ => None, } } - // https://html.spec.whatwg.org/multipage/#concept-input-value-date-string - // This does the safe Rust part of conversion; the unsafe JS Date part - // is in SetValueAsDate - fn convert_naive_datetime_to_string(&self, value: NaiveDateTime) -> Result { - match self.input_type() { - InputType::Date => Ok(DOMString::from(value.format("%Y-%m-%d").to_string())), - InputType::Month => Ok(DOMString::from(value.format("%Y-%m").to_string())), - InputType::Week => { - let year = value.iso_week().year(); // not necessarily the same as value.year() - let week = value.iso_week().week(); - Ok(DOMString::from(format!("{:04}-W{:02}", year, week))) + /// + /// This does the safe Rust part of conversion; the unsafe JS Date part + /// is in SetValueAsDate + fn convert_datetime_to_dom_string(&self, value: OffsetDateTime) -> DOMString { + DOMString::from_string(match self.input_type() { + InputType::Date => value.to_date_string(), + InputType::Month => value.to_month_string(), + InputType::Week => value.to_week_string(), + InputType::Time => value.to_time_string(), + InputType::DatetimeLocal => value.to_local_date_time_string(), + _ => { + unreachable!("Should not have called convert_datetime_to_string for non-Date types") }, - InputType::Time => Ok(DOMString::from(value.format("%H:%M:%S%.3f").to_string())), - // this won't be called from other input types - _ => unreachable!(), - } + }) } } @@ -2877,16 +2863,6 @@ fn round_halves_positive(n: f64) -> f64 { } } -fn milliseconds_to_datetime(value: f64) -> Result { - let seconds = (value / 1000.0).floor(); - let milliseconds = value - (seconds * 1000.0); - let nanoseconds = milliseconds * 1e6; - match DateTime::from_timestamp(seconds as i64, nanoseconds as u32) { - Some(datetime) => Ok(datetime.naive_utc()), - None => Err(()), - } -} - // This is used to compile JS-compatible regex provided in pattern attribute // that matches only the entirety of string. // https://html.spec.whatwg.org/multipage/#compiled-pattern-regular-expression diff --git a/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasdate.html.ini b/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasdate.html.ini deleted file mode 100644 index 528fb0d0e82..00000000000 --- a/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasdate.html.ini +++ /dev/null @@ -1,9 +0,0 @@ -[input-valueasdate.html] - [valueAsDate setter on type time (new Date("1970-01-01T00:00:00.000Z"))] - expected: FAIL - - [valueAsDate setter on type time (new Date("1970-01-01T12:00:00.000Z"))] - expected: FAIL - - [valueAsDate setter on type time (new Date("1970-01-01T23:59:00.000Z"))] - expected: FAIL diff --git a/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasnumber.html.ini b/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasnumber.html.ini deleted file mode 100644 index 123706b36a6..00000000000 --- a/tests/wpt/meta/html/semantics/forms/the-input-element/input-valueasnumber.html.ini +++ /dev/null @@ -1,9 +0,0 @@ -[input-valueasnumber.html] - [valueAsNumber setter on type time (actual valueAsNumber: 86340000, expected value: 23:59)] - expected: FAIL - - [valueAsNumber setter on type time (actual valueAsNumber: 43200000, expected value: 12:00)] - expected: FAIL - - [valueAsNumber setter on type time (actual valueAsNumber: 0, expected value: 00:00)] - expected: FAIL