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