Skip to content

Commit 933c73d

Browse files
committed
windows: fix conversions related to Filetime
Filetime.Nanoseconds() has a major drawback: the returned int64 is too small to represent Filetime's zero value (January 1, 1601) in terms of nanoseconds since Epoch (00:00:00 UTC, January 1, 1970); MinInt64 only dates back to year 1677. This has real-life implications, e.g., some Windows sub systems (Perflib, to name one) create registry keys with the last write time property set to zero. In this case, using Nanoseconds() reports an underflow-affected value of 2185-07-22T00:34:33.709551+01:00. This commit clearly marks the well-defined boundaries of Nanoseconds(). It introduces the new Unix() that returns a well-known pair of seconds and nanoseconds (see other mentions in syscall.go) thus is capable to cover the full range of Filetime values. Additionally, other conversions related to Filetime are refactored to use full-range values where possible, and to reflect boundaries in the docstring where not. See also golang/go#74335
1 parent 5e63aa5 commit 933c73d

File tree

3 files changed

+119
-13
lines changed

3 files changed

+119
-13
lines changed

windows/example_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package windows_test
6+
7+
import (
8+
"fmt"
9+
"math"
10+
"time"
11+
12+
"golang.org/x/sys/windows"
13+
)
14+
15+
func ExampleFiletime_Unix() {
16+
ftZero := windows.Filetime{
17+
HighDateTime: 0x0,
18+
LowDateTime: 0x0,
19+
}
20+
secs, nsecs := ftZero.Unix()
21+
fmt.Println(secs, nsecs)
22+
23+
zeroTime := time.Unix(ftZero.Unix())
24+
match := zeroTime.Equal(time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC))
25+
fmt.Printf("Matches January 1, 1601: %v\n", match)
26+
27+
// Output:
28+
// -11644473600 0
29+
// Matches January 1, 1601: true
30+
}
31+
32+
func ExampleNsecToFiletime() {
33+
ftMin := windows.NsecToFiletime(math.MinInt64)
34+
fmt.Println("Minimal Filetime with nanoseconds only:")
35+
fmt.Printf("hi: %#x lo: %#x\n", ftMin.HighDateTime, ftMin.LowDateTime)
36+
fmt.Printf("= %v\n", time.Unix(0, math.MinInt64).UTC())
37+
38+
ftMax := windows.NsecToFiletime(math.MaxInt64)
39+
fmt.Println("\nMaximum Filetime with nanoseconds only:")
40+
fmt.Printf("hi: %#x lo: %#x\n", ftMax.HighDateTime, ftMax.LowDateTime)
41+
fmt.Printf("= %v\n", time.Unix(0, math.MaxInt64).UTC())
42+
43+
// Output:
44+
// Minimal Filetime with nanoseconds only:
45+
// hi: 0x5603ca lo: 0x5a5d3852
46+
// = 1677-09-21 00:12:43.145224192 +0000 UTC
47+
//
48+
// Maximum Filetime with nanoseconds only:
49+
// hi: 0x2e55ff3 lo: 0x501fc7ae
50+
// = 2262-04-11 23:47:16.854775807 +0000 UTC
51+
}

windows/syscall_windows.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ func Ftruncate(fd Handle, length int64) (err error) {
740740
func Gettimeofday(tv *Timeval) (err error) {
741741
var ft Filetime
742742
GetSystemTimeAsFileTime(&ft)
743-
*tv = NsecToTimeval(ft.Nanoseconds())
743+
*tv = ft.ToTimeval()
744744
return nil
745745
}
746746

@@ -773,8 +773,8 @@ func Utimes(path string, tv []Timeval) (err error) {
773773
return e
774774
}
775775
defer CloseHandle(h)
776-
a := NsecToFiletime(tv[0].Nanoseconds())
777-
w := NsecToFiletime(tv[1].Nanoseconds())
776+
a := tv[0].ToFiletime()
777+
w := tv[1].ToFiletime()
778778
return SetFileTime(h, nil, &a, &w)
779779
}
780780

windows/types_windows.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -779,10 +779,34 @@ func (tv *Timeval) Nanoseconds() int64 {
779779
return (int64(tv.Sec)*1e6 + int64(tv.Usec)) * 1e3
780780
}
781781

782+
func (tv *Timeval) ToFiletime() Filetime {
783+
// Convert to 100-nanosecond intervals
784+
hsec := uint64(tv.Sec)*1e7 + uint64(tv.Usec)/100
785+
// Change starting time to January 1, 1601
786+
// Note: No overflow here (11644473600*1e7 < math.MaxUint64/1e2)
787+
hsec += 116444736000000000
788+
// Split into high / low.
789+
return Filetime{
790+
LowDateTime: uint32(hsec & 0xffffffff),
791+
HighDateTime: uint32(hsec >> 32 & 0xffffffff),
792+
}
793+
}
794+
795+
// NsecToTimeval converts a nanosecond value nsec to a Timeval tv. The
796+
// result is undefined if the nanosecond value cannot be represented by
797+
// a Timeval (values equivalent to dates before the year 1901 or after
798+
// the year 2038).
782799
func NsecToTimeval(nsec int64) (tv Timeval) {
783-
tv.Sec = int32(nsec / 1e9)
784-
tv.Usec = int32(nsec % 1e9 / 1e3)
785-
return
800+
// Ignore overflow (math.MaxInt64/1e9 > math.MaxInt32)
801+
sec := int32(nsec / 1e9)
802+
usec := int32(nsec % 1e9 / 1e3)
803+
if usec < 0 {
804+
usec += 1e6
805+
sec--
806+
}
807+
tv.Sec = sec
808+
tv.Usec = usec
809+
return tv
786810
}
787811

788812
type Overlapped struct {
@@ -805,22 +829,53 @@ type Filetime struct {
805829
HighDateTime uint32
806830
}
807831

832+
// Unix returns Filetime ft in seconds and nanoseconds
833+
// since Epoch (00:00:00 UTC, January 1, 1970).
834+
func (ft *Filetime) Unix() (sec int64, nsec int64) {
835+
// 100-nanosecond intervals since January 1, 1601
836+
hsec := uint64(ft.HighDateTime)<<32 + uint64(ft.LowDateTime)
837+
// Convert _before_ gauging; the nanosecond difference between Epoch
838+
// (00:00:00 UTC, January 1, 1970) and Filetime's zero offset (January
839+
// 1, 1601) is out of bounds for int64:
840+
// -11644473600*1e7*1e2 < math.MinInt64
841+
sec = int64(hsec/1e7) - 11644473600
842+
nsec = int64(hsec%1e7) * 100
843+
return sec, nsec
844+
}
845+
808846
// Nanoseconds returns Filetime ft in nanoseconds
809847
// since Epoch (00:00:00 UTC, January 1, 1970).
848+
// The result is undefined if the Filetime cannot be represented by an
849+
// int64 (values equivalent to dates before the year 1677 or after the
850+
// year 2262, see also example of NsecToFiletime). Note that this
851+
// explicitly excludes the zero value of Filetime, which is equivalent
852+
// to January 1, 1601.
853+
//
854+
// Deprecated: use Unix instead, which returns both seconds and
855+
// nanoseconds thus covers the full available range of Filetime.
810856
func (ft *Filetime) Nanoseconds() int64 {
811-
// 100-nanosecond intervals since January 1, 1601
812-
nsec := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime)
813-
// change starting time to the Epoch (00:00:00 UTC, January 1, 1970)
814-
nsec -= 116444736000000000
815-
// convert into nanoseconds
816-
nsec *= 100
817-
return nsec
857+
sec, nsec := ft.Unix()
858+
return (sec*1e9 + nsec)
859+
}
860+
861+
// ToTimeval converts a Filetime ft to a Timeval tv. The result is
862+
// undefined if the Filetime cannot be represented by a Timeval (values
863+
// equivalent to dates before the year 1901 or after the year 2038).
864+
func (ft *Filetime) ToTimeval() Timeval {
865+
sec, nsec := ft.Unix()
866+
// Ignore overflow (math.MaxUint64*1e2/1e9 > math.MaxInt32)
867+
return Timeval{
868+
Sec: int32(sec),
869+
Usec: int32(nsec / 1e3),
870+
}
818871
}
819872

873+
// TODO: Drop NsecTo... altogether (at least in windows)
820874
func NsecToFiletime(nsec int64) (ft Filetime) {
821875
// convert into 100-nanosecond
822876
nsec /= 100
823877
// change starting time to January 1, 1601
878+
// note: no overflow here (11644473600*1e7 < math.MaxInt64/1e1)
824879
nsec += 116444736000000000
825880
// split into high / low
826881
ft.LowDateTime = uint32(nsec & 0xffffffff)

0 commit comments

Comments
 (0)