Skip to content

Commit e26badf

Browse files
committed
fix: path for windows
1 parent 05d1af3 commit e26badf

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

internal/mcp/tools/common.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io/fs"
77
"os"
8+
"path"
89
"path/filepath"
910
"slices"
1011
"strings"
@@ -211,13 +212,20 @@ func stripFrontmatter(content string) string {
211212

212213
// validateArchcorePath normalises and validates a document path.
213214
// It returns the cleaned path or an error if the path is invalid.
215+
//
216+
// Uses path.Clean (POSIX, forward-slash) rather than filepath.Clean because on
217+
// Windows filepath.Clean would re-introduce backslashes after ToSlash, breaking
218+
// the subsequent ".archcore/" prefix check.
214219
func validateArchcorePath(relPath string) (string, error) {
220+
if filepath.IsAbs(relPath) {
221+
return "", fmt.Errorf("invalid path: must be relative and within .archcore/")
222+
}
215223
relPath = filepath.ToSlash(relPath)
216224
if !strings.HasPrefix(relPath, ".archcore/") {
217225
return "", fmt.Errorf("invalid path: must start with \".archcore/\"")
218226
}
219-
cleaned := filepath.Clean(relPath)
220-
if strings.HasPrefix(cleaned, "..") || filepath.IsAbs(cleaned) || !strings.HasPrefix(cleaned, ".archcore/") {
227+
cleaned := path.Clean(relPath)
228+
if strings.HasPrefix(cleaned, "..") || !strings.HasPrefix(cleaned, ".archcore/") {
221229
return "", fmt.Errorf("invalid path: must be relative and within .archcore/")
222230
}
223231
return cleaned, nil

internal/mcp/tools/common_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,45 @@ func TestReadDocumentContent_WithTags(t *testing.T) {
508508
t.Errorf("tags = %v, want [frontend]", doc.Tags)
509509
}
510510
}
511+
512+
func TestValidateArchcorePath(t *testing.T) {
513+
t.Parallel()
514+
cases := []struct {
515+
name string
516+
input string
517+
want string
518+
wantErr bool
519+
errMatch string
520+
}{
521+
{name: "forward slashes", input: ".archcore/features/lfrom/x.doc.md", want: ".archcore/features/lfrom/x.doc.md"},
522+
{name: "redundant segments cleaned", input: ".archcore/a/./b/../c.md", want: ".archcore/a/c.md"},
523+
// Regression: filepath.Clean on Windows re-introduces backslashes, which
524+
// broke the second prefix check. The cleaned output must always use
525+
// forward slashes, regardless of platform.
526+
{name: "output uses forward slashes", input: ".archcore/a/b.md", want: ".archcore/a/b.md"},
527+
{name: "missing prefix", input: "features/lfrom/x.doc.md", wantErr: true, errMatch: "must start with"},
528+
{name: "traversal escape", input: ".archcore/../etc/passwd", wantErr: true, errMatch: "must be relative"},
529+
{name: "absolute unix", input: "/etc/passwd", wantErr: true, errMatch: "must be relative"},
530+
}
531+
for _, tc := range cases {
532+
t.Run(tc.name, func(t *testing.T) {
533+
t.Parallel()
534+
got, err := validateArchcorePath(tc.input)
535+
if tc.wantErr {
536+
if err == nil {
537+
t.Fatalf("expected error, got nil (result=%q)", got)
538+
}
539+
if tc.errMatch != "" && !strings.Contains(err.Error(), tc.errMatch) {
540+
t.Errorf("error = %q, want substring %q", err.Error(), tc.errMatch)
541+
}
542+
return
543+
}
544+
if err != nil {
545+
t.Fatalf("unexpected error: %v", err)
546+
}
547+
if got != tc.want {
548+
t.Errorf("got %q, want %q", got, tc.want)
549+
}
550+
})
551+
}
552+
}

0 commit comments

Comments
 (0)