Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for zstd compressed layers #562

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
71 changes: 52 additions & 19 deletions dive/image/docker/image_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"path"
"strings"

"github.com/klauspost/compress/zstd"

"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
Expand Down Expand Up @@ -77,6 +79,26 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
return img, err
}

// add the layer to the image
img.layerMap[tree.Name] = tree
} else if strings.HasSuffix(name, ".tar.zst") || strings.HasSuffix(name, ".zst") {
currentLayer++

// Add zstd reader
zst, err := zstd.NewReader(tarReader)
if err != nil {
return img, err
}

// Add tar reader
layerReader := tar.NewReader(zst)

// Process layer
tree, err := processLayerTar(name, layerReader)
if err != nil {
return img, err
}

// add the layer to the image
img.layerMap[tree.Name] = tree
} else if strings.HasSuffix(name, ".json") || strings.HasPrefix(name, "sha256:") {
Expand All @@ -89,36 +111,47 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
// For the OCI-compatible image format (used since Docker 25), use mime sniffing
// but limit this to only the blobs/ (containing the config, and the layers)

// The idea here is that we try various formats in turn, and those tries should
// never consume more bytes than this buffer contains so we can start again.
// The idea here is that we read the first few bytes of the file to determine if
// it's encrypted with gzip or zstd. If it is, we'll decompress it first.

// There is one edge case to consider: if the tar file's first file's name begins
// with 0x1f8b or 0x28b52ffd, the file will be erronously decompressed. This is
// very unlikely: filenames usually are not binary, but it's worth mentioning.

// 512 bytes ought to be enough (as that's the size of a TAR entry header),
// but play it safe with 1024 bytes. This should also include very small layers
// (unless they've also been gzipped, but Docker does not appear to do it)
buffer := make([]byte, 1024)
n, err := io.ReadFull(tarReader, buffer)

if err != nil && err != io.ErrUnexpectedEOF {
return img, err
}

// Only try reading a TAR if file is "big enough"
if n == cap(buffer) {
var unwrappedReader io.Reader
var unwrappedReader io.Reader

if n >= 2 && buffer[0] == 0x1f && buffer[1] == 0x8b {
// This is a gzipped file
unwrappedReader, err = gzip.NewReader(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
if err != nil {
// Not a gzipped entry
unwrappedReader = io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
return img, err
}

// Try reading a TAR
layerReader := tar.NewReader(unwrappedReader)
tree, err := processLayerTar(name, layerReader)
if err == nil {
currentLayer++
// add the layer to the image
img.layerMap[tree.Name] = tree
continue
} else if n >= 4 && buffer[0] == 0x28 && buffer[1] == 0xb5 && buffer[2] == 0x2f && buffer[3] == 0xfd {
// This is a zstd file
unwrappedReader, err = zstd.NewReader(io.MultiReader(bytes.NewReader(buffer[:n]), tarReader))
if err != nil {
return img, err
}
} else {
// This is not a compressed file
unwrappedReader = io.MultiReader(bytes.NewReader(buffer[:n]), tarReader)
}

// Try reading a TAR
layerReader := tar.NewReader(unwrappedReader)
tree, err := processLayerTar(name, layerReader)
if err == nil {
currentLayer++
// add the layer to the image
img.layerMap[tree.Name] = tree
continue
}

// Not a TAR (or smaller than our buffer), might be a JSON file
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.0
github.com/google/uuid v1.1.1
github.com/klauspost/compress v1.17.6
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b
github.com/lunixbochs/vtclean v1.0.0
github.com/mitchellh/go-homedir v1.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down