From 3bea73f8574a1c064a8f9e264387af46db5ad5e7 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Tue, 23 Jun 2026 23:04:40 +0100 Subject: [PATCH] fix(store): expand a leading ~ in EMCLI_DB A literal "~/..." in EMCLI_DB has no shell to expand it, so SQLite opened it relative to the cwd and silently created a stray "~" directory tree. Expand a leading "~" or "~/" to the user's home dir; "~user", mid-path tildes, and absolute/relative paths are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/store/store.go | 17 +++++++++++++- internal/store/store_test.go | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/store/store.go b/internal/store/store.go index 9b3d695..c232e5b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -8,6 +8,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" _ "modernc.org/sqlite" ) @@ -77,10 +78,24 @@ func (s *Store) migrate() error { func (s *Store) Close() error { return s.db.Close() } +// expandUserHome replaces a leading "~" or "~/" in p with the user's home +// directory. Only a leading tilde is expanded (the usual shell convention) — +// "~user" and a tilde elsewhere in the path are left untouched. This guards +// against an EMCLI_DB set to a literal "~/..." (no shell to expand it), which +// would otherwise be opened relative to the cwd and create a stray "~" dir. +func expandUserHome(p string) string { + if p == "~" || strings.HasPrefix(p, "~/") { + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, strings.TrimPrefix(p[1:], "/")) + } + } + return p +} + // DefaultDBPath resolves EMCLI_DB or the per-OS default location. func DefaultDBPath() (string, error) { if p := os.Getenv("EMCLI_DB"); p != "" { - return p, nil + return expandUserHome(p), nil } if runtime.GOOS == "windows" { if dir := os.Getenv("AppData"); dir != "" { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 5ea3da5..09c57a6 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -2,10 +2,53 @@ package store import ( "database/sql" + "os" "path/filepath" + "strings" "testing" ) +// A leading "~" in EMCLI_DB must be expanded to the home dir, so a literal +// tilde (no shell to expand it) can't be opened relative to the cwd and +// silently create a stray "~" directory. +func TestDefaultDBPathExpandsLeadingTilde(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home dir: %v", err) + } + cases := map[string]string{ + "~/.config/emcli/emcli.db": filepath.Join(home, ".config", "emcli", "emcli.db"), + "~": home, + } + for in, want := range cases { + t.Setenv("EMCLI_DB", in) + got, err := DefaultDBPath() + if err != nil { + t.Fatalf("DefaultDBPath(%q): %v", in, err) + } + if got != want { + t.Fatalf("EMCLI_DB=%q -> %q, want %q", in, got, want) + } + if strings.Contains(got, "~") { + t.Fatalf("EMCLI_DB=%q left a literal tilde: %q", in, got) + } + } +} + +// A non-leading tilde or "~user" is NOT a path we should rewrite — leave it be. +func TestDefaultDBPathLeavesOtherPathsUntouched(t *testing.T) { + for _, p := range []string{"/var/lib/emcli.db", "./rel/emcli.db", "~user/db"} { + t.Setenv("EMCLI_DB", p) + got, err := DefaultDBPath() + if err != nil { + t.Fatalf("DefaultDBPath(%q): %v", p, err) + } + if got != p { + t.Fatalf("EMCLI_DB=%q was rewritten to %q", p, got) + } + } +} + // openTemp opens a fresh store in a temp dir and initialises keys so that // account tests (which do crypto) work without needing their own setup. func openTemp(t *testing.T) *Store {