How FileTrees Are Built and Stacked in Dive for Container Image Analysis
Dive constructs in-memory FileTrees from each container image layer's tar archive, then stacks them sequentially to generate a unified view of the final filesystem state.
When analyzing container images with Dive, the tool processes each layer's contents into a hierarchical structure called a FileTree. Understanding how these FileTrees are built from raw tar archives and subsequently merged through stacking is essential for grasping how Dive calculates efficiency scores and visualizes filesystem changes across image layers.
Building FileTrees from Docker Image Layers
Processing Layer Tar Archives with processLayerTar
The construction process begins in dive/image/docker/image_archive.go, where the processLayerTar function (lines 200‑210) initializes an empty tree and populates it by walking through the tar entries.
// dive/image/docker/image_archive.go:200‑210
func processLayerTar(name string, reader *tar.Reader) (*filetree.FileTree, error) {
tree := filetree.NewFileTree() // <‑‑ New empty tree
tree.Name = name // keep the layer name for UI
fileInfos, err := getFileList(reader)
…
}
Constructing File Metadata and Nodes
The getFileList helper (lines 44‑48) iterates over the tar stream, converting each header into a filetree.FileInfo object via NewFileInfoFromTarHeader. These metadata objects are then inserted into the tree using AddPath (lines 44‑55 in dive/filetree/file_tree.go), which creates a hierarchy of FileNode objects.
// dive/image/docker/image_archive.go:44‑48
files = append(files, filetree.NewFileInfoFromTarHeader(tarReader, header, name))
// dive/filetree/file_tree.go:44‑55
_, _, err := tree.AddPath(element.Path, element)
The resulting FileTree contains a complete representation of the layer's filesystem, including directories, files, white‑out markers, and total size (tree.FileSize). This tree is stored in the image.Layer object for subsequent diffing and UI rendering.
Stacking FileTrees to Create a Unified Filesystem View
A container image's final filesystem is the cumulative result of all layers. Dive implements this by stacking the trees one‑by‑one, with later layers applied on top of earlier ones.
The Stack Method and Whiteout Handling
The core merging logic resides in FileTree.Stack (lines 7‑25 in dive/filetree/file_tree.go). This method walks the upper tree depth‑first and applies a graft function to each node. When IsWhiteout() detects a white‑out marker (special ".wh." entries indicating file deletion), the method removes the corresponding path from the lower tree using RemovePath. Otherwise, it adds or overwrites the path using AddPath.
// dive/filetree/file_tree.go:7‑25
func (tree *FileTree) Stack(upper *FileTree) (failed []PathError, stackErr error) {
graft := func(node *FileNode) error {
if node.IsWhiteout() { // white‑out → delete path
err := tree.RemovePath(node.Path())
…
} else { // normal file/dir → add/overwrite
_, _, err := tree.AddPath(node.Path(), node.Data.FileInfo)
…
}
return nil
}
// Walk the upper tree depth‑first, applying `graft` to every node
stackErr = upper.VisitDepthChildFirst(graft, nil)
return failed, stackErr
}
Stacking Multiple Layers with StackTreeRange
For operations requiring the complete image filesystem, Dive uses StackTreeRange (lines 376‑388 in dive/filetree/file_tree.go). This function creates a copy of the first tree, then iteratively applies Stack for each subsequent layer in the specified range.
// dive/filetree/file_tree.go:376‑388
func StackTreeRange(trees []*FileTree, start, stop int) (*FileTree, []PathError, error) {
tree := trees[0].Copy() // start from a clean copy
for idx := start; idx <= stop; idx++ {
failedPaths, err := tree.Stack(trees[idx])
…
}
return tree, errors, nil
}
The caller passes the slice of layer trees in chronological order (oldest to newest), resulting in a final tree that reflects the complete filesystem after all layers have been applied.
Applications in Image Analysis
Stacked trees power Dive's core analytical features. In dive/comparer/comparer.go (lines 71‑73), the Comparer builds lower and upper trees for a selected layer and calls StackTreeRange to obtain a merged view for diff calculations. Similarly, dive/filetree/efficiency.go iterates over stacked views to compute how many files changed between layers, determining image efficiency scores.
Practical Code Examples
Creating a FileTree from a Layer Tar Archive
package main
import (
"archive/tar"
"os"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image/docker"
)
func main() {
f, _ := os.Open("layer.tar")
defer f.Close()
tr := tar.NewReader(f)
// Build a tree from the tar archive
tree, err := docker.ProcessLayerTar("layer-1", tr) // see dive/image/docker/image_archive.go
if err != nil {
panic(err)
}
// Print a pretty tree representation
println(tree.String(true)) // includes size info
}
Manually Stacking Two FileTrees
package main
import (
"github.com/wagoodman/dive/dive/filetree"
)
func main() {
// Assume lowerTree and upperTree were already built from layers
var lowerTree, upperTree *filetree.FileTree
// Stack the upper tree onto the lower one
failed, err := lowerTree.Stack(upperTree)
if err != nil {
panic(err)
}
if len(failed) > 0 {
// handle white‑out or add failures
}
// The resulting tree now represents the merged filesystem
println(lowerTree.String(false))
}
Building the Complete Image Tree
package main
import (
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
)
func buildFullTree(img *image.Image) (*filetree.FileTree, error) {
// img.Trees contains a FileTree per layer in chronological order
return filetree.StackTreeRange(img.Trees, 0, len(img.Trees)-1)
}
Key Implementation Files
| File | Purpose |
|---|---|
dive/filetree/file_tree.go |
Core FileTree implementation; creation, traversal, stacking, copying, diff‑marking. |
dive/filetree/file_node.go |
Definition of a node in the tree and helper methods (AddChild, Remove, IsWhiteout). |
dive/image/docker/image_archive.go |
Parses Docker image tarballs, builds a FileTree per layer via processLayerTar. |
dive/comparer/comparer.go |
Uses StackTreeRange to create merged trees for diffing individual layers. |
dive/filetree/efficiency.go |
Calculates efficiency scores by stacking ranges of trees. |
Summary
- FileTrees are built from layer tar archives using
processLayerTarindive/image/docker/image_archive.go, which populates the tree viaAddPathwithFileInfometadata extracted byNewFileInfoFromTarHeader. - Whiteout markers are preserved during construction and processed during stacking to handle file deletions across layers.
- Stacking merges layers using the
Stackmethod indive/filetree/file_tree.go, which applies upper layers onto lower ones while handling whiteouts viaRemovePathand additions viaAddPath. - StackTreeRange combines multiple layers for full image analysis, powering diff calculations in
dive/comparer/comparer.goand efficiency scoring indive/filetree/efficiency.go.
Frequently Asked Questions
What is a FileTree in Dive?
A FileTree is an in-memory hierarchical representation of a filesystem layer constructed from tar archive entries. Each node in the tree represents a file or directory and contains a FileInfo payload with metadata such as size, permissions, and modification times extracted via NewFileInfoFromTarHeader.
How does Dive handle deleted files when stacking layers?
Dive detects whiteout markers—special entries prefixed with ".wh." that indicate file deletion in Docker layers—during the Stack operation. When IsWhiteout() returns true, the method removes the corresponding path from the lower tree using RemovePath, effectively deleting the file from the cumulative filesystem view.
Can I use Dive's FileTree implementation independently of the UI?
Yes, the filetree package located at dive/filetree/file_tree.go is self-contained and can be imported independently. You can use it to build trees from tar archives, stack them with Stack or StackTreeRange, and analyze filesystem structures programmatically without running the interactive TUI.
What is the difference between Stack and StackTreeRange?
Stack is a method on FileTree that merges a single upper FileTree onto the receiver tree, handling individual layer operations and whiteout processing. StackTreeRange is a convenience function that takes a slice of FileTrees and sequentially stacks a specified range (start to stop) onto a copy of the first tree, returning the cumulative result for full image analysis.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →