filer: allow renaming into a sibling that shares the name prefix (#12337)

CanRename rejected a move whenever the new parent directory's path started
with the source path as a plain string, so moving /dir/a into /dir/a.d
(or anything under /dir/ab) failed with "can not move directory to a
subdirectory of itself". Check the path boundary instead: reject only
when the new parent is the source itself or lies under it.


Claude-Session: https://claude.ai/code/session_01VncHKTehVroSAzRMUHykEX

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jiri PucandClaude Opus 5.5 authored and GitHub committed 2026-11-02 10:09:22 -08:00
1 parent 7298b4ed37
commit 1637941368
2 files changed
+32 -1

No files matched your search

+8 -1
View File
@@ -10,7 +10,7 @@ import (
func (f *Filer) CanRename(ctx context.Context, source, target util.FullPath, oldName string) error {
sourcePath := source.Child(oldName)
if strings.HasPrefix(string(target), string(sourcePath)) {
if movesIntoItself(sourcePath, target) {
return fmt.Errorf("mv: can not move directory to a subdirectory of itself")
}
@@ -33,6 +33,13 @@ func (f *Filer) CanRename(ctx context.Context, source, target util.FullPath, old
return nil
}
// movesIntoItself reports whether target, the new parent directory, is
// sourcePath or lies under it. A sibling that only shares a name prefix,
// such as /a.d for /a, does not.
func movesIntoItself(sourcePath, target util.FullPath) bool {
return target == sourcePath || target.IsUnder(sourcePath)
}
func (f *Filer) DetectBucket(source util.FullPath) (bucket string) {
if strings.HasPrefix(string(source), f.DirBucketsPath+"/") {
bucketAndObjectKey := string(source)[len(f.DirBucketsPath)+1:]
+24
View File
@@ -29,3 +29,27 @@ func TestDetectBucket(t *testing.T) {
})
}
}
func TestMovesIntoItself(t *testing.T) {
tests := []struct {
name string
sourcePath string
target string
want bool
}{
{"into itself", "/dir/a", "/dir/a", true},
{"into a child", "/dir/a", "/dir/a/b", true},
{"into a deeper descendant", "/dir/a", "/dir/a/b/c", true},
{"into a sibling sharing the name prefix", "/dir/a", "/dir/a.d", false},
{"into a descendant of that sibling", "/dir/a", "/dir/ab/c", false},
{"into the parent", "/dir/a", "/dir", false},
{"into an unrelated directory", "/dir/a", "/other", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := movesIntoItself(util.FullPath(tt.sourcePath), util.FullPath(tt.target)); got != tt.want {
t.Errorf("movesIntoItself(%q, %q) = %v, want %v", tt.sourcePath, tt.target, got, tt.want)
}
})
}
}