Skip to content

Commit c23e4e5

Browse files
authored
Merge pull request #79 from thaJeztah/test_chtimes
archive: fix creation time updates on Windows
2 parents a11565d + aa1541a commit c23e4e5

6 files changed

Lines changed: 119 additions & 24 deletions

File tree

archive.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,13 +610,13 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
610610
// Follow the hardlink only when its target is not itself a symlink.
611611
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
612612
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
613-
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
613+
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
614614
return err
615615
}
616616
}
617617
default:
618618
// All other file types follow symlinks.
619-
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
619+
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
620620
return err
621621
}
622622
}
@@ -996,7 +996,7 @@ loop:
996996

997997
for _, d := range dirs {
998998
aTime := boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime))
999-
if err := root.Chtimes(d.name, aTime, boundTime(d.hdr.ModTime)); err != nil {
999+
if err := chtimes(root, d.name, aTime, boundTime(d.hdr.ModTime)); err != nil {
10001000
return err
10011001
}
10021002
}

archive_windows_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ package archive
55
import (
66
"os"
77
"path/filepath"
8+
"syscall"
89
"testing"
10+
"time"
11+
12+
"gotest.tools/v3/assert"
913
)
1014

1115
func TestCopyFileWithInvalidDest(t *testing.T) {
@@ -67,3 +71,35 @@ func TestChmodTarEntry(t *testing.T) {
6771
}
6872
}
6973
}
74+
75+
// TestChtimesSetsCreationTime verifies that updating file timestamps also
76+
// sets the Windows creation time from the modification time.
77+
func TestChtimesSetsCreationTime(t *testing.T) {
78+
tmpDir := t.TempDir()
79+
file := filepath.Join(tmpDir, "file")
80+
assert.NilError(t, os.WriteFile(file, []byte("hello toto"), 0o644))
81+
82+
root, err := os.OpenRoot(tmpDir)
83+
assert.NilError(t, err)
84+
t.Cleanup(func() { _ = root.Close() })
85+
86+
aTime := time.Date(2000, time.January, 2, 3, 4, 5, 0, time.UTC)
87+
mTime := time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC)
88+
assert.NilError(t, chtimes(root, "file", aTime, mTime))
89+
90+
fi, err := root.Stat("file")
91+
assert.NilError(t, err)
92+
93+
data, ok := fi.Sys().(*syscall.Win32FileAttributeData)
94+
assert.Assert(t, ok)
95+
96+
var (
97+
creationTime = time.Unix(0, data.CreationTime.Nanoseconds()).UTC()
98+
accessTime = time.Unix(0, data.LastAccessTime.Nanoseconds()).UTC()
99+
modificationTime = time.Unix(0, data.LastWriteTime.Nanoseconds()).UTC()
100+
)
101+
102+
assert.Equal(t, creationTime, mTime)
103+
assert.Equal(t, accessTime, aTime)
104+
assert.Equal(t, modificationTime, mTime)
105+
}

changes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func provisionSampleDir(t *testing.T, rootPath string, files []FileData) {
122122

123123
if info.filetype != Symlink {
124124
// Set a consistent ctime, atime for all files and dirs
125-
err := chtimes(filepath.Join(rootPath, name), now, now)
125+
err := chtimes(root, name, now, now)
126126
assert.NilError(t, err)
127127
}
128128
}
@@ -310,7 +310,7 @@ func mutateSampleDir(t *testing.T, rootPath string) {
310310
assert.NilError(t, err)
311311

312312
// Touch file
313-
err = chtimes(filepath.Join(rootPath, "file4"), time.Now().Add(time.Second), time.Now().Add(time.Second))
313+
err = chtimes(root, "file4", time.Now().Add(time.Second), time.Now().Add(time.Second))
314314
assert.NilError(t, err)
315315

316316
// Replace file with dir
@@ -345,7 +345,7 @@ func mutateSampleDir(t *testing.T, rootPath string) {
345345
assert.NilError(t, err)
346346

347347
// Touch dir
348-
err = chtimes(filepath.Join(rootPath, "dir3"), time.Now().Add(time.Second), time.Now().Add(time.Second))
348+
err = chtimes(root, "dir3", time.Now().Add(time.Second), time.Now().Add(time.Second))
349349
assert.NilError(t, err)
350350
}
351351

diff.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
213213
}
214214

215215
for _, d := range dirs {
216-
if err := root.Chtimes(d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil {
216+
if err := chtimes(root, d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil {
217217
return 0, err
218218
}
219219
}

time_nonwindows.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515
)
1616

1717
// chtimes changes the access and modification time of a file at the given
18-
// path.
18+
// path relative to root.
1919
//
2020
// Callers must use boundTime to ensure timestamps are within the range
2121
// supported by os.Chtimes.
22-
func chtimes(name string, atime time.Time, mtime time.Time) error {
23-
return os.Chtimes(name, atime, mtime)
22+
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
23+
return root.Chtimes(name, atime, mtime)
2424
}
2525

2626
func lchtimes(root *os.Root, name string, atime, mtime time.Time) error {

time_windows.go

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,95 @@ package archive
22

33
import (
44
"os"
5+
"path/filepath"
56
"time"
7+
"unsafe"
68

79
"golang.org/x/sys/windows"
810
)
911

1012
// chtimes changes the access and modification time of a file at the given
11-
// path.
13+
// path relative to root.
1214
//
1315
// Callers must use boundTime to ensure timestamps are within the range
1416
// supported by os.Chtimes.
15-
func chtimes(name string, atime time.Time, mtime time.Time) error {
16-
if err := os.Chtimes(name, atime, mtime); err != nil {
17+
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
18+
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
19+
if err != nil {
1720
return err
1821
}
22+
defer parent.Close()
23+
24+
return chtimesAt(parent, filepath.Base(name), atime, mtime, false)
25+
}
1926

20-
pathp, err := windows.UTF16PtrFromString(name)
27+
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
28+
return nil
29+
}
30+
31+
func chtimesAt(parent *os.File, name string, atime, mtime time.Time, noFollow bool) error {
32+
h, err := openForWriteAttributesAt(windows.Handle(parent.Fd()), name, noFollow)
2133
if err != nil {
2234
return err
2335
}
24-
h, err := windows.CreateFile(pathp,
25-
windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil,
26-
windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
36+
defer func() { _ = windows.Close(h) }()
37+
38+
var (
39+
creationTime = windows.NsecToFiletime(mtime.UnixNano())
40+
accessTime = windows.NsecToFiletime(atime.UnixNano())
41+
modificationTime = windows.NsecToFiletime(mtime.UnixNano())
42+
)
43+
return windows.SetFileTime(h, &creationTime, &accessTime, &modificationTime)
44+
}
45+
46+
// openForWriteAttributesAt opens name relative to parent with permission to
47+
// modify its file attributes. If noFollow is true, it does not follow reparse
48+
// points.
49+
//
50+
// This implementation is based on Go's internal Windows Openat support:
51+
//
52+
// https://github.com/golang/go/blob/go1.26.0/src/internal/syscall/windows/at_windows.go
53+
//
54+
// It is used by os.Root's Windows implementation for root-relative filesystem
55+
// operations:
56+
//
57+
// https://github.com/golang/go/blob/go1.26.0/src/os/root_windows.go
58+
//
59+
// Keep this implementation aligned with the upstream code until an equivalent
60+
// operation is available from golang.org/x/sys/windows.
61+
func openForWriteAttributesAt(parent windows.Handle, name string, noFollow bool) (windows.Handle, error) {
62+
name16, err := windows.UTF16FromString(name)
2763
if err != nil {
28-
return err
64+
return windows.InvalidHandle, err
2965
}
30-
defer windows.Close(h)
31-
c := windows.NsecToFiletime(mtime.UnixNano())
32-
return windows.SetFileTime(h, &c, nil, nil)
33-
}
3466

35-
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
36-
return nil
67+
attrs := uint32(windows.OBJ_CASE_INSENSITIVE)
68+
if noFollow {
69+
attrs |= windows.OBJ_DONT_REPARSE
70+
}
71+
72+
var handle windows.Handle
73+
err = windows.NtCreateFile(
74+
&handle,
75+
windows.SYNCHRONIZE|windows.FILE_WRITE_ATTRIBUTES,
76+
&windows.OBJECT_ATTRIBUTES{
77+
Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})),
78+
RootDirectory: parent,
79+
ObjectName: &windows.NTUnicodeString{
80+
Length: uint16((len(name16) - 1) * 2), // #nosec G115 -- Length is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
81+
MaximumLength: uint16(len(name16) * 2), // #nosec G115 -- MaximumLength is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
82+
Buffer: &name16[0],
83+
},
84+
Attributes: attrs,
85+
},
86+
&windows.IO_STATUS_BLOCK{},
87+
nil,
88+
windows.FILE_ATTRIBUTE_NORMAL,
89+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
90+
windows.FILE_OPEN,
91+
windows.FILE_OPEN_FOR_BACKUP_INTENT|windows.FILE_SYNCHRONOUS_IO_NONALERT,
92+
0, // EA buffer
93+
0, // EA length
94+
)
95+
return handle, err
3796
}

0 commit comments

Comments
 (0)