225 lines
6.8 KiB
Go
225 lines
6.8 KiB
Go
package fileops
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWriteFileAtomicOverwritesAndLeavesNoTempFile(t *testing.T) {
|
|
root := t.TempDir()
|
|
dst := filepath.Join(root, "out", "value.txt")
|
|
|
|
if err := WriteFileAtomic(dst, []byte("one"), 0o644); err != nil {
|
|
t.Fatalf("WriteFileAtomic(first) error = %v", err)
|
|
}
|
|
if err := WriteFileAtomic(dst, []byte("two"), 0o644); err != nil {
|
|
t.Fatalf("WriteFileAtomic(second) error = %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile() error = %v", err)
|
|
}
|
|
if string(data) != "two" {
|
|
t.Fatalf("file content = %q, want %q", string(data), "two")
|
|
}
|
|
|
|
assertNoMatchingTempFiles(t, filepath.Dir(dst), "."+filepath.Base(dst)+".tmp-")
|
|
}
|
|
|
|
func TestWriteFileAtomicCleansTempFileOnInstallFailure(t *testing.T) {
|
|
root := t.TempDir()
|
|
blockedPath := filepath.Join(root, "blocked")
|
|
if err := os.MkdirAll(blockedPath, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(blockedPath) error = %v", err)
|
|
}
|
|
|
|
err := WriteFileAtomic(blockedPath, []byte("data"), 0o644)
|
|
if err == nil {
|
|
t.Fatal("WriteFileAtomic() error = nil, want install failure")
|
|
}
|
|
assertNoMatchingTempFiles(t, root, ".blocked.tmp-")
|
|
}
|
|
|
|
func TestReplaceFileAtomicCancellationPreservesDestinationAndCleansTemp(t *testing.T) {
|
|
root := t.TempDir()
|
|
dst := filepath.Join(root, "value.txt")
|
|
if err := os.WriteFile(dst, []byte("old"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(destination) error = %v", err)
|
|
}
|
|
wantErr := errors.New("cancel replacement")
|
|
|
|
err := ReplaceFileAtomic(dst, []byte("new"), ReplaceFileOptions{
|
|
Mode: WorkspaceFileMode,
|
|
BeforeRename: func() error {
|
|
return wantErr
|
|
},
|
|
})
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("ReplaceFileAtomic() error = %v, want wrapped cancellation", err)
|
|
}
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(destination) error = %v", err)
|
|
}
|
|
if string(data) != "old" {
|
|
t.Fatalf("destination content = %q, want old content", data)
|
|
}
|
|
assertNoMatchingTempFiles(t, root, ".value.txt.tmp-")
|
|
}
|
|
|
|
func TestCopyFileAtomicWithChecksumMatchesDestination(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source.txt")
|
|
dst := filepath.Join(root, "out", "copied.txt")
|
|
if err := os.WriteFile(src, []byte("copied-data"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(source) error = %v", err)
|
|
}
|
|
|
|
checksum, err := CopyFileAtomicWithChecksum(src, dst, 0o644)
|
|
if err != nil {
|
|
t.Fatalf("CopyFileAtomicWithChecksum() error = %v", err)
|
|
}
|
|
wantChecksum := "6e5c3f239e28cc315d57b2fcfc24169369c44a25802c0616a6d7081707fd24df"
|
|
if checksum != wantChecksum {
|
|
t.Fatalf("checksum = %q, want %q", checksum, wantChecksum)
|
|
}
|
|
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(destination) error = %v", err)
|
|
}
|
|
if string(data) != "copied-data" {
|
|
t.Fatalf("destination content = %q, want %q", string(data), "copied-data")
|
|
}
|
|
assertNoMatchingTempFiles(t, filepath.Dir(dst), ".copied.txt.tmp-")
|
|
}
|
|
|
|
func TestWriteReaderAtomicWithChecksumMatchesDestination(t *testing.T) {
|
|
root := t.TempDir()
|
|
dst := filepath.Join(root, "nested", "snapshot.yml")
|
|
payload := "verified reference bytes\n"
|
|
|
|
checksum, err := WriteReaderAtomicWithChecksum(dst, strings.NewReader(payload), WorkspaceFileMode)
|
|
if err != nil {
|
|
t.Fatalf("WriteReaderAtomicWithChecksum() error = %v", err)
|
|
}
|
|
wantChecksum := sha256.Sum256([]byte(payload))
|
|
if checksum != hex.EncodeToString(wantChecksum[:]) {
|
|
t.Fatalf("checksum = %q, want %q", checksum, hex.EncodeToString(wantChecksum[:]))
|
|
}
|
|
data, err := os.ReadFile(dst)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile() error = %v", err)
|
|
}
|
|
if string(data) != payload {
|
|
t.Fatalf("destination = %q, want %q", data, payload)
|
|
}
|
|
}
|
|
|
|
func TestCopyFileAtomicCleansTempFileOnInstallFailure(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source.txt")
|
|
if err := os.WriteFile(src, []byte("copied-data"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(source) error = %v", err)
|
|
}
|
|
|
|
blockedPath := filepath.Join(root, "blocked")
|
|
if err := os.MkdirAll(blockedPath, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(blockedPath) error = %v", err)
|
|
}
|
|
err := CopyFileAtomic(src, blockedPath, 0o644)
|
|
if err == nil {
|
|
t.Fatal("CopyFileAtomic() error = nil, want install failure")
|
|
}
|
|
assertNoMatchingTempFiles(t, root, ".blocked.tmp-")
|
|
}
|
|
|
|
func TestInstallSiblingTempFileSetsPermissions(t *testing.T) {
|
|
root := t.TempDir()
|
|
dst := filepath.Join(root, "payload.json")
|
|
tmpPath := filepath.Join(root, ".payload.tmp")
|
|
if err := os.WriteFile(tmpPath, []byte("{\"ok\":true}\n"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(temp) error = %v", err)
|
|
}
|
|
|
|
if err := InstallSiblingTempFile(tmpPath, dst, 0o644); err != nil {
|
|
t.Fatalf("InstallSiblingTempFile() error = %v", err)
|
|
}
|
|
if _, err := os.Stat(tmpPath); !os.IsNotExist(err) {
|
|
t.Fatalf("temp file still exists: stat err = %v", err)
|
|
}
|
|
info, err := os.Stat(dst)
|
|
if err != nil {
|
|
t.Fatalf("Stat(destination) error = %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0o644 {
|
|
t.Fatalf("destination mode = %o, want 644", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestWriteFileAtomicUsesOrdinaryDestinationPathSemantics(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
if err := os.Symlink(outside, filepath.Join(root, "redirect")); err != nil {
|
|
t.Skipf("Symlink unavailable: %v", err)
|
|
}
|
|
|
|
path := filepath.Join(root, "redirect", "output.txt")
|
|
if err := WriteFileAtomic(path, []byte("new"), 0o640); err != nil {
|
|
t.Fatalf("WriteFileAtomic() error = %v", err)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(outside, "output.txt"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(output) error = %v", err)
|
|
}
|
|
if string(data) != "new" {
|
|
t.Fatalf("outside output = %q, want new", data)
|
|
}
|
|
}
|
|
|
|
func TestWriteFileAtomicReplacesLeafSymlinkWithoutFollowingIt(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := filepath.Join(t.TempDir(), "outside.txt")
|
|
if err := os.WriteFile(outside, []byte("outside"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(outside) error = %v", err)
|
|
}
|
|
destination := filepath.Join(root, "output.txt")
|
|
if err := os.Symlink(outside, destination); err != nil {
|
|
t.Skipf("Symlink unavailable: %v", err)
|
|
}
|
|
|
|
if err := WriteFileAtomic(destination, []byte("inside"), 0o640); err != nil {
|
|
t.Fatalf("WriteFileAtomic() error = %v", err)
|
|
}
|
|
outsideData, err := os.ReadFile(outside)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(outside) error = %v", err)
|
|
}
|
|
if string(outsideData) != "outside" {
|
|
t.Fatalf("outside file = %q, want unchanged", outsideData)
|
|
}
|
|
info, err := os.Lstat(destination)
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 {
|
|
t.Fatalf("destination was not replaced with a regular file: info=%v err=%v", info, err)
|
|
}
|
|
}
|
|
|
|
func assertNoMatchingTempFiles(t *testing.T, dir, prefix string) {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir(%q) error = %v", dir, err)
|
|
}
|
|
for _, e := range entries {
|
|
if strings.HasPrefix(e.Name(), prefix) {
|
|
t.Fatalf("unexpected temp file residue: %s", filepath.Join(dir, e.Name()))
|
|
}
|
|
}
|
|
}
|