1// +build !windows
2
3/*
4   Copyright The containerd Authors.
5
6   Licensed under the Apache License, Version 2.0 (the "License");
7   you may not use this file except in compliance with the License.
8   You may obtain a copy of the License at
9
10       http://www.apache.org/licenses/LICENSE-2.0
11
12   Unless required by applicable law or agreed to in writing, software
13   distributed under the License is distributed on an "AS IS" BASIS,
14   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   See the License for the specific language governing permissions and
16   limitations under the License.
17*/
18
19package fs
20
21import (
22	"io/ioutil"
23	"os"
24	"path/filepath"
25	"testing"
26
27	"github.com/containerd/continuity/fs/fstest"
28	"github.com/pkg/errors"
29)
30
31type RootCheck struct {
32	unresolved string
33	expected   string
34	scope      func(string) string
35	cause      error
36}
37
38func TestRootPath(t *testing.T) {
39	tests := []struct {
40		name   string
41		apply  fstest.Applier
42		checks []RootCheck
43	}{
44		{
45			name:   "SymlinkAbsolute",
46			apply:  Symlink("/b", "fs/a/d"),
47			checks: Check("fs/a/d/c/data", "b/c/data"),
48		},
49		{
50			name:   "SymlinkRelativePath",
51			apply:  Symlink("a", "fs/i"),
52			checks: Check("fs/i", "fs/a"),
53		},
54		{
55			name:   "SymlinkSkipSymlinksOutsideScope",
56			apply:  Symlink("realdir", "linkdir"),
57			checks: CheckWithScope("foo/bar", "foo/bar", "linkdir"),
58		},
59		{
60			name:   "SymlinkLastLink",
61			apply:  Symlink("/b", "fs/a/d"),
62			checks: Check("fs/a/d", "b"),
63		},
64		{
65			name:  "SymlinkRelativeLinkChangeScope",
66			apply: Symlink("../b", "fs/a/e"),
67			checks: CheckAll(
68				Check("fs/a/e/c/data", "fs/b/c/data"),
69				CheckWithScope("e", "b", "fs/a"), // Original return
70			),
71		},
72		{
73			name:  "SymlinkDeepRelativeLinkChangeScope",
74			apply: Symlink("../../../../test", "fs/a/f"),
75			checks: CheckAll(
76				Check("fs/a/f", "test"),             // Original return
77				CheckWithScope("a/f", "test", "fs"), // Original return
78			),
79		},
80		{
81			name: "SymlinkRelativeLinkChain",
82			apply: fstest.Apply(
83				Symlink("../g", "fs/b/h"),
84				fstest.Symlink("../../../../../../../../../../../../root", "fs/g"),
85			),
86			checks: Check("fs/b/h", "root"),
87		},
88		{
89			name:   "SymlinkBreakoutPath",
90			apply:  Symlink("../i/a", "fs/j/k"),
91			checks: CheckWithScope("k", "i/a", "fs/j"),
92		},
93		{
94			name:   "SymlinkToRoot",
95			apply:  Symlink("/", "foo"),
96			checks: Check("foo", ""),
97		},
98		{
99			name:   "SymlinkSlashDotdot",
100			apply:  Symlink("/../../", "foo"),
101			checks: Check("foo", ""),
102		},
103		{
104			name:   "SymlinkDotdot",
105			apply:  Symlink("../../", "foo"),
106			checks: Check("foo", ""),
107		},
108		{
109			name:   "SymlinkRelativePath2",
110			apply:  Symlink("baz/target", "bar/foo"),
111			checks: Check("bar/foo", "bar/baz/target"),
112		},
113		{
114			name: "SymlinkScopeLink",
115			apply: fstest.Apply(
116				Symlink("root2", "root"),
117				Symlink("../bar", "root2/foo"),
118			),
119			checks: CheckWithScope("foo", "bar", "root"),
120		},
121		{
122			name: "SymlinkSelf",
123			apply: fstest.Apply(
124				Symlink("foo", "root/foo"),
125			),
126			checks: ErrorWithScope("foo", "root", errTooManyLinks),
127		},
128		{
129			name: "SymlinkCircular",
130			apply: fstest.Apply(
131				Symlink("foo", "bar"),
132				Symlink("bar", "foo"),
133			),
134			checks: ErrorWithScope("foo", "", errTooManyLinks), //TODO: Test for circular error
135		},
136		{
137			name: "SymlinkCircularUnderRoot",
138			apply: fstest.Apply(
139				Symlink("baz", "root/bar"),
140				Symlink("../bak", "root/baz"),
141				Symlink("/bar", "root/bak"),
142			),
143			checks: ErrorWithScope("bar", "root", errTooManyLinks), // TODO: Test for circular error
144		},
145		{
146			name: "SymlinkComplexChain",
147			apply: fstest.Apply(
148				fstest.CreateDir("root2", 0777),
149				Symlink("root2", "root"),
150				Symlink("r/s", "root/a"),
151				Symlink("../root/t", "root/r"),
152				Symlink("/../u", "root/root/t/s/b"),
153				Symlink(".", "root/u/c"),
154				Symlink("../v", "root/u/x/y"),
155				Symlink("/../w", "root/u/v"),
156			),
157			checks: CheckWithScope("a/b/c/x/y/z", "w/z", "root"), // Original return
158		},
159		{
160			name: "SymlinkBreakoutNonExistent",
161			apply: fstest.Apply(
162				Symlink("/", "root/slash"),
163				Symlink("/idontexist/../slash", "root/sym"),
164			),
165			checks: CheckWithScope("sym/file", "file", "root"),
166		},
167		{
168			name: "SymlinkNoLexicalCleaning",
169			apply: fstest.Apply(
170				Symlink("/foo/bar", "root/sym"),
171				Symlink("/sym/../baz", "root/hello"),
172			),
173			checks: CheckWithScope("hello", "foo/baz", "root"),
174		},
175	}
176
177	for _, test := range tests {
178		t.Run(test.name, makeRootPathTest(t, test.apply, test.checks))
179	}
180
181	// Add related tests which are unable to follow same pattern
182	t.Run("SymlinkRootScope", testRootPathSymlinkRootScope)
183	t.Run("SymlinkEmpty", testRootPathSymlinkEmpty)
184}
185
186func TestDirectoryCompare(t *testing.T) {
187	for i, tc := range []struct {
188		p1 string
189		p2 string
190		r  int
191	}{
192		{"", "", 0},
193		{"", "/", -1},
194		{"/", "", 1},
195		{"/", "/", 0},
196		{"", "", 0},
197		{"/dir1", "/dir1/", -1},
198		{"/dir1", "/dir1", 0},
199		{"/dir1/", "/dir1", 1},
200		{"/dir1", "/dir2", -1},
201		{"/dir2", "/dir1", 1},
202		{"/dir1/1", "/dir1-1", -1},
203		{"/dir1-1", "/dir1/1", 1},
204		{"/dir1/dir2", "/dir1/dir2", 0},
205		{"/dir1/dir2", "/dir1/dir2/", -1},
206		{"/dir1/dir2", "/dir1/dir2/f1", -1},
207		{"/dir1/dir2/", "/dir1/dir2", 1},
208		{"/dir1/dir2/f1", "/dir1/dir2", 1},
209		{"/dir1/dir2-f1", "/dir1/dir2", 1},
210		{"/dir1/dir2-f1", "/dir1/dir2/", 1},
211		{"/dir1/dir2/", "/dir1/dir2/", 0},
212		{"/dir1/dir2你", "/dir1/dir2a", 1},
213		{"/dir1/dir2你", "/dir1/dir2", 1},
214		{"/dir1/dir2你", "/dir1/dir2/", 1},
215		{"/dir1/dir2你/", "/dir1/dir2/", 1},
216		{"/dir1/dir2你/", "/dir1/dir2你/", 0},
217		{"/dir1/dir2你/", "/dir1/dir2你好/", -1},
218		{"/dir1/dir2你/", "/dir1/dir2你-好/", -1},
219		{"/dir1/dir2你/", "/dir1/dir2好/", -1},
220		{"/dir1/dir2/f1", "/dir1/dir2/f1", 0},
221		{"/d1/d2/d3/d4/d5/d6/d7/d8/d9/d10", "/d1/d2/d3/d4/d5/d6/d7/d8/d9/d10", 0},
222		{"/d1/d2/d3/d4/d5/d6/d7/d8/d9/d10", "/d1/d2/d3/d4/d5/d6/d7/d8/d9/d11", -1},
223	} {
224		r := directoryCompare(tc.p1, tc.p2)
225		if r != tc.r {
226			t.Errorf("[%d] Test case failed, %q <> %q = %d, expected %d", i, tc.p1, tc.p2, r, tc.r)
227		}
228	}
229}
230
231func testRootPathSymlinkRootScope(t *testing.T) {
232	tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRootScope")
233	if err != nil {
234		t.Fatal(err)
235	}
236	defer os.RemoveAll(tmpdir)
237
238	expected, err := filepath.EvalSymlinks(tmpdir)
239	if err != nil {
240		t.Fatal(err)
241	}
242	rewrite, err := RootPath("/", tmpdir)
243	if err != nil {
244		t.Fatal(err)
245	}
246	if rewrite != expected {
247		t.Fatalf("expected %q got %q", expected, rewrite)
248	}
249}
250
251func testRootPathSymlinkEmpty(t *testing.T) {
252	wd, err := os.Getwd()
253	if err != nil {
254		t.Fatal(err)
255	}
256	res, err := RootPath(wd, "")
257	if err != nil {
258		t.Fatal(err)
259	}
260	if res != wd {
261		t.Fatalf("expected %q got %q", wd, res)
262	}
263}
264
265func makeRootPathTest(t *testing.T, apply fstest.Applier, checks []RootCheck) func(t *testing.T) {
266	return func(t *testing.T) {
267		applyDir, err := ioutil.TempDir("", "test-root-path-")
268		if err != nil {
269			t.Fatalf("Unable to make temp directory: %+v", err)
270		}
271		defer os.RemoveAll(applyDir)
272
273		if apply != nil {
274			if err := apply.Apply(applyDir); err != nil {
275				t.Fatalf("Apply failed: %+v", err)
276			}
277		}
278
279		for i, check := range checks {
280			root := applyDir
281			if check.scope != nil {
282				root = check.scope(root)
283			}
284
285			actual, err := RootPath(root, check.unresolved)
286			if check.cause != nil {
287				if err == nil {
288					t.Errorf("(Check %d) Expected error %q, %q evaluated as %q", i+1, check.cause.Error(), check.unresolved, actual)
289				}
290				if errors.Cause(err) != check.cause {
291					t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
292				}
293			} else {
294				expected := filepath.Join(root, check.expected)
295				if err != nil {
296					t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
297				}
298				if actual != expected {
299					t.Errorf("(Check %d) Unexpected evaluated path %q, expected %q", i+1, actual, expected)
300				}
301			}
302		}
303	}
304}
305
306func Check(unresolved, expected string) []RootCheck {
307	return []RootCheck{
308		{
309			unresolved: unresolved,
310			expected:   expected,
311		},
312	}
313}
314
315func CheckWithScope(unresolved, expected, scope string) []RootCheck {
316	return []RootCheck{
317		{
318			unresolved: unresolved,
319			expected:   expected,
320			scope: func(root string) string {
321				return filepath.Join(root, scope)
322			},
323		},
324	}
325}
326
327func ErrorWithScope(unresolved, scope string, cause error) []RootCheck {
328	return []RootCheck{
329		{
330			unresolved: unresolved,
331			cause:      cause,
332			scope: func(root string) string {
333				return filepath.Join(root, scope)
334			},
335		},
336	}
337}
338
339func CheckAll(checks ...[]RootCheck) []RootCheck {
340	all := make([]RootCheck, 0, len(checks))
341	for _, c := range checks {
342		all = append(all, c...)
343	}
344	return all
345}
346
347func Symlink(oldname, newname string) fstest.Applier {
348	dir := filepath.Dir(newname)
349	if dir != "" {
350		return fstest.Apply(
351			fstest.CreateDir(dir, 0755),
352			fstest.Symlink(oldname, newname),
353		)
354	}
355	return fstest.Symlink(oldname, newname)
356}
357