diff --git a/decode_test.go b/decode_test.go index 4ec7893c..8239623f 100644 --- a/decode_test.go +++ b/decode_test.go @@ -606,6 +606,19 @@ var unmarshalTests = []struct { &struct{ B []int }{[]int{1, 2}}, }, + // Bug https://github.com/yaml/go-yaml/issues/109 + { + // alias must be followed by a space in mapping node + "foo: &bar bar\n*bar : quz\n", + map[string]any{"foo": "bar", "bar": "quz"}, + }, + + { + // alias can contain various characters specified by the YAML specification + "foo: &b./ar bar\n*b./ar : quz\n", + map[string]any{"foo": "bar", "bar": "quz"}, + }, + // Bug #1133337 { "foo: ''", @@ -1111,6 +1124,9 @@ var unmarshalErrorTests = []struct { {"a: 1\nb: 2\nc 2\nd: 3\n", "^yaml: line 3: could not find expected ':'$"}, {"#\n-\n{", "yaml: line 3: could not find expected ':'"}, // Issue #665 {"0: [:!00 \xef", "yaml: incomplete UTF-8 octet sequence"}, // Issue #666 + // anchor cannot contain a colon + // https://github.com/yaml/go-yaml/issues/109 + {"foo: &bar: bar\n*bar: : quz\n", "^yaml: mapping values are not allowed in this context$"}, { "a: &a [00,00,00,00,00,00,00,00,00]\n" + "b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]\n" + diff --git a/emitterc.go b/emitterc.go index aaf37746..4e6e497f 100644 --- a/emitterc.go +++ b/emitterc.go @@ -813,7 +813,16 @@ func (emitter *yamlEmitter) emitBlockMappingKey(event *yamlEvent, first bool) bo } if emitter.checkSimpleKey() { emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE) - return emitter.emitNode(event, false, false, true, true) + if !emitter.emitNode(event, false, false, true, true) { + return false + } + + if event.typ == yaml_ALIAS_EVENT { + // make sure there's a space after the alias + return emitter.put(' ') + } + + return true } if !emitter.writeIndicator([]byte{'?'}, true, false, true) { return false diff --git a/node_test.go b/node_test.go index 9a0394ab..836463b2 100644 --- a/node_test.go +++ b/node_test.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "os" + "reflect" "strings" "testing" @@ -1129,7 +1130,7 @@ var nodeTests = []struct { }}, }, }, { - "a: &anchor(.!@#$%^&*+=?:;)name [1, 2]\nb: *anchor(.!@#$%^&*+=?:;)name\n", + "a: &anchor(.!@#$%^&*+=?;)name [1, 2]\nb: *anchor(.!@#$%^&*+=?;)name\n", yaml.Node{ Kind: yaml.DocumentNode, Line: 1, @@ -1147,11 +1148,11 @@ var nodeTests = []struct { Line: 1, Column: 1, }, - saveNode("anchor(.!@#$%^&*+=?:;)name", &yaml.Node{ + saveNode("anchor(.!@#$%^&*+=?;)name", &yaml.Node{ Kind: yaml.SequenceNode, Style: yaml.FlowStyle, Tag: "!!seq", - Anchor: "anchor(.!@#$%^&*+=?:;)name", + Anchor: "anchor(.!@#$%^&*+=?;)name", Line: 1, Column: 4, Content: []*yaml.Node{{ @@ -1159,13 +1160,13 @@ var nodeTests = []struct { Value: "1", Tag: "!!int", Line: 1, - Column: 33, + Column: 32, }, { Kind: yaml.ScalarNode, Value: "2", Tag: "!!int", Line: 1, - Column: 36, + Column: 35, }}, }), { @@ -1177,14 +1178,58 @@ var nodeTests = []struct { }, { Kind: yaml.AliasNode, - Value: "anchor(.!@#$%^&*+=?:;)name", - Alias: dropNode("anchor(.!@#$%^&*+=?:;)name"), + Value: "anchor(.!@#$%^&*+=?;)name", + Alias: dropNode("anchor(.!@#$%^&*+=?;)name"), Line: 2, Column: 4, }, }, }}, }, + }, { + "a: &x 1\n*x : c\n", + yaml.Node{ + Kind: yaml.DocumentNode, + Line: 1, + Column: 1, + Content: []*yaml.Node{{ + Kind: yaml.MappingNode, + Line: 1, + Column: 1, + Tag: "!!map", + Content: []*yaml.Node{ + { + Kind: yaml.ScalarNode, + Value: "a", + Tag: "!!str", + Line: 1, + Column: 1, + }, + saveNode("x", &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "1", + Tag: "!!int", + Anchor: "x", + Line: 1, + Column: 4, + }), + { + Kind: yaml.AliasNode, + Value: "x", + Alias: dropNode("x"), + Line: 2, + Column: 1, + }, + { + Kind: yaml.ScalarNode, + Value: "c", + Tag: "!!str", + Line: 2, + Column: 6, + }, + }, + }}, + }, }, { "# One\n# Two\ntrue # Three\n# Four\n# Five\n", yaml.Node{ @@ -2756,53 +2801,125 @@ func TestNodeRoundtrip(t *testing.T) { defer os.Setenv("TZ", os.Getenv("TZ")) os.Setenv("TZ", "UTC") for i, item := range nodeTests { - t.Logf("test %d: %q", i, item.yaml) + t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { + t.Logf("test %d: %q", i, item.yaml) + if strings.Contains(item.yaml, "#") { + var buf bytes.Buffer + fprintComments(&buf, &item.node, " ") + t.Logf(" expected comments:\n%s", buf.Bytes()) + } - if strings.Contains(item.yaml, "#") { - var buf bytes.Buffer - fprintComments(&buf, &item.node, " ") - t.Logf(" expected comments:\n%s", buf.Bytes()) - } + decode := true + encode := true - decode := true - encode := true + testYaml := item.yaml + if s := strings.TrimPrefix(testYaml, "[decode]"); s != testYaml { + encode = false + testYaml = s + } + if s := strings.TrimPrefix(testYaml, "[encode]"); s != testYaml { + decode = false + testYaml = s + } - testYaml := item.yaml - if s := strings.TrimPrefix(testYaml, "[decode]"); s != testYaml { - encode = false - testYaml = s - } - if s := strings.TrimPrefix(testYaml, "[encode]"); s != testYaml { - decode = false - testYaml = s - } + if decode { + var node yaml.Node + err := yaml.Unmarshal([]byte(testYaml), &node) + assert.NoError(t, err) + if strings.Contains(item.yaml, "#") { + var buf bytes.Buffer + fprintComments(&buf, &node, " ") + t.Logf(" obtained comments:\n%s", buf.Bytes()) + } - if decode { - var node yaml.Node - err := yaml.Unmarshal([]byte(testYaml), &node) - assert.NoError(t, err) - if strings.Contains(item.yaml, "#") { - var buf bytes.Buffer - fprintComments(&buf, &node, " ") - t.Logf(" obtained comments:\n%s", buf.Bytes()) + assertNodeEqual(t, &item.node, &node) } - assert.DeepEqual(t, &item.node, &node) + if encode { + node := deepCopyNode(&item.node, nil) + buf := bytes.Buffer{} + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + err := enc.Encode(node) + assert.NoError(t, err) + err = enc.Close() + assert.NoError(t, err) + assert.Equal(t, buf.String(), testYaml) + + // Ensure there were no mutations to the tree. + assertNodeEqual(t, &item.node, node) + } + }) + } +} + +// assertNodeEqual is a helper to check whether two YAML nodes are equal. +func assertNodeEqual(t *testing.T, want *yaml.Node, got *yaml.Node) { + t.Helper() + + if reflect.DeepEqual(got, want) { + // fast path + return + } + + if got.Tag != want.Tag { + t.Errorf("Tag mismatch: want: %q got: %q", want.Tag, got.Tag) + } + + if got.Kind != want.Kind { + t.Errorf("Kind mismatch: want: %q got: %q", want.Kind, got.Kind) + } + + if got.Style != want.Style { + t.Errorf("Style mismatch: want: %q got: %q", want.Style, got.Style) + } + + if got.HeadComment != want.HeadComment { + t.Errorf("HeadComment mismatch: want: %#v got: %#v", want.HeadComment, got.HeadComment) + } + + if got.LineComment != want.LineComment { + t.Errorf("LineComment mismatch: want: %#v got: %#v", want.LineComment, got.LineComment) + } + + if got.FootComment != want.FootComment { + t.Errorf("FootComment mismatch: want: %#v got: %#v", want.FootComment, got.FootComment) + } + + if got.Value != want.Value { + t.Errorf("Value mismatch: want: %q got: %q", want.Value, got.Value) + } + + if got.Anchor != want.Anchor { + t.Errorf("Anchor mismatch: want: %q got: %q", want.Anchor, got.Anchor) + } + + if got.Line != want.Line { + t.Errorf("Line mismatch: want: %d got: %d", want.Line, got.Line) + } + + if got.Column != want.Column { + t.Errorf("Column mismatch: want: %d got: %d", want.Column, got.Column) + } + + if !reflect.DeepEqual(got.Content, want.Content) { + // Content differs + + if len(got.Content) != len(want.Content) { + t.Errorf("Content length mismatch:\nwant: %d\ngot: %d", len(want.Content), len(got.Content)) } - if encode { - node := deepCopyNode(&item.node, nil) - buf := bytes.Buffer{} - enc := yaml.NewEncoder(&buf) - enc.SetIndent(2) - err := enc.Encode(node) - assert.NoError(t, err) - err = enc.Close() - assert.NoError(t, err) - assert.Equal(t, buf.String(), testYaml) - // Ensure there were no mutations to the tree. - assert.DeepEqual(t, &item.node, node) + for i := 0; i < len(want.Content) && i < len(got.Content); i++ { + assertNodeEqual(t, want.Content[i], got.Content[i]) } } + + if t.Failed() { + // we already reported an error, there is no need to report it again. + return + } + + // this error message is harder to read, and is only shown if no other errors were reported. + t.Errorf("nodes differ:\nwant:\n%#v\ngot:\n%#v", want, got) } func deepCopyNode(node *yaml.Node, cache map[*yaml.Node]*yaml.Node) *yaml.Node { @@ -2909,7 +3026,7 @@ func TestSetString(t *testing.T) { node.SetString(item.str) - assert.DeepEqual(t, item.node, node) + assertNodeEqual(t, &item.node, &node) buf := bytes.Buffer{} enc := yaml.NewEncoder(&buf) diff --git a/yamlprivateh.go b/yamlprivateh.go index 58b9a4ee..70d27aac 100644 --- a/yamlprivateh.go +++ b/yamlprivateh.go @@ -67,6 +67,18 @@ func isFlowIndicator(b []byte, i int) bool { // We further limit it to ascii chars only, which is a subset of the spec // production but is usually what most people expect. func isAnchorChar(b []byte, i int) bool { + if isColon(b, i) { + // [Go] we exclude colons from anchor/alias names. + // + // A colon is a valid anchor character according to the YAML 1.2 specification, + // but it can lead to ambiguity. + // https://github.com/yaml/go-yaml/issues/109 + // + // Also, it would have been a breaking change to support it, as go.yaml.in/yaml/v3 ignores it. + // Supporting it could lead to unexpected behavior. + return false + } + return isPrintable(b, i) && !isLineBreak(b, i) && !isBlank(b, i) && @@ -75,6 +87,11 @@ func isAnchorChar(b []byte, i int) bool { isASCII(b, i) } +// isColon checks whether the character at the specified position is a colon. +func isColon(b []byte, i int) bool { + return b[i] == ':' +} + // Check if the character at the specified position is a digit. func isDigit(b []byte, i int) bool { return b[i] >= '0' && b[i] <= '9' diff --git a/yts/known-failing-tests b/yts/known-failing-tests index f8e91cff..714438e8 100644 --- a/yts/known-failing-tests +++ b/yts/known-failing-tests @@ -4,6 +4,8 @@ TestYAMLSuite/2JQS/UnmarshalTest TestYAMLSuite/2JQS/EventComparisonTest TestYAMLSuite/2LFX/UnmarshalTest TestYAMLSuite/2LFX/EventComparisonTest +TestYAMLSuite/2SXE/UnmarshalTest +TestYAMLSuite/2SXE/EventComparisonTest TestYAMLSuite/35KP/JSONComparisonTest TestYAMLSuite/3HFZ/UnmarshalTest TestYAMLSuite/3UYS/UnmarshalTest @@ -185,6 +187,8 @@ TestYAMLSuite/01#15/UnmarshalTest TestYAMLSuite/01#15/EventComparisonTest TestYAMLSuite/W4TN/UnmarshalTest TestYAMLSuite/W4TN/EventComparisonTest +TestYAMLSuite/W5VH/UnmarshalTest +TestYAMLSuite/W5VH/EventComparisonTest TestYAMLSuite/WZ62/UnmarshalTest TestYAMLSuite/WZ62/EventComparisonTest TestYAMLSuite/X38W/UnmarshalTest @@ -192,6 +196,8 @@ TestYAMLSuite/X38W/EventComparisonTest TestYAMLSuite/X4QW/UnmarshalTest TestYAMLSuite/X4QW/EventComparisonTest TestYAMLSuite/XW4D/UnmarshalTest +TestYAMLSuite/Y2GN/EventComparisonTest +TestYAMLSuite/Y2GN/JSONComparisonTest TestYAMLSuite/001/UnmarshalTest TestYAMLSuite/001/EventComparisonTest TestYAMLSuite/003/UnmarshalTest