Skip to content
This repository was archived by the owner on Sep 1, 2025. It is now read-only.

Commit 0a469b2

Browse files
committed
BUG/MEDIUM: defaults: order defaults sections in order that allows proper inheritance
default section is sorted by name, if from exist, then, after sorting by name, section are moved after the one it depends on. non existing dependency is not allowed, also circular dependency is not allowed
1 parent c27e834 commit 0a469b2

7 files changed

Lines changed: 334 additions & 2 deletions

File tree

errors/parse.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ var ErrScopeMissing = errors.New("scope missing")
5555
var ErrScopeAlreadyExists = errors.New("scope already exists")
5656

5757
var ErrFromDefaultsSectionMissing = errors.New("defaults section specified in from does not exist")
58+
59+
var ErrCircularDependency = errors.New("circular dependency detected")

fetch.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/haproxytech/config-parser/v4/common"
2424
"github.com/haproxytech/config-parser/v4/errors"
25+
"github.com/haproxytech/config-parser/v4/sorter"
2526
)
2627

2728
func (p *configParser) lock() {
@@ -183,7 +184,26 @@ func (p *configParser) SectionsDefaultsFromSet(sectionType Section, sectionName,
183184
p.lock()
184185
defer p.unLock()
185186
switch sectionType { //nolint:exhaustive
186-
case Defaults, Frontends, Backends, Listen:
187+
case Defaults:
188+
// for defaults we need to check for circular dependencies
189+
// and whether that section exists or not due to sorting method
190+
sections := p.Parsers[Defaults]
191+
listDefaults := []sorter.Section{}
192+
for k, v := range sections {
193+
s := sorter.Section{
194+
Name: k,
195+
From: v.DefaultSectionName,
196+
}
197+
if s.Name == sectionName {
198+
s.From = defaultsSection
199+
}
200+
listDefaults = append(listDefaults, s)
201+
}
202+
err := sorter.Sort(listDefaults)
203+
if err != nil {
204+
return err
205+
}
206+
case Frontends, Backends, Listen:
187207
default:
188208
// catch all other sections
189209
return errors.ErrSectionTypeNotAllowed
@@ -315,3 +335,24 @@ func (p *configParser) getSortedList(data map[string]*Parsers) []string {
315335
sort.Strings(result)
316336
return result
317337
}
338+
339+
// getSortedListWithFrom returns list of parses sorted,
340+
// since every section can have a from that might depend on,
341+
// we take that into account
342+
func getSortedListWithFrom(data map[string]*Parsers) ([]string, error) {
343+
sortedSections := make([]sorter.Section, len(data))
344+
index := 0
345+
for parserSectionName := range data {
346+
sortedSections[index] = sorter.Section{
347+
Name: parserSectionName,
348+
From: data[parserSectionName].DefaultSectionName,
349+
}
350+
index++
351+
}
352+
err := sorter.Sort(sortedSections)
353+
result := make([]string, len(data))
354+
for index, value := range sortedSections {
355+
result[index] = value.Name
356+
}
357+
return result, err
358+
}

sorter/equal_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2019 HAProxy Technologies
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sorter_test
18+
19+
import (
20+
"github.com/haproxytech/config-parser/v4/sorter"
21+
)
22+
23+
func Equal(a, b []sorter.Section) bool {
24+
if len(a) != len(b) {
25+
return false
26+
}
27+
for i, v := range a {
28+
if v != b[i] {
29+
return false
30+
}
31+
}
32+
return true
33+
}

sorter/sort.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright 2019 HAProxy Technologies
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sorter
18+
19+
import (
20+
"sort"
21+
22+
parserErrors "github.com/haproxytech/config-parser/v4/errors"
23+
)
24+
25+
type Section struct {
26+
Name string
27+
From string
28+
}
29+
30+
// Sort sections based on rules:
31+
// - all dependencies must exist
32+
// - there must be no circular dependencies
33+
// - sort by Name
34+
// - if there is a dependency, section is moved after the one it depends on
35+
func Sort(sections []Section) error {
36+
// first check that all dependencies exist
37+
for _, section := range sections {
38+
if section.From == "" {
39+
continue
40+
}
41+
depOK := false
42+
for _, cmp := range sections {
43+
if section.From == cmp.Name {
44+
depOK = true
45+
break
46+
}
47+
}
48+
if !depOK {
49+
return parserErrors.ErrFromDefaultsSectionMissing
50+
}
51+
}
52+
// first check for circular dependencies
53+
for _, section := range sections {
54+
if circularDependency(map[string]struct{}{section.Name: {}}, section.From, sections) {
55+
return parserErrors.ErrCircularDependency
56+
}
57+
}
58+
59+
// first sort by name
60+
sort.SliceStable(sections, func(i, j int) bool {
61+
return sections[i].Name < sections[j].Name
62+
})
63+
// then go through list and check for circular dependencies
64+
sortByFrom(0, sections)
65+
// done
66+
return nil
67+
}
68+
69+
func circularDependency(visited map[string]struct{}, current string, sections []Section) bool {
70+
_, alreadyVisited := visited[current]
71+
if alreadyVisited {
72+
return true
73+
}
74+
if current == "" {
75+
return false
76+
}
77+
for _, next := range sections {
78+
if next.Name == current {
79+
visited[next.Name] = struct{}{}
80+
return circularDependency(visited, next.From, sections)
81+
}
82+
}
83+
return false
84+
}
85+
86+
func sortByFrom(index int, sections []Section) {
87+
// if section has from, move it until
88+
if index >= len(sections) {
89+
return
90+
}
91+
if sections[index].From == "" {
92+
sortByFrom(index+1, sections)
93+
return
94+
}
95+
// we check if from is before, if it is, its ok
96+
for i := 0; i < index; i++ {
97+
if sections[i].Name == sections[index].From {
98+
sortByFrom(index+1, sections)
99+
return
100+
}
101+
}
102+
// we have a from, find that from and move this one after that one
103+
hasChange := false
104+
for i := index + 1; i < len(sections); i++ {
105+
hasChange = true
106+
sections[i-1], sections[i] = sections[i], sections[i-1]
107+
if sections[i-1].Name == sections[i].From {
108+
break
109+
}
110+
}
111+
if sections[index].From != "" && hasChange {
112+
sortByFrom(index, sections)
113+
return
114+
}
115+
sortByFrom(index+1, sections)
116+
}

sorter/sort_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2019 HAProxy Technologies
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sorter_test
18+
19+
import (
20+
"testing"
21+
22+
parserErrors "github.com/haproxytech/config-parser/v4/errors"
23+
"github.com/haproxytech/config-parser/v4/sorter"
24+
)
25+
26+
func TestSortWithFrom(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
sections []sorter.Section
30+
result []sorter.Section
31+
ExpectedError error
32+
}{
33+
{"empty", []sorter.Section{}, []sorter.Section{}, nil},
34+
{"1", []sorter.Section{{"1", ""}}, []sorter.Section{{"1", ""}}, nil},
35+
{"2", []sorter.Section{{"1", ""}, {"2", ""}}, []sorter.Section{{"1", ""}, {"2", ""}}, nil},
36+
{"2a", []sorter.Section{{"1", ""}, {"2", "1"}}, []sorter.Section{{"1", ""}, {"2", "1"}}, nil},
37+
{"2b", []sorter.Section{{"2", "1"}, {"1", ""}}, []sorter.Section{{"1", ""}, {"2", "1"}}, nil},
38+
{"2 self depend", []sorter.Section{{"2", "2"}, {"1", "1"}}, []sorter.Section{{"2", "2"}, {"1", "1"}}, parserErrors.ErrCircularDependency},
39+
{"with non existent sorter.Section", []sorter.Section{{"2", "3"}, {"1", ""}}, []sorter.Section{{"1", ""}, {"2", "3"}}, parserErrors.ErrFromDefaultsSectionMissing},
40+
{"with non existent sorter.Section 2", []sorter.Section{{"0", "2"}, {"2", "3"}, {"1", ""}}, []sorter.Section{{"1", ""}, {"2", "3"}, {"0", "2"}}, parserErrors.ErrFromDefaultsSectionMissing},
41+
{"3", []sorter.Section{{"0", ""}, {"2", "1"}, {"1", ""}}, []sorter.Section{{"0", ""}, {"1", ""}, {"2", "1"}}, nil},
42+
{"4", []sorter.Section{{"0", ""}, {"3", "1"}, {"2", "1"}, {"1", ""}}, []sorter.Section{{"0", ""}, {"1", ""}, {"2", "1"}, {"3", "1"}}, nil},
43+
{"4a", []sorter.Section{{"0", ""}, {"3", "0"}, {"2", "1"}, {"1", ""}}, []sorter.Section{{"0", ""}, {"1", ""}, {"2", "1"}, {"3", "0"}}, nil},
44+
{"err 1", []sorter.Section{{"0", "1"}, {"1", "0"}}, []sorter.Section{{"0", "1"}, {"1", "0"}}, parserErrors.ErrCircularDependency},
45+
{"err 2", []sorter.Section{{"0", "1"}, {"1", "2"}, {"2", "3"}, {"3", "0"}}, []sorter.Section{{"0", "1"}, {"1", "2"}, {"2", "3"}, {"3", "0"}}, parserErrors.ErrCircularDependency},
46+
{"malicious", []sorter.Section{{"0", "3"}, {"1", "2"}, {"2", "3"}, {"3", "1"}}, []sorter.Section{{"0", "3"}, {"1", "2"}, {"2", "3"}, {"3", "1"}}, parserErrors.ErrCircularDependency},
47+
{"ok but reorder", []sorter.Section{{"0", "3"}, {"1", ""}, {"2", ""}, {"3", ""}}, []sorter.Section{{"1", ""}, {"2", ""}, {"3", ""}, {"0", "3"}}, nil},
48+
{"ok but reorder 2", []sorter.Section{{"0", "3"}, {"1", "2"}, {"2", ""}, {"3", ""}}, []sorter.Section{{"2", ""}, {"1", "2"}, {"3", ""}, {"0", "3"}}, nil},
49+
{"ok but reorder 2", []sorter.Section{{"0", "3"}, {"1", "0"}, {"2", "1"}, {"3", ""}}, []sorter.Section{{"3", ""}, {"0", "3"}, {"1", "0"}, {"2", "1"}}, nil},
50+
}
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
err := sorter.Sort(tt.sections)
54+
if tt.ExpectedError == nil {
55+
if err != nil {
56+
t.Errorf("SortWithFrom() error = %v, expectedError %v", err, tt.ExpectedError)
57+
}
58+
} else {
59+
if err != tt.ExpectedError { //nolint:errorlint
60+
t.Errorf("SortWithFrom() error = %v, expectedError %v", err, tt.ExpectedError)
61+
}
62+
}
63+
if err == nil {
64+
if !Equal(tt.sections, tt.result) {
65+
t.Errorf("SortWithFrom() src = %v, result %v", tt.sections, tt.result)
66+
}
67+
}
68+
})
69+
}
70+
}

tests/configs/parser_defaults_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ package configs //nolint:testpackage
1717

1818
import (
1919
"bytes"
20+
"errors"
2021
"testing"
2122

2223
parser "github.com/haproxytech/config-parser/v4"
24+
parserErrors "github.com/haproxytech/config-parser/v4/errors"
2325
"github.com/haproxytech/config-parser/v4/options"
2426
)
2527

@@ -49,3 +51,61 @@ func TestDefaultsConfigs(t *testing.T) {
4951
})
5052
}
5153
}
54+
55+
func TestDefaultsConfigsSetDef(t *testing.T) {
56+
tests := []struct {
57+
Name, Config string
58+
}{
59+
{"configDefaultsTwo", configDefaultsTwo},
60+
{"configDefaultsThree", configDefaultsThree},
61+
}
62+
for _, config := range tests {
63+
t.Run(config.Name, func(t *testing.T) {
64+
var buffer bytes.Buffer
65+
buffer.WriteString(config.Config)
66+
p, err := parser.New(options.Reader(&buffer))
67+
if err != nil {
68+
t.Fatalf(err.Error())
69+
}
70+
err = p.SectionsDefaultsFromSet(parser.Defaults, "???", "nonexisting")
71+
if !errors.Is(err, parserErrors.ErrSectionMissing) {
72+
t.Fatalf("expected (%v) got (%v)", parserErrors.ErrSectionMissing, err)
73+
}
74+
err = p.SectionsDefaultsFromSet(parser.Defaults, "A", "nonexisting")
75+
if !errors.Is(err, parserErrors.ErrFromDefaultsSectionMissing) {
76+
t.Fatalf("expected (%v) got (%v)", parserErrors.ErrFromDefaultsSectionMissing, err)
77+
}
78+
err = p.SectionsDefaultsFromSet(parser.Defaults, "A", "withName")
79+
if err != nil {
80+
t.Fatalf("expected (%v) got (%v)", parserErrors.ErrFromDefaultsSectionMissing, err)
81+
}
82+
})
83+
}
84+
}
85+
86+
func TestDefaultsConfigsSetCircular(t *testing.T) {
87+
tests := []struct {
88+
Name, Config string
89+
}{
90+
{"configDefaultsTwo", configDefaultsTwo},
91+
{"configDefaultsThree", configDefaultsThree},
92+
}
93+
for _, config := range tests {
94+
t.Run(config.Name, func(t *testing.T) {
95+
var buffer bytes.Buffer
96+
buffer.WriteString(config.Config)
97+
p, err := parser.New(options.Reader(&buffer))
98+
if err != nil {
99+
t.Fatalf(err.Error())
100+
}
101+
err = p.SectionsDefaultsFromSet(parser.Defaults, "A", "withName")
102+
if err != nil {
103+
t.Fatalf("expected (%v) got (%v)", parserErrors.ErrFromDefaultsSectionMissing, err)
104+
}
105+
err = p.SectionsDefaultsFromSet(parser.Defaults, "withName", "A")
106+
if !errors.Is(err, parserErrors.ErrCircularDependency) {
107+
t.Fatalf("expected (%v) got (%v)", parserErrors.ErrCircularDependency, err)
108+
}
109+
})
110+
}
111+
}

writer.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,17 @@ func (p *configParser) String() string {
4242
sections := []Section{Defaults, UserList, Peers, Mailers, Resolvers, Cache, Ring, LogForward, HTTPErrors, Frontends, Backends, Listen, Program, FCGIApp}
4343

4444
for _, section := range sections {
45-
sortedSections := p.getSortedList(p.Parsers[section])
45+
var sortedSections []string
46+
if section == Defaults {
47+
var err error
48+
sortedSections, err = getSortedListWithFrom(p.Parsers[section])
49+
if err != nil && p.Options.Log {
50+
p.Options.Logger.Errorf("%s", err.Error())
51+
}
52+
} else {
53+
sortedSections = p.getSortedList(p.Parsers[section])
54+
}
55+
4656
for _, sectionName := range sortedSections {
4757
var sName string
4858
if sectionName != "" {

0 commit comments

Comments
 (0)