f28d85fad5
Most corruption cases except xattr should be covered. With test filesystem. The output is still pretty ugly. xattr support will be added in the next commits.
415 lines
9.5 KiB
Go
415 lines
9.5 KiB
Go
package test_helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rfjakob/gocryptfs/internal/ctlsock"
|
|
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
)
|
|
|
|
// TmpDir will be created inside this directory
|
|
var testParentDir = "/tmp/gocryptfs-test-parent"
|
|
|
|
// GocryptfsBinary is the assumed path to the gocryptfs build.
|
|
const GocryptfsBinary = "../../gocryptfs"
|
|
|
|
// UnmountScript is the fusermount/umount compatibility wrapper script
|
|
const UnmountScript = "../fuse-unmount.bash"
|
|
|
|
// X255 contains 255 uppercase "X". This can be used as a maximum-length filename.
|
|
var X255 string
|
|
|
|
// TmpDir is a unique temporary directory. "go test" runs package tests in parallel. We create a
|
|
// unique TmpDir in init() so the tests do not interfere.
|
|
var TmpDir string
|
|
|
|
// DefaultPlainDir is TmpDir + "/default-plain"
|
|
var DefaultPlainDir string
|
|
|
|
// DefaultCipherDir is TmpDir + "/default-cipher"
|
|
var DefaultCipherDir string
|
|
|
|
// SwitchTestParentDir changes testParentDir. This is used when you want
|
|
// to perform tests on a special filesystem. For example, the xattr tests
|
|
// cannot run on tmpfs and use /var/tmp instead of /tmp.
|
|
func SwitchTestParentDir(newDir string) {
|
|
testParentDir = newDir
|
|
doInit()
|
|
}
|
|
|
|
func init() {
|
|
doInit()
|
|
}
|
|
|
|
func doInit() {
|
|
X255 = string(bytes.Repeat([]byte("X"), 255))
|
|
|
|
os.MkdirAll(testParentDir, 0700)
|
|
var err error
|
|
TmpDir, err = ioutil.TempDir(testParentDir, "")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
DefaultPlainDir = TmpDir + "/default-plain"
|
|
DefaultCipherDir = TmpDir + "/default-cipher"
|
|
}
|
|
|
|
// ResetTmpDir deletes TmpDir, create new dir tree:
|
|
//
|
|
// TmpDir
|
|
// |-- DefaultPlainDir
|
|
// *-- DefaultCipherDir
|
|
// *-- gocryptfs.diriv
|
|
func ResetTmpDir(createDirIV bool) {
|
|
// Try to unmount and delete everything
|
|
entries, err := ioutil.ReadDir(TmpDir)
|
|
if err == nil {
|
|
for _, e := range entries {
|
|
d := filepath.Join(TmpDir, e.Name())
|
|
err = os.Remove(d)
|
|
if err != nil {
|
|
pe := err.(*os.PathError)
|
|
if pe.Err == syscall.EBUSY {
|
|
if testing.Verbose() {
|
|
fmt.Printf("Remove failed: %v. Maybe still mounted?\n", pe)
|
|
}
|
|
err = UnmountErr(d)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else if pe.Err != syscall.ENOTEMPTY {
|
|
panic("Unhandled error: " + pe.Err.Error())
|
|
}
|
|
err = os.RemoveAll(d)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
err = os.Mkdir(DefaultPlainDir, 0700)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = os.Mkdir(DefaultCipherDir, 0700)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if createDirIV {
|
|
err = nametransform.WriteDirIV(nil, DefaultCipherDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// InitFS calls "gocryptfs -init" on a new directory in TmpDir, passing
|
|
// "extraArgs" in addition to useful defaults.
|
|
//
|
|
// The returned cipherdir has NO trailing slash.
|
|
func InitFS(t *testing.T, extraArgs ...string) string {
|
|
dir, err := ioutil.TempDir(TmpDir, "")
|
|
if err != nil {
|
|
if t != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
log.Panic(err)
|
|
}
|
|
}
|
|
args := []string{"-q", "-init", "-extpass", "echo test", "-scryptn=10"}
|
|
args = append(args, extraArgs...)
|
|
args = append(args, dir)
|
|
|
|
cmd := exec.Command(GocryptfsBinary, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
if t != nil {
|
|
t.Fatalf("InitFS with args %v failed: %v", args, err)
|
|
} else {
|
|
log.Panic(err)
|
|
}
|
|
}
|
|
|
|
return dir
|
|
}
|
|
|
|
// Mount CIPHERDIR "c" on PLAINDIR "p"
|
|
// Creates "p" if it does not exist.
|
|
func Mount(c string, p string, showOutput bool, extraArgs ...string) error {
|
|
var args []string
|
|
args = append(args, "-q", "-wpanic", "-nosyslog")
|
|
args = append(args, extraArgs...)
|
|
//args = append(args, "-fusedebug")
|
|
//args = append(args, "-d")
|
|
args = append(args, c)
|
|
args = append(args, p)
|
|
|
|
if _, err := os.Stat(p); err != nil {
|
|
err = os.Mkdir(p, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(GocryptfsBinary, args...)
|
|
if showOutput {
|
|
// The Go test logic waits for our stdout to close, and when we share
|
|
// it with the subprocess, it will wait for it to close it as well.
|
|
// Use an intermediate pipe so the tests do not hang when unmouting
|
|
// fails.
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Stderr = pw
|
|
cmd.Stdout = pw
|
|
go func() { io.Copy(os.Stdout, pr) }()
|
|
}
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
// MountOrExit calls Mount() and exits on failure.
|
|
func MountOrExit(c string, p string, extraArgs ...string) {
|
|
err := Mount(c, p, true, extraArgs...)
|
|
if err != nil {
|
|
fmt.Printf("mount failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// MountOrFatal calls Mount() and calls t.Fatal() on failure.
|
|
func MountOrFatal(t *testing.T, c string, p string, extraArgs ...string) {
|
|
err := Mount(c, p, true, extraArgs...)
|
|
if err != nil {
|
|
t.Fatal(fmt.Errorf("mount failed: %v", err))
|
|
}
|
|
}
|
|
|
|
// UnmountPanic tries to umount "dir" and panics on error.
|
|
func UnmountPanic(dir string) {
|
|
err := UnmountErr(dir)
|
|
if err != nil {
|
|
fmt.Printf("UnmountPanic: %v. Running lsof %s\n", err, dir)
|
|
cmd := exec.Command("lsof", dir)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Run()
|
|
panic("UnmountPanic: unmount failed: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// UnmountErr tries to unmount "dir" and returns the resulting error.
|
|
func UnmountErr(dir string) error {
|
|
cmd := exec.Command(UnmountScript, "-u", dir)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// Md5fn returns an md5 string for file "filename"
|
|
func Md5fn(filename string) string {
|
|
buf, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
fmt.Printf("ReadFile: %v\n", err)
|
|
return ""
|
|
}
|
|
return Md5hex(buf)
|
|
}
|
|
|
|
// Md5hex returns an md5 string for "buf"
|
|
func Md5hex(buf []byte) string {
|
|
rawHash := md5.Sum(buf)
|
|
hash := hex.EncodeToString(rawHash[:])
|
|
return hash
|
|
}
|
|
|
|
// VerifySize checks that the file size equals "want". This checks:
|
|
// 1) Size reported by Stat()
|
|
// 2) Number of bytes returned when reading the whole file
|
|
func VerifySize(t *testing.T, path string, want int) {
|
|
buf, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
t.Errorf("ReadFile failed: %v", err)
|
|
} else if len(buf) != want {
|
|
t.Errorf("wrong read size: got=%d want=%d", len(buf), want)
|
|
}
|
|
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Errorf("Stat failed: %v", err)
|
|
} else if fi.Size() != int64(want) {
|
|
t.Errorf("wrong stat file size, got=%d want=%d", fi.Size(), want)
|
|
}
|
|
}
|
|
|
|
// TestMkdirRmdir creates and deletes a directory
|
|
func TestMkdirRmdir(t *testing.T, plainDir string) {
|
|
dir := plainDir + "/dir1"
|
|
err := os.Mkdir(dir, 0777)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
err = syscall.Rmdir(dir)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
// Removing a non-empty dir should fail with ENOTEMPTY
|
|
if os.Mkdir(dir, 0777) != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
f, err := os.Create(dir + "/file")
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
f.Close()
|
|
err = syscall.Rmdir(dir)
|
|
errno := err.(syscall.Errno)
|
|
if errno != syscall.ENOTEMPTY {
|
|
t.Errorf("Should have gotten ENOTEMPTY, go %v", errno)
|
|
}
|
|
if syscall.Unlink(dir+"/file") != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if syscall.Rmdir(dir) != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
// We should also be able to remove a directory we do not have permissions to
|
|
// read or write
|
|
err = os.Mkdir(dir, 0000)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
err = syscall.Rmdir(dir)
|
|
if err != nil {
|
|
// Make sure the directory can cleaned up by the next test run
|
|
os.Chmod(dir, 0700)
|
|
t.Error(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// TestRename creates and renames a file
|
|
func TestRename(t *testing.T, plainDir string) {
|
|
file1 := plainDir + "/rename1"
|
|
file2 := plainDir + "/rename2"
|
|
err := ioutil.WriteFile(file1, []byte("content"), 0777)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
err = syscall.Rename(file1, file2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
syscall.Unlink(file2)
|
|
}
|
|
|
|
// VerifyExistence checks in 3 ways that "path" exists:
|
|
// stat, open, readdir
|
|
func VerifyExistence(path string) bool {
|
|
// Check that file can be stated
|
|
_, err := os.Stat(path)
|
|
if err != nil {
|
|
//t.Log(err)
|
|
return false
|
|
}
|
|
// Check that file can be opened
|
|
fd, err := os.Open(path)
|
|
if err != nil {
|
|
//t.Log(err)
|
|
return false
|
|
}
|
|
fd.Close()
|
|
// Check that file shows up in directory listing
|
|
dir := filepath.Dir(path)
|
|
name := filepath.Base(path)
|
|
fi, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
//t.Log(err)
|
|
return false
|
|
}
|
|
for _, i := range fi {
|
|
if i.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Du returns the disk usage of the file "fd" points to, in bytes.
|
|
// Same as "du --block-size=1".
|
|
func Du(t *testing.T, fd int) (nBytes int64) {
|
|
var st syscall.Stat_t
|
|
err := syscall.Fstat(fd, &st)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// st.Blocks = number of 512-byte blocks
|
|
return st.Blocks * 512
|
|
}
|
|
|
|
// QueryCtlSock sends a request to the control socket at "socketPath" and
|
|
// returns the response.
|
|
func QueryCtlSock(t *testing.T, socketPath string, req ctlsock.RequestStruct) (response ctlsock.ResponseStruct) {
|
|
conn, err := net.DialTimeout("unix", socketPath, 1*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(time.Second))
|
|
msg, err := json.Marshal(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = conn.Write(msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buf := make([]byte, ctlsock.ReadBufSize)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buf = buf[:n]
|
|
json.Unmarshal(buf, &response)
|
|
return response
|
|
}
|
|
|
|
// Extract the exit code from an error value that was returned from
|
|
// exec / cmd.Run()
|
|
func ExtractCmdExitCode(err error) int {
|
|
if err == nil {
|
|
return 0
|
|
}
|
|
// OMG this is convoluted
|
|
err2 := err.(*exec.ExitError)
|
|
code := err2.Sys().(syscall.WaitStatus).ExitStatus()
|
|
return code
|
|
}
|