Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ To save to a remote pod on the LocalStack platform, use the pod: prefix:
destArg = args[0]
}

dest, err := snapshot.ParseDestination(destArg, time.Now())
home, _ := os.UserHomeDir()
dest, err := snapshot.ParseDestination(destArg, home, time.Now())
if err != nil {
return err
}
Expand Down
11 changes: 7 additions & 4 deletions internal/snapshot/destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"time"
)

// ErrHomeNotSet is returned when a destination needs "~" expansion but no home directory was provided.
var ErrHomeNotSet = errors.New("home directory is not set")

var (
// ErrRemoteNotSupported is returned for known remote schemes (s3://, oras://).
ErrRemoteNotSupported = errors.New("remote destinations are not yet supported — coming soon")
Expand Down Expand Up @@ -53,7 +56,8 @@ func displayPath(abs, cwd, home string) string {
}

// ParseDestination resolves a user-supplied destination to a local path (KindLocal) or validated pod name (KindPod).
func ParseDestination(dest string, now time.Time) (Destination, error) {
// home is used to expand a leading "~" or "~/"; pass "" to disable tilde expansion.
func ParseDestination(dest, home string, now time.Time) (Destination, error) {
if dest == "" {
b := make([]byte, 2)
_, _ = rand.Read(b)
Expand All @@ -80,9 +84,8 @@ func ParseDestination(dest string, now time.Time) (Destination, error) {
}

if dest == "~" || strings.HasPrefix(dest, "~/") || strings.HasPrefix(dest, `~\`) {
home, err := os.UserHomeDir()
if err != nil {
return Destination{}, fmt.Errorf("resolve home directory: %w", err)
if home == "" {
return Destination{}, fmt.Errorf("cannot expand %q: %w", dest, ErrHomeNotSet)
}
dest = filepath.Join(home, strings.TrimLeft(dest[1:], `/\`))
}
Expand Down
28 changes: 25 additions & 3 deletions internal/snapshot/destination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ func TestParseDestination(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
require.NoError(t, err)
home, err := os.UserHomeDir()
require.NoError(t, err)
// Use a temp dir as home so the test doesn't depend on the real $HOME
// (e.g. under Nix's sandboxed build, $HOME is a non-existent placeholder).
home := t.TempDir()

now := time.Date(2026, 5, 11, 21, 4, 32, 0, time.UTC)

Expand Down Expand Up @@ -274,7 +275,7 @@ func TestParseDestination(t *testing.T) {
}
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := snapshot.ParseDestination(tc.input, now)
got, err := snapshot.ParseDestination(tc.input, home, now)
if tc.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
Expand All @@ -300,3 +301,24 @@ func TestParseDestination(t *testing.T) {
})
}
}

// TestParseDestinationTildeWithoutHome covers the Nix sandbox scenario where
// the build runs without a usable home directory. Tilde expansion must fail
// with a clear error instead of silently using a non-existent path.
func TestParseDestinationTildeWithoutHome(t *testing.T) {
t.Parallel()

now := time.Date(2026, 5, 11, 21, 4, 32, 0, time.UTC)

tests := []struct{ name, input string }{
{"bare tilde", "~"},
{"tilde slash", "~/snap"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := snapshot.ParseDestination(tc.input, "", now)
require.ErrorIs(t, err, snapshot.ErrHomeNotSet)
})
}
}
Loading