From c0f9107ac571e25087dc3956d9ada5c06d6d71a4 Mon Sep 17 00:00:00 2001 From: Rupayon Haldar <80724680+rupayon123@users.noreply.github.com> Date: Wed, 23 Sep 2026 08:33:49 -0400 Subject: [PATCH] Stop recursive key references from overflowing stack --- key.go | 15 ++++++++++++++- key_test.go | 13 +++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/key.go b/key.go index 800d04a..89a27e3 100644 --- a/key.go +++ b/key.go @@ -140,6 +140,10 @@ func (k *Key) NestedValues() []string { // transformValue takes a raw value and transforms to its final string. func (k *Key) transformValue(val string) string { + return k.transformValueWithVisited(val, map[*Key]bool{k: true}) +} + +func (k *Key) transformValueWithVisited(val string, visited map[*Key]bool) string { if k.s.f.ValueMapper != nil { val = k.s.f.ValueMapper(val) } @@ -169,8 +173,17 @@ func (k *Key) transformValue(val string) string { } } + // A reference back to a key already being resolved is left intact, + // just like a direct self-reference. + if visited[nk] { + break + } + visited[nk] = true + replacement := nk.transformValueWithVisited(nk.value, visited) + delete(visited, nk) + // Substitute by new value and take off leading '%(' and trailing ')s'. - val = strings.ReplaceAll(val, vr, nk.String()) + val = strings.ReplaceAll(val, vr, replacement) } return val } diff --git a/key_test.go b/key_test.go index e57a7bd..e013abe 100644 --- a/key_test.go +++ b/key_test.go @@ -626,6 +626,19 @@ expires = %(expires)s`)) assert.Equal(t, "yes", f.Section("package").Key("expires").String()) }) + t.Run("mutually recursive keys remain unresolved", func(t *testing.T) { + f, err := Load([]byte("[s]\na = %(b)s\nb = %(a)s\n")) + require.NoError(t, err) + assert.Equal(t, "%(a)s", f.Section("s").Key("a").String()) + assert.Equal(t, "%(b)s", f.Section("s").Key("b").String()) + }) + + t.Run("longer reference cycle remains unresolved", func(t *testing.T) { + f, err := Load([]byte("[s]\na = %(b)s\nb = %(c)s\nc = %(a)s\n")) + require.NoError(t, err) + assert.Equal(t, "%(a)s", f.Section("s").Key("a").String()) + }) + t.Run("recursive value with no target found", func(t *testing.T) { f, err := Load([]byte(` [foo]