Repository: tetratelabs/wazero
Pull request #838 (2022-11-03 08:39:34)

Implement WASI FS fd_filestat_get
This is the proper version of #836 

Things that have not been fully implemented of `__wasi_filestat_t`:

- dev: device, id of device, don't know if we can get this from somewhere?
- ino: inode, file serial number, don't know if we can get this from somewhere?
- nlink: linkcount, number of hard links to the file, don't know if we can get this from somewhere?
- atim: timestamp, last data access timestamp, we just have the mod time so I made it use that, maybe we should leave this 0?
- ctim: timestamp, last file status change timestamp, we just have the mod time so I made it use that, maybe we should leave this 0?

codefromthecrypt: @jerbob92 what you've done looks like a good start. I would move the questions in your PR desc to comments in the impl as this being a virtual filesystem and wasi being underspecified.. there aren't perfect choices. Later, we'll have to add a 

next steps would be to add unit test similar to the existing ones, and also vet through a couple compilers that it works. The latter might be easier as so many use wasi-libc.

Maybe try tinygo rust or zig as they are already in the example project.  This will help ensure that the partial implementation is usable (often is!) What I mean is you can play around in imports/wasi_snapshot_preview1/example/testdata easier as it has Makefile targets to compile the sources. This may help in ad-hoc testing to ensure it works'ish. I don't mean to change the cat example, just use the infra there to ensure the implementation is usable.

Thanks!
jerbob92: @codefromthecrypt done, let me know what you think :)
codefromthecrypt: I plan to contribute to polishing this tomorrow or earlier
codefromthecrypt: rebased
jerbob92: > @jerbob92 looks good to me. once you ack I'll merge

Ack

Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 45e99629..da96f404 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -2,6 +2,7 @@ package wasi_snapshot_preview1
 
 import (
 	"context"
+	"encoding/binary"
 	"errors"
 	"io"
 	"io/fs"
@@ -194,15 +195,123 @@ var fdFdstatSetRights = stubFunction(
 )
 
 // fdFilestatGet is the WASI function named functionFdFilestatGet which returns
-// the attributes of an open file.
+// the stat attributes of an open file.
 //
+// # Parameters
+//
+//   - fd: file descriptor to get the filestat attributes data for
+//   - resultFilestat: offset to write the result filestat data
+//
+// Result (Errno)
+//
+// The return value is ErrnoSuccess except the following error conditions:
+//   - ErrnoBadf: `fd` is invalid
+//   - ErrnoIo: could not stat `fd` on filesystem
+//   - ErrnoFault: `resultFilestat` points to an offset out of memory
+//
+// filestat byte layout is 64-byte size, with the following fields:
+//   - dev 8 bytes: the device ID of device containing the file
+//   - ino 8 bytes: the file serial number
+//   - filetype 1 byte: the type of the file
+//   - 7 pad bytes
+//   - nlink 8 bytes: number of hard links to the file
+//   - size 8 bytes: for regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link
+//   - atim 8 bytes: ast data access timestamp
+//   - mtim 8 bytes: last data modification timestamp
+//   - ctim 8 bytes: ast file status change timestamp
+//
+// For example, with a regular file this function writes the below to api.Memory:
+//
+//	                                                             uint8 --+
+//		                         uint64le                uint64le        |        padding               uint64le                uint64le                         uint64le                               uint64le                             uint64le
+//		                 +--------------------+  +--------------------+  |  +-----------------+  +--------------------+  +-----------------------+  +----------------------------------+  +----------------------------------+  +----------------------------------+
+//		                 |                    |  |                    |  |  |                 |  |                    |  |                       |  |                                  |  |                                  |  |                                  |
+//		          []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 117, 80, 0, 0, 0, 0, 0, 0, 160, 153, 212, 128, 110, 221, 35, 23, 160, 153, 212, 128, 110, 221, 35, 23, 160, 153, 212, 128, 110, 221, 35, 23}
+//		resultFilestat   ^-- dev                 ^-- ino                 ^                       ^-- nlink               ^-- size                   ^-- atim                              ^-- mtim                              ^-- ctim
+//		                                                                 |
+//		                                                                 +-- filetype
+//
+// The following properties of filestat are not implemented:
+//   - dev: not supported by Golang FS
+//   - ino: not supported by Golang FS
+//   - nlink: not supported by Golang FS
+//   - atime: not supported by Golang FS, we use mtim for this
+//   - ctim: not supported by Golang FS, we use mtim for this
+//
+// Note: This is similar to `fstat` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
-var fdFilestatGet = stubFunction(
-	functionFdFilestatGet,
-	[]wasm.ValueType{i32, i32},
-	[]string{"fd", "result.buf"},
+// and https://linux.die.net/man/3/fstat
+var fdFilestatGet = &wasm.HostFunc{
+	ExportNames: []string{functionFdFilestatGet},
+	Name:        functionFdFilestatGet,
+	ParamTypes:  []api.ValueType{i32, i32},
+	ParamNames:  []string{"fd", "result.buf"},
+	ResultTypes: []api.ValueType{i32},
+	Code: &wasm.Code{
+		IsHostFunction: true,
+		GoFunc:         api.GoModuleFunc(fdFilestatGetFn),
+	},
+}
+
+type wasiFiletype uint8
+
+const (
+	wasiFiletypeUnknown wasiFiletype = iota
+	wasiFiletypeBlockDevice
+	wasiFiletypeCharacterDevice
+	wasiFiletypeDirectory
+	wasiFiletypeRegularFile
+	wasiFiletypeSocketDgram
+	wasiFiletypeSocketStream
+	wasiFiletypeSymbolicLink
 )
 
+func fdFilestatGetFn(ctx context.Context, mod api.Module, params []uint64) []uint64 {
+	fd := uint32(params[0])
+	resultBuf := uint32(params[1])
+
+	sysCtx := mod.(*wasm.CallContext).Sys
+	file, ok := sysCtx.FS(ctx).OpenedFile(ctx, fd)
+	if !ok {
+		return errnoBadf
+	}
+
+	fileStat, err := file.File.Stat()
+	if err != nil {
+		return errnoIo
+	}
+
+	fileMode := fileStat.Mode()
+
+	wasiFileMode := wasiFiletypeUnknown
+	if fileMode&fs.ModeDevice != 0 {
+		wasiFileMode = wasiFiletypeBlockDevice
+	} else if fileMode&fs.ModeCharDevice != 0 {
+		wasiFileMode = wasiFiletypeCharacterDevice
+	} else if fileMode&fs.ModeDir != 0 {
+		wasiFileMode = wasiFiletypeDirectory
+	} else if fileMode&fs.ModeType == 0 {
+		wasiFileMode = wasiFiletypeRegularFile
+	} else if fileMode&fs.ModeSymlink != 0 {
+		wasiFileMode = wasiFiletypeSymbolicLink
+	}
+
+	buf, ok := mod.Memory().Read(ctx, resultBuf, 64)
+	if !ok {
+		return errnoFault
+	}
+
+	buf[16] = uint8(wasiFileMode)
+	size := uint64(fileStat.Size())
+	binary.LittleEndian.PutUint64(buf[32:], size)
+	mtim := uint64(fileStat.ModTime().UnixNano())
+	binary.LittleEndian.PutUint64(buf[40:], mtim)
+	binary.LittleEndian.PutUint64(buf[48:], mtim)
+	binary.LittleEndian.PutUint64(buf[56:], mtim)
+
+	return errnoSuccess
+}
+
 // fdFilestatSetSize is the WASI function named functionFdFilestatSetSize which
 // adjusts the size of an open file.
 //


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index ffdcbdc3..c50b7df0 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -9,6 +9,7 @@ import (
 	"path"
 	"testing"
 	"testing/fstest"
+	"time"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
@@ -200,15 +201,111 @@ func Test_fdFdstatSetRights(t *testing.T) {
 `, log)
 }
 
-// Test_fdFilestatGet only tests it is stubbed for GrainLang per #271
 func Test_fdFilestatGet(t *testing.T) {
-	log := requireErrnoNosys(t, functionFdFilestatGet, 0, 0)
-	require.Equal(t, `
---> proxy.fd_filestat_get(fd=0,result.buf=0)
-	--> wasi_snapshot_preview1.fd_filestat_get(fd=0,result.buf=0)
-	<-- ENOSYS
-<-- (52)
-`, log)
+	file, dir := "a", "b"
+	testFS := fstest.MapFS{file: {Data: make([]byte, 123456), ModTime: time.Unix(1667482413, 0)}, dir: {Mode: fs.ModeDir, ModTime: time.Unix(1667482413, 0)}}
+
+	mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(testFS))
+	defer r.Close(testCtx)
+	memorySize := mod.Memory().Size(testCtx)
+
+	// open both paths without using WASI
+	fsc := mod.(*wasm.CallContext).Sys.FS(testCtx)
+
+	fileFd, err := fsc.OpenFile(testCtx, file)
+	require.NoError(t, err)
+
+	dirFd, err := fsc.OpenFile(testCtx, dir)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name               string
+		fd, resultFilestat uint32
+		expectedMemory     []byte
+		expectedErrno      Errno
+		expectedLog        string
+	}{
+		{
+			name: "file",
+			fd:   fileFd,
+			expectedMemory: []byte{
+				'?', '?', '?', '?', '?', '?', '?', '?', // dev
+				'?', '?', '?', '?', '?', '?', '?', '?', // ino
+				4, '?', '?', '?', '?', '?', '?', '?', // filetype + padding
+				'?', '?', '?', '?', '?', '?', '?', '?', // nlink
+				0x40, 0xe2, 0x1, 0, 0, 0, 0, 0, // size
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // atim
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // mtim
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
+			},
+			expectedLog: `
+--> proxy.fd_filestat_get(fd=4,result.buf=0)
+	==> wasi_snapshot_preview1.fd_filestat_get(fd=4,result.buf=0)
+	<== ESUCCESS
+<-- (0)
+`,
+		},
+		{
+			name: "dir",
+			fd:   dirFd,
+			expectedMemory: []byte{
+				'?', '?', '?', '?', '?', '?', '?', '?', // dev
+				'?', '?', '?', '?', '?', '?', '?', '?', // ino
+				3, '?', '?', '?', '?', '?', '?', '?', // filetype + padding
+				'?', '?', '?', '?', '?', '?', '?', '?', // nlink
+				0, 0, 0, 0, 0, 0, 0, 0, // size
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // atim
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // mtim
+				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
+			},
+			expectedLog: `
+--> proxy.fd_filestat_get(fd=5,result.buf=0)
+	==> wasi_snapshot_preview1.fd_filestat_get(fd=5,result.buf=0)
+	<== ESUCCESS
+<-- (0)
+`,
+		},
+		{
+			name:          "bad FD",
+			fd:            math.MaxUint32,
+			expectedErrno: ErrnoBadf,
+			expectedLog: `
+--> proxy.fd_filestat_get(fd=4294967295,result.buf=0)
+	==> wasi_snapshot_preview1.fd_filestat_get(fd=4294967295,result.buf=0)
+	<== EBADF
+<-- (8)
+`,
+		},
+		{
+			name:           "resultFilestat exceeds the maximum valid address by 1",
+			fd:             dirFd,
+			resultFilestat: memorySize - 64 + 1,
+			expectedErrno:  ErrnoFault,
+			expectedLog: `
+--> proxy.fd_filestat_get(fd=5,result.buf=65473)
+	==> wasi_snapshot_preview1.fd_filestat_get(fd=5,result.buf=65473)
+	<== EFAULT
+<-- (21)
+`,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			defer log.Reset()
+
+			maskMemory(t, testCtx, mod, len(tc.expectedMemory))
+
+			requireErrno(t, tc.expectedErrno, mod, functionFdFilestatGet, uint64(tc.fd), uint64(tc.resultFilestat))
+			require.Equal(t, tc.expectedLog, "\n"+log.String())
+
+			actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory)))
+			require.True(t, ok)
+			require.Equal(t, tc.expectedMemory, actual)
+		})
+	}
 }
 
 // Test_fdFilestatSetSize only tests it is stubbed for GrainLang per #271