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) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 23:04:40 +01:00
parent c651b00d08
commit 3bea73f857
2 changed files with 59 additions and 1 deletions
+16 -1
View File
@@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@@ -77,10 +78,24 @@ func (s *Store) migrate() error {
func (s *Store) Close() error { return s.db.Close() } 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. // DefaultDBPath resolves EMCLI_DB or the per-OS default location.
func DefaultDBPath() (string, error) { func DefaultDBPath() (string, error) {
if p := os.Getenv("EMCLI_DB"); p != "" { if p := os.Getenv("EMCLI_DB"); p != "" {
return p, nil return expandUserHome(p), nil
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if dir := os.Getenv("AppData"); dir != "" { if dir := os.Getenv("AppData"); dir != "" {
+43
View File
@@ -2,10 +2,53 @@ package store
import ( import (
"database/sql" "database/sql"
"os"
"path/filepath" "path/filepath"
"strings"
"testing" "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 // 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. // account tests (which do crypto) work without needing their own setup.
func openTemp(t *testing.T) *Store { func openTemp(t *testing.T) *Store {