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