From 34438cb5b993b96c0bf42615d7034724dba3fac5 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 12:03:11 -0800 Subject: [PATCH] fix: URL-encode file paths in fetch command to handle special characters File paths with spaces, #, ?, %, etc. were concatenated directly into URLs without encoding, producing malformed download URLs. Add encodeFilePath() that encodes each path segment individually (preserving directory separators) and use it in fetch. --- internal/cli/fetch.go | 11 ++++++++++- internal/cli/fetch_test.go | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/internal/cli/fetch.go b/internal/cli/fetch.go index fda1881..ade2080 100644 --- a/internal/cli/fetch.go +++ b/internal/cli/fetch.go @@ -113,7 +113,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error { return fmt.Errorf("invalid path in manifest: %w", err) } - fileURL := baseURL.String() + f.Path + fileURL := baseURL.String() + encodeFilePath(f.Path) log.Infof("fetching %s", f.Path) if err := downloadFile(fileURL, localPath, f, progress); err != nil { @@ -139,6 +139,15 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error { return nil } +// encodeFilePath URL-encodes each segment of a file path while preserving slashes. +func encodeFilePath(p string) string { + segments := strings.Split(p, "/") + for i, seg := range segments { + segments[i] = url.PathEscape(seg) + } + return strings.Join(segments, "/") +} + // sanitizePath validates and sanitizes a file path from the manifest. // It prevents path traversal attacks and rejects unsafe paths. func sanitizePath(p string) (string, error) { diff --git a/internal/cli/fetch_test.go b/internal/cli/fetch_test.go index 5809534..43414a7 100644 --- a/internal/cli/fetch_test.go +++ b/internal/cli/fetch_test.go @@ -16,6 +16,29 @@ import ( "sneak.berlin/go/mfer/mfer" ) +func TestEncodeFilePath(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"file.txt", "file.txt"}, + {"dir/file.txt", "dir/file.txt"}, + {"my file.txt", "my%20file.txt"}, + {"dir/my file.txt", "dir/my%20file.txt"}, + {"file#1.txt", "file%231.txt"}, + {"file?v=1.txt", "file%3Fv=1.txt"}, + {"path/to/file with spaces.txt", "path/to/file%20with%20spaces.txt"}, + {"100%done.txt", "100%25done.txt"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := encodeFilePath(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestSanitizePath(t *testing.T) { // Valid paths that should be accepted validTests := []struct {