diff --git a/crates/js/src/date_builtins.rs b/crates/js/src/date_builtins.rs index e81f73ee9..55f9e1e81 100644 --- a/crates/js/src/date_builtins.rs +++ b/crates/js/src/date_builtins.rs @@ -346,28 +346,22 @@ fn parse_iso(s: &[u8]) -> Option { /// year, an optional `HH:mm[:ss]` time and an optional `GMT[±hhmm]` offset, in either order of /// day and month. Without an offset the time is local. fn parse_text(interp: &Interp, s: &str) -> Option { - let mut words: Vec<&str> = Vec::new(); - for w in s.split(|c: char| c.is_whitespace() || c == ',') { - if w.starts_with('(') { - break; - } - if !w.is_empty() { - words.push(w); - } - } + let starts = |w: &str, prefix: &str| w.get(..prefix.len()).is_some_and(|head| head.eq_ignore_ascii_case(prefix)); let (mut month, mut date, mut year) = (None, None, None); let (mut h, mut m, mut sec) = (0.0, 0.0, 0.0); let mut offset: Option = None; + // The words are read one at a time, and none is copied. + let words = s.split(|c: char| c.is_whitespace() || c == ',').take_while(|w| !w.starts_with('(')).filter(|w| !w.is_empty()); for w in words { - let lower = w.to_ascii_lowercase(); - if DAYS.iter().any(|d| lower.starts_with(&d.to_ascii_lowercase())) && month.is_none() && date.is_none() && w.len() >= 3 && !w.contains(':') { + if DAYS.iter().any(|d| starts(w, d)) && month.is_none() && date.is_none() && w.len() >= 3 && !w.contains(':') { continue; } - if let Some(mi) = MONTHS.iter().position(|mo| lower.starts_with(&mo.to_ascii_lowercase())) { + if let Some(mi) = MONTHS.iter().position(|mo| starts(w, mo)) { month = Some(mi as f64); continue; } - if let Some(rest) = lower.strip_prefix("gmt").or_else(|| lower.strip_prefix("utc")).or_else(|| (lower == "z").then_some("")) { + let zone = if starts(w, "gmt") || starts(w, "utc") { w.get(3..) } else { w.eq_ignore_ascii_case("z").then_some("") }; + if let Some(rest) = zone { if rest.is_empty() { offset = Some(0.0); continue; @@ -377,7 +371,8 @@ fn parse_text(interp: &Interp, s: &str) -> Option { Some(b'-') => (-1.0, rest.get(1..)?), _ => return None, }; - let digits = digits.replace(':', ""); + // Five units are enough to tell whether four digits remain without the colons. + let digits: String = digits.chars().filter(|&c| c != ':').take(5).collect(); if digits.len() != 4 || !digits.bytes().all(|b| b.is_ascii_digit()) { return None; } @@ -386,6 +381,9 @@ fn parse_text(interp: &Interp, s: &str) -> Option { continue; } if w.contains(':') { + if w.split(':').count() > 3 { + return None; + } let parts: Vec<&str> = w.split(':').collect(); let field = |i: usize| -> Option { parts.get(i).map(|p| p.parse::().ok()).unwrap_or(Some(0.0)) }; if parts.len() < 2 || parts.len() > 3 || !parts.iter().all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())) { diff --git a/crates/js/src/regexp_builtins.rs b/crates/js/src/regexp_builtins.rs index 8733ded43..fcafd82c5 100644 --- a/crates/js/src/regexp_builtins.rs +++ b/crates/js/src/regexp_builtins.rs @@ -117,6 +117,12 @@ fn source_units(pattern: &[u16], mut emit: impl FnMut(&[u16])) { while let Some(&u) = pattern.get(i) { match u { 0x5C => { + // A backslash before a line terminator escapes the terminator as itself (section + // 15.10.2.10); the terminator's escape written below matches the same unit. + if pattern.get(i + 1).is_some_and(|&n| crate::chars::is_line_terminator(u32::from(n))) { + i += 1; + continue; + } emit(pattern.get(i..i + 2).unwrap_or(&[BACKSLASH])); i += 2; continue; diff --git a/crates/js/src/text_tests.rs b/crates/js/src/text_tests.rs index 70e3cd6eb..b150ba3f5 100644 --- a/crates/js/src/text_tests.rs +++ b/crates/js/src/text_tests.rs @@ -411,6 +411,12 @@ fn a_constructed_source_counts_against_the_heap_before_it_is_built() { let Ok(source) = crate::regexp_builtins::source_text(interp, &pattern) else { panic!("the source fits") }; assert_eq!(source.flat().len(), 60_000); assert_eq!(string("String(new RegExp('a/\\\\/[/]\\n\\r\\u2028\\u2029'))"), "/a\\/\\/[/]\\n\\r\\u2028\\u2029/"); + // A backslash before a line terminator reads back as the terminator's escape. + let escaped = "var r = new RegExp(String.fromCharCode(92, 10, 124, 92, 0x2028)); var back = eval(String(r));"; + assert_eq!( + string(&format!("{escaped} String(r) + ' ' + r.test('\\n') + back.test('\\n') + back.test('\\u2028') + back.test('x')")), + "/\\n|\\u2028/ truetruetruefalse" + ); } #[test] @@ -600,6 +606,8 @@ fn dates_round_trip_through_their_strings() { "Object.prototype.toString.call(Date.prototype) === '[object Date]' && isNaN_(Date.prototype.getTime())", "var d = new Date(0); d + d === d.toString() + d.toString() && d - d === 0 && d == d.toString()", "isNaN_(Date.parse('1970-00-01')) && isNaN_(Date.parse('1970-13-01')) && isNaN_(Date.parse('1970-02-30'))", + "Date.parse('thu, 01 JAN 1970 00:00:00 gmt') === 0 && Date.parse('jan 2 1970 utc+01:00') === 82800000", + "isNaN_(Date.parse('Jan 1 1970 1:2:3:4 GMT')) && isNaN_(Date.parse('Jan 1 1970 GMT+01000')) && isNaN_(Date.parse('Jan 1 1970 x'))", ] { let full = format!("function Math_floor(t) {{ return t - t % 1000; }} function isNaN_(n) {{ return n !== n; }} {src}"); assert_eq!(run_in(&mut realm, &full), JsValue::Bool(true), "{src}");