Skip to content

Commit

Permalink
cli: add mount option to expose paths to wasm app (#816)
Browse files Browse the repository at this point in the history
Signed-off-by: Anuraag Agrawal <[email protected]>
  • Loading branch information
anuraaga authored Sep 29, 2022
1 parent 9a623c4 commit 6cf113b
Show file tree
Hide file tree
Showing 13 changed files with 620 additions and 18 deletions.
37 changes: 37 additions & 0 deletions cmd/wazero/compositefs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"io/fs"
"strings"
)

type compositeFS struct {
paths map[string]fs.FS
}

func (c *compositeFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
for path, f := range c.paths {
if !strings.HasPrefix(name, path) {
continue
}
rest := name[len(path):]
if len(rest) == 0 {
// Special case reading directory
rest = "."
} else {
// fs.Open requires a relative path
if rest[0] == '/' {
rest = rest[1:]
}
}
file, err := f.Open(rest)
if err == nil {
return file, err
}
}

return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
234 changes: 234 additions & 0 deletions cmd/wazero/compositefs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package main

import (
"bytes"
"embed"
"fmt"
"io/fs"
"testing"
"testing/fstest"

"github.com/tetratelabs/wazero/internal/testing/require"
)

//go:embed testdata/fs
var testFS embed.FS

func TestCompositeFS(t *testing.T) {
tests := []struct {
name string
fs *compositeFS
path string
content string
}{
{
name: "empty",
fs: &compositeFS{},
path: "bear.txt",
},
{
name: "single mount to root",
fs: &compositeFS{
paths: map[string]fs.FS{
"": testFSSub(""),
},
},
path: "bear.txt",
content: "pooh",
},
{
name: "single mount to root",
fs: &compositeFS{
paths: map[string]fs.FS{
"": testFSSub(""),
},
},
path: "fish/clownfish.txt",
content: "nemo",
},
{
name: "single mount to root",
fs: &compositeFS{
paths: map[string]fs.FS{
"": testFSSub(""),
},
},
path: "mammals/primates/ape.txt",
content: "king kong",
},
{
name: "single mount to path",
fs: &compositeFS{
paths: map[string]fs.FS{
"mammals": testFSSub(""),
},
},
path: "mammals/bear.txt",
content: "pooh",
},
{
name: "single mount to path",
fs: &compositeFS{
paths: map[string]fs.FS{
"mammals": testFSSub(""),
},
},
path: "mammals/whale.txt",
},
{
name: "single mount to path",
fs: &compositeFS{
paths: map[string]fs.FS{
"mammals": testFSSub(""),
},
},
path: "bear.txt",
},
{
name: "non-overlapping mounts",
fs: &compositeFS{
paths: map[string]fs.FS{
"fish": testFSSub("fish"),
"mammals": testFSSub("mammals"),
},
},
path: "fish/clownfish.txt",
content: "nemo",
},
{
name: "non-overlapping mounts",
fs: &compositeFS{
paths: map[string]fs.FS{
"fish": testFSSub("fish"),
"mammals": testFSSub("mammals"),
},
},
path: "mammals/whale.txt",
content: "moby dick",
},
{
name: "non-overlapping mounts",
fs: &compositeFS{
paths: map[string]fs.FS{
"fish": testFSSub("fish"),
"mammals": testFSSub("mammals"),
},
},
path: "mammals/primates/ape.txt",
content: "king kong",
},
{
name: "non-overlapping mounts",
fs: &compositeFS{
paths: map[string]fs.FS{
"fish": testFSSub("fish"),
"mammals": testFSSub("mammals"),
},
},
path: "bear.txt",
},
{
name: "overlapping mounts, deep first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals/fish": testFSSub("fish"),
"animals": testFSSub("mammals"),
},
},
path: "animals/fish/clownfish.txt",
content: "nemo",
},
{
name: "overlapping mounts, deep first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals/fish": testFSSub("fish"),
"animals": testFSSub("mammals"),
},
},
path: "animals/whale.txt",
content: "moby dick",
},
{
name: "overlapping mounts, deep first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals/fish": testFSSub("fish"),
"animals": testFSSub("mammals"),
},
},
path: "animals/bear.txt",
},
{
name: "overlapping mounts, shallow first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals": testFSSub("mammals"),
"animals/fish": testFSSub("fish"),
},
},
path: "animals/fish/clownfish.txt",
content: "nemo",
},
{
name: "overlapping mounts, shallow first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals": testFSSub("mammals"),
"animals/fish": testFSSub("fish"),
},
},
path: "animals/whale.txt",
content: "moby dick",
},
{
name: "overlapping mounts, shallow first",
fs: &compositeFS{
paths: map[string]fs.FS{
"animals": testFSSub("mammals"),
"animals/fish": testFSSub("fish"),
},
},
path: "animals/bear.txt",
},
}

for _, tc := range tests {
tt := tc
t.Run(fmt.Sprintf("%s - %s", tt.name, tt.path), func(t *testing.T) {
content, err := fs.ReadFile(tt.fs, tt.path)
if tt.content != "" {
require.NoError(t, err)
require.Equal(t, tt.content, string(bytes.TrimSpace(content)))
} else {
require.ErrorIs(t, err, fs.ErrNotExist)
}
})
}
}

func TestFSTest(t *testing.T) {
fs := &compositeFS{
paths: map[string]fs.FS{
// TestFS requires non-rooted paths to be read from current directory so we mount
// both with . and no prefix.
".": testFSSub(""),
"": testFSSub(""),
},
}

require.NoError(t, fstest.TestFS(fs, "bear.txt"))
}

func testFSSub(path string) fs.FS {
// Can't use filepath.Join because we need unix behavior even on Windows.
p := "testdata/fs"
if len(path) > 0 {
p = fmt.Sprintf("%s/%s", p, path)
}
f, err := fs.Sub(testFS, p)
if err != nil {
panic(err)
}
return f
}
1 change: 1 addition & 0 deletions cmd/wazero/testdata/fs/bear.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pooh
1 change: 1 addition & 0 deletions cmd/wazero/testdata/fs/fish/clownfish.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nemo
1 change: 1 addition & 0 deletions cmd/wazero/testdata/fs/mammals/primates/ape.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
king kong
1 change: 1 addition & 0 deletions cmd/wazero/testdata/fs/mammals/whale.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
moby dick
Binary file added cmd/wazero/testdata/wasi_env.wasm
Binary file not shown.
61 changes: 61 additions & 0 deletions cmd/wazero/testdata/wasi_env.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
;; $wasi_env is a WASI command which copies null-terminated environ to stdout.
(module $wasi_env
;; environ_get reads environment variables.
;;
;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_getenviron-pointerpointeru8-environ_buf-pointeru8---errno
(import "wasi_snapshot_preview1" "environ_get"
(func $wasi.environ_get (param $environ i32) (param $environ_buf i32) (result (;errno;) i32)))

;; environ_sizes_get returns environment variables sizes.
;;
;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-environ_sizes_get---errno-size-size
(import "wasi_snapshot_preview1" "environ_sizes_get"
(func $wasi.environ_sizes_get (param $result.environc i32) (param $result.environ_buf_size i32) (result (;errno;) i32)))

;; fd_write write bytes to a file descriptor.
;;
;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
(import "wasi_snapshot_preview1" "fd_write"
(func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))

;; WASI commands are required to export "memory". Particularly, imported functions mutate this.
;;
;; Note: 1 is the size in pages (64KB), not bytes!
;; See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7
(memory (export "memory") 1)

;; $iovs are offset/length pairs in memory fd_write copies to the file descriptor.
;; $main will only write one offset/length pair, corresponding to null-terminated environ.
(global $iovs i32 i32.const 1024) ;; 1024 is an arbitrary offset larger than the environ.

;; WASI parameters are usually memory offsets, you can ignore values by writing them to an unread offset.
(global $ignored i32 i32.const 32768)

;; _start is a special function defined by a WASI Command that runs like a main function would.
;;
;; See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
(func $main (export "_start")
;; To copy an env to a file, we first need to load it into memory.
(call $wasi.environ_get
(global.get $ignored) ;; ignore $environ as we only read the environ_buf
(i32.const 0) ;; Write $environ_buf (null-terminated environ) to memory offset zero.
)
drop ;; ignore the errno returned

;; Next, we need to know how many bytes were loaded, as that's how much we'll copy to the file.
(call $wasi.environ_sizes_get
(global.get $ignored) ;; ignore $result.environc as we only read the environ_buf.
(i32.add (global.get $iovs) (i32.const 4)) ;; store $result.environ_buf_size as the length to copy
)
drop ;; ignore the errno returned

;; Finally, write the memory region to the file.
(call $wasi.fd_write
(i32.const 1) ;; $fd is a file descriptor and 1 is stdout (console).
(global.get $iovs) ;; $iovs is the start offset of the IO vectors to copy.
(i32.const 1) ;; $iovs_len is the count of offset/length pairs to copy to memory.
(global.get $ignored) ;; ignore $result.size as we aren't verifying it.
)
drop ;; ignore the errno returned
)
)
Binary file added cmd/wazero/testdata/wasi_fd.wasm
Binary file not shown.
Loading

0 comments on commit 6cf113b

Please sign in to comment.