Skip to content

Commit 2148309

Browse files
neildgopherbot
authored andcommitted
os: add Root.Chtimes
For #67002 Change-Id: I9b10ac30f852052c85d6d21eb1752a9de5474346 Reviewed-on: https://go-review.googlesource.com/c/go/+/649515 Auto-Submit: Damien Neil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Kirill Kolyshkin <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 3309658 commit 2148309

File tree

11 files changed

+178
-5
lines changed

11 files changed

+178
-5
lines changed

api/next/67002.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002
22
pkg os, method (*Root) Chown(string, int, int) error #67002
3+
pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002

doc/next/6-stdlib/99-minor/os/67002.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ The [os.Root] type supports the following additional methods:
22

33
* [os.Root.Chmod]
44
* [os.Root.Chown]
5+
* [os.Root.Chtimes]

src/internal/syscall/unix/utimes.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
//go:build unix && !wasip1
6+
7+
package unix
8+
9+
import (
10+
"syscall"
11+
_ "unsafe" // for //go:linkname
12+
)
13+
14+
//go:linkname Utimensat syscall.utimensat
15+
func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
//go:build wasip1
6+
7+
package unix
8+
9+
import (
10+
"syscall"
11+
"unsafe"
12+
)
13+
14+
//go:wasmimport wasi_snapshot_preview1 path_filestat_set_times
15+
//go:noescape
16+
func path_filestat_set_times(fd int32, flags uint32, path *byte, pathLen size, atim uint64, mtim uint64, fstflags uint32) syscall.Errno
17+
18+
func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error {
19+
if path == "" {
20+
return syscall.EINVAL
21+
}
22+
atime := syscall.TimespecToNsec(times[0])
23+
mtime := syscall.TimespecToNsec(times[1])
24+
25+
var fflag uint32
26+
if times[0].Nsec != UTIME_OMIT {
27+
fflag |= syscall.FILESTAT_SET_ATIM
28+
}
29+
if times[1].Nsec != UTIME_OMIT {
30+
fflag |= syscall.FILESTAT_SET_MTIM
31+
}
32+
errno := path_filestat_set_times(
33+
int32(dirfd),
34+
syscall.LOOKUP_SYMLINK_FOLLOW,
35+
unsafe.StringData(path),
36+
size(len(path)),
37+
uint64(atime),
38+
uint64(mtime),
39+
fflag,
40+
)
41+
return errnoErr(errno)
42+
}

src/os/file_posix.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ func (f *File) Sync() error {
177177
// less precise time unit.
178178
// If there is an error, it will be of type [*PathError].
179179
func Chtimes(name string, atime time.Time, mtime time.Time) error {
180+
utimes := chtimesUtimes(atime, mtime)
181+
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
182+
return &PathError{Op: "chtimes", Path: name, Err: e}
183+
}
184+
return nil
185+
}
186+
187+
func chtimesUtimes(atime, mtime time.Time) [2]syscall.Timespec {
180188
var utimes [2]syscall.Timespec
181189
set := func(i int, t time.Time) {
182190
if t.IsZero() {
@@ -187,10 +195,7 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
187195
}
188196
set(0, atime)
189197
set(1, mtime)
190-
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
191-
return &PathError{Op: "chtimes", Path: name, Err: e}
192-
}
193-
return nil
198+
return utimes
194199
}
195200

196201
// Chdir changes the current working directory to the file,

src/os/root.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io/fs"
1313
"runtime"
1414
"slices"
15+
"time"
1516
)
1617

1718
// OpenInRoot opens the file name in the directory dir.
@@ -54,7 +55,7 @@ func OpenInRoot(dir, name string) (*File, error) {
5455
//
5556
// - When GOOS=windows, file names may not reference Windows reserved device names
5657
// such as NUL and COM1.
57-
// - On Unix, [Root.Chmod] and [Root.Chown] are vulnerable to a race condition.
58+
// - On Unix, [Root.Chmod], [Root.Chown], and [Root.Chtimes] are vulnerable to a race condition.
5859
// If the target of the operation is changed from a regular file to a symlink
5960
// while the operation is in progress, the operation may be peformed on the link
6061
// rather than the link target.
@@ -158,6 +159,12 @@ func (r *Root) Chown(name string, uid, gid int) error {
158159
return rootChown(r, name, uid, gid)
159160
}
160161

162+
// Chtimes changes the access and modification times of the named file in the root.
163+
// See [Chtimes] for more details.
164+
func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error {
165+
return rootChtimes(r, name, atime, mtime)
166+
}
167+
161168
// Remove removes the named file or (empty) directory in the root.
162169
// See [Remove] for more details.
163170
func (r *Root) Remove(name string) error {

src/os/root_noopenat.go

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package os
99
import (
1010
"errors"
1111
"sync/atomic"
12+
"time"
1213
)
1314

1415
// root implementation for platforms with no openat.
@@ -115,6 +116,16 @@ func rootChown(r *Root, name string, uid, gid int) error {
115116
return nil
116117
}
117118

119+
func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
120+
if err := checkPathEscapes(r, name); err != nil {
121+
return &PathError{Op: "chtimesat", Path: name, Err: err}
122+
}
123+
if err := Chtimes(joinPath(r.root.name, name), atime, mtime); err != nil {
124+
return &PathError{Op: "chtimesat", Path: name, Err: underlyingError(err)}
125+
}
126+
return nil
127+
}
128+
118129
func rootMkdir(r *Root, name string, perm FileMode) error {
119130
if err := checkPathEscapes(r, name); err != nil {
120131
return &PathError{Op: "mkdirat", Path: name, Err: err}

src/os/root_openat.go

+11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"slices"
1212
"sync"
1313
"syscall"
14+
"time"
1415
)
1516

1617
// root implementation for platforms with a function to open a file
@@ -87,6 +88,16 @@ func rootChown(r *Root, name string, uid, gid int) error {
8788
return nil
8889
}
8990

91+
func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
92+
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
93+
return struct{}{}, chtimesat(parent, name, atime, mtime)
94+
})
95+
if err != nil {
96+
return &PathError{Op: "chtimesat", Path: name, Err: err}
97+
}
98+
return err
99+
}
100+
90101
func rootMkdir(r *Root, name string, perm FileMode) error {
91102
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
92103
return struct{}{}, mkdirat(parent, name, perm)

src/os/root_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,53 @@ func TestRootChmod(t *testing.T) {
426426
}
427427
}
428428

429+
func TestRootChtimes(t *testing.T) {
430+
for _, test := range rootTestCases {
431+
test.run(t, func(t *testing.T, target string, root *os.Root) {
432+
if target != "" {
433+
if err := os.WriteFile(target, nil, 0o666); err != nil {
434+
t.Fatal(err)
435+
}
436+
}
437+
for _, times := range []struct {
438+
atime, mtime time.Time
439+
}{{
440+
atime: time.Now().Add(-1 * time.Minute),
441+
mtime: time.Now().Add(-1 * time.Minute),
442+
}, {
443+
atime: time.Now().Add(1 * time.Minute),
444+
mtime: time.Now().Add(1 * time.Minute),
445+
}, {
446+
atime: time.Time{},
447+
mtime: time.Now(),
448+
}, {
449+
atime: time.Now(),
450+
mtime: time.Time{},
451+
}} {
452+
if runtime.GOOS == "js" {
453+
times.atime = times.atime.Truncate(1 * time.Second)
454+
times.mtime = times.mtime.Truncate(1 * time.Second)
455+
}
456+
457+
err := root.Chtimes(test.open, times.atime, times.mtime)
458+
if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
459+
return
460+
}
461+
st, err := os.Stat(target)
462+
if err != nil {
463+
t.Fatalf("os.Stat(%q) = %v", target, err)
464+
}
465+
if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
466+
t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
467+
}
468+
if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
469+
t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
470+
}
471+
}
472+
})
473+
}
474+
}
475+
429476
func TestRootMkdir(t *testing.T) {
430477
for _, test := range rootTestCases {
431478
test.run(t, func(t *testing.T, target string, root *os.Root) {

src/os/root_unix.go

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"internal/syscall/unix"
1212
"runtime"
1313
"syscall"
14+
"time"
1415
)
1516

1617
type sysfdType = int
@@ -165,6 +166,15 @@ func chownat(parent int, name string, uid, gid int) error {
165166
})
166167
}
167168

169+
func chtimesat(parent int, name string, atime time.Time, mtime time.Time) error {
170+
return afterResolvingSymlink(parent, name, func() error {
171+
return ignoringEINTR(func() error {
172+
utimes := chtimesUtimes(atime, mtime)
173+
return unix.Utimensat(parent, name, &utimes, unix.AT_SYMLINK_NOFOLLOW)
174+
})
175+
})
176+
}
177+
168178
func mkdirat(fd int, name string, perm FileMode) error {
169179
return ignoringEINTR(func() error {
170180
return unix.Mkdirat(fd, name, syscallMode(perm))

src/os/root_windows.go

+23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"internal/syscall/windows"
1414
"runtime"
1515
"syscall"
16+
"time"
1617
"unsafe"
1718
)
1819

@@ -287,3 +288,25 @@ func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
287288
func removeat(dirfd syscall.Handle, name string) error {
288289
return windows.Deleteat(dirfd, name)
289290
}
291+
292+
func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {
293+
h, err := windows.Openat(dirfd, name, syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY|windows.O_WRITE_ATTRS, 0)
294+
if err == syscall.ELOOP || err == syscall.ENOTDIR {
295+
if link, err := readReparseLinkAt(dirfd, name); err == nil {
296+
return errSymlink(link)
297+
}
298+
}
299+
if err != nil {
300+
return err
301+
}
302+
defer syscall.CloseHandle(h)
303+
a := syscall.Filetime{}
304+
w := syscall.Filetime{}
305+
if !atime.IsZero() {
306+
a = syscall.NsecToFiletime(atime.UnixNano())
307+
}
308+
if !mtime.IsZero() {
309+
w = syscall.NsecToFiletime(mtime.UnixNano())
310+
}
311+
return syscall.SetFileTime(h, nil, &a, &w)
312+
}

0 commit comments

Comments
 (0)