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..bb5b277c 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.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 diff --git a/go.sum b/go.sum index 729cb9e4..a87ef795 100644 --- a/go.sum +++ b/go.sum @@ -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=