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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
23
internal/gojs/testdata/writefs/main.go
vendored
23
internal/gojs/testdata/writefs/main.go
vendored
@@ -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" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user