os: make Chtimes accept empty time values to skip file time modification

Empty time value time.Time{} leaves the corresponding time of the file
unchanged.

Fixes #32558

Change-Id: I1aff42f30668ff505ecec2e9509d8f2b8e4b1b6a
Reviewed-on: https://go-review.googlesource.com/c/go/+/219638
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Ian Lance Taylor <iant@google.com>
Run-TryBot: Ian Lance Taylor <iant@google.com>
Run-TryBot: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Constantin Konstantinidis 2020-10-19 19:19:17 +02:00 committed by Gopher Robot
parent 96add980ad
commit 5a9b6432ec
18 changed files with 232 additions and 8 deletions

View file

@ -11,4 +11,5 @@ package unix
const (
AT_REMOVEDIR = 0x1
AT_SYMLINK_NOFOLLOW = 0x1
UTIME_OMIT = -0x3
)

View file

@ -0,0 +1,13 @@
// Copyright 2020 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.
package unix
const (
// UTIME_OMIT is the sentinel value to indicate that a time value should not
// be changed. It is useful for example to indicate for example with UtimesNano
// to avoid changing AccessTime or ModifiedTime.
// Its value must match syscall/fs_js.go
UTIME_OMIT = -0x2
)

View file

@ -16,4 +16,6 @@ func syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err
const (
AT_REMOVEDIR = 0x1
AT_SYMLINK_NOFOLLOW = 0x1000
UTIME_OMIT = -0x2
)

View file

@ -6,3 +6,5 @@ package unix
const AT_REMOVEDIR = 0x80
const AT_SYMLINK_NOFOLLOW = 0x0020
const UTIME_OMIT = -0x2

View file

@ -12,3 +12,5 @@ const fstatatTrap uintptr = syscall.SYS_FSTATAT
const AT_REMOVEDIR = 0x2
const AT_SYMLINK_NOFOLLOW = 0x1
const UTIME_OMIT = -0x1

View file

@ -10,6 +10,8 @@ const (
AT_REMOVEDIR = 0x800
AT_SYMLINK_NOFOLLOW = 0x200
UTIME_OMIT = -0x2
unlinkatTrap uintptr = syscall.SYS_UNLINKAT
openatTrap uintptr = syscall.SYS_OPENAT
posixFallocateTrap uintptr = syscall.SYS_POSIX_FALLOCATE

View file

@ -14,4 +14,6 @@ const (
AT_FDCWD = -0x64
AT_REMOVEDIR = 0x200
AT_SYMLINK_NOFOLLOW = 0x100
UTIME_OMIT = 0x3ffffffe
)

View file

@ -12,3 +12,5 @@ const fstatatTrap uintptr = syscall.SYS_FSTATAT
const AT_REMOVEDIR = 0x800
const AT_SYMLINK_NOFOLLOW = 0x200
const UTIME_OMIT = (1 << 30) - 2

View file

@ -12,3 +12,5 @@ const fstatatTrap uintptr = syscall.SYS_FSTATAT
const AT_REMOVEDIR = 0x08
const AT_SYMLINK_NOFOLLOW = 0x02
const UTIME_OMIT = -0x1

View file

@ -0,0 +1,13 @@
// Copyright 2020 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.
package unix
const (
// UTIME_OMIT is the sentinel value to indicate that a time value should not
// be changed. It is useful for example to indicate for example with UtimesNano
// to avoid changing AccessTime or ModifiedTime.
// Its value must match syscall/fs_wasip1.go
UTIME_OMIT = -0x2
)

View file

@ -447,6 +447,7 @@ func chmod(name string, mode FileMode) error {
// Chtimes changes the access and modification times of the named
// file, similar to the Unix utime() or utimes() functions.
// A zero time.Time value will leave the corresponding file time unchanged.
//
// The underlying filesystem may truncate or round the values to a
// less precise time unit.
@ -457,6 +458,12 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
d.Null()
d.Atime = uint32(atime.Unix())
d.Mtime = uint32(mtime.Unix())
if atime.IsZero() {
d.Atime = 0xFFFFFFFF
}
if mtime.IsZero() {
d.Mtime = 0xFFFFFFFF
}
var buf [syscall.STATFIXLEN]byte
n, err := d.Marshal(buf[:])

View file

@ -173,14 +173,22 @@ func (f *File) Sync() error {
// Chtimes changes the access and modification times of the named
// file, similar to the Unix utime() or utimes() functions.
// A zero time.Time value will leave the corresponding file time unchanged.
//
// The underlying filesystem may truncate or round the values to a
// 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 {
var utimes [2]syscall.Timespec
utimes[0] = syscall.NsecToTimespec(atime.UnixNano())
utimes[1] = syscall.NsecToTimespec(mtime.UnixNano())
set := func(i int, t time.Time) {
if t.IsZero() {
utimes[i] = syscall.Timespec{Sec: _UTIME_OMIT, Nsec: _UTIME_OMIT}
} else {
utimes[i] = syscall.NsecToTimespec(t.UnixNano())
}
}
set(0, atime)
set(1, mtime)
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
return &PathError{Op: "chtimes", Path: name, Err: e}
}

View file

@ -14,6 +14,8 @@ import (
"syscall"
)
const _UTIME_OMIT = unix.UTIME_OMIT
// fixLongPath is a noop on non-Windows platforms.
func fixLongPath(path string) string {
return path

View file

@ -15,6 +15,8 @@ import (
"unsafe"
)
const _UTIME_OMIT = 0
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer

View file

@ -1386,6 +1386,128 @@ func TestChtimes(t *testing.T) {
testChtimes(t, f.Name())
}
func TestChtimesWithZeroTimes(t *testing.T) {
file := newFile("chtimes-with-zero", t)
_, err := file.Write([]byte("hello, world\n"))
if err != nil {
t.Fatalf("Write: %s", err)
}
fName := file.Name()
defer Remove(file.Name())
err = file.Close()
if err != nil {
t.Errorf("%v", err)
}
fs, err := Stat(fName)
if err != nil {
t.Fatal(err)
}
startAtime := Atime(fs)
startMtime := fs.ModTime()
switch runtime.GOOS {
case "js":
startAtime = startAtime.Truncate(time.Second)
startMtime = startMtime.Truncate(time.Second)
}
at0 := startAtime
mt0 := startMtime
t0 := startMtime.Truncate(time.Second).Add(1 * time.Hour)
tests := []struct {
aTime time.Time
mTime time.Time
wantATime time.Time
wantMTime time.Time
}{
{
aTime: time.Time{},
mTime: time.Time{},
wantATime: startAtime,
wantMTime: startMtime,
},
{
aTime: t0.Add(200 * time.Second),
mTime: time.Time{},
wantATime: t0.Add(200 * time.Second),
wantMTime: startMtime,
},
{
aTime: time.Time{},
mTime: t0.Add(100 * time.Second),
wantATime: t0.Add(200 * time.Second),
wantMTime: t0.Add(100 * time.Second),
},
{
aTime: t0.Add(300 * time.Second),
mTime: t0.Add(100 * time.Second),
wantATime: t0.Add(300 * time.Second),
wantMTime: t0.Add(100 * time.Second),
},
}
for _, tt := range tests {
// Now change the times accordingly.
if err := Chtimes(fName, tt.aTime, tt.mTime); err != nil {
t.Error(err)
}
// Finally verify the expectations.
fs, err = Stat(fName)
if err != nil {
t.Error(err)
}
at0 = Atime(fs)
mt0 = fs.ModTime()
if got, want := at0, tt.wantATime; !got.Equal(want) {
errormsg := fmt.Sprintf("AccessTime mismatch with values ATime:%q-MTime:%q\ngot: %q\nwant: %q", tt.aTime, tt.mTime, got, want)
switch runtime.GOOS {
case "plan9":
// Mtime is the time of the last change of
// content. Similarly, atime is set whenever
// the contents are accessed; also, it is set
// whenever mtime is set.
case "windows":
t.Error(errormsg)
default: // unix's
if got, want := at0, tt.wantATime; !got.Equal(want) {
mounts, err := ReadFile("/bin/mounts")
if err != nil {
mounts, err = ReadFile("/etc/mtab")
}
if strings.Contains(string(mounts), "noatime") {
t.Log(errormsg)
t.Log("A filesystem is mounted with noatime; ignoring.")
} else {
switch runtime.GOOS {
case "netbsd", "dragonfly":
// On a 64-bit implementation, birth time is generally supported and cannot be changed.
// When supported, atime update is restricted and depends on the file system and on the
// OS configuration.
if strings.Contains(runtime.GOARCH, "64") {
t.Log(errormsg)
t.Log("Filesystem might not support atime changes; ignoring.")
}
default:
t.Error(errormsg)
}
}
}
}
}
if got, want := mt0, tt.wantMTime; !got.Equal(want) {
errormsg := fmt.Sprintf("ModTime mismatch with values ATime:%q-MTime:%q\ngot: %q\nwant: %q", tt.aTime, tt.mTime, got, want)
switch runtime.GOOS {
case "dragonfly":
t.Log(errormsg)
t.Log("Mtime is always updated; ignoring.")
default:
t.Error(errormsg)
}
}
}
}
// Use TempDir (via newDir) to make sure we're on a local file system,
// so that timings are not distorted by latency and caching.
// On NFS, timings can be off due to caching of meta-data on

View file

@ -273,6 +273,8 @@ func Lchown(path string, uid, gid int) error {
}
func UtimesNano(path string, ts []Timespec) error {
// UTIME_OMIT value must match internal/syscall/unix/at_js.go
const UTIME_OMIT = -0x2
if err := checkPath(path); err != nil {
return err
}
@ -281,6 +283,18 @@ func UtimesNano(path string, ts []Timespec) error {
}
atime := ts[0].Sec
mtime := ts[1].Sec
if atime == UTIME_OMIT || mtime == UTIME_OMIT {
var st Stat_t
if err := Stat(path, &st); err != nil {
return err
}
if atime == UTIME_OMIT {
atime = st.Atime
}
if mtime == UTIME_OMIT {
mtime = st.Mtime
}
}
_, err := fsCall("utimes", path, atime, mtime)
return err
}

View file

@ -663,17 +663,33 @@ func Lchown(path string, uid, gid int) error {
}
func UtimesNano(path string, ts []Timespec) error {
// UTIME_OMIT value must match internal/syscall/unix/at_wasip1.go
const UTIME_OMIT = -0x2
if path == "" {
return EINVAL
}
dirFd, pathPtr, pathLen := preparePath(path)
atime := TimespecToNsec(ts[0])
mtime := TimespecToNsec(ts[1])
if ts[0].Nsec == UTIME_OMIT || ts[1].Nsec == UTIME_OMIT {
var st Stat_t
if err := Stat(path, &st); err != nil {
return err
}
if ts[0].Nsec == UTIME_OMIT {
atime = int64(st.Atime)
}
if ts[1].Nsec == UTIME_OMIT {
mtime = int64(st.Mtime)
}
}
errno := path_filestat_set_times(
dirFd,
LOOKUP_SYMLINK_FOLLOW,
pathPtr,
pathLen,
timestamp(TimespecToNsec(ts[0])),
timestamp(TimespecToNsec(ts[1])),
timestamp(atime),
timestamp(mtime),
FILESTAT_SET_ATIM|FILESTAT_SET_MTIM,
)
return errnoErr(errno)

View file

@ -635,8 +635,14 @@ func Utimes(path string, tv []Timeval) (err error) {
return e
}
defer Close(h)
a := NsecToFiletime(tv[0].Nanoseconds())
w := NsecToFiletime(tv[1].Nanoseconds())
a := Filetime{}
w := Filetime{}
if tv[0].Nanoseconds() != 0 {
a = NsecToFiletime(tv[0].Nanoseconds())
}
if tv[0].Nanoseconds() != 0 {
w = NsecToFiletime(tv[1].Nanoseconds())
}
return SetFileTime(h, nil, &a, &w)
}
@ -655,8 +661,14 @@ func UtimesNano(path string, ts []Timespec) (err error) {
return e
}
defer Close(h)
a := NsecToFiletime(TimespecToNsec(ts[0]))
w := NsecToFiletime(TimespecToNsec(ts[1]))
a := Filetime{}
w := Filetime{}
if TimespecToNsec(ts[0]) != 0 {
a = NsecToFiletime(TimespecToNsec(ts[0]))
}
if TimespecToNsec(ts[1]) != 0 {
w = NsecToFiletime(TimespecToNsec(ts[1]))
}
return SetFileTime(h, nil, &a, &w)
}