github.com/ttys3/engine@v17.12.1-ce-rc2+incompatible/pkg/archive/archive_test.go (about) 1 package archive 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/docker/docker/pkg/idtools" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 ) 21 22 var tmp string 23 24 func init() { 25 tmp = "/tmp/" 26 if runtime.GOOS == "windows" { 27 tmp = os.Getenv("TEMP") + `\` 28 } 29 } 30 31 var defaultArchiver = NewDefaultArchiver() 32 33 func defaultTarUntar(src, dst string) error { 34 return defaultArchiver.TarUntar(src, dst) 35 } 36 37 func defaultUntarPath(src, dst string) error { 38 return defaultArchiver.UntarPath(src, dst) 39 } 40 41 func defaultCopyFileWithTar(src, dst string) (err error) { 42 return defaultArchiver.CopyFileWithTar(src, dst) 43 } 44 45 func defaultCopyWithTar(src, dst string) error { 46 return defaultArchiver.CopyWithTar(src, dst) 47 } 48 49 func TestIsArchivePathDir(t *testing.T) { 50 cmd := exec.Command("sh", "-c", "mkdir -p /tmp/archivedir") 51 output, err := cmd.CombinedOutput() 52 if err != nil { 53 t.Fatalf("Fail to create an archive file for test : %s.", output) 54 } 55 if IsArchivePath(tmp + "archivedir") { 56 t.Fatalf("Incorrectly recognised directory as an archive") 57 } 58 } 59 60 func TestIsArchivePathInvalidFile(t *testing.T) { 61 cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=1 of=/tmp/archive && gzip --stdout /tmp/archive > /tmp/archive.gz") 62 output, err := cmd.CombinedOutput() 63 if err != nil { 64 t.Fatalf("Fail to create an archive file for test : %s.", output) 65 } 66 if IsArchivePath(tmp + "archive") { 67 t.Fatalf("Incorrectly recognised invalid tar path as archive") 68 } 69 if IsArchivePath(tmp + "archive.gz") { 70 t.Fatalf("Incorrectly recognised invalid compressed tar path as archive") 71 } 72 } 73 74 func TestIsArchivePathTar(t *testing.T) { 75 whichTar := "tar" 76 cmdStr := fmt.Sprintf("touch /tmp/archivedata && %s -cf /tmp/archive /tmp/archivedata && gzip --stdout /tmp/archive > /tmp/archive.gz", whichTar) 77 cmd := exec.Command("sh", "-c", cmdStr) 78 output, err := cmd.CombinedOutput() 79 if err != nil { 80 t.Fatalf("Fail to create an archive file for test : %s.", output) 81 } 82 if !IsArchivePath(tmp + "/archive") { 83 t.Fatalf("Did not recognise valid tar path as archive") 84 } 85 if !IsArchivePath(tmp + "archive.gz") { 86 t.Fatalf("Did not recognise valid compressed tar path as archive") 87 } 88 } 89 90 func testDecompressStream(t *testing.T, ext, compressCommand string) { 91 cmd := exec.Command("sh", "-c", 92 fmt.Sprintf("touch /tmp/archive && %s /tmp/archive", compressCommand)) 93 output, err := cmd.CombinedOutput() 94 if err != nil { 95 t.Fatalf("Failed to create an archive file for test : %s.", output) 96 } 97 filename := "archive." + ext 98 archive, err := os.Open(tmp + filename) 99 if err != nil { 100 t.Fatalf("Failed to open file %s: %v", filename, err) 101 } 102 defer archive.Close() 103 104 r, err := DecompressStream(archive) 105 if err != nil { 106 t.Fatalf("Failed to decompress %s: %v", filename, err) 107 } 108 if _, err = ioutil.ReadAll(r); err != nil { 109 t.Fatalf("Failed to read the decompressed stream: %v ", err) 110 } 111 if err = r.Close(); err != nil { 112 t.Fatalf("Failed to close the decompressed stream: %v ", err) 113 } 114 } 115 116 func TestDecompressStreamGzip(t *testing.T) { 117 testDecompressStream(t, "gz", "gzip -f") 118 } 119 120 func TestDecompressStreamBzip2(t *testing.T) { 121 testDecompressStream(t, "bz2", "bzip2 -f") 122 } 123 124 func TestDecompressStreamXz(t *testing.T) { 125 if runtime.GOOS == "windows" { 126 t.Skip("Xz not present in msys2") 127 } 128 testDecompressStream(t, "xz", "xz -f") 129 } 130 131 func TestCompressStreamXzUnsupported(t *testing.T) { 132 dest, err := os.Create(tmp + "dest") 133 if err != nil { 134 t.Fatalf("Fail to create the destination file") 135 } 136 defer dest.Close() 137 138 _, err = CompressStream(dest, Xz) 139 if err == nil { 140 t.Fatalf("Should fail as xz is unsupported for compression format.") 141 } 142 } 143 144 func TestCompressStreamBzip2Unsupported(t *testing.T) { 145 dest, err := os.Create(tmp + "dest") 146 if err != nil { 147 t.Fatalf("Fail to create the destination file") 148 } 149 defer dest.Close() 150 151 _, err = CompressStream(dest, Xz) 152 if err == nil { 153 t.Fatalf("Should fail as xz is unsupported for compression format.") 154 } 155 } 156 157 func TestCompressStreamInvalid(t *testing.T) { 158 dest, err := os.Create(tmp + "dest") 159 if err != nil { 160 t.Fatalf("Fail to create the destination file") 161 } 162 defer dest.Close() 163 164 _, err = CompressStream(dest, -1) 165 if err == nil { 166 t.Fatalf("Should fail as xz is unsupported for compression format.") 167 } 168 } 169 170 func TestExtensionInvalid(t *testing.T) { 171 compression := Compression(-1) 172 output := compression.Extension() 173 if output != "" { 174 t.Fatalf("The extension of an invalid compression should be an empty string.") 175 } 176 } 177 178 func TestExtensionUncompressed(t *testing.T) { 179 compression := Uncompressed 180 output := compression.Extension() 181 if output != "tar" { 182 t.Fatalf("The extension of an uncompressed archive should be 'tar'.") 183 } 184 } 185 func TestExtensionBzip2(t *testing.T) { 186 compression := Bzip2 187 output := compression.Extension() 188 if output != "tar.bz2" { 189 t.Fatalf("The extension of a bzip2 archive should be 'tar.bz2'") 190 } 191 } 192 func TestExtensionGzip(t *testing.T) { 193 compression := Gzip 194 output := compression.Extension() 195 if output != "tar.gz" { 196 t.Fatalf("The extension of a bzip2 archive should be 'tar.gz'") 197 } 198 } 199 func TestExtensionXz(t *testing.T) { 200 compression := Xz 201 output := compression.Extension() 202 if output != "tar.xz" { 203 t.Fatalf("The extension of a bzip2 archive should be 'tar.xz'") 204 } 205 } 206 207 func TestCmdStreamLargeStderr(t *testing.T) { 208 cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello") 209 out, _, err := cmdStream(cmd, nil) 210 if err != nil { 211 t.Fatalf("Failed to start command: %s", err) 212 } 213 errCh := make(chan error) 214 go func() { 215 _, err := io.Copy(ioutil.Discard, out) 216 errCh <- err 217 }() 218 select { 219 case err := <-errCh: 220 if err != nil { 221 t.Fatalf("Command should not have failed (err=%.100s...)", err) 222 } 223 case <-time.After(5 * time.Second): 224 t.Fatalf("Command did not complete in 5 seconds; probable deadlock") 225 } 226 } 227 228 func TestCmdStreamBad(t *testing.T) { 229 // TODO Windows: Figure out why this is failing in CI but not locally 230 if runtime.GOOS == "windows" { 231 t.Skip("Failing on Windows CI machines") 232 } 233 badCmd := exec.Command("sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1") 234 out, _, err := cmdStream(badCmd, nil) 235 if err != nil { 236 t.Fatalf("Failed to start command: %s", err) 237 } 238 if output, err := ioutil.ReadAll(out); err == nil { 239 t.Fatalf("Command should have failed") 240 } else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" { 241 t.Fatalf("Wrong error value (%s)", err) 242 } else if s := string(output); s != "hello\n" { 243 t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) 244 } 245 } 246 247 func TestCmdStreamGood(t *testing.T) { 248 cmd := exec.Command("sh", "-c", "echo hello; exit 0") 249 out, _, err := cmdStream(cmd, nil) 250 if err != nil { 251 t.Fatal(err) 252 } 253 if output, err := ioutil.ReadAll(out); err != nil { 254 t.Fatalf("Command should not have failed (err=%s)", err) 255 } else if s := string(output); s != "hello\n" { 256 t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) 257 } 258 } 259 260 func TestUntarPathWithInvalidDest(t *testing.T) { 261 tempFolder, err := ioutil.TempDir("", "docker-archive-test") 262 require.NoError(t, err) 263 defer os.RemoveAll(tempFolder) 264 invalidDestFolder := filepath.Join(tempFolder, "invalidDest") 265 // Create a src file 266 srcFile := filepath.Join(tempFolder, "src") 267 tarFile := filepath.Join(tempFolder, "src.tar") 268 os.Create(srcFile) 269 os.Create(invalidDestFolder) // being a file (not dir) should cause an error 270 271 // Translate back to Unix semantics as next exec.Command is run under sh 272 srcFileU := srcFile 273 tarFileU := tarFile 274 if runtime.GOOS == "windows" { 275 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" 276 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" 277 } 278 279 cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) 280 _, err = cmd.CombinedOutput() 281 require.NoError(t, err) 282 283 err = defaultUntarPath(tarFile, invalidDestFolder) 284 if err == nil { 285 t.Fatalf("UntarPath with invalid destination path should throw an error.") 286 } 287 } 288 289 func TestUntarPathWithInvalidSrc(t *testing.T) { 290 dest, err := ioutil.TempDir("", "docker-archive-test") 291 if err != nil { 292 t.Fatalf("Fail to create the destination file") 293 } 294 defer os.RemoveAll(dest) 295 err = defaultUntarPath("/invalid/path", dest) 296 if err == nil { 297 t.Fatalf("UntarPath with invalid src path should throw an error.") 298 } 299 } 300 301 func TestUntarPath(t *testing.T) { 302 tmpFolder, err := ioutil.TempDir("", "docker-archive-test") 303 require.NoError(t, err) 304 defer os.RemoveAll(tmpFolder) 305 srcFile := filepath.Join(tmpFolder, "src") 306 tarFile := filepath.Join(tmpFolder, "src.tar") 307 os.Create(filepath.Join(tmpFolder, "src")) 308 309 destFolder := filepath.Join(tmpFolder, "dest") 310 err = os.MkdirAll(destFolder, 0740) 311 if err != nil { 312 t.Fatalf("Fail to create the destination file") 313 } 314 315 // Translate back to Unix semantics as next exec.Command is run under sh 316 srcFileU := srcFile 317 tarFileU := tarFile 318 if runtime.GOOS == "windows" { 319 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" 320 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" 321 } 322 cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) 323 _, err = cmd.CombinedOutput() 324 require.NoError(t, err) 325 326 err = defaultUntarPath(tarFile, destFolder) 327 if err != nil { 328 t.Fatalf("UntarPath shouldn't throw an error, %s.", err) 329 } 330 expectedFile := filepath.Join(destFolder, srcFileU) 331 _, err = os.Stat(expectedFile) 332 if err != nil { 333 t.Fatalf("Destination folder should contain the source file but did not.") 334 } 335 } 336 337 // Do the same test as above but with the destination as file, it should fail 338 func TestUntarPathWithDestinationFile(t *testing.T) { 339 tmpFolder, err := ioutil.TempDir("", "docker-archive-test") 340 if err != nil { 341 t.Fatal(err) 342 } 343 defer os.RemoveAll(tmpFolder) 344 srcFile := filepath.Join(tmpFolder, "src") 345 tarFile := filepath.Join(tmpFolder, "src.tar") 346 os.Create(filepath.Join(tmpFolder, "src")) 347 348 // Translate back to Unix semantics as next exec.Command is run under sh 349 srcFileU := srcFile 350 tarFileU := tarFile 351 if runtime.GOOS == "windows" { 352 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" 353 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" 354 } 355 cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) 356 _, err = cmd.CombinedOutput() 357 if err != nil { 358 t.Fatal(err) 359 } 360 destFile := filepath.Join(tmpFolder, "dest") 361 _, err = os.Create(destFile) 362 if err != nil { 363 t.Fatalf("Fail to create the destination file") 364 } 365 err = defaultUntarPath(tarFile, destFile) 366 if err == nil { 367 t.Fatalf("UntarPath should throw an error if the destination if a file") 368 } 369 } 370 371 // Do the same test as above but with the destination folder already exists 372 // and the destination file is a directory 373 // It's working, see https://github.com/docker/docker/issues/10040 374 func TestUntarPathWithDestinationSrcFileAsFolder(t *testing.T) { 375 tmpFolder, err := ioutil.TempDir("", "docker-archive-test") 376 if err != nil { 377 t.Fatal(err) 378 } 379 defer os.RemoveAll(tmpFolder) 380 srcFile := filepath.Join(tmpFolder, "src") 381 tarFile := filepath.Join(tmpFolder, "src.tar") 382 os.Create(srcFile) 383 384 // Translate back to Unix semantics as next exec.Command is run under sh 385 srcFileU := srcFile 386 tarFileU := tarFile 387 if runtime.GOOS == "windows" { 388 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar" 389 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src" 390 } 391 392 cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU) 393 _, err = cmd.CombinedOutput() 394 if err != nil { 395 t.Fatal(err) 396 } 397 destFolder := filepath.Join(tmpFolder, "dest") 398 err = os.MkdirAll(destFolder, 0740) 399 if err != nil { 400 t.Fatalf("Fail to create the destination folder") 401 } 402 // Let's create a folder that will has the same path as the extracted file (from tar) 403 destSrcFileAsFolder := filepath.Join(destFolder, srcFileU) 404 err = os.MkdirAll(destSrcFileAsFolder, 0740) 405 if err != nil { 406 t.Fatal(err) 407 } 408 err = defaultUntarPath(tarFile, destFolder) 409 if err != nil { 410 t.Fatalf("UntarPath should throw not throw an error if the extracted file already exists and is a folder") 411 } 412 } 413 414 func TestCopyWithTarInvalidSrc(t *testing.T) { 415 tempFolder, err := ioutil.TempDir("", "docker-archive-test") 416 if err != nil { 417 t.Fatal(nil) 418 } 419 destFolder := filepath.Join(tempFolder, "dest") 420 invalidSrc := filepath.Join(tempFolder, "doesnotexists") 421 err = os.MkdirAll(destFolder, 0740) 422 if err != nil { 423 t.Fatal(err) 424 } 425 err = defaultCopyWithTar(invalidSrc, destFolder) 426 if err == nil { 427 t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.") 428 } 429 } 430 431 func TestCopyWithTarInexistentDestWillCreateIt(t *testing.T) { 432 tempFolder, err := ioutil.TempDir("", "docker-archive-test") 433 if err != nil { 434 t.Fatal(nil) 435 } 436 srcFolder := filepath.Join(tempFolder, "src") 437 inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists") 438 err = os.MkdirAll(srcFolder, 0740) 439 if err != nil { 440 t.Fatal(err) 441 } 442 err = defaultCopyWithTar(srcFolder, inexistentDestFolder) 443 if err != nil { 444 t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.") 445 } 446 _, err = os.Stat(inexistentDestFolder) 447 if err != nil { 448 t.Fatalf("CopyWithTar with an inexistent folder should create it.") 449 } 450 } 451 452 // Test CopyWithTar with a file as src 453 func TestCopyWithTarSrcFile(t *testing.T) { 454 folder, err := ioutil.TempDir("", "docker-archive-test") 455 if err != nil { 456 t.Fatal(err) 457 } 458 defer os.RemoveAll(folder) 459 dest := filepath.Join(folder, "dest") 460 srcFolder := filepath.Join(folder, "src") 461 src := filepath.Join(folder, filepath.Join("src", "src")) 462 err = os.MkdirAll(srcFolder, 0740) 463 if err != nil { 464 t.Fatal(err) 465 } 466 err = os.MkdirAll(dest, 0740) 467 if err != nil { 468 t.Fatal(err) 469 } 470 ioutil.WriteFile(src, []byte("content"), 0777) 471 err = defaultCopyWithTar(src, dest) 472 if err != nil { 473 t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err) 474 } 475 _, err = os.Stat(dest) 476 // FIXME Check the content 477 if err != nil { 478 t.Fatalf("Destination file should be the same as the source.") 479 } 480 } 481 482 // Test CopyWithTar with a folder as src 483 func TestCopyWithTarSrcFolder(t *testing.T) { 484 folder, err := ioutil.TempDir("", "docker-archive-test") 485 if err != nil { 486 t.Fatal(err) 487 } 488 defer os.RemoveAll(folder) 489 dest := filepath.Join(folder, "dest") 490 src := filepath.Join(folder, filepath.Join("src", "folder")) 491 err = os.MkdirAll(src, 0740) 492 if err != nil { 493 t.Fatal(err) 494 } 495 err = os.MkdirAll(dest, 0740) 496 if err != nil { 497 t.Fatal(err) 498 } 499 ioutil.WriteFile(filepath.Join(src, "file"), []byte("content"), 0777) 500 err = defaultCopyWithTar(src, dest) 501 if err != nil { 502 t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err) 503 } 504 _, err = os.Stat(dest) 505 // FIXME Check the content (the file inside) 506 if err != nil { 507 t.Fatalf("Destination folder should contain the source file but did not.") 508 } 509 } 510 511 func TestCopyFileWithTarInvalidSrc(t *testing.T) { 512 tempFolder, err := ioutil.TempDir("", "docker-archive-test") 513 if err != nil { 514 t.Fatal(err) 515 } 516 defer os.RemoveAll(tempFolder) 517 destFolder := filepath.Join(tempFolder, "dest") 518 err = os.MkdirAll(destFolder, 0740) 519 if err != nil { 520 t.Fatal(err) 521 } 522 invalidFile := filepath.Join(tempFolder, "doesnotexists") 523 err = defaultCopyFileWithTar(invalidFile, destFolder) 524 if err == nil { 525 t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.") 526 } 527 } 528 529 func TestCopyFileWithTarInexistentDestWillCreateIt(t *testing.T) { 530 tempFolder, err := ioutil.TempDir("", "docker-archive-test") 531 if err != nil { 532 t.Fatal(nil) 533 } 534 defer os.RemoveAll(tempFolder) 535 srcFile := filepath.Join(tempFolder, "src") 536 inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists") 537 _, err = os.Create(srcFile) 538 if err != nil { 539 t.Fatal(err) 540 } 541 err = defaultCopyFileWithTar(srcFile, inexistentDestFolder) 542 if err != nil { 543 t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.") 544 } 545 _, err = os.Stat(inexistentDestFolder) 546 if err != nil { 547 t.Fatalf("CopyWithTar with an inexistent folder should create it.") 548 } 549 // FIXME Test the src file and content 550 } 551 552 func TestCopyFileWithTarSrcFolder(t *testing.T) { 553 folder, err := ioutil.TempDir("", "docker-archive-copyfilewithtar-test") 554 if err != nil { 555 t.Fatal(err) 556 } 557 defer os.RemoveAll(folder) 558 dest := filepath.Join(folder, "dest") 559 src := filepath.Join(folder, "srcfolder") 560 err = os.MkdirAll(src, 0740) 561 if err != nil { 562 t.Fatal(err) 563 } 564 err = os.MkdirAll(dest, 0740) 565 if err != nil { 566 t.Fatal(err) 567 } 568 err = defaultCopyFileWithTar(src, dest) 569 if err == nil { 570 t.Fatalf("CopyFileWithTar should throw an error with a folder.") 571 } 572 } 573 574 func TestCopyFileWithTarSrcFile(t *testing.T) { 575 folder, err := ioutil.TempDir("", "docker-archive-test") 576 if err != nil { 577 t.Fatal(err) 578 } 579 defer os.RemoveAll(folder) 580 dest := filepath.Join(folder, "dest") 581 srcFolder := filepath.Join(folder, "src") 582 src := filepath.Join(folder, filepath.Join("src", "src")) 583 err = os.MkdirAll(srcFolder, 0740) 584 if err != nil { 585 t.Fatal(err) 586 } 587 err = os.MkdirAll(dest, 0740) 588 if err != nil { 589 t.Fatal(err) 590 } 591 ioutil.WriteFile(src, []byte("content"), 0777) 592 err = defaultCopyWithTar(src, dest+"/") 593 if err != nil { 594 t.Fatalf("archiver.CopyFileWithTar shouldn't throw an error, %s.", err) 595 } 596 _, err = os.Stat(dest) 597 if err != nil { 598 t.Fatalf("Destination folder should contain the source file but did not.") 599 } 600 } 601 602 func TestTarFiles(t *testing.T) { 603 // TODO Windows: Figure out how to port this test. 604 if runtime.GOOS == "windows" { 605 t.Skip("Failing on Windows") 606 } 607 // try without hardlinks 608 if err := checkNoChanges(1000, false); err != nil { 609 t.Fatal(err) 610 } 611 // try with hardlinks 612 if err := checkNoChanges(1000, true); err != nil { 613 t.Fatal(err) 614 } 615 } 616 617 func checkNoChanges(fileNum int, hardlinks bool) error { 618 srcDir, err := ioutil.TempDir("", "docker-test-srcDir") 619 if err != nil { 620 return err 621 } 622 defer os.RemoveAll(srcDir) 623 624 destDir, err := ioutil.TempDir("", "docker-test-destDir") 625 if err != nil { 626 return err 627 } 628 defer os.RemoveAll(destDir) 629 630 _, err = prepareUntarSourceDirectory(fileNum, srcDir, hardlinks) 631 if err != nil { 632 return err 633 } 634 635 err = defaultTarUntar(srcDir, destDir) 636 if err != nil { 637 return err 638 } 639 640 changes, err := ChangesDirs(destDir, srcDir) 641 if err != nil { 642 return err 643 } 644 if len(changes) > 0 { 645 return fmt.Errorf("with %d files and %v hardlinks: expected 0 changes, got %d", fileNum, hardlinks, len(changes)) 646 } 647 return nil 648 } 649 650 func tarUntar(t *testing.T, origin string, options *TarOptions) ([]Change, error) { 651 archive, err := TarWithOptions(origin, options) 652 if err != nil { 653 t.Fatal(err) 654 } 655 defer archive.Close() 656 657 buf := make([]byte, 10) 658 if _, err := archive.Read(buf); err != nil { 659 return nil, err 660 } 661 wrap := io.MultiReader(bytes.NewReader(buf), archive) 662 663 detectedCompression := DetectCompression(buf) 664 compression := options.Compression 665 if detectedCompression.Extension() != compression.Extension() { 666 return nil, fmt.Errorf("Wrong compression detected. Actual compression: %s, found %s", compression.Extension(), detectedCompression.Extension()) 667 } 668 669 tmp, err := ioutil.TempDir("", "docker-test-untar") 670 if err != nil { 671 return nil, err 672 } 673 defer os.RemoveAll(tmp) 674 if err := Untar(wrap, tmp, nil); err != nil { 675 return nil, err 676 } 677 if _, err := os.Stat(tmp); err != nil { 678 return nil, err 679 } 680 681 return ChangesDirs(origin, tmp) 682 } 683 684 func TestTarUntar(t *testing.T) { 685 // TODO Windows: Figure out how to fix this test. 686 if runtime.GOOS == "windows" { 687 t.Skip("Failing on Windows") 688 } 689 origin, err := ioutil.TempDir("", "docker-test-untar-origin") 690 if err != nil { 691 t.Fatal(err) 692 } 693 defer os.RemoveAll(origin) 694 if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { 695 t.Fatal(err) 696 } 697 if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil { 698 t.Fatal(err) 699 } 700 if err := ioutil.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700); err != nil { 701 t.Fatal(err) 702 } 703 704 for _, c := range []Compression{ 705 Uncompressed, 706 Gzip, 707 } { 708 changes, err := tarUntar(t, origin, &TarOptions{ 709 Compression: c, 710 ExcludePatterns: []string{"3"}, 711 }) 712 713 if err != nil { 714 t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err) 715 } 716 717 if len(changes) != 1 || changes[0].Path != "/3" { 718 t.Fatalf("Unexpected differences after tarUntar: %v", changes) 719 } 720 } 721 } 722 723 func TestTarWithOptionsChownOptsAlwaysOverridesIdPair(t *testing.T) { 724 origin, err := ioutil.TempDir("", "docker-test-tar-chown-opt") 725 require.NoError(t, err) 726 727 defer os.RemoveAll(origin) 728 filePath := filepath.Join(origin, "1") 729 err = ioutil.WriteFile(filePath, []byte("hello world"), 0700) 730 require.NoError(t, err) 731 732 idMaps := []idtools.IDMap{ 733 0: { 734 ContainerID: 0, 735 HostID: 0, 736 Size: 65536, 737 }, 738 1: { 739 ContainerID: 0, 740 HostID: 100000, 741 Size: 65536, 742 }, 743 } 744 745 cases := []struct { 746 opts *TarOptions 747 expectedUID int 748 expectedGID int 749 }{ 750 {&TarOptions{ChownOpts: &idtools.IDPair{UID: 1337, GID: 42}}, 1337, 42}, 751 {&TarOptions{ChownOpts: &idtools.IDPair{UID: 100001, GID: 100001}, UIDMaps: idMaps, GIDMaps: idMaps}, 100001, 100001}, 752 {&TarOptions{ChownOpts: &idtools.IDPair{UID: 0, GID: 0}, NoLchown: false}, 0, 0}, 753 {&TarOptions{ChownOpts: &idtools.IDPair{UID: 1, GID: 1}, NoLchown: true}, 1, 1}, 754 {&TarOptions{ChownOpts: &idtools.IDPair{UID: 1000, GID: 1000}, NoLchown: true}, 1000, 1000}, 755 } 756 for _, testCase := range cases { 757 reader, err := TarWithOptions(filePath, testCase.opts) 758 require.NoError(t, err) 759 tr := tar.NewReader(reader) 760 defer reader.Close() 761 for { 762 hdr, err := tr.Next() 763 if err == io.EOF { 764 // end of tar archive 765 break 766 } 767 require.NoError(t, err) 768 assert.Equal(t, hdr.Uid, testCase.expectedUID, "Uid equals expected value") 769 assert.Equal(t, hdr.Gid, testCase.expectedGID, "Gid equals expected value") 770 } 771 } 772 } 773 774 func TestTarWithOptions(t *testing.T) { 775 // TODO Windows: Figure out how to fix this test. 776 if runtime.GOOS == "windows" { 777 t.Skip("Failing on Windows") 778 } 779 origin, err := ioutil.TempDir("", "docker-test-untar-origin") 780 if err != nil { 781 t.Fatal(err) 782 } 783 if _, err := ioutil.TempDir(origin, "folder"); err != nil { 784 t.Fatal(err) 785 } 786 defer os.RemoveAll(origin) 787 if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil { 788 t.Fatal(err) 789 } 790 if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil { 791 t.Fatal(err) 792 } 793 794 cases := []struct { 795 opts *TarOptions 796 numChanges int 797 }{ 798 {&TarOptions{IncludeFiles: []string{"1"}}, 2}, 799 {&TarOptions{ExcludePatterns: []string{"2"}}, 1}, 800 {&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2}, 801 {&TarOptions{IncludeFiles: []string{"1", "1"}}, 2}, 802 {&TarOptions{IncludeFiles: []string{"1"}, RebaseNames: map[string]string{"1": "test"}}, 4}, 803 } 804 for _, testCase := range cases { 805 changes, err := tarUntar(t, origin, testCase.opts) 806 if err != nil { 807 t.Fatalf("Error tar/untar when testing inclusion/exclusion: %s", err) 808 } 809 if len(changes) != testCase.numChanges { 810 t.Errorf("Expected %d changes, got %d for %+v:", 811 testCase.numChanges, len(changes), testCase.opts) 812 } 813 } 814 } 815 816 // Some tar archives such as http://haproxy.1wt.eu/download/1.5/src/devel/haproxy-1.5-dev21.tar.gz 817 // use PAX Global Extended Headers. 818 // Failing prevents the archives from being uncompressed during ADD 819 func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) { 820 hdr := tar.Header{Typeflag: tar.TypeXGlobalHeader} 821 tmpDir, err := ioutil.TempDir("", "docker-test-archive-pax-test") 822 if err != nil { 823 t.Fatal(err) 824 } 825 defer os.RemoveAll(tmpDir) 826 err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil, false) 827 if err != nil { 828 t.Fatal(err) 829 } 830 } 831 832 // Some tar have both GNU specific (huge uid) and Ustar specific (long name) things. 833 // Not supposed to happen (should use PAX instead of Ustar for long name) but it does and it should still work. 834 func TestUntarUstarGnuConflict(t *testing.T) { 835 f, err := os.Open("testdata/broken.tar") 836 if err != nil { 837 t.Fatal(err) 838 } 839 defer f.Close() 840 841 found := false 842 tr := tar.NewReader(f) 843 // Iterate through the files in the archive. 844 for { 845 hdr, err := tr.Next() 846 if err == io.EOF { 847 // end of tar archive 848 break 849 } 850 if err != nil { 851 t.Fatal(err) 852 } 853 if hdr.Name == "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm" { 854 found = true 855 break 856 } 857 } 858 if !found { 859 t.Fatalf("%s not found in the archive", "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm") 860 } 861 } 862 863 func prepareUntarSourceDirectory(numberOfFiles int, targetPath string, makeLinks bool) (int, error) { 864 fileData := []byte("fooo") 865 for n := 0; n < numberOfFiles; n++ { 866 fileName := fmt.Sprintf("file-%d", n) 867 if err := ioutil.WriteFile(filepath.Join(targetPath, fileName), fileData, 0700); err != nil { 868 return 0, err 869 } 870 if makeLinks { 871 if err := os.Link(filepath.Join(targetPath, fileName), filepath.Join(targetPath, fileName+"-link")); err != nil { 872 return 0, err 873 } 874 } 875 } 876 totalSize := numberOfFiles * len(fileData) 877 return totalSize, nil 878 } 879 880 func BenchmarkTarUntar(b *testing.B) { 881 origin, err := ioutil.TempDir("", "docker-test-untar-origin") 882 if err != nil { 883 b.Fatal(err) 884 } 885 tempDir, err := ioutil.TempDir("", "docker-test-untar-destination") 886 if err != nil { 887 b.Fatal(err) 888 } 889 target := filepath.Join(tempDir, "dest") 890 n, err := prepareUntarSourceDirectory(100, origin, false) 891 if err != nil { 892 b.Fatal(err) 893 } 894 defer os.RemoveAll(origin) 895 defer os.RemoveAll(tempDir) 896 897 b.ResetTimer() 898 b.SetBytes(int64(n)) 899 for n := 0; n < b.N; n++ { 900 err := defaultTarUntar(origin, target) 901 if err != nil { 902 b.Fatal(err) 903 } 904 os.RemoveAll(target) 905 } 906 } 907 908 func BenchmarkTarUntarWithLinks(b *testing.B) { 909 origin, err := ioutil.TempDir("", "docker-test-untar-origin") 910 if err != nil { 911 b.Fatal(err) 912 } 913 tempDir, err := ioutil.TempDir("", "docker-test-untar-destination") 914 if err != nil { 915 b.Fatal(err) 916 } 917 target := filepath.Join(tempDir, "dest") 918 n, err := prepareUntarSourceDirectory(100, origin, true) 919 if err != nil { 920 b.Fatal(err) 921 } 922 defer os.RemoveAll(origin) 923 defer os.RemoveAll(tempDir) 924 925 b.ResetTimer() 926 b.SetBytes(int64(n)) 927 for n := 0; n < b.N; n++ { 928 err := defaultTarUntar(origin, target) 929 if err != nil { 930 b.Fatal(err) 931 } 932 os.RemoveAll(target) 933 } 934 } 935 936 func TestUntarInvalidFilenames(t *testing.T) { 937 // TODO Windows: Figure out how to fix this test. 938 if runtime.GOOS == "windows" { 939 t.Skip("Passes but hits breakoutError: platform and architecture is not supported") 940 } 941 for i, headers := range [][]*tar.Header{ 942 { 943 { 944 Name: "../victim/dotdot", 945 Typeflag: tar.TypeReg, 946 Mode: 0644, 947 }, 948 }, 949 { 950 { 951 // Note the leading slash 952 Name: "/../victim/slash-dotdot", 953 Typeflag: tar.TypeReg, 954 Mode: 0644, 955 }, 956 }, 957 } { 958 if err := testBreakout("untar", "docker-TestUntarInvalidFilenames", headers); err != nil { 959 t.Fatalf("i=%d. %v", i, err) 960 } 961 } 962 } 963 964 func TestUntarHardlinkToSymlink(t *testing.T) { 965 // TODO Windows. There may be a way of running this, but turning off for now 966 if runtime.GOOS == "windows" { 967 t.Skip("hardlinks on Windows") 968 } 969 for i, headers := range [][]*tar.Header{ 970 { 971 { 972 Name: "symlink1", 973 Typeflag: tar.TypeSymlink, 974 Linkname: "regfile", 975 Mode: 0644, 976 }, 977 { 978 Name: "symlink2", 979 Typeflag: tar.TypeLink, 980 Linkname: "symlink1", 981 Mode: 0644, 982 }, 983 { 984 Name: "regfile", 985 Typeflag: tar.TypeReg, 986 Mode: 0644, 987 }, 988 }, 989 } { 990 if err := testBreakout("untar", "docker-TestUntarHardlinkToSymlink", headers); err != nil { 991 t.Fatalf("i=%d. %v", i, err) 992 } 993 } 994 } 995 996 func TestUntarInvalidHardlink(t *testing.T) { 997 // TODO Windows. There may be a way of running this, but turning off for now 998 if runtime.GOOS == "windows" { 999 t.Skip("hardlinks on Windows") 1000 } 1001 for i, headers := range [][]*tar.Header{ 1002 { // try reading victim/hello (../) 1003 { 1004 Name: "dotdot", 1005 Typeflag: tar.TypeLink, 1006 Linkname: "../victim/hello", 1007 Mode: 0644, 1008 }, 1009 }, 1010 { // try reading victim/hello (/../) 1011 { 1012 Name: "slash-dotdot", 1013 Typeflag: tar.TypeLink, 1014 // Note the leading slash 1015 Linkname: "/../victim/hello", 1016 Mode: 0644, 1017 }, 1018 }, 1019 { // try writing victim/file 1020 { 1021 Name: "loophole-victim", 1022 Typeflag: tar.TypeLink, 1023 Linkname: "../victim", 1024 Mode: 0755, 1025 }, 1026 { 1027 Name: "loophole-victim/file", 1028 Typeflag: tar.TypeReg, 1029 Mode: 0644, 1030 }, 1031 }, 1032 { // try reading victim/hello (hardlink, symlink) 1033 { 1034 Name: "loophole-victim", 1035 Typeflag: tar.TypeLink, 1036 Linkname: "../victim", 1037 Mode: 0755, 1038 }, 1039 { 1040 Name: "symlink", 1041 Typeflag: tar.TypeSymlink, 1042 Linkname: "loophole-victim/hello", 1043 Mode: 0644, 1044 }, 1045 }, 1046 { // Try reading victim/hello (hardlink, hardlink) 1047 { 1048 Name: "loophole-victim", 1049 Typeflag: tar.TypeLink, 1050 Linkname: "../victim", 1051 Mode: 0755, 1052 }, 1053 { 1054 Name: "hardlink", 1055 Typeflag: tar.TypeLink, 1056 Linkname: "loophole-victim/hello", 1057 Mode: 0644, 1058 }, 1059 }, 1060 { // Try removing victim directory (hardlink) 1061 { 1062 Name: "loophole-victim", 1063 Typeflag: tar.TypeLink, 1064 Linkname: "../victim", 1065 Mode: 0755, 1066 }, 1067 { 1068 Name: "loophole-victim", 1069 Typeflag: tar.TypeReg, 1070 Mode: 0644, 1071 }, 1072 }, 1073 } { 1074 if err := testBreakout("untar", "docker-TestUntarInvalidHardlink", headers); err != nil { 1075 t.Fatalf("i=%d. %v", i, err) 1076 } 1077 } 1078 } 1079 1080 func TestUntarInvalidSymlink(t *testing.T) { 1081 // TODO Windows. There may be a way of running this, but turning off for now 1082 if runtime.GOOS == "windows" { 1083 t.Skip("hardlinks on Windows") 1084 } 1085 for i, headers := range [][]*tar.Header{ 1086 { // try reading victim/hello (../) 1087 { 1088 Name: "dotdot", 1089 Typeflag: tar.TypeSymlink, 1090 Linkname: "../victim/hello", 1091 Mode: 0644, 1092 }, 1093 }, 1094 { // try reading victim/hello (/../) 1095 { 1096 Name: "slash-dotdot", 1097 Typeflag: tar.TypeSymlink, 1098 // Note the leading slash 1099 Linkname: "/../victim/hello", 1100 Mode: 0644, 1101 }, 1102 }, 1103 { // try writing victim/file 1104 { 1105 Name: "loophole-victim", 1106 Typeflag: tar.TypeSymlink, 1107 Linkname: "../victim", 1108 Mode: 0755, 1109 }, 1110 { 1111 Name: "loophole-victim/file", 1112 Typeflag: tar.TypeReg, 1113 Mode: 0644, 1114 }, 1115 }, 1116 { // try reading victim/hello (symlink, symlink) 1117 { 1118 Name: "loophole-victim", 1119 Typeflag: tar.TypeSymlink, 1120 Linkname: "../victim", 1121 Mode: 0755, 1122 }, 1123 { 1124 Name: "symlink", 1125 Typeflag: tar.TypeSymlink, 1126 Linkname: "loophole-victim/hello", 1127 Mode: 0644, 1128 }, 1129 }, 1130 { // try reading victim/hello (symlink, hardlink) 1131 { 1132 Name: "loophole-victim", 1133 Typeflag: tar.TypeSymlink, 1134 Linkname: "../victim", 1135 Mode: 0755, 1136 }, 1137 { 1138 Name: "hardlink", 1139 Typeflag: tar.TypeLink, 1140 Linkname: "loophole-victim/hello", 1141 Mode: 0644, 1142 }, 1143 }, 1144 { // try removing victim directory (symlink) 1145 { 1146 Name: "loophole-victim", 1147 Typeflag: tar.TypeSymlink, 1148 Linkname: "../victim", 1149 Mode: 0755, 1150 }, 1151 { 1152 Name: "loophole-victim", 1153 Typeflag: tar.TypeReg, 1154 Mode: 0644, 1155 }, 1156 }, 1157 { // try writing to victim/newdir/newfile with a symlink in the path 1158 { 1159 // this header needs to be before the next one, or else there is an error 1160 Name: "dir/loophole", 1161 Typeflag: tar.TypeSymlink, 1162 Linkname: "../../victim", 1163 Mode: 0755, 1164 }, 1165 { 1166 Name: "dir/loophole/newdir/newfile", 1167 Typeflag: tar.TypeReg, 1168 Mode: 0644, 1169 }, 1170 }, 1171 } { 1172 if err := testBreakout("untar", "docker-TestUntarInvalidSymlink", headers); err != nil { 1173 t.Fatalf("i=%d. %v", i, err) 1174 } 1175 } 1176 } 1177 1178 func TestTempArchiveCloseMultipleTimes(t *testing.T) { 1179 reader := ioutil.NopCloser(strings.NewReader("hello")) 1180 tempArchive, err := NewTempArchive(reader, "") 1181 require.NoError(t, err) 1182 buf := make([]byte, 10) 1183 n, err := tempArchive.Read(buf) 1184 require.NoError(t, err) 1185 if n != 5 { 1186 t.Fatalf("Expected to read 5 bytes. Read %d instead", n) 1187 } 1188 for i := 0; i < 3; i++ { 1189 if err = tempArchive.Close(); err != nil { 1190 t.Fatalf("i=%d. Unexpected error closing temp archive: %v", i, err) 1191 } 1192 } 1193 } 1194 1195 func TestReplaceFileTarWrapper(t *testing.T) { 1196 filesInArchive := 20 1197 testcases := []struct { 1198 doc string 1199 filename string 1200 modifier TarModifierFunc 1201 expected string 1202 fileCount int 1203 }{ 1204 { 1205 doc: "Modifier creates a new file", 1206 filename: "newfile", 1207 modifier: createModifier(t), 1208 expected: "the new content", 1209 fileCount: filesInArchive + 1, 1210 }, 1211 { 1212 doc: "Modifier replaces a file", 1213 filename: "file-2", 1214 modifier: createOrReplaceModifier, 1215 expected: "the new content", 1216 fileCount: filesInArchive, 1217 }, 1218 { 1219 doc: "Modifier replaces the last file", 1220 filename: fmt.Sprintf("file-%d", filesInArchive-1), 1221 modifier: createOrReplaceModifier, 1222 expected: "the new content", 1223 fileCount: filesInArchive, 1224 }, 1225 { 1226 doc: "Modifier appends to a file", 1227 filename: "file-3", 1228 modifier: appendModifier, 1229 expected: "fooo\nnext line", 1230 fileCount: filesInArchive, 1231 }, 1232 } 1233 1234 for _, testcase := range testcases { 1235 sourceArchive, cleanup := buildSourceArchive(t, filesInArchive) 1236 defer cleanup() 1237 1238 resultArchive := ReplaceFileTarWrapper( 1239 sourceArchive, 1240 map[string]TarModifierFunc{testcase.filename: testcase.modifier}) 1241 1242 actual := readFileFromArchive(t, resultArchive, testcase.filename, testcase.fileCount, testcase.doc) 1243 assert.Equal(t, testcase.expected, actual, testcase.doc) 1244 } 1245 } 1246 1247 // TestPrefixHeaderReadable tests that files that could be created with the 1248 // version of this package that was built with <=go17 are still readable. 1249 func TestPrefixHeaderReadable(t *testing.T) { 1250 // https://gist.github.com/stevvooe/e2a790ad4e97425896206c0816e1a882#file-out-go 1251 var testFile = []byte("\x1f\x8b\x08\x08\x44\x21\x68\x59\x00\x03\x74\x2e\x74\x61\x72\x00\x4b\xcb\xcf\x67\xa0\x35\x30\x80\x00\x86\x06\x10\x47\x01\xc1\x37\x40\x00\x54\xb6\xb1\xa1\xa9\x99\x09\x48\x25\x1d\x40\x69\x71\x49\x62\x91\x02\xe5\x76\xa1\x79\x84\x21\x91\xd6\x80\x72\xaf\x8f\x82\x51\x30\x0a\x46\x36\x00\x00\xf0\x1c\x1e\x95\x00\x06\x00\x00") 1252 1253 tmpDir, err := ioutil.TempDir("", "prefix-test") 1254 require.NoError(t, err) 1255 defer os.RemoveAll(tmpDir) 1256 err = Untar(bytes.NewReader(testFile), tmpDir, nil) 1257 require.NoError(t, err) 1258 1259 baseName := "foo" 1260 pth := strings.Repeat("a", 100-len(baseName)) + "/" + baseName 1261 1262 _, err = os.Lstat(filepath.Join(tmpDir, pth)) 1263 require.NoError(t, err) 1264 } 1265 1266 func buildSourceArchive(t *testing.T, numberOfFiles int) (io.ReadCloser, func()) { 1267 srcDir, err := ioutil.TempDir("", "docker-test-srcDir") 1268 require.NoError(t, err) 1269 1270 _, err = prepareUntarSourceDirectory(numberOfFiles, srcDir, false) 1271 require.NoError(t, err) 1272 1273 sourceArchive, err := TarWithOptions(srcDir, &TarOptions{}) 1274 require.NoError(t, err) 1275 return sourceArchive, func() { 1276 os.RemoveAll(srcDir) 1277 sourceArchive.Close() 1278 } 1279 } 1280 1281 func createOrReplaceModifier(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) { 1282 return &tar.Header{ 1283 Mode: 0600, 1284 Typeflag: tar.TypeReg, 1285 }, []byte("the new content"), nil 1286 } 1287 1288 func createModifier(t *testing.T) TarModifierFunc { 1289 return func(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) { 1290 assert.Nil(t, content) 1291 return createOrReplaceModifier(path, header, content) 1292 } 1293 } 1294 1295 func appendModifier(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) { 1296 buffer := bytes.Buffer{} 1297 if content != nil { 1298 if _, err := buffer.ReadFrom(content); err != nil { 1299 return nil, nil, err 1300 } 1301 } 1302 buffer.WriteString("\nnext line") 1303 return &tar.Header{Mode: 0600, Typeflag: tar.TypeReg}, buffer.Bytes(), nil 1304 } 1305 1306 func readFileFromArchive(t *testing.T, archive io.ReadCloser, name string, expectedCount int, doc string) string { 1307 destDir, err := ioutil.TempDir("", "docker-test-destDir") 1308 require.NoError(t, err) 1309 defer os.RemoveAll(destDir) 1310 1311 err = Untar(archive, destDir, nil) 1312 require.NoError(t, err) 1313 1314 files, _ := ioutil.ReadDir(destDir) 1315 assert.Len(t, files, expectedCount, doc) 1316 1317 content, err := ioutil.ReadFile(filepath.Join(destDir, name)) 1318 assert.NoError(t, err) 1319 return string(content) 1320 }