Skip to content

Commit a130770

Browse files
committed
config: Clone language map entries before modifying them
Now, with YAML anchor and alias support, these can point to shared data, which must not be modified in place. Fixes #14072
1 parent 9425b93 commit a130770

File tree

4 files changed

+104
-10
lines changed

4 files changed

+104
-10
lines changed

‎common/hdebug/debug.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package hdebug
15+
16+
import (
17+
"fmt"
18+
"strings"
19+
20+
"github.com/gohugoio/hugo/common/types"
21+
"github.com/gohugoio/hugo/htesting"
22+
)
23+
24+
// Printf is a debug print function that should be removed before committing code to the repository.
25+
func Printf(format string, args ...any) {
26+
panicIfRealCI()
27+
if len(args) == 1 && !strings.Contains(format, "%") {
28+
format = format + ": %v"
29+
}
30+
if !strings.HasSuffix(format, "\n") {
31+
format = format + "\n"
32+
}
33+
fmt.Printf(format, args...)
34+
}
35+
36+
func AssertNotNil(a ...any) {
37+
panicIfRealCI()
38+
for _, v := range a {
39+
if types.IsNil(v) {
40+
panic("hdebug.AssertNotNil: value is nil")
41+
}
42+
}
43+
}
44+
45+
func Panicf(format string, args ...any) {
46+
panicIfRealCI()
47+
// fmt.Println(stack())
48+
if len(args) == 1 && !strings.Contains(format, "%") {
49+
format = format + ": %v"
50+
}
51+
if !strings.HasSuffix(format, "\n") {
52+
format = format + "\n"
53+
}
54+
panic(fmt.Sprintf(format, args...))
55+
}
56+
57+
func panicIfRealCI() {
58+
if htesting.IsRealCI() {
59+
panic("This debug statement should be removed before committing code!")
60+
}
61+
}

‎common/maps/params.go‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package maps
1616
import (
1717
"errors"
1818
"fmt"
19-
xmaps "maps"
2019
"strings"
2120

2221
"github.com/spf13/cast"
@@ -42,6 +41,14 @@ func (p Params) GetNested(indices ...string) any {
4241
// SetParams overwrites values in dst with values in src for common or new keys.
4342
// This is done recursively.
4443
func SetParams(dst, src Params) {
44+
setParams(dst, src, 0)
45+
}
46+
47+
func setParams(dst, src Params, depth int) {
48+
const maxDepth = 1000
49+
if depth > maxDepth {
50+
panic(errors.New("max depth exceeded"))
51+
}
4552
for k, v := range src {
4653
vv, found := dst[k]
4754
if !found {
@@ -50,7 +57,7 @@ func SetParams(dst, src Params) {
5057
switch vvv := vv.(type) {
5158
case Params:
5259
if pv, ok := v.(Params); ok {
53-
SetParams(vvv, pv)
60+
setParams(vvv, pv, depth+1)
5461
} else {
5562
dst[k] = v
5663
}
@@ -116,9 +123,6 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
116123
}
117124
}
118125
} else if !noUpdate {
119-
if vvv, ok := v.(Params); ok {
120-
v = xmaps.Clone(vvv)
121-
}
122126
p[k] = v
123127
}
124128

@@ -356,6 +360,29 @@ func PrepareParams(m Params) {
356360
}
357361
}
358362

363+
// CloneParamsDeep does a deep clone of the given Params,
364+
// meaning that any nested Params will be cloned as well.
365+
func CloneParamsDeep(m Params) Params {
366+
return cloneParamsDeep(m, 0)
367+
}
368+
369+
func cloneParamsDeep(m Params, depth int) Params {
370+
const maxDepth = 1000
371+
if depth > maxDepth {
372+
panic(errors.New("max depth exceeded"))
373+
}
374+
m2 := make(Params)
375+
for k, v := range m {
376+
switch vv := v.(type) {
377+
case Params:
378+
m2[k] = cloneParamsDeep(vv, depth+1)
379+
default:
380+
m2[k] = v
381+
}
382+
}
383+
return m2
384+
}
385+
359386
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
360387
func PrepareParamsClone(m Params) Params {
361388
m2 := make(Params)

‎config/allconfig/allconfig.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ import (
5555
"github.com/gohugoio/hugo/resources/page"
5656
"github.com/gohugoio/hugo/resources/page/pagemeta"
5757
"github.com/spf13/afero"
58-
59-
xmaps "maps"
6058
)
6159

6260
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
@@ -1071,6 +1069,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
10711069
// baseURL configure don the language level is a multihost setup.
10721070
isMultihost = true
10731071
}
1072+
1073+
if p, ok := vv.(maps.Params); ok {
1074+
// With the introduction of YAML anchor and alias support, language config entries
1075+
// may be contain shared references.
1076+
// This also break potential cycles.
1077+
vv = maps.CloneParamsDeep(p)
1078+
}
1079+
10741080
mergedConfig.Set(kk, vv)
10751081
rootv := cfg.Get(kk)
10761082
if rootv != nil && cfg.IsSet(kk) {
@@ -1081,7 +1087,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
10811087
differentRootKeys = append(differentRootKeys, kk)
10821088

10831089
// Use the language value as base.
1084-
mergedConfigEntry := xmaps.Clone(vvv)
1090+
// Note that this is already cloned above.
1091+
mergedConfigEntry := vvv
10851092
// Merge in the root value.
10861093
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
10871094

‎hugolib/config_test.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,6 @@ params: *params
16191619
}
16201620

16211621
func TestConfigYAMLAnchorsCyclicReference(t *testing.T) {
1622-
t.Skip("Skip flaky test for now, will be fixed in issue 14072.")
16231622
t.Parallel()
16241623

16251624
files := `
@@ -1643,7 +1642,7 @@ Params: {{ site.Params }}|
16431642
16441643
`
16451644

1646-
for range 3 {
1645+
for range 4 {
16471646
b := Test(t, files)
16481647
b.AssertFileContent("public/index.html", "Params: map[p3:map[p1:p1alias]]|")
16491648
b.AssertFileContent("public/sv/index.html", "Params: map[p1:p1alias p3:map[p1:p1alias]]|")

0 commit comments

Comments
 (0)