From 2b9d055101a0a2731af9ef5d2caf513bfb7da75e Mon Sep 17 00:00:00 2001 From: Craig Hesling Date: Tue, 2 Sep 2025 02:27:34 -0700 Subject: [PATCH] drive: fix StatCache mishandling of paths with spaces Fix "file not found" errors when WebDAV clients access files/dirs inside directories with spaces. The issue occurred because StatCache was mixing URL-escaped and unescaped paths, causing cache key mismatches. Specifically, StatCache.set() parsed WebDAV responses containing URL-escaped paths (ex. "Dir%20Space/file1.txt") and stored them alongside unescaped cache keys (ex. "Dir Space/file1.txt"). This mismatch prevented StatCache.get() from correctly determining whether a child file existed. See https://github.com/tailscale/tailscale/issues/13632#issuecomment-3243522449 for the full explanation of the issue. The decision to keep all paths references unescaped inside the StatCache is consistent with net/http.Request.URL.Path and rewrite.go (sole consumer) Update unit test to detect this directory space mishandling. Fixes tailscale#13632 Signed-off-by: Craig Hesling --- drive/driveimpl/compositedav/stat_cache.go | 8 +++++++- drive/driveimpl/compositedav/stat_cache_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/drive/driveimpl/compositedav/stat_cache.go b/drive/driveimpl/compositedav/stat_cache.go index fc57ff064..36463fe7e 100644 --- a/drive/driveimpl/compositedav/stat_cache.go +++ b/drive/driveimpl/compositedav/stat_cache.go @@ -8,6 +8,7 @@ import ( "encoding/xml" "log" "net/http" + "net/url" "sync" "time" @@ -165,7 +166,12 @@ func (c *StatCache) set(name string, depth int, ce *cacheEntry) { children = make(map[string]*cacheEntry, len(ms.Responses)-1) for i := 0; i < len(ms.Responses); i++ { response := ms.Responses[i] - name := shared.Normalize(response.Href) + name, err := url.PathUnescape(response.Href) + if err != nil { + log.Printf("statcache.set child parse error: %s", err) + return + } + name = shared.Normalize(name) raw := marshalMultiStatus(response) entry := newCacheEntry(ce.Status, raw) if i == 0 { diff --git a/drive/driveimpl/compositedav/stat_cache_test.go b/drive/driveimpl/compositedav/stat_cache_test.go index fa63457a2..baa4fdda2 100644 --- a/drive/driveimpl/compositedav/stat_cache_test.go +++ b/drive/driveimpl/compositedav/stat_cache_test.go @@ -16,12 +16,12 @@ import ( "tailscale.com/tstest" ) -var parentPath = "/parent" +var parentPath = "/parent with spaces" -var childPath = "/parent/child.txt" +var childPath = "/parent with spaces/child.txt" var parentResponse = ` -/parent/ +/parent%20with%20spaces/ Mon, 29 Apr 2024 19:52:23 GMT @@ -36,7 +36,7 @@ var parentResponse = ` var childResponse = ` -/parent/child.txt +/parent%20with%20spaces/child.txt Mon, 29 Apr 2024 19:52:23 GMT