From 9cf15288423f5350d7a72f646d6e16cbc57d7d99 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 29 Sep 2026 17:05:48 +0200 Subject: [PATCH] analysis: Make non-opaque whiteouts also remove child files An opaque whiteout is where you remove all the children of the directory, but the directory is still there. However, we can also have non-opaque directory removals, and these must also remove all children from previous layers. Signed-off-by: Alexander Larsson --- pkg/tar-diff/analysis.go | 13 +++++-------- pkg/tar-diff/analysis_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pkg/tar-diff/analysis.go b/pkg/tar-diff/analysis.go index d960757..1c0150c 100644 --- a/pkg/tar-diff/analysis.go +++ b/pkg/tar-diff/analysis.go @@ -345,16 +345,13 @@ func buildSourceAnalysis(oldInfos []*tarInfo, numOldFiles int, options *Options) pathToFileIndex := make(map[string]int) for fileIdx, oldInfo := range oldInfos { - // Apply whiteouts from this layer to sources from earlier layers + // A whiteout can remove a directory, so exclude its descendants too. + // Opaque whiteouts likewise exclude all earlier entries below their directory. for _, wo := range oldInfo.whiteouts { - if wo.opaque { - for p := range pathToFileIndex { - if hasPathPrefix(p, wo.path) { - delete(pathToFileIndex, p) - } + for p := range pathToFileIndex { + if hasPathPrefix(p, wo.path) { + delete(pathToFileIndex, p) } - } else { - delete(pathToFileIndex, wo.path) } } diff --git a/pkg/tar-diff/analysis_test.go b/pkg/tar-diff/analysis_test.go index 93ac03f..9c6f312 100644 --- a/pkg/tar-diff/analysis_test.go +++ b/pkg/tar-diff/analysis_test.go @@ -49,6 +49,38 @@ type tarEntry struct { mode int64 } +func TestBuildSourceAnalysis_DirectoryWhiteout(t *testing.T) { + oldTar, err := createTestTar([]tarEntry{ + {name: "boot/.kernel/version/.vmlinuz.hmac", typeflag: tar.TypeReg, data: []byte("hmac")}, + {name: "boot/.kernel-old/keep", typeflag: tar.TypeReg, data: []byte("sibling")}, + {name: "boot/EFI/Linux/image.efi", typeflag: tar.TypeReg, data: []byte("uki")}, + }) + if err != nil { + t.Fatal(err) + } + whiteoutTar, err := createTestTar([]tarEntry{ + {name: "boot/.wh..kernel", typeflag: tar.TypeReg}, + }) + if err != nil { + t.Fatal(err) + } + + options := NewOptions() + options.SetApplyWhiteouts(true) + sources, err := AnalyzeSources([]io.ReadSeeker{oldTar, whiteoutTar}, options) + if err != nil { + t.Fatal(err) + } + if _, ok := sources.sourceByPath["boot/.kernel/version/.vmlinuz.hmac"]; ok { + t.Error("file beneath a whiteouted directory remains a delta source") + } + for _, path := range []string{"boot/.kernel-old/keep", "boot/EFI/Linux/image.efi"} { + if _, ok := sources.sourceByPath[path]; !ok { + t.Errorf("unaffected file %q is missing from delta sources", path) + } + } +} + func TestAnalyzeTar_Hardlinks(t *testing.T) { entries := []tarEntry{ {name: "original.txt", typeflag: tar.TypeReg, data: []byte("content")},