Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/docker/distribution v2.8.2+incompatible
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/moby/go-archive v0.3.0
github.com/moby/go-archive v0.3.3
github.com/moby/sys/symlink v0.2.0
github.com/pkg/errors v0.9.1
github.com/urfave/cli v1.22.12
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
Expand Down
43 changes: 43 additions & 0 deletions scan/context_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package scan

import (
"archive/tar"
"bytes"
"errors"
"os"
"path/filepath"
"syscall"
"testing"

"golang.org/x/sys/unix"
)

func TestGetContextFromReaderUntarsDeviceNode(t *testing.T) {
// Creating device nodes requires CAP_MKNOD, which ordinary test runners lack.
probe := filepath.Join(t.TempDir(), "ptmx")
if err := unix.Mknod(probe, unix.S_IFCHR|0600, int(unix.Mkdev(5, 2))); err != nil {
if errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) {
t.Skipf("device creation is not permitted: %v", err)
}
t.Fatal(err)
}

archive := createContextTar(t, []contextTarEntry{
{header: tar.Header{Name: "dev/ptmx", Typeflag: tar.TypeChar, Mode: 0640, Devmajor: 5, Devminor: 2}},
})
destination := t.TempDir()
scanner := &Scanner{destinationFolder: destination}
if err := scanner.getContextFromReader(bytes.NewReader(archive)); err != nil {
t.Fatal(err)
}
info, err := os.Lstat(filepath.Join(destination, "dev", "ptmx"))
if err != nil {
t.Fatal(err)
}
if info.Mode()&os.ModeCharDevice == 0 || info.Mode().Perm() != 0640 {
t.Fatalf("extracted device mode = %v, want character device with mode 0640", info.Mode())
}
}
127 changes: 127 additions & 0 deletions scan/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package scan

import (
"archive/tar"
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
)

func TestGetContextFromReaderCreatesImpliedParentForDirectory(t *testing.T) {
const contents = "FROM scratch\n"
archive := createContextTar(t, []contextTarEntry{
{header: tar.Header{Name: "etc/dnf/", Typeflag: tar.TypeDir, Mode: 0755}},
{header: tar.Header{Name: "build/server/deep/", Typeflag: tar.TypeDir, Mode: 0755}},
{header: tar.Header{Name: "build/server/deep/Dockerfile", Typeflag: tar.TypeReg, Mode: 0644}, content: contents},
{header: tar.Header{Name: "src/FieldLevel.SchemaChangeController/marker", Typeflag: tar.TypeReg, Mode: 0644}, content: "present"},
})

destination := t.TempDir()
scanner := &Scanner{destinationFolder: destination}
if err := scanner.getContextFromReader(bytes.NewReader(archive)); err != nil {
t.Fatalf("extract context with an implied parent directory: %v", err)
}

if info, err := os.Stat(filepath.Join(destination, "etc", "dnf")); err != nil || !info.IsDir() {
t.Fatalf("etc/dnf was not extracted as a directory: %v", err)
}
got, err := os.ReadFile(filepath.Join(destination, "build", "server", "deep", "Dockerfile"))
if err != nil {
t.Fatal(err)
}
if string(got) != contents {
t.Fatalf("Dockerfile contents = %q, want %q", got, contents)
}
got, err = os.ReadFile(filepath.Join(destination, "src", "FieldLevel.SchemaChangeController", "marker"))
if err != nil {
t.Fatal(err)
}
if string(got) != "present" {
t.Fatalf("marker contents = %q, want present", got)
}
}

func TestGetContextFromReaderUntarsAbsoluteLinks(t *testing.T) {
t.Run("symlink", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not support absolute Unix symlinks")
}
archive := createContextTar(t, []contextTarEntry{
{header: tar.Header{Name: "var/run", Typeflag: tar.TypeSymlink, Linkname: "/run", Mode: 0777}},
{header: tar.Header{Name: "var/run/existing/non-existing/file", Typeflag: tar.TypeReg, Mode: 0644}, content: "content"},
})
destination := t.TempDir()
scanner := &Scanner{destinationFolder: destination}
if err := scanner.getContextFromReader(bytes.NewReader(archive)); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(filepath.Join(destination, "run", "existing", "non-existing", "file"))
if err != nil {
t.Fatal(err)
}
if string(got) != "content" {
t.Fatalf("extracted content = %q, want %q", got, "content")
}
target, err := os.Readlink(filepath.Join(destination, "var", "run"))
if err != nil {
t.Fatal(err)
}
if target != "/run" {
t.Fatalf("symlink target = %q, want /run", target)
}
})

t.Run("hardlink", func(t *testing.T) {
archive := createContextTar(t, []contextTarEntry{
{header: tar.Header{Name: "usr/bin/perlbug", Typeflag: tar.TypeReg, Mode: 0755}, content: "hello"},
{header: tar.Header{Name: "usr/bin/perlthanks", Typeflag: tar.TypeLink, Linkname: "/usr/bin/perlbug", Mode: 0755}},
})
destination := t.TempDir()
scanner := &Scanner{destinationFolder: destination}
if err := scanner.getContextFromReader(bytes.NewReader(archive)); err != nil {
t.Fatal(err)
}
target, err := os.Stat(filepath.Join(destination, "usr", "bin", "perlbug"))
if err != nil {
t.Fatal(err)
}
link, err := os.Stat(filepath.Join(destination, "usr", "bin", "perlthanks"))
if err != nil {
t.Fatal(err)
}
if !os.SameFile(target, link) {
t.Fatal("absolute-target hardlink does not refer to the extracted file")
}
})
}

type contextTarEntry struct {
header tar.Header
content string
}

func createContextTar(t *testing.T, entries []contextTarEntry) []byte {
t.Helper()
var archive bytes.Buffer
tw := tar.NewWriter(&archive)
for _, entry := range entries {
entry.header.Size = int64(len(entry.content))
entry.header.Uid = os.Getuid()
entry.header.Gid = os.Getgid()
if err := tw.WriteHeader(&entry.header); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(entry.content)); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
return archive.Bytes()
}
Loading