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