Skip to content

Commit 8443a7e

Browse files
committed
windows: fix ModTime to cover Filetime zero value
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, ModTime() reports an underflow-affected value of 2185-07-22T00:34:33.709551+01:00. This commit drops usage of Nanoseconds() in favor of a conversion that converts first to seconds and nanoseconds before gauging thus is capable to cover the full range of Filetime values. Additionally, ModTimeZero() provides a convenient way to check for a last write time value of zero in analogy to time.Time.IsZero(); no need to specify January 1, 1601 manually. Fixes golang/go#74335
1 parent 5e63aa5 commit 8443a7e

File tree

4 files changed

+105
-15
lines changed

4 files changed

+105
-15
lines changed

windows/registry/key.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,20 @@ type KeyInfo struct {
198198

199199
// ModTime returns the key's last write time.
200200
func (ki *KeyInfo) ModTime() time.Time {
201-
return time.Unix(0, ki.lastWriteTime.Nanoseconds())
201+
lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime
202+
// 100-nanosecond intervals since January 1, 1601
203+
hsec := uint64(lastHigh)<<32 + uint64(lastLow)
204+
// Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00
205+
// UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out
206+
// of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64
207+
sec := int64(hsec/1e7) - 11644473600
208+
nsec := int64(hsec%1e7) * 100
209+
return time.Unix(sec, nsec)
210+
}
211+
212+
// ModTimeZero reports whether the key's last write time is zero.
213+
func (ki *KeyInfo) ModTimeZero() bool {
214+
return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0
202215
}
203216

204217
// Stat retrieves information about the open key k.

windows/registry/registry_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package registry_test
99
import (
1010
"bytes"
1111
"crypto/rand"
12+
"errors"
1213
"os"
1314
"syscall"
1415
"testing"
@@ -674,3 +675,30 @@ func GetDynamicTimeZoneInformation(dtzi *DynamicTimezoneinformation) (rc uint32,
674675
}
675676
return
676677
}
678+
679+
func TestModTimeZeroValue(t *testing.T) {
680+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009`, registry.READ)
681+
if err != nil {
682+
if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) {
683+
t.Skip("Perflib key not found; skipping")
684+
}
685+
t.Fatal(err)
686+
}
687+
defer k.Close()
688+
689+
// Modification time of Perflib keys is known to be set to
690+
// Filetime's zero value: get stats and check.
691+
stats, err := k.Stat()
692+
if err != nil {
693+
t.Fatal(err)
694+
}
695+
// First verify input is zero (assume ModTimeZero uses it directly).
696+
if !stats.ModTimeZero() {
697+
t.Error("Modification time of Perflib key should be zero")
698+
}
699+
// Then check ModTime directly thus conversion implicitly.
700+
modTime := stats.ModTime()
701+
if !modTime.Equal(time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)) {
702+
t.Errorf("ModTime should be 1601-01-01, but is %v", modTime)
703+
}
704+
}

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 = FiletimeToTimeval(ft)
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 := TimevalToFiletime(tv[0])
777+
w := TimevalToFiletime(tv[1])
778778
return SetFileTime(h, nil, &a, &w)
779779
}
780780

windows/types_windows.go

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

782+
func TimevalToFiletime(tv Timeval) 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 result is
796+
// undefined if the nanosecond value cannot be represented by a Timeval (values
797+
// equivalent to dates before the year 1901 or after the year 2038).
782798
func NsecToTimeval(nsec int64) (tv Timeval) {
783-
tv.Sec = int32(nsec / 1e9)
784-
tv.Usec = int32(nsec % 1e9 / 1e3)
785-
return
799+
// Ignore overflow (math.MaxInt64/1e9 > math.MaxInt32)
800+
sec := int32(nsec / 1e9)
801+
usec := int32(nsec % 1e9 / 1e3)
802+
if usec < 0 {
803+
usec += 1e6
804+
sec--
805+
}
806+
tv.Sec = sec
807+
tv.Usec = usec
808+
return tv
786809
}
787810

788811
type Overlapped struct {
@@ -805,22 +828,48 @@ type Filetime struct {
805828
HighDateTime uint32
806829
}
807830

808-
// Nanoseconds returns Filetime ft in nanoseconds
831+
// Unix returns ft in seconds and nanoseconds
809832
// since Epoch (00:00:00 UTC, January 1, 1970).
810-
func (ft *Filetime) Nanoseconds() int64 {
833+
func (ft *Filetime) Unix() (sec, nsec int64) {
811834
// 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
835+
hsec := uint64(ft.HighDateTime)<<32 + uint64(ft.LowDateTime)
836+
// Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00
837+
// UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out
838+
// of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64
839+
sec = int64(hsec/1e7) - 11644473600
840+
nsec = int64(hsec%1e7) * 100
841+
return sec, nsec
842+
}
843+
844+
// Nanoseconds returns ft in nanoseconds since Epoch (00:00:00 UTC,
845+
// January 1, 1970). The result is undefined if the Filetime cannot be
846+
// represented by an int64 (values equivalent to dates before the year
847+
// 1677 or after the year 2262). Note that this explicitly excludes the
848+
// zero value of Filetime, which is equivalent to January 1, 1601.
849+
//
850+
// Deprecated: use Unix instead, which returns both seconds and
851+
// nanoseconds thus covers the full available range of Filetime.
852+
func (ft *Filetime) Nanoseconds() int64 {
853+
sec, nsec := ft.Unix()
854+
return (sec*1e9 + nsec)
855+
}
856+
857+
// FiletimeToTimeval converts a Filetime ft to a Timeval tv. The result is
858+
// undefined if the Filetime cannot be represented by a Timeval (values
859+
// equivalent to dates before the year 1901 or after the year 2038).
860+
func FiletimeToTimeval(ft Filetime) (tv Timeval) {
861+
sec, nsec := ft.Unix()
862+
// Ignore overflow (math.MaxUint64*1e2/1e9 > math.MaxInt32)
863+
tv.Sec = int32(sec)
864+
tv.Usec = int32(nsec / 1e3)
865+
return tv
818866
}
819867

820868
func NsecToFiletime(nsec int64) (ft Filetime) {
821869
// convert into 100-nanosecond
822870
nsec /= 100
823871
// change starting time to January 1, 1601
872+
// note: no overflow here (11644473600*1e7 < math.MaxInt64/1e1)
824873
nsec += 116444736000000000
825874
// split into high / low
826875
ft.LowDateTime = uint32(nsec & 0xffffffff)

0 commit comments

Comments
 (0)