Skip to content

Commit 4b7e3c2

Browse files
committed
archive: fix creation time updates on Windows
Restore updating the creation time when applying timestamps on Windows. The previous refactoring to os.Root.Chtimes() only updated the access and modification times, causing a regression. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 384284b commit 4b7e3c2

6 files changed

Lines changed: 114 additions & 44 deletions

File tree

archive.go

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

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

archive_windows_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ func TestChtimesSetsCreationTime(t *testing.T) {
8484
t.Cleanup(func() { _ = root.Close() })
8585

8686
want := time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC)
87-
assert.NilError(t, root.Chtimes("file", want, want))
87+
assert.NilError(t, chtimes(root, "file", want, want))
8888

89-
fi, err := os.Stat(file)
89+
fi, err := root.Stat("file")
9090
assert.NilError(t, err)
9191

9292
data, ok := fi.Sys().(*syscall.Win32FileAttributeData)

changes_test.go

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,17 @@ func createSampleDir(t *testing.T, root string) {
9191
provisionSampleDir(t, root, files)
9292
}
9393

94-
func provisionSampleDir(t *testing.T, root string, files []FileData) {
94+
func provisionSampleDir(t *testing.T, rootPath string, files []FileData) {
95+
t.Helper()
96+
97+
root, err := os.OpenRoot(rootPath)
98+
assert.NilError(t, err)
99+
t.Cleanup(func() { _ = root.Close() })
100+
95101
now := time.Now()
96102
for _, info := range files {
97-
p := path.Join(root, info.path)
103+
name := path.Clean(info.path)
104+
p := filepath.Join(rootPath, filepath.FromSlash(name))
98105
switch info.filetype {
99106
case Dir:
100107
err := os.MkdirAll(p, info.permissions)
@@ -109,7 +116,7 @@ func provisionSampleDir(t *testing.T, root string, files []FileData) {
109116

110117
if info.filetype != Symlink {
111118
// Set a consistent ctime, atime for all files and dirs
112-
err := chtimes(p, now, now)
119+
err := chtimes(root, filepath.FromSlash(name), now, now)
113120
assert.NilError(t, err)
114121
}
115122
}
@@ -267,66 +274,70 @@ func TestChangesDirsEmpty(t *testing.T) {
267274
assert.NilError(t, os.RemoveAll(dst))
268275
}
269276

270-
func mutateSampleDir(t *testing.T, root string) {
277+
func mutateSampleDir(t *testing.T, rootPath string) {
278+
root, err := os.OpenRoot(rootPath)
279+
assert.NilError(t, err)
280+
t.Cleanup(func() { _ = root.Close() })
281+
271282
// Remove a regular file
272-
err := os.RemoveAll(path.Join(root, "file1"))
283+
err = root.RemoveAll("file1")
273284
assert.NilError(t, err)
274285

275286
// Remove a directory
276-
err = os.RemoveAll(path.Join(root, "dir1"))
287+
err = root.RemoveAll("dir1")
277288
assert.NilError(t, err)
278289

279290
// Remove a symlink
280-
err = os.RemoveAll(path.Join(root, "symlink1"))
291+
err = root.RemoveAll("symlink1")
281292
assert.NilError(t, err)
282293

283294
// Rewrite a file
284-
err = os.WriteFile(path.Join(root, "file2"), []byte("fileNN\n"), 0o777)
295+
err = root.WriteFile("file2", []byte("fileNN\n"), 0o777)
285296
assert.NilError(t, err)
286297

287298
// Replace a file
288-
err = os.RemoveAll(path.Join(root, "file3"))
299+
err = root.RemoveAll("file3")
289300
assert.NilError(t, err)
290-
err = os.WriteFile(path.Join(root, "file3"), []byte("fileMM\n"), 0o404)
301+
err = root.WriteFile("file3", []byte("fileMM\n"), 0o404)
291302
assert.NilError(t, err)
292303

293304
// Touch file
294-
err = chtimes(path.Join(root, "file4"), time.Now().Add(time.Second), time.Now().Add(time.Second))
305+
err = chtimes(root, "file4", time.Now().Add(time.Second), time.Now().Add(time.Second))
295306
assert.NilError(t, err)
296307

297308
// Replace file with dir
298-
err = os.RemoveAll(path.Join(root, "file5"))
309+
err = root.RemoveAll("file5")
299310
assert.NilError(t, err)
300-
err = os.MkdirAll(path.Join(root, "file5"), 0o666)
311+
err = root.Mkdir("file5", 0o666)
301312
assert.NilError(t, err)
302313

303314
// Create new file
304-
err = os.WriteFile(path.Join(root, "filenew"), []byte("filenew\n"), 0o777)
315+
err = root.WriteFile("filenew", []byte("filenew\n"), 0o777)
305316
assert.NilError(t, err)
306317

307318
// Create new dir
308-
err = os.MkdirAll(path.Join(root, "dirnew"), 0o766)
319+
err = root.MkdirAll("dirnew", 0o766)
309320
assert.NilError(t, err)
310321

311322
// Create a new symlink
312-
err = os.Symlink("targetnew", path.Join(root, "symlinknew"))
323+
err = root.Symlink("targetnew", "symlinknew")
313324
assert.NilError(t, err)
314325

315326
// Change a symlink
316-
err = os.RemoveAll(path.Join(root, "symlink2"))
327+
err = root.RemoveAll("symlink2")
317328
assert.NilError(t, err)
318329

319-
err = os.Symlink("target2change", path.Join(root, "symlink2"))
330+
err = root.Symlink("target2change", "symlink2")
320331
assert.NilError(t, err)
321332

322333
// Replace dir with file
323-
err = os.RemoveAll(path.Join(root, "dir2"))
334+
err = root.RemoveAll("dir2")
324335
assert.NilError(t, err)
325-
err = os.WriteFile(path.Join(root, "dir2"), []byte("dir2\n"), 0o777)
336+
err = root.WriteFile("dir2", []byte("dir2\n"), 0o777)
326337
assert.NilError(t, err)
327338

328339
// Touch dir
329-
err = chtimes(path.Join(root, "dir3"), time.Now().Add(time.Second), time.Now().Add(time.Second))
340+
err = chtimes(root, "dir3", time.Now().Add(time.Second), time.Now().Add(time.Second))
330341
assert.NilError(t, err)
331342
}
332343

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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import (
1414
"golang.org/x/sys/unix"
1515
)
1616

17-
// chtimes changes the access time and modified time of a file at the given path.
17+
// chtimes changes the access and modification times of a file at the given
18+
// path relative to root.
19+
//
1820
// If the modified time is prior to the Unix Epoch (unixMinTime), or after the
19-
// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior. In this
20-
// case, Chtimes defaults to Unix Epoch, just in case.
21-
func chtimes(name string, atime time.Time, mtime time.Time) error {
22-
return os.Chtimes(name, atime, mtime)
21+
// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior.
22+
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
23+
return root.Chtimes(name, atime, mtime)
2324
}
2425

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

time_windows.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,89 @@ package archive
22

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

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

10-
func chtimes(name string, atime time.Time, mtime time.Time) error {
11-
if err := os.Chtimes(name, atime, mtime); err != nil {
12-
return err
12+
// openForWriteAttributesAt opens name relative to parent with permission to
13+
// If noFollow is true, it does not follow reparse points.
14+
//
15+
// This implementation is based on Go's internal Windows Openat support:
16+
//
17+
// https://github.com/golang/go/blob/go1.26.0/src/internal/syscall/windows/at_windows.go
18+
//
19+
// It is used by os.Root's Windows implementation for root-relative filesystem
20+
// operations:
21+
//
22+
// https://github.com/golang/go/blob/go1.26.0/src/os/root_windows.go
23+
//
24+
// Keep this implementation aligned with the upstream code until an equivalent
25+
// operation is available from golang.org/x/sys/windows.
26+
func openForWriteAttributesAt(parent windows.Handle, name string, noFollow bool) (windows.Handle, error) {
27+
name16, err := windows.UTF16FromString(name)
28+
if err != nil {
29+
return windows.InvalidHandle, err
1330
}
1431

15-
pathp, err := windows.UTF16PtrFromString(name)
16-
if err != nil {
17-
return err
32+
attrs := uint32(windows.OBJ_CASE_INSENSITIVE)
33+
if noFollow {
34+
attrs |= windows.OBJ_DONT_REPARSE
1835
}
19-
h, err := windows.CreateFile(pathp,
20-
windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil,
21-
windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
36+
37+
var handle windows.Handle
38+
err = windows.NtCreateFile(
39+
&handle,
40+
windows.SYNCHRONIZE|windows.FILE_WRITE_ATTRIBUTES,
41+
&windows.OBJECT_ATTRIBUTES{
42+
Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})),
43+
RootDirectory: parent,
44+
ObjectName: &windows.NTUnicodeString{
45+
Length: uint16((len(name16) - 1) * 2), // #nosec G115 -- Length is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
46+
MaximumLength: uint16(len(name16) * 2), // #nosec G115 -- MaximumLength is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
47+
Buffer: &name16[0],
48+
},
49+
Attributes: attrs,
50+
},
51+
&windows.IO_STATUS_BLOCK{},
52+
nil,
53+
windows.FILE_ATTRIBUTE_NORMAL,
54+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
55+
windows.FILE_OPEN,
56+
windows.FILE_OPEN_FOR_BACKUP_INTENT|windows.FILE_SYNCHRONOUS_IO_NONALERT,
57+
0, // EA buffer
58+
0, // EA length
59+
)
60+
return handle, err
61+
}
62+
63+
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
64+
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
2265
if err != nil {
2366
return err
2467
}
25-
defer windows.Close(h)
26-
c := windows.NsecToFiletime(mtime.UnixNano())
27-
return windows.SetFileTime(h, &c, nil, nil)
68+
defer parent.Close()
69+
70+
return chtimesAt(parent, filepath.Base(name), atime, mtime, false)
2871
}
2972

3073
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
3174
return nil
3275
}
76+
77+
func chtimesAt(parent *os.File, name string, atime, mtime time.Time, noFollow bool) error {
78+
h, err := openForWriteAttributesAt(windows.Handle(parent.Fd()), name, noFollow)
79+
if err != nil {
80+
return err
81+
}
82+
defer func() { _ = windows.Close(h) }()
83+
84+
var (
85+
creationTime = windows.NsecToFiletime(mtime.UnixNano())
86+
accessTime = windows.NsecToFiletime(atime.UnixNano())
87+
modificationTime = windows.NsecToFiletime(mtime.UnixNano())
88+
)
89+
return windows.SetFileTime(h, &creationTime, &accessTime, &modificationTime)
90+
}

0 commit comments

Comments
 (0)