From 4105a0167d6f063e9f8bbba66cf132722e657cb1 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sat, 30 May 2026 13:21:33 +0800 Subject: [PATCH] fix: use atomic rename that refuses existing destinations Replace Stat-then-Rename with RENAME_NOREPLACE / RENAME_EXCL so concurrent creates cannot be silently overwritten during path renames. Fixes #4 --- find_replace.go | 7 +------ go.mod | 2 ++ go.sum | 4 ++-- rename_darwin.go | 18 ++++++++++++++++++ rename_linux.go | 18 ++++++++++++++++++ rename_other.go | 18 ++++++++++++++++++ rename_windows.go | 18 ++++++++++++++++++ 7 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 rename_darwin.go create mode 100644 rename_linux.go create mode 100644 rename_other.go create mode 100644 rename_windows.go diff --git a/find_replace.go b/find_replace.go index 9bdbe79..8525104 100644 --- a/find_replace.go +++ b/find_replace.go @@ -184,14 +184,9 @@ func (fr *findReplace) RenameFile(f *File) error { } newPath := filepath.Join(f.Dir(), newBaseName) - if _, err := os.Stat(newPath); err == nil { - return fmt.Errorf("refusing to rename %v to %v: %v already exists", f.Path, newBaseName, newPath) - } else if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("stat rename destination %v: %w", newPath, err) - } log.Printf("Renaming %v to %v", f.Path, newBaseName) - if err := os.Rename(f.Path, newPath); err != nil { + if err := atomicRenameNoReplace(f.Path, newPath); err != nil { return fmt.Errorf("rename %v to %v: %w", f.Path, newBaseName, err) } return nil diff --git a/go.mod b/go.mod index 66fd9af..144cdc5 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/dolph/find-replace go 1.20 require golang.org/x/tools v0.7.0 + +require golang.org/x/sys v0.26.0 diff --git a/go.sum b/go.sum index b522ba0..7ce55e9 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= diff --git a/rename_darwin.go b/rename_darwin.go new file mode 100644 index 0000000..b798428 --- /dev/null +++ b/rename_darwin.go @@ -0,0 +1,18 @@ +//go:build darwin + +package main + +import ( + "errors" + "fmt" + + "golang.org/x/sys/unix" +) + +func atomicRenameNoReplace(oldpath, newpath string) error { + err := unix.RenameatxNp(unix.AT_FDCWD, oldpath, unix.AT_FDCWD, newpath, unix.RENAME_EXCL) + if errors.Is(err, unix.EEXIST) { + return fmt.Errorf("refusing to rename %v to %v: %v already exists", oldpath, newpath, newpath) + } + return err +} diff --git a/rename_linux.go b/rename_linux.go new file mode 100644 index 0000000..3df46ca --- /dev/null +++ b/rename_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package main + +import ( + "errors" + "fmt" + + "golang.org/x/sys/unix" +) + +func atomicRenameNoReplace(oldpath, newpath string) error { + err := unix.Renameat2(unix.AT_FDCWD, oldpath, unix.AT_FDCWD, newpath, unix.RENAME_NOREPLACE) + if errors.Is(err, unix.EEXIST) { + return fmt.Errorf("refusing to rename %v to %v: %v already exists", oldpath, newpath, newpath) + } + return err +} diff --git a/rename_other.go b/rename_other.go new file mode 100644 index 0000000..b16846d --- /dev/null +++ b/rename_other.go @@ -0,0 +1,18 @@ +//go:build !linux && !darwin && !windows + +package main + +import ( + "errors" + "fmt" + "os" +) + +func atomicRenameNoReplace(oldpath, newpath string) error { + if _, err := os.Stat(newpath); err == nil { + return fmt.Errorf("refusing to rename %v to %v: %v already exists", oldpath, newpath, newpath) + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("stat rename destination %v: %w", newpath, err) + } + return os.Rename(oldpath, newpath) +} diff --git a/rename_windows.go b/rename_windows.go new file mode 100644 index 0000000..3f9bce4 --- /dev/null +++ b/rename_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package main + +import ( + "errors" + "fmt" + "os" +) + +func atomicRenameNoReplace(oldpath, newpath string) error { + if _, err := os.Stat(newpath); err == nil { + return fmt.Errorf("refusing to rename %v to %v: %v already exists", oldpath, newpath, newpath) + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("stat rename destination %v: %w", newpath, err) + } + return os.Rename(oldpath, newpath) +}