// Package yaml adapts the private upstream parser to ConfigKit's document model. package yaml import ( "errors" "io" "regexp" "strings" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/plugin/limit" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) const ( codeInvalid = "yaml.invalid" codeEmpty = "yaml.empty" codeMultipleDocuments = "yaml.multiple_documents" codeRootType = "yaml.root_type" codeDuplicateKey = "yaml.duplicate_key" codeTag = "yaml.tag" codeAnchor = "yaml.anchor" codeAlias = "yaml.alias" codeMergeKey = "yaml.merge_key" codeScalar = "yaml.scalar" codeDepthLimit = "yaml.depth_limit" codeNodeLimit = "yaml.node_limit" ) var ( jsonNumberPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$`) decimalLikePattern = regexp.MustCompile(`^[+-]?[0-9][0-9_]*(\.[0-9_]*)?([eE][+-]?[0-9_]*)?$`) prefixedNumberPattern = regexp.MustCompile(`^[+-]?0[xXoO][0-9A-Fa-f_]+$`) leadingDecimalPattern = regexp.MustCompile(`^[+-]?\.[0-9_]+$`) nonFiniteNumberPattern = regexp.MustCompile(`(?i)^[+-]?\.(inf|nan)$`) errParserDepth = errors.New("configkit yaml parser depth limit") ) // Limits controls private parser and translated-tree resource bounds. type Limits struct { Depth int Nodes int ParserDepth int } // Issue is a parser-independent, value-free YAML diagnostic. type Issue struct { Code string Message string Line int Column int Related []document.Location } // Parse loads exactly one policy-valid mapping document. func Parse(input io.Reader, sourceID string, limits Limits) (*document.Node, []Issue) { parserDepth := limits.ParserDepth if parserDepth <= 0 { parserDepth = limits.Depth + 2 } depthExceeded := false plugin := limit.New(limit.DepthFunc(func(depth int, _ *limit.DepthContext) error { if depth > parserDepth { depthExceeded = true return errParserDepth } return nil })) loader, err := yaml.NewLoader(input, yaml.WithPlugin(plugin)) if err != nil { return nil, []Issue{{Code: codeInvalid, Message: "YAML parser could not be initialized"}} } var parsed yaml.Node if err := loader.Load(&parsed); err != nil { if errors.Is(err, io.EOF) { return nil, []Issue{{Code: codeEmpty, Message: "configuration input is empty"}} } return nil, []Issue{parserIssue(err, depthExceeded)} } var trailing yaml.Node if err := loader.Load(&trailing); err == nil { return nil, []Issue{{Code: codeMultipleDocuments, Message: "configuration input must contain exactly one document", Line: trailing.Line, Column: trailing.Column}} } else if !errors.Is(err, io.EOF) { return nil, []Issue{parserIssue(err, depthExceeded)} } root := documentRoot(&parsed) if root == nil || (root.Kind == yaml.ScalarNode && root.Value == "" && root.ShortTag() == "!!null") { return nil, []Issue{{Code: codeEmpty, Message: "configuration document is empty", Line: parsed.Line, Column: parsed.Column}} } if root.Kind != yaml.MappingNode { return nil, []Issue{{Code: codeRootType, Message: "configuration document root must be a mapping", Line: root.Line, Column: root.Column}} } state := translationState{sourceID: sourceID, limits: limits} translated, issue := state.translate(root, 0) if issue != nil { return nil, []Issue{*issue} } return translated, nil } type translationState struct { sourceID string limits Limits nodes int } func (s *translationState) translate(node *yaml.Node, depth int) (*document.Node, *Issue) { if depth > s.limits.Depth { return nil, nodeIssue(codeDepthLimit, "configuration exceeds the nesting depth limit", node) } if node.Style&yaml.TaggedStyle != 0 { return nil, nodeIssue(codeTag, "explicit YAML tags are not supported", node) } if node.Anchor != "" { return nil, nodeIssue(codeAnchor, "YAML anchors are not supported", node) } if node.Kind == yaml.AliasNode || node.Alias != nil { return nil, nodeIssue(codeAlias, "YAML aliases are not supported", node) } if issue := s.chargeNode(node); issue != nil { return nil, issue } location := document.Location{SourceID: s.sourceID, Line: node.Line, Column: node.Column} switch node.Kind { case yaml.MappingNode: if len(node.Content)%2 != 0 { return nil, nodeIssue(codeInvalid, "mapping structure is invalid", node) } result := &document.Node{Kind: document.Mapping, Location: location, Mapping: make([]document.Entry, 0, len(node.Content)/2)} seen := make(map[string]document.Location, len(node.Content)/2) for i := 0; i < len(node.Content); i += 2 { keyNode, valueNode := node.Content[i], node.Content[i+1] if issue := s.chargeNode(keyNode); issue != nil { return nil, issue } key, issue := mappingKey(keyNode) if issue != nil { return nil, issue } if key == "<<" { return nil, nodeIssue(codeMergeKey, "YAML merge keys are not supported", keyNode) } keyLocation := document.Location{SourceID: s.sourceID, Line: keyNode.Line, Column: keyNode.Column} if previous, ok := seen[key]; ok { issue := nodeIssue(codeDuplicateKey, "mapping key is defined more than once", keyNode) issue.Related = []document.Location{previous} return nil, issue } seen[key] = keyLocation value, issue := s.translate(valueNode, depth+1) if issue != nil { return nil, issue } result.Mapping = append(result.Mapping, document.Entry{ Key: key, KeyLocation: keyLocation, ValueLocation: value.Location, Value: value, }) } return result, nil case yaml.SequenceNode: result := &document.Node{Kind: document.Sequence, Location: location, Sequence: make([]*document.Node, 0, len(node.Content))} for _, child := range node.Content { translated, issue := s.translate(child, depth+1) if issue != nil { return nil, issue } result.Sequence = append(result.Sequence, translated) } return result, nil case yaml.ScalarNode: kind, text, issue := scalar(node) if issue != nil { return nil, issue } return &document.Node{Kind: kind, Location: location, Text: text}, nil default: return nil, nodeIssue(codeInvalid, "YAML node kind is not supported", node) } } func (s *translationState) chargeNode(node *yaml.Node) *Issue { s.nodes++ if s.limits.Nodes >= 0 && s.nodes > s.limits.Nodes { return nodeIssue(codeNodeLimit, "configuration exceeds the node limit", node) } return nil } func mappingKey(node *yaml.Node) (string, *Issue) { if node.Kind == yaml.AliasNode || node.Alias != nil { return "", nodeIssue(codeAlias, "YAML aliases are not supported", node) } if node.Kind != yaml.ScalarNode { return "", nodeIssue(codeScalar, "mapping keys must be strings", node) } if node.Style&yaml.TaggedStyle != 0 { return "", nodeIssue(codeTag, "explicit YAML tags are not supported", node) } if node.Anchor != "" { return "", nodeIssue(codeAnchor, "YAML anchors are not supported", node) } kind, text, issue := scalar(node) if issue != nil { return "", issue } if kind != document.String { return "", nodeIssue(codeScalar, "mapping keys must be strings", node) } return text, nil } func scalar(node *yaml.Node) (document.Kind, string, *Issue) { if node.Style&(yaml.DoubleQuotedStyle|yaml.SingleQuotedStyle|yaml.LiteralStyle|yaml.FoldedStyle) != 0 { return document.String, node.Value, nil } value := node.Value switch value { case "": if node.ShortTag() == "!!null" { return document.Null, "", nil } case "null", "~": return document.Null, "", nil case "true", "false": return document.Boolean, value, nil } lower := strings.ToLower(value) if lower == "true" || lower == "false" || lower == "null" || lower == "yes" || lower == "no" || lower == "on" || lower == "off" { return 0, "", nodeIssue(codeScalar, "plain scalar uses an unsupported reserved form", node) } if jsonNumberPattern.MatchString(value) { return document.Number, value, nil } if decimalLikePattern.MatchString(value) || prefixedNumberPattern.MatchString(value) || leadingDecimalPattern.MatchString(value) || nonFiniteNumberPattern.MatchString(value) { return 0, "", nodeIssue(codeScalar, "plain scalar uses an unsupported numeric form", node) } if tag := node.ShortTag(); tag == "!!int" || tag == "!!float" { return 0, "", nodeIssue(codeScalar, "plain scalar uses an unsupported numeric form", node) } return document.String, value, nil } func documentRoot(node *yaml.Node) *yaml.Node { if node.Kind == yaml.DocumentNode && len(node.Content) == 1 { return node.Content[0] } return node } func parserIssue(err error, depthExceeded bool) Issue { code := codeInvalid if depthExceeded || errors.Is(err, errParserDepth) { code = codeDepthLimit } issue := Issue{Code: code, Message: "configuration contains invalid YAML"} var loadError *yaml.LoadError if errors.As(err, &loadError) { issue.Line = loadError.Mark.Line issue.Column = loadError.Mark.Column } return issue } func nodeIssue(code, message string, node *yaml.Node) *Issue { return &Issue{Code: code, Message: message, Line: node.Line, Column: node.Column} }