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 }