Skip to content

Commit

Permalink
sysfs: adds chmod (#1135)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
codefromthecrypt committed Feb 17, 2023
1 parent add6458 commit e2ebce5
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 14 deletions.
4 changes: 4 additions & 0 deletions internal/gojs/errno.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions internal/gojs/errno_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 14 additions & 4 deletions internal/gojs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
23 changes: 23 additions & 0 deletions internal/gojs/testdata/writefs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package writefs
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"path"
Expand Down Expand Up @@ -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" {
Expand Down
9 changes: 9 additions & 0 deletions internal/sysfs/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 10 additions & 5 deletions internal/sysfs/dirfs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sysfs

import (
"errors"
"io/fs"
"os"
"syscall"
Expand Down Expand Up @@ -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)
}

Expand Down
45 changes: 42 additions & 3 deletions internal/sysfs/dirfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
})
Expand Down
8 changes: 8 additions & 0 deletions internal/sysfs/readfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 32 additions & 1 deletion internal/sysfs/sysfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
//
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -221,13 +249,16 @@ type file interface {
readFile
io.Writer
io.WriterAt // for pwrite
chmoder
syncer
truncater
fder // for the number of links.
}

// 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
Expand Down
22 changes: 22 additions & 0 deletions internal/sysfs/sysfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions internal/sysfs/unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion internal/wasi_snapshot_preview1/errno.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions internal/wasi_snapshot_preview1/errno_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit e2ebce5

Please sign in to comment.