Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions find_replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
18 changes: 18 additions & 0 deletions rename_darwin.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions rename_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions rename_other.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid the fallback clobbering race on other Unix targets

For the !linux && !darwin && !windows build, this helper is still a stat-then-os.Rename, and on Unix targets such as FreeBSD/Solaris rename replaces an existing non-directory destination. If another concurrent rename creates newpath after the check, including sibling files processed by this tool whose names map to the same replacement target, this path can silently overwrite that file rather than preserving the no-replace guarantee.

Useful? React with 👍 / 👎.

}
18 changes: 18 additions & 0 deletions rename_windows.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a non-replacing rename on Windows

On Windows this still has the same TOCTOU clobber window: os.Rename is implemented with replace-existing semantics, so any file that appears at newpath after the os.Stat check is overwritten. Because WalkDir renames siblings concurrently, two sibling names that collapse to the same target, e.g. with find="ab", replace="a", can both pass the pre-check and then one rename replaces the other instead of returning the refusal error this change is meant to guarantee.

Useful? React with 👍 / 👎.

}