fix: use slash-separated lock paths on Windows (#344)

This commit is contained in:
snowy_smile
2026-07-25 07:15:27 +02:00
committed by GitHub
parent 44e5e02dd3
commit 390fe21ed9
5 changed files with 136 additions and 29 deletions
+12 -3
View File
@@ -10,12 +10,21 @@ on:
jobs:
test:
name: test
runs-on: ubuntu-latest
name: test (${{ matrix.os }})
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.26.x"
- name: Run test with coverage
- name: Run test with race detector and coverage
if: runner.os != 'Windows'
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Run test with coverage
if: runner.os == 'Windows'
run: go test "-coverprofile=coverage.txt" -covermode=atomic ./...
+14 -4
View File
@@ -60,12 +60,19 @@ func TestConfigDefaults(t *testing.T) {
func TestConfigCascade(t *testing.T) {
t.Parallel()
// Directories are resolved to absolute paths, which differ by platform
// (for example "/" becomes the current drive root on Windows).
rootDirectory, err := filepath.Abs("/")
require.NoError(t, err)
basicDirectory, err := filepath.Abs("/basic")
require.NoError(t, err)
check := func(t *testing.T, cfg *Config) {
require.True(t, cfg.Permissions.Read)
require.True(t, cfg.Permissions.Create)
require.False(t, cfg.Permissions.Delete)
require.False(t, cfg.Permissions.Update)
require.Equal(t, "/", cfg.Directory)
require.Equal(t, rootDirectory, cfg.Directory)
require.Len(t, cfg.Rules, 1)
require.Len(t, cfg.Users, 2)
@@ -73,14 +80,14 @@ func TestConfigCascade(t *testing.T) {
require.True(t, cfg.Users[0].Permissions.Create)
require.False(t, cfg.Users[0].Permissions.Delete)
require.False(t, cfg.Users[0].Permissions.Update)
require.Equal(t, "/", cfg.Users[0].Directory)
require.Equal(t, rootDirectory, cfg.Users[0].Directory)
require.Len(t, cfg.Users[0].Rules, 1)
require.True(t, cfg.Users[1].Permissions.Read)
require.False(t, cfg.Users[1].Permissions.Create)
require.False(t, cfg.Users[1].Permissions.Delete)
require.False(t, cfg.Users[1].Permissions.Update)
require.Equal(t, "/basic", cfg.Users[1].Directory)
require.Equal(t, basicDirectory, cfg.Users[1].Directory)
require.Len(t, cfg.Users[1].Rules, 0)
}
@@ -485,8 +492,11 @@ func TestConfigEnv(t *testing.T) {
cfg, err := ParseConfig("", nil)
require.NoError(t, err)
expectedDirectory, err := filepath.Abs("/test")
require.NoError(t, err)
assert.Equal(t, 1234, cfg.Port)
assert.Equal(t, "/test", cfg.Directory)
assert.Equal(t, expectedDirectory, cfg.Directory)
assert.Equal(t, true, cfg.Debug)
require.True(t, cfg.Permissions.Read)
require.True(t, cfg.Permissions.Create)
+22 -20
View File
@@ -7,6 +7,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
@@ -486,13 +487,21 @@ func TestServerPartialUpdateHonorsLocks(t *testing.T) {
func TestServerListingCharacters(t *testing.T) {
t.Parallel()
dir := makeTestDirectory(t, map[string][]byte{
contents := map[string][]byte{
"富/foo.txt": []byte("foo"),
"你好.txt": []byte("bar"),
"z*.txt": []byte("zbar"),
"foo.txt": []byte("foo"),
"🌹.txt": []byte("foo"),
})
}
expectedNames := []string{"foo.txt", "你好.txt", "富", "🌹.txt"}
if runtime.GOOS != "windows" {
// Asterisks are invalid in Windows filenames.
contents["z*.txt"] = []byte("zbar")
expectedNames = append(expectedNames, "z*.txt")
}
sort.Strings(expectedNames)
dir := makeTestDirectory(t, contents)
srv := makeTestServer(t, "directory: "+dir)
client := gowebdav.NewClient(srv.URL, "", "")
@@ -500,28 +509,21 @@ func TestServerListingCharacters(t *testing.T) {
// By default, reading permissions.
files, err := client.ReadDir("/")
require.NoError(t, err)
require.Len(t, files, 5)
require.Len(t, files, len(expectedNames))
names := []string{
files[0].Name(),
files[1].Name(),
files[2].Name(),
files[3].Name(),
files[4].Name(),
names := make([]string, len(files))
for i, file := range files {
names[i] = file.Name()
}
sort.Strings(names)
require.Equal(t, []string{
"foo.txt",
"z*.txt",
"你好.txt",
"富",
"🌹.txt",
}, names)
require.Equal(t, expectedNames, names)
data, err := client.Read("/z*.txt")
require.NoError(t, err)
require.EqualValues(t, []byte("zbar"), data)
if runtime.GOOS != "windows" {
data, err := client.Read("/z*.txt")
require.NoError(t, err)
require.EqualValues(t, []byte("zbar"), data)
}
}
func TestServerAuthentication(t *testing.T) {
+9 -2
View File
@@ -1,6 +1,7 @@
package lib
import (
"path"
"path/filepath"
"time"
@@ -24,7 +25,10 @@ func newLockSystem(ls webdav.LockSystem, directory string) *lockSystem {
return &lockSystem{
LockSystem: ls,
resolve: func(name string) (string, error) {
return filepath.Join(directory, name), nil
// Lock names share a slash-separated namespace across users, even
// on Windows where filepath.Join would emit backslashes and break
// descendant-lock matching in the underlying LockSystem.
return path.Join(filepath.ToSlash(directory), name), nil
},
}
}
@@ -44,7 +48,10 @@ func newMultiDirLockSystem(ls webdav.LockSystem, mounts DirectoryMounts) *lockSy
return "", err
}
return mount.filePath(rest), nil
// filePath returns an OS-native path for real file operations; the
// lock namespace must stay slash-separated so descendant locks match
// on Windows.
return filepath.ToSlash(mount.filePath(rest)), nil
},
}
}
+79
View File
@@ -0,0 +1,79 @@
package lib
import (
"path"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/net/webdav"
)
func TestLockSystemRootLockProtectsDescendants(t *testing.T) {
t.Parallel()
locks := newLockSystem(webdav.NewMemLS(), filepath.Join(t.TempDir(), "nested"))
now := time.Now()
token, err := locks.Create(now, webdav.LockDetails{
Root: "/",
Duration: time.Minute,
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, locks.Unlock(time.Now(), token))
})
_, err = locks.Create(now, webdav.LockDetails{
Root: "/child.txt",
Duration: time.Minute,
ZeroDepth: true,
})
require.ErrorIs(t, err, webdav.ErrLocked)
}
func TestLockSystemSharesLocksAcrossNestedUserDirectories(t *testing.T) {
t.Parallel()
shared := webdav.NewMemLS()
parentDirectory := t.TempDir()
childDirectory := filepath.Join(parentDirectory, "child")
parent := newLockSystem(shared, parentDirectory)
child := newLockSystem(shared, childDirectory)
now := time.Now()
token, err := parent.Create(now, webdav.LockDetails{
Root: "/",
Duration: time.Minute,
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, parent.Unlock(time.Now(), token))
})
_, err = child.Create(now, webdav.LockDetails{
Root: "/file.txt",
Duration: time.Minute,
ZeroDepth: true,
})
require.ErrorIs(t, err, webdav.ErrLocked)
// The lock key is slash-separated on every platform, so the child's file
// nests under the parent's root lock rather than diverging on Windows.
key, err := child.resolve("/file.txt")
require.NoError(t, err)
require.Equal(t, path.Join(filepath.ToSlash(childDirectory), "file.txt"), key)
}
func TestMultiDirLockSystemUsesSlashSeparatedKeys(t *testing.T) {
t.Parallel()
mounts := DirectoryMounts{{Name: "docs", Path: filepath.Join(t.TempDir(), "docs")}}
locks := newMultiDirLockSystem(webdav.NewMemLS(), mounts)
key, err := locks.resolve("/docs/report.txt")
require.NoError(t, err)
require.Equal(t, path.Join(filepath.ToSlash(mounts[0].Path), "report.txt"), key)
require.NotContains(t, key, "\\")
}