1package tui
2
3import (
4 "net/url"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10)
11
12// makeFiles creates files (and intermediate dirs) under base for each
13// relative path in rels. Returns base for convenience.
14func makeFiles(t *testing.T, base string, rels ...string) string {
15 t.Helper()
16 for _, rel := range rels {
17 full := filepath.Join(base, rel)
18 if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
19 t.Fatalf("mkdir: %v", err)
20 }
21 if err := os.WriteFile(full, []byte("x"), 0o644); err != nil {
22 t.Fatalf("write: %v", err)
23 }
24 }
25 return base
26}
27
28func TestQuotePastedFilePaths(t *testing.T) {
29 // normalisePathToken's heuristics target Unix-shaped paths
30 // (leading "/", "~", or "file://" with a "/" path component) —
31 // the same shapes drag-and-drop produces on macOS and Linux.
32 // Windows TempDir paths look like C:\Users\... which never
33 // match those prefixes, so the test fixtures wouldn't exercise
34 // any real code path on that platform.
35 if runtime.GOOS == "windows" {
36 t.Skip("path-quoting heuristics are unix-shaped")
37 }
38 dir := t.TempDir()
39 makeFiles(t, dir,
40 "foo bar.png",
41 "file.png",
42 "a.png",
43 "b.png",
44 "x.png",
45 "it's.png",
46 "only.png",
47 )
48
49 // file:// URL form needs URL-encoded spaces.
50 fileURL := "file://" + dir + "/" + url.PathEscape("foo bar.png")
51 // PathEscape doesn't escape "/" but escapes the space — that's
52 // what macOS Finder produces. Build the path-only segment so the
53 // "file://" prefix is added cleanly.
54 fileURLPath := dir + "/" + strings.ReplaceAll("foo bar.png", " ", "%20")
55 fileURLForm := "file://" + fileURLPath
56 _ = fileURL // keep linter happy if unused
57
58 cases := []struct {
59 name string
60 in string
61 want string
62 }{
63 // macOS Terminal default: backslash-escaped spaces in path.
64 {"backslash-escaped space", dir + `/foo\ bar.png`, `'` + dir + `/foo bar.png'`},
65
66 // Plain absolute path with no special chars.
67 {"plain absolute", dir + `/file.png`, `'` + dir + `/file.png'`},
68
69 // file:// URL form; URL-decoded and scheme stripped.
70 {"file url", fileURLForm, `'` + dir + `/foo bar.png'`},
71
72 // Multi-file drop, each path quoted independently.
73 {"two paths", dir + `/a.png ` + dir + `/b.png`, `'` + dir + `/a.png' '` + dir + `/b.png'`},
74
75 // Already single-quoted: re-normalise to consistent quoting.
76 {"already single quoted", `'` + dir + `/x.png'`, `'` + dir + `/x.png'`},
77
78 // Already double-quoted: re-normalise.
79 {"already double quoted", `"` + dir + `/x.png"`, `'` + dir + `/x.png'`},
80
81 // Embedded apostrophe gets the standard '\'' splice escape.
82 {"embedded apostrophe", dir + `/it's.png`, `'` + dir + `/it'\''s.png'`},
83
84 // Plain prose left alone.
85 {"prose", `hello world`, `hello world`},
86
87 // Multi-line paste left alone (typical code paste).
88 {"multiline", "foo\nbar", "foo\nbar"},
89
90 // Anything containing shell metacharacters is left alone so a
91 // crafted "drop" can't smuggle a command.
92 {"metachar", `/foo;rm -rf /`, `/foo;rm -rf /`},
93
94 // Mixed: valid path token quoted, surrounding words preserved.
95 {"path mixed with prose", dir + `/only.png is good`, `'` + dir + `/only.png' is good`},
96
97 // URL path segment that happens to start with "/" but doesn't
98 // exist on disk: must NOT be quoted or chip-collapsed. This
99 // is the regression that motivated the existence check.
100 {"url path segment", `/de/downloads/dokumentenarchiv`, `/de/downloads/dokumentenarchiv`},
101
102 // Non-existent path that looks like a real one: also untouched.
103 {"non-existent absolute", `/this/does/not/exist.png`, `/this/does/not/exist.png`},
104 }
105 for _, c := range cases {
106 t.Run(c.name, func(t *testing.T) {
107 got := quotePastedFilePaths(c.in)
108 if got != c.want {
109 t.Errorf("input %q\n want %q\n got %q", c.in, c.want, got)
110 }
111 })
112 }
113}
114
115// TestTildePathExists checks that ~ expansion in pathExists works
116// when the home directory contains the candidate. We don't write
117// into the user's real home; instead, override HOME for the test.
118func TestTildePathExists(t *testing.T) {
119 if runtime.GOOS == "windows" {
120 t.Skip("path-quoting heuristics are unix-shaped")
121 }
122 tmp := t.TempDir()
123 makeFiles(t, tmp, "tilde-test.png")
124 t.Setenv("HOME", tmp)
125
126 in := `~/tilde-test.png`
127 want := `'~/tilde-test.png'`
128 got := quotePastedFilePaths(in)
129 if got != want {
130 t.Errorf("input %q\n want %q\n got %q", in, want, got)
131 }
132
133 // A tilde path that doesn't exist must be left alone.
134 in = `~/no-such-file.png`
135 got = quotePastedFilePaths(in)
136 if got != in {
137 t.Errorf("non-existent tilde path mutated: got %q want %q", got, in)
138 }
139}