diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index d86eab52..4845cbf1 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -263,7 +263,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod case modeGo: gojs.MustInstantiate(ctx, rt) - config := gojs.NewConfig(conf) + config := gojs.NewConfig(conf).WithOSUser() // Strip the volume of the path, for example C:\ rootDir := rootPath[len(filepath.VolumeName(rootPath)):] diff --git a/experimental/gojs/gojs.go b/experimental/gojs/gojs.go index c5353dcf..67f85566 100644 --- a/experimental/gojs/gojs.go +++ b/experimental/gojs/gojs.go @@ -112,6 +112,17 @@ type Config interface { // as the guest path "", while the current directory is inside `D:\`. WithOSWorkdir() Config + // WithOSUser allows the guest to see the current user's uid, gid, euid and + // groups, instead of zero for each value. + // + // Here's an example that uses the real user's IDs: + // + // err = gojs.Run(ctx, r, compiled, gojs.NewConfig(moduleConfig). + // WithOSUser()) + // + // Note: This has no effect on windows. + WithOSUser() Config + // WithRoundTripper sets the http.RoundTripper used to Run Wasm. // // For example, if the code compiled via `GOARCH=wasm GOOS=js` uses @@ -144,6 +155,13 @@ func (c *cfg) WithOSWorkdir() Config { return ret } +// WithOSUser implements Config.WithOSUser +func (c *cfg) WithOSUser() Config { + ret := c.clone() + ret.internal.OsUser = true + return ret +} + // WithRoundTripper implements Config.WithRoundTripper func (c *cfg) WithRoundTripper(rt http.RoundTripper) Config { ret := c.clone() diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go index 17cfbd37..e69e807f 100644 --- a/internal/gojs/builtin.go +++ b/internal/gojs/builtin.go @@ -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 // diff --git a/internal/gojs/config/config.go b/internal/gojs/config/config.go index dab63bfe..4fb23935 100644 --- a/internal/gojs/config/config.go +++ b/internal/gojs/config/config.go @@ -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 } diff --git a/internal/gojs/config/config_test.go b/internal/gojs/config/config_test.go index 870231b7..ce32b564 100644 --- a/internal/gojs/config/config_test.go +++ b/internal/gojs/config/config_test.go @@ -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 diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go index bc60ecd1..280ab9eb 100644 --- a/internal/gojs/fs.go +++ b/internal/gojs/fs.go @@ -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) -} diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go index b5dae811..81def6d3 100644 --- a/internal/gojs/fs_test.go +++ b/internal/gojs/fs_test.go @@ -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 diff --git a/internal/gojs/goos/goos.go b/internal/gojs/goos/goos.go index c56ab0d3..477b02a3 100644 --- a/internal/gojs/goos/goos.go +++ b/internal/gojs/goos/goos.go @@ -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)) diff --git a/internal/gojs/http.go b/internal/gojs/http.go index 1026d0d7..13130dfe 100644 --- a/internal/gojs/http.go +++ b/internal/gojs/http.go @@ -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) diff --git a/internal/gojs/http_test.go b/internal/gojs/http_test.go index c8cc2dbe..90ae0cf6 100644 --- a/internal/gojs/http_test.go +++ b/internal/gojs/http_test.go @@ -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)`) diff --git a/internal/gojs/js.go b/internal/gojs/js.go index bf419ada..2667fa6e 100644 --- a/internal/gojs/js.go +++ b/internal/gojs/js.go @@ -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 } diff --git a/internal/gojs/process.go b/internal/gojs/process.go new file mode 100644 index 00000000..adcf17da --- /dev/null +++ b/internal/gojs/process.go @@ -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 +} diff --git a/internal/gojs/syscall_test.go b/internal/gojs/process_test.go similarity index 51% rename from internal/gojs/syscall_test.go rename to internal/gojs/process_test.go index e2b81df7..1e2a9c1c 100644 --- a/internal/gojs/syscall_test.go +++ b/internal/gojs/process_test.go @@ -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) } diff --git a/internal/gojs/state.go b/internal/gojs/state.go index e0380a53..57ba3ea1 100644 --- a/internal/gojs/state.go +++ b/internal/gojs/state.go @@ -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 { diff --git a/internal/gojs/syscall.go b/internal/gojs/syscall.go index 1edc7885..8a8dca15 100644 --- a/internal/gojs/syscall.go +++ b/internal/gojs/syscall.go @@ -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. diff --git a/internal/gojs/testdata/fs/main.go b/internal/gojs/testdata/fs/main.go index 9c07f3dc..410e4a5e 100644 --- a/internal/gojs/testdata/fs/main.go +++ b/internal/gojs/testdata/fs/main.go @@ -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 { diff --git a/internal/gojs/testdata/main.go b/internal/gojs/testdata/main.go index a78f0d22..9c66072f 100644 --- a/internal/gojs/testdata/main.go +++ b/internal/gojs/testdata/main.go @@ -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])) } diff --git a/internal/gojs/testdata/process/main.go b/internal/gojs/testdata/process/main.go new file mode 100644 index 00000000..c85df222 --- /dev/null +++ b/internal/gojs/testdata/process/main.go @@ -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. + } +} diff --git a/internal/gojs/testdata/syscall/main.go b/internal/gojs/testdata/syscall/main.go deleted file mode 100644 index 2c593f12..00000000 --- a/internal/gojs/testdata/syscall/main.go +++ /dev/null @@ -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) - } -} diff --git a/internal/gojs/util/util.go b/internal/gojs/util/util.go index 8c0fba4b..21db4c92 100644 --- a/internal/gojs/util/util.go +++ b/internal/gojs/util/util.go @@ -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 +} diff --git a/internal/gojs/util/util_test.go b/internal/gojs/util/util_test.go index e3027872..db02beb6 100644 --- a/internal/gojs/util/util_test.go +++ b/internal/gojs/util/util_test.go @@ -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)) + }) + } +} diff --git a/internal/platform/stat.go b/internal/platform/stat.go index e04be140..f41b3e5f 100644 --- a/internal/platform/stat.go +++ b/internal/platform/stat.go @@ -19,6 +19,14 @@ type Stat_t struct { // Ino is the file serial number. Ino uint64 + // Uid is the user ID that owns the file, or zero if unsupported. + // For example, this is unsupported on some virtual filesystems or windows. + Uid uint32 + + // Gid is the group ID that owns the file, or zero if unsupported. + // For example, this is unsupported on some virtual filesystems or windows. + Gid uint32 + // Mode is the same as Mode on fs.FileInfo containing bits to identify the // type of the file (fs.ModeType) and its permissions (fs.ModePerm). Mode fs.FileMode diff --git a/internal/platform/stat_bsd.go b/internal/platform/stat_bsd.go index c2e1ffc6..4e8c9a20 100644 --- a/internal/platform/stat_bsd.go +++ b/internal/platform/stat_bsd.go @@ -37,8 +37,10 @@ func inoFromFileInfo(_ readdirFile, t fs.FileInfo) (ino uint64, err error) { func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) { if d, ok := t.Sys().(*syscall.Stat_t); ok { - st.Ino = d.Ino st.Dev = uint64(d.Dev) + st.Ino = d.Ino + st.Uid = d.Uid + st.Gid = d.Gid st.Mode = t.Mode() st.Nlink = uint64(d.Nlink) st.Size = d.Size diff --git a/internal/platform/stat_linux.go b/internal/platform/stat_linux.go index 55487192..03e94555 100644 --- a/internal/platform/stat_linux.go +++ b/internal/platform/stat_linux.go @@ -40,8 +40,10 @@ func inoFromFileInfo(_ readdirFile, t fs.FileInfo) (ino uint64, err error) { func fillStatFromFileInfo(st *Stat_t, t fs.FileInfo) { if d, ok := t.Sys().(*syscall.Stat_t); ok { - st.Ino = uint64(d.Ino) st.Dev = uint64(d.Dev) + st.Ino = uint64(d.Ino) + st.Uid = d.Uid + st.Gid = d.Gid st.Mode = t.Mode() st.Nlink = uint64(d.Nlink) st.Size = d.Size diff --git a/internal/platform/stat_test.go b/internal/platform/stat_test.go index 731985c8..57e016d0 100644 --- a/internal/platform/stat_test.go +++ b/internal/platform/stat_test.go @@ -315,7 +315,8 @@ func TestStatFile_dev_inode(t *testing.T) { } func requireDirectoryDevIno(t *testing.T, st Stat_t) { - // windows before go 1.20 has trouble reading the inode information on directories. + // windows before go 1.20 has trouble reading the inode information on + // directories. if runtime.GOOS != "windows" || IsGo120 { require.NotEqual(t, uint64(0), st.Dev) require.NotEqual(t, uint64(0), st.Ino) @@ -324,3 +325,57 @@ func requireDirectoryDevIno(t *testing.T, st Stat_t) { require.Zero(t, st.Ino) } } + +// TestStat_uid_gid is similar to os.TestChown +func TestStat_uid_gid(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows") + } + + // We don't attempt changing the uid of a file, as only root can do that. + // Also, this isn't a test of chown. The main goal here is to read-back + // the uid, gid, both of which are zero if run as root. + uid := uint32(os.Getuid()) + gid := uint32(os.Getgid()) + + t.Run("Stat", func(t *testing.T) { + tmpDir := t.TempDir() + dir := path.Join(tmpDir, "dir") + require.NoError(t, os.Mkdir(dir, 0o0700)) + require.NoError(t, chgid(dir, gid)) + + var st Stat_t + require.NoError(t, Stat(dir, &st)) + require.Equal(t, uid, st.Uid) + require.Equal(t, gid, st.Gid) + }) + + t.Run("LStat", func(t *testing.T) { + tmpDir := t.TempDir() + link := path.Join(tmpDir, "link") + require.NoError(t, os.Symlink(tmpDir, link)) + require.NoError(t, chgid(link, gid)) + + var st Stat_t + require.NoError(t, Lstat(link, &st)) + require.Equal(t, uid, st.Uid) + require.Equal(t, gid, st.Gid) + }) + + t.Run("StatFile", func(t *testing.T) { + tmpDir := t.TempDir() + file := path.Join(tmpDir, "file") + require.NoError(t, os.WriteFile(file, nil, 0o0600)) + require.NoError(t, chgid(file, gid)) + + var st Stat_t + require.NoError(t, Lstat(file, &st)) + require.Equal(t, uid, st.Uid) + require.Equal(t, gid, st.Gid) + }) +} + +func chgid(path string, gid uint32) error { + // Note: In Chown, -1 is means leave the uid alone + return Chown(path, -1, int(gid)) +}