diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index b9da60b..ddd07ba 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -231,7 +231,7 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE var flagShort bool c.NewCommand("ps", "List active instances", func(args []string) error { var sc hst.Paths - app.CopyPaths(&sc, new(app.Hsu).MustID()) + app.CopyPaths().Copy(&sc, new(app.Hsu).MustID()) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON) return errSuccess }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index eee7d32..14b9c16 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -89,7 +89,7 @@ func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state. msg.Verbose("argument looks like prefix") var sc hst.Paths - app.CopyPaths(&sc, new(app.Hsu).MustID()) + app.CopyPaths().Copy(&sc, new(app.Hsu).MustID()) s := state.NewMulti(msg, sc.RunDirPath.String()) if entries, err := state.Join(s); err != nil { log.Printf("cannot join store: %v", err) diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 938517b..eefe134 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -22,7 +22,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) { defer t.MustFlush() info := &hst.Info{User: new(app.Hsu).MustID()} - app.CopyPaths(&info.Paths, info.User) + app.CopyPaths().Copy(&info.Paths, info.User) if flagJSON { printJSON(output, short, info) diff --git a/internal/app/dispatcher_test.go b/internal/app/dispatcher_test.go new file mode 100644 index 0000000..2c7d926 --- /dev/null +++ b/internal/app/dispatcher_test.go @@ -0,0 +1,26 @@ +package app + +import ( + "os" + "os/exec" + + "hakurei.app/container" +) + +type panicDispatcher struct{} + +func (panicDispatcher) new(func(k syscallDispatcher)) { panic("unreachable") } +func (panicDispatcher) getuid() int { panic("unreachable") } +func (panicDispatcher) getgid() int { panic("unreachable") } +func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") } +func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") } +func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") } +func (panicDispatcher) tempdir() string { panic("unreachable") } +func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") } +func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") } +func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") } +func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") } +func (panicDispatcher) overflowUid(container.Msg) int { panic("unreachable") } +func (panicDispatcher) overflowGid(container.Msg) int { panic("unreachable") } +func (panicDispatcher) mustHsuPath() *container.Absolute { panic("unreachable") } +func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") } diff --git a/internal/app/finalise.go b/internal/app/finalise.go index 708df05..fbe4bb3 100644 --- a/internal/app/finalise.go +++ b/internal/app/finalise.go @@ -275,7 +275,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C // TODO(ophestra): revert this after params to shim share := &shareHost{seal: k} - copyPaths(k.syscallDispatcher, &share.sc, hsu.MustIDMsg(msg)) + copyPaths(k.syscallDispatcher).Copy(&share.sc, hsu.MustIDMsg(msg)) msg.Verbosef("process share directory at %q, runtime directory at %q", share.sc.SharePath, share.sc.RunDirPath) var mapuid, mapgid *stringPair[int] diff --git a/internal/app/paths.go b/internal/app/paths.go index 267d0ea..4305c66 100644 --- a/internal/app/paths.go +++ b/internal/app/paths.go @@ -7,28 +7,53 @@ import ( "hakurei.app/hst" ) -// CopyPaths populates a [hst.Paths] struct. -func CopyPaths(v *hst.Paths, userid int) { copyPaths(direct{}, v, userid) } +// EnvPaths holds paths copied from the environment and is used to create [hst.Paths]. +type EnvPaths struct { + // TempDir is returned by [os.TempDir]. + TempDir *container.Absolute + // RuntimePath is copied from $XDG_RUNTIME_DIR. + RuntimePath *container.Absolute +} -// copyPaths populates a [hst.Paths] struct. -func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) { - const xdgRuntimeDir = "XDG_RUNTIME_DIR" - - if tempDir, err := container.NewAbs(k.tempdir()); err != nil { - k.fatalf("invalid TMPDIR: %v", err) - } else { - v.TempDir = tempDir +// Copy expands [EnvPaths] into [hst.Paths]. +func (env *EnvPaths) Copy(v *hst.Paths, userid int) { + if env == nil || env.TempDir == nil || v == nil { + panic("attempting to use an invalid EnvPaths") } - v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid)) + v.TempDir = env.TempDir + v.SharePath = env.TempDir.Append("hakurei." + strconv.Itoa(userid)) - r, _ := k.lookupEnv(xdgRuntimeDir) - if a, err := container.NewAbs(r); err != nil { + if env.RuntimePath == nil { // fall back to path in share since hakurei has no hard XDG dependency v.RunDirPath = v.SharePath.Append("run") v.RuntimePath = v.RunDirPath.Append("compat") } else { - v.RuntimePath = a - v.RunDirPath = v.RuntimePath.Append("hakurei") + v.RuntimePath = env.RuntimePath + v.RunDirPath = env.RuntimePath.Append("hakurei") } } + +// CopyPaths returns a populated [EnvPaths]. +func CopyPaths() *EnvPaths { return copyPaths(direct{}) } + +// copyPaths returns a populated [EnvPaths]. +func copyPaths(k syscallDispatcher) *EnvPaths { + const xdgRuntimeDir = "XDG_RUNTIME_DIR" + + var env EnvPaths + + if tempDir, err := container.NewAbs(k.tempdir()); err != nil { + k.fatalf("invalid TMPDIR: %v", err) + panic("unreachable") + } else { + env.TempDir = tempDir + } + + r, _ := k.lookupEnv(xdgRuntimeDir) + if a, err := container.NewAbs(r); err == nil { + env.RuntimePath = a + } + + return &env +} diff --git a/internal/app/paths_test.go b/internal/app/paths_test.go new file mode 100644 index 0000000..04e2134 --- /dev/null +++ b/internal/app/paths_test.go @@ -0,0 +1,129 @@ +package app + +import ( + "fmt" + "reflect" + "testing" + + "hakurei.app/container" + "hakurei.app/container/stub" + "hakurei.app/hst" +) + +func TestEnvPaths(t *testing.T) { + testCases := []struct { + name string + env *EnvPaths + want hst.Paths + + wantPanic string + }{ + {"nil", nil, hst.Paths{}, "attempting to use an invalid EnvPaths"}, + {"zero", new(EnvPaths), hst.Paths{}, "attempting to use an invalid EnvPaths"}, + + {"nil tempdir", &EnvPaths{ + RuntimePath: container.AbsFHSTmp, + }, hst.Paths{}, "attempting to use an invalid EnvPaths"}, + + {"nil runtime", &EnvPaths{ + TempDir: container.AbsFHSTmp, + }, hst.Paths{ + TempDir: container.AbsFHSTmp, + SharePath: container.AbsFHSTmp.Append("hakurei.3735928559"), + RuntimePath: container.AbsFHSTmp.Append("hakurei.3735928559/run/compat"), + RunDirPath: container.AbsFHSTmp.Append("hakurei.3735928559/run"), + }, ""}, + + {"full", &EnvPaths{ + TempDir: container.AbsFHSTmp, + RuntimePath: container.AbsFHSRunUser.Append("1000"), + }, hst.Paths{ + TempDir: container.AbsFHSTmp, + SharePath: container.AbsFHSTmp.Append("hakurei.3735928559"), + RuntimePath: container.AbsFHSRunUser.Append("1000"), + RunDirPath: container.AbsFHSRunUser.Append("1000/hakurei"), + }, ""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.wantPanic != "" { + defer func() { + if r := recover(); r != tc.wantPanic { + t.Errorf("Copy: panic = %#v, want %q", r, tc.wantPanic) + } + }() + } + + var sc hst.Paths + tc.env.Copy(&sc, 0xdeadbeef) + if !reflect.DeepEqual(&sc, &tc.want) { + t.Errorf("Copy: %#v, want %#v", sc, tc.want) + } + }) + } +} + +func TestCopyPaths(t *testing.T) { + testCases := []struct { + name string + env map[string]string + tmp string + fatal string + want EnvPaths + }{ + {"invalid tempdir", nil, "\x00", + "invalid TMPDIR: path \"\\x00\" is not absolute", EnvPaths{}}, + {"empty environment", make(map[string]string), container.Nonexistent, + "", EnvPaths{TempDir: container.MustAbs(container.Nonexistent)}}, + {"invalid XDG_RUNTIME_DIR", map[string]string{"XDG_RUNTIME_DIR": "\x00"}, container.Nonexistent, + "", EnvPaths{TempDir: container.MustAbs(container.Nonexistent)}}, + {"full", map[string]string{"XDG_RUNTIME_DIR": "/\x00"}, container.Nonexistent, + "", EnvPaths{TempDir: container.MustAbs(container.Nonexistent), RuntimePath: container.MustAbs("/\x00")}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.fatal != "" { + defer stub.HandleExit(t) + } + + k := copyPathsDispatcher{t: t, env: tc.env, tmp: tc.tmp, expectsFatal: tc.fatal} + got := copyPaths(k) + + if tc.fatal != "" { + t.Fatalf("copyPaths: expected fatal %q", tc.fatal) + } + + if !reflect.DeepEqual(got, &tc.want) { + t.Errorf("copyPaths: %#v, want %#v", got, &tc.want) + } + }) + } +} + +// copyPathsDispatcher implements enough of syscallDispatcher for all copyPaths code paths. +type copyPathsDispatcher struct { + env map[string]string + tmp string + + // must be checked at the conclusion of the test + expectsFatal string + + t *testing.T + panicDispatcher +} + +func (k copyPathsDispatcher) tempdir() string { return k.tmp } +func (k copyPathsDispatcher) lookupEnv(key string) (value string, ok bool) { + value, ok = k.env[key] + return +} +func (k copyPathsDispatcher) fatalf(format string, v ...any) { + if k.expectsFatal == "" { + k.t.Fatalf("unexpected call to fatalf: format = %q, v = %#v", format, v) + } + + if got := fmt.Sprintf(format, v...); got != k.expectsFatal { + k.t.Fatalf("fatalf: %q, want %q", got, k.expectsFatal) + } + panic(stub.PanicExit) +}