201 lines
8.0 KiB
Go
201 lines
8.0 KiB
Go
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}
|
|
}
|