diff --git a/Cargo.toml b/Cargo.toml index 5ba2515..812552d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ num-traits = { version = "0.2.0", optional = true } log = { version = "0.4.8", optional = true } once_cell = "1.2.0" +[dev-dependencies] +rstest = "0.15.0" + [workspace] members = [ "libquickjs-sys", diff --git a/libquickjs-sys/embed/quickjs/quickjs.c b/libquickjs-sys/embed/quickjs/quickjs.c index a3325b3..68f0120 100644 --- a/libquickjs-sys/embed/quickjs/quickjs.c +++ b/libquickjs-sys/embed/quickjs/quickjs.c @@ -1436,7 +1436,7 @@ static inline int is_digit(int c) { } static inline int is_space_like(char c) { - return c == ' ' || c == ',' || c == ':' || c == '-'; + return c == ' ' || c == ',' || c == ':' || c == '-' || c == '/'; } typedef struct JSClassShortDef { @@ -48322,7 +48322,7 @@ static void string_skip_spaces_and_comments(JSString *sp, int *pp) { int nxt = *pp + 1; // interpret - before a number as a sign rather than a comment char - if (ch == '-' && nxt < sp->len && is_digit(string_get(sp, nxt))) { + if (ch == '-' && nesting == 0 && nxt < sp->len && is_digit(string_get(sp, nxt))) { break; } if (!is_space_like(ch)) { @@ -48339,12 +48339,10 @@ static void string_skip_spaces_and_comments(JSString *sp, int *pp) { } } -static BOOL char_eq_ignorecase(char c1, char c2) { - if (c1 == c2) return TRUE; - - if (c1 >= 'A' && c1 <= 'Z' && c2 == c1 + 32) return TRUE; - if (c1 >= 'a' && c1 <= 'z' && c2 == c1 - 32) return TRUE; - return FALSE; +static inline BOOL char_eq_ignorecase(char c1, char c2) { + if ((c1 >= 'A' && c1 <= 'Z') || (c1 >= 'a' && c1 <= 'z')) + return (c1 | 0x20) == (c2 | 0x20); + return c1 == c2; } static BOOL string_eq_ignorecase(JSString *s1, int p, const char *s2, int len) { @@ -48442,6 +48440,34 @@ static int string_get_milliseconds(JSString *sp, int *pp, int64_t *pval) { return 0; } +static int string_get_num_timezone(JSString *sp, int *pp, int64_t *pval) +{ + int p = *pp; + + int64_t o; + if (string_get_signed_digits(sp, &p, &o)) + return -1; + + if (o < -9959 || o > 9959) { + return -1; + } + + int sgn = (o < 0) ? -1 : 1; + o = abs((int32_t) o); + + if (string_get(sp, p) != ':') { + *pval = ((o / 100) * 60 + (o % 100)) * sgn; + } else { + p++; + int64_t o2; + if (string_get_digits(sp, &p, &o2)) + return -1; + *pval = (o * 60 + o2) * sgn; + } + + *pp = p; + return 0; +} static int find_abbrev(JSString *sp, int p, const char *list, int count) { int n, i; @@ -48496,7 +48522,18 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, sp = JS_VALUE_GET_STRING(s); p = 0; - if (p < sp->len && (((c = string_get(sp, p)) >= '0' && c <= '9') || c == '+' || c == '-')) { + string_skip_spaces_and_comments(sp, &p); + + int end_of_digits = p; + if (string_get(sp, end_of_digits) == '+' || string_get(sp, end_of_digits) == '-') { + p++; + } + while (end_of_digits < sp->len && is_digit(string_get(sp, end_of_digits))) { + end_of_digits++; + } + + if ((end_of_digits - p) > 0 && + (string_get(sp, end_of_digits) == '-' || string_get(sp, end_of_digits) == 'T')) { /* ISO format */ /* year field can be negative */ if (string_get_signed_digits(sp, &p, &fields[0])) @@ -48506,23 +48543,28 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (p >= sp->len) break; switch(i) { - case 1: - case 2: + case 1: // Year + case 2: // Month c = '-'; break; - case 3: + case 3: // Day c = 'T'; break; - case 4: - case 5: + case 4: // Hour + case 5: // Minute c = ':'; break; - case 6: + case 6: // Second c = '.'; break; } - if (string_get(sp, p) != c) - break; + if (string_get(sp, p) != c) { + // 2000T08:00Z + if (i < 3 && string_get(sp, p) == 'T') { + i = 3; + } + else break; + } p++; if (i == 6) { if (string_get_milliseconds(sp, &p, &fields[i])) @@ -48532,6 +48574,9 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, goto done; } } + // Hour only is invalid + if (i == 4) goto done; + /* no time: UTC by default */ is_local = (i > 3); fields[1] -= 1; @@ -48579,7 +48624,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (p - word_start >= 3) { month = find_abbrev(sp, word_start, month_names, 12); } - string_skip_spaces_and_comments(sp, &p); // and comments + string_skip_spaces_and_comments(sp, &p); word_start = p; } else { p++; @@ -48593,7 +48638,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, month = find_abbrev(sp, word_start, month_names, 12); } - string_skip_spaces_and_comments(sp, &p); // and comments + string_skip_spaces_and_comments(sp, &p); if (sp->len <= p) goto done; @@ -48602,7 +48647,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (string_get_digits(sp, &p, &day)) goto done; - int64_t year = 0; + int64_t year = -1; if (day > 31) { if (string_get(sp, p) != '/') goto done; @@ -48631,7 +48676,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (string_get_digits(sp, &p, &day)) goto done; - if (string_get(sp, p) != '/') + if (!is_space_like(string_get(sp, p))) goto done; p++; if (sp->len <= p) @@ -48640,40 +48685,31 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (string_get(sp, p) == '-') { p++; } - string_skip_spaces_and_comments(sp, &p); // and comments - - if (string_get(sp, p) == ',') { - p++; - } + string_skip_spaces_and_comments(sp, &p); + // Jan,2000,08:00:00 UT if (month == -1) { month = find_abbrev(sp, p, month_names, 12); if (month == -1) goto done; - while (p < sp->len && string_get(sp, p) != '-' && string_get(sp, p) != ' ') { + while (p < sp->len && !is_space_like(string_get(sp, p))) { p++; } if (sp->len <= p) goto done; - - // '-99 23:12:40 GMT' - if (string_get(sp, p) != '-' && string_get(sp, p) != '/' && string_get(sp, p) != ' ') { - goto done; - } p++; } - - if (month < 0 || month > 11) { - goto done; - } } - if (year <= 0) { - if (string_get_digits(sp, &p, &year)) - goto done; - if (year <= 0) { - goto done; + string_skip_spaces_and_comments(sp, &p); + + if (year < 0) { + // Year following, e.g. 01 Jan 2000 08:00:00 UT + // Time following, e.g. Jan 01 08:00:00 UT 2000 + if (sp->len <= p + 2 || string_get(sp, p + 2) != ':') { + if (string_get_digits(sp, &p, &year)) + goto done; } } @@ -48689,9 +48725,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, year = -1; } else if (is_space_like(string_get(sp, p))) { p++; - string_skip_spaces_and_comments(sp, &p); // and comments - } else { - goto done; + string_skip_spaces_and_comments(sp, &p); } // Read a number? If not, this might be a timezone name. @@ -48711,10 +48745,6 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (string_get_digits(sp, &p, &minute)) goto done; - - if (minute < 0 || minute > 59) { - goto done; - } // ':40 GMT' // seconds are optional in rfc822 + rfc2822 @@ -48723,10 +48753,6 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, if (string_get_digits(sp, &p, &second)) goto done; - - if (second < 0 || second > 59) { - goto done; - } // disallow trailing colon seconds if (string_get(sp, p) == ':') { @@ -48736,7 +48762,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, goto done; } - string_skip_spaces_and_comments(sp, &p); // and comments + string_skip_spaces_and_comments(sp, &p); if (string_eq_ignorecase(sp, p, "AM", 2)) { if (hour > 12) { @@ -48746,7 +48772,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, hour = 0; } p += 2; - string_skip_spaces_and_comments(sp, &p); // and comments + string_skip_spaces_and_comments(sp, &p); } else if (string_eq_ignorecase(sp, p, "PM", 2)) { if (hour > 12) { goto done; @@ -48755,7 +48781,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, hour += 12; } p += 2; - string_skip_spaces_and_comments(sp, &p); // and comments + string_skip_spaces_and_comments(sp, &p); } } } @@ -48764,26 +48790,8 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, // Some websites omit the time zone if (sp->len > p) { if (string_get(sp, p) == '+' || string_get(sp, p) == '-') { - int64_t o; - if (string_get_digits(sp, &p, &o)) + if (string_get_num_timezone(sp, &p, &tz)) goto done; - - if (o < -9959 || o > 9959) { - goto done; - } - - int sgn = (o < 0) ? -1 : 1; - o = abs((int32_t) o); - - if (string_get(sp, p) != ':') { - tz = ((o / 100) * 60 + (o % 100)) * sgn; - } else { - p++; - int64_t o2; - if (string_get_digits(sp, &p, &o2)) - goto done; - tz = (o * 60 + o2) * sgn; - } is_local = FALSE; } else { for (int i = 0; i < sizeof(known_zones) / sizeof(struct KnownZone); i++) { @@ -48791,6 +48799,15 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, tz = known_zones[i].tzOffset; p += strlen(known_zones[i].tzName); is_local = FALSE; + + // TZ offset (GMT+0) + if (string_get(sp, p) == '+' || string_get(sp, p) == '-') { + int64_t o; + if (string_get_num_timezone(sp, &p, &o)) + goto done; + + tz += o; + } break; } } @@ -48828,6 +48845,16 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val, fields[5] = second; } + // Validate fields + if (fields[1] < 0 || fields[1] > 11 || + fields[2] < 1 || fields[2] > 31 || + fields[3] < 0 || fields[3] > 24 || + fields[4] < 0 || fields[4] > 59 || + fields[5] < 0 || fields[5] > 59 || + fields[6] < 0 || fields[6] > 999 || + fields[3] * 3600 * 1000 + fields[4] * 60000 + fields[5] * 1000 + fields[6] > 24 * 3600 * 1000 + ) goto done; + for(i = 0; i < 7; i++) fields1[i] = fields[i]; d = set_date_fields(fields1, is_local) - (tz * 60000); diff --git a/src/lib.rs b/src/lib.rs index aeaeab5..9c5bb82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ //! "#).unwrap(); //! ``` +#![allow(dead_code)] #![deny(missing_docs)] mod bindings; diff --git a/src/tests.rs b/src/tests.rs index a6a4773..b35900b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, convert::TryInto}; -#[cfg(feature = "chrono")] -use chrono::{Date, DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc}; +use rstest::rstest; use super::*; @@ -626,9 +625,162 @@ fn test_global_setter() { ctx.eval("a + 1").unwrap(); } -#[cfg(feature = "chrono")] +// Test suite was taken from V8 +// Source: https://github.com/v8/v8/blob/9433ad119aadfe10d60935029195c31f25ab8624/test/mjsunit/date-parse.js + +#[rstest] +// testCasesES5Misc +#[case("2000-01-01T08:00:00.000Z", 946713600000)] +#[case("2000-01-01T08:00:00Z", 946713600000)] +#[case("2000-01-01T08:00Z", 946713600000)] +#[case("2000-01T08:00:00.000Z", 946713600000)] +#[case("2000T08:00:00.000Z", 946713600000)] +#[case("2000T08:00Z", 946713600000)] +#[case("2000-01T00:00:00.000-08:00", 946713600000)] +#[case("2000-01T08:00:00.001Z", 946713600001)] +#[case("2000-01T08:00:00.099Z", 946713600099)] +#[case("2000-01T08:00:00.999Z", 946713600999)] +#[case("2000-01T00:00:00.001-08:00", 946713600001)] +#[case("2000-01-01T24:00Z", 946771200000)] +#[case("2000-01-01T24:00:00Z", 946771200000)] +#[case("2000-01-01T24:00:00.000Z", 946771200000)] +fn test_date_iso(#[case] input: &str, #[case] expect: i64) { + let ctx = Context::new().unwrap(); + let res = ctx + .eval(&format!(r#"new Date("{}").getTime()"#, input)) + .unwrap(); + + let n: f64 = res.try_into().unwrap(); + assert_eq!(n, expect as f64); +} + +#[rstest] +#[case("Sat, 01-Jan-2000 08:00:00 GMT", 946713600000)] +#[case("Sat, 01-Jan-2000 08:00:00 GMT+0", 946713600000)] +#[case("Sat, 01-Jan-2000 08:00:00 GMT+00", 946713600000)] +#[case("Sat, 01-Jan-2000 08:00:00 GMT+000", 946713600000)] +#[case("Sat, 01-Jan-2000 08:00:00 GMT+0000", 946713600000)] +#[case("Sat, 01-Jan-2000 08:00:00 GMT+00:00", 946713600000)] +#[case("Sat, 01 Jan 2000 08:00:00 GMT", 946713600000)] +#[case("Saturday, 01-Jan-00 08:00:00 GMT", 946713600000)] +#[case("01 Jan 00 08:00 -0000", 946713600000)] +#[case("01 Jan 00 08:00 +0000", 946713600000)] +fn test_date_gmt(#[case] input: &str, #[case] expect: i64) { + let ctx = Context::new().unwrap(); + let res = ctx + .eval(&format!(r#"new Date("{}").getTime()"#, input)) + .unwrap(); + + let n: f64 = res.try_into().unwrap(); + assert_eq!(n, expect as f64); +} + +#[rstest] +// EST (-5:00) +#[case("Sat, 01-Jan-2000 03:00:00 UTC-0500", 946713600000)] +#[case("Sat, 01-Jan-2000 03:00:00 UTC-05:00", 946713600000)] +#[case("Sat, 01-Jan-2000 03:00:00 EST", 946713600000)] +#[case("Sat, 01 Jan 2000 03:00:00 EST", 946713600000)] +#[case("Saturday, 01-Jan-00 03:00:00 EST", 946713600000)] +#[case("01 Jan 00 03:00 -0500", 946713600000)] +// EDT (-4:00) +#[case("Sat, 01-Jan-2000 04:00:00 EDT", 946713600000)] +#[case("Sat, 01 Jan 2000 04:00:00 EDT", 946713600000)] +#[case("Saturday, 01-Jan-00 04:00:00 EDT", 946713600000)] +#[case("01 Jan 00 04:00 -0400", 946713600000)] +// CST (-6:00) +#[case("Sat, 01-Jan-2000 02:00:00 CST", 946713600000)] +#[case("Sat, 01 Jan 2000 02:00:00 CST", 946713600000)] +#[case("Saturday, 01-Jan-00 02:00:00 CST", 946713600000)] +#[case("01 Jan 00 02:00 -0600", 946713600000)] +// CDT (-5:00) +#[case("Sat, 01-Jan-2000 03:00:00 CDT", 946713600000)] +#[case("Sat, 01 Jan 2000 03:00:00 CDT", 946713600000)] +#[case("Saturday, 01-Jan-00 03:00:00 CDT", 946713600000)] +#[case("01 Jan 00 03:00 -0500", 946713600000)] +// MST (-7:00) +#[case("Sat, 01-Jan-2000 01:00:00 MST", 946713600000)] +#[case("Sat, 01 Jan 2000 01:00:00 MST", 946713600000)] +#[case("Saturday, 01-Jan-00 01:00:00 MST", 946713600000)] +#[case("01 Jan 00 01:00 -0700", 946713600000)] +// MDT (-6:00) +#[case("Sat, 01-Jan-2000 02:00:00 MDT", 946713600000)] +#[case("Sat, 01 Jan 2000 02:00:00 MDT", 946713600000)] +#[case("Saturday, 01-Jan-00 02:00:00 MDT", 946713600000)] +#[case("01 Jan 00 02:00 -0600", 946713600000)] +// PST (-8:00) +#[case("Sat, 01-Jan-2000 00:00:00 PST", 946713600000)] +#[case("Sat, 01 Jan 2000 00:00:00 PST", 946713600000)] +#[case("Saturday, 01-Jan-00 00:00:00 PST", 946713600000)] +#[case("01 Jan 00 00:00 -0800", 946713600000)] +#[case("Sat, 01-Jan-2000 PST", 946713600000)] +// PDT (-7:00) +#[case("Sat, 01-Jan-2000 01:00:00 PDT", 946713600000)] +#[case("Sat, 01 Jan 2000 01:00:00 PDT", 946713600000)] +#[case("Saturday, 01-Jan-00 01:00:00 PDT", 946713600000)] +#[case("01 Jan 00 01:00 -0700", 946713600000)] +fn test_date_tz(#[case] input: &str, #[case] expect: i64) { + let ctx = Context::new().unwrap(); + let res = ctx + .eval(&format!(r#"new Date("{}").getTime()"#, input)) + .unwrap(); + + let n: f64 = res.try_into().unwrap(); + assert_eq!(n, expect as f64); +} + +#[rstest] +// Special handling for years in the [0, 100) range. +#[case("Sat, 01 Jan 0 08:00:00 UT", 946713600000)] // year 2000 +#[case("Sat, 01 Jan 49 08:00:00 UT", 2493100800000)] // year 2049 +#[case("Sat, 01 Jan 50 08:00:00 UT", -631123200000)] // year 1950 +#[case("Sat, 01 Jan 99 08:00:00 UT", 915177600000)] // year 1999 +#[case("Sat, 01 Jan 100 08:00:00 UT", -59011430400000)] // year 100 +// Test PM after time. +#[case("Sat, 01-Jan-2000 08:00 PM UT", 946756800000)] +#[case("Sat, 01 Jan 2000 08:00 PM UT", 946756800000)] +#[case("Jan 01 2000 08:00 PM UT", 946756800000)] +#[case("Jan 01 08:00 PM UT 2000", 946756800000)] +#[case("Saturday, 01-Jan-00 08:00 PM UT", 946756800000)] +#[case("01 Jan 00 08:00 PM +0000", 946756800000)] +fn test_date_special(#[case] input: &str, #[case] expect: i64) { + let ctx = Context::new().unwrap(); + let res = ctx + .eval(&format!(r#"new Date("{}").getTime()"#, input)) + .unwrap(); + + let n: f64 = res.try_into().unwrap(); + assert_eq!(n, expect as f64); +} + +#[rstest] +#[case("2000-01-01TZ")] +#[case("2000-01-01T60Z")] +#[case("2000-01-01T60:60Z")] +#[case("2000-01-0108:00Z")] +#[case("2000-01-01T08Z")] +#[case("2000-01-01T24:01")] +#[case("2000-01-01T24:00:01")] +#[case("2000-01-01T24:00:00.001")] +#[case("2000-01-01T24:00:00.999Z")] +#[case("May 25 2008 1:30 (PM)) UTC")] +#[case("May 25 2008 1:30( )AM (PM)")] +#[case("a1")] +#[case("nasfdjklsfjoaifg1")] +#[case("x_2")] +#[case("May 25 2008 AAA (GMT)")] +fn test_date_invalid(#[case] input: &str) { + let ctx = Context::new().unwrap(); + let res = ctx + .eval(&format!(r#"new Date("{}").getTime()"#, input)) + .unwrap(); + + let n: f64 = res.try_into().unwrap(); + assert!(n.is_nan(), "got: {}", n); +} + #[test] -fn test_ut() { +fn test_date_ut() { let test_cases_ut = vec![ "Sat, 01-Jan-2000 08:00:00 UT", "Sat, 01 Jan 2000 08:00:00 UT", @@ -698,27 +850,20 @@ fn test_ut() { " 01 Jan 00 08:00 +0000()((asfd)(Sat, 01-Jan-2000)) ", ]; - let expected = JsValue::Date(DateTime::from_utc( - NaiveDateTime::new( - NaiveDate::from_ymd(2000, 1, 1), - NaiveTime::from_hms(8, 0, 0), - ), - Utc, - )); - let mut passed = 0; let mut failed = 0; - + for case in test_cases_ut { let ctx = Context::new().unwrap(); let res = ctx - .eval(&format!(r#"new Date("{}")"#, case)) + .eval(&format!(r#"new Date("{}").getTime()"#, case)) .unwrap(); - - if res == expected { + let n: f64 = res.try_into().unwrap(); + + if n == 946713600000.0 { passed += 1; } else { - println!("FAIL: `{}` - {:?}", case, res); + println!("FAIL: `{}` - {}", case, n); failed += 1; } } @@ -726,3 +871,27 @@ fn test_ut() { println!("{}/{} Passed", passed, passed + failed); assert_eq!(failed, 0); } + +#[test] +// Test if the JS interpreter can parse its own date format +// (Dates from 1970 to ~2070 with 150h steps.) +fn test_date_selfparse() { + let ctx = Context::new().unwrap(); + let res = ctx + .eval( + r#" + test = () => { + for (var i = 0; i < 24 * 365 * 100; i += 150) { + var ms = i * (3600 * 1000); + var s = (new Date(ms)).toString(); + if(ms != Date.parse(s)) return false; + } + return true; + } + test(); + "#, + ) + .unwrap(); + let res: bool = res.try_into().unwrap(); + assert!(res); +}