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