From 26c94eb3db62ec6eebbf8d22c11afe691d9520c4 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sun, 13 Sep 2026 02:31:32 +0200 Subject: krino: release 0.0.1 — man pages, install, examples, cross and release, README, changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/apply/fs.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'internal/apply/fs.go') 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 -- cgit v1.3