Adds Utimens to platform.File (#1440)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-05-08 11:14:28 +08:00
committed by GitHub
parent 76ef347b8c
commit 3bf7e342c2
8 changed files with 103 additions and 81 deletions

View File

@@ -457,7 +457,7 @@ func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) sy
} }
// Try to update the file timestamps by file-descriptor. // Try to update the file timestamps by file-descriptor.
errno = platform.UtimensFile(f.File.File(), &times) errno = f.File.Utimens(&times)
// Fall back to path based, despite it being less precise. // Fall back to path based, despite it being less precise.
switch errno { switch errno {

View File

@@ -9,13 +9,15 @@ import (
"testing" "testing"
) )
func Benchmark_UtimensFile(b *testing.B) { func BenchmarkFsFileUtimesNs(b *testing.B) {
tmpDir := b.TempDir() tmpDir := b.TempDir()
f, err := os.Create(path.Join(tmpDir, "file")) path := path.Join(tmpDir, "file")
f, err := os.Create(path)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
defer f.Close() defer f.Close()
fs := NewFsFile(path, syscall.O_RDONLY, f)
times := &[2]syscall.Timespec{ times := &[2]syscall.Timespec{
{Sec: 123, Nsec: 4 * 1e3}, {Sec: 123, Nsec: 4 * 1e3},
@@ -24,8 +26,8 @@ func Benchmark_UtimensFile(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if err := UtimensFile(f, times); err != 0 { if errno := fs.Utimens(times); errno != 0 {
b.Fatal(err) b.Fatal(errno)
} }
} }
} }

View File

@@ -207,6 +207,30 @@ type File interface {
// - This always returns syscall.ENOSYS on windows. // - This always returns syscall.ENOSYS on windows.
Chown(uid, gid int) syscall.Errno Chown(uid, gid int) syscall.Errno
// Utimens set file access and modification times of this file, at
// nanosecond precision.
//
// # Parameters
//
// The `times` parameter includes the access and modification timestamps to
// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be
// specified instead of real timestamps. A nil `times` parameter behaves the
// same as if both were set to UTIME_NOW.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.UtimesNano and `futimens` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
// - Windows requires files to be open with syscall.O_RDWR, which means you
// cannot use this to update timestamps on a directory (syscall.EPERM).
Utimens(times *[2]syscall.Timespec) syscall.Errno
// Close closes the underlying file. // Close closes the underlying file.
// //
// A zero syscall.Errno is success. The below are expected otherwise: // A zero syscall.Errno is success. The below are expected otherwise:
@@ -276,6 +300,11 @@ func (UnimplementedFile) Chown(int, int) syscall.Errno {
return syscall.ENOSYS return syscall.ENOSYS
} }
// Utimens implements File.Utimens
func (UnimplementedFile) Utimens(*[2]syscall.Timespec) syscall.Errno {
return syscall.ENOSYS
}
func NewFsFile(openPath string, openFlag int, f fs.File) File { func NewFsFile(openPath string, openFlag int, f fs.File) File {
return &fsFile{ return &fsFile{
path: openPath, path: openPath,
@@ -442,6 +471,15 @@ func (f *fsFile) Chown(uid, gid int) syscall.Errno {
return syscall.ENOSYS return syscall.ENOSYS
} }
// Utimens implements File.Utimens
func (f *fsFile) Utimens(times *[2]syscall.Timespec) syscall.Errno {
if f, ok := f.file.(fdFile); ok {
err := futimens(f.Fd(), times)
return UnwrapOSError(err)
}
return syscall.ENOSYS
}
// Close implements File.Close // Close implements File.Close
func (f *fsFile) Close() syscall.Errno { func (f *fsFile) Close() syscall.Errno {
return UnwrapOSError(f.file.Close()) return UnwrapOSError(f.file.Close())

View File

@@ -456,6 +456,7 @@ func testSync(t *testing.T, sync func(File) syscall.Errno) {
// Windows allows you to sync a closed file // Windows allows you to sync a closed file
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
testEBADFIfFileClosed(t, sync) testEBADFIfFileClosed(t, sync)
testEBADFIfDirClosed(t, sync)
} }
} }
@@ -530,6 +531,39 @@ func TestFsFileTruncate(t *testing.T) {
}) })
} }
func TestFsFileUtimens(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin": // supported
case "freebsd": // TODO: support freebsd w/o CGO
case "windows":
if !IsGo120 {
t.Skip("windows only works after Go 1.20") // TODO: possibly 1.19 ;)
}
default: // expect ENOSYS and callers need to fall back to Utimens
t.Skip("unsupported GOOS", runtime.GOOS)
}
testUtimens(t, true)
testEBADFIfFileClosed(t, func(f File) syscall.Errno {
return f.Utimens(nil)
})
testEBADFIfDirClosed(t, func(d File) syscall.Errno {
return d.Utimens(nil)
})
}
func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool {
return t.Run("EBADF if dir closed", func(t *testing.T) {
d := openFsFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
// close the directory underneath
require.Zero(t, d.Close())
require.EqualErrno(t, syscall.EBADF, fn(d))
})
}
func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool { func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool {
return t.Run("EBADF if file closed", func(t *testing.T) { return t.Run("EBADF if file closed", func(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()

View File

@@ -1,7 +1,6 @@
package platform package platform
import ( import (
"io/fs"
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
@@ -33,39 +32,21 @@ const (
// //
// # Errors // # Errors
// //
// The following errors are expected: // A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid. // - syscall.EINVAL: `path` is invalid.
// - syscall.EEXIST: `path` exists and is a directory. // - syscall.EEXIST: `path` exists and is a directory.
// - syscall.ENOTDIR: `path` exists and is a file. // - syscall.ENOTDIR: `path` exists and is a file.
// //
// # Notes // # Notes
// //
// - This is similar to syscall.UtimesNano, except that doesn't have flags to // - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in
// control expansion of symbolic links. It also doesn't support special // POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
// values UTIME_NOW or UTIME_NOW.
// - This is like `utimensat` with `AT_FDCWD` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
func Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { func Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
err := utimens(path, times, symlinkFollow) err := utimens(path, times, symlinkFollow)
return UnwrapOSError(err) return UnwrapOSError(err)
} }
// UtimensFile is like Utimens, except it works on a file, not a path.
//
// # Notes
//
// - Windows requires files to be open with syscall.O_RDWR, which means you
// cannot use this to update timestamps on a directory (syscall.EPERM).
// - This is like the function `futimens` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
func UtimensFile(f fs.File, times *[2]syscall.Timespec) syscall.Errno {
if f, ok := f.(fdFile); ok {
err := futimens(f.Fd(), times)
return UnwrapOSError(err)
}
return syscall.ENOSYS
}
func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused
var _p0 unsafe.Pointer var _p0 unsafe.Pointer
if times != nil { if times != nil {

View File

@@ -23,10 +23,10 @@ func TestUtimens(t *testing.T) {
require.EqualErrno(t, syscall.ENOSYS, err) require.EqualErrno(t, syscall.ENOSYS, err)
} }
}) })
testFutimens(t, true) testUtimens(t, false)
} }
func testFutimens(t *testing.T, usePath bool) { func testUtimens(t *testing.T, futimes bool) {
// Note: This sets microsecond granularity because Windows doesn't support // Note: This sets microsecond granularity because Windows doesn't support
// nanosecond. // nanosecond.
// //
@@ -112,7 +112,7 @@ func testFutimens(t *testing.T, usePath bool) {
// symlinkNoFollow is invalid for file descriptor based operations, // symlinkNoFollow is invalid for file descriptor based operations,
// because the default for open is to follow links. You can't avoid // because the default for open is to follow links. You can't avoid
// this. O_NOFOLLOW is used only to return ELOOP on a link. // this. O_NOFOLLOW is used only to return ELOOP on a link.
if !usePath && symlinkNoFollow { if futimes && symlinkNoFollow {
continue continue
} }
@@ -150,7 +150,7 @@ func testFutimens(t *testing.T, usePath bool) {
oldSt, errno := Lstat(statPath) oldSt, errno := Lstat(statPath)
require.EqualErrno(t, 0, errno) require.EqualErrno(t, 0, errno)
if usePath { if !futimes {
err = Utimens(path, tc.times, !symlinkNoFollow) err = Utimens(path, tc.times, !symlinkNoFollow)
if symlinkNoFollow && !SupportsSymlinkNoFollow { if symlinkNoFollow && !SupportsSymlinkNoFollow {
require.EqualErrno(t, syscall.ENOSYS, err) require.EqualErrno(t, syscall.ENOSYS, err)
@@ -169,7 +169,7 @@ func testFutimens(t *testing.T, usePath bool) {
f := openFsFile(t, path, flag, 0) f := openFsFile(t, path, flag, 0)
errno = UtimensFile(f.File(), tc.times) errno = f.Utimens(tc.times)
require.Zero(t, f.Close()) require.Zero(t, f.Close())
require.EqualErrno(t, 0, errno) require.EqualErrno(t, 0, errno)
} }
@@ -201,42 +201,3 @@ func testFutimens(t *testing.T, usePath bool) {
} }
} }
} }
func TestUtimensFile(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin": // supported
case "freebsd": // TODO: support freebsd w/o CGO
case "windows":
if !IsGo120 {
t.Skip("windows only works after Go 1.20") // TODO: possibly 1.19 ;)
}
default: // expect ENOSYS and callers need to fall back to Utimens
t.Skip("unsupported GOOS", runtime.GOOS)
}
testFutimens(t, false)
t.Run("closed file", func(t *testing.T) {
file := path.Join(t.TempDir(), "file")
err := os.WriteFile(file, []byte{}, 0o700)
require.NoError(t, err)
fileF := openFsFile(t, file, syscall.O_RDWR, 0)
require.Zero(t, fileF.Close())
errno := UtimensFile(fileF.File(), nil)
require.EqualErrno(t, syscall.EBADF, errno)
})
t.Run("closed dir", func(t *testing.T) {
dir := path.Join(t.TempDir(), "dir")
err := os.Mkdir(dir, 0o700)
require.NoError(t, err)
dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
require.Zero(t, dirF.Close())
err = UtimensFile(dirF.File(), nil)
require.EqualErrno(t, syscall.EBADF, err)
})
}

View File

@@ -240,6 +240,15 @@ func (r *lazyDir) Chown(uid, gid int) syscall.Errno {
} }
} }
// Utimens implements the same method as documented on platform.File
func (r *lazyDir) Utimens(times *[2]syscall.Timespec) syscall.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
} else {
return f.Utimens(times)
}
}
// File implements the same method as documented on platform.File // File implements the same method as documented on platform.File
func (r *lazyDir) File() fs.File { func (r *lazyDir) File() fs.File {
if f, ok := r.file(); !ok { if f, ok := r.file(); !ok {

View File

@@ -346,9 +346,10 @@ type FS interface {
// # Parameters // # Parameters
// //
// The `times` parameter includes the access and modification timestamps to // The `times` parameter includes the access and modification timestamps to
// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be // assign. Special syscall.Timespec NSec values platform.UTIME_NOW and
// specified instead of real timestamps. A nil `times` parameter behaves the // platform.UTIME_OMIT may be specified instead of real timestamps. A nil
// same as if both were set to UTIME_NOW. // `times` parameter behaves the same as if both were set to
// platform.UTIME_NOW.
// //
// When the `symlinkFollow` parameter is true and the path is a symbolic link, // When the `symlinkFollow` parameter is true and the path is a symbolic link,
// the target of expanding that link is updated. // the target of expanding that link is updated.
@@ -363,11 +364,7 @@ type FS interface {
// //
// # Notes // # Notes
// //
// - This is like syscall.Utimes, except the path is relative to this // - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in
// filesystem. It also doesn't have flags to control expansion of // POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
// symbolic links. Neither does this support the support special values
// UTIME_NOW or UTIME_NOW.
// - This is like `utimensat` with `AT_FDCWD` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno
} }