github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/pkg/archive/archive_unix_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package archive // import "github.com/docker/docker/pkg/archive" 5 6 import ( 7 "archive/tar" 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "syscall" 16 "testing" 17 18 "github.com/containerd/containerd/pkg/userns" 19 "github.com/docker/docker/pkg/system" 20 "golang.org/x/sys/unix" 21 "gotest.tools/v3/assert" 22 is "gotest.tools/v3/assert/cmp" 23 "gotest.tools/v3/skip" 24 ) 25 26 func TestCanonicalTarName(t *testing.T) { 27 cases := []struct { 28 in string 29 isDir bool 30 expected string 31 }{ 32 {"foo", false, "foo"}, 33 {"foo", true, "foo/"}, 34 {"foo/bar", false, "foo/bar"}, 35 {"foo/bar", true, "foo/bar/"}, 36 } 37 for _, v := range cases { 38 if canonicalTarName(v.in, v.isDir) != v.expected { 39 t.Fatalf("wrong canonical tar name. expected:%s got:%s", v.expected, canonicalTarName(v.in, v.isDir)) 40 } 41 } 42 } 43 44 func TestChmodTarEntry(t *testing.T) { 45 cases := []struct { 46 in, expected os.FileMode 47 }{ 48 {0000, 0000}, 49 {0777, 0777}, 50 {0644, 0644}, 51 {0755, 0755}, 52 {0444, 0444}, 53 } 54 for _, v := range cases { 55 if out := chmodTarEntry(v.in); out != v.expected { 56 t.Fatalf("wrong chmod. expected:%v got:%v", v.expected, out) 57 } 58 } 59 } 60 61 func TestTarWithHardLink(t *testing.T) { 62 origin, err := os.MkdirTemp("", "docker-test-tar-hardlink") 63 assert.NilError(t, err) 64 defer os.RemoveAll(origin) 65 66 err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700) 67 assert.NilError(t, err) 68 69 err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2")) 70 assert.NilError(t, err) 71 72 var i1, i2 uint64 73 i1, err = getNlink(filepath.Join(origin, "1")) 74 assert.NilError(t, err) 75 76 // sanity check that we can hardlink 77 if i1 != 2 { 78 t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1) 79 } 80 81 dest, err := os.MkdirTemp("", "docker-test-tar-hardlink-dest") 82 assert.NilError(t, err) 83 defer os.RemoveAll(dest) 84 85 // we'll do this in two steps to separate failure 86 fh, err := Tar(origin, Uncompressed) 87 assert.NilError(t, err) 88 89 // ensure we can read the whole thing with no error, before writing back out 90 buf, err := io.ReadAll(fh) 91 assert.NilError(t, err) 92 93 bRdr := bytes.NewReader(buf) 94 err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed}) 95 assert.NilError(t, err) 96 97 i1, err = getInode(filepath.Join(dest, "1")) 98 assert.NilError(t, err) 99 100 i2, err = getInode(filepath.Join(dest, "2")) 101 assert.NilError(t, err) 102 103 assert.Check(t, is.Equal(i1, i2)) 104 } 105 106 func TestTarWithHardLinkAndRebase(t *testing.T) { 107 tmpDir, err := os.MkdirTemp("", "docker-test-tar-hardlink-rebase") 108 assert.NilError(t, err) 109 defer os.RemoveAll(tmpDir) 110 111 origin := filepath.Join(tmpDir, "origin") 112 err = os.Mkdir(origin, 0700) 113 assert.NilError(t, err) 114 115 err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700) 116 assert.NilError(t, err) 117 118 err = os.Link(filepath.Join(origin, "1"), filepath.Join(origin, "2")) 119 assert.NilError(t, err) 120 121 var i1, i2 uint64 122 i1, err = getNlink(filepath.Join(origin, "1")) 123 assert.NilError(t, err) 124 125 // sanity check that we can hardlink 126 if i1 != 2 { 127 t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1) 128 } 129 130 dest := filepath.Join(tmpDir, "dest") 131 bRdr, err := TarResourceRebase(origin, "origin") 132 assert.NilError(t, err) 133 134 dstDir, srcBase := SplitPathDirEntry(origin) 135 _, dstBase := SplitPathDirEntry(dest) 136 content := RebaseArchiveEntries(bRdr, srcBase, dstBase) 137 err = Untar(content, dstDir, &TarOptions{Compression: Uncompressed, NoLchown: true, NoOverwriteDirNonDir: true}) 138 assert.NilError(t, err) 139 140 i1, err = getInode(filepath.Join(dest, "1")) 141 assert.NilError(t, err) 142 i2, err = getInode(filepath.Join(dest, "2")) 143 assert.NilError(t, err) 144 145 assert.Check(t, is.Equal(i1, i2)) 146 } 147 148 // TestUntarParentPathPermissions is a regression test to check that missing 149 // parent directories are created with the expected permissions 150 func TestUntarParentPathPermissions(t *testing.T) { 151 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 152 buf := &bytes.Buffer{} 153 w := tar.NewWriter(buf) 154 err := w.WriteHeader(&tar.Header{Name: "foo/bar"}) 155 assert.NilError(t, err) 156 tmpDir, err := os.MkdirTemp("", t.Name()) 157 assert.NilError(t, err) 158 defer os.RemoveAll(tmpDir) 159 err = Untar(buf, tmpDir, nil) 160 assert.NilError(t, err) 161 162 fi, err := os.Lstat(filepath.Join(tmpDir, "foo")) 163 assert.NilError(t, err) 164 assert.Equal(t, fi.Mode(), 0755|os.ModeDir) 165 } 166 167 func getNlink(path string) (uint64, error) { 168 stat, err := os.Stat(path) 169 if err != nil { 170 return 0, err 171 } 172 statT, ok := stat.Sys().(*syscall.Stat_t) 173 if !ok { 174 return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys()) 175 } 176 // We need this conversion on ARM64 177 //nolint: unconvert 178 return uint64(statT.Nlink), nil 179 } 180 181 func getInode(path string) (uint64, error) { 182 stat, err := os.Stat(path) 183 if err != nil { 184 return 0, err 185 } 186 statT, ok := stat.Sys().(*syscall.Stat_t) 187 if !ok { 188 return 0, fmt.Errorf("expected type *syscall.Stat_t, got %t", stat.Sys()) 189 } 190 return statT.Ino, nil 191 } 192 193 func TestTarWithBlockCharFifo(t *testing.T) { 194 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 195 skip.If(t, userns.RunningInUserNS(), "skipping test that requires initial userns") 196 origin, err := os.MkdirTemp("", "docker-test-tar-hardlink") 197 assert.NilError(t, err) 198 199 defer os.RemoveAll(origin) 200 err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700) 201 assert.NilError(t, err) 202 203 err = system.Mknod(filepath.Join(origin, "2"), unix.S_IFBLK, int(system.Mkdev(int64(12), int64(5)))) 204 assert.NilError(t, err) 205 err = system.Mknod(filepath.Join(origin, "3"), unix.S_IFCHR, int(system.Mkdev(int64(12), int64(5)))) 206 assert.NilError(t, err) 207 err = system.Mknod(filepath.Join(origin, "4"), unix.S_IFIFO, int(system.Mkdev(int64(12), int64(5)))) 208 assert.NilError(t, err) 209 210 dest, err := os.MkdirTemp("", "docker-test-tar-hardlink-dest") 211 assert.NilError(t, err) 212 defer os.RemoveAll(dest) 213 214 // we'll do this in two steps to separate failure 215 fh, err := Tar(origin, Uncompressed) 216 assert.NilError(t, err) 217 218 // ensure we can read the whole thing with no error, before writing back out 219 buf, err := io.ReadAll(fh) 220 assert.NilError(t, err) 221 222 bRdr := bytes.NewReader(buf) 223 err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed}) 224 assert.NilError(t, err) 225 226 changes, err := ChangesDirs(origin, dest) 227 assert.NilError(t, err) 228 229 if len(changes) > 0 { 230 t.Fatalf("Tar with special device (block, char, fifo) should keep them (recreate them when untar) : %v", changes) 231 } 232 } 233 234 // TestTarUntarWithXattr is Unix as Lsetxattr is not supported on Windows 235 func TestTarUntarWithXattr(t *testing.T) { 236 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 237 if _, err := exec.LookPath("setcap"); err != nil { 238 t.Skip("setcap not installed") 239 } 240 if _, err := exec.LookPath("getcap"); err != nil { 241 t.Skip("getcap not installed") 242 } 243 244 origin, err := os.MkdirTemp("", "docker-test-untar-origin") 245 assert.NilError(t, err) 246 defer os.RemoveAll(origin) 247 err = os.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700) 248 assert.NilError(t, err) 249 250 err = os.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700) 251 assert.NilError(t, err) 252 err = os.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700) 253 assert.NilError(t, err) 254 // there is no known Go implementation of setcap/getcap with support for v3 file capability 255 out, err := exec.Command("setcap", "cap_block_suspend+ep", filepath.Join(origin, "2")).CombinedOutput() 256 assert.NilError(t, err, string(out)) 257 258 for _, c := range []Compression{ 259 Uncompressed, 260 Gzip, 261 } { 262 changes, err := tarUntar(t, origin, &TarOptions{ 263 Compression: c, 264 ExcludePatterns: []string{"3"}, 265 }) 266 267 if err != nil { 268 t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err) 269 } 270 271 if len(changes) != 1 || changes[0].Path != "/3" { 272 t.Fatalf("Unexpected differences after tarUntar: %v", changes) 273 } 274 out, err := exec.Command("getcap", filepath.Join(origin, "2")).CombinedOutput() 275 assert.NilError(t, err, string(out)) 276 assert.Check(t, is.Contains(string(out), "cap_block_suspend=ep"), "untar should have kept the 'security.capability' xattr") 277 } 278 } 279 280 func TestCopyInfoDestinationPathSymlink(t *testing.T) { 281 tmpDir, _ := getTestTempDirs(t) 282 defer removeAllPaths(tmpDir) 283 284 root := strings.TrimRight(tmpDir, "/") + "/" 285 286 type FileTestData struct { 287 resource FileData 288 file string 289 expected CopyInfo 290 } 291 292 testData := []FileTestData{ 293 // Create a directory: /tmp/archive-copy-test*/dir1 294 // Test will "copy" file1 to dir1 295 {resource: FileData{filetype: Dir, path: "dir1", permissions: 0740}, file: "file1", expected: CopyInfo{Path: root + "dir1/file1", Exists: false, IsDir: false}}, 296 297 // Create a symlink directory to dir1: /tmp/archive-copy-test*/dirSymlink -> dir1 298 // Test will "copy" file2 to dirSymlink 299 {resource: FileData{filetype: Symlink, path: "dirSymlink", contents: root + "dir1", permissions: 0600}, file: "file2", expected: CopyInfo{Path: root + "dirSymlink/file2", Exists: false, IsDir: false}}, 300 301 // Create a file in tmp directory: /tmp/archive-copy-test*/file1 302 // Test to cover when the full file path already exists. 303 {resource: FileData{filetype: Regular, path: "file1", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "file1", Exists: true}}, 304 305 // Create a directory: /tmp/archive-copy*/dir2 306 // Test to cover when the full directory path already exists 307 {resource: FileData{filetype: Dir, path: "dir2", permissions: 0740}, file: "", expected: CopyInfo{Path: root + "dir2", Exists: true, IsDir: true}}, 308 309 // Create a symlink to a non-existent target: /tmp/archive-copy*/symlink1 -> noSuchTarget 310 // Negative test to cover symlinking to a target that does not exit 311 {resource: FileData{filetype: Symlink, path: "symlink1", contents: "noSuchTarget", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "noSuchTarget", Exists: false}}, 312 313 // Create a file in tmp directory for next test: /tmp/existingfile 314 {resource: FileData{filetype: Regular, path: "existingfile", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}}, 315 316 // Create a symlink to an existing file: /tmp/archive-copy*/symlink2 -> /tmp/existingfile 317 // Test to cover when the parent directory of a new file is a symlink 318 {resource: FileData{filetype: Symlink, path: "symlink2", contents: "existingfile", permissions: 0600}, file: "", expected: CopyInfo{Path: root + "existingfile", Exists: true}}, 319 } 320 321 var dirs []FileData 322 for _, data := range testData { 323 dirs = append(dirs, data.resource) 324 } 325 provisionSampleDir(t, tmpDir, dirs) 326 327 for _, info := range testData { 328 p := filepath.Join(tmpDir, info.resource.path, info.file) 329 ci, err := CopyInfoDestinationPath(p) 330 assert.Check(t, err) 331 assert.Check(t, is.DeepEqual(info.expected, ci)) 332 } 333 }