Skip to content

Commit a3d9548

Browse files
committed
Replace to gopkg.in/yaml with github.com/goccy/go-yaml (note)
This commit also adds validation to prevent the "Billion Laughs" attack (see goccy/go-yaml#461). The limit of non-scalar aliases to the same node is set to 10,000. See benchmarks below. ``` │ sec/op │ UnmarshalBillionLaughs/Billion_Laughs_no_validation-10 125.2µ ± ∞ ¹ UnmarshalBillionLaughs/Billion_Laughs_with_validation-10 655.8µ ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10 9.223µ ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10 9.443µ ± ∞ ¹ geomean 51.71µ ¹ need >= 6 samples for confidence interval at level 0.95 │ fix-goyaml-8822.bench │ │ B/op │ UnmarshalBillionLaughs/Billion_Laughs_no_validation-10 177.0Ki ± ∞ ¹ UnmarshalBillionLaughs/Billion_Laughs_with_validation-10 177.0Ki ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10 11.67Ki ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10 11.67Ki ± ∞ ¹ geomean 45.45Ki ¹ need >= 6 samples for confidence interval at level 0.95 │ fix-goyaml-8822.bench │ │ allocs/op │ UnmarshalBillionLaughs/Billion_Laughs_no_validation-10 3.302k ± ∞ ¹ UnmarshalBillionLaughs/Billion_Laughs_with_validation-10 3.305k ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10 253.0 ± ∞ ¹ UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10 253.0 ± ∞ ¹ ```` Fixes #8822 Fixes #13043 Fixes #14053 Fixes ##8427
1 parent 9e344bb commit a3d9548

21 files changed

+446
-248
lines changed

‎commands/gen.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/alecthomas/chroma/v2/formatters/html"
2929
"github.com/alecthomas/chroma/v2/styles"
3030
"github.com/bep/simplecobra"
31+
"github.com/goccy/go-yaml"
3132
"github.com/gohugoio/hugo/common/hugo"
3233
"github.com/gohugoio/hugo/docshelper"
3334
"github.com/gohugoio/hugo/helpers"
@@ -36,7 +37,6 @@ import (
3637
"github.com/gohugoio/hugo/parser"
3738
"github.com/spf13/cobra"
3839
"github.com/spf13/cobra/doc"
39-
"gopkg.in/yaml.v2"
4040
)
4141

4242
func newGenCommand() *genCommand {

‎commands/server.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,6 @@ func chmodFilter(dst, src os.FileInfo) bool {
11661166
}
11671167

11681168
func cleanErrorLog(content string) string {
1169-
content = strings.ReplaceAll(content, "\n", " ")
11701169
content = logReplacer.Replace(content)
11711170
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
11721171
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")

‎common/herrors/file_error.go‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
110110

111111
fe.errorContext = ectx
112112

113-
if ectx.Position.LineNumber > 0 {
113+
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
114114
fe.position.LineNumber = ectx.Position.LineNumber
115115
}
116116

117-
if ectx.Position.ColumnNumber > 0 {
117+
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
118118
fe.position.ColumnNumber = ectx.Position.ColumnNumber
119119
}
120120

@@ -177,6 +177,7 @@ func NewFileErrorFromName(err error, name string) FileError {
177177
// Filetype is used to determine the Chroma lexer to use.
178178
fileType, pos := extractFileTypePos(err)
179179
pos.Filename = name
180+
180181
if fileType == "" {
181182
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
182183
}
@@ -234,7 +235,9 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
234235
return NewFileErrorFromName(err, realFilename)
235236
}
236237
defer f.Close()
237-
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
238+
fe := NewFileErrorFromName(err, realFilename)
239+
fe = fe.UpdateContent(f, linematcher)
240+
return fe
238241
}
239242

240243
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -321,13 +324,9 @@ func extractFileTypePos(err error) (string, text.Position) {
321324
}
322325

323326
// Look in the error message for the line number.
324-
for _, handle := range lineNumberExtractors {
325-
lno, col := handle(err)
326-
if lno > 0 {
327-
pos.ColumnNumber = col
328-
pos.LineNumber = lno
329-
break
330-
}
327+
if lno, col := commonLineNumberExtractor(err); lno > 0 {
328+
pos.ColumnNumber = col
329+
pos.LineNumber = lno
331330
}
332331

333332
if fileType == "" && pos.Filename != "" {

‎common/herrors/line_number_extractors.go‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,27 @@ import (
1919
)
2020

2121
var lineNumberExtractors = []lineNumberExtractor{
22+
// YAML parse errors.
23+
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
24+
2225
// Template/shortcode parse errors
2326
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
2427
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
2528

26-
// YAML parse errors
27-
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
28-
2929
// i18n bundle errors
3030
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
3131
}
3232

33+
func commonLineNumberExtractor(e error) (int, int) {
34+
for _, handler := range lineNumberExtractors {
35+
lno, col := handler(e)
36+
if lno > 0 {
37+
return lno, col
38+
}
39+
}
40+
return 0, 0
41+
}
42+
3343
type lineNumberExtractor func(e error) (int, int)
3444

3545
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 herrors
15+
16+
import (
17+
"errors"
18+
"testing"
19+
20+
qt "github.com/frankban/quicktest"
21+
)
22+
23+
func TestCommonLineNumberExtractor(t *testing.T) {
24+
t.Parallel()
25+
26+
c := qt.New(t)
27+
28+
lno, col := commonLineNumberExtractor(errors.New("[4:9] value is not allowed in this context"))
29+
c.Assert(lno, qt.Equals, 4)
30+
c.Assert(col, qt.Equals, 9)
31+
}

‎go.mod‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ require (
3333
github.com/frankban/quicktest v1.14.6
3434
github.com/fsnotify/fsnotify v1.9.0
3535
github.com/getkin/kin-openapi v0.133.0
36-
github.com/ghodss/yaml v1.0.0
3736
github.com/gobuffalo/flect v1.0.3
3837
github.com/gobwas/glob v0.2.3
38+
github.com/goccy/go-yaml v1.18.0
3939
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e
4040
github.com/gohugoio/hashstructure v0.6.0
4141
github.com/gohugoio/httpcache v0.8.0
@@ -83,7 +83,6 @@ require (
8383
golang.org/x/text v0.30.0
8484
golang.org/x/tools v0.38.0
8585
google.golang.org/api v0.251.0
86-
gopkg.in/yaml.v2 v2.4.0
8786
rsc.io/qr v0.2.0
8887
)
8988

@@ -188,6 +187,7 @@ require (
188187
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
189188
google.golang.org/grpc v1.75.1 // indirect
190189
google.golang.org/protobuf v1.36.9 // indirect
190+
gopkg.in/yaml.v2 v2.4.0 // indirect
191191
gopkg.in/yaml.v3 v3.0.1 // indirect
192192
howett.net/plist v1.0.0 // indirect
193193
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
242242
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
243243
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
244244
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
245-
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
246-
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
247245
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
248246
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
249247
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -266,6 +264,8 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4
266264
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
267265
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
268266
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
267+
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
268+
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
269269
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
270270
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
271271
github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=

‎hugolib/alias_test.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,26 @@ func TestTargetPathHTMLRedirectAlias(t *testing.T) {
172172
}
173173
}
174174
}
175+
176+
func TestAliasNIssue14053(t *testing.T) {
177+
t.Parallel()
178+
179+
files := `
180+
-- hugo.toml --
181+
baseURL = "http://example.com"
182+
-- layouts/all.html --
183+
All.
184+
-- content/page.md --
185+
---
186+
title: "Page"
187+
aliases:
188+
- n
189+
- y
190+
- no
191+
- yes
192+
---
193+
`
194+
b := Test(t, files)
195+
196+
b.AssertPublishDir("n/index.html", "yes/index.html", "no/index.html", "yes/index.html")
197+
}

‎hugolib/frontmatter_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Strings: {{ printf "%T" .Params.strings }} {{ range .Params.strings }}Strings: {
4040

4141
b.Build()
4242

43-
b.AssertFileContent("public/post/one/index.html", "Ints: []interface {} Int: 1 (int)|Int: 2 (int)|Int: 3 (int)|")
44-
b.AssertFileContent("public/post/one/index.html", "Mixed: []interface {} Mixed: 1 (string)|Mixed: 2 (int)|Mixed: 3 (int)|")
43+
b.AssertFileContent("public/post/one/index.html", "Ints: []interface {} Int: 1 (uint64)|Int: 2 (uint64)|Int: 3 (uint64)|")
44+
b.AssertFileContent("public/post/one/index.html", "Mixed: []interface {} Mixed: 1 (string)|Mixed: 2 (uint64)|Mixed: 3 (uint64)|")
4545
b.AssertFileContent("public/post/one/index.html", "Strings: []string Strings: 1 (string)|Strings: 2 (string)|Strings: 3 (string)|")
4646
}

‎hugolib/hugo_sites_build_errors_test.go‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ line 5
476476
errors := herrors.UnwrapFileErrorsWithErrorContext(err)
477477

478478
b.Assert(errors, qt.HasLen, 3)
479-
b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:1:1": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`))
479+
b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:2:5": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`))
480480
}
481481

482482
func TestErrorRenderHookCodeblock(t *testing.T) {
@@ -642,3 +642,35 @@ Home.
642642
b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.html:2:3`))
643643
b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`)
644644
}
645+
646+
func TestErrorFrontmatterYAMLSyntax(t *testing.T) {
647+
t.Parallel()
648+
649+
files := `
650+
-- hugo.toml --
651+
-- content/_index.md --
652+
653+
654+
655+
656+
657+
---
658+
line1: 'value1'
659+
x
660+
line2: 'value2'
661+
line3: 'value3'
662+
---
663+
`
664+
665+
b, err := TestE(t, files)
666+
667+
b.Assert(err, qt.Not(qt.IsNil))
668+
b.Assert(err.Error(), qt.Contains, "[2:1] non-map value is specified")
669+
fe := herrors.UnwrapFileError(err)
670+
b.Assert(fe, qt.Not(qt.IsNil))
671+
pos := fe.Position()
672+
b.Assert(pos.Filename, qt.Contains, filepath.FromSlash("content/_index.md"))
673+
b.Assert(fe.ErrorContext(), qt.Not(qt.IsNil))
674+
b.Assert(pos.LineNumber, qt.Equals, 8)
675+
b.Assert(pos.ColumnNumber, qt.Equals, 1)
676+
}

0 commit comments

Comments
 (0)