From 01fc31cb71b39ec81ec66d40decd667b74707a6e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 9 Sep 2026 10:43:16 -0700 Subject: [PATCH] fix(filer): use path.Split instead of filepath.Split for filer paths (#11246) FullPath.DirAndName() and FullPath.Name() used filepath.Split, which is OS-dependent: on Windows it treats backslash as a path separator, corrupting filer paths that contain literal backslashes. Filer paths always use "/" as the separator, so switch to path.Split and path.Join which only split on "/" regardless of the host OS. This fixes the backslash case from #11243 where a file saved as /test/special\reverseslash4.jpg was stored with a corrupted path on Windows filer builds. The #, ?, and % cases from the same issue are client-side URL-encoding problems (the server never receives the raw characters), but once the client properly percent-encodes them the server now handles the decoded path correctly on all platforms. --- weed/util/fullpath.go | 7 ++- weed/util/fullpath_test.go | 91 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/weed/util/fullpath.go b/weed/util/fullpath.go index e72341ba4..6e4a5f248 100644 --- a/weed/util/fullpath.go +++ b/weed/util/fullpath.go @@ -2,7 +2,6 @@ package util import ( "path" - "path/filepath" "strings" "unicode/utf8" ) @@ -40,7 +39,7 @@ func NewFullPath(dir, name string) FullPath { } func (fp FullPath) DirAndName() (string, string) { - dir, name := filepath.Split(string(fp)) + dir, name := path.Split(string(fp)) name = SanitizeUTF8Name(name) if dir == "/" { return dir, name @@ -55,7 +54,7 @@ func (fp FullPath) DirAndName() (string, string) { // via SanitizeUTF8Name so the result is always safe to place in a proto // string field or HTTP URL. func (fp FullPath) Name() string { - _, name := filepath.Split(string(fp)) + _, name := path.Split(string(fp)) return SanitizeUTF8Name(name) } @@ -104,7 +103,7 @@ func (fp FullPath) Split() []string { } func Join(names ...string) string { - return filepath.ToSlash(filepath.Join(names...)) + return path.Join(names...) } func JoinPath(names ...string) FullPath { diff --git a/weed/util/fullpath_test.go b/weed/util/fullpath_test.go index af29e3d71..95856d532 100644 --- a/weed/util/fullpath_test.go +++ b/weed/util/fullpath_test.go @@ -82,3 +82,94 @@ func TestFullPathDirAndName_OnlyNameSanitized(t *testing.T) { t.Fatalf("regression: dir should remain raw (%q); callers needing a clean path must use Sanitized()", dir) } } + +// TestFullPathBackslashNotSeparator ensures a literal backslash in a filer +// path component is treated as a regular character, not as a path separator. +// Filer paths always use "/" as the separator; using filepath.Split (which +// treats "\" as a separator on Windows) corrupts paths that contain literal +// backslashes (#11243). path.Split is OS-independent and only splits on "/". +func TestFullPathBackslashNotSeparator(t *testing.T) { + tests := []struct { + fullPath string + wantDir string + wantName string + }{ + // Backslash in the filename — must stay in the name, not split the path. + {"/test/special\\reverseslash4.jpg", "/test", "special\\reverseslash4.jpg"}, + // Backslash in a directory component — the dir keeps it, name is after last "/". + {"/a\\b/c.jpg", "/a\\b", "c.jpg"}, + // Multiple backslashes in the filename. + {"/dir/a\\b\\c.txt", "/dir", "a\\b\\c.txt"}, + // Backslash at the start of the filename. + {"/dir/\\file.txt", "/dir", "\\file.txt"}, + } + for _, tc := range tests { + t.Run(tc.fullPath, func(t *testing.T) { + fp := FullPath(tc.fullPath) + dir, name := fp.DirAndName() + if dir != tc.wantDir { + t.Errorf("DirAndName dir: got %q want %q", dir, tc.wantDir) + } + if name != tc.wantName { + t.Errorf("DirAndName name: got %q want %q", name, tc.wantName) + } + if got := fp.Name(); got != tc.wantName { + t.Errorf("Name: got %q want %q", got, tc.wantName) + } + }) + } +} + +// TestFullPathSpecialCharactersInName ensures that special characters that are +// valid in filenames (#, ?, %) are preserved by DirAndName and Name. These +// characters must be percent-encoded by the client when constructing the HTTP +// request URL, but once decoded by the server they are regular path bytes. +// The "/" in the path is always a separator; special chars in a directory +// component stay in the dir, and the last component is the name. +func TestFullPathSpecialCharactersInName(t *testing.T) { + tests := []struct { + fullPath string + wantDir string + wantName string + }{ + // "#" in a directory component — dir keeps it, name is after last "/". + {"/test/special#/endhashfolder.jpg", "/test/special#", "endhashfolder.jpg"}, + // "?" in a directory component. + {"/test/special?/endqfolder.jpg", "/test/special?", "endqfolder.jpg"}, + // "%" in a directory component. + {"/test/special%/endpctfolder.jpg", "/test/special%", "endpctfolder.jpg"}, + // "&" in a directory component. + {"/test/special&/endampfolder.jpg", "/test/special&", "endampfolder.jpg"}, + // "=" in a directory component. + {"/test/special=/endeqfolder.jpg", "/test/special=", "endeqfolder.jpg"}, + // Special chars in the filename itself (no "/" after them). + {"/test/hash#file.jpg", "/test", "hash#file.jpg"}, + {"/test/q?file.jpg", "/test", "q?file.jpg"}, + {"/test/pct%file.jpg", "/test", "pct%file.jpg"}, + } + for _, tc := range tests { + t.Run(tc.fullPath, func(t *testing.T) { + fp := FullPath(tc.fullPath) + dir, name := fp.DirAndName() + if dir != tc.wantDir { + t.Errorf("DirAndName dir: got %q want %q", dir, tc.wantDir) + } + if name != tc.wantName { + t.Errorf("DirAndName name: got %q want %q", name, tc.wantName) + } + if got := fp.Name(); got != tc.wantName { + t.Errorf("Name: got %q want %q", got, tc.wantName) + } + }) + } +} + +// TestJoinPreservesBackslash ensures Join does not convert backslashes to +// forward slashes — they are regular filename characters in filer paths. +func TestJoinPreservesBackslash(t *testing.T) { + got := Join("/parent", "child\\name.txt") + want := "/parent/child\\name.txt" + if got != want { + t.Fatalf("Join: got %q want %q", got, want) + } +}