image

package
v1.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

View Source
const MaxSaveSize = 2 << 30 // 2 GiB

MaxSaveSize bounds save-to-disk extraction. The viewer caps reads at MaxViewSize, but the save path historically called io.ReadAll with no limit — a 10 GB file inside a layer (or a maliciously crafted tar entry with an inflated header.Size) would OOM the process. 2 GiB is generous enough for legitimate single-file extracts and well below typical RAM.

View Source
const MaxViewSize = 1 << 20 // 1 MB
View Source
const SchemaVersion = 2

SchemaVersion is the on-disk schema version for cached analysis files. Bump when the cacheEnvelope, cachedLayer, or cachedNode shape changes (or when the parse output produced by docker.go/parseLayers changes in a way that affects what we persist). Do NOT bump for stack.go or size.go changes — those run on every load and are not persisted.

Variables

This section is empty.

Functions

func CacheDir added in v1.2.0

func CacheDir() (string, error)

CacheDir returns the directory where layerx persists analysis caches. Resolution order:

  1. LAYERX_CACHE_DIR (if set and usable as a directory)
  2. os.UserCacheDir() joined with "layerx"

CacheDir does not create the directory; saveCache creates digest subdirectories on demand with mode 0700.

func FormatBytes

func FormatBytes(b int64) string

func FormatMode

func FormatMode(m fs.FileMode) string

func FormatSignedBytes

func FormatSignedBytes(b int64) string

FormatSignedBytes formats b like FormatBytes but with an explicit sign for non-zero values: "+12 MB", "-3.0 MB", "0 B". Used for net deltas.

func IsBinary

func IsBinary(data []byte) bool

IsBinary reports whether data appears to be binary content. Trusts net/http content detection for text/* (covering UTF-8/16/32 with BOMs) and only falls back to null-byte scanning when detection is inconclusive.

func TreeLiveFileBytes

func TreeLiveFileBytes(tree *FileTree) int64

TreeLiveFileBytes returns the sum of file sizes in tree, excluding directories and any nodes marked Removed (their subtrees are not descended). Returns 0 for nil tree or nil root.

Types

type Analysis

type Analysis struct {
	ImageRef     string
	Layers       []Layer
	StackedTrees []*FileTree
	TotalSize    int64
}

Analysis holds the complete result of inspecting an image.

func Analyze

func Analyze(ctx context.Context, resolver Resolver, imageRef string) (*Analysis, error)

Analyze resolves an image and computes stacked file trees. Backwards-compatible shim around AnalyzeWithOptions; caching is enabled, no progress channel.

func AnalyzeWithOptions added in v1.2.0

func AnalyzeWithOptions(ctx context.Context, resolver Resolver, imageRef string, opts AnalyzeOptions) (*Analysis, error)

AnalyzeWithOptions is the cache-aware entry point.

Flow:

  1. Try to obtain image content digest via Resolver.ImageID. On error (e.g. image not local yet, daemon hiccup) we skip the cache lookup and proceed to a cold resolve; ResolveWithProgress will pull as needed.
  2. If !opts.NoCache and the digest is known, try loadCache. On hit we skip ResolveWithProgress entirely; emit PhaseCacheLoad.
  3. Otherwise call ResolveWithProgress (existing pull/export/parse path).
  4. After a successful resolve, re-read ImageID and require the result to match the pre-resolve digest before persisting the cache. A mismatch means the tag flipped during the run (concurrent docker pull) — caching either set of layers under either digest would lie.
  5. Stack + assignNetDeltas + TotalSize, build Analysis, return.

Hit and miss paths converge at step 5: the same Analysis assembly runs either way. ImageRef is taken from imageRef (the current arg), never from the envelope.

func AnalyzeWithProgress

func AnalyzeWithProgress(ctx context.Context, resolver Resolver, imageRef string, progress chan<- ProgressEvent) (*Analysis, error)

AnalyzeWithProgress is preserved for callers that already pass a progress channel. Equivalent to AnalyzeWithOptions{Progress: progress}.

type AnalyzeOptions added in v1.2.0

type AnalyzeOptions struct {
	// NoCache forces a cold resolve. The cache is still written on success
	// so the next default-mode run is a hit.
	NoCache bool

	// Progress, if non-nil, receives ProgressEvents during the run. Same
	// semantics as AnalyzeWithProgress; sending PhaseCacheLoad on hit.
	Progress chan<- ProgressEvent

	// CacheRoot overrides CacheDir() for this call. Empty = use CacheDir().
	// Tests can also set LAYERX_CACHE_DIR; this field exists for callers
	// that want explicit control without environment.
	CacheRoot string
}

AnalyzeOptions controls the analyze pipeline. Zero value is valid: caching enabled, no progress channel, default cache directory.

type ArchiveExtractor added in v1.2.3

type ArchiveExtractor struct {
	// contains filtered or unexported fields
}

ArchiveExtractor extracts file content from layers in a local image archive. Reuses findFileInLayer (daemon-independent) for the per-layer search.

func (*ArchiveExtractor) Extract added in v1.2.3

func (e *ArchiveExtractor) Extract(ctx context.Context, imageRef string, filePath string) (*FileContent, error)

Extract is provided for interface completeness. The TUI uses ExtractFromLayer; full-image extract via container-create has no archive equivalent, so we route to the layer-walk over all layers.

func (*ArchiveExtractor) ExtractFromLayer added in v1.2.3

func (e *ArchiveExtractor) ExtractFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) (*FileContent, error)

ExtractFromLayer mirrors DockerExtractor.ExtractFromLayer: walk back from layerCursor toward layer 0, return the file from the most recent layer whose tar contains it as a regular entry, or surface a typed whiteout / non-regular error if encountered first.

func (*ArchiveExtractor) ExtractRaw added in v1.2.3

func (e *ArchiveExtractor) ExtractRaw(ctx context.Context, imageRef string, filePath string) ([]byte, error)

ExtractRaw is the raw-bytes variant of Extract, same routing.

func (*ArchiveExtractor) ExtractRawFromLayer added in v1.2.3

func (e *ArchiveExtractor) ExtractRawFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) ([]byte, error)

ExtractRawFromLayer is the raw-bytes variant of ExtractFromLayer.

type ArchiveResolver added in v1.2.3

type ArchiveResolver struct {
	// contains filtered or unexported fields
}

ArchiveResolver resolves image layers from a local image archive on disk (docker save / OCI layout tarball) without contacting any container runtime.

The archive is opened lazily on each method call rather than held open by the resolver. This keeps the resolver cheap to construct and lets the caller move or delete the file between calls (the active call still finishes against the file handle it opened).

func NewArchiveResolver added in v1.2.3

func NewArchiveResolver(path string) *ArchiveResolver

NewArchiveResolver creates a resolver that reads from the archive at path. The path is not validated here; errors surface when methods are called (so the constructor cannot fail and Resolve / Inspect / ImageID share a single file-open code path with consistent error wrapping).

func (*ArchiveResolver) ImageID added in v1.2.3

func (r *ArchiveResolver) ImageID(ctx context.Context, imageRef string) (string, error)

ImageID returns the image's content digest, derived from the config blob's bytes. This is the same identity used by Docker and OCI registries: the sha256 of the JSON config file referenced by manifest.json. Two archives with identical image content (same config + same layers) share an ImageID regardless of where they live on disk or how the tar was rebuilt; two archives that differ in any layer or config field do not.

For OCI layouts the config path embeds the digest (blobs/sha256/<hex>) and we use it directly. For legacy docker-save layouts the filename stem (<hex>.json) is conventionally the digest but is not authoritative — a rewritten tar can keep the filename while changing the bytes — so we hash the config blob itself.

func (*ArchiveResolver) Inspect added in v1.2.3

func (r *ArchiveResolver) Inspect(ctx context.Context, imageRef string) (*ImageMeta, error)

Inspect returns lightweight metadata (total declared layer size) by scanning the outer tar headers once. Does not read layer bodies.

"Total layer size" is summed strictly over the entries listed in the manifest's Layers — manifest.json, the config blob, and any unrelated root-level JSON are excluded. This matches what users mean by "image size" and what the spinner / status bar displays.

func (*ArchiveResolver) NewExtractor added in v1.2.3

func (r *ArchiveResolver) NewExtractor() Extractor

NewExtractor returns an ArchiveExtractor bound to this resolver's archive path. Implements ExtractorSource so the TUI's file viewer and save-to-disk features work in archive mode.

func (*ArchiveResolver) Resolve added in v1.2.3

func (r *ArchiveResolver) Resolve(ctx context.Context, imageRef string) ([]Layer, error)

Resolve reads the archive and returns the parsed layer list.

func (*ArchiveResolver) ResolveWithProgress added in v1.2.3

func (r *ArchiveResolver) ResolveWithProgress(ctx context.Context, imageRef string, progress chan<- ProgressEvent) ([]Layer, error)

ResolveWithProgress reads the archive, emitting Parsing progress. There is no Pulling phase (no daemon, no network) and no Exporting phase (no ImageSave call) — archive mode goes straight to parse.

type DiffType

type DiffType int

DiffType indicates how a file changed relative to previous layers.

const (
	Unchanged DiffType = iota
	Added
	Modified
	Removed
)

type DockerExtractor

type DockerExtractor struct {
	// contains filtered or unexported fields
}

DockerExtractor extracts files from images via the Docker daemon.

func NewDockerExtractor

func NewDockerExtractor(cli client.APIClient) *DockerExtractor

NewDockerExtractor creates an extractor using the provided Docker client.

func (*DockerExtractor) Extract

func (e *DockerExtractor) Extract(ctx context.Context, imageRef string, filePath string) (*FileContent, error)

Extract creates a temporary container, copies the file out, and removes the container.

func (*DockerExtractor) ExtractFromLayer added in v1.2.1

func (e *DockerExtractor) ExtractFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) (*FileContent, error)

ExtractFromLayer extracts a file as it exists at the given layer cursor. It walks back from layerCursor toward layer 0 looking for the most recent layer whose tar contains the path as a regular file. A whiteout encountered during the walk-back stops the search and returns an error.

func (*DockerExtractor) ExtractRaw

func (e *DockerExtractor) ExtractRaw(ctx context.Context, imageRef string, filePath string) ([]byte, error)

ExtractRaw extracts a file's raw bytes without truncation or binary detection.

func (*DockerExtractor) ExtractRawFromLayer added in v1.2.1

func (e *DockerExtractor) ExtractRawFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) ([]byte, error)

ExtractRawFromLayer is the raw-bytes variant of ExtractFromLayer. No truncation, no binary detection. Used by save-to-disk.

type DockerResolver

type DockerResolver struct {
	// contains filtered or unexported fields
}

DockerResolver resolves image layers via the Docker daemon.

func (*DockerResolver) ImageID added in v1.2.0

func (r *DockerResolver) ImageID(ctx context.Context, imageRef string) (string, error)

ImageID returns the image content digest from the Docker daemon. It does not pull; the caller is expected to ensure the image is local first (AnalyzeWithProgress does this).

func (*DockerResolver) Inspect

func (r *DockerResolver) Inspect(ctx context.Context, imageRef string) (*ImageMeta, error)

Inspect returns lightweight image metadata without exporting the full tar. It does not pull the image — if the image is not local, it returns an error.

func (*DockerResolver) NewExtractor

func (r *DockerResolver) NewExtractor() Extractor

NewExtractor creates an Extractor using this resolver's Docker client.

func (*DockerResolver) Resolve

func (r *DockerResolver) Resolve(ctx context.Context, imageRef string) ([]Layer, error)

Resolve fetches the image, exports it as a tar, and parses the layer list.

func (*DockerResolver) ResolveWithProgress

func (r *DockerResolver) ResolveWithProgress(ctx context.Context, imageRef string, progress chan<- ProgressEvent) ([]Layer, error)

ResolveWithProgress fetches the image with progress reporting via the channel.

type EfficiencyResult

type EfficiencyResult struct {
	Score       float64
	WastedBytes int64
	WastedFiles []WastedFile
}

EfficiencyResult holds the efficiency analysis output.

func Efficiency

func Efficiency(layers []Layer) *EfficiencyResult

Efficiency computes how much space is wasted by files appearing in multiple layers. A file at the same path in N layers means N-1 occurrences are redundant overwrites.

type ErrArchiveInfra added in v1.2.3

type ErrArchiveInfra struct {
	Op    string
	Cause error
}

ErrArchiveInfra signals an infrastructure-class failure encountered while processing an archive: temp file creation, disk-full while spooling, etc. Distinct from ErrInvalidArchive — the archive may be perfectly valid; the host environment failed. Callers should surface the cause directly rather than telling the user their tarball is malformed.

func (*ErrArchiveInfra) Error added in v1.2.3

func (e *ErrArchiveInfra) Error() string

func (*ErrArchiveInfra) Unwrap added in v1.2.3

func (e *ErrArchiveInfra) Unwrap() error

type ErrArchiveNotFound added in v1.2.3

type ErrArchiveNotFound struct {
	Path  string
	Cause error
}

ErrArchiveNotFound is returned when the archive file path does not exist or is not a regular file. Distinct from ErrInvalidArchive so callers can give a different message for "wrong path" vs "wrong contents".

func (*ErrArchiveNotFound) Error added in v1.2.3

func (e *ErrArchiveNotFound) Error() string

func (*ErrArchiveNotFound) Unwrap added in v1.2.3

func (e *ErrArchiveNotFound) Unwrap() error

type ErrArchivePermission added in v1.2.3

type ErrArchivePermission struct {
	Path  string
	Cause error
}

ErrArchivePermission is returned when the archive file exists but the current user cannot open it (permission denied). Distinct from ErrArchiveNotFound so the user sees "fix your permissions" rather than "fix your path".

func (*ErrArchivePermission) Error added in v1.2.3

func (e *ErrArchivePermission) Error() string

func (*ErrArchivePermission) Unwrap added in v1.2.3

func (e *ErrArchivePermission) Unwrap() error

type ErrDaemonNotRunning

type ErrDaemonNotRunning struct {
	Cause error
}

func (*ErrDaemonNotRunning) Error

func (e *ErrDaemonNotRunning) Error() string

func (*ErrDaemonNotRunning) Unwrap

func (e *ErrDaemonNotRunning) Unwrap() error

type ErrImageNotFound

type ErrImageNotFound struct {
	Ref   string
	Cause error
}

func (*ErrImageNotFound) Error

func (e *ErrImageNotFound) Error() string

func (*ErrImageNotFound) Unwrap

func (e *ErrImageNotFound) Unwrap() error

type ErrInvalidArchive added in v1.2.3

type ErrInvalidArchive struct {
	Path  string
	Cause error
}

ErrInvalidArchive is returned when the file exists and is readable but is not a valid docker-save / OCI image archive (missing manifest.json, malformed manifest, malformed config, etc).

func (*ErrInvalidArchive) Error added in v1.2.3

func (e *ErrInvalidArchive) Error() string

func (*ErrInvalidArchive) Unwrap added in v1.2.3

func (e *ErrInvalidArchive) Unwrap() error

type ErrPullFailed

type ErrPullFailed struct {
	Ref   string
	Cause error
}

func (*ErrPullFailed) Error

func (e *ErrPullFailed) Error() string

func (*ErrPullFailed) Unwrap

func (e *ErrPullFailed) Unwrap() error

type Extractor

type Extractor interface {
	Extract(ctx context.Context, imageRef string, filePath string) (*FileContent, error)
	ExtractRaw(ctx context.Context, imageRef string, filePath string) ([]byte, error)
	// ExtractFromLayer returns the file as it exists at the given layer.
	// It walks back from layerCursor toward layer 0 to find the most recent
	// layer that physically contains the path. A whiteout encountered during
	// the walk-back stops the search and returns an error.
	ExtractFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) (*FileContent, error)
	// ExtractRawFromLayer is the raw-bytes variant of ExtractFromLayer; same
	// walk-back semantics, no truncation, no binary detection.
	ExtractRawFromLayer(ctx context.Context, imageRef string, filePath string, layerCursor int) ([]byte, error)
}

Extractor retrieves file contents from a container image.

type ExtractorSource

type ExtractorSource interface {
	NewExtractor() Extractor
}

ExtractorSource is implemented by resolvers that can produce an Extractor.

type FileContent

type FileContent struct {
	Path      string
	Data      []byte
	Size      int64 // actual file size (may exceed len(Data) if truncated)
	Truncated bool
	Binary    bool
}

FileContent holds the result of extracting a file from an image.

type FileNode

type FileNode struct {
	Name              string
	Path              string
	Linkname          string
	Size              int64
	Mode              fs.FileMode
	UID               int
	GID               int
	DiffType          DiffType
	IntroducedInLayer int
	Children          []*FileNode
	IsDir             bool
	IsHardlink        bool
}

FileNode represents a single file or directory in a layer's filesystem.

func (*FileNode) AddChild

func (n *FileNode) AddChild(child *FileNode)

func (*FileNode) FindChild

func (n *FileNode) FindChild(name string) *FileNode

func (*FileNode) RemoveChild

func (n *FileNode) RemoveChild(name string) bool

type FileTree

type FileTree struct {
	Root *FileNode
}

FileTree represents the filesystem state of one or more stacked layers.

func NewFileTree

func NewFileTree() *FileTree

func ParseLayerTar

func ParseLayerTar(r io.Reader) (*FileTree, error)

ParseLayerTar reads a tar archive (a single layer) and builds a FileTree.

func Stack

func Stack(layers []Layer) []*FileTree

Stack takes layers with populated Trees and returns one FileTree per layer, each representing the cumulative filesystem state at that layer with DiffType set on every node to indicate what the current layer changed.

func (*FileTree) Walk

func (t *FileTree) Walk(fn func(*FileNode))

type ImageMeta

type ImageMeta struct {
	Size int64
}

ImageMeta holds lightweight metadata obtainable without exporting the full image.

type Layer

type Layer struct {
	Index   int
	ID      string // short digest (first 12 chars of sha256)
	Size    int64
	Command string    // Dockerfile instruction that created this layer
	Tree    *FileTree // nil until M05 populates it
	// NetDelta is the merged-filesystem change at this step.
	// [0] = live file bytes after the first layer (full size, not 0).
	// [i>0] = live bytes in stacked[i] minus stacked[i-1]. Negative when
	// a layer net-removes more than it adds (cleanup, whiteout deletes).
	NetDelta int64
}

Layer holds metadata for a single image layer.

type Option

type Option func(*DockerResolver)

Option configures a DockerResolver.

func WithClient

func WithClient(cli client.APIClient) Option

WithClient injects a Docker API client (used for testing).

type ProgressEvent

type ProgressEvent struct {
	Phase       ProgressPhase
	LayersDone  int
	LayersTotal int
	BytesCurr   int64
	BytesTotal  int64
	// Message carries human-readable diagnostic text for PhaseCacheWarn.
	// Empty for normal phases.
	Message string
}

ProgressEvent reports loading progress back to the caller.

type ProgressPhase

type ProgressPhase int

ProgressPhase identifies what the resolver is currently doing.

const (
	// PhaseUnknown is the zero value, used by callers to mean "no phase
	// observed yet" so a UI can avoid rendering a stale default phase
	// before the first event arrives.
	PhaseUnknown ProgressPhase = iota
	PhasePulling
	PhaseExporting
	PhaseParsing
	PhaseCacheLoad // emitted on cache hit; no live Docker work follows
	// PhaseCacheWarn is a non-fatal diagnostic: the cache load or save
	// failed with an unexpected I/O error. The run continues with live
	// data; UIs typically render Message in a status bar.
	PhaseCacheWarn
)

type Resolver

type Resolver interface {
	Resolve(ctx context.Context, imageRef string) ([]Layer, error)
	ResolveWithProgress(ctx context.Context, imageRef string, progress chan<- ProgressEvent) ([]Layer, error)
	Inspect(ctx context.Context, imageRef string) (*ImageMeta, error)
	// ImageID returns the image content digest (e.g. "sha256:abc...") for ref.
	// Implementations may return an error if the image is not local; callers
	// that want pull-on-miss must call ensure-image semantics first.
	ImageID(ctx context.Context, imageRef string) (string, error)
}

Resolver fetches image layer information from a container runtime.

func NewDockerResolver

func NewDockerResolver(opts ...Option) (Resolver, error)

NewDockerResolver creates a resolver connected to the local Docker daemon.

type WastedFile

type WastedFile struct {
	Path        string
	TotalWasted int64
	LayerCount  int
}

WastedFile records a file path that appears redundantly across multiple layers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL