//! JavaScript semantics the TypeScript worker relies on: `\s` and `trim()`, //! UTF-16 lengths, `parseInt`, `Date.parse` (for Usenet dates) and sha1 hex. use sha1::{Digest, Sha1}; /// JS `\s` / `trim()` whitespace (WhiteSpace and LineTerminator). pub fn is_space(c: char) -> bool { matches!( c, '\t' | '\n' | '\u{0B}' | '\u{0C}' | '\r' | ' ' | '\u{A0}' | '\u{1680}' | '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}' ) } pub fn trim(s: &str) -> &str { s.trim_matches(is_space) } /// `s.replace(/\s+/g, ' ')`. pub fn collapse_spaces(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut in_space = false; for c in s.chars() { if is_space(c) { if !in_space { out.push(' '); in_space = true; } } else { out.push(c); in_space = false; } } out } /// `s.split(/\s+/)`: like JS, a leading or trailing run gives an empty word. pub fn split_spaces(s: &str) -> Vec<&str> { let mut out = Vec::new(); let mut start = 0; let mut in_space = false; let mut run_start = 0; for (i, c) in s.char_indices() { if is_space(c) { if !in_space { run_start = i; in_space = true; } } else if in_space { out.push(&s[start..run_start]); start = i; in_space = false; } } if in_space { out.push(&s[start..run_start]); out.push(""); } else { out.push(&s[start..]); } out } /// A string's `.length` in JavaScript (UTF-16 code units). pub fn len16(s: &str) -> usize { if s.is_ascii() { s.len() } else { s.encode_utf16().count() } } /// `Number.parseInt(s, 10)`, None for NaN. pub fn parse_int(s: &str) -> Option { let s = s.trim_start_matches(is_space); let (neg, digits) = match s.as_bytes().first() { Some(b'-') => (true, &s[1..]), Some(b'+') => (false, &s[1..]), _ => (false, s), }; let mut n: i64 = 0; let mut any = false; for b in digits.bytes() { if !b.is_ascii_digit() { break; } n = n.wrapping_mul(10).wrapping_add((b - b'0') as i64); any = true; } if any { Some(if neg { -n } else { n }) } else { None } } /// Days since 1970-01-01 of a proleptic Gregorian date (month 1..=12; a day past the month's end runs on, as in JS). fn days_from_civil(y: i64, m: i64, d: i64) -> i64 { let y = if m <= 2 { y - 1 } else { y }; let era = y.div_euclid(400); let yoe = y - era * 400; let mp = (m + 9) % 12; let doy = (153 * mp + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe - 719_468 } /// The usual Date headers: [Day, ]D[D] Mon YY[YY] HH:MM:SS (GMT|UTC|UT|±hhmm). None for anything else. fn fast_date(s: &[u8]) -> Option { let mut i = 0; if s.len() > 4 && s[3] == b',' && s[..3].iter().all(u8::is_ascii_alphabetic) { i = 4; while s.get(i) == Some(&b' ') { i += 1; } } let num = |i: &mut usize, min: usize, max: usize| -> Option { let start = *i; while *i < s.len() && s[*i].is_ascii_digit() && *i - start < max { *i += 1; } if *i - start < min || s.get(*i).is_some_and(u8::is_ascii_digit) { return None; } Some(std::str::from_utf8(&s[start..*i]).ok()?.parse().ok()?) }; let expect = |i: &mut usize, c: u8| -> Option<()> { (s.get(*i) == Some(&c)).then(|| *i += 1) }; let day = num(&mut i, 1, 2)?; expect(&mut i, b' ')?; const MONTHS: [&[u8]; 12] = [b"jan", b"feb", b"mar", b"apr", b"may", b"jun", b"jul", b"aug", b"sep", b"oct", b"nov", b"dec"]; let mon = s.get(i..i + 3)?; let month = MONTHS.iter().position(|m| mon.iter().zip(m.iter()).all(|(a, b)| a.to_ascii_lowercase() == *b))? as i64 + 1; i += 3; expect(&mut i, b' ')?; let year_start = i; let mut year = num(&mut i, 2, 4)?; match i - year_start { 2 => year += if year < 50 { 2000 } else { 1900 }, 4 => {} _ => return None, } expect(&mut i, b' ')?; let hour = num(&mut i, 2, 2)?; expect(&mut i, b':')?; let minute = num(&mut i, 2, 2)?; expect(&mut i, b':')?; let second = num(&mut i, 2, 2)?; expect(&mut i, b' ')?; if day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 { return None; } let offset_minutes = match &s[i..] { b"GMT" | b"UTC" | b"UT" => 0, [sign @ (b'+' | b'-'), h1, h2, m1, m2] if [h1, h2, m1, m2].iter().all(|c| c.is_ascii_digit()) => { let minutes = ((h1 - b'0') as i64 * 10 + (h2 - b'0') as i64) * 60 + (m1 - b'0') as i64 * 10 + (m2 - b'0') as i64; if *sign == b'+' { minutes } else { -minutes } } _ => return None, }; let days = days_from_civil(year, month, day); Some((((days * 24 + hour) * 60 + minute - offset_minutes) * 60 + second) * 1000) } /// `Date.parse` for the Date header (RFC 5322 and the obsolete forms); None for NaN. pub fn parse_date(s: &str) -> Option { let s = trim(s); if let Some(t) = fast_date(s.as_bytes()) { return Some(t); } parse_date_chrono(s) } fn parse_date_chrono(s: &str) -> Option { // "UTC" isn't an RFC 5322 zone (UT and GMT are), but Date.parse takes it (~a quarter of a.b.boneless). let owned; let s = match s.strip_suffix(" UTC") { Some(rest) => { owned = format!("{rest} GMT"); owned.as_str() } None => s, }; if let Ok(d) = chrono::DateTime::parse_from_rfc2822(s) { return Some(d.timestamp_millis()); } // "Mon, 5 Oct 2026 10:00:00 +0000 (UTC)" and other trailing comments. if let Some(i) = s.find(" (") { if let Ok(d) = chrono::DateTime::parse_from_rfc2822(&s[..i]) { return Some(d.timestamp_millis()); } } None } /// sha1 of the UTF-8 text, as lowercase hex (node's `hash('sha1', text)`). pub fn sha1_hex(text: &str) -> String { let digest = Sha1::digest(text.as_bytes()); let mut out = String::with_capacity(40); for b in digest { out.push(char::from_digit((b >> 4) as u32, 16).unwrap()); out.push(char::from_digit((b & 15) as u32, 16).unwrap()); } out } /// JSON.stringify of a string. pub fn json_str(s: &str, out: &mut String) { out.push('"'); for c in s.chars() { match c { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), '\u{08}' => out.push_str("\\b"), '\u{0C}' => out.push_str("\\f"), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), c => out.push(c), } } out.push('"'); } #[cfg(test)] mod tests { use super::*; #[test] fn split_like_js() { assert_eq!(split_spaces("a b"), vec!["a", "b"]); assert_eq!(split_spaces(" a b "), vec!["", "a", "b", ""]); assert_eq!(split_spaces(""), vec![""]); } #[test] fn parse_int_like_js() { assert_eq!(parse_int("123abc"), Some(123)); assert_eq!(parse_int(" -5"), Some(-5)); assert_eq!(parse_int("x"), None); } #[test] fn dates() { assert_eq!(parse_date("Mon, 05 Oct 2026 10:00:00 +0000"), Some(1_791_194_400_000)); assert_eq!(parse_date("5 Oct 2026 10:00:00 GMT"), Some(1_791_194_400_000)); assert_eq!(parse_date("Mon, 05 Oct 26 10:00:00 UTC"), Some(1_791_194_400_000)); for d in [ "Mon, 05 Oct 2026 10:00:00 +0000", "Mon, 5 Oct 2026 12:00:00 +0200", "05 Oct 2026 10:00:00 GMT", "Thu, 01 Jan 1970 00:00:00 GMT", "Sat, 29 Feb 2020 23:59:59 -0130", "Fri, 31 Dec 99 00:00:00 UTC", "Mon, 05 Oct 2026 10:00:00 UT", ] { assert_eq!(fast_date(d.as_bytes()), parse_date_chrono(d), "{d}"); } assert_eq!(fast_date(b"Mon, 05 Oct 2026 10:00:00 EST"), None); } }