From a52f7038e5a607dd0901244abb791c96deaf9c2d Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 29 Oct 2025 04:09:42 +0900 Subject: [PATCH] internal/env: relocate from app This package is much cleaner to stub independently, and makes no sense to lump into app. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 3 +- cmd/hakurei/parse.go | 3 +- cmd/hakurei/print.go | 3 +- internal/app/env.go | 59 -------------------------- internal/app/outcome.go | 7 ++-- internal/app/outcome_test.go | 9 ++-- internal/app/shim_test.go | 3 +- internal/env/env.go | 66 +++++++++++++++++++++++++++++ internal/{app => env}/env_test.go | 69 +++++++++++-------------------- 9 files changed, 108 insertions(+), 114 deletions(-) delete mode 100644 internal/app/env.go create mode 100644 internal/env/env.go rename internal/{app => env}/env_test.go (57%) diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 53db753..968bb56 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -20,6 +20,7 @@ import ( "hakurei.app/internal" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/internal/env" "hakurei.app/message" "hakurei.app/system/dbus" ) @@ -320,7 +321,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr var flagShort bool c.NewCommand("ps", "List active instances", func(args []string) error { var sc hst.Paths - app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) + env.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath), 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 13f4d37..7041975 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -13,6 +13,7 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/internal/env" "hakurei.app/message" ) @@ -83,7 +84,7 @@ func shortIdentifierString(s string) string { func tryIdentifier(msg message.Msg, name string) (config *hst.Config, entry *hst.State) { return tryIdentifierEntries(msg, name, func() map[hst.ID]*hst.State { var sc hst.Paths - app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) + env.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil)) s := state.NewMulti(msg, sc.RunDirPath) if entries, err := state.Join(s); err != nil { msg.GetLogger().Printf("cannot join store: %v", err) // not fatal diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 06c8b2a..0dcb59e 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -14,6 +14,7 @@ import ( "hakurei.app/internal" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/internal/env" "hakurei.app/message" ) @@ -23,7 +24,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) { defer t.MustFlush() info := &hst.Info{Version: internal.Version(), User: new(app.Hsu).MustID(nil)} - app.CopyPaths().Copy(&info.Paths, info.User) + env.CopyPaths().Copy(&info.Paths, info.User) if flagJSON { encodeJSON(log.Fatal, output, short, info) diff --git a/internal/app/env.go b/internal/app/env.go deleted file mode 100644 index e6d137f..0000000 --- a/internal/app/env.go +++ /dev/null @@ -1,59 +0,0 @@ -package app - -import ( - "strconv" - - "hakurei.app/container/check" - "hakurei.app/hst" -) - -// EnvPaths holds paths copied from the environment and is used to create [hst.Paths]. -type EnvPaths struct { - // TempDir is returned by [os.TempDir]. - TempDir *check.Absolute - // RuntimePath is copied from $XDG_RUNTIME_DIR. - RuntimePath *check.Absolute -} - -// 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.TempDir = env.TempDir - v.SharePath = env.TempDir.Append("hakurei." + strconv.Itoa(userid)) - - 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 = 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 := check.NewAbs(k.tempdir()); err != nil { - k.fatalf("invalid TMPDIR: %v", err) - panic("unreachable") - } else { - env.TempDir = tempDir - } - - r, _ := k.lookupEnv(xdgRuntimeDir) - if a, err := check.NewAbs(r); err == nil { - env.RuntimePath = a - } - - return &env -} diff --git a/internal/app/outcome.go b/internal/app/outcome.go index 237095a..59cdfbd 100644 --- a/internal/app/outcome.go +++ b/internal/app/outcome.go @@ -8,6 +8,7 @@ import ( "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/hst" + "hakurei.app/internal/env" "hakurei.app/message" "hakurei.app/system" "hakurei.app/system/acl" @@ -58,7 +59,7 @@ type outcomeState struct { // Copied from [EnvPaths] per-process. sc hst.Paths - *EnvPaths + *env.Paths // Copied via populateLocal. k syscallDispatcher @@ -72,7 +73,7 @@ func (s *outcomeState) valid() bool { s.Shim.valid() && s.ID != nil && s.Container != nil && - s.EnvPaths != nil + s.Paths != nil } // newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher. @@ -82,7 +83,7 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h ID: id, Identity: config.Identity, UserID: hsu.MustID(msg), - EnvPaths: copyPaths(k), + Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }), Container: config.Container, } diff --git a/internal/app/outcome_test.go b/internal/app/outcome_test.go index 740e7db..5ea3ff9 100644 --- a/internal/app/outcome_test.go +++ b/internal/app/outcome_test.go @@ -4,6 +4,7 @@ import ( "testing" "hakurei.app/hst" + "hakurei.app/internal/env" ) func TestOutcomeStateValid(t *testing.T) { @@ -16,11 +17,11 @@ func TestOutcomeStateValid(t *testing.T) { }{ {"nil", nil, false}, {"zero", new(outcomeState), false}, - {"shim", &outcomeState{Shim: &shimParams{PrivPID: -1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), EnvPaths: new(EnvPaths)}, false}, - {"id", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), EnvPaths: new(EnvPaths)}, false}, - {"container", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), EnvPaths: new(EnvPaths)}, false}, + {"shim", &outcomeState{Shim: &shimParams{PrivPID: -1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false}, + {"id", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false}, + {"container", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Paths: new(env.Paths)}, false}, {"envpaths", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig)}, false}, - {"valid", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig), EnvPaths: new(EnvPaths)}, true}, + {"valid", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/app/shim_test.go b/internal/app/shim_test.go index e207dfd..19dedd6 100644 --- a/internal/app/shim_test.go +++ b/internal/app/shim_test.go @@ -14,6 +14,7 @@ import ( "hakurei.app/container/seccomp" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/env" ) func TestShimEntrypoint(t *testing.T) { @@ -128,7 +129,7 @@ func TestShimEntrypoint(t *testing.T) { Container: hst.Template().Container, Mapuid: 1000, Mapgid: 100, - EnvPaths: &EnvPaths{TempDir: fhs.AbsTmp, RuntimePath: fhs.AbsRunUser.Append("1000")}, + Paths: &env.Paths{TempDir: fhs.AbsTmp, RuntimePath: fhs.AbsRunUser.Append("1000")}, }, nil}, nil, nil), call("swapVerbose", stub.ExpectArgs{true}, false, nil), call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), diff --git a/internal/env/env.go b/internal/env/env.go new file mode 100644 index 0000000..ebb816d --- /dev/null +++ b/internal/env/env.go @@ -0,0 +1,66 @@ +// Package env provides the [Paths] struct for efficiently building paths from the environment. +package env + +import ( + "log" + "os" + "strconv" + + "hakurei.app/container/check" + "hakurei.app/hst" +) + +// Paths holds paths copied from the environment and is used to create [hst.Paths]. +type Paths struct { + // TempDir is returned by [os.TempDir]. + TempDir *check.Absolute + // RuntimePath is copied from $XDG_RUNTIME_DIR. + RuntimePath *check.Absolute +} + +// Copy expands [Paths] into [hst.Paths]. +func (env *Paths) Copy(v *hst.Paths, userid int) { + if env == nil || env.TempDir == nil || v == nil { + panic("attempting to use an invalid Paths") + } + + v.TempDir = env.TempDir + v.SharePath = env.TempDir.Append("hakurei." + strconv.Itoa(userid)) + + 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 = env.RuntimePath + v.RunDirPath = env.RuntimePath.Append("hakurei") + } +} + +// CopyPaths returns a populated [Paths]. +func CopyPaths() *Paths { return CopyPathsFunc(log.Fatalf, os.TempDir, os.Getenv) } + +// CopyPathsFunc returns a populated [Paths], +// using the provided [log.Fatalf], [os.TempDir], [os.Getenv] functions. +func CopyPathsFunc( + fatalf func(format string, v ...any), + tempdir func() string, + getenv func(key string) string, +) *Paths { + const xdgRuntimeDir = "XDG_RUNTIME_DIR" + + var env Paths + + if tempDir, err := check.NewAbs(tempdir()); err != nil { + fatalf("invalid TMPDIR: %v", err) + panic("unreachable") + } else { + env.TempDir = tempDir + } + + if a, err := check.NewAbs(getenv(xdgRuntimeDir)); err == nil { + env.RuntimePath = a + } + + return &env +} diff --git a/internal/app/env_test.go b/internal/env/env_test.go similarity index 57% rename from internal/app/env_test.go rename to internal/env/env_test.go index 9e2c959..8a07d6b 100644 --- a/internal/app/env_test.go +++ b/internal/env/env_test.go @@ -1,4 +1,4 @@ -package app +package env_test import ( "fmt" @@ -10,26 +10,27 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/env" ) -func TestEnvPaths(t *testing.T) { +func TestPaths(t *testing.T) { t.Parallel() testCases := []struct { name string - env *EnvPaths + env *env.Paths 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", nil, hst.Paths{}, "attempting to use an invalid Paths"}, + {"zero", new(env.Paths), hst.Paths{}, "attempting to use an invalid Paths"}, - {"nil tempdir", &EnvPaths{ + {"nil tempdir", &env.Paths{ RuntimePath: fhs.AbsTmp, - }, hst.Paths{}, "attempting to use an invalid EnvPaths"}, + }, hst.Paths{}, "attempting to use an invalid Paths"}, - {"nil runtime", &EnvPaths{ + {"nil runtime", &env.Paths{ TempDir: fhs.AbsTmp, }, hst.Paths{ TempDir: fhs.AbsTmp, @@ -38,7 +39,7 @@ func TestEnvPaths(t *testing.T) { RunDirPath: fhs.AbsTmp.Append("hakurei.3735928559/run"), }, ""}, - {"full", &EnvPaths{ + {"full", &env.Paths{ TempDir: fhs.AbsTmp, RuntimePath: fhs.AbsRunUser.Append("1000"), }, hst.Paths{ @@ -76,16 +77,16 @@ func TestCopyPaths(t *testing.T) { env map[string]string tmp string fatal string - want EnvPaths + want env.Paths }{ {"invalid tempdir", nil, "\x00", - "invalid TMPDIR: path \"\\x00\" is not absolute", EnvPaths{}}, + "invalid TMPDIR: path \"\\x00\" is not absolute", env.Paths{}}, {"empty environment", make(map[string]string), container.Nonexistent, - "", EnvPaths{TempDir: check.MustAbs(container.Nonexistent)}}, + "", env.Paths{TempDir: check.MustAbs(container.Nonexistent)}}, {"invalid XDG_RUNTIME_DIR", map[string]string{"XDG_RUNTIME_DIR": "\x00"}, container.Nonexistent, - "", EnvPaths{TempDir: check.MustAbs(container.Nonexistent)}}, + "", env.Paths{TempDir: check.MustAbs(container.Nonexistent)}}, {"full", map[string]string{"XDG_RUNTIME_DIR": "/\x00"}, container.Nonexistent, - "", EnvPaths{TempDir: check.MustAbs(container.Nonexistent), RuntimePath: check.MustAbs("/\x00")}}, + "", env.Paths{TempDir: check.MustAbs(container.Nonexistent), RuntimePath: check.MustAbs("/\x00")}}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -94,8 +95,16 @@ func TestCopyPaths(t *testing.T) { defer stub.HandleExit(t) } - k := copyPathsDispatcher{t: t, env: tc.env, tmp: tc.tmp, expectsFatal: tc.fatal} - got := copyPaths(k) + got := env.CopyPathsFunc(func(format string, v ...any) { + if tc.fatal == "" { + t.Fatalf("unexpected call to fatalf: format = %q, v = %#v", format, v) + } + + if got := fmt.Sprintf(format, v...); got != tc.fatal { + t.Fatalf("fatalf: %q, want %q", got, tc.fatal) + } + panic(stub.PanicExit) + }, func() string { return tc.tmp }, func(key string) string { return tc.env[key] }) if tc.fatal != "" { t.Fatalf("copyPaths: expected fatal %q", tc.fatal) @@ -107,31 +116,3 @@ func TestCopyPaths(t *testing.T) { }) } } - -// 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) -}