Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 133 additions & 15 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,23 @@ typedef struct {

/* end JS Malloc */

#define JS_TZ_OFFSET_CACHE_SIZE 8
#define JS_TZ_OFFSET_CACHE_TTL_MS 1000

typedef struct JSTimezoneOffsetCacheEntry {
int64_t start_sec;
int64_t end_sec;
int64_t expires_ms;
int offset;
uint64_t tz_hash;
BOOL valid;
} JSTimezoneOffsetCacheEntry;

typedef struct JSTimezoneOffsetCache {
JSTimezoneOffsetCacheEntry entries[JS_TZ_OFFSET_CACHE_SIZE];
uint32_t next_entry;
} JSTimezoneOffsetCache;

struct JSRuntime {
JSMallocContext malloc_ctx;
const char *rt_info;
Expand Down Expand Up @@ -375,6 +392,7 @@ struct JSRuntime {
void *module_loader_opaque;
/* timestamp for internal use in module evaluation */
int64_t module_async_evaluation_next_timestamp;
JSTimezoneOffsetCache tz_offset_cache;

BOOL can_block : 8; /* TRUE if Atomics.wait can block */
/* used to allocate, free and clone SharedArrayBuffers */
Expand Down Expand Up @@ -47451,12 +47469,8 @@ static const JSCFunctionListEntry js_math_obj[] = {

/* OS dependent. d = argv[0] is in ms from 1970. Return the difference
between UTC time and local time 'd' in minutes */
static int getTimezoneOffset(int64_t time)
static int64_t clampTimezoneTimeSec(int64_t time)
{
time_t ti;
int res;

time /= 1000; /* convert to seconds */
if (sizeof(time_t) == 4) {
/* on 32-bit systems, we need to clamp the time value to the
range of `time_t`. This is better than truncating values to
Expand All @@ -47477,6 +47491,41 @@ static int getTimezoneOffset(int64_t time)
}
}
}
return time;
}

static int64_t normalizeTimezoneTimeSec(int64_t time)
{
return clampTimezoneTimeSec(time / 1000); /* convert to seconds */
}

static int64_t timezoneFloorDiv(int64_t a, int64_t b)
{
int64_t m = a % b;
return (a - (m + (m < 0) * b)) / b;
}

static int64_t getTimezoneCacheTimeMs(void)
{
#if defined(__linux__) || defined(__APPLE__)
struct timespec ts;

clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
#else
struct timeval tv;

gettimeofday(&tv, NULL);
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
#endif
}

static int getTimezoneOffsetSecRaw(int64_t time)
{
time_t ti;
int res;

time = clampTimezoneTimeSec(time);
ti = time;
#if defined(_WIN32)
{
Expand Down Expand Up @@ -47505,6 +47554,75 @@ static int getTimezoneOffset(int64_t time)
return res;
}

static int getTimezoneOffsetRaw(int64_t time)
{
return getTimezoneOffsetSecRaw(normalizeTimezoneTimeSec(time));
}

static uint64_t getTimezoneHash(void)
{
const char *tz = getenv("TZ");
uint64_t h = UINT64_C(1469598103934665603);

if (!tz)
return 0;
while (*tz) {
h ^= (uint8_t)*tz++;
h *= UINT64_C(1099511628211);
}
return h;
}

static void refreshTimezoneState(void)
{
#if !defined(_WIN32)
tzset();
#endif
}

static int getTimezoneOffset(JSRuntime *rt, int64_t time)
{
JSTimezoneOffsetCache *cache = &rt->tz_offset_cache;
int64_t time_sec = normalizeTimezoneTimeSec(time);
uint64_t tz_hash = getTimezoneHash();
int64_t start_sec, end_sec;
int64_t now_ms = getTimezoneCacheTimeMs();
int i, offset;

for(i = 0; i < JS_TZ_OFFSET_CACHE_SIZE; i++) {
JSTimezoneOffsetCacheEntry *e = &cache->entries[i];
if (e->valid && e->tz_hash == tz_hash &&
now_ms < e->expires_ms &&
time_sec >= e->start_sec && time_sec <= e->end_sec) {
return e->offset;
}
}

refreshTimezoneState();
offset = getTimezoneOffsetRaw(time);
start_sec = time_sec;
end_sec = time_sec;
{
int64_t hour_start, hour_end;

hour_start = timezoneFloorDiv(time_sec, 3600) * 3600;
hour_end = hour_start + 3599;
if (getTimezoneOffsetSecRaw(hour_start) == offset &&
getTimezoneOffsetSecRaw(hour_end) == offset) {
start_sec = hour_start;
end_sec = hour_end;
}
}
cache->entries[cache->next_entry].start_sec = start_sec;
cache->entries[cache->next_entry].end_sec = end_sec;
cache->entries[cache->next_entry].expires_ms = now_ms + JS_TZ_OFFSET_CACHE_TTL_MS;
cache->entries[cache->next_entry].offset = offset;
cache->entries[cache->next_entry].tz_hash = tz_hash;
cache->entries[cache->next_entry].valid = TRUE;
cache->next_entry = (cache->next_entry + 1) % JS_TZ_OFFSET_CACHE_SIZE;
return offset;
}

#if 0
static JSValue js___date_getTimezoneOffset(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
Expand Down Expand Up @@ -55103,7 +55221,7 @@ static __exception int get_date_fields(JSContext *ctx, JSValueConst obj,
} else {
d = dval; /* assuming -8.64e15 <= dval <= -8.64e15 */
if (is_local) {
tz = -getTimezoneOffset(d);
tz = -getTimezoneOffset(ctx->rt, d);
d += tz * 60000;
}
}
Expand Down Expand Up @@ -55149,7 +55267,7 @@ static double time_clip(double t) {

/* The spec mandates the use of 'double' and it specifies the order
of the operations */
static double set_date_fields(double fields[minimum_length(7)], int is_local) {
static double set_date_fields(JSRuntime *rt, double fields[minimum_length(7)], int is_local) {
double y, m, dt, ym, mn, day, h, s, milli, time, tv;
int yi, mi, i;
int64_t days;
Expand Down Expand Up @@ -55201,12 +55319,12 @@ static double set_date_fields(double fields[minimum_length(7)], int is_local) {
/* adjust for local time and clip */
if (is_local) {
int64_t ti = tv < INT64_MIN ? INT64_MIN : tv >= 0x1p63 ? INT64_MAX : (int64_t)tv;
tv += getTimezoneOffset(ti) * 60000;
tv += getTimezoneOffset(rt, ti) * 60000;
}
return time_clip(tv);
}

static double set_date_fields_checked(double fields[minimum_length(7)], int is_local)
static double set_date_fields_checked(JSRuntime *rt, double fields[minimum_length(7)], int is_local)
{
int i;
double a;
Expand All @@ -55218,7 +55336,7 @@ static double set_date_fields_checked(double fields[minimum_length(7)], int is_l
if (i == 0 && fields[0] >= 0 && fields[0] < 100)
fields[0] += 1900;
}
return set_date_fields(fields, is_local);
return set_date_fields(rt, fields, is_local);
}

static JSValue get_date_field(JSContext *ctx, JSValueConst this_val,
Expand Down Expand Up @@ -55274,7 +55392,7 @@ static JSValue set_date_field(JSContext *ctx, JSValueConst this_val,
return JS_NAN; /* thisTimeValue is NaN */

if (res && argc > 0)
d = set_date_fields(fields, is_local);
d = set_date_fields(ctx->rt, fields, is_local);

return JS_SetThisTimeValue(ctx, this_val, d);
}
Expand Down Expand Up @@ -55447,7 +55565,7 @@ static JSValue js_date_constructor(JSContext *ctx, JSValueConst new_target,
if (JS_ToFloat64(ctx, &fields[i], argv[i]))
return JS_EXCEPTION;
}
val = set_date_fields_checked(fields, 1);
val = set_date_fields_checked(ctx->rt, fields, 1);
}
has_val:
#if 0
Expand Down Expand Up @@ -55487,7 +55605,7 @@ static JSValue js_Date_UTC(JSContext *ctx, JSValueConst this_val,
if (JS_ToFloat64(ctx, &fields[i], argv[i]))
return JS_EXCEPTION;
}
return JS_NewFloat64(ctx, set_date_fields_checked(fields, 0));
return JS_NewFloat64(ctx, set_date_fields_checked(ctx->rt, fields, 0));
}

/* Date string parsing */
Expand Down Expand Up @@ -55946,7 +56064,7 @@ static JSValue js_Date_parse(JSContext *ctx, JSValueConst this_val,
if (valid) {
for(i = 0; i < 7; i++)
fields1[i] = fields[i];
d = set_date_fields(fields1, is_local) - fields[8] * 60000;
d = set_date_fields(ctx->rt, fields1, is_local) - fields[8] * 60000;
rv = JS_NewFloat64(ctx, d);
}
}
Expand Down Expand Up @@ -56005,7 +56123,7 @@ static JSValue js_date_getTimezoneOffset(JSContext *ctx, JSValueConst this_val,
return JS_NAN;
else
/* assuming -8.64e15 <= v <= -8.64e15 */
return JS_NewInt64(ctx, getTimezoneOffset((int64_t)trunc(v)));
return JS_NewInt64(ctx, getTimezoneOffset(ctx->rt, (int64_t)trunc(v)));
}

static JSValue js_date_getTime(JSContext *ctx, JSValueConst this_val,
Expand Down
99 changes: 99 additions & 0 deletions tests/test_builtin.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,79 @@ function test_json()
assert_json_error('\n{ "a": @x }"');
}

function test_date_timezone_offset_cache()
{
if (typeof std === "undefined" ||
typeof std.getenv !== "function" ||
typeof std.setenv !== "function" ||
typeof std.unsetenv !== "function") {
return;
}

var old_tz = std.getenv("TZ");
function restore_tz()
{
if (old_tz === undefined)
std.unsetenv("TZ");
else
std.setenv("TZ", old_tz);
}

try {
var i, d, tz;
var utc_tz = "UTC0";
var fixed_pacific_tz = "QST8";
var dst_pacific_tz = "PST8PDT,M3.2.0/2,M11.1.0/2";
var t = 1234567890000;

std.setenv("TZ", utc_tz);
d = new Date(t);
for (i = 0; i < 16; i++)
assert(d.getTimezoneOffset(), 0, "UTC timezone offset cache");

std.setenv("TZ", fixed_pacific_tz);
assert(d.getTimezoneOffset(), 480, "TZ change invalidates timezone offset cache");
assert(Date.parse(d.toString()), t, "TZ change local string roundtrip");

std.setenv("TZ", utc_tz);
assert(d.getTimezoneOffset(), 0, "TZ change invalidates timezone offset cache back to UTC");

std.setenv("TZ", dst_pacific_tz);
var dst_cases = [
["2020-03-08T09:59:59Z", 480],
["2020-03-08T10:00:00Z", 420],
["2020-11-01T08:59:59Z", 420],
["2020-11-01T09:00:00Z", 480],
];
var supports_dst_tz = true;
for (i = 0; i < dst_cases.length; i++) {
d = new Date(Date.parse(dst_cases[i][0]));
if (d.getTimezoneOffset() !== dst_cases[i][1])
supports_dst_tz = false;
}
if (supports_dst_tz) {
for (i = 0; i < dst_cases.length; i++) {
d = new Date(Date.parse(dst_cases[i][0]));
assert(d.getTimezoneOffset(), dst_cases[i][1], "DST transition timezone offset");
assert(Date.parse(d.toString()), d.getTime(), "DST transition local string roundtrip");
}
}

var extreme_times = [
-8640000000000000,
8640000000000000,
];
for (i = 0; i < extreme_times.length; i++) {
d = new Date(extreme_times[i]);
tz = d.getTimezoneOffset();
assert(typeof tz, "number", "Date limit timezone offset type");
assert(isFinite(tz), true, "Date limit timezone offset finite");
}
} finally {
restore_tz();
}
}

function test_date()
{
// Date Time String format is YYYY-MM-DDTHH:mm:ss.sssZ
Expand Down Expand Up @@ -664,6 +737,32 @@ function test_date()
var a = Date.parse(d.toISOString());
assert((new Date(a)).toISOString(), d.toISOString());

var roundtrip_times = [
0,
1000,
946684800000,
1234567890000,
-1234567890000,
1583650799000,
1583650800000,
1604210399000,
1604210400000,
];
var i, t, s, tz;
for (i = 0; i < roundtrip_times.length; i++) {
t = roundtrip_times[i];
d = new Date(t);
s = d.toString();
assert(Date.parse(s), t, "Date local string roundtrip");
}

d = new Date(1234567890000);
tz = d.getTimezoneOffset();
for (i = 0; i < 1000; i++)
assert(d.getTimezoneOffset(), tz, "repeated timezone offset");

test_date_timezone_offset_cache();

assert((new Date("2020-01-01T01:01:01.123Z")).toISOString(),
"2020-01-01T01:01:01.123Z");
/* implementation defined behavior */
Expand Down