github.com/moby/docker@v26.1.3+incompatible/pkg/archive/archive_test.go (about)

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