247 lines
8.4 KiB
Go
247 lines
8.4 KiB
Go
package fileops
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPromoteDirectoryCopiesNestedRegularTree(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source")
|
|
dst := filepath.Join(root, "promoted")
|
|
mustWriteFile(t, filepath.Join(src, "z-last.txt"), []byte("last"), 0o777)
|
|
mustWriteFile(t, filepath.Join(src, "nested", "binary.dat"), []byte{0, 1, 2, 0xff}, 0o600)
|
|
mustWriteFile(t, filepath.Join(src, "a-first.txt"), []byte("first"), 0o400)
|
|
if err := os.Mkdir(filepath.Join(src, "empty"), 0o700); err != nil {
|
|
t.Fatalf("Mkdir(empty) error = %v", err)
|
|
}
|
|
|
|
if err := PromoteDirectory(src, dst); err != nil {
|
|
t.Fatalf("PromoteDirectory() error = %v", err)
|
|
}
|
|
|
|
wantLayout := []string{".", "a-first.txt", "empty", "nested", "nested/binary.dat", "z-last.txt"}
|
|
if got := treeLayout(t, dst); !reflect.DeepEqual(got, wantLayout) {
|
|
t.Fatalf("promoted layout = %#v, want %#v", got, wantLayout)
|
|
}
|
|
assertFileBytes(t, filepath.Join(dst, "a-first.txt"), []byte("first"))
|
|
assertFileBytes(t, filepath.Join(dst, "nested", "binary.dat"), []byte{0, 1, 2, 0xff})
|
|
assertFileBytes(t, filepath.Join(dst, "z-last.txt"), []byte("last"))
|
|
|
|
for _, path := range []string{dst, filepath.Join(dst, "nested"), filepath.Join(dst, "empty")} {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("Stat(%q) error = %v", path, err)
|
|
}
|
|
if got := info.Mode().Perm(); got != promotedDirectoryMode {
|
|
t.Fatalf("directory mode for %q = %o, want %o", path, got, promotedDirectoryMode)
|
|
}
|
|
}
|
|
for _, path := range []string{filepath.Join(dst, "a-first.txt"), filepath.Join(dst, "nested", "binary.dat"), filepath.Join(dst, "z-last.txt")} {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("Stat(%q) error = %v", path, err)
|
|
}
|
|
if got := info.Mode().Perm(); got != promotedFileMode {
|
|
t.Fatalf("file mode for %q = %o, want %o", path, got, promotedFileMode)
|
|
}
|
|
}
|
|
|
|
assertFileBytes(t, filepath.Join(src, "nested", "binary.dat"), []byte{0, 1, 2, 0xff})
|
|
assertNoMatchingTempDirectories(t, root, ".promoted.tmp-")
|
|
}
|
|
|
|
func TestPromoteDirectoryRejectsInvalidPaths(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatalf("Mkdir(source) error = %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
src string
|
|
dst string
|
|
}{
|
|
{name: "empty source", src: "", dst: filepath.Join(root, "out-a")},
|
|
{name: "empty destination", src: src, dst: " "},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if err := PromoteDirectory(test.src, test.dst); err == nil {
|
|
t.Fatal("PromoteDirectory() error = nil, want path validation failure")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromoteDirectoryRejectsExistingDestination(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source")
|
|
dst := filepath.Join(root, "destination")
|
|
mustWriteFile(t, filepath.Join(src, "value.txt"), []byte("source"), 0o644)
|
|
mustWriteFile(t, filepath.Join(dst, "value.txt"), []byte("existing"), 0o644)
|
|
|
|
err := PromoteDirectory(src, dst)
|
|
if err == nil || !strings.Contains(err.Error(), "already exists") {
|
|
t.Fatalf("PromoteDirectory() error = %v, want existing destination error", err)
|
|
}
|
|
assertFileBytes(t, filepath.Join(dst, "value.txt"), []byte("existing"))
|
|
assertFileBytes(t, filepath.Join(src, "value.txt"), []byte("source"))
|
|
assertNoMatchingTempDirectories(t, root, ".destination.tmp-")
|
|
}
|
|
|
|
func TestPromoteDirectoryRejectsNonDirectorySource(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source.txt")
|
|
dst := filepath.Join(root, "destination")
|
|
mustWriteFile(t, src, []byte("source"), 0o644)
|
|
|
|
if err := PromoteDirectory(src, dst); err == nil {
|
|
t.Fatal("PromoteDirectory() error = nil, want non-directory source error")
|
|
}
|
|
assertFileBytes(t, src, []byte("source"))
|
|
if _, err := os.Lstat(dst); !os.IsNotExist(err) {
|
|
t.Fatalf("Lstat(destination) error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestPromoteDirectoryRejectsSymlinksWithoutFollowingThem(t *testing.T) {
|
|
root := t.TempDir()
|
|
externalFile := filepath.Join(root, "external.txt")
|
|
externalDirectory := filepath.Join(root, "external-directory")
|
|
mustWriteFile(t, externalFile, []byte("outside"), 0o644)
|
|
mustWriteFile(t, filepath.Join(externalDirectory, "secret.txt"), []byte("secret"), 0o644)
|
|
|
|
tests := []struct {
|
|
name string
|
|
target string
|
|
link string
|
|
}{
|
|
{name: "file", target: externalFile, link: "file-link"},
|
|
{name: "directory", target: externalDirectory, link: "directory-link"},
|
|
{name: "escaping", target: filepath.Join("..", "external.txt"), link: "escaping-link"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
src := filepath.Join(root, "source-"+test.name)
|
|
dst := filepath.Join(root, "destination-"+test.name)
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatalf("Mkdir(source) error = %v", err)
|
|
}
|
|
if err := os.Symlink(test.target, filepath.Join(src, test.link)); err != nil {
|
|
t.Skipf("Symlink() unavailable: %v", err)
|
|
}
|
|
|
|
if err := PromoteDirectory(src, dst); err == nil {
|
|
t.Fatal("PromoteDirectory() error = nil, want symlink rejection")
|
|
}
|
|
linkInfo, err := os.Lstat(filepath.Join(src, test.link))
|
|
if err != nil || linkInfo.Mode()&os.ModeSymlink == 0 {
|
|
t.Fatalf("source symlink was not preserved: info=%v err=%v", linkInfo, err)
|
|
}
|
|
if _, err := os.Lstat(dst); !os.IsNotExist(err) {
|
|
t.Fatalf("Lstat(destination) error = %v, want not exist", err)
|
|
}
|
|
assertNoMatchingTempDirectories(t, root, ".destination-"+test.name+".tmp-")
|
|
})
|
|
}
|
|
assertFileBytes(t, externalFile, []byte("outside"))
|
|
assertFileBytes(t, filepath.Join(externalDirectory, "secret.txt"), []byte("secret"))
|
|
}
|
|
|
|
func TestPromoteDirectoryDoesNotReplaceDestinationCreatedBeforeInstall(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source")
|
|
dst := filepath.Join(root, "destination")
|
|
mustWriteFile(t, filepath.Join(src, "value.txt"), []byte("source"), 0o644)
|
|
|
|
err := promoteDirectory(src, dst, func(temporary, destination string) error {
|
|
if err := os.Mkdir(destination, 0o755); err != nil {
|
|
t.Fatalf("Mkdir(concurrent destination) error = %v", err)
|
|
}
|
|
mustWriteFile(t, filepath.Join(destination, "value.txt"), []byte("concurrent"), 0o644)
|
|
return renameDirectoryNoReplace(temporary, destination)
|
|
})
|
|
if err == nil {
|
|
t.Fatal("promoteDirectory() error = nil, want no-replace install failure")
|
|
}
|
|
assertFileBytes(t, filepath.Join(dst, "value.txt"), []byte("concurrent"))
|
|
assertFileBytes(t, filepath.Join(src, "value.txt"), []byte("source"))
|
|
assertNoMatchingTempDirectories(t, root, ".destination.tmp-")
|
|
}
|
|
|
|
func TestPromoteDirectoryRejectsDestinationInsideSource(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "source")
|
|
dst := filepath.Join(src, "nested", "destination")
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(destination parent) error = %v", err)
|
|
}
|
|
|
|
if err := PromoteDirectory(src, dst); err == nil {
|
|
t.Fatal("PromoteDirectory() error = nil, want nested destination rejection")
|
|
}
|
|
if _, err := os.Lstat(dst); !os.IsNotExist(err) {
|
|
t.Fatalf("Lstat(destination) error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func mustWriteFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(%q) error = %v", filepath.Dir(path), err)
|
|
}
|
|
if err := os.WriteFile(path, data, mode); err != nil {
|
|
t.Fatalf("WriteFile(%q) error = %v", path, err)
|
|
}
|
|
}
|
|
|
|
func assertFileBytes(t *testing.T, path string, want []byte) {
|
|
t.Helper()
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%q) error = %v", path, err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("ReadFile(%q) = %v, want %v", path, got, want)
|
|
}
|
|
}
|
|
|
|
func treeLayout(t *testing.T, root string) []string {
|
|
t.Helper()
|
|
var layout []string
|
|
err := filepath.WalkDir(root, func(path string, _ os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relative, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layout = append(layout, filepath.ToSlash(relative))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("WalkDir(%q) error = %v", root, err)
|
|
}
|
|
return layout
|
|
}
|
|
|
|
func assertNoMatchingTempDirectories(t *testing.T, parent, prefix string) {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(parent)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir(%q) error = %v", parent, err)
|
|
}
|
|
for _, entry := range entries {
|
|
if strings.HasPrefix(entry.Name(), prefix) {
|
|
t.Fatalf("unexpected temporary directory residue: %s", filepath.Join(parent, entry.Name()))
|
|
}
|
|
}
|
|
}
|