wa-lang.org/wazero@v1.0.2/internal/integration_test/fs/fs_test.go (about)

     1  package fs
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"testing"
    10  	"testing/fstest"
    11  	"testing/iotest"
    12  
    13  	"wa-lang.org/wazero"
    14  	"wa-lang.org/wazero/api"
    15  	"wa-lang.org/wazero/imports/wasi_snapshot_preview1"
    16  	"wa-lang.org/wazero/internal/testing/require"
    17  )
    18  
    19  var testCtx = context.Background()
    20  
    21  //go:embed testdata/animals.txt
    22  var animals []byte
    23  
    24  // fsWasm was generated by the following:
    25  //
    26  //	cd testdata; wat2wasm --debug-names fs.wat
    27  //
    28  //go:embed testdata/fs.wasm
    29  var fsWasm []byte
    30  
    31  // wasiFs is an implementation of fs.Fs calling into wasiWASI. Not thread-safe because we use
    32  // fixed Memory offsets for transferring data with wasm.
    33  type wasiFs struct {
    34  	t *testing.T
    35  
    36  	wasm   wazero.Runtime
    37  	memory api.Memory
    38  
    39  	workdirFd uint32
    40  
    41  	pathOpen api.Function
    42  	fdClose  api.Function
    43  	fdRead   api.Function
    44  	fdSeek   api.Function
    45  }
    46  
    47  func (fs *wasiFs) Open(name string) (fs.File, error) {
    48  	pathBytes := []byte(name)
    49  	// Pick anywhere in memory to write the path to.
    50  	pathPtr := uint32(0)
    51  	ok := fs.memory.Write(testCtx, pathPtr, pathBytes)
    52  	require.True(fs.t, ok)
    53  	resultOpenedFd := pathPtr + uint32(len(pathBytes))
    54  
    55  	fd := fs.workdirFd
    56  	dirflags := uint32(0) // arbitrary dirflags
    57  	pathLen := len(pathBytes)
    58  	oflags := uint32(0) // arbitrary oflags
    59  	// rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
    60  	fsRightsBase, fsRightsInheriting := uint64(1), uint64(2)
    61  	fdflags := uint32(0) // arbitrary fdflags
    62  	res, err := fs.pathOpen.Call(
    63  		testCtx,
    64  		uint64(fd), uint64(dirflags), uint64(pathPtr), uint64(pathLen), uint64(oflags),
    65  		fsRightsBase, fsRightsInheriting, uint64(fdflags), uint64(resultOpenedFd))
    66  	require.NoError(fs.t, err)
    67  	require.Equal(fs.t, uint64(wasi_snapshot_preview1.ErrnoSuccess), res[0])
    68  
    69  	resFd, ok := fs.memory.ReadUint32Le(testCtx, resultOpenedFd)
    70  	require.True(fs.t, ok)
    71  
    72  	return &wasiFile{fd: resFd, fs: fs}, nil
    73  }
    74  
    75  // wasiFile implements io.Reader and io.Seeker using wasiWASI functions. It does not
    76  // implement io.ReaderAt because there is no wasiWASI function for directly reading
    77  // from an offset.
    78  type wasiFile struct {
    79  	fd uint32
    80  	fs *wasiFs
    81  }
    82  
    83  func (f *wasiFile) Stat() (fs.FileInfo, error) {
    84  	// We currently don't implement wasi's fd_stat but also don't use this method from this test.
    85  	panic("unused")
    86  }
    87  
    88  func (f *wasiFile) Read(bytes []byte) (int, error) {
    89  	// Pick anywhere in memory for wasm to write resultSize too. We do this first since it's fixed length
    90  	// while iovs is variable.
    91  	resultSizeOff := uint32(0)
    92  	// next place iovs
    93  	iovsOff := uint32(4)
    94  	// We do not directly write to hardware, there is no need for more than one iovec
    95  	iovsCount := uint32(1)
    96  	// iov starts at iovsOff + 8 because we first write four bytes for the offset itself, and
    97  	// four bytes for the length of the iov.
    98  	iovOff := iovsOff + uint32(8)
    99  	ok := f.fs.memory.WriteUint32Le(testCtx, iovsOff, iovOff)
   100  	require.True(f.fs.t, ok)
   101  	// next write the length.
   102  	ok = f.fs.memory.WriteUint32Le(testCtx, iovsOff+uint32(4), uint32(len(bytes)))
   103  	require.True(f.fs.t, ok)
   104  
   105  	res, err := f.fs.fdRead.Call(testCtx, uint64(f.fd), uint64(iovsOff), uint64(iovsCount), uint64(resultSizeOff))
   106  	require.NoError(f.fs.t, err)
   107  
   108  	require.NotEqual(f.fs.t, uint64(wasi_snapshot_preview1.ErrnoFault), res[0])
   109  
   110  	numRead, ok := f.fs.memory.ReadUint32Le(testCtx, resultSizeOff)
   111  	require.True(f.fs.t, ok)
   112  
   113  	if numRead == 0 {
   114  		if len(bytes) == 0 {
   115  			return 0, nil
   116  		}
   117  		if wasi_snapshot_preview1.Errno(res[0]) == wasi_snapshot_preview1.ErrnoSuccess {
   118  			return 0, io.EOF
   119  		} else {
   120  			return 0, fmt.Errorf("could not read from file")
   121  		}
   122  	}
   123  
   124  	buf, ok := f.fs.memory.Read(testCtx, iovOff, numRead)
   125  	require.True(f.fs.t, ok)
   126  	copy(bytes, buf)
   127  	return int(numRead), nil
   128  }
   129  
   130  func (f *wasiFile) Close() error {
   131  	res, err := f.fs.fdClose.Call(testCtx, uint64(f.fd))
   132  	require.NoError(f.fs.t, err)
   133  	require.NotEqual(f.fs.t, uint64(wasi_snapshot_preview1.ErrnoFault), res[0])
   134  	return nil
   135  }
   136  
   137  func (f *wasiFile) Seek(offset int64, whence int) (int64, error) {
   138  	// Pick anywhere in memory for wasm to write the result newOffset to
   139  	resultNewoffsetOff := uint32(0)
   140  
   141  	res, err := f.fs.fdSeek.Call(testCtx, uint64(f.fd), uint64(offset), uint64(whence), uint64(resultNewoffsetOff))
   142  	require.NoError(f.fs.t, err)
   143  	require.NotEqual(f.fs.t, uint64(wasi_snapshot_preview1.ErrnoFault), res[0])
   144  
   145  	newOffset, ok := f.fs.memory.ReadUint32Le(testCtx, resultNewoffsetOff)
   146  	require.True(f.fs.t, ok)
   147  
   148  	return int64(newOffset), nil
   149  }
   150  
   151  func TestReader(t *testing.T) {
   152  	r := wazero.NewRuntime(testCtx)
   153  	defer r.Close(testCtx)
   154  
   155  	wasi_snapshot_preview1.MustInstantiate(testCtx, r)
   156  
   157  	realFs := fstest.MapFS{"animals.txt": &fstest.MapFile{Data: animals}}
   158  	sys := wazero.NewModuleConfig().WithFS(realFs)
   159  
   160  	// Create a module that just delegates to wasi functions.
   161  	compiled, err := r.CompileModule(testCtx, fsWasm)
   162  	require.NoError(t, err)
   163  
   164  	mod, err := r.InstantiateModule(testCtx, compiled, sys)
   165  	require.NoError(t, err)
   166  
   167  	pathOpen := mod.ExportedFunction("path_open")
   168  	fdClose := mod.ExportedFunction("fd_close")
   169  	fdRead := mod.ExportedFunction("fd_read")
   170  	fdSeek := mod.ExportedFunction("fd_seek")
   171  
   172  	wasiFs := &wasiFs{
   173  		t:         t,
   174  		wasm:      r,
   175  		memory:    mod.Memory(),
   176  		workdirFd: uint32(3),
   177  		pathOpen:  pathOpen,
   178  		fdClose:   fdClose,
   179  		fdRead:    fdRead,
   180  		fdSeek:    fdSeek,
   181  	}
   182  
   183  	f, err := wasiFs.Open("animals.txt")
   184  	require.NoError(t, err)
   185  	defer f.Close()
   186  
   187  	err = iotest.TestReader(f, animals)
   188  	require.NoError(t, err)
   189  }