File 0024-compare-move-FreeBSD-loose-keyword-comparisons-to-go.patch of Package go-mtree
From 1ce6aa8db51fa0c3b76be2cff88252a28c5284b5 Mon Sep 17 00:00:00 2001
From: Aleksa Sarai <cyphar@cyphar.com>
Date: Tue, 16 Sep 2025 22:27:20 +1000
Subject: [PATCH 24/25] compare: move FreeBSD loose keyword comparisons to
gomtree command
FreeBSD has quite unfortunate behaviour when dealing with keywords that
are missing in one of the manifests being compared -- namely, they
ignore these instances.
Commit 21723a3974bc ("*: fix comparison of missing keywords") re-added
this behaviour after the introduction of the Compare API, but
unfortunately it was implemented in the Compare API itself -- meaning
that library users (which didn't want this behaviour) were silently
opted into it.
This patch moves the behaviour to the command-line, where it belongs
(a future patch in this series will allow users to opt-out of this
unfortunate behaviour, as well as some other unfortunate FreeBSD
compatibility behaviours).
Fixes: 21723a3974bc ("*: fix comparison of missing keywords")
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
---
cmd/gomtree/cmd/validate.go | 20 +++++++++++
compare.go | 26 --------------
compare_test.go | 72 +++++++++++++++++++++++++++++++++++++
3 files changed, 92 insertions(+), 26 deletions(-)
diff --git a/cmd/gomtree/cmd/validate.go b/cmd/gomtree/cmd/validate.go
index 7186be3fbe3f..bbff281057d4 100644
--- a/cmd/gomtree/cmd/validate.go
+++ b/cmd/gomtree/cmd/validate.go
@@ -368,6 +368,7 @@ func validateAction(c *cli.Context) error {
if isTarSpec(specDh) || c.String("tar") != "" {
filters = append(filters, tarKeywordFilter)
}
+ filters = append(filters, freebsdCompatKeywordFilter)
res = filterDeltas(res, filters...)
if len(res) > 0 {
@@ -458,6 +459,25 @@ func tarKeywordFilter(delta *mtree.InodeDelta) bool {
return true
}
+// freebsdCompatKeywordFilter removes any deltas where a key is not present in
+// both manifests being compared. This is necessary for compatibility with
+// FreeBSD's mtree(8) but is generally undesireable for most users.
+func freebsdCompatKeywordFilter(delta *mtree.InodeDelta) bool {
+ if delta.Type() != mtree.Modified {
+ return true
+ }
+ keys := delta.DiffPtr()
+ *keys = slices.DeleteFunc(*keys, func(kd mtree.KeyDelta) bool {
+ if kd.Name().Prefix() == "xattr" {
+ // Even in FreeBSD compatibility mode, any xattr changes should
+ // still be treated as a proper change and not filtered out.
+ return false
+ }
+ return kd.Type() != mtree.Modified
+ })
+ return true
+}
+
type deltaFilterFn func(*mtree.InodeDelta) bool
// filterDeltas takes the set of deltas generated by mtree and applies the
diff --git a/compare.go b/compare.go
index 67d92fc81604..90916dd60711 100644
--- a/compare.go
+++ b/compare.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"iter"
- "maps"
"slices"
"github.com/sirupsen/logrus"
@@ -247,31 +246,6 @@ func compareEntry(oldEntry, newEntry Entry) ([]KeyDelta, error) {
newKeys = newEntry.allKeysMap()
)
- // Delete any keys which are not present in both maps.
- keyFilterFn := func(otherMap map[Keyword]KeyVal) func(Keyword, KeyVal) bool {
- return func(k Keyword, _ KeyVal) bool {
- switch {
- case k.Prefix() == "xattr":
- // xattrs are presented as one keyword to users but are actually
- // implemented as separate keywords and so we should always include
- // them (even if the same xattr is not present in both sides).
- // TODO: I actually think this is not inline with the original
- // purpose of this code, but I'm leaving it as-is to not not
- // introduce bugs.
- return false
- case k == "time" || k == "tar_time":
- // These have special handling later.
- return false
- default:
- // Drop keys which do not exist in the other entry.
- _, ok := otherMap[k]
- return !ok
- }
- }
- }
- maps.DeleteFunc(oldKeys, keyFilterFn(newKeys))
- maps.DeleteFunc(newKeys, keyFilterFn(oldKeys))
-
// If both tar_time and time were specified in the set of keys, we have to
// convert the "time" entries to "tar_time" to allow for tar archive
// manifests to be compared with proper filesystem manifests.
diff --git a/compare_test.go b/compare_test.go
index d8e07107af7e..19539aa1012e 100644
--- a/compare_test.go
+++ b/compare_test.go
@@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
+ "slices"
"testing"
"time"
@@ -230,6 +231,77 @@ func TestCompareKeySubset(t *testing.T) {
}
}
+func TestCompareKeyDelta(t *testing.T) {
+ dir := t.TempDir()
+
+ // Create a bunch of objects.
+ tmpfile := filepath.Join(dir, "tmpfile")
+ require.NoError(t, os.WriteFile(tmpfile, []byte("some content here"), 0666))
+
+ tmpdir := filepath.Join(dir, "testdir")
+ require.NoError(t, os.Mkdir(tmpdir, 0755))
+
+ tmpsubfile := filepath.Join(tmpdir, "anotherfile")
+ require.NoError(t, os.WriteFile(tmpsubfile, []byte("aaa"), 0666))
+
+ // Walk the current state.
+ manifestKeywords := append(DefaultKeywords[:], "sha1digest")
+ old, err := Walk(dir, nil, manifestKeywords, nil)
+ require.NoErrorf(t, err, "walk %s", dir)
+
+ t.Run("Extra-Key", func(t *testing.T) {
+ extraKeyword := Keyword("sha256digest")
+ newManifestKeywords := append(manifestKeywords[:], extraKeyword)
+
+ new, err := Walk(dir, nil, newManifestKeywords, nil)
+ require.NoErrorf(t, err, "walk %s", dir)
+
+ diffs, err := Compare(old, new, nil)
+ require.NoError(t, err, "compare")
+
+ assert.NotEmpty(t, diffs, "extra keys in manifest should result in deltas")
+ for _, diff := range diffs {
+ if assert.Equal(t, Modified, diff.Type(), "extra keyword diff element should be 'modified'") {
+ kds := diff.Diff()
+ if assert.Len(t, kds, 1, "should only get a single key delta") {
+ kd := kds[0]
+ assert.Equalf(t, Extra, kd.Type(), "key %q", kd.Name())
+ assert.Equal(t, extraKeyword, kd.Name())
+ assert.Nil(t, kd.Old(), "Old for extra keyword delta")
+ assert.NotNil(t, kd.New(), "New for extra keyword delta")
+ }
+ }
+ }
+ })
+
+ t.Run("Missing-Key", func(t *testing.T) {
+ missingKeyword := Keyword("sha1digest")
+ newManifestKeywords := slices.DeleteFunc(manifestKeywords[:], func(kw Keyword) bool {
+ return kw == missingKeyword
+ })
+
+ new, err := Walk(dir, nil, newManifestKeywords, nil)
+ require.NoErrorf(t, err, "walk %s", dir)
+
+ diffs, err := Compare(old, new, nil)
+ require.NoError(t, err, "compare")
+
+ assert.NotEmpty(t, diffs, "missing keys in manifest should result in deltas")
+ for _, diff := range diffs {
+ if assert.Equal(t, Modified, diff.Type(), "missing keyword diff element should be 'modified'") {
+ kds := diff.Diff()
+ if assert.Len(t, kds, 1, "should only get a single key delta") {
+ kd := kds[0]
+ assert.Equalf(t, Missing, kd.Type(), "key %q", kd.Name())
+ assert.Equal(t, missingKeyword, kd.Name())
+ assert.NotNil(t, kd.Old(), "Old for missing keyword delta")
+ assert.Nil(t, kd.New(), "New for missing keyword delta")
+ }
+ }
+ }
+ })
+}
+
//gocyclo:ignore
func TestTarCompare(t *testing.T) {
dir := t.TempDir()
--
2.51.0