From 65096ac1f846f4ea367464c00fd59cd2247cf645 Mon Sep 17 00:00:00 2001 From: June Yi Date: Sun, 11 Oct 2026 18:22:50 +0900 Subject: [PATCH 1/2] Preserve repeated keys in INI store Allow duplicate keys when parsing and encoding INI files, and expand shadow values into separate SOPS tree items. This preserves repeated keys across an encrypt/decrypt round trip. Assisted-by: OpenAI Codex Signed-off-by: June Yi --- stores/ini/store.go | 33 ++++++++++++++++++++++----------- stores/ini/store_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/stores/ini/store.go b/stores/ini/store.go index 9378fc62de..8f5186632c 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -26,7 +26,12 @@ func (store *Store) Name() string { } func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { - iniFile := ini.Empty(ini.LoadOptions{AllowNonUniqueSections: true}) + iniFile := ini.Empty(ini.LoadOptions{ + AllowNonUniqueSections: true, + AllowShadows: true, + // Keep identical repeated values too; they are separate INI entries. + AllowDuplicateShadowValues: true, + }) iniFile.DeleteSection(ini.DefaultSection) for _, branch := range branches { for _, item := range branch { @@ -85,7 +90,11 @@ func (store Store) iniFromTreeBranches(branches sops.TreeBranches) ([]byte, erro } func (store Store) treeBranchesFromIni(in []byte) (sops.TreeBranches, error) { - iniFile, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, in) + iniFile, err := ini.LoadSources(ini.LoadOptions{ + AllowNonUniqueSections: true, + AllowShadows: true, + AllowDuplicateShadowValues: true, + }, in) if err != nil { return nil, err } @@ -116,15 +125,17 @@ func (store Store) treeItemFromSection(section *ini.Section) (sops.TreeItem, err } for _, key := range section.Keys() { - item := sops.TreeItem{Key: key.Name(), Value: key.Value()} - items = append(items, item) - if key.Comment != "" { - items = append(items, sops.TreeItem{ - Key: sops.Comment{ - Value: store.stripCommentChar(key.Comment), - }, - Value: nil, - }) + for _, value := range key.ValueWithShadows() { + item := sops.TreeItem{Key: key.Name(), Value: value} + items = append(items, item) + if key.Comment != "" { + items = append(items, sops.TreeItem{ + Key: sops.Comment{ + Value: store.stripCommentChar(key.Comment), + }, + Value: nil, + }) + } } } sectionItem.Value = items diff --git a/stores/ini/store_test.go b/stores/ini/store_test.go index b1aff6cf9a..2f1278de37 100644 --- a/stores/ini/store_test.go +++ b/stores/ini/store_test.go @@ -1,6 +1,7 @@ package ini import ( + "strings" "testing" "github.com/getsops/sops/v3" @@ -71,6 +72,30 @@ file="payroll.dat" assert.Equal(t, expected, branch) } +func TestRepeatedKeysRoundTrip(t *testing.T) { + input := []byte("[oslo_messaging_notifications]\ndriver = messagingv2\ndriver = log\ndriver = messagingv2\n") + store := Store{} + + branches, err := store.treeBranchesFromIni(input) + assert.NoError(t, err) + + section := branches[0][1].Value.(sops.TreeBranch) + assert.Equal(t, sops.TreeBranch{ + {Key: "driver", Value: "messagingv2"}, + {Key: "driver", Value: "log"}, + {Key: "driver", Value: "messagingv2"}, + }, section) + + encoded, err := store.iniFromTreeBranches(branches) + assert.NoError(t, err) + assert.Equal(t, input, encoded) + assert.Equal(t, 3, strings.Count(string(encoded), "driver =")) + + decoded, err := store.treeBranchesFromIni(encoded) + assert.NoError(t, err) + assert.Equal(t, branches, decoded) +} + func TestEncodeSimpleIni(t *testing.T) { branches := sops.TreeBranches{ sops.TreeBranch{ From 9ab57ed08c9542ac0c614708771e64f576531594 Mon Sep 17 00:00:00 2001 From: June Yi Date: Wed, 14 Oct 2026 10:12:38 +0900 Subject: [PATCH 2/2] Avoid duplicating comments for repeated INI keys Move the key comment outside the ValueWithShadows loop so it is added to the SOPS tree only once for repeated keys. Add a regression test for the reported case. Assisted-by: OpenAI Codex Signed-off-by: June Yi --- stores/ini/store.go | 16 ++++++++-------- stores/ini/store_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/stores/ini/store.go b/stores/ini/store.go index 8f5186632c..1a3a63f54d 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -128,14 +128,14 @@ func (store Store) treeItemFromSection(section *ini.Section) (sops.TreeItem, err for _, value := range key.ValueWithShadows() { item := sops.TreeItem{Key: key.Name(), Value: value} items = append(items, item) - if key.Comment != "" { - items = append(items, sops.TreeItem{ - Key: sops.Comment{ - Value: store.stripCommentChar(key.Comment), - }, - Value: nil, - }) - } + } + if key.Comment != "" { + items = append(items, sops.TreeItem{ + Key: sops.Comment{ + Value: store.stripCommentChar(key.Comment), + }, + Value: nil, + }) } } sectionItem.Value = items diff --git a/stores/ini/store_test.go b/stores/ini/store_test.go index 2f1278de37..bbc0c3e1e2 100644 --- a/stores/ini/store_test.go +++ b/stores/ini/store_test.go @@ -96,6 +96,24 @@ func TestRepeatedKeysRoundTrip(t *testing.T) { assert.Equal(t, branches, decoded) } +func TestRepeatedKeyCommentIsAddedOnce(t *testing.T) { + input := []byte("[sec]\ndriver = a\n; note mid\ndriver = b\n") + store := Store{} + + branches, err := store.treeBranchesFromIni(input) + assert.NoError(t, err) + + section := branches[0][1].Value.(sops.TreeBranch) + assert.Equal(t, sops.TreeBranch{ + {Key: "driver", Value: "a"}, + {Key: "driver", Value: "b"}, + {Key: sops.Comment{Value: "note mid"}, Value: nil}, + }, section) + encoded, err := store.iniFromTreeBranches(branches) + assert.NoError(t, err) + assert.Equal(t, 1, strings.Count(string(encoded), "note mid")) +} + func TestEncodeSimpleIni(t *testing.T) { branches := sops.TreeBranches{ sops.TreeBranch{