mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 06:06:38 +00:00
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:
@@ -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())) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}");
|
||||
|
||||
Reference in New Issue
Block a user