sysfs: adds chmod (#1135)

This adds `FS.Chmod` and implements it for `GOOS=js`. This function
isn't defined in WASI snapshot01, but it is in `wasi-filesystem`, e.g.
`change-file-permissions-at`.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-02-17 20:55:03 +09:00
committed by GitHub
parent add6458c99
commit e2ebce5d23
13 changed files with 181 additions and 14 deletions

View File

@@ -21,6 +21,8 @@ func (e *Errno) Error() string {
// This order match constants from wasi_snapshot_preview1.ErrnoSuccess for
// easier maintenance.
var (
// ErrnoAcces Permission denied.
ErrnoAcces = &Errno{"EACCES"}
// ErrnoAgain Resource unavailable, or operation would block.
ErrnoAgain = &Errno{"EAGAIN"}
// ErrnoBadf Bad file descriptor.
@@ -61,6 +63,8 @@ func ToErrno(err error) *Errno {
errno := sysfs.UnwrapOSError(err)
switch errno {
case syscall.EACCES:
return ErrnoAcces
case syscall.EAGAIN:
return ErrnoAgain
case syscall.EBADF:

View File

@@ -13,6 +13,11 @@ func TestToErrno(t *testing.T) {
input error
expected *Errno
}{
{
name: "syscall.EACCES",
input: syscall.EACCES,
expected: ErrnoAcces,
},
{
name: "syscall.EAGAIN",
input: syscall.EAGAIN,

View File

@@ -84,6 +84,8 @@ var (
// The following interfaces are used until we finalize our own FD-scoped file.
type (
// chmoder is implemented by os.File in file_posix.go
chmoder interface{ Chmod(fs.FileMode) error }
// syncer is implemented by os.File in file_posix.go
syncer interface{ Sync() error }
// truncater is implemented by os.File in file_posix.go
@@ -528,8 +530,8 @@ func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}
mode := goos.ValueToUint32(args[1])
callback := args[2].(funcWrapper)
_, _ = path, mode // TODO
var err error = syscall.ENOSYS
fsc := mod.(*wasm.CallContext).Sys.FS()
err := fsc.RootFS().Chmod(path, fs.FileMode(mode))
return jsfsInvoke(ctx, mod, callback, err)
}
@@ -544,8 +546,16 @@ func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{
mode := goos.ValueToUint32(args[1])
callback := args[2].(funcWrapper)
_, _ = fd, mode // TODO
var err error = syscall.ENOSYS
// Check to see if the file descriptor is available
fsc := mod.(*wasm.CallContext).Sys.FS()
var err error
if f, ok := fsc.LookupFile(fd); !ok {
err = syscall.EBADF
} else if chmoder, ok := f.File.(chmoder); !ok {
err = syscall.EBADF // possibly a fake file
} else {
err = chmoder.Chmod(fs.FileMode(mode))
}
return jsfsInvoke(ctx, mod, callback, err)
}

View File

@@ -3,6 +3,7 @@ package writefs
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"path"
@@ -73,9 +74,31 @@ func Main() {
if err = f.Sync(); err != nil {
log.Panicln(err)
}
// Next, chmod it (tests Fchmod)
if err = f.Chmod(0o400); err != nil {
log.Panicln(err)
}
if stat, err := f.Stat(); err != nil {
log.Panicln(err)
} else if mode := stat.Mode() & fs.ModePerm; mode != 0o400 {
log.Panicln("expected mode = 0o400", mode)
}
// Finally, close it.
if err = f.Close(); err != nil {
log.Panicln(err)
}
// Revert to writeable
if err = syscall.Chmod(file1, 0o600); err != nil {
log.Panicln(err)
}
if stat, err := os.Stat(file1); err != nil {
log.Panicln(err)
} else if mode := stat.Mode() & fs.ModePerm; mode != 0o600 {
log.Panicln("expected mode = 0o600", mode)
}
// Check the file was truncated.
if bytes, err := os.ReadFile(file1); err != nil {
log.Panicln(err)
} else if string(bytes) != "wa" {

View File

@@ -24,6 +24,13 @@ func TestAdapt_MkDir(t *testing.T) {
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Chmod(t *testing.T) {
testFS := Adapt(os.DirFS(t.TempDir()))
err := testFS.Chmod("chmod", fs.ModeDir)
require.Equal(t, syscall.ENOSYS, err)
}
func TestAdapt_Rename(t *testing.T) {
tmpDir := t.TempDir()
testFS := Adapt(os.DirFS(tmpDir))
@@ -110,6 +117,8 @@ func (dir hackFS) Open(name string) (fs.File, error) {
return f, nil
} else if errors.Is(err, syscall.EISDIR) {
return os.OpenFile(path, os.O_RDONLY, 0)
} else if errors.Is(err, syscall.ENOENT) {
return os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0o444)
} else {
return nil, err
}

View File

@@ -1,7 +1,6 @@
package sysfs
import (
"errors"
"io/fs"
"os"
"syscall"
@@ -51,11 +50,17 @@ func (d *dirFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, erro
}
// Mkdir implements FS.Mkdir
func (d *dirFS) Mkdir(name string, perm fs.FileMode) error {
err := os.Mkdir(d.join(name), perm)
if errors.Is(err, syscall.ENOTDIR) {
return syscall.ENOENT
func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) {
err = os.Mkdir(d.join(name), perm)
if err = UnwrapOSError(err); err == syscall.ENOTDIR {
err = syscall.ENOENT
}
return
}
// Chmod implements FS.Chmod
func (d *dirFS) Chmod(name string, perm fs.FileMode) error {
err := os.Chmod(d.join(name), perm)
return UnwrapOSError(err)
}

View File

@@ -76,6 +76,45 @@ func TestDirFS_MkDir(t *testing.T) {
err := testFS.Mkdir(filePath, fs.ModeDir)
require.Equal(t, syscall.ENOENT, err)
})
// Remove the path so that we can test creating it with perms.
require.NoError(t, os.Remove(realPath))
// Setting mode only applies to files on windows
if runtime.GOOS != "windows" {
t.Run("dir", func(t *testing.T) {
require.NoError(t, os.Mkdir(realPath, 0o444))
defer os.RemoveAll(realPath)
testChmod(t, testFS, name)
})
}
t.Run("file", func(t *testing.T) {
require.NoError(t, os.WriteFile(realPath, nil, 0o444))
defer os.RemoveAll(realPath)
testChmod(t, testFS, name)
})
}
func testChmod(t *testing.T, testFS FS, path string) {
// Test base case, using 0o444 not 0o400 for read-back on windows.
requireMode(t, testFS, path, 0o444)
// Test adding write, using 0o666 not 0o600 for read-back on windows.
require.NoError(t, testFS.Chmod(path, 0o666))
requireMode(t, testFS, path, 0o666)
if runtime.GOOS != "windows" {
// Test clearing group and world, setting owner read+execute.
require.NoError(t, testFS.Chmod(path, 0o500))
requireMode(t, testFS, path, 0o500)
}
}
func requireMode(t *testing.T, testFS FS, path string, mode fs.FileMode) {
stat, err := StatPath(testFS, path)
require.NoError(t, err)
require.Equal(t, mode, stat.Mode()&fs.ModePerm)
}
func TestDirFS_Rename(t *testing.T) {
@@ -371,7 +410,7 @@ func TestDirFS_Utimes(t *testing.T) {
testUtimes(t, tmpDir, testFS)
}
func TestDirFS_Open(t *testing.T) {
func TestDirFS_OpenFile(t *testing.T) {
tmpDir := t.TempDir()
// Create a subdirectory, so we can test reads outside the FS root.
@@ -466,9 +505,9 @@ func TestDirFS_Truncate(t *testing.T) {
require.NoError(t, os.Remove(realPath))
})
t.Run("negative", func(t *testing.T) {
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
t.Run("negative", func(t *testing.T) {
err := testFS.Truncate(name, -1)
require.Equal(t, syscall.EINVAL, err)
})

View File

@@ -41,6 +41,14 @@ func TestReadFS_MkDir(t *testing.T) {
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Chmod(t *testing.T) {
writeable := NewDirFS(t.TempDir())
testFS := NewReadFS(writeable)
err := testFS.Chmod("chmod", fs.ModeDir)
require.Equal(t, syscall.ENOSYS, err)
}
func TestReadFS_Rename(t *testing.T) {
tmpDir := t.TempDir()
writeable := NewDirFS(tmpDir)

View File

@@ -52,6 +52,12 @@ type FS interface {
// fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce
// open flags. Instead, we encourage good behavior and test our built-in
// implementations.
//
// # Notes
//
// - flag are the same as OpenFile, for example, os.O_CREATE.
// - Implications of permissions when os.O_CREATE are described in Chmod
// notes.
OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error)
// ^^ TODO: Consider syscall.Open, though this implies defining and
// coercing flags and perms similar to what is done in os.OpenFile.
@@ -66,10 +72,31 @@ type FS interface {
// - syscall.EEXIST: `path` exists and is a directory.
// - syscall.ENOTDIR: `path` exists and is a file.
//
// # Notes
//
// - Implications of permissions are described in Chmod notes.
Mkdir(path string, perm fs.FileMode) error
// ^^ TODO: Consider syscall.Mkdir, though this implies defining and
// coercing flags and perms similar to what is done in os.Mkdir.
// Chmod is similar to os.Chmod, except the path is relative to this file
// system, and syscall.Errno are returned instead of a os.PathError.
//
// # Errors
//
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` does not exist.
//
// # Notes
//
// - Windows ignores the execute bit, and any permissions come back as
// group and world. For example, chmod of 0400 reads back as 0444, and
// 0700 0666. Also, permissions on directories aren't supported at all.
Chmod(path string, perm fs.FileMode) error
// ^^ TODO: Consider syscall.Chmod, though this implies defining and
// coercing flags and perms similar to what is done in os.Chmod.
// Rename is similar to syscall.Rename, except the path is relative to this
// file system.
//
@@ -171,7 +198,8 @@ type FS interface {
// The following errors are expected:
// - syscall.EINVAL: `path` is invalid or size is negative.
// - syscall.ENOENT: `path` doesn't exist
Truncate(name string, size int64) error
// - syscall.EACCES: `path` doesn't have write access.
Truncate(path string, size int64) error
// Utimes is similar to syscall.UtimesNano, except the path is relative to
// this file system.
@@ -221,6 +249,7 @@ type file interface {
readFile
io.Writer
io.WriterAt // for pwrite
chmoder
syncer
truncater
fder // for the number of links.
@@ -228,6 +257,8 @@ type file interface {
// The following interfaces are used until we finalize our own FD-scoped file.
type (
// chmoder is implemented by os.File in file_posix.go
chmoder interface{ Chmod(fs.FileMode) error }
// syncer is implemented by os.File in file_posix.go
syncer interface{ Sync() error }
// truncater is implemented by os.File in file_posix.go

View File

@@ -45,6 +45,28 @@ func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) {
b, err := os.ReadFile(realPath)
require.NoError(t, err)
require.Equal(t, fileContents, b)
require.NoError(t, f.Close())
// re-create as read-only, using 0444 to allow read-back on windows.
require.NoError(t, os.Remove(realPath))
f, err = testFS.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0o444)
require.NoError(t, err)
defer f.Close()
w, ok = f.(io.Writer)
require.True(t, ok)
if runtime.GOOS != "windows" {
// If the read-only flag was honored, we should not be able to write!
_, err = w.Write(fileContents)
require.Equal(t, syscall.EBADF, UnwrapOSError(err))
}
// Verify stat on the file
stat, err := f.Stat()
require.NoError(t, err)
require.Equal(t, fs.FileMode(0o444), stat.Mode()&fs.ModePerm)
}
func testOpen_Read(t *testing.T, tmpDir string, testFS FS) {

View File

@@ -29,6 +29,11 @@ func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) error {
return syscall.ENOSYS
}
// Chmod implements FS.Chmod
func (UnimplementedFS) Chmod(path string, perm fs.FileMode) error {
return syscall.ENOSYS
}
// Rename implements FS.Rename
func (UnimplementedFS) Rename(from, to string) error {
return syscall.ENOSYS

View File

@@ -268,8 +268,9 @@ var errnoToString = [...]string{
func ToErrno(err error) Errno {
errno := sysfs.UnwrapOSError(err)
// The below Errno have references in existing WASI code.
switch errno {
case syscall.EACCES:
return ErrnoAcces
case syscall.EAGAIN:
return ErrnoAgain
case syscall.EBADF:

View File

@@ -13,6 +13,11 @@ func TestToErrno(t *testing.T) {
input error
expected Errno
}{
{
name: "syscall.EACCES",
input: syscall.EACCES,
expected: ErrnoAcces,
},
{
name: "syscall.EAGAIN",
input: syscall.EAGAIN,