]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
config: Clone language map entries before modifying them
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 21 Oct 2025 09:09:29 +0000 (11:09 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 21 Oct 2025 11:34:15 +0000 (13:34 +0200)
Now, with YAML anchor and alias support, these can point to shared data,
which must not be modified in place.

Fixes #14072

common/hdebug/debug.go [new file with mode: 0644]
common/maps/params.go
config/allconfig/allconfig.go
hugolib/config_test.go

diff --git a/common/hdebug/debug.go b/common/hdebug/debug.go
new file mode 100644 (file)
index 0000000..a977367
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hdebug
+
+import (
+       "fmt"
+       "strings"
+
+       "github.com/gohugoio/hugo/common/types"
+       "github.com/gohugoio/hugo/htesting"
+)
+
+// Printf is a debug print function that should be removed before committing code to the repository.
+func Printf(format string, args ...any) {
+       panicIfRealCI()
+       if len(args) == 1 && !strings.Contains(format, "%") {
+               format = format + ": %v"
+       }
+       if !strings.HasSuffix(format, "\n") {
+               format = format + "\n"
+       }
+       fmt.Printf(format, args...)
+}
+
+func AssertNotNil(a ...any) {
+       panicIfRealCI()
+       for _, v := range a {
+               if types.IsNil(v) {
+                       panic("hdebug.AssertNotNil: value is nil")
+               }
+       }
+}
+
+func Panicf(format string, args ...any) {
+       panicIfRealCI()
+       // fmt.Println(stack())
+       if len(args) == 1 && !strings.Contains(format, "%") {
+               format = format + ": %v"
+       }
+       if !strings.HasSuffix(format, "\n") {
+               format = format + "\n"
+       }
+       panic(fmt.Sprintf(format, args...))
+}
+
+func panicIfRealCI() {
+       if htesting.IsRealCI() {
+               panic("This debug statement should be removed before committing code!")
+       }
+}
index aa1dce20c75cc1b5c049b81fefc544e317c61f51..ce03b73f1432148131b707604d26eaceb19dbdea 100644 (file)
@@ -16,7 +16,6 @@ package maps
 import (
        "errors"
        "fmt"
-       xmaps "maps"
        "strings"
 
        "github.com/spf13/cast"
@@ -42,6 +41,14 @@ func (p Params) GetNested(indices ...string) any {
 // SetParams overwrites values in dst with values in src for common or new keys.
 // This is done recursively.
 func SetParams(dst, src Params) {
+       setParams(dst, src, 0)
+}
+
+func setParams(dst, src Params, depth int) {
+       const maxDepth = 1000
+       if depth > maxDepth {
+               panic(errors.New("max depth exceeded"))
+       }
        for k, v := range src {
                vv, found := dst[k]
                if !found {
@@ -50,7 +57,7 @@ func SetParams(dst, src Params) {
                        switch vvv := vv.(type) {
                        case Params:
                                if pv, ok := v.(Params); ok {
-                                       SetParams(vvv, pv)
+                                       setParams(vvv, pv, depth+1)
                                } else {
                                        dst[k] = v
                                }
@@ -116,9 +123,6 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
                                }
                        }
                } else if !noUpdate {
-                       if vvv, ok := v.(Params); ok {
-                               v = xmaps.Clone(vvv)
-                       }
                        p[k] = v
                }
 
@@ -356,6 +360,29 @@ func PrepareParams(m Params) {
        }
 }
 
+// CloneParamsDeep does a deep clone of the given Params,
+// meaning that any nested Params will be cloned as well.
+func CloneParamsDeep(m Params) Params {
+       return cloneParamsDeep(m, 0)
+}
+
+func cloneParamsDeep(m Params, depth int) Params {
+       const maxDepth = 1000
+       if depth > maxDepth {
+               panic(errors.New("max depth exceeded"))
+       }
+       m2 := make(Params)
+       for k, v := range m {
+               switch vv := v.(type) {
+               case Params:
+                       m2[k] = cloneParamsDeep(vv, depth+1)
+               default:
+                       m2[k] = v
+               }
+       }
+       return m2
+}
+
 // PrepareParamsClone is like PrepareParams, but it does not modify the input.
 func PrepareParamsClone(m Params) Params {
        m2 := make(Params)
index c21a18f94fa0be958d0ce39b8998a742c0ae1149..4e9cbcc5a456f77b7f8975e323ec4b1289334ec9 100644 (file)
@@ -55,8 +55,6 @@ import (
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/resources/page/pagemeta"
        "github.com/spf13/afero"
-
-       xmaps "maps"
 )
 
 // 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
                                        // baseURL configure don the language level is a multihost setup.
                                        isMultihost = true
                                }
+
+                               if p, ok := vv.(maps.Params); ok {
+                                       // With the introduction of YAML anchor and alias support, language config entries
+                                       // may be contain shared references.
+                                       // This also break potential cycles.
+                                       vv = maps.CloneParamsDeep(p)
+                               }
+
                                mergedConfig.Set(kk, vv)
                                rootv := cfg.Get(kk)
                                if rootv != nil && cfg.IsSet(kk) {
@@ -1081,7 +1087,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
                                                        differentRootKeys = append(differentRootKeys, kk)
 
                                                        // Use the language value as base.
-                                                       mergedConfigEntry := xmaps.Clone(vvv)
+                                                       // Note that this is already cloned above.
+                                                       mergedConfigEntry := vvv
                                                        // Merge in the root value.
                                                        maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
 
index 189cec7b6a8cf2f4807ce2bd54a668ff01d02231..27e5f6d41b5d4e709f21c992b699cebd2f1dbadf 100644 (file)
@@ -1619,7 +1619,6 @@ params: *params
 }
 
 func TestConfigYAMLAnchorsCyclicReference(t *testing.T) {
-       t.Skip("Skip flaky test for now, will be fixed in issue 14072.")
        t.Parallel()
 
        files := `
@@ -1643,7 +1642,7 @@ Params: {{ site.Params }}|
 
 `
 
-       for range 3 {
+       for range 4 {
                b := Test(t, files)
                b.AssertFileContent("public/index.html", "Params: map[p3:map[p1:p1alias]]|")
                b.AssertFileContent("public/sv/index.html", "Params: map[p1:p1alias p3:map[p1:p1alias]]|")