From 204a3261be8c120d2209d6cb48b42d9b28646b95 Mon Sep 17 00:00:00 2001 From: mornyx Date: Tue, 6 Oct 2026 22:05:50 +0800 Subject: [PATCH] fix(fs): stop truncating names with spaces in companion ls/find output parseDrive9LS split `drive9 fs ls -l` lines with strings.Fields and kept only fields[2], cutting the final name column at its first space; the companion prints "kindsizename" through a space-padding tabwriter, so only the kind and size tokens are fixed. parseDrive9Paths kept fields[0] of each line, truncating `fs find` paths (and `fs grep` paths) at the first space even though the whole line is the path except for grep's TAB-separated score. Parse the two fixed ls tokens and cut paths only at tabs so names and paths keep their spaces. Fixes #73 --- internal/fs/drive9_companion.go | 28 ++++++++++--- internal/fs/drive9_companion_test.go | 60 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/internal/fs/drive9_companion.go b/internal/fs/drive9_companion.go index a116c54..f8ea0e0 100644 --- a/internal/fs/drive9_companion.go +++ b/internal/fs/drive9_companion.go @@ -1606,16 +1606,29 @@ func appendFlagValue(args *[]string, flag, value string) { *args = append(*args, flag, strings.TrimSpace(value)) } +// parseDrive9LS parses `drive9 fs ls -l` output. The companion prints +// "kindsizename" through a space-padding tabwriter with the name +// as the final column, so only the kind and size tokens are fixed: parse +// those two and keep the remainder verbatim as the name. func parseDrive9LS(raw []byte) []FileEntry { lines := strings.Split(strings.TrimSpace(string(raw)), "\n") entries := make([]FileEntry, 0, len(lines)) for _, line := range lines { - fields := strings.Fields(line) - if len(fields) < 3 { + rest := strings.TrimLeft(line, " ") + if len(rest) < 2 { continue } - size, _ := strconv.ParseInt(fields[1], 10, 64) - entries = append(entries, FileEntry{Name: fields[2], SizeBytes: size, IsDir: fields[0] == "d"}) + kind := rest[:1] + rest = strings.TrimLeft(rest[1:], " ") + sizeText, name, ok := strings.Cut(rest, " ") + if !ok { + continue + } + size, err := strconv.ParseInt(sizeText, 10, 64) + if err != nil { + continue + } + entries = append(entries, FileEntry{Name: strings.TrimLeft(name, " "), SizeBytes: size, IsDir: kind == "d"}) } if entries == nil { return []FileEntry{} @@ -1623,6 +1636,9 @@ func parseDrive9LS(raw []byte) []FileEntry { return entries } +// parseDrive9Paths parses line-oriented companion output where each line is +// a path (`fs find`) or a path followed by a TAB-separated score +// (`fs grep`). Paths may contain spaces; only a tab terminates the path. func parseDrive9Paths(raw []byte, limit int32) []SearchResult { lines := strings.Split(strings.TrimSpace(string(raw)), "\n") results := make([]SearchResult, 0, len(lines)) @@ -1631,8 +1647,8 @@ func parseDrive9Paths(raw []byte, limit int32) []SearchResult { if line == "" { continue } - fields := strings.Fields(line) - pathValue := fields[0] + pathValue, _, _ := strings.Cut(line, "\t") + pathValue = strings.TrimSpace(pathValue) if strings.HasPrefix(pathValue, ":") { pathValue = strings.TrimPrefix(pathValue, ":") } diff --git a/internal/fs/drive9_companion_test.go b/internal/fs/drive9_companion_test.go index e38ce11..3f1e564 100644 --- a/internal/fs/drive9_companion_test.go +++ b/internal/fs/drive9_companion_test.go @@ -790,6 +790,66 @@ func TestDrive9VaultHelpers(t *testing.T) { } } +func TestParseDrive9LSPreservesNamesWithSpaces(t *testing.T) { + // Mirrors `drive9 fs ls -l` output: kind and size columns are + // space-padded by the companion's tabwriter, the name is the final + // column and may contain (multiple) spaces. + raw := []byte("" + + "- 17 alpha beta.txt\n" + + "d 4096 reports q3.md\n" + + "- 17 double space.txt\n" + + "- 9 plain.txt\n") + entries := parseDrive9LS(raw) + if len(entries) != 4 { + t.Fatalf("entries = %d, want 4: %#v", len(entries), entries) + } + want := []FileEntry{ + {Name: "alpha beta.txt", SizeBytes: 17, IsDir: false}, + {Name: "reports q3.md", SizeBytes: 4096, IsDir: true}, + {Name: "double space.txt", SizeBytes: 17, IsDir: false}, + {Name: "plain.txt", SizeBytes: 9, IsDir: false}, + } + for i := range want { + if entries[i] != want[i] { + t.Errorf("entry[%d] = %#v, want %#v", i, entries[i], want[i]) + } + } +} + +func TestParseDrive9LSSkipsMalformedLines(t *testing.T) { + entries := parseDrive9LS([]byte("total 0\nnot a listing line\n- x bad size.txt\n")) + if len(entries) != 0 { + t.Fatalf("entries = %#v, want none", entries) + } + if got := parseDrive9LS(nil); len(got) != 0 { + t.Fatalf("entries = %#v, want empty", got) + } +} + +func TestParseDrive9PathsPreservesSpaces(t *testing.T) { + raw := []byte("" + + "/docs/alpha beta.txt\n" + + ":/docs/nested dir/two words.md\n" + + "/search hit\t0.85\n") + results := parseDrive9Paths(raw, 0) + if len(results) != 3 { + t.Fatalf("results = %d, want 3: %#v", len(results), results) + } + want := []SearchResult{ + {Path: "/docs/alpha beta.txt", Name: "alpha beta.txt"}, + {Path: "/docs/nested dir/two words.md", Name: "two words.md"}, + {Path: "/search hit", Name: "search hit"}, + } + for i := range want { + if results[i] != want[i] { + t.Errorf("results[%d] = %#v, want %#v", i, results[i], want[i]) + } + } + if got := parseDrive9Paths(raw, 2); len(got) != 2 { + t.Fatalf("limit results = %d, want 2", len(got)) + } +} + func buildFakeDrive9(t *testing.T) (binPath, recordPath string) { t.Helper() dir := t.TempDir()