diff --git a/dive/image/docker/engine_resolver.go b/dive/image/docker/engine_resolver.go index 0068d09e..85481c68 100644 --- a/dive/image/docker/engine_resolver.go +++ b/dive/image/docker/engine_resolver.go @@ -56,14 +56,15 @@ func (r *engineResolver) Build(ctx context.Context, args []string) (*image.Image func (r *engineResolver) Extract(ctx context.Context, id string, l string, p string) error { reader, err := r.fetchArchive(ctx, id) if err != nil { - return err + return fmt.Errorf("unable to extract from image '%s': %w", id, err) } - if err := ExtractFromImage(io.NopCloser(reader), l, p); err == nil { - return nil + err = ExtractFromImage(io.NopCloser(reader), l, p) + if err != nil { + return fmt.Errorf("unable to extract from image '%s': %w", id, err) } - return fmt.Errorf("unable to extract from image '%s': %+v", id, err) + return nil } func (r *engineResolver) fetchArchive(ctx context.Context, id string) (io.ReadCloser, error) { diff --git a/dive/image/docker/image_archive.go b/dive/image/docker/image_archive.go index 51d992e6..4239daff 100644 --- a/dive/image/docker/image_archive.go +++ b/dive/image/docker/image_archive.go @@ -16,6 +16,7 @@ import ( "github.com/wagoodman/dive/dive/filetree" "github.com/wagoodman/dive/dive/image" + "github.com/wagoodman/dive/internal/log" ) type ImageArchive struct { @@ -301,8 +302,12 @@ func (img *ImageArchive) ToImage(id string) (*image.Image, error) { } func ExtractFromImage(tarFile io.ReadCloser, l string, p string) error { + defer tarFile.Close() tarReader := tar.NewReader(tarFile) + // sanity check + log.Debugf("ExtractFromImage: searching for layer '%s'", l) + for { header, err := tarReader.Next() @@ -311,42 +316,92 @@ func ExtractFromImage(tarFile io.ReadCloser, l string, p string) error { } if err != nil { - fmt.Println(err) - os.Exit(1) + return fmt.Errorf("error reading tar entry: %w", err) } name := header.Name switch header.Typeflag { case tar.TypeReg: - if name == l { - err = extractInner(tar.NewReader(tarReader), p) - if err != nil { - return err - } - return nil + switch name { + // Docker format: layers stored as /layer.tar + case l + "/layer.tar": + log.Debugf("ExtractFromImage: found docker format layer '%s'", name) + return extractFromTar(tar.NewReader(tarReader), strings.TrimPrefix(p, "/")) + // OCI format: layers stored as blobs/ + case "blobs/" + l: + log.Debugf("ExtractFromImage: found OCI format layer '%s'", name) + return extractCompressedLayer(tarReader, strings.TrimPrefix(p, "/")) + default: + continue } + case tar.TypeSymlink: + log.Debugf("ExtractFromImage: found symlink '%s'", name) + continue default: continue } } - return nil + return fmt.Errorf("layer '%s' not found in image archive", l) } -func extractInner(reader *tar.Reader, p string) error { - target := strings.TrimPrefix(p, "/") +func extractCompressedLayer(reader io.Reader, target string) error { + if target == "" { + target = "." + } + + // Read a buffer to figure out the compression type + buf := make([]byte, 1024) + n, err := io.ReadFull(reader, buf) + if err != nil && err != io.ErrUnexpectedEOF { + return fmt.Errorf("failed to read layer header: %w", err) + } + if n == 0 { + return fmt.Errorf("empty layer entry") + } + + layerReader := io.MultiReader(bytes.NewReader(buf[:n]), reader) + + // Check magic bytes to determine compression type + if n >= 2 && buf[0] == 0x1f && buf[1] == 0x8b { + // gzip magic bytes + gzReader, err := gzip.NewReader(layerReader) + if err != nil { + return fmt.Errorf("failed to decompress gzip layer: %w", err) + } + defer gzReader.Close() + return extractFromTar(tar.NewReader(gzReader), target) + } + + if n >= 4 && buf[0] == 0x28 && buf[1] == 0xb5 && buf[2] == 0x2f && buf[3] == 0xfd { + // zstd magic bytes (0xFD2FB528 little-endian) + zstdReader, err := zstd.NewReader(layerReader) + if err != nil { + return fmt.Errorf("failed to decompress zstd layer: %w", err) + } + defer zstdReader.Close() + return extractFromTar(tar.NewReader(zstdReader), target) + } + + // No compression — plain tar archive + return extractFromTar(tar.NewReader(layerReader), target) +} + +func extractFromTar(tarReader *tar.Reader, target string) error { + if target == "" { + target = "." + } for { - header, err := reader.Next() + header, err := tarReader.Next() if err == io.EOF { break } if err != nil { - fmt.Println(err) - os.Exit(1) + return fmt.Errorf("error reading layer tar entry: %w", err) } name := header.Name @@ -354,20 +409,25 @@ func extractInner(reader *tar.Reader, p string) error { switch header.Typeflag { case tar.TypeReg: if strings.HasPrefix(name, target) { - err := os.MkdirAll(filepath.Dir(name), 0755) - if err != nil { - return err + outPath := filepath.Clean(name) + if filepath.IsAbs(outPath) { + outPath = outPath[1:] } - out, err := os.Create(name) - if err != nil { - return err + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(outPath), err) } - _, err = io.Copy(out, reader) + out, err := os.Create(outPath) if err != nil { - return err + return fmt.Errorf("failed to create file %s: %w", outPath, err) + } + + if _, err := io.Copy(out, tarReader); err != nil { + out.Close() + return fmt.Errorf("failed to write file %s: %w", outPath, err) } + out.Close() } default: continue diff --git a/dive/image/docker/layer.go b/dive/image/docker/layer.go index 72227353..a5fe29d7 100644 --- a/dive/image/docker/layer.go +++ b/dive/image/docker/layer.go @@ -16,7 +16,14 @@ type layer struct { // String represents a layer in a columnar format. func (l *layer) ToLayer() *image.Layer { - id := strings.Split(l.tree.Name, "/")[0] + // For Docker format: tree.Name is like "/layer.tar" -> ID is "" + // For OCI format: tree.Name is like "blobs/sha256/" -> ID is "sha256:" + var id string + if strings.HasPrefix(l.tree.Name, "blobs/") { + id = strings.TrimPrefix(l.tree.Name, "blobs/") + } else { + id = strings.Split(l.tree.Name, "/")[0] + } return &image.Layer{ Id: id, Index: l.index, diff --git a/dive/image/podman/resolver.go b/dive/image/podman/resolver.go index 7bdf23f9..ea95b335 100644 --- a/dive/image/podman/resolver.go +++ b/dive/image/podman/resolver.go @@ -49,11 +49,11 @@ func (r *resolver) Extract(ctx context.Context, id string, l string, p string) e return err } - if err := docker.ExtractFromImage(io.NopCloser(reader), l, p); err == nil { - return nil + if err := docker.ExtractFromImage(io.NopCloser(reader), l, p); err != nil { + return fmt.Errorf("unable to extract from image %q: %w", id, err) } - return fmt.Errorf("unable to extract from image %q: %+v", id, err) + return nil } func (r *resolver) resolveFromDockerArchive(id string) (*image.Image, error) {