gojs: adds support for uid and gid (#1245)

This adds `gojs.WithOSUser` which passes through current user IDs so
that GOOS=js compiled wasm can read them. This also adds support for
reading back the uid and gid on files. In summary, this passes
`os.TestChown` except on windows where it will not work due to lack of
support.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-03-16 11:07:27 +08:00
committed by GitHub
parent e17a85146a
commit 8464474e21
25 changed files with 556 additions and 270 deletions

View File

@@ -1,18 +1,25 @@
package gojs
import (
"net/http"
"github.com/tetratelabs/wazero/internal/gojs/custom"
"github.com/tetratelabs/wazero/internal/gojs/config"
"github.com/tetratelabs/wazero/internal/gojs/goos"
)
// newJsGlobal = js.Global() // js.go init
func newJsGlobal(rt http.RoundTripper) *jsVal {
func newJsGlobal(config *config.Config) *jsVal {
var fetchProperty interface{} = goos.Undefined
if rt != nil {
uid, gid, euid := config.Uid, config.Gid, config.Euid
groups := config.Groups
proc := &processState{
cwd: config.Workdir,
umask: config.Umask,
}
rt := config.Rt
if config.Rt != nil {
fetchProperty = goos.RefHttpFetch
}
return newJsVal(goos.RefValueGlobal, "global").
addProperties(map[string]interface{}{
"Object": objectConstructor,
@@ -22,8 +29,8 @@ func newJsGlobal(rt http.RoundTripper) *jsVal {
"fetch": fetchProperty,
"AbortController": goos.Undefined,
"Headers": headersConstructor,
"process": jsProcess,
"fs": jsfs,
"process": newJsProcess(uid, gid, euid, groups, proc),
"fs": newJsFs(proc),
"Date": jsDateConstructor,
}).
addFunction("fetch", &httpFetch{rt})
@@ -43,20 +50,6 @@ var (
// Get("Array") // js.go init
arrayConstructor = newJsVal(goos.RefArrayConstructor, "Array")
// jsProcess = js.Global().Get("process") // fs_js.go init
jsProcess = newJsVal(goos.RefJsProcess, custom.NameProcess).
addProperties(map[string]interface{}{
"pid": float64(1), // Get("pid").Int() in syscall_js.go for syscall.Getpid
"ppid": goos.RefValueZero, // Get("ppid").Int() in syscall_js.go for syscall.Getppid
}).
addFunction(custom.NameProcessCwd, processCwd{}). // syscall.Cwd in fs_js.go
addFunction(custom.NameProcessChdir, processChdir{}). // syscall.Chdir in fs_js.go
addFunction(custom.NameProcessGetuid, returnZero{}). // syscall.Getuid in syscall_js.go
addFunction(custom.NameProcessGetgid, returnZero{}). // syscall.Getgid in syscall_js.go
addFunction(custom.NameProcessGeteuid, returnZero{}). // syscall.Geteuid in syscall_js.go
addFunction(custom.NameProcessGetgroups, returnSliceOfZero{}). // syscall.Getgroups in syscall_js.go
addFunction(custom.NameProcessUmask, processUmask{}) // syscall.Umask in syscall_js.go
// uint8ArrayConstructor = js.Global().Get("Uint8Array")
// // fs_js.go, rand_js.go, roundtrip_js.go init
//

View File

@@ -3,23 +3,41 @@
package config
import (
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"syscall"
"github.com/tetratelabs/wazero/internal/platform"
)
type Config struct {
OsWorkdir bool
Rt http.RoundTripper
OsUser bool
Uid, Gid, Euid int
Groups []int
// Workdir is the actual working directory value.
Workdir string
Umask uint32
Rt http.RoundTripper
}
func NewConfig() *Config {
return &Config{Workdir: "/"}
return &Config{
OsWorkdir: false,
OsUser: false,
Uid: 0,
Gid: 0,
Euid: 0,
Groups: []int{0},
Workdir: "/",
Umask: uint32(0o0022),
Rt: nil,
}
}
func (c *Config) Clone() *Config {
@@ -38,5 +56,16 @@ func (c *Config) Init() error {
// Strip the volume of the path, for example C:\
c.Workdir = workdir[len(filepath.VolumeName(workdir)):]
}
// Windows does not support any of these properties
if c.OsUser && runtime.GOOS != "windows" {
c.Uid = syscall.Getuid()
c.Gid = syscall.Getgid()
c.Euid = syscall.Geteuid()
var err error
if c.Groups, err = syscall.Getgroups(); err != nil {
return fmt.Errorf("couldn't read groups: %w", err)
}
}
return nil
}

View File

@@ -1,7 +1,9 @@
package config
import (
"runtime"
"strings"
"syscall"
"testing"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -10,8 +12,37 @@ import (
func TestConfig_Init(t *testing.T) {
t.Parallel()
t.Run("OsWorkdir", func(t *testing.T) {
c := &Config{OsWorkdir: true}
t.Run("User", func(t *testing.T) {
c := NewConfig()
// values should be 0 which is root
require.Equal(t, 0, c.Uid)
require.Equal(t, 0, c.Gid)
require.Equal(t, 0, c.Euid)
require.Equal(t, []int{0}, c.Groups)
require.False(t, c.OsUser)
if runtime.GOOS != "windows" {
c.OsUser = true
require.NoError(t, c.Init())
require.Equal(t, syscall.Getuid(), c.Uid)
require.Equal(t, syscall.Getgid(), c.Gid)
require.Equal(t, syscall.Geteuid(), c.Euid)
groups, err := syscall.Getgroups()
require.NoError(t, err)
require.Equal(t, groups, c.Groups)
}
})
t.Run("Workdir", func(t *testing.T) {
c := NewConfig()
require.Equal(t, "/", c.Workdir)
require.False(t, c.OsWorkdir)
c.OsWorkdir = true
require.NoError(t, c.Init())
actual := c.Workdir

View File

@@ -6,12 +6,12 @@ import (
"io"
"io/fs"
"os"
"path"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/custom"
"github.com/tetratelabs/wazero/internal/gojs/goos"
"github.com/tetratelabs/wazero/internal/gojs/util"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
@@ -19,40 +19,6 @@ import (
)
var (
// jsfs = js.Global().Get("fs") // fs_js.go init
//
// js.fsCall conventions:
// * funcWrapper callback is the last parameter
// * arg0 is error and up to one result in arg1
jsfs = newJsVal(goos.RefJsfs, custom.NameFs).
addProperties(map[string]interface{}{
"constants": jsfsConstants, // = jsfs.Get("constants") // init
}).
addFunction(custom.NameFsOpen, jsfsOpen{}).
addFunction(custom.NameFsStat, jsfsStat{}).
addFunction(custom.NameFsFstat, jsfsFstat{}).
addFunction(custom.NameFsLstat, jsfsLstat{}).
addFunction(custom.NameFsClose, jsfsClose{}).
addFunction(custom.NameFsRead, jsfsRead{}).
addFunction(custom.NameFsWrite, jsfsWrite{}).
addFunction(custom.NameFsReaddir, jsfsReaddir{}).
addFunction(custom.NameFsMkdir, jsfsMkdir{}).
addFunction(custom.NameFsRmdir, jsfsRmdir{}).
addFunction(custom.NameFsRename, jsfsRename{}).
addFunction(custom.NameFsUnlink, jsfsUnlink{}).
addFunction(custom.NameFsUtimes, jsfsUtimes{}).
addFunction(custom.NameFsChmod, jsfsChmod{}).
addFunction(custom.NameFsFchmod, jsfsFchmod{}).
addFunction(custom.NameFsChown, jsfsChown{}).
addFunction(custom.NameFsFchown, jsfsFchown{}).
addFunction(custom.NameFsLchown, jsfsLchown{}).
addFunction(custom.NameFsTruncate, jsfsTruncate{}).
addFunction(custom.NameFsFtruncate, jsfsFtruncate{}).
addFunction(custom.NameFsReadlink, jsfsReadlink{}).
addFunction(custom.NameFsLink, jsfsLink{}).
addFunction(custom.NameFsSymlink, jsfsSymlink{}).
addFunction(custom.NameFsFsync, jsfsFsync{})
// jsfsConstants = jsfs Get("constants") // fs_js.go init
jsfsConstants = newJsVal(goos.RefJsfsConstants, "constants").
addProperties(map[string]interface{}{
@@ -93,15 +59,53 @@ type (
truncateFile interface{ Truncate(size int64) error }
)
// jsfs = js.Global().Get("fs") // fs_js.go init
//
// js.fsCall conventions:
// * funcWrapper callback is the last parameter
// - arg0 is error and up to one result in arg1
func newJsFs(proc *processState) *jsVal {
return newJsVal(goos.RefJsfs, custom.NameFs).
addProperties(map[string]interface{}{
"constants": jsfsConstants, // = jsfs.Get("constants") // init
}).
addFunction(custom.NameFsOpen, &jsfsOpen{proc: proc}).
addFunction(custom.NameFsStat, &jsfsStat{proc: proc}).
addFunction(custom.NameFsFstat, jsfsFstat{}).
addFunction(custom.NameFsLstat, &jsfsLstat{proc: proc}).
addFunction(custom.NameFsClose, jsfsClose{}).
addFunction(custom.NameFsRead, jsfsRead{}).
addFunction(custom.NameFsWrite, jsfsWrite{}).
addFunction(custom.NameFsReaddir, &jsfsReaddir{proc: proc}).
addFunction(custom.NameFsMkdir, &jsfsMkdir{proc: proc}).
addFunction(custom.NameFsRmdir, &jsfsRmdir{proc: proc}).
addFunction(custom.NameFsRename, &jsfsRename{proc: proc}).
addFunction(custom.NameFsUnlink, &jsfsUnlink{proc: proc}).
addFunction(custom.NameFsUtimes, &jsfsUtimes{proc: proc}).
addFunction(custom.NameFsChmod, &jsfsChmod{proc: proc}).
addFunction(custom.NameFsFchmod, jsfsFchmod{}).
addFunction(custom.NameFsChown, &jsfsChown{proc: proc}).
addFunction(custom.NameFsFchown, jsfsFchown{}).
addFunction(custom.NameFsLchown, &jsfsLchown{proc: proc}).
addFunction(custom.NameFsTruncate, &jsfsTruncate{proc: proc}).
addFunction(custom.NameFsFtruncate, jsfsFtruncate{}).
addFunction(custom.NameFsReadlink, &jsfsReadlink{proc: proc}).
addFunction(custom.NameFsLink, &jsfsLink{proc: proc}).
addFunction(custom.NameFsSymlink, &jsfsSymlink{proc: proc}).
addFunction(custom.NameFsFsync, jsfsFsync{})
}
// jsfsOpen implements implements jsFn for syscall.Open
//
// jsFD /* Int */, err := fsCall("open", path, flags, perm)
type jsfsOpen struct{}
type jsfsOpen struct {
proc *processState
}
func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (o *jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(o.proc.cwd, args[0].(string))
flags := toUint64(args[1]) // flags are derived from constants like oWRONLY
perm := getPerm(ctx, goos.ValueToUint32(args[2]))
perm := custom.FromJsMode(goos.ValueToUint32(args[2]), o.proc.umask)
callback := args[3].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -114,10 +118,12 @@ func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{})
// jsfsStat implements jsFn for syscall.Stat
//
// jsSt, err := fsCall("stat", path)
type jsfsStat struct{}
type jsfsStat struct {
proc *processState
}
func (jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (s *jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(s.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
stat, err := syscallStat(mod, path)
@@ -138,10 +144,12 @@ func syscallStat(mod api.Module, path string) (*jsSt, error) {
// jsfsLstat implements jsFn for syscall.Lstat
//
// jsSt, err := fsCall("lstat", path)
type jsfsLstat struct{}
type jsfsLstat struct {
proc *processState
}
func (jsfsLstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (l *jsfsLstat) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(l.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
lstat, err := syscallLstat(mod, path)
@@ -194,6 +202,8 @@ func newJsSt(st *platform.Stat_t) *jsSt {
ret.isDir = st.Mode.IsDir()
ret.dev = st.Dev
ret.ino = st.Ino
ret.uid = st.Uid
ret.gid = st.Gid
ret.mode = custom.ToJsMode(st.Mode)
ret.nlink = uint32(st.Nlink)
ret.size = st.Size
@@ -319,10 +329,12 @@ func syscallWrite(mod api.Module, fd uint32, offset interface{}, p []byte) (n ui
//
// dir, err := fsCall("readdir", path)
// dir.Length(), dir.Index(i).String()
type jsfsReaddir struct{}
type jsfsReaddir struct {
proc *processState
}
func (jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (r *jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(r.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
stat, err := syscallReaddir(ctx, mod, path)
@@ -350,64 +362,16 @@ func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArra
}
}
// returnZero implements jsFn
type returnZero struct{}
func (returnZero) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return goos.RefValueZero, nil
}
// returnSliceOfZero implements jsFn
type returnSliceOfZero struct{}
func (returnSliceOfZero) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return &objectArray{slice: []interface{}{goos.RefValueZero}}, nil
}
// processCwd implements jsFn for fs.Open syscall.Getcwd in fs_js.go
type processCwd struct{}
func (processCwd) invoke(ctx context.Context, _ api.Module, _ ...interface{}) (interface{}, error) {
return getState(ctx).cwd, nil
}
// processChdir implements jsFn for fs.Open syscall.Chdir in fs_js.go
type processChdir struct{}
func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := path.Clean(args[0].(string))
if s, err := syscallStat(mod, path); err != nil {
return nil, err
} else if !s.isDir {
return nil, syscall.ENOTDIR
} else {
getState(ctx).cwd = path
return nil, nil
}
}
// processUmask implements jsFn for fs.Open syscall.Umask in fs_js.go
type processUmask struct{}
func (processUmask) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
mask := goos.ValueToUint32(args[0])
s := getState(ctx)
oldmask := s.umask
s.umask = mask
return oldmask, nil
}
// jsfsMkdir implements implements jsFn for fs.Mkdir
//
// jsFD /* Int */, err := fsCall("mkdir", path, perm)
type jsfsMkdir struct{}
type jsfsMkdir struct {
proc *processState
}
func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
perm := getPerm(ctx, goos.ValueToUint32(args[1]))
func (m *jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(m.proc.cwd, args[0].(string))
perm := custom.FromJsMode(goos.ValueToUint32(args[1]), m.proc.umask)
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -429,10 +393,12 @@ func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
// jsfsRmdir implements jsFn for the following
//
// _, err := fsCall("rmdir", path) // syscall.Rmdir
type jsfsRmdir struct{}
type jsfsRmdir struct {
proc *processState
}
func (jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (r *jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(r.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -444,11 +410,14 @@ func (jsfsRmdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
// jsfsRename implements jsFn for the following
//
// _, err := fsCall("rename", from, to) // syscall.Rename
type jsfsRename struct{}
type jsfsRename struct {
proc *processState
}
func (jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
from := resolvePath(ctx, args[0].(string))
to := resolvePath(ctx, args[1].(string))
func (r *jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
cwd := r.proc.cwd
from := util.ResolvePath(cwd, args[0].(string))
to := util.ResolvePath(cwd, args[1].(string))
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -460,10 +429,12 @@ func (jsfsRename) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsUnlink implements jsFn for the following
//
// _, err := fsCall("unlink", path) // syscall.Unlink
type jsfsUnlink struct{}
type jsfsUnlink struct {
proc *processState
}
func (jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (u *jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(u.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -475,10 +446,12 @@ func (jsfsUnlink) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsUtimes implements jsFn for the following
//
// _, err := fsCall("utimes", path, atime, mtime) // syscall.Utimens
type jsfsUtimes struct{}
type jsfsUtimes struct {
proc *processState
}
func (jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (u *jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(u.proc.cwd, args[0].(string))
atimeSec := toInt64(args[1])
mtimeSec := toInt64(args[2])
callback := args[3].(funcWrapper)
@@ -495,10 +468,12 @@ func (jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsChmod implements jsFn for the following
//
// _, err := fsCall("chmod", path, mode) // syscall.Chmod
type jsfsChmod struct{}
type jsfsChmod struct {
proc *processState
}
func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (c *jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(c.proc.cwd, args[0].(string))
mode := custom.FromJsMode(goos.ValueToUint32(args[1]), 0)
callback := args[2].(funcWrapper)
@@ -535,10 +510,12 @@ func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsChown implements jsFn for the following
//
// _, err := fsCall("chown", path, uint32(uid), uint32(gid)) // syscall.Chown
type jsfsChown struct{}
type jsfsChown struct {
proc *processState
}
func (jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (c *jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(c.proc.cwd, args[0].(string))
uid := goos.ValueToInt32(args[1])
gid := goos.ValueToInt32(args[2])
callback := args[3].(funcWrapper)
@@ -575,10 +552,12 @@ func (jsfsFchown) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsLchown implements jsFn for the following
//
// _, err := fsCall("lchown", path, uint32(uid), uint32(gid)) // syscall.Lchown
type jsfsLchown struct{}
type jsfsLchown struct {
proc *processState
}
func (jsfsLchown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (l *jsfsLchown) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(l.proc.cwd, args[0].(string))
uid := goos.ValueToUint32(args[1])
gid := goos.ValueToUint32(args[2])
callback := args[3].(funcWrapper)
@@ -592,10 +571,12 @@ func (jsfsLchown) invoke(ctx context.Context, mod api.Module, args ...interface{
// jsfsTruncate implements jsFn for the following
//
// _, err := fsCall("truncate", path, length) // syscall.Truncate
type jsfsTruncate struct{}
type jsfsTruncate struct {
proc *processState
}
func (jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (t *jsfsTruncate) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(t.proc.cwd, args[0].(string))
length := toInt64(args[1])
callback := args[2].(funcWrapper)
@@ -632,10 +613,12 @@ func (jsfsFtruncate) invoke(ctx context.Context, mod api.Module, args ...interfa
// jsfsReadlink implements jsFn for syscall.Readlink
//
// dst, err := fsCall("readlink", path) // syscall.Readlink
type jsfsReadlink struct{}
type jsfsReadlink struct {
proc *processState
}
func (jsfsReadlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
func (r *jsfsReadlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := util.ResolvePath(r.proc.cwd, args[0].(string))
callback := args[1].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -647,11 +630,14 @@ func (jsfsReadlink) invoke(ctx context.Context, mod api.Module, args ...interfac
// jsfsLink implements jsFn for the following
//
// _, err := fsCall("link", path, link) // syscall.Link
type jsfsLink struct{}
type jsfsLink struct {
proc *processState
}
func (jsfsLink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
path := resolvePath(ctx, args[0].(string))
link := resolvePath(ctx, args[1].(string))
func (l *jsfsLink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
cwd := l.proc.cwd
path := util.ResolvePath(cwd, args[0].(string))
link := util.ResolvePath(cwd, args[1].(string))
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -663,11 +649,13 @@ func (jsfsLink) invoke(ctx context.Context, mod api.Module, args ...interface{})
// jsfsSymlink implements jsFn for the following
//
// _, err := fsCall("symlink", path, link) // syscall.Symlink
type jsfsSymlink struct{}
type jsfsSymlink struct {
proc *processState
}
func (jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
func (s *jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
dst := args[0].(string) // The dst of a symlink must not be resolved, as it should be resolved during readLink.
link := resolvePath(ctx, args[1].(string))
link := util.ResolvePath(s.proc.cwd, args[1].(string))
callback := args[2].(funcWrapper)
fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -723,7 +711,7 @@ func (s *jsSt) String() string {
}
// Get implements the same method as documented on goos.GetFunction
func (s *jsSt) Get(_ context.Context, propertyKey string) interface{} {
func (s *jsSt) Get(propertyKey string) interface{} {
switch propertyKey {
case "dev":
return s.dev
@@ -766,25 +754,3 @@ func (s *jsSt) call(_ context.Context, _ api.Module, _ goos.Ref, method string,
func jsfsInvoke(ctx context.Context, mod api.Module, callback funcWrapper, err error) (interface{}, error) {
return callback.invoke(ctx, mod, goos.RefJsfs, err, err == nil) // note: error first
}
// resolvePath is needed when a non-absolute path is given to a function.
// Unlike other host ABI, GOOS=js maintains the CWD host side.
func resolvePath(ctx context.Context, path string) string {
if len(path) == 0 || path[0] == '/' {
return path // leave alone .. or absolute paths.
}
return joinPath(getState(ctx).cwd, path)
}
// joinPath avoids us having to rename fields just to avoid conflict with the
// path package.
func joinPath(dirName, baseName string) string {
return path.Join(dirName, baseName)
}
// getPerm converts the input js permissions to a go-compatible one, after
// subtracting the current umask.
func getPerm(ctx context.Context, perm uint32) fs.FileMode {
umask := getState(ctx).umask
return custom.FromJsMode(perm, umask)
}

View File

@@ -21,9 +21,7 @@ func Test_fs(t *testing.T) {
require.Zero(t, stderr)
require.EqualError(t, err, `module "" closed with exit_code(0)`)
require.Equal(t, `wd ok
Not a directory
sub mode drwxr-xr-x
require.Equal(t, `sub mode drwxr-xr-x
/animals.txt mode -rw-r--r--
animals.txt mode -rw-r--r--
contents: bear

View File

@@ -276,7 +276,7 @@ func ValueToInt32(arg interface{}) int32 {
// GetFunction allows getting a JavaScript property by name.
type GetFunction interface {
Get(ctx context.Context, propertyKey string) interface{}
Get(propertyKey string) interface{}
}
// ByteArray is a result of uint8ArrayConstructor which temporarily stores
@@ -297,7 +297,7 @@ func (a *ByteArray) Unwrap() []byte {
}
// Get implements GetFunction
func (a *ByteArray) Get(_ context.Context, propertyKey string) interface{} {
func (a *ByteArray) Get(propertyKey string) interface{} {
switch propertyKey {
case "byteLength":
return uint32(len(a.slice))

View File

@@ -67,7 +67,7 @@ type fetchResult struct {
}
// Get implements the same method as documented on goos.GetFunction
func (s *fetchResult) Get(_ context.Context, propertyKey string) interface{} {
func (s *fetchResult) Get(propertyKey string) interface{} {
switch propertyKey {
case "headers":
names := make([]string, 0, len(s.res.Header))
@@ -104,7 +104,7 @@ type headers struct {
}
// Get implements the same method as documented on goos.GetFunction
func (h *headers) Get(_ context.Context, propertyKey string) interface{} {
func (h *headers) Get(propertyKey string) interface{} {
switch propertyKey {
case "done":
return h.i == len(h.names)

View File

@@ -41,9 +41,9 @@ func Test_http(t *testing.T) {
})
stdout, stderr, err := compileAndRun(testCtx, "http", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
return moduleConfig.WithEnv("BASE_URL", "http://host"), &config.Config{
Rt: rt,
}
config := config.NewConfig()
config.Rt = rt
return moduleConfig.WithEnv("BASE_URL", "http://host"), config
})
require.EqualError(t, err, `module "" closed with exit_code(0)`)

View File

@@ -51,7 +51,7 @@ func (v *jsVal) addFunction(method string, fn jsFn) *jsVal {
}
// Get implements the same method as documented on goos.GetFunction
func (v *jsVal) Get(_ context.Context, propertyKey string) interface{} {
func (v *jsVal) Get(propertyKey string) interface{} {
if v, ok := v.properties[propertyKey]; ok {
return v
}

103
internal/gojs/process.go Normal file
View File

@@ -0,0 +1,103 @@
package gojs
import (
"context"
"path"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/custom"
"github.com/tetratelabs/wazero/internal/gojs/goos"
"github.com/tetratelabs/wazero/internal/gojs/util"
)
// processState are the mutable fields of the current process.
type processState struct {
cwd string
umask uint32
}
func newJsProcess(uid, gid, euid int, groups []int, proc *processState) *jsVal {
uidRef := toFloatRef(float64(uid))
gidRef := toFloatRef(float64(gid))
euidRef := toFloatRef(float64(euid))
groupSlice := make([]interface{}, 0, len(groups))
for _, group := range groups {
groupSlice = append(groupSlice, toFloatRef(float64(group)))
}
// jsProcess = js.Global().Get("process") // fs_js.go init
return newJsVal(goos.RefJsProcess, custom.NameProcess).
addProperties(map[string]interface{}{
"pid": float64(1), // Get("pid").Int() in syscall_js.go for syscall.Getpid
"ppid": goos.RefValueZero, // Get("ppid").Int() in syscall_js.go for syscall.Getppid
}).
addFunction(custom.NameProcessCwd, &processCwd{proc: proc}). // syscall.Cwd in fs_js.go
addFunction(custom.NameProcessChdir, &processChdir{proc: proc}). // syscall.Chdir in fs_js.go
addFunction(custom.NameProcessGetuid, getId(uidRef)). // syscall.Getuid in syscall_js.go
addFunction(custom.NameProcessGetgid, getId(gidRef)). // syscall.Getgid in syscall_js.go
addFunction(custom.NameProcessGeteuid, getId(euidRef)). // syscall.Geteuid in syscall_js.go
addFunction(custom.NameProcessGetgroups, returnSlice(groupSlice)). // syscall.Getgroups in syscall_js.go
addFunction(custom.NameProcessUmask, &processUmask{proc: proc}) // syscall.Umask in syscall_js.go
}
// processCwd implements jsFn for fs.Open syscall.Getcwd in fs_js.go
type processCwd struct {
proc *processState
}
func (p *processCwd) invoke(_ context.Context, _ api.Module, _ ...interface{}) (interface{}, error) {
return p.proc.cwd, nil
}
// processChdir implements jsFn for fs.Open syscall.Chdir in fs_js.go
type processChdir struct {
proc *processState
}
func (p *processChdir) invoke(_ context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
oldWd := p.proc.cwd
newWd := util.ResolvePath(oldWd, args[0].(string))
newWd = path.Clean(newWd)
if newWd == oldWd { // handle .
return nil, nil
}
if s, err := syscallStat(mod, newWd); err != nil {
return nil, err
} else if !s.isDir {
return nil, syscall.ENOTDIR
} else {
p.proc.cwd = newWd
return nil, nil
}
}
// processUmask implements jsFn for fs.Open syscall.Umask in fs_js.go
type processUmask struct {
proc *processState
}
func (p *processUmask) invoke(_ context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
newUmask := goos.ValueToUint32(args[0])
oldUmask := p.proc.umask
p.proc.umask = newUmask
return oldUmask, nil
}
// getId implements jsFn for syscall.Getuid, syscall.Getgid and syscall.Geteuid in syscall_js.go
type getId goos.Ref
func (i getId) invoke(_ context.Context, _ api.Module, _ ...interface{}) (interface{}, error) {
return goos.Ref(i), nil
}
// returnSlice implements jsFn for syscall.Getgroups in syscall_js.go
type returnSlice []interface{}
func (s returnSlice) invoke(context.Context, api.Module, ...interface{}) (interface{}, error) {
return &objectArray{slice: s}, nil
}

View File

@@ -1,18 +1,24 @@
package gojs_test
import (
"os"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/gojs/config"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func Test_syscall(t *testing.T) {
func Test_process(t *testing.T) {
t.Parallel()
stdout, stderr, err := compileAndRun(testCtx, "syscall", defaultConfig)
require.NoError(t, os.Chdir("/.."))
stdout, stderr, err := compileAndRun(testCtx, "process", func(moduleConfig wazero.ModuleConfig) (wazero.ModuleConfig, *config.Config) {
return defaultConfig(moduleConfig.WithFS(testFS))
})
require.EqualError(t, err, `module "" closed with exit_code(0)`)
require.Zero(t, stderr)
require.EqualError(t, err, `module "" closed with exit_code(0)`)
require.Equal(t, `syscall.Getpid()=1
syscall.Getppid()=0
syscall.Getuid()=0
@@ -21,5 +27,7 @@ syscall.Geteuid()=0
syscall.Umask(0077)=0o22
syscall.Getgroups()=[0]
os.FindProcess(1).Pid=1
wd ok
Not a directory
`, stdout)
}

View File

@@ -14,9 +14,7 @@ import (
func NewState(config *config.Config) *State {
return &State{
values: values.NewValues(),
valueGlobal: newJsGlobal(config.Rt),
cwd: config.Workdir,
umask: 0o0022,
valueGlobal: newJsGlobal(config),
_nextCallbackTimeoutID: 1,
_scheduledTimeouts: map[uint32]chan bool{},
}
@@ -48,7 +46,7 @@ type event struct {
}
// Get implements the same method as documented on goos.GetFunction
func (e *event) Get(_ context.Context, propertyKey string) interface{} {
func (e *event) Get(propertyKey string) interface{} {
switch propertyKey {
case "id":
return e.id
@@ -89,9 +87,9 @@ func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
case goos.RefArrayConstructor:
return arrayConstructor
case goos.RefJsProcess:
return jsProcess
return getState(ctx).valueGlobal.Get("process")
case goos.RefJsfs:
return jsfs
return getState(ctx).valueGlobal.Get("fs")
case goos.RefJsfsConstants:
return jsfsConstants
case goos.RefUint8ArrayConstructor:
@@ -180,15 +178,10 @@ type State struct {
_nextCallbackTimeoutID uint32
_scheduledTimeouts map[uint32]chan bool
// cwd is initially "/"
cwd string
// umask is initially 0022
umask uint32
}
// Get implements the same method as documented on goos.GetFunction
func (s *State) Get(_ context.Context, propertyKey string) interface{} {
func (s *State) Get(propertyKey string) interface{} {
switch propertyKey {
case "_pendingEvent":
return s._pendingEvent
@@ -220,8 +213,6 @@ func (s *State) close() {
s._pendingEvent = nil
s._lastEvent = nil
s._nextCallbackTimeoutID = 1
s.cwd = "/"
s.umask = 0o0022
}
func toInt64(arg interface{}) int64 {

View File

@@ -55,7 +55,7 @@ func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
var result interface{}
if g, ok := v.(goos.GetFunction); ok {
result = g.Get(ctx, p)
result = g.Get(p)
} else if e, ok := v.(error); ok {
switch p {
case "message": // js (GOOS=js) error, can be anything.

View File

@@ -6,7 +6,6 @@ import (
"io"
"log"
"os"
"syscall"
)
func Main() {
@@ -14,19 +13,6 @@ func Main() {
}
func testAdHoc() {
if wd, err := syscall.Getwd(); err != nil {
log.Panicln(err)
} else if wd != "/" {
log.Panicln("not root")
}
fmt.Println("wd ok")
if err := syscall.Chdir("/animals.txt"); err == nil {
log.Panicln("shouldn't be able to chdir to file")
} else {
fmt.Println(err) // should be the textual message of the errno.
}
// Ensure stat works, particularly mode.
for _, path := range []string{"sub", "/animals.txt", "animals.txt"} {
if stat, err := os.Stat(path); err != nil {

View File

@@ -11,8 +11,8 @@ import (
"github.com/tetratelabs/wazero/internal/gojs/testdata/goroutine"
"github.com/tetratelabs/wazero/internal/gojs/testdata/http"
"github.com/tetratelabs/wazero/internal/gojs/testdata/mem"
"github.com/tetratelabs/wazero/internal/gojs/testdata/process"
"github.com/tetratelabs/wazero/internal/gojs/testdata/stdio"
"github.com/tetratelabs/wazero/internal/gojs/testdata/syscall"
"github.com/tetratelabs/wazero/internal/gojs/testdata/testfs"
"github.com/tetratelabs/wazero/internal/gojs/testdata/time"
"github.com/tetratelabs/wazero/internal/gojs/testdata/writefs"
@@ -29,24 +29,24 @@ func main() {
os.Exit(255)
case "fs":
fs.Main()
case "testfs":
testfs.Main()
case "writefs":
writefs.Main()
case "gc":
gc.Main()
case "goroutine":
goroutine.Main()
case "http":
http.Main()
case "goroutine":
goroutine.Main()
case "mem":
mem.Main()
case "process":
process.Main()
case "stdio":
stdio.Main()
case "syscall":
syscall.Main()
case "testfs":
testfs.Main()
case "time":
time.Main()
case "writefs":
writefs.Main()
default:
panic(fmt.Errorf("unsupported arg: %s", os.Args[1]))
}

64
internal/gojs/testdata/process/main.go vendored Normal file
View File

@@ -0,0 +1,64 @@
// Package process is an integration test of system calls mapped to the
// JavaScript object "process". e.g. `go.syscall/js.valueCall(process.chdir...`
package process
import (
"fmt"
"log"
"os"
"syscall"
)
func Main() {
fmt.Printf("syscall.Getpid()=%d\n", syscall.Getpid())
fmt.Printf("syscall.Getppid()=%d\n", syscall.Getppid())
fmt.Printf("syscall.Getuid()=%d\n", syscall.Getuid())
fmt.Printf("syscall.Getgid()=%d\n", syscall.Getgid())
fmt.Printf("syscall.Geteuid()=%d\n", syscall.Geteuid())
fmt.Printf("syscall.Umask(0077)=%O\n", syscall.Umask(0o077))
if g, err := syscall.Getgroups(); err != nil {
log.Panicln(err)
} else {
fmt.Printf("syscall.Getgroups()=%v\n", g)
}
pid := syscall.Getpid()
if p, err := os.FindProcess(pid); err != nil {
log.Panicln(err)
} else {
fmt.Printf("os.FindProcess(%d).Pid=%d\n", pid, p.Pid)
}
if wd, err := syscall.Getwd(); err != nil {
log.Panicln(err)
} else if wd != "/" {
log.Panicln("not root")
}
fmt.Println("wd ok")
dirs := []struct {
path, wd string
}{
{"dir", "/dir"},
{".", "/dir"},
{"..", "/"},
{".", "/"},
{"..", "/"},
}
for _, dir := range dirs {
if err := syscall.Chdir(dir.path); err != nil {
log.Panicln(dir.path, err)
} else if wd, err := syscall.Getwd(); err != nil {
log.Panicln(dir.path, err)
} else if wd != dir.wd {
log.Panicf("cd %s: expected wd=%s, but have %s", dir.path, dir.wd, wd)
}
}
if err := syscall.Chdir("/animals.txt"); err == nil {
log.Panicln("shouldn't be able to chdir to file")
} else {
fmt.Println(err) // should be the textual message of the errno.
}
}

View File

@@ -1,29 +0,0 @@
package syscall
import (
"fmt"
"log"
"os"
"syscall"
)
func Main() {
fmt.Printf("syscall.Getpid()=%d\n", syscall.Getpid())
fmt.Printf("syscall.Getppid()=%d\n", syscall.Getppid())
fmt.Printf("syscall.Getuid()=%d\n", syscall.Getuid())
fmt.Printf("syscall.Getgid()=%d\n", syscall.Getgid())
fmt.Printf("syscall.Geteuid()=%d\n", syscall.Geteuid())
fmt.Printf("syscall.Umask(0077)=%O\n", syscall.Umask(0o077))
if g, err := syscall.Getgroups(); err != nil {
log.Panicln(err)
} else {
fmt.Printf("syscall.Getgroups()=%v\n", g)
}
pid := syscall.Getpid()
if p, err := os.FindProcess(pid); err != nil {
log.Panicln(err)
} else {
fmt.Printf("os.FindProcess(%d).Pid=%d\n", pid, p.Pid)
}
}

View File

@@ -2,6 +2,7 @@ package util
import (
"fmt"
pathutil "path"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/gojs/custom"
@@ -44,3 +45,26 @@ func NewFunc(name string, goFunc api.GoModuleFunc) *wasm.HostFunc {
Code: wasm.Code{GoFunc: goFunc},
}
}
// ResolvePath is needed when a non-absolute path is given to a function.
// Unlike other host ABI, GOOS=js maintains the CWD host side.
func ResolvePath(cwd, path string) (resolved string) {
pathLen := len(path)
switch {
case pathLen == 0:
return cwd
case pathLen == 1 && path[0] == '.':
return cwd
case path[0] == '/':
resolved = pathutil.Clean(path)
default:
resolved = pathutil.Join(cwd, path)
}
// If there's a trailing slash, we need to retain it for symlink edge
// cases. See https://github.com/golang/go/issues/27225
if len(resolved) > 1 && path[pathLen-1] == '/' {
return resolved + "/"
}
return resolved
}

View File

@@ -1,6 +1,7 @@
package util
import (
"fmt"
"testing"
"github.com/tetratelabs/wazero/internal/gojs/custom"
@@ -73,3 +74,39 @@ func TestMustRead(t *testing.T) {
})
}
}
func TestResolvePath(t *testing.T) {
t.Parallel()
tests := []struct {
cwd, path string
expected string
}{
{cwd: "/", path: ".", expected: "/"},
{cwd: "/", path: "/", expected: "/"},
{cwd: "/", path: "..", expected: "/"},
{cwd: "/", path: "a", expected: "/a"},
{cwd: "/", path: "/a", expected: "/a"},
{cwd: "/", path: "./a/", expected: "/a/"}, // retain trailing slash
{cwd: "/", path: "./a/.", expected: "/a"},
{cwd: "/", path: "a/.", expected: "/a"},
{cwd: "/a", path: "/..", expected: "/"},
{cwd: "/a", path: "/", expected: "/"},
{cwd: "/a", path: "b", expected: "/a/b"},
{cwd: "/a", path: "/b", expected: "/b"},
{cwd: "/a", path: "/b/", expected: "/b/"}, // retain trailing slash
{cwd: "/a", path: "./b/.", expected: "/a/b"},
{cwd: "/a/b", path: ".", expected: "/a/b"},
{cwd: "/a/b", path: "../.", expected: "/a"},
{cwd: "/a/b", path: "../..", expected: "/"},
{cwd: "/a/b", path: "../../..", expected: "/"},
}
for _, tt := range tests {
tc := tt
t.Run(fmt.Sprintf("%s,%s", tc.cwd, tc.path), func(t *testing.T) {
require.Equal(t, tc.expected, ResolvePath(tc.cwd, tc.path))
})
}
}