Files
雷电芽衣 c1caed3d42 chore(go): audit transaction engine
Share change filtering between the durable and test transaction paths,
collapse repeated precondition handling, unify journal sequence parsing,
and split the subprocess worker test file.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:20:38 -04:00

230 lines
5.7 KiB
Go

package plugins
import (
"bytes"
"errors"
"fmt"
"os"
"runtime"
)
type renameFunc func(source, destination string) error
type replaceWithBackupFunc func(target, replacement, backup string) error
type fileChange struct {
filePath string
label string
original []byte
originalInfo os.FileInfo
originalID string
originalMode os.FileMode
existed bool
next []byte
mode os.FileMode
remove bool
}
type stagedChange struct {
change fileChange
temporaryPath string
temporaryInfo os.FileInfo
rollbackPath string
originalCaptured bool
committedInfo os.FileInfo
committed bool
}
type transactionFileOps struct {
moveNoReplace renameFunc
compensateNoReplace renameFunc
replaceWithBackup replaceWithBackupFunc
production bool
}
func newTransactionFileOps(rename renameFunc) transactionFileOps {
if rename == nil {
rename = atomicRenameNoReplace
return transactionFileOps{
moveNoReplace: rename,
compensateNoReplace: rename,
replaceWithBackup: atomicReplaceWithBackup,
production: true,
}
}
return transactionFileOps{
moveNoReplace: rename,
compensateNoReplace: atomicRenameNoReplace,
}
}
func sameManagedFileMode(current, expected os.FileMode) bool {
return runtime.GOOS == "windows" || current.Perm() == expected.Perm()
}
func readOptionalFile(path, label string) ([]byte, bool, error) {
snapshot, err := readManagedFile(path, label, maxManagedSourceBytes)
if err != nil {
return nil, false, err
}
if snapshot == nil {
return nil, false, nil
}
return append([]byte(nil), snapshot.contents...), true, nil
}
func prepareFileChange(filePath, label string, next []byte, mode os.FileMode) (*fileChange, error) {
original, existed, err := readOptionalFile(filePath, label)
if err != nil {
return nil, err
}
return prepareSourceChange(filePath, label, original, existed, next, mode, false)
}
func prepareSourceChange(
filePath, label string,
original []byte,
existed bool,
next []byte,
mode os.FileMode,
remove bool,
) (*fileChange, error) {
if !existed {
original = nil
}
if !remove && int64(len(next)) > maxManagedSourceBytes {
return nil, fmt.Errorf(
"%s exceeds %d bytes: %s",
label,
maxManagedSourceBytes,
filePath,
)
}
snapshot, err := readManagedFile(filePath, label, maxManagedSourceBytes)
if err != nil {
return nil, err
}
currentExists := snapshot != nil
if currentExists != existed ||
(currentExists && !bytes.Equal(snapshot.contents, original)) {
return nil, fmt.Errorf("%s changed before update: %s", label, filePath)
}
if existed && !remove && bytes.Equal(original, next) &&
sameManagedFileMode(snapshot.mode, mode) {
return nil, nil
}
if !existed && (remove || next == nil) {
return nil, nil
}
originalMode := mode
var originalInfo os.FileInfo
if existed {
originalInfo = snapshot.info
originalMode = snapshot.mode
}
return &fileChange{
filePath: filePath, label: label, original: append([]byte(nil), original...),
originalInfo: originalInfo, originalID: snapshotIdentity(snapshot),
originalMode: originalMode, existed: existed,
next: append([]byte(nil), next...),
mode: mode, remove: remove,
}, nil
}
func snapshotIdentity(snapshot *managedFileSnapshot) string {
if snapshot == nil {
return ""
}
return snapshot.identity
}
func writeChanges(
changes []*fileChange,
label string,
rename renameFunc,
preconditions ...filePrecondition,
) error {
return writeChangesWithFileOps(
changes,
label,
newTransactionFileOps(rename),
preconditions...,
)
}
func writeChangesWithFileOps(
changes []*fileChange,
label string,
ops transactionFileOps,
preconditions ...filePrecondition,
) error {
if ops.production {
return writeDurableChanges(changes, label, ops, preconditions...)
}
if ops.moveNoReplace == nil {
return fmt.Errorf("%s is missing atomic file operations", label)
}
filtered, err := filterDurableChanges(changes, label)
if err != nil {
return err
}
checkPreconditions := func() error {
if err := assertFilePreconditions(preconditions, label); err != nil {
return fmt.Errorf("%s: %w", label, err)
}
return nil
}
if err := checkPreconditions(); err != nil {
return err
}
if len(filtered) == 0 {
return nil
}
staged := make([]stagedChange, 0, len(filtered))
abortStaging := func(cause error) error {
return joinTransactionFailure(cause, cleanupStaging(staged), "cleanup failed")
}
for _, change := range filtered {
if err := checkPreconditions(); err != nil {
return abortStaging(err)
}
item, err := stageChange(change)
if err != nil {
return abortStaging(fmt.Errorf("%s: %w", label, err))
}
staged = append(staged, item)
}
if err := checkPreconditions(); err != nil {
return abortStaging(err)
}
recoverStaged := func(cause error) error {
failures := rollbackChanges(staged, ops)
failures = append(failures, cleanupStaging(staged)...)
return joinTransactionFailure(cause, failures, "recovery failed")
}
for index := range staged {
if err := checkPreconditions(); err != nil {
return recoverStaged(err)
}
if err := commitChange(&staged[index], ops); err != nil {
return recoverStaged(fmt.Errorf("%s: %w", label, err))
}
}
if err := checkPreconditions(); err != nil {
return recoverStaged(err)
}
if err := assertCommittedChanges(staged); err != nil {
return recoverStaged(fmt.Errorf("%s: %w", label, err))
}
if failures := cleanupStaging(staged); len(failures) > 0 {
return joinTransactionFailure(fmt.Errorf("%s cleanup failed", label), failures, "cleanup failed")
}
return nil
}
func joinTransactionFailure(cause error, related []error, label string) error {
if len(related) == 0 {
return cause
}
return errors.Join(cause, fmt.Errorf("%s: %w", label, errors.Join(related...)))
}