From 933af5e5b98744360171eb52a575ed7e36982448 Mon Sep 17 00:00:00 2001 From: gremat <50012463+gremat@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:06:27 +0200 Subject: [PATCH] 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 --- windows/registry/key.go | 15 ++++++++++++++- windows/registry/registry_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/windows/registry/key.go b/windows/registry/key.go index 39aeeb644f..25aa1f4f64 100644 --- a/windows/registry/key.go +++ b/windows/registry/key.go @@ -198,7 +198,20 @@ type KeyInfo struct { // ModTime returns the key's last write time. func (ki *KeyInfo) ModTime() time.Time { - return time.Unix(0, ki.lastWriteTime.Nanoseconds()) + lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime + // 100-nanosecond intervals since January 1, 1601 + hsec := uint64(lastHigh)<<32 + uint64(lastLow) + // Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00 + // UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out + // of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64 + sec := int64(hsec/1e7) - 11644473600 + nsec := int64(hsec%1e7) * 100 + return time.Unix(sec, nsec) +} + +// ModTimeZero reports whether the key's last write time is zero. +func (ki *KeyInfo) ModTimeZero() bool { + return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0 } // Stat retrieves information about the open key k. diff --git a/windows/registry/registry_test.go b/windows/registry/registry_test.go index 6e7bec505e..995acc96bf 100644 --- a/windows/registry/registry_test.go +++ b/windows/registry/registry_test.go @@ -9,6 +9,7 @@ package registry_test import ( "bytes" "crypto/rand" + "errors" "os" "syscall" "testing" @@ -674,3 +675,30 @@ func GetDynamicTimeZoneInformation(dtzi *DynamicTimezoneinformation) (rc uint32, } return } + +func TestModTimeZeroValue(t *testing.T) { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009`, registry.READ) + if err != nil { + if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) { + t.Skip("Perflib key not found; skipping") + } + t.Fatal(err) + } + defer k.Close() + + // Modification time of Perflib keys is known to be set to + // Filetime's zero value: get stats and check. + stats, err := k.Stat() + if err != nil { + t.Fatal(err) + } + // First verify input is zero (assume ModTimeZero uses it directly). + if !stats.ModTimeZero() { + t.Error("Modification time of Perflib key should be zero") + } + // Then check ModTime directly thus conversion implicitly. + modTime := stats.ModTime() + if !modTime.Equal(time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)) { + t.Errorf("ModTime should be 1601-01-01, but is %v", modTime) + } +}