mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 05:01:36 -10:00
* feat(tools): retain bounded structured file diffs * feat(acp): emit structured file changes in tool updates * fix(acp): harden structured file diff transport * fix(tools): reject unsafe rich diff previews * fix(acp): preserve safe rich diff semantics * fix(tools): bind structured diffs to committed changes * fix(acp): preserve structured diff identity * fix(tools): close rich diff race gaps * fix(tools): report partial patch paths from workspace * fix(tools): finalize structured diff evidence * fix(tools): separate unverified patch writes * test(tools): cover failed formatter recovery * fix(acp): serialize file deletions as nullable diffs * fix(tools): recreate files after formatter failure * test(acp): require nullable diff fields on wire
109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
package tools
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
var errFileChangedDuringWrite = errors.New("file changed on disk before the write committed")
|
|
|
|
// fileWriteBeforeCommit is a deterministic test hook. Production leaves it
|
|
// nil; tests use it to replace a path after observation but before opening the
|
|
// object that will actually be mutated.
|
|
var fileWriteBeforeCommit func(path string)
|
|
|
|
// fileWriteStat is a deterministic test seam for proving that the opened-file
|
|
// identity is captured before the final preimage comparison. Production uses
|
|
// the file descriptor directly.
|
|
var fileWriteStat = func(file *os.File) (os.FileInfo, error) { return file.Stat() }
|
|
|
|
// commitFileContents binds an overwrite to the file identity and bytes that
|
|
// the caller observed. A create uses exclusive creation. An overwrite opens the
|
|
// observed object without truncation, verifies identity/content through that
|
|
// handle, then truncates and writes the same handle. A path replacement before
|
|
// or during commit therefore fails instead of publishing stale rich evidence.
|
|
//
|
|
// expectedInfo nil means the caller observed a missing path. expectedContent
|
|
// may be nil for an existing but unreadable file; that path may still be
|
|
// overwritten, but callers must omit rich before/after evidence.
|
|
func commitFileContents(path string, expectedInfo os.FileInfo, expectedContent *string, content string) error {
|
|
if fileWriteBeforeCommit != nil {
|
|
fileWriteBeforeCommit(path)
|
|
}
|
|
|
|
if expectedInfo == nil {
|
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
openedInfo, err := fileWriteStat(file)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
return writeAndVerifyFileIdentity(path, file, openedInfo, content, false)
|
|
}
|
|
|
|
flags := os.O_WRONLY
|
|
if expectedContent != nil {
|
|
flags = os.O_RDWR
|
|
}
|
|
file, err := os.OpenFile(path, flags, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
openedInfo, err := fileWriteStat(file)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if !os.SameFile(expectedInfo, openedInfo) {
|
|
_ = file.Close()
|
|
return errFileChangedDuringWrite
|
|
}
|
|
pathInfo, err := os.Stat(path)
|
|
if err != nil || !os.SameFile(openedInfo, pathInfo) {
|
|
_ = file.Close()
|
|
return errFileChangedDuringWrite
|
|
}
|
|
if expectedContent != nil {
|
|
current, readErr := io.ReadAll(file)
|
|
if readErr != nil {
|
|
_ = file.Close()
|
|
return readErr
|
|
}
|
|
if string(current) != *expectedContent {
|
|
_ = file.Close()
|
|
return errFileChangedDuringWrite
|
|
}
|
|
}
|
|
return writeAndVerifyFileIdentity(path, file, openedInfo, content, true)
|
|
}
|
|
|
|
func writeAndVerifyFileIdentity(path string, file *os.File, openedInfo os.FileInfo, content string, truncate bool) error {
|
|
if truncate {
|
|
if err := file.Truncate(0); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
}
|
|
if _, err := io.WriteString(file, content); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return err
|
|
}
|
|
pathInfo, err := os.Stat(path)
|
|
if err != nil || !os.SameFile(openedInfo, pathInfo) {
|
|
return fmt.Errorf("%w: path identity changed", errFileChangedDuringWrite)
|
|
}
|
|
return nil
|
|
}
|