tui: require pasted path candidates to exist on disk
normalisePathToken used a purely syntactic heuristic ("starts with
/ or ~ and contains / or .") to decide whether a pasted token was
a drag-dropped file path. URL path segments pasted from a browser
address bar (e.g. /de/downloads/dokumentenarchiv) matched that
shape and got collapsed to a [file:N:basename] chip or wrapped in
single quotes, which is never what the user meant.
Add a pathExists check (with ~ / ~/ expansion via os.UserHomeDir)
and gate normalisePathToken on it. Real drag-and-drop from a file
manager still works because those paths exist; URL fragments and
fictional paths fall through untouched.
Rewrote quote_paste_test.go to build fixtures under t.TempDir() so
the existence check has something real to stat, plus regression
cases for URL-segment and non-existent-path pastes.- commit
- 902122ee7b3e88c18a85b8752dc8e78452aef316
- author
- patriceckhart · · 2026-04-27 08:59 UTC
- parent
- ec79818
- signature
- unsigned
2 files · +119 -12
M internal/tui/editor.go +37 -0
| @@ -308,6 +308,13 @@ func splitPreservingSeparators(s string) []pasteToken { | |||
| 308 | 308 | ||
| 309 | 309 | // normalisePathToken decides whether tk is a drag-dropped path and, | |
| 310 | 310 | // if so, returns the cleaned-up path string. | |
| 311 | + | // | |
| 312 | + | // To avoid mistaking URL path segments (e.g. "/de/downloads/foo" | |
| 313 | + | // pasted from a browser address bar) for filesystem paths, the | |
| 314 | + | // candidate must point to an entry that actually exists on disk. | |
| 315 | + | // Drag-and-drop from a file manager always satisfies this; a | |
| 316 | + | // hand-typed URL fragment doesn't, so it falls through to the | |
| 317 | + | // regular insert path untouched. | |
| 311 | 318 | func normalisePathToken(tk string) (string, bool) { | |
| 312 | 319 | // Strip pre-existing surrounding quotes; we'll re-quote consistently. | |
| 313 | 320 | if n := len(tk); n >= 2 { | |
| @@ -325,6 +332,9 @@ func normalisePathToken(tk string) (string, bool) { | |||
| 325 | 332 | if decoded == "" || decoded[0] != '/' { | |
| 326 | 333 | return "", false | |
| 327 | 334 | } | |
| 335 | + | if !pathExists(decoded) { | |
| 336 | + | return "", false | |
| 337 | + | } | |
| 328 | 338 | return decoded, true | |
| 329 | 339 | } | |
| 330 | 340 | ||
| @@ -341,9 +351,36 @@ func normalisePathToken(tk string) (string, bool) { | |||
| 341 | 351 | if strings.ContainsAny(unescaped, "|;&$`<>") { | |
| 342 | 352 | return "", false | |
| 343 | 353 | } | |
| 354 | + | if !pathExists(unescaped) { | |
| 355 | + | return "", false | |
| 356 | + | } | |
| 344 | 357 | return unescaped, true | |
| 345 | 358 | } | |
| 346 | 359 | ||
| 360 | + | // pathExists reports whether p resolves to an existing filesystem | |
| 361 | + | // entry (file, directory, or symlink target). "~" / "~/" prefixes | |
| 362 | + | // are expanded relative to the current user's home directory before | |
| 363 | + | // the check, mirroring how the agent will later read the path. | |
| 364 | + | func pathExists(p string) bool { | |
| 365 | + | if p == "" { | |
| 366 | + | return false | |
| 367 | + | } | |
| 368 | + | expanded := p | |
| 369 | + | if p == "~" || strings.HasPrefix(p, "~/") { | |
| 370 | + | home, err := os.UserHomeDir() | |
| 371 | + | if err != nil || home == "" { | |
| 372 | + | return false | |
| 373 | + | } | |
| 374 | + | if p == "~" { | |
| 375 | + | expanded = home | |
| 376 | + | } else { | |
| 377 | + | expanded = filepath.Join(home, p[2:]) | |
| 378 | + | } | |
| 379 | + | } | |
| 380 | + | _, err := os.Stat(expanded) | |
| 381 | + | return err == nil | |
| 382 | + | } | |
| 383 | + | ||
| 347 | 384 | // unescapeBackslashes turns "foo\ bar" into "foo bar". Terminal | |
| 348 | 385 | // drag-and-drop on macOS uses backslash escaping by default; quoting | |
| 349 | 386 | // makes that unnecessary. | |
| @@ -308,6 +308,13 @@ func splitPreservingSeparators(s string) []pasteToken { | |||||
| 308 | 308 | ||||
| 309 | // normalisePathToken decides whether tk is a drag-dropped path and, | 309 | // normalisePathToken decides whether tk is a drag-dropped path and, | ||
| 310 | // if so, returns the cleaned-up path string. | 310 | // if so, returns the cleaned-up path string. | ||
| 311 | + | // | |||
| 312 | + | // To avoid mistaking URL path segments (e.g. "/de/downloads/foo" | |||
| 313 | + | // pasted from a browser address bar) for filesystem paths, the | |||
| 314 | + | // candidate must point to an entry that actually exists on disk. | |||
| 315 | + | // Drag-and-drop from a file manager always satisfies this; a | |||
| 316 | + | // hand-typed URL fragment doesn't, so it falls through to the | |||
| 317 | + | // regular insert path untouched. | |||
| 311 | func normalisePathToken(tk string) (string, bool) { | 318 | func normalisePathToken(tk string) (string, bool) { | ||
| 312 | // Strip pre-existing surrounding quotes; we'll re-quote consistently. | 319 | // Strip pre-existing surrounding quotes; we'll re-quote consistently. | ||
| 313 | if n := len(tk); n >= 2 { | 320 | if n := len(tk); n >= 2 { | ||
| @@ -325,6 +332,9 @@ func normalisePathToken(tk string) (string, bool) { | |||||
| 325 | if decoded == "" || decoded[0] != '/' { | 332 | if decoded == "" || decoded[0] != '/' { | ||
| 326 | return "", false | 333 | return "", false | ||
| 327 | } | 334 | } | ||
| 335 | + | if !pathExists(decoded) { | |||
| 336 | + | return "", false | |||
| 337 | + | } | |||
| 328 | return decoded, true | 338 | return decoded, true | ||
| 329 | } | 339 | } | ||
| 330 | 340 | ||||
| @@ -341,9 +351,36 @@ func normalisePathToken(tk string) (string, bool) { | |||||
| 341 | if strings.ContainsAny(unescaped, "|;&$`<>") { | 351 | if strings.ContainsAny(unescaped, "|;&$`<>") { | ||
| 342 | return "", false | 352 | return "", false | ||
| 343 | } | 353 | } | ||
| 354 | + | if !pathExists(unescaped) { | |||
| 355 | + | return "", false | |||
| 356 | + | } | |||
| 344 | return unescaped, true | 357 | return unescaped, true | ||
| 345 | } | 358 | } | ||
| 346 | 359 | ||||
| 360 | + | // pathExists reports whether p resolves to an existing filesystem | |||
| 361 | + | // entry (file, directory, or symlink target). "~" / "~/" prefixes | |||
| 362 | + | // are expanded relative to the current user's home directory before | |||
| 363 | + | // the check, mirroring how the agent will later read the path. | |||
| 364 | + | func pathExists(p string) bool { | |||
| 365 | + | if p == "" { | |||
| 366 | + | return false | |||
| 367 | + | } | |||
| 368 | + | expanded := p | |||
| 369 | + | if p == "~" || strings.HasPrefix(p, "~/") { | |||
| 370 | + | home, err := os.UserHomeDir() | |||
| 371 | + | if err != nil || home == "" { | |||
| 372 | + | return false | |||
| 373 | + | } | |||
| 374 | + | if p == "~" { | |||
| 375 | + | expanded = home | |||
| 376 | + | } else { | |||
| 377 | + | expanded = filepath.Join(home, p[2:]) | |||
| 378 | + | } | |||
| 379 | + | } | |||
| 380 | + | _, err := os.Stat(expanded) | |||
| 381 | + | return err == nil | |||
| 382 | + | } | |||
| 383 | + | ||||
| 347 | // unescapeBackslashes turns "foo\ bar" into "foo bar". Terminal | 384 | // unescapeBackslashes turns "foo\ bar" into "foo bar". Terminal | ||
| 348 | // drag-and-drop on macOS uses backslash escaping by default; quoting | 385 | // drag-and-drop on macOS uses backslash escaping by default; quoting | ||
| 349 | // makes that unnecessary. | 386 | // makes that unnecessary. | ||
M internal/tui/quote_paste_test.go +82 -12
| @@ -1,36 +1,75 @@ | |||
| 1 | 1 | package tui | |
| 2 | 2 | ||
| 3 | - | import "testing" | |
| 3 | + | import ( | |
| 4 | + | "net/url" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // makeFiles creates files (and intermediate dirs) under base for each | |
| 12 | + | // relative path in rels. Returns base for convenience. | |
| 13 | + | func makeFiles(t *testing.T, base string, rels ...string) string { | |
| 14 | + | t.Helper() | |
| 15 | + | for _, rel := range rels { | |
| 16 | + | full := filepath.Join(base, rel) | |
| 17 | + | if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { | |
| 18 | + | t.Fatalf("mkdir: %v", err) | |
| 19 | + | } | |
| 20 | + | if err := os.WriteFile(full, []byte("x"), 0o644); err != nil { | |
| 21 | + | t.Fatalf("write: %v", err) | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | return base | |
| 25 | + | } | |
| 4 | 26 | ||
| 5 | 27 | func TestQuotePastedFilePaths(t *testing.T) { | |
| 28 | + | dir := t.TempDir() | |
| 29 | + | makeFiles(t, dir, | |
| 30 | + | "foo bar.png", | |
| 31 | + | "file.png", | |
| 32 | + | "a.png", | |
| 33 | + | "b.png", | |
| 34 | + | "x.png", | |
| 35 | + | "it's.png", | |
| 36 | + | "only.png", | |
| 37 | + | ) | |
| 38 | + | ||
| 39 | + | // file:// URL form needs URL-encoded spaces. | |
| 40 | + | fileURL := "file://" + dir + "/" + url.PathEscape("foo bar.png") | |
| 41 | + | // PathEscape doesn't escape "/" but escapes the space — that's | |
| 42 | + | // what macOS Finder produces. Build the path-only segment so the | |
| 43 | + | // "file://" prefix is added cleanly. | |
| 44 | + | fileURLPath := dir + "/" + strings.ReplaceAll("foo bar.png", " ", "%20") | |
| 45 | + | fileURLForm := "file://" + fileURLPath | |
| 46 | + | _ = fileURL // keep linter happy if unused | |
| 47 | + | ||
| 6 | 48 | cases := []struct { | |
| 7 | 49 | name string | |
| 8 | 50 | in string | |
| 9 | 51 | want string | |
| 10 | 52 | }{ | |
| 11 | 53 | // macOS Terminal default: backslash-escaped spaces in path. | |
| 12 | - | {"backslash-escaped space", `/Users/pat/foo\ bar.png`, `'/Users/pat/foo bar.png'`}, | |
| 54 | + | {"backslash-escaped space", dir + `/foo\ bar.png`, `'` + dir + `/foo bar.png'`}, | |
| 13 | 55 | ||
| 14 | 56 | // Plain absolute path with no special chars. | |
| 15 | - | {"plain absolute", `/Users/pat/file.png`, `'/Users/pat/file.png'`}, | |
| 57 | + | {"plain absolute", dir + `/file.png`, `'` + dir + `/file.png'`}, | |
| 16 | 58 | ||
| 17 | 59 | // file:// URL form; URL-decoded and scheme stripped. | |
| 18 | - | {"file url", `file:///Users/pat/foo%20bar.png`, `'/Users/pat/foo bar.png'`}, | |
| 19 | - | ||
| 20 | - | // Tilde paths. | |
| 21 | - | {"tilde", `~/Pictures/x.png`, `'~/Pictures/x.png'`}, | |
| 60 | + | {"file url", fileURLForm, `'` + dir + `/foo bar.png'`}, | |
| 22 | 61 | ||
| 23 | 62 | // Multi-file drop, each path quoted independently. | |
| 24 | - | {"two paths", `/a.png /b.png`, `'/a.png' '/b.png'`}, | |
| 63 | + | {"two paths", dir + `/a.png ` + dir + `/b.png`, `'` + dir + `/a.png' '` + dir + `/b.png'`}, | |
| 25 | 64 | ||
| 26 | 65 | // Already single-quoted: re-normalise to consistent quoting. | |
| 27 | - | {"already single quoted", `'/Users/pat/x.png'`, `'/Users/pat/x.png'`}, | |
| 66 | + | {"already single quoted", `'` + dir + `/x.png'`, `'` + dir + `/x.png'`}, | |
| 28 | 67 | ||
| 29 | 68 | // Already double-quoted: re-normalise. | |
| 30 | - | {"already double quoted", `"/Users/pat/x.png"`, `'/Users/pat/x.png'`}, | |
| 69 | + | {"already double quoted", `"` + dir + `/x.png"`, `'` + dir + `/x.png'`}, | |
| 31 | 70 | ||
| 32 | 71 | // Embedded apostrophe gets the standard '\'' splice escape. | |
| 33 | - | {"embedded apostrophe", `/Users/pat/it's.png`, `'/Users/pat/it'\''s.png'`}, | |
| 72 | + | {"embedded apostrophe", dir + `/it's.png`, `'` + dir + `/it'\''s.png'`}, | |
| 34 | 73 | ||
| 35 | 74 | // Plain prose left alone. | |
| 36 | 75 | {"prose", `hello world`, `hello world`}, | |
| @@ -43,7 +82,15 @@ func TestQuotePastedFilePaths(t *testing.T) { | |||
| 43 | 82 | {"metachar", `/foo;rm -rf /`, `/foo;rm -rf /`}, | |
| 44 | 83 | ||
| 45 | 84 | // Mixed: valid path token quoted, surrounding words preserved. | |
| 46 | - | {"path mixed with prose", `/x.png is good`, `'/x.png' is good`}, | |
| 85 | + | {"path mixed with prose", dir + `/only.png is good`, `'` + dir + `/only.png' is good`}, | |
| 86 | + | ||
| 87 | + | // URL path segment that happens to start with "/" but doesn't | |
| 88 | + | // exist on disk: must NOT be quoted or chip-collapsed. This | |
| 89 | + | // is the regression that motivated the existence check. | |
| 90 | + | {"url path segment", `/de/downloads/dokumentenarchiv`, `/de/downloads/dokumentenarchiv`}, | |
| 91 | + | ||
| 92 | + | // Non-existent path that looks like a real one: also untouched. | |
| 93 | + | {"non-existent absolute", `/this/does/not/exist.png`, `/this/does/not/exist.png`}, | |
| 47 | 94 | } | |
| 48 | 95 | for _, c := range cases { | |
| 49 | 96 | t.Run(c.name, func(t *testing.T) { | |
| @@ -54,3 +101,26 @@ func TestQuotePastedFilePaths(t *testing.T) { | |||
| 54 | 101 | }) | |
| 55 | 102 | } | |
| 56 | 103 | } | |
| 104 | + | ||
| 105 | + | // TestTildePathExists checks that ~ expansion in pathExists works | |
| 106 | + | // when the home directory contains the candidate. We don't write | |
| 107 | + | // into the user's real home; instead, override HOME for the test. | |
| 108 | + | func TestTildePathExists(t *testing.T) { | |
| 109 | + | tmp := t.TempDir() | |
| 110 | + | makeFiles(t, tmp, "tilde-test.png") | |
| 111 | + | t.Setenv("HOME", tmp) | |
| 112 | + | ||
| 113 | + | in := `~/tilde-test.png` | |
| 114 | + | want := `'~/tilde-test.png'` | |
| 115 | + | got := quotePastedFilePaths(in) | |
| 116 | + | if got != want { | |
| 117 | + | t.Errorf("input %q\n want %q\n got %q", in, want, got) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // A tilde path that doesn't exist must be left alone. | |
| 121 | + | in = `~/no-such-file.png` | |
| 122 | + | got = quotePastedFilePaths(in) | |
| 123 | + | if got != in { | |
| 124 | + | t.Errorf("non-existent tilde path mutated: got %q want %q", got, in) | |
| 125 | + | } | |
| 126 | + | } | |
| @@ -1,36 +1,75 @@ | |||||
| 1 | package tui | 1 | package tui | ||
| 2 | 2 | ||||
| 3 | - | import "testing" | 3 | + | import ( |
| 4 | + | "net/url" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // makeFiles creates files (and intermediate dirs) under base for each | |||
| 12 | + | // relative path in rels. Returns base for convenience. | |||
| 13 | + | func makeFiles(t *testing.T, base string, rels ...string) string { | |||
| 14 | + | t.Helper() | |||
| 15 | + | for _, rel := range rels { | |||
| 16 | + | full := filepath.Join(base, rel) | |||
| 17 | + | if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { | |||
| 18 | + | t.Fatalf("mkdir: %v", err) | |||
| 19 | + | } | |||
| 20 | + | if err := os.WriteFile(full, []byte("x"), 0o644); err != nil { | |||
| 21 | + | t.Fatalf("write: %v", err) | |||
| 22 | + | } | |||
| 23 | + | } | |||
| 24 | + | return base | |||
| 25 | + | } | |||
| 4 | 26 | ||||
| 5 | func TestQuotePastedFilePaths(t *testing.T) { | 27 | func TestQuotePastedFilePaths(t *testing.T) { | ||
| 28 | + | dir := t.TempDir() | |||
| 29 | + | makeFiles(t, dir, | |||
| 30 | + | "foo bar.png", | |||
| 31 | + | "file.png", | |||
| 32 | + | "a.png", | |||
| 33 | + | "b.png", | |||
| 34 | + | "x.png", | |||
| 35 | + | "it's.png", | |||
| 36 | + | "only.png", | |||
| 37 | + | ) | |||
| 38 | + | ||||
| 39 | + | // file:// URL form needs URL-encoded spaces. | |||
| 40 | + | fileURL := "file://" + dir + "/" + url.PathEscape("foo bar.png") | |||
| 41 | + | // PathEscape doesn't escape "/" but escapes the space — that's | |||
| 42 | + | // what macOS Finder produces. Build the path-only segment so the | |||
| 43 | + | // "file://" prefix is added cleanly. | |||
| 44 | + | fileURLPath := dir + "/" + strings.ReplaceAll("foo bar.png", " ", "%20") | |||
| 45 | + | fileURLForm := "file://" + fileURLPath | |||
| 46 | + | _ = fileURL // keep linter happy if unused | |||
| 47 | + | ||||
| 6 | cases := []struct { | 48 | cases := []struct { | ||
| 7 | name string | 49 | name string | ||
| 8 | in string | 50 | in string | ||
| 9 | want string | 51 | want string | ||
| 10 | }{ | 52 | }{ | ||
| 11 | // macOS Terminal default: backslash-escaped spaces in path. | 53 | // macOS Terminal default: backslash-escaped spaces in path. | ||
| 12 | - | {"backslash-escaped space", `/Users/pat/foo\ bar.png`, `'/Users/pat/foo bar.png'`}, | 54 | + | {"backslash-escaped space", dir + `/foo\ bar.png`, `'` + dir + `/foo bar.png'`}, |
| 13 | 55 | ||||
| 14 | // Plain absolute path with no special chars. | 56 | // Plain absolute path with no special chars. | ||
| 15 | - | {"plain absolute", `/Users/pat/file.png`, `'/Users/pat/file.png'`}, | 57 | + | {"plain absolute", dir + `/file.png`, `'` + dir + `/file.png'`}, |
| 16 | 58 | ||||
| 17 | // file:// URL form; URL-decoded and scheme stripped. | 59 | // file:// URL form; URL-decoded and scheme stripped. | ||
| 18 | - | {"file url", `file:///Users/pat/foo%20bar.png`, `'/Users/pat/foo bar.png'`}, | 60 | + | {"file url", fileURLForm, `'` + dir + `/foo bar.png'`}, |
| 19 | - | ||||
| 20 | - | // Tilde paths. | |||
| 21 | - | {"tilde", `~/Pictures/x.png`, `'~/Pictures/x.png'`}, | |||
| 22 | 61 | ||||
| 23 | // Multi-file drop, each path quoted independently. | 62 | // Multi-file drop, each path quoted independently. | ||
| 24 | - | {"two paths", `/a.png /b.png`, `'/a.png' '/b.png'`}, | 63 | + | {"two paths", dir + `/a.png ` + dir + `/b.png`, `'` + dir + `/a.png' '` + dir + `/b.png'`}, |
| 25 | 64 | ||||
| 26 | // Already single-quoted: re-normalise to consistent quoting. | 65 | // Already single-quoted: re-normalise to consistent quoting. | ||
| 27 | - | {"already single quoted", `'/Users/pat/x.png'`, `'/Users/pat/x.png'`}, | 66 | + | {"already single quoted", `'` + dir + `/x.png'`, `'` + dir + `/x.png'`}, |
| 28 | 67 | ||||
| 29 | // Already double-quoted: re-normalise. | 68 | // Already double-quoted: re-normalise. | ||
| 30 | - | {"already double quoted", `"/Users/pat/x.png"`, `'/Users/pat/x.png'`}, | 69 | + | {"already double quoted", `"` + dir + `/x.png"`, `'` + dir + `/x.png'`}, |
| 31 | 70 | ||||
| 32 | // Embedded apostrophe gets the standard '\'' splice escape. | 71 | // Embedded apostrophe gets the standard '\'' splice escape. | ||
| 33 | - | {"embedded apostrophe", `/Users/pat/it's.png`, `'/Users/pat/it'\''s.png'`}, | 72 | + | {"embedded apostrophe", dir + `/it's.png`, `'` + dir + `/it'\''s.png'`}, |
| 34 | 73 | ||||
| 35 | // Plain prose left alone. | 74 | // Plain prose left alone. | ||
| 36 | {"prose", `hello world`, `hello world`}, | 75 | {"prose", `hello world`, `hello world`}, | ||
| @@ -43,7 +82,15 @@ func TestQuotePastedFilePaths(t *testing.T) { | |||||
| 43 | {"metachar", `/foo;rm -rf /`, `/foo;rm -rf /`}, | 82 | {"metachar", `/foo;rm -rf /`, `/foo;rm -rf /`}, | ||
| 44 | 83 | ||||
| 45 | // Mixed: valid path token quoted, surrounding words preserved. | 84 | // Mixed: valid path token quoted, surrounding words preserved. | ||
| 46 | - | {"path mixed with prose", `/x.png is good`, `'/x.png' is good`}, | 85 | + | {"path mixed with prose", dir + `/only.png is good`, `'` + dir + `/only.png' is good`}, |
| 86 | + | ||||
| 87 | + | // URL path segment that happens to start with "/" but doesn't | |||
| 88 | + | // exist on disk: must NOT be quoted or chip-collapsed. This | |||
| 89 | + | // is the regression that motivated the existence check. | |||
| 90 | + | {"url path segment", `/de/downloads/dokumentenarchiv`, `/de/downloads/dokumentenarchiv`}, | |||
| 91 | + | ||||
| 92 | + | // Non-existent path that looks like a real one: also untouched. | |||
| 93 | + | {"non-existent absolute", `/this/does/not/exist.png`, `/this/does/not/exist.png`}, | |||
| 47 | } | 94 | } | ||
| 48 | for _, c := range cases { | 95 | for _, c := range cases { | ||
| 49 | t.Run(c.name, func(t *testing.T) { | 96 | t.Run(c.name, func(t *testing.T) { | ||
| @@ -54,3 +101,26 @@ func TestQuotePastedFilePaths(t *testing.T) { | |||||
| 54 | }) | 101 | }) | ||
| 55 | } | 102 | } | ||
| 56 | } | 103 | } | ||
| 104 | + | ||||
| 105 | + | // TestTildePathExists checks that ~ expansion in pathExists works | |||
| 106 | + | // when the home directory contains the candidate. We don't write | |||
| 107 | + | // into the user's real home; instead, override HOME for the test. | |||
| 108 | + | func TestTildePathExists(t *testing.T) { | |||
| 109 | + | tmp := t.TempDir() | |||
| 110 | + | makeFiles(t, tmp, "tilde-test.png") | |||
| 111 | + | t.Setenv("HOME", tmp) | |||
| 112 | + | ||||
| 113 | + | in := `~/tilde-test.png` | |||
| 114 | + | want := `'~/tilde-test.png'` | |||
| 115 | + | got := quotePastedFilePaths(in) | |||
| 116 | + | if got != want { | |||
| 117 | + | t.Errorf("input %q\n want %q\n got %q", in, want, got) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | // A tilde path that doesn't exist must be left alone. | |||
| 121 | + | in = `~/no-such-file.png` | |||
| 122 | + | got = quotePastedFilePaths(in) | |||
| 123 | + | if got != in { | |||
| 124 | + | t.Errorf("non-existent tilde path mutated: got %q want %q", got, in) | |||
| 125 | + | } | |||
| 126 | + | } | |||