Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,26 @@ func (k *Key) NestedValues() []string {

// transformValue takes a raw value and transforms to its final string.
func (k *Key) transformValue(val string) string {
res, _ := k.transformValueWithVisited(val, nil)
return res
}

func (k *Key) transformValueWithVisited(val string, visited map[*Key]struct{}) (string, bool) {
if k.s.f.ValueMapper != nil {
val = k.s.f.ValueMapper(val)
}

// Fail-fast if no indicate char found for recursive value
if !strings.Contains(val, "%") {
return val
return val, false
}

if visited == nil {
visited = make(map[*Key]struct{})
}
visited[k] = struct{}{}
defer delete(visited, k)

for i := 0; i < depthValues; i++ {
vr := varPattern.FindString(val)
if len(vr) == 0 {
Expand All @@ -167,12 +179,25 @@ func (k *Key) transformValue(val string) string {
// and returns the value as-is.
break
}
if nk == k {
// Stop when key references itself to prevent cycle.
return val, true
}
}

if _, ok := visited[nk]; ok {
return val, true
}

newVal, hasCycle := nk.transformValueWithVisited(nk.value, visited)
if hasCycle {
return val, true
}

// Substitute by new value and take off leading '%(' and trailing ')s'.
val = strings.ReplaceAll(val, vr, nk.String())
val = strings.ReplaceAll(val, vr, newVal)
}
return val
return val, false
}

// String returns string representation of value.
Expand Down
70 changes: 70 additions & 0 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,4 +654,74 @@ ENDPOINT = https://%(ACCOUNT_ID)s.r2.cloudflarestorage.com

assert.Equal(t, "https://abc123.r2.cloudflarestorage.com", f.Section("").Key("ENDPOINT").String())
})

t.Run("self-reference in default section", func(t *testing.T) {
f, err := Load([]byte("a = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "%(a)s", f.Section("").Key("a").String())
})

t.Run("self-reference in section", func(t *testing.T) {
f, err := Load([]byte("[s]\na = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "%(a)s", f.Section("s").Key("a").String())
})

t.Run("mutual reference between keys in section", func(t *testing.T) {
f, err := Load([]byte("[s]\na = %(b)s\nb = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "%(b)s", f.Section("s").Key("a").String())
assert.Equal(t, "%(a)s", f.Section("s").Key("b").String())
})

t.Run("mutual reference between keys in default section", func(t *testing.T) {
f, err := Load([]byte("a = %(b)s\nb = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "%(b)s", f.Section("").Key("a").String())
assert.Equal(t, "%(a)s", f.Section("").Key("b").String())
})

t.Run("multi-key cyclic reference", func(t *testing.T) {
f, err := Load([]byte("[s]\na = %(b)s\nb = %(c)s\nc = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "%(b)s", f.Section("s").Key("a").String())
assert.Equal(t, "%(c)s", f.Section("s").Key("b").String())
assert.Equal(t, "%(a)s", f.Section("s").Key("c").String())
})

t.Run("partial cycle with valid reference", func(t *testing.T) {
f, err := Load([]byte("[s]\nvalid = hello\na = %(valid)s %(b)s\nb = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "hello %(b)s", f.Section("s").Key("a").String())
})

t.Run("diamond reference without cycle", func(t *testing.T) {
f, err := Load([]byte("[s]\nd = leaf\nb = %(d)s\nc = %(d)s\na = %(b)s and %(c)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

assert.Equal(t, "leaf and leaf", f.Section("s").Key("a").String())
})

t.Run("shadows with cyclic references", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowShadows: true}, []byte("[s]\na = %(b)s\na = %(a)s\nb = %(a)s\n"))
require.NoError(t, err)
require.NotNil(t, f)

vals := f.Section("s").Key("a").ValueWithShadows()
require.Len(t, vals, 2)
assert.Equal(t, "%(b)s", f.Section("s").Key("a").String())
})
}