Skip to content
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
9 changes: 5 additions & 4 deletions dive/image/docker/engine_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
104 changes: 82 additions & 22 deletions dive/image/docker/image_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()

Expand All @@ -311,63 +316,118 @@ 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-id>/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/<layer-id>
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

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
Expand Down
9 changes: 8 additions & 1 deletion dive/image/docker/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-id>/layer.tar" -> ID is "<layer-id>"
// For OCI format: tree.Name is like "blobs/sha256/<digest>" -> ID is "sha256:<digest>"
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,
Expand Down
6 changes: 3 additions & 3 deletions dive/image/podman/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down