github.com/kaisenlinux/docker@v0.0.0-20230510090727-ea55db55fac7/engine/pkg/archive/archive_unix_test.go (about)

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