Skip to content

Commit

Permalink
fix!: date underflow / overflow and utc (#464)
Browse files Browse the repository at this point in the history
Use time.UnixMilli instead of UnixNano for time calculations to avoid
underflow / overflow issues.

BREAKING CHANGE: Use a GMT fixed time zone for UTC to mimic toUTCString
behaviour of Javascript which outputs in GMT not UTC.

Fixes #302
  • Loading branch information
stevenh authored Nov 28, 2022
1 parent a5b0ade commit aeda017
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 33 deletions.
9 changes: 8 additions & 1 deletion builtin_date.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const (
builtinDate_goTimeLayout = "15:04:05 MST"
)

var (
// utcTimeZone is the time zone used for UTC calculations.
// It is GMT not UTC as that's what Javascript does because toUTCString is
// actually an alias to toGMTString.
utcTimeZone = Time.FixedZone("GMT", 0)
)

func builtinDate(call FunctionCall) Value {
date := &_dateObject{}
date.Set(newDateTime([]Value{}, Time.Local))
Expand Down Expand Up @@ -54,7 +61,7 @@ func builtinDate_toUTCString(call FunctionCall) Value {
if date.isNaN {
return toValue_string("Invalid Date")
}
return toValue_string(date.Time().Format(builtinDate_goDateTimeLayout))
return toValue_string(date.Time().In(utcTimeZone).Format(builtinDate_goDateTimeLayout))
}

func builtinDate_toISOString(call FunctionCall) Value {
Expand Down
30 changes: 15 additions & 15 deletions date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestDate(t *testing.T) {
time0 := time.Unix(1348616313, 47*1000*1000).Local()

test(`Date`, "function Date() { [native code] }")
test(`new Date(0).toUTCString()`, "Thu, 01 Jan 1970 00:00:00 UTC")
test(`new Date(0).toUTCString()`, "Thu, 01 Jan 1970 00:00:00 GMT")
test(`new Date(0).toGMTString()`, "Thu, 01 Jan 1970 00:00:00 GMT")
if false {
// TODO toLocale{Date,Time}String
Expand All @@ -37,8 +37,8 @@ func TestDate(t *testing.T) {
test(`new Date(0).toLocaleTimeString()`, "")
}
test(`new Date(1348616313).getTime()`, 1348616313)
test(`new Date(1348616313).toUTCString()`, "Fri, 16 Jan 1970 14:36:56 UTC")
test(`abc = new Date(1348616313047); abc.toUTCString()`, "Tue, 25 Sep 2012 23:38:33 UTC")
test(`new Date(1348616313).toUTCString()`, "Fri, 16 Jan 1970 14:36:56 GMT")
test(`abc = new Date(1348616313047); abc.toUTCString()`, "Tue, 25 Sep 2012 23:38:33 GMT")
test(`abc.getYear()`, time0.Year()-1900)
test(`abc.getFullYear()`, time0.Year())
test(`abc.getUTCFullYear()`, 2012)
Expand All @@ -61,39 +61,39 @@ func TestDate(t *testing.T) {

test(`new Date("Xyzzy").getTime()`, math.NaN())

test(`abc.setFullYear(2011); abc.toUTCString()`, "Sun, 25 Sep 2011 23:38:33 UTC")
test(`new Date(12564504e5).toUTCString()`, "Sun, 25 Oct 2009 06:00:00 UTC")
test(`new Date(2009, 9, 25).toUTCString()`, "Sun, 25 Oct 2009 00:00:00 UTC")
test(`abc.setFullYear(2011); abc.toUTCString()`, "Sun, 25 Sep 2011 23:38:33 GMT")
test(`new Date(12564504e5).toUTCString()`, "Sun, 25 Oct 2009 06:00:00 GMT")
test(`new Date(2009, 9, 25).toUTCString()`, "Sun, 25 Oct 2009 00:00:00 GMT")
test(`+(new Date(2009, 9, 25))`, int64(1256428800000))

format := "Mon, 2 Jan 2006 15:04:05 MST"

time1 := time.Unix(1256450400, 0)
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)

time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), 2001*1000*1000, time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), 2001*1000*1000, time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setMilliseconds(2001); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), 61, time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), 61, time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setSeconds("61"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), 61, time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), 61, time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setMinutes("61"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), 5, time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), 5, time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setHours("5"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), time1.Month(), 26, time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), time1.Month(), 26, time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setDate("26"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), 10, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), 10, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setMonth(9); abc.toUTCString()`, time0.Format(format))
test(`abc = new Date(12564504e5); abc.setMonth("09"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(time1.Year(), 11, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(time1.Year(), 11, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setMonth("10"); abc.toUTCString()`, time0.Format(format))

time0 = time.Date(2010, time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).UTC()
time0 = time.Date(2010, time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone)
test(`abc = new Date(12564504e5); abc.setFullYear(2010); abc.toUTCString()`, time0.Format(format))

test(`new Date("2001-01-01T10:01:02.000").getTime()`, int64(978343262000))
Expand Down
32 changes: 32 additions & 0 deletions issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -962,3 +962,35 @@ func Test_issue357(t *testing.T) {

require.Equal(t, []string{"wow", "hey", "another", "more"}, slice)
}

func Test_issue302(t *testing.T) {
tests := map[string]struct {
code string
want string
}{
"underflow": {
code: "new Date(9223372036855).toUTCString();",
want: "Fri, 11 Apr 2262 23:47:16 GMT",
},
"after-2262": {
code: "new Date('2263-04-11T23:47:16.855Z').toUTCString();",
want: "Sat, 11 Apr 2263 23:47:16 GMT",
},
"before-1677": {
code: "new Date('1676-09-21T00:12:43.146Z').toUTCString();",
want: "Mon, 21 Sep 1676 00:12:43 GMT",
},
}

vm := New()
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
val, err := vm.Run(tt.code)
require.NoError(t, err)

exp, err := val.Export()
require.NoError(t, err)
require.Equal(t, tt.want, exp)
})
}
}
35 changes: 18 additions & 17 deletions type_date.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ func epochToTime(value float64) (time Time.Time, err error) {
epoch := int64(epochWithMilli / 1000)
milli := int64(epochWithMilli) % 1000

time = Time.Unix(int64(epoch), milli*1000000).UTC()
time = Time.Unix(int64(epoch), milli*1000000).In(utcTimeZone)
return
}

func timeToEpoch(time Time.Time) float64 {
return float64(time.UnixNano() / (1000 * 1000))
return float64(time.UnixMilli())
}

func (runtime *_runtime) newDateObject(epoch float64) *_object {
Expand Down Expand Up @@ -207,7 +207,7 @@ func newDateTime(argumentList []Value, location *Time.Location) (epoch float64)
time := Time.Date(int(year), dateToGoMonth(int(month)), int(day), int(hour), int(minute), int(second), int(millisecond)*1000*1000, location)
return timeToEpoch(time)
} else if len(argumentList) == 0 { // 0-argument
time := Time.Now().UTC()
time := Time.Now().In(utcTimeZone)
return timeToEpoch(time)
} else { // 1-argument
value := valueOfArrayIndex(argumentList, 0)
Expand Down Expand Up @@ -263,24 +263,25 @@ func dateParse(date string) (epoch float64) {
// YYYY-MM-DDTHH:mm:ss.sssZ
var time Time.Time
var err error
{
date := date
if match := matchDateTimeZone.FindStringSubmatch(date); match != nil {
if match[2] == "Z" {
date = match[1] + "+0000"
} else {
date = match[1] + match[3] + match[4]
}

if match := matchDateTimeZone.FindStringSubmatch(date); match != nil {
if match[2] == "Z" {
date = match[1] + "+0000"
} else {
date = match[1] + match[3] + match[4]
}
for _, layout := range dateLayoutList {
time, err = Time.Parse(layout, date)
if err == nil {
break
}
}

for _, layout := range dateLayoutList {
time, err = Time.Parse(layout, date)
if err == nil {
break
}
}

if err != nil {
return math.NaN()
}
return float64(time.UnixNano()) / (1000 * 1000) // UnixMilli()

return float64(time.UnixMilli())
}

0 comments on commit aeda017

Please sign in to comment.