diff --git a/dive/image/docker/image_archive.go b/dive/image/docker/image_archive.go index 827b58ba..87438ecd 100644 --- a/dive/image/docker/image_archive.go +++ b/dive/image/docker/image_archive.go @@ -11,6 +11,8 @@ import ( "path" "strings" + "github.com/klauspost/compress/zstd" + "github.com/wagoodman/dive/dive/filetree" "github.com/wagoodman/dive/dive/image" ) @@ -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:") { @@ -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 diff --git a/go.mod b/go.mod index a3fa807a..91db6708 100644 --- a/go.mod +++ b/go.mod @@ -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.11 github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b github.com/lunixbochs/vtclean v1.0.0 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 729cb9e4..c76869da 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= @@ -70,6 +71,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -85,6 +87,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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 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=