aboutsummaryrefslogtreecommitdiff
path: root/tests/plaintextnames/file_holes_test.go
blob: f4dfad96de490e9a44da0d1fa4fe0a085285e2ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package plaintextnames

import (
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"syscall"
	"testing"
	"time"

	"github.com/rfjakob/gocryptfs/v2/tests/test_helpers"

	"github.com/rfjakob/gocryptfs/v2/contrib/findholes/holes"
)

func findHolesPretty(t *testing.T, path string) string {
	f, err := os.Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	segments, err := holes.Find(int(f.Fd()))
	if err != nil {
		t.Fatal(err)
	}

	return holes.PrettyPrint(segments)
}

func doTestFileHoleCopy(t *testing.T, name string, writeOffsets []int64) {
	n := "TestFileHoleCopy." + name
	pPath := []string{pDir + "/" + n}
	cPath := []string{cDir + "/" + n}

	os.Remove(pPath[0])
	holes.Create(pPath[0])

	// expected md6
	md5 := test_helpers.Md5fn(pPath[0])

	pSegments := []string{findHolesPretty(t, pPath[0])}
	cSegments := []string{findHolesPretty(t, cPath[0])}

	// create 5 more copies
	for i := 1; i < 5; i++ {
		pPath = append(pPath, fmt.Sprintf("%s.%d", pPath[0], i))
		cPath = append(cPath, fmt.Sprintf("%s.%d", cPath[0], i))

		out, err := exec.Command("cp", "--sparse=auto", pPath[i-1], pPath[i]).CombinedOutput()
		if err != nil {
			t.Fatal(string(out))
		}

		tmp := test_helpers.Md5fn(pPath[0])
		if tmp != md5 {
			t.Errorf("pPath[%d]: wrong md5, have %s, want %s", i, tmp, md5)
		}

		pSegments = append(pSegments, findHolesPretty(t, pPath[i]))
		cSegments = append(cSegments, findHolesPretty(t, cPath[i]))
	}

	// "cp --sparse=auto" checks of the file has fewer blocks on disk than it
	// should have for its size. Only then it will try to create a sparse copy.
	var st syscall.Stat_t
	err := syscall.Stat(pPath[0], &st)
	if err != nil {
		t.Fatal(err)
	}
	// convert 512 byte blocks to 4k blocks
	blocks4k := st.Blocks / 8
	// For more than a few fragments, ext4 allocates one extra block
	blocks4k++
	if blocks4k >= (st.Size+4095)/4096 {
		t.Logf("file will look non-sparse to cp, skipping segment check")
		return
	}

	// Check that size on disk stays the same across copies
	var st0 syscall.Stat_t
	if err := syscall.Stat(pPath[0], &st0); err != nil {
		t.Fatal(err)
	}
	for i := range pSegments {
		var st syscall.Stat_t
		if err := syscall.Stat(pPath[i], &st); err != nil {
			t.Fatal(err)
		}
		// Size on disk fluctuates by +-4kB due to different number of extents
		// (looking at "filefrag -v", it seems like ext4 needs 4kB extra once
		//  you have >=4 extents)
		if st.Blocks != st0.Blocks && st.Blocks != st0.Blocks-8 && st.Blocks != st0.Blocks+8 {
			t.Errorf("error: size changed: st0.Blocks=%d st%d.Blocks=%d", st0.Blocks, i, st.Blocks)
		}
	}

	// Check that hole/data segments stays the same across copies
	out := ""
	same := true
	for i := range pSegments {
		out += fmt.Sprintf("pSegments[%d]:\n%s\n", i, pSegments[i])
		if i < len(pSegments)-1 {
			if pSegments[i+1] != pSegments[i] {
				same = false
				t.Errorf("error: pSegments[%d] is different than pSegments[%d]!", i, i+1)
			}
		}
	}
	out += "------------------------------------\n"
	for i := range cSegments {
		out += fmt.Sprintf("cSegments[%d]:\n%s\n", i, cSegments[i])
		if i < len(pSegments)-1 {
			if cSegments[i+1] != cSegments[i] {
				same = false
				t.Errorf("error: cSegments[%d] is different than cSegments[%d]!", i, i+1)
			}
		}
	}
	if !same {
		t.Log(out)
	}
}

// TestFileHoleCopy creates a sparse file, copies it a few times, and checks if
// the copies are the same (including the location of holes and data sections).
//
// The test runs with -plaintextnames because that makes it easier to manipulate
// cipherdir directly.
func TestFileHoleCopy(t *testing.T) {
	// | hole | x | hole | x | hole |
	// truncate -s 50000 foo && dd if=/dev/zero of=foo bs=1 seek=10000 count=1 conv=notrunc && dd if=/dev/zero of=foo bs=1 seek=30000 count=1 conv=notrunc
	name := "c0"
	c0 := []int64{10000, 30000}
	if !t.Run("c0", func(t *testing.T) { doTestFileHoleCopy(t, name, c0) }) {
		t.Log("aborting further subtests")
		return
	}

	rand.Seed(time.Now().UnixNano())
	for k := 0; k < 100; k++ {
		c1 := make([]int64, 10)
		for i := range c1 {
			c1[i] = int64(rand.Int31n(60000))
		}
		name := fmt.Sprintf("k%d", k)
		if !t.Run(name, func(t *testing.T) { doTestFileHoleCopy(t, name, c1) }) {
			t.Log("aborting further subtests")
			return
		}
	}
}