2026-09-06 19:30:31 +00:00
// 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"
2026-11-01 14:19:37 +00:00
"go.maximumdirect.net/configkit/internal/document"
2026-09-06 19:30:31 +00:00
)
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 }
}