From cbd828e19070ba354bc76d4091566ee88e5d71f9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 5 May 2026 14:46:36 +0300 Subject: [PATCH 1/2] fix(session): strip illegal characters from windows session dir (#18) - Encode cwd via new encodeCwdForDir helper that handles both `/` and `\` separators and strips characters illegal in Windows directory names (`: < > " | ? *`) - Fixes session creation on Windows where the drive-letter colon produced names like `C:--test` and caused mkdir to fail - Add regression tests covering Unix paths, Windows drive roots, secondary drives, mixed separators, and other illegal chars Fixes #18 --- internal/session/session_dir_test.go | 70 ++++++++++++++++++++++++++++ internal/session/tree_manager.go | 32 +++++++++++-- 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 internal/session/session_dir_test.go diff --git a/internal/session/session_dir_test.go b/internal/session/session_dir_test.go new file mode 100644 index 00000000..764bfceb --- /dev/null +++ b/internal/session/session_dir_test.go @@ -0,0 +1,70 @@ +package session + +import ( + "strings" + "testing" +) + +// TestEncodeCwdForDir verifies the working-directory → session-directory +// name encoding strips characters that are illegal on Windows (notably the +// drive-letter colon, see issue #18) while preserving the previous output +// for the typical Unix paths. +func TestEncodeCwdForDir(t *testing.T) { + tests := []struct { + name string + cwd string + want string + }{ + { + name: "unix absolute path", + cwd: "/home/user/proj", + want: "home--user--proj", + }, + { + name: "unix relative path", + cwd: "proj/sub", + want: "proj--sub", + }, + { + name: "windows drive root", + cwd: `C:\test`, + want: "C--test", + }, + { + name: "windows nested path", + cwd: `C:\Users\User\code`, + want: "C--Users--User--code", + }, + { + name: "windows secondary drive", + cwd: `S:\work\repo`, + want: "S--work--repo", + }, + { + name: "windows mixed separators", + cwd: `C:\Users/User\code`, + want: "C--Users--User--code", + }, + { + name: "windows other illegal chars stripped", + cwd: `C:\ac|d?e*f"g`, + want: "C--abcdefg", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := encodeCwdForDir(tc.cwd) + if got != tc.want { + t.Errorf("encodeCwdForDir(%q) = %q, want %q", tc.cwd, got, tc.want) + } + // Encoded directory must never contain characters that are + // illegal in Windows directory names. + for _, bad := range []string{":", "<", ">", "\"", "|", "?", "*", "\\", "/"} { + if strings.Contains(got, bad) { + t.Errorf("encodeCwdForDir(%q) = %q contains illegal char %q", tc.cwd, got, bad) + } + } + }) + } +} diff --git a/internal/session/tree_manager.go b/internal/session/tree_manager.go index a8914e02..2b1733bd 100644 --- a/internal/session/tree_manager.go +++ b/internal/session/tree_manager.go @@ -1356,9 +1356,35 @@ func DefaultSessionDir(cwd string) string { if err != nil { home = "." } - // Convert path separators to double dashes. - safeCwd := strings.ReplaceAll(cwd, string(filepath.Separator), "--") + return filepath.Join(home, ".kit", "sessions", encodeCwdForDir(cwd)) +} + +// encodeCwdForDir converts a working-directory path into a single, filesystem- +// safe directory name. Path separators are replaced with double dashes and +// characters that are illegal in Windows directory names — most importantly +// the colon that follows the drive letter (e.g. `C:\foo` → `C--foo`) — are +// stripped. The result is identical to the previous Unix-only encoding for +// paths that do not contain such characters, so existing session directories +// are preserved. +func encodeCwdForDir(cwd string) string { + // Convert both `/` and `\` to double dashes so encoding is stable across + // platforms and remains correct on Windows where `filepath.Separator` + // would otherwise miss forward-slash style paths. + safeCwd := strings.ReplaceAll(cwd, "\\", "--") + safeCwd = strings.ReplaceAll(safeCwd, "/", "--") // Remove leading separator replacement. safeCwd = strings.TrimPrefix(safeCwd, "--") - return filepath.Join(home, ".kit", "sessions", safeCwd) + // Strip characters that are illegal in directory names on Windows + // (`< > : " | ? *`). On Unix these characters are legal but rare in + // practice; stripping them keeps the encoding portable. + replacer := strings.NewReplacer( + ":", "", + "<", "", + ">", "", + "\"", "", + "|", "", + "?", "", + "*", "", + ) + return replacer.Replace(safeCwd) } From 4ef57eec4ef1d035726d75c16b55565e55877d59 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 5 May 2026 14:54:20 +0300 Subject: [PATCH 2/2] docs(session): correct DefaultSessionDir convention comment - Stale comment showed ~/.kit/sessions/----/ which does not match the actual encoding (no leading/trailing dashes) - Update to reflect the real format and point to encodeCwdForDir for full rules --- internal/session/tree_manager.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/session/tree_manager.go b/internal/session/tree_manager.go index 2b1733bd..b3959846 100644 --- a/internal/session/tree_manager.go +++ b/internal/session/tree_manager.go @@ -1350,7 +1350,10 @@ func (tm *TreeManager) buildTreeNodeDepth(id string, depth int, visited map[stri // --- Path conventions --- // DefaultSessionDir returns the default session storage directory for a cwd. -// Convention: ~/.kit/sessions/----/ +// Convention: ~/.kit/sessions/, where path separators are +// encoded as "--" with no leading or trailing dashes — e.g. +// /home/user/proj becomes home--user--proj. See encodeCwdForDir for the +// full encoding rules (including Windows path handling). func DefaultSessionDir(cwd string) string { home, err := os.UserHomeDir() if err != nil {