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