JavaScript: a RegExp source escapes a line terminator after a backslash, and Date.parse reads its words in place

A constructor pattern with a backslash before a line terminator wrote the raw terminator into source and toString, so the text did not read back as a literal; it now writes the terminator's escape, which matches the same unit (ECMA-262 5.1 section 15.10.2.10).

Date.parse no longer collects every word of a text date into a list before it checks them, and reads month, weekday and zone names without copying the word.
This commit is contained in:
dotCooCoo
2026-11-07 20:33:40 -08:00
parent 971a0d6626
commit 328aa0e340
3 changed files with 26 additions and 14 deletions
+12 -14
View File
@@ -346,28 +346,22 @@ fn parse_iso(s: &[u8]) -> Option<f64> {
/// 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<f64> {
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<f64> = 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<f64> {
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<f64> {
continue;
}
if w.contains(':') {
if w.split(':').count() > 3 {
return None;
}
let parts: Vec<&str> = w.split(':').collect();
let field = |i: usize| -> Option<f64> { parts.get(i).map(|p| p.parse::<f64>().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())) {
+6
View File
@@ -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;
+8
View File
@@ -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}");