Repository: tetratelabs/wazero
Pull request #1100 (2023-02-06 01:03:11)

wasi: implements filestatset
Part of #1036. With this, only two cases are left to pass 
> Test suites: 1 failed, 0 total
Tests:       2 failed, 8 passed, 1 skipped, 1 total


Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>


Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index b8168fd0..d39be295 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -24,12 +24,45 @@ import (
 // advisory information on a file descriptor.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_advisefd-fd-offset-filesize-len-filesize-advice-advice---errno
-var fdAdvise = stubFunction(
-	FdAdviseName,
+var fdAdvise = newHostFunc(
+	FdAdviseName, fdAdviseFn,
 	[]wasm.ValueType{i32, i64, i64, i32},
 	"fd", "offset", "len", "advice",
 )
 
+func fdAdviseFn(_ context.Context, mod api.Module, params []uint64) Errno {
+	fd := uint32(params[0])
+	_ = params[1]
+	_ = params[2]
+	advice := byte(params[3])
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+
+	_, ok := fsc.LookupFile(fd)
+	if !ok {
+		return ErrnoBadf
+	}
+
+	switch advice {
+	case FdAdviceNormal,
+		FdAdviceSequential,
+		FdAdviceRandom,
+		FdAdviceWillNeed,
+		FdAdviceDontNeed,
+		FdAdviceNoReuse:
+	default:
+		return ErrnoInval
+	}
+
+	// FdAdvice corresponds to posix_fadvise, but it can only be supported on linux.
+	// However, the purpose of the call is just to do best-effort optimization on OS kernels,
+	// so just making this noop rather than returning NoSup error makes sense and doesn't affect
+	// the semantics of Wasm applications.
+	// TODO: invoke posix_fadvise on linux, and partially on darwin.
+	// - https://gitlab.com/cznic/fileutil/-/blob/v1.1.2/fileutil_linux.go#L87-95
+	// - https://github.com/bytecodealliance/system-interface/blob/62b97f9776b86235f318c3a6e308395a1187439b/src/fs/file_io_ext.rs#L430-L442
+	return ErrnoSuccess
+}
+
 // fdAllocate is the WASI function named FdAllocateName which forces the
 // allocation of space in a file.
 //
@@ -160,9 +193,27 @@ func writeFdstat(buf []byte, filetype uint8, fdflags uint16) {
 
 // fdFdstatSetFlags is the WASI function named FdFdstatSetFlagsName which
 // adjusts the flags associated with a file descriptor.
-//
-// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_fdstat_set_flagsfd-fd-flags-fdflags---errnoand is stubbed for GrainLang per #271
-var fdFdstatSetFlags = stubFunction(FdFdstatSetFlagsName, []wasm.ValueType{i32, i32}, "fd", "flags")
+var fdFdstatSetFlags = newHostFunc(FdFdstatSetFlagsName, fdFdstatSetFlagsFn, []wasm.ValueType{i32, i32}, "fd", "flags")
+
+func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) Errno {
+	fd, wasiFlag := uint32(params[0]), uint16(params[1])
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+
+	// We can only support APPEND flag.
+	if FD_DSYNC&wasiFlag != 0 || FD_NONBLOCK&wasiFlag != 0 || FD_RSYNC&wasiFlag != 0 || FD_SYNC&wasiFlag != 0 {
+		return ErrnoInval
+	}
+
+	var flag int
+	if FD_APPEND&wasiFlag != 0 {
+		flag = syscall.O_APPEND
+	}
+
+	if err := fsc.ChangeOpenFlag(fd, flag); err != nil {
+		return ToErrno(err)
+	}
+	return ErrnoSuccess
+}
 
 // fdFdstatSetRights will not be implemented as rights were removed from WASI.
 //
diff --git a/internal/sys/fs.go b/internal/sys/fs.go
index ac00cf08..725edeb1 100644
--- a/internal/sys/fs.go
+++ b/internal/sys/fs.go
@@ -323,20 +323,60 @@ func (c *FSContext) ReOpenDir(fd uint32) (*FileEntry, error) {
 		return nil, syscall.EISDIR
 	}
 
+	if err := c.reopen(f); err != nil {
+		return f, err
+	}
+
+	f.ReadDir.CountRead, f.ReadDir.Entries = 0, nil
+	return f, nil
+}
+
+func (c *FSContext) reopen(f *FileEntry) error {
 	if err := f.File.Close(); err != nil {
-		return nil, err
+		return err
 	}
 
 	// Re-opens with  the same parameters as before.
 	opened, err := f.FS.OpenFile(f.openPath, f.openFlag, f.openPerm)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	// Reset the state.
 	f.File = opened
-	f.ReadDir.CountRead, f.ReadDir.Entries = 0, nil
-	return f, nil
+	return nil
+}
+
+// ChangeOpenFlag changes the open flag of the given opened file pointed by `fd`.
+// Currently, this only supports the change of syscall.O_APPEND flag.
+func (c *FSContext) ChangeOpenFlag(fd uint32, flag int) error {
+	f, ok := c.LookupFile(fd)
+	if !ok {
+		return syscall.EBADF
+	} else if f.IsDir() {
+		return syscall.EISDIR
+	}
+
+	if flag&syscall.O_APPEND != 0 {
+		f.openFlag |= syscall.O_APPEND
+	} else {
+		f.openFlag &= ^syscall.O_APPEND
+	}
+
+	// Changing the flag while opening is not really supported well in Go. Even when using
+	// syscall package, the feasibility of doing so really depends on the platform. For examples:
+	//
+	// 	* This appendMode (bool) cannot be changed later.
+	// 	https://github.com/golang/go/blob/2da8a55584aa65ce1b67431bb8ecebf66229d462/src/os/file_unix.go#L60
+	// 	* On Windows, re-opening it is the only way to emulate the behavior.
+	// 	https://github.com/bytecodealliance/system-interface/blob/62b97f9776b86235f318c3a6e308395a1187439b/src/fs/fd_flags.rs#L196
+	//
+	// Therefore, here we re-open the file while keeping the file descriptor.
+	// TODO: this might be improved once we have our own File type.
+	if err := c.reopen(f); err != nil {
+		return err
+	}
+	return nil
 }
 
 // LookupFile returns a file if it is in the table.
diff --git a/internal/wasi_snapshot_preview1/fs.go b/internal/wasi_snapshot_preview1/fs.go
index b5990ca8..ede6d38a 100644
--- a/internal/wasi_snapshot_preview1/fs.go
+++ b/internal/wasi_snapshot_preview1/fs.go
@@ -141,3 +141,13 @@ const (
 	FileStatAdjustFlagsMtim
 	FileStatAdjustFlagsMtimNow
 )
+
+// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-advice-enumu8
+const (
+	FdAdviceNormal byte = iota
+	FdAdviceSequential
+	FdAdviceRandom
+	FdAdviceWillNeed
+	FdAdviceDontNeed
+	FdAdviceNoReuse
+)


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index e1147d53..91c6a306 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -24,13 +24,17 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-// Test_fdAdvise only tests it is stubbed for GrainLang per #271
 func Test_fdAdvise(t *testing.T) {
-	log := requireErrnoNosys(t, FdAdviseName, 0, 0, 0, 0)
-	require.Equal(t, `
---> wasi_snapshot_preview1.fd_advise(fd=0,offset=0,len=0,advice=0)
-<-- errno=ENOSYS
-`, log)
+	mod, r, _ := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.FS))
+	defer r.Close(testCtx)
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNormal))
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceSequential))
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceRandom))
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceWillNeed))
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceDontNeed))
+	requireErrno(t, ErrnoSuccess, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse))
+	requireErrno(t, ErrnoInval, mod, FdAdviseName, uint64(3), 0, 0, uint64(FdAdviceNoReuse+1))
+	requireErrno(t, ErrnoBadf, mod, FdAdviseName, uint64(1111111), 0, 0, uint64(FdAdviceNoReuse+1))
 }
 
 // Test_fdAllocate only tests it is stubbed for GrainLang per #271
@@ -246,13 +250,94 @@ func Test_fdFdstatGet(t *testing.T) {
 	}
 }
 
-// Test_fdFdstatSetFlags only tests it is stubbed for GrainLang per #271
 func Test_fdFdstatSetFlags(t *testing.T) {
-	log := requireErrnoNosys(t, FdFdstatSetFlagsName, 0, 0)
+	tmpDir := t.TempDir() // open before loop to ensure no locking problems.
+	const fileName = "file.txt"
+
+	// Create the target file.
+	realPath := path.Join(tmpDir, fileName)
+	require.NoError(t, os.WriteFile(realPath, []byte("0123456789"), 0o600))
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(wazero.NewFSConfig().
+		WithDirMount(tmpDir, "/")))
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	preopen := fsc.RootFS()
+	defer r.Close(testCtx)
+
+	// First, open it with O_APPEND.
+	fd, err := fsc.OpenFile(preopen, fileName, os.O_RDWR|os.O_APPEND, 0)
+	require.NoError(t, err)
+
+	writeWazero := func() {
+		iovs := uint32(1) // arbitrary offset
+		initialMemory := []byte{
+			'?',         // `iovs` is after this
+			18, 0, 0, 0, // = iovs[0].offset
+			4, 0, 0, 0, // = iovs[0].length
+			23, 0, 0, 0, // = iovs[1].offset
+			2, 0, 0, 0, // = iovs[1].length
+			'?',                // iovs[0].offset is after this
+			'w', 'a', 'z', 'e', // iovs[0].length bytes
+			'?',      // iovs[1].offset is after this
+			'r', 'o', // iovs[1].length bytes
+			'?',
+		}
+		iovsCount := uint32(2)       // The count of iovs
+		resultNwritten := uint32(26) // arbitrary offset
+
+		ok := mod.Memory().Write(0, initialMemory)
+		require.True(t, ok)
+
+		requireErrno(t, ErrnoSuccess, mod, FdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten))
+		require.Equal(t, `
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2)
+<== (nwritten=6,errno=ESUCCESS)
+`, "\n"+log.String())
+		log.Reset()
+	}
+
+	requireFileContent := func(exp string) {
+		buf, err := os.ReadFile(path.Join(tmpDir, fileName))
+		require.NoError(t, err)
+		require.Equal(t, exp, string(buf))
+	}
+
+	// with O_APPEND flag, the data is appended to buffer.
+	writeWazero()
+	requireFileContent("0123456789" + "wazero")
+
+	// Let's remove O_APPEND.
+	requireErrno(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(0))
 	require.Equal(t, `
---> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=0,flags=0)
-<-- errno=ENOSYS
-`, log)
+==> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=4,flags=0)
+<== errno=ESUCCESS
+`, "\n"+log.String())
+	log.Reset()
+
+	// Without O_APPEND flag, the data is written at the beginning.
+	writeWazero()
+	requireFileContent("wazero6789" + "wazero")
+
+	// Restore the O_APPEND flag.
+	requireErrno(t, ErrnoSuccess, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_APPEND))
+	require.Equal(t, `
+==> wasi_snapshot_preview1.fd_fdstat_set_flags(fd=4,flags=1)
+<== errno=ESUCCESS
+`, "\n"+log.String())
+	log.Reset()
+
+	// with O_APPEND flag, the data is appended to buffer.
+	writeWazero()
+	requireFileContent("wazero6789" + "wazero" + "wazero")
+
+	t.Run("errors", func(t *testing.T) {
+		requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_DSYNC))
+		requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_NONBLOCK))
+		requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_RSYNC))
+		requireErrno(t, ErrnoInval, mod, FdFdstatSetFlagsName, uint64(fd), uint64(FD_SYNC))
+		requireErrno(t, ErrnoBadf, mod, FdFdstatSetFlagsName, uint64(12345), uint64(FD_APPEND))
+		requireErrno(t, ErrnoIsdir, mod, FdFdstatSetFlagsName, uint64(3) /* preopen */, uint64(FD_APPEND))
+	})
 }
 
 // Test_fdFdstatSetRights only tests it is stubbed for GrainLang per #271
@@ -2096,7 +2181,6 @@ func Test_fdReaddir_Errors(t *testing.T) {
 	}
 }
 
-// Test_fdRenumber only tests it is stubbed for GrainLang per #271
 func Test_fdRenumber(t *testing.T) {
 	const preopenFd, fileFd, dirFd = 3, 4, 5
 
diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go
index 4a780c0c..4db9d290 100644
--- a/internal/sys/fs_test.go
+++ b/internal/sys/fs_test.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"io/fs"
 	"os"
+	"path"
 	"syscall"
 	"testing"
 	"testing/fstest"
@@ -312,3 +313,30 @@ func TestFSContext_Renumber(t *testing.T) {
 		require.Equal(t, syscall.ENOTSUP, c.Renumber(3, 3))
 	})
 }
+
+func TestFSContext_ChangeOpenFlag(t *testing.T) {
+	tmpDir := t.TempDir()
+	dirFs := sysfs.NewDirFS(tmpDir)
+
+	const fileName = "dir"
+	require.NoError(t, os.WriteFile(path.Join(tmpDir, fileName), []byte("0123456789"), 0o600))
+
+	c, err := NewFSContext(nil, nil, nil, dirFs)
+	require.NoError(t, err)
+	defer func() {
+		require.NoError(t, c.Close(context.Background()))
+	}()
+
+	// Without APPEND.
+	fd, err := c.OpenFile(dirFs, fileName, os.O_RDWR, 0o600)
+	require.NoError(t, err)
+	require.Equal(t, c.openedFiles.files[fd].openFlag&syscall.O_APPEND, 0)
+
+	// Set the APPEND flag.
+	require.NoError(t, c.ChangeOpenFlag(fd, syscall.O_APPEND))
+	require.Equal(t, c.openedFiles.files[fd].openFlag&syscall.O_APPEND, syscall.O_APPEND)
+
+	// Remove the APPEND flag.
+	require.NoError(t, c.ChangeOpenFlag(fd, 0))
+	require.Equal(t, c.openedFiles.files[fd].openFlag&syscall.O_APPEND, 0)
+}