Repository: goccy/go-yaml

Issue:
Block scalar string in CRLF encoding does not strip CR
**Describe the bug**
The parser does not strip CR characters in block scalar string when the line encoding is CRLF.

**To Reproduce**

```go
package main

import (
	"fmt"
	"strings"

	goccy "github.com/goccy/go-yaml"
	goyaml "gopkg.in/yaml.v3"
)

func main() {
	s := "x: |\r\n  a\r\n  b\r\n"
	var v, w any
	err := goyaml.NewDecoder(strings.NewReader(s)).Decode(&v)
	fmt.Printf("%q, %v\n", v, err)

	err = goccy.NewDecoder(strings.NewReader(s)).Decode(&w)
	fmt.Printf("%q, %v\n", w, err)
}
```
```
map["x":"a\nb\n"], <nil>
map["x":"\na\r\nb\r\n"], <nil>
```
**Expected behavior**
```
map["x":"a\nb\n"], <nil>
map["x":"a\nb\n"], <nil>
```

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Version Variables**
 - Go version: 1.24.3
 - go-yaml's Version: v1.18.0

**Additional context**
Add any other context about the problem here.


Discussion:
@BlackBuck: The bug occurs in the `scanNewLine` function in `scanner.go`, where the code encounters a CRLF (`\r\n`) and assumes that it is outside scalar content.

However, according to the [YAML 1.2.2 specification](https://yaml.org/spec/1.2.2/#scalar):

Line breaks inside [scalar content](https://yaml.org/spec/1.2.2/#scalar) must be normalized by the YAML [processor](https://yaml.org/spec/1.2.2/#processes-and-models).

Despite this, the function treats CRLF as a raw line break and does not normalize it when inside scalars. Specifically:

```
if c == '\r' && ctx.nextChar() == '\n' {
    ctx.addOriginBuf('\r') // <-- This line causes the issue
    s.progress(ctx, 1)
    s.offset++
    c = '\n'
}
```
This causes '\r' characters to leak into scalar content instead of being normalized into a single '\n', violating YAML 1.2 behavior.

Just flagging this for now — I’ve traced it down while debugging a block scalar issue with CRLF-encoded files.

Solution patch:
diff --git a/decode.go b/decode.go
index 19414dd..43c317f 100644
--- a/decode.go
+++ b/decode.go
@@ -40,6 +40,7 @@ type Decoder struct {
 	isResolvedReference  bool
 	validator            StructValidator
 	disallowUnknownField bool
+	allowedFieldPrefixes []string
 	allowDuplicateMapKey bool
 	useOrderedMap        bool
 	useJSONUnmarshaler   bool
@@ -1446,12 +1447,21 @@ func (d *Decoder) decodeStruct(ctx context.Context, dst reflect.Value, src ast.N
 	// Unknown fields are expected (they could be fields from the parent struct).
 	if len(unknownFields) != 0 && d.disallowUnknownField && src.GetToken() != nil {
 		for key, node := range unknownFields {
-			return errors.ErrUnknownField(fmt.Sprintf(`unknown field "%s"`, key), node.GetToken())
+			var ok bool
+			for _, prefix := range d.allowedFieldPrefixes {
+				if strings.HasPrefix(key, prefix) {
+					ok = true
+					break
+				}
+			}
+			if !ok {
+				return errors.ErrUnknownField(fmt.Sprintf(`unknown field "%s"`, key), node.GetToken())
+			}
 		}
 	}
 
 	if d.validator != nil {
-		if err := d.validator.Struct(dst.Addr().Interface()); err != nil {
+		if err := d.validator.Struct(dst.Interface()); err != nil {
 			ev := reflect.ValueOf(err)
 			if ev.Type().Kind() == reflect.Slice {
 				for i := 0; i < ev.Len(); i++ {
diff --git a/option.go b/option.go
index 32dbe23..12b8f27 100644
--- a/option.go
+++ b/option.go
@@ -69,6 +69,15 @@ func DisallowUnknownField() DecodeOption {
 	}
 }
 
+// AllowFieldPrefixes, when paired with [DisallowUnknownField], allows fields
+// with the specified prefixes to bypass the unknown field check.
+func AllowFieldPrefixes(prefixes ...string) DecodeOption {
+	return func(d *Decoder) error {
+		d.allowedFieldPrefixes = append(d.allowedFieldPrefixes, prefixes...)
+		return nil
+	}
+}
+
 // AllowDuplicateMapKey ignore syntax error when mapping keys that are duplicates.
 func AllowDuplicateMapKey() DecodeOption {
 	return func(d *Decoder) error {
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 13a7ecc..799f469 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -777,6 +777,15 @@ func (s *Scanner) scanComment(ctx *Context) bool {
 func (s *Scanner) scanMultiLine(ctx *Context, c rune) error {
 	state := ctx.getMultiLineState()
 	ctx.addOriginBuf(c)
+	// normalize CR and CRLF to LF
+	if c == '\r' {
+		if ctx.nextChar() == '\n' {
+			ctx.addOriginBuf('\n')
+			s.progress(ctx, 1)
+			s.offset++
+		}
+		c = '\n'
+	}
 	if ctx.isEOS() {
 		if s.isFirstCharAtLine && c == ' ' {
 			state.addIndent(ctx, s.column)
@@ -1148,14 +1157,25 @@ func (s *Scanner) scanMultiLineHeaderOption(ctx *Context) error {
 	s.progress(ctx, 1) // skip '|' or '>' character
 
 	var progress int
+	var crlf bool
 	for idx, c := range ctx.src[ctx.idx:] {
 		progress = idx
 		ctx.addOriginBuf(c)
 		if s.isNewLineChar(c) {
+			nextIdx := ctx.idx + idx + 1
+			if c == '\r' && nextIdx < len(ctx.src) && ctx.src[nextIdx] == '\n' {
+				crlf = true
+				continue // process \n in the next iteration
+			}
 			break
 		}
 	}
-	value := strings.TrimRight(ctx.source(ctx.idx, ctx.idx+progress), " ")
+	endPos := ctx.idx + progress
+	if crlf {
+		// Exclude \r
+		endPos = endPos - 1
+	}
+	value := strings.TrimRight(ctx.source(ctx.idx, endPos), " ")
 	commentValueIndex := strings.Index(value, "#")
 	opt := value
 	if commentValueIndex > 0 {
@@ -1189,7 +1209,7 @@ func (s *Scanner) scanMultiLineHeaderOption(ctx *Context) error {
 		ctx.setFolded(s.lastDelimColumn, opt)
 	}
 	if commentIndex > 0 {
-		comment := string(value[commentValueIndex+1:])
+		comment := value[commentValueIndex+1:]
 		s.offset += len(headerBuf)
 		s.column += len(headerBuf)
 		ctx.addToken(token.Comment(comment, string(ctx.obuf[len(headerBuf):]), s.pos()))



Test patch:
diff --git a/decode_test.go b/decode_test.go
index 798e5e7..60ba511 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -954,6 +954,14 @@ merge:
 			source: "v: |\n  hello\n  ...\n  world\n",
 			value:  map[string]string{"v": "hello\n...\nworld\n"},
 		},
+		{
+			source: "v: |\r\n  hello\r\n  ...\r\n  world\r\n",
+			value:  map[string]string{"v": "hello\n...\nworld\n"},
+		},
+		{
+			source: "v: |\r  hello\r  ...\r  world\r",
+			value:  map[string]string{"v": "hello\n...\nworld\n"},
+		},
 		{
 			source: "a: !!binary gIGC\n",
 			value:  map[string]string{"a": "\x80\x81\x82"},
@@ -970,6 +978,22 @@ merge:
 				},
 			},
 		},
+		{
+			source: "v:\r\n- A\r\n- |-\r\n  B\r\n  C\r\n",
+			value: map[string][]string{
+				"v": {
+					"A", "B\nC",
+				},
+			},
+		},
+		{
+			source: "v:\r- A\r- |-\r  B\r  C\r",
+			value: map[string][]string{
+				"v": {
+					"A", "B\nC",
+				},
+			},
+		},
 		{
 			source: "v:\n- A\n- |-\n  B\n  C\n\n\n",
 			value: map[string][]string{
@@ -986,6 +1010,22 @@ merge:
 				},
 			},
 		},
+		{
+			source: "v:\r\n- A\r\n- >-\r\n  B\r\n  C\r\n",
+			value: map[string][]string{
+				"v": {
+					"A", "B C",
+				},
+			},
+		},
+		{
+			source: "v:\r- A\r- >-\r  B\r  C\r",
+			value: map[string][]string{
+				"v": {
+					"A", "B C",
+				},
+			},
+		},
 		{
 			source: "v:\n- A\n- >-\n  B\n  C\n\n\n",
 			value: map[string][]string{
@@ -1933,6 +1973,32 @@ children:
 			t.Fatalf(`v.Children[1].B should be 2, got %d`, v.Children[1].B)
 		}
 	})
+
+	t.Run("with allowed prefixes", func(t *testing.T) {
+		var v struct {
+			A string `yaml:"a"`
+		}
+		yml := `---
+a: a_value
+x-some-extra-thing: b_value
+`
+
+		if err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowUnknownField(), yaml.AllowFieldPrefixes("x-")).Decode(&v); err != nil {
+			t.Fatalf(`parsing should succeed: %s`, err)
+		}
+
+		yml = `---
+a: a_value
+b: b_value
+x-some-extra-thing: b_value
+`
+
+		err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowUnknownField(), yaml.AllowFieldPrefixes("x-")).Decode(&v)
+		if err == nil {
+			t.Fatal("expected error")
+		}
+	})
+
 }
 
 func TestDecoder_AllowDuplicateMapKey(t *testing.T) {