package yaml import ( "io" "strings" "testing" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/plugin/limit" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestParsePreservesDocumentValuesAndLocations(t *testing.T) { t.Parallel() input := "# comment\nwide: 9223372036854775808\nwhen: 2026-09-06\nquoted: \"yes\"\nflow: [true, null, ~, false]\nempty:\n" root, issues := Parse(strings.NewReader(input), "source-1", generousLimits()) if len(issues) != 0 { t.Fatalf("Parse() issues = %#v", issues) } if root.Kind != document.Mapping || len(root.Mapping) != 5 { t.Fatalf("root = %#v", root) } if root.Mapping[0].Value.Kind != document.Number || root.Mapping[0].Value.Text != "9223372036854775808" { t.Fatalf("wide number = %#v", root.Mapping[0].Value) } if root.Mapping[0].KeyLocation.Line != 2 || root.Mapping[0].ValueLocation.Column != 7 { t.Fatalf("locations = %#v", root.Mapping[0]) } if root.Mapping[1].Value.Kind != document.String || root.Mapping[1].Value.Text != "2026-09-06" { t.Fatalf("timestamp-like text = %#v", root.Mapping[1].Value) } if root.Mapping[3].Value.Sequence[1].Kind != document.Null || root.Mapping[3].Value.Sequence[2].Kind != document.Null { t.Fatalf("null spellings = %#v", root.Mapping[3].Value.Sequence) } if root.Mapping[4].Value.Kind != document.Null { t.Fatalf("empty value = %#v", root.Mapping[4].Value) } } func TestParsePolicyTable(t *testing.T) { t.Parallel() tests := []struct { name string input string code string }{ {name: "empty input", input: "", code: codeEmpty}, {name: "empty document", input: "---\n", code: codeEmpty}, {name: "sequence root", input: "- value\n", code: codeRootType}, {name: "scalar root", input: "value\n", code: codeRootType}, {name: "second document", input: "a: 1\n---\nb: 2\n", code: codeMultipleDocuments}, {name: "empty second document", input: "a: 1\n---\n", code: codeMultipleDocuments}, {name: "duplicate key", input: "a: 1\na: 2\n", code: codeDuplicateKey}, {name: "nested duplicate", input: "a: {b: 1, b: 2}\n", code: codeDuplicateKey}, {name: "explicit standard tag", input: "a: !!str value\n", code: codeTag}, {name: "explicit custom tag", input: "a: !custom value\n", code: codeTag}, {name: "anchor", input: "a: &name value\n", code: codeAnchor}, {name: "alias source anchor", input: "a: &name value\nb: *name\n", code: codeAnchor}, {name: "merge key", input: "<<: value\n", code: codeMergeKey}, {name: "quoted merge key", input: "\"<<\": value\n", code: codeMergeKey}, {name: "boolean key", input: "true: value\n", code: codeScalar}, {name: "number key", input: "1: value\n", code: codeScalar}, {name: "null key", input: "null: value\n", code: codeScalar}, {name: "legacy yes", input: "a: yes\n", code: codeScalar}, {name: "legacy mixed", input: "a: OfF\n", code: codeScalar}, {name: "reserved case", input: "a: TRUE\n", code: codeScalar}, {name: "hex", input: "a: 0x10\n", code: codeScalar}, {name: "octal", input: "a: 0o10\n", code: codeScalar}, {name: "separator", input: "a: 1_000\n", code: codeScalar}, {name: "leading plus", input: "a: +1\n", code: codeScalar}, {name: "leading zero", input: "a: 01\n", code: codeScalar}, {name: "negative leading zero", input: "a: -01\n", code: codeScalar}, {name: "leading dot", input: "a: .5\n", code: codeScalar}, {name: "trailing dot", input: "a: 1.\n", code: codeScalar}, {name: "incomplete exponent", input: "a: 1e\n", code: codeScalar}, {name: "incomplete signed exponent", input: "a: 1e+\n", code: codeScalar}, {name: "infinity", input: "a: .inf\n", code: codeScalar}, {name: "nan", input: "a: .NaN\n", code: codeScalar}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, issues := Parse(strings.NewReader(test.input), "source-1", generousLimits()) if len(issues) != 1 || issues[0].Code != test.code { t.Fatalf("issues = %#v, want code %q", issues, test.code) } }) } } func TestTranslationRejectsAliasNodes(t *testing.T) { t.Parallel() node := &yaml.Node{Kind: yaml.AliasNode, Line: 2, Column: 3} state := translationState{sourceID: "source-1", limits: generousLimits()} if _, issue := state.translate(node, 0); issue == nil || issue.Code != codeAlias { t.Fatalf("alias issue = %#v", issue) } } func TestParseAcceptsSupportedSyntax(t *testing.T) { t.Parallel() inputs := []string{ "plain: text\nquoted_bool: 'ON'\nquoted_number: \"01\"\nblock: |\n text\n", "{zero: 0, negative: -1, fraction: 1.25, exponent: 1e+3, boolean: false}\n", "\"true\": value\n\"1\": number key\n\"null\": null key\n", } for _, input := range inputs { if _, issues := Parse(strings.NewReader(input), "source-1", generousLimits()); len(issues) != 0 { t.Errorf("Parse(%q) issues = %#v", input, issues) } } } func TestDuplicateKeyReportsBothLocations(t *testing.T) { t.Parallel() _, issues := Parse(strings.NewReader("same: first\nsame: second\n"), "source-1", generousLimits()) if len(issues) != 1 || issues[0].Code != codeDuplicateKey || issues[0].Line != 2 || len(issues[0].Related) != 1 || issues[0].Related[0].Line != 1 { t.Fatalf("duplicate issues = %#v", issues) } } func TestParseEnforcesTreeAndParserLimits(t *testing.T) { t.Parallel() for _, input := range []string{"a:\n b: value\n", "{a: {b: value}}\n"} { if _, issues := Parse(strings.NewReader(input), "source-1", Limits{Depth: 2, Nodes: 20, ParserDepth: 20}); len(issues) != 0 { t.Fatalf("depth boundary rejected for %q: %#v", input, issues) } if _, issues := Parse(strings.NewReader(input), "source-1", Limits{Depth: 1, Nodes: 20, ParserDepth: 20}); len(issues) != 1 || issues[0].Code != codeDepthLimit { t.Fatalf("depth overflow for %q = %#v", input, issues) } } if _, issues := Parse(strings.NewReader("a: 1\n"), "source-1", Limits{Depth: 2, Nodes: 3, ParserDepth: 20}); len(issues) != 0 { t.Fatalf("node boundary rejected: %#v", issues) } if _, issues := Parse(strings.NewReader("a: 1\n"), "source-1", Limits{Depth: 2, Nodes: 2, ParserDepth: 20}); len(issues) != 1 || issues[0].Code != codeNodeLimit { t.Fatalf("node overflow = %#v", issues) } if _, issues := Parse(strings.NewReader("a:\n b:\n c: value\n"), "source-1", Limits{Depth: 20, Nodes: 20, ParserDepth: 1}); len(issues) != 1 || issues[0].Code != codeDepthLimit { t.Fatalf("parser depth overflow = %#v", issues) } } func TestParserDepthCalibrationForBlockAndFlow(t *testing.T) { t.Parallel() inputs := []string{"a:\n b: value\n", "{a: {b: value}}\n"} for _, input := range inputs { maximum := observedParserDepth(t, input) if maximum > 4 { t.Fatalf("parser depth %d exceeds root-zero tree depth plus safety offset", maximum) } if _, issues := Parse(strings.NewReader(input), "source-1", Limits{Depth: 2, Nodes: 20, ParserDepth: maximum}); len(issues) != 0 { t.Fatalf("parser boundary %d rejected %q: %#v", maximum, input, issues) } if _, issues := Parse(strings.NewReader(input), "source-1", Limits{Depth: 20, Nodes: 20, ParserDepth: maximum - 1}); len(issues) != 1 || issues[0].Code != codeDepthLimit { t.Fatalf("parser overflow at %d for %q: %#v", maximum-1, input, issues) } } } func observedParserDepth(t *testing.T, input string) int { t.Helper() maximum := 0 depthPlugin := limit.New(limit.DepthFunc(func(depth int, _ *limit.DepthContext) error { maximum = max(maximum, depth) return nil })) loader, err := yaml.NewLoader(strings.NewReader(input), yaml.WithPlugin(depthPlugin)) if err != nil { t.Fatal(err) } var node yaml.Node if err := loader.Load(&node); err != nil && err != io.EOF { t.Fatal(err) } return maximum } func TestMalformedParserDiagnosticIsSafe(t *testing.T) { t.Parallel() const canary = "CANARY_SECRET_VALUE" _, issues := Parse(strings.NewReader("value: ["+canary+"\n"), "source-1", generousLimits()) if len(issues) != 1 || issues[0].Code != codeInvalid || issues[0].Line == 0 { t.Fatalf("issues = %#v", issues) } if strings.Contains(issues[0].Message, canary) { t.Fatal("parser diagnostic exposed input") } } func generousLimits() Limits { return Limits{Depth: 32, Nodes: 1_000, ParserDepth: 64} }