]> Cypherpunks repositories - gostls13.git/commitdiff
os: add Root.Chtimes
authorDamien Neil <dneil@google.com>
Tue, 11 Feb 2025 18:47:20 +0000 (10:47 -0800)
committerGopher Robot <gobot@golang.org>
Wed, 19 Mar 2025 00:32:48 +0000 (17:32 -0700)
For #67002

Change-Id: I9b10ac30f852052c85d6d21eb1752a9de5474346
Reviewed-on: https://go-review.googlesource.com/c/go/+/649515
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Kirill Kolyshkin <kolyshkin@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
api/next/67002.txt
doc/next/6-stdlib/99-minor/os/67002.md
src/internal/syscall/unix/utimes.go [new file with mode: 0644]
src/internal/syscall/unix/utimes_wasip1.go [new file with mode: 0644]
src/os/file_posix.go
src/os/root.go
src/os/root_noopenat.go
src/os/root_openat.go
src/os/root_test.go
src/os/root_unix.go
src/os/root_windows.go

index 216d3a3afeb606496cf5e8fd3071aa64aa1a8641..2e6b6fe6628af9d4ec28a9f18cdba8bafad08774 100644 (file)
@@ -1,2 +1,3 @@
 pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002
 pkg os, method (*Root) Chown(string, int, int) error #67002
+pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002
index 04ff6d5de0fec0fc77d8314676fed8bb6f36b63b..0f82fb31e6d2a3e34ec71f9e25d589f3e23db450 100644 (file)
@@ -2,3 +2,4 @@ The [os.Root] type supports the following additional methods:
 
   * [os.Root.Chmod]
   * [os.Root.Chown]
+  * [os.Root.Chtimes]
diff --git a/src/internal/syscall/unix/utimes.go b/src/internal/syscall/unix/utimes.go
new file mode 100644 (file)
index 0000000..332a4b6
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build unix && !wasip1
+
+package unix
+
+import (
+       "syscall"
+       _ "unsafe" // for //go:linkname
+)
+
+//go:linkname Utimensat syscall.utimensat
+func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error
diff --git a/src/internal/syscall/unix/utimes_wasip1.go b/src/internal/syscall/unix/utimes_wasip1.go
new file mode 100644 (file)
index 0000000..a0711b0
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build wasip1
+
+package unix
+
+import (
+       "syscall"
+       "unsafe"
+)
+
+//go:wasmimport wasi_snapshot_preview1 path_filestat_set_times
+//go:noescape
+func path_filestat_set_times(fd int32, flags uint32, path *byte, pathLen size, atim uint64, mtim uint64, fstflags uint32) syscall.Errno
+
+func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error {
+       if path == "" {
+               return syscall.EINVAL
+       }
+       atime := syscall.TimespecToNsec(times[0])
+       mtime := syscall.TimespecToNsec(times[1])
+
+       var fflag uint32
+       if times[0].Nsec != UTIME_OMIT {
+               fflag |= syscall.FILESTAT_SET_ATIM
+       }
+       if times[1].Nsec != UTIME_OMIT {
+               fflag |= syscall.FILESTAT_SET_MTIM
+       }
+       errno := path_filestat_set_times(
+               int32(dirfd),
+               syscall.LOOKUP_SYMLINK_FOLLOW,
+               unsafe.StringData(path),
+               size(len(path)),
+               uint64(atime),
+               uint64(mtime),
+               fflag,
+       )
+       return errnoErr(errno)
+}
index 8b06227d42e75fa04f530a32eae1d3d1f6640b2e..68904f62e1ffcaa42bfac2b19210b6f82b49f92f 100644 (file)
@@ -177,6 +177,14 @@ func (f *File) Sync() error {
 // less precise time unit.
 // If there is an error, it will be of type [*PathError].
 func Chtimes(name string, atime time.Time, mtime time.Time) error {
+       utimes := chtimesUtimes(atime, mtime)
+       if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
+               return &PathError{Op: "chtimes", Path: name, Err: e}
+       }
+       return nil
+}
+
+func chtimesUtimes(atime, mtime time.Time) [2]syscall.Timespec {
        var utimes [2]syscall.Timespec
        set := func(i int, t time.Time) {
                if t.IsZero() {
@@ -187,10 +195,7 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
        }
        set(0, atime)
        set(1, mtime)
-       if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
-               return &PathError{Op: "chtimes", Path: name, Err: e}
-       }
-       return nil
+       return utimes
 }
 
 // Chdir changes the current working directory to the file,
index 41342fcf53e02a2cdcd1f82b314148e272dff59d..bcabb496bcb9026e4a69539ebd278c3cf00f8e5d 100644 (file)
@@ -12,6 +12,7 @@ import (
        "io/fs"
        "runtime"
        "slices"
+       "time"
 )
 
 // OpenInRoot opens the file name in the directory dir.
@@ -54,7 +55,7 @@ func OpenInRoot(dir, name string) (*File, error) {
 //
 //   - When GOOS=windows, file names may not reference Windows reserved device names
 //     such as NUL and COM1.
-//   - On Unix, [Root.Chmod] and [Root.Chown] are vulnerable to a race condition.
+//   - On Unix, [Root.Chmod], [Root.Chown], and [Root.Chtimes] are vulnerable to a race condition.
 //     If the target of the operation is changed from a regular file to a symlink
 //     while the operation is in progress, the operation may be peformed on the link
 //     rather than the link target.
@@ -158,6 +159,12 @@ func (r *Root) Chown(name string, uid, gid int) error {
        return rootChown(r, name, uid, gid)
 }
 
+// Chtimes changes the access and modification times of the named file in the root.
+// See [Chtimes] for more details.
+func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error {
+       return rootChtimes(r, name, atime, mtime)
+}
+
 // Remove removes the named file or (empty) directory in the root.
 // See [Remove] for more details.
 func (r *Root) Remove(name string) error {
index 919e78c777825d3a60a70f7af051c35a7028345f..186a382df3c9c51baeaf9f9fd35d9c4e327e7351 100644 (file)
@@ -9,6 +9,7 @@ package os
 import (
        "errors"
        "sync/atomic"
+       "time"
 )
 
 // root implementation for platforms with no openat.
@@ -115,6 +116,16 @@ func rootChown(r *Root, name string, uid, gid int) error {
        return nil
 }
 
+func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
+       if err := checkPathEscapes(r, name); err != nil {
+               return &PathError{Op: "chtimesat", Path: name, Err: err}
+       }
+       if err := Chtimes(joinPath(r.root.name, name), atime, mtime); err != nil {
+               return &PathError{Op: "chtimesat", Path: name, Err: underlyingError(err)}
+       }
+       return nil
+}
+
 func rootMkdir(r *Root, name string, perm FileMode) error {
        if err := checkPathEscapes(r, name); err != nil {
                return &PathError{Op: "mkdirat", Path: name, Err: err}
index e25cba64af95f67f4a83399ce4478e872b5f7f63..e28b192f4cdbd16e6dfb7c2a6cdcf30deef07161 100644 (file)
@@ -11,6 +11,7 @@ import (
        "slices"
        "sync"
        "syscall"
+       "time"
 )
 
 // 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 {
        return nil
 }
 
+func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
+       _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
+               return struct{}{}, chtimesat(parent, name, atime, mtime)
+       })
+       if err != nil {
+               return &PathError{Op: "chtimesat", Path: name, Err: err}
+       }
+       return err
+}
+
 func rootMkdir(r *Root, name string, perm FileMode) error {
        _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
                return struct{}{}, mkdirat(parent, name, perm)
index 5560d435debf31f1198d47981a7fafa94080b207..5f0e733fe1425e558dfce021b05d8b0069a28020 100644 (file)
@@ -426,6 +426,53 @@ func TestRootChmod(t *testing.T) {
        }
 }
 
+func TestRootChtimes(t *testing.T) {
+       for _, test := range rootTestCases {
+               test.run(t, func(t *testing.T, target string, root *os.Root) {
+                       if target != "" {
+                               if err := os.WriteFile(target, nil, 0o666); err != nil {
+                                       t.Fatal(err)
+                               }
+                       }
+                       for _, times := range []struct {
+                               atime, mtime time.Time
+                       }{{
+                               atime: time.Now().Add(-1 * time.Minute),
+                               mtime: time.Now().Add(-1 * time.Minute),
+                       }, {
+                               atime: time.Now().Add(1 * time.Minute),
+                               mtime: time.Now().Add(1 * time.Minute),
+                       }, {
+                               atime: time.Time{},
+                               mtime: time.Now(),
+                       }, {
+                               atime: time.Now(),
+                               mtime: time.Time{},
+                       }} {
+                               if runtime.GOOS == "js" {
+                                       times.atime = times.atime.Truncate(1 * time.Second)
+                                       times.mtime = times.mtime.Truncate(1 * time.Second)
+                               }
+
+                               err := root.Chtimes(test.open, times.atime, times.mtime)
+                               if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
+                                       return
+                               }
+                               st, err := os.Stat(target)
+                               if err != nil {
+                                       t.Fatalf("os.Stat(%q) = %v", target, err)
+                               }
+                               if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
+                                       t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
+                               }
+                               if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
+                                       t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
+                               }
+                       }
+               })
+       }
+}
+
 func TestRootMkdir(t *testing.T) {
        for _, test := range rootTestCases {
                test.run(t, func(t *testing.T, target string, root *os.Root) {
index f2f8e52bb23466f6cddbbea3aa5e0ea6e720b6bb..884c1a38d951d6d359ae38b1ef30b3d4a5e9a1e2 100644 (file)
@@ -11,6 +11,7 @@ import (
        "internal/syscall/unix"
        "runtime"
        "syscall"
+       "time"
 )
 
 type sysfdType = int
@@ -165,6 +166,15 @@ func chownat(parent int, name string, uid, gid int) error {
        })
 }
 
+func chtimesat(parent int, name string, atime time.Time, mtime time.Time) error {
+       return afterResolvingSymlink(parent, name, func() error {
+               return ignoringEINTR(func() error {
+                       utimes := chtimesUtimes(atime, mtime)
+                       return unix.Utimensat(parent, name, &utimes, unix.AT_SYMLINK_NOFOLLOW)
+               })
+       })
+}
+
 func mkdirat(fd int, name string, perm FileMode) error {
        return ignoringEINTR(func() error {
                return unix.Mkdirat(fd, name, syscallMode(perm))
index 4f391cb2a7d9fd54740d7e451ecd7a66f9300a20..eed81ea51b85bd76cee76970936577a55c83167e 100644 (file)
@@ -13,6 +13,7 @@ import (
        "internal/syscall/windows"
        "runtime"
        "syscall"
+       "time"
        "unsafe"
 )
 
@@ -287,3 +288,25 @@ func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
 func removeat(dirfd syscall.Handle, name string) error {
        return windows.Deleteat(dirfd, name)
 }
+
+func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {
+       h, err := windows.Openat(dirfd, name, syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY|windows.O_WRITE_ATTRS, 0)
+       if err == syscall.ELOOP || err == syscall.ENOTDIR {
+               if link, err := readReparseLinkAt(dirfd, name); err == nil {
+                       return errSymlink(link)
+               }
+       }
+       if err != nil {
+               return err
+       }
+       defer syscall.CloseHandle(h)
+       a := syscall.Filetime{}
+       w := syscall.Filetime{}
+       if !atime.IsZero() {
+               a = syscall.NsecToFiletime(atime.UnixNano())
+       }
+       if !mtime.IsZero() {
+               w = syscall.NsecToFiletime(mtime.UnixNano())
+       }
+       return syscall.SetFileTime(h, nil, &a, &w)
+}