Repository: goccy/go-yaml

Issue:
`AnnotateSource` fails with YAML containing directives (e.g., %YAML 1.2)
**Describe the bug**
`AnnotateSource` fails with YAML containing directives (e.g., %YAML 1.2)

**To Reproduce**
[go playground](https://go.dev/play/p/psnui9tPgA2)
```go
package main

import (
	"fmt"

	"github.com/goccy/go-yaml"
)

type S struct {
	A int    `yaml:"a"`
	B string `yaml:"b"`
}

func main() {
	ymlA := `
a: 1
b: "foo"
`
	ymlB := `
%YAML 1.2
---
a: 2
b: "bar"
`
	var a S
	var b S
	var err error

	yaml.Unmarshal([]byte(ymlA), &a)
	yaml.Unmarshal([]byte(ymlB), &b)
	fmt.Printf("a: %+v\n", a)
	fmt.Printf("b: %+v\n", b)

	path, err := yaml.PathString("$.a")
	if err != nil {
		panic(err)
	}

	srcA, err := path.AnnotateSource([]byte(ymlA), true)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(srcA))

	// panic: expected node type is map or map value. but got Directive: invalid query
	srcB, err := path.AnnotateSource([]byte(ymlB), true)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(srcB))
}
```

**Expected behavior**
`AnnotateSource` should correctly process YAML documents containing directives.

**Actual Behavior​**
```txt
a: {A:1 B:foo}
b: {A:2 B:bar}
>  2 | a: 1
          ^
   3 | b: "foo"
panic: expected node type is map or map value. but got Directive: invalid query
```

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




Hints from issue comments:
@gaogao-qwq: btw, here is an ugly workaround for this issue. It skips directive nodes by returning nil for `rootNode` case.
```diff
diff --git a/path.go b/path.go
index 4d2cbba..d96da7d 100644
--- a/path.go
+++ b/path.go
@@ -483,6 +483,9 @@ func (n *rootNode) filter(node ast.Node) (ast.Node, error) {
 	if n.child == nil {
 		return node, nil
 	}
+	if node.Type() == ast.DirectiveType {
+		return nil, nil
+	}
 	filtered, err := n.child.filter(node)
 	if err != nil {
 		return nil, err
```

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/encode.go b/encode.go
index a686f28..e810608 100644
--- a/encode.go
+++ b/encode.go
@@ -712,9 +712,15 @@ func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int
 			anchorNode.Value = encoded
 			encoded = anchorNode
 		}
+
+		kn, err := e.encodeValue(ctx, reflect.ValueOf(key), column)
+		keyNode, ok := kn.(ast.MapKeyNode)
+		if !ok || err != nil {
+			keyNode = e.encodeString(fmt.Sprint(key), column)
+		}
 		node.Values = append(node.Values, ast.MappingValue(
 			nil,
-			e.encodeString(keyText, column),
+			keyNode,
 			encoded,
 		))
 		e.setSmartAnchor(vRef, keyText)
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/path.go b/path.go
index 4d2cbba..568c4b4 100644
--- a/path.go
+++ b/path.go
@@ -258,6 +258,10 @@ func (p *Path) Filter(target, v interface{}) error {
 // FilterFile filter from ast.File by YAMLPath.
 func (p *Path) FilterFile(f *ast.File) (ast.Node, error) {
 	for _, doc := range f.Docs {
+		// For simplicity, directives cannot be the target of operations
+		if doc.Body != nil && doc.Body.Type() == ast.DirectiveType {
+			continue
+		}
 		node, err := p.FilterNode(doc.Body)
 		if err != nil {
 			return nil, err
@@ -352,6 +356,10 @@ func (p *Path) ReplaceWithFile(dst *ast.File, src *ast.File) error {
 // ReplaceNode replace ast.File with ast.Node.
 func (p *Path) ReplaceWithNode(dst *ast.File, node ast.Node) error {
 	for _, doc := range dst.Docs {
+		// For simplicity, directives cannot be the target of operations
+		if doc.Body != nil && doc.Body.Type() == ast.DirectiveType {
+			continue
+		}
 		if node.Type() == ast.DocumentType {
 			node = node.(*ast.DocumentNode).Body
 		}
@@ -364,7 +372,7 @@ func (p *Path) ReplaceWithNode(dst *ast.File, node ast.Node) error {
 
 // AnnotateSource add annotation to passed source ( see section 5.1 in README.md ).
 func (p *Path) AnnotateSource(source []byte, colored bool) ([]byte, error) {
-	file, err := parser.ParseBytes([]byte(source), 0)
+	file, err := parser.ParseBytes(source, 0)
 	if err != nil {
 		return nil, err
 	}
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..2d56942 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -494,6 +494,24 @@ func TestDecoder(t *testing.T) {
 			value:  map[string]interface{}{"a": "50cent_of_dollar"},
 		},
 
+		// Unconventional keys
+		{
+			source: "1: v\n",
+			value:  map[int]string{1: "v"},
+		},
+		{
+			source: "1.1: v\n",
+			value:  map[float64]string{1.1: "v"},
+		},
+		{
+			source: "true: v\n",
+			value:  map[bool]string{true: "v"},
+		},
+		{
+			source: "2015-01-01T00:00:00Z: v\n",
+			value:  map[time.Time]string{time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC): "v"},
+		},
+
 		// Nulls
 		{
 			source: "null",
@@ -954,6 +972,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 +996,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 +1028,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 +1991,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) {
diff --git a/encode_test.go b/encode_test.go
index 85a33dd..8d91996 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -187,6 +187,26 @@ func TestEncoder(t *testing.T) {
 			map[string]string{"a": "-"},
 			nil,
 		},
+		{
+			"1: v\n",
+			map[int]string{1: "v"},
+			nil,
+		},
+		{
+			"1.1: v\n",
+			map[float64]string{1.1: "v"},
+			nil,
+		},
+		{
+			"true: v\n",
+			map[bool]string{true: "v"},
+			nil,
+		},
+		{
+			"2015-01-01T00:00:00Z: v\n",
+			map[time.Time]string{time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC): "v"},
+			nil,
+		},
 		{
 			"123\n",
 			123,
diff --git a/path_test.go b/path_test.go
index efdee3d..0e29306 100644
--- a/path_test.go
+++ b/path_test.go
@@ -214,6 +214,82 @@ store:
 	})
 }
 
+func TestPath_FilterFile(t *testing.T) {
+	tests := []struct {
+		name        string
+		path        string
+		src         string
+		expected    any
+		expectedErr string
+	}{
+		{
+			name:     "simple key",
+			path:     "$.key",
+			src:      `key: value`,
+			expected: "value",
+		},
+		{
+			name: "with directive",
+			path: "$.key",
+			src: `%YAML 1.2
+---
+key: value`,
+			expected: "value",
+		},
+		{
+			name: "multiple docs",
+			path: "$.key2",
+			src: `key1: value1
+---
+key2: value2
+`,
+			expected: "value2",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			path, err := yaml.PathString(test.path)
+			if err != nil {
+				t.Fatalf("unexpected error during path parsing: %+v", err)
+			}
+
+			file, err := parser.ParseBytes([]byte(test.src), 0)
+			if err != nil {
+				t.Fatalf("failed to parse YAML: %+v", err)
+			}
+
+			node, err := path.FilterFile(file)
+			if test.expectedErr != "" {
+				if err == nil {
+					t.Fatal("expected error but got none")
+				}
+				if !strings.Contains(err.Error(), test.expectedErr) {
+					t.Fatalf("expected error containing %q but got %q", test.expectedErr, err.Error())
+				}
+				return
+			}
+
+			if err != nil {
+				t.Fatalf("unexpected error: %+v", err)
+			}
+
+			if node == nil {
+				t.Fatal("expected node but got nil")
+			}
+
+			var actual any
+			if err := yaml.Unmarshal([]byte(node.String()), &actual); err != nil {
+				t.Fatalf("failed to unmarshal result: %+v", err)
+			}
+
+			if !reflect.DeepEqual(test.expected, actual) {
+				t.Fatalf("expected %v(%T) but got %v(%T)", test.expected, test.expected, actual, actual)
+			}
+		})
+	}
+}
+
 func TestPath_ReservedKeyword(t *testing.T) {
 	tests := []struct {
 		name     string
@@ -605,6 +681,22 @@ b: 2
 			expected: `
 a: 3
 b: 2
+`,
+		},
+		{
+			path: "$.a",
+			dst: `
+%YAML 1.2
+---
+a: 1
+b: 2
+`,
+			src: `3`,
+			expected: `
+%YAML 1.2
+---
+a: 3
+b: 2
 `,
 		},
 		{