diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-13 02:31:32 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-13 02:31:32 +0200 |
| commit | 26c94eb3db62ec6eebbf8d22c11afe691d9520c4 (patch) | |
| tree | 165e5bf69234b4f96c9b74deb4898d7143ddf120 /internal/apply/fs.go | |
| parent | a6e442a645902011b2081c216daaec052cdc6ce6 (diff) | |
| download | krino-306b2cdd83d429b6c1a94f828f4ad2ab57bcc6ee.tar.gz krino-306b2cdd83d429b6c1a94f828f4ad2ab57bcc6ee.zip | |
krino: release 0.0.1 — man pages, install, examples, cross and release, README, changelogv0.0.1
Also: undo removes the directories its run created; a hardlink is never a
duplicate of its own other name; a flag written before "undo" is honoured;
--version prints no leading v. Duplicate conditions with different scopes
not sharing an original is documented as a known limitation.
Diffstat (limited to 'internal/apply/fs.go')
| -rw-r--r-- | internal/apply/fs.go | 35 |
1 files changed, 35 insertions, 0 deletions
diff --git a/internal/apply/fs.go b/internal/apply/fs.go index 205089f..823d5c8 100644 --- a/internal/apply/fs.go +++ b/internal/apply/fs.go @@ -108,6 +108,16 @@ func moveFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } + // Item 16 (fix round 2026-09-12, plan 5 Task 2): this guard must hold + // independently of runFileStep's own pre-check, layered rather than + // moved - the exact arrangement that produced plan 4's Task 5 Critical, + // where a helper that replaced silently was trusted because some caller + // had checked. Placed immediately before the operation that would + // otherwise clobber dst, the same way copyFile's own guard sits right + // before its rename into place. + if err := refuseIfExists(dst); err != nil { + return err + } err := os.Rename(src, dst) if err == nil { return nil @@ -121,6 +131,31 @@ func moveFile(src, dst string) error { return os.Remove(src) } +// renameFile renames src to dst, refusing on its own when dst already +// exists rather than trusting that a caller checked first (item 16, same +// reasoning as moveFile's guard above): a bare os.Rename silently replaces +// an occupied destination, and runFileStep's own pre-check must not be the +// only thing standing between a rename step and that. +func renameFile(src, dst string) error { + if err := refuseIfExists(dst); err != nil { + return err + } + return os.Rename(src, dst) +} + +// refuseIfExists reports an error naming dst if something is already there +// (os.Lstat succeeds, following no symlink), and propagates any other stat +// failure. A nil return means dst was confirmed absent at the moment of the +// check. +func refuseIfExists(dst string) error { + if _, err := os.Lstat(dst); err == nil { + return fmt.Errorf("destination already exists: %s", dst) + } else if !os.IsNotExist(err) { + return err + } + return nil +} + // maxSuffixAttempts bounds nextFreeName. internal/plan/conflict.go and // internal/trash/trash.go each have their own cap of the same size, for the // same reason given below: nextFreeName solves yet another, independent |
