diff --git a/go.mod b/go.mod index e3f660eb1..6b814e53c 100644 --- a/go.mod +++ b/go.mod @@ -144,7 +144,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.150.0 // indirect github.com/openai/openai-go/v3 v3.32.0 // indirect github.com/pb33f/jsonpath v0.8.2 // indirect - github.com/pb33f/libopenapi v0.36.1 // indirect + github.com/pb33f/libopenapi v0.38.3 // indirect github.com/pb33f/libopenapi-validator v0.13.4 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -186,11 +186,11 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect diff --git a/go.sum b/go.sum index 49f9dee7a..b2000019b 100644 --- a/go.sum +++ b/go.sum @@ -406,12 +406,14 @@ github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= -github.com/pb33f/libopenapi v0.36.1 h1:CNZ52e+/W9fA1kAgL8EePDQQrKPfN9+HdLR6XAxUEpw= -github.com/pb33f/libopenapi v0.36.1/go.mod h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4= +github.com/pb33f/libopenapi v0.38.3 h1:ToJU49mGkr6IVTPvTY9V0QkzuoS0+HarSOuzHN8z54A= +github.com/pb33f/libopenapi v0.38.3/go.mod h1:8yHl64vr+ICrnzSgiwJmZ54heRqCqrhI/JL1ge+CPIY= github.com/pb33f/libopenapi-validator v0.13.4 h1:/6NJtmPIZYPT2mEbMAz+BfSoGoCNrwYL46ikEaFy0pM= github.com/pb33f/libopenapi-validator v0.13.4/go.mod h1:e6HLPJM493yfJmnNn81fb3Mt6Iyy8Knd1iuzyyW1A9Y= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +github.com/pb33f/testify v0.1.0 h1:g48/HDU/jn2COspS4nM0scptxiKTJ4DnbX/4ehK6IZ8= +github.com/pb33f/testify v0.1.0/go.mod h1:nq283P/jJ8hXMmdhAqfj7BJIz0y+6IOHj9q0044rKt4= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -581,8 +583,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= -go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= +go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -619,8 +621,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/vendor/github.com/pb33f/libopenapi/.gitignore b/vendor/github.com/pb33f/libopenapi/.gitignore index 069611e7d..d890c2ef0 100644 --- a/vendor/github.com/pb33f/libopenapi/.gitignore +++ b/vendor/github.com/pb33f/libopenapi/.gitignore @@ -1,3 +1,4 @@ test-operation.yaml .idea/ -*.iml \ No newline at end of file +*.iml +.zed/ diff --git a/vendor/github.com/pb33f/libopenapi/README.md b/vendor/github.com/pb33f/libopenapi/README.md index fa79a5711..649ea081a 100644 --- a/vendor/github.com/pb33f/libopenapi/README.md +++ b/vendor/github.com/pb33f/libopenapi/README.md @@ -72,6 +72,8 @@ See all the documentation at https://pb33f.io/libopenapi/ - [What Changed / Diff Engine](https://pb33f.io/libopenapi/what-changed/) - [Overlays](https://pb33f.io/libopenapi/overlays/) - [Arazzo](https://pb33f.io/libopenapi/arazzo/) +- [Generating Code](https://pb33f.io/libopenapi/generating-code/) +- [Parsing Code](https://pb33f.io/libopenapi/parsing-code/) - [FAQ](https://pb33f.io/libopenapi/faq/) - [About libopenapi](https://pb33f.io/libopenapi/about/) --- diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/document_config.go b/vendor/github.com/pb33f/libopenapi/datamodel/document_config.go index 5da94ef31..3a431005a 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/document_config.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/document_config.go @@ -39,8 +39,8 @@ type DocumentConfiguration struct { // RemoteURLHandler is a function that will be used to retrieve remote documents. If not set, the default // remote document getter will be used. // - // The remote handler is only used if the BaseURL is set. If the BaseURL is not set, then the remote handler - // will not be used, as there will be nothing to use it against. + // The remote handler is only used if AllowRemoteReferences is true. If AllowRemoteReferences is false, then + // the remote handler will not be used even when BaseURL is set. // // Resolves [#132]: https://github.com/pb33f/libopenapi/issues/132 RemoteURLHandler utils.RemoteURLHandler @@ -89,12 +89,8 @@ type DocumentConfiguration struct { // AllowRemoteReferences will allow the index to lookup remote references. This is disabled by default. // - // This behavior is now driven by the inclusion of a BaseURL. If a BaseURL is set, then the - // rolodex will look for remote references. If no BaseURL is set, then the rolodex will not look for - // remote references. This value has no effect as of version 0.13.0 and will be removed in a future release. - // - // This value when set, will force the creation of a remote file system even when the BaseURL has not been set. - // it will suck in every http link it finds, and recurse through all references located in each document. + // BaseURL is used to resolve relative references, but it does not enable remote fetching on its own. Remote + // lookup only occurs when this value is true. AllowRemoteReferences bool // AvoidIndexBuild will avoid building the index. This is disabled by default, only use if you are sure you don't need it. @@ -105,11 +101,16 @@ type DocumentConfiguration struct { // passed in and used. Only enable this when parsing non openapi documents. BypassDocumentCheck bool - // SkipJSONConversion skips the YAML-to-JSON conversion during spec parsing. - // SpecJSON and SpecJSONBytes on SpecInfo will be nil when enabled. - // This also skips structural validation that parseJSON performs (e.g., duplicate key detection). + // SkipJSONConversion disables the JSON representation of the document entirely: + // SpecInfo.GetSpecJSON and GetSpecJSONBytes return nil when enabled (and the + // deprecated SpecJSON/SpecJSONBytes fields stay nil). This also skips the eager + // structural validation performed at parse time (e.g., duplicate key detection). // Safe when document-level schema validation rules are not running and no custom // functions depend on the JSON representation. + // + // Note: the JSON representation is built lazily on first accessor call, so leaving + // this disabled no longer costs anything at parse time. Enable it only to also + // skip the eager structural validation, or to guarantee accessors return nil. SkipJSONConversion bool // IgnorePolymorphicCircularReferences will skip over checking for circular references in polymorphic schemas. @@ -143,6 +144,20 @@ type DocumentConfiguration struct { // defaults to false (which means extensions will be included) ExcludeExtensionRefs bool + // SkipMetadataCollection disables the collection of diagnostic metadata during indexing: + // descriptions, summaries, enums, objects-with-properties, security requirement + // references, and the JSONPath `Path` values on inline schema references. Skipping + // them significantly reduces allocations and retained memory when parsing large + // documents. Reference extraction, resolution and model building are unaffected. + // + // -- UNSAFE FOR DIAGNOSTIC, RULE, OR PATH CONSUMERS -- + // When enabled, the index methods GetAllDescriptions, GetAllSummaries, GetAllEnums, + // GetAllObjectsWithProperties, GetSecurityRequirementReferences and the related + // counts are intentionally empty/zero, and inline schema Reference.Path values are + // empty strings. vacuum and any other tool that consumes index metadata or Path + // values must NOT enable this. Defaults to false (everything is collected). + SkipMetadataCollection bool + // BundleInlineRefs controls whether local component references are inlined during bundling. // When false (default): Local refs like #/components/schemas/Pet are preserved // When true: Local refs are also inlined (may break discriminator mappings) diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema.go b/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema.go index 16f1070a4..58dec858a 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema.go @@ -56,16 +56,19 @@ type Schema struct { // 3.1 Specific properties Contains *SchemaProxy `json:"contains,omitempty" yaml:"contains,omitempty"` - MinContains *int64 `json:"minContains,omitempty" yaml:"minContains,omitempty"` - MaxContains *int64 `json:"maxContains,omitempty" yaml:"maxContains,omitempty"` + MinContains *int64 `json:"minContains,renderZero,omitempty" yaml:"minContains,renderZero,omitempty"` + MaxContains *int64 `json:"maxContains,renderZero,omitempty" yaml:"maxContains,renderZero,omitempty"` If *SchemaProxy `json:"if,omitempty" yaml:"if,omitempty"` Else *SchemaProxy `json:"else,omitempty" yaml:"else,omitempty"` Then *SchemaProxy `json:"then,omitempty" yaml:"then,omitempty"` DependentSchemas *orderedmap.Map[string, *SchemaProxy] `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"` DependentRequired *orderedmap.Map[string, []string] `json:"dependentRequired,omitempty" yaml:"dependentRequired,omitempty"` PatternProperties *orderedmap.Map[string, *SchemaProxy] `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` - PropertyNames *SchemaProxy `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` - UnevaluatedItems *SchemaProxy `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` + + // Defs holds reusable JSON Schema definitions declared under the $defs keyword. + Defs *orderedmap.Map[string, *SchemaProxy] `json:"$defs,omitempty" yaml:"$defs,omitempty"` + PropertyNames *SchemaProxy `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` + UnevaluatedItems *SchemaProxy `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` // in 3.1 UnevaluatedProperties can be a Schema or a boolean // https://github.com/pb33f/libopenapi/issues/118 @@ -102,15 +105,15 @@ type Schema struct { MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` Maximum *float64 `json:"maximum,renderZero,omitempty" yaml:"maximum,renderZero,omitempty"` Minimum *float64 `json:"minimum,renderZero,omitempty," yaml:"minimum,renderZero,omitempty"` - MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *int64 `json:"maxLength,renderZero,omitempty" yaml:"maxLength,renderZero,omitempty"` + MinLength *int64 `json:"minLength,renderZero,omitempty" yaml:"minLength,renderZero,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` - MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItems *int64 `json:"maxItems,renderZero,omitempty" yaml:"maxItems,renderZero,omitempty"` + MinItems *int64 `json:"minItems,renderZero,omitempty" yaml:"minItems,renderZero,omitempty"` UniqueItems *bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProperties *int64 `json:"maxProperties,renderZero,omitempty" yaml:"maxProperties,renderZero,omitempty"` + MinProperties *int64 `json:"minProperties,renderZero,omitempty" yaml:"minProperties,renderZero,omitempty"` Required []string `json:"required,omitempty" yaml:"required,omitempty"` Enum []*yaml.Node `json:"enum,omitempty" yaml:"enum,omitempty"` AdditionalProperties *DynamicValue[*SchemaProxy, bool] `json:"additionalProperties,renderZero,omitempty" yaml:"additionalProperties,renderZero,omitempty"` @@ -367,50 +370,21 @@ func NewSchema(schema *base.Schema) *Schema { } s.Enum = enum - // async work. - // any polymorphic properties need to be handled in their own threads - // any properties each need to be processed in their own thread. - // we go as fast as we can. - polyCompletedChan := make(chan struct{}) - errChan := make(chan error) - - type buildResult struct { - idx int - s *SchemaProxy - } - - // for every item, build schema async - buildSchema := func(sch lowmodel.ValueReference[*base.SchemaProxy], idx int, bChan chan buildResult) { - n := &lowmodel.NodeReference[*base.SchemaProxy]{ - ValueNode: sch.ValueNode, - Value: sch.Value, - } - n.SetReference(sch.GetReference(), sch.GetReferenceNode()) - - p := NewSchemaProxy(n) - - bChan <- buildResult{idx: idx, s: p} - } - - // schema async - buildOutSchemas := func(schemas []lowmodel.ValueReference[*base.SchemaProxy], items *[]*SchemaProxy, - doneChan chan struct{}, e chan error, - ) { - bChan := make(chan buildResult) - totalSchemas := len(schemas) + // each item is a single SchemaProxy struct construction: spinning up goroutines + // and channels per item costs far more than the work itself, so build inline. + buildOutSchemas := func(schemas []lowmodel.ValueReference[*base.SchemaProxy]) []*SchemaProxy { + items := make([]*SchemaProxy, len(schemas)) for i := range schemas { - go buildSchema(schemas[i], i, bChan) - } - j := 0 - for j < totalSchemas { - r := <-bChan - j++ - (*items)[r.idx] = r.s + n := &lowmodel.NodeReference[*base.SchemaProxy]{ + ValueNode: schemas[i].ValueNode, + Value: schemas[i].Value, + } + n.SetReference(schemas[i].GetReference(), schemas[i].GetReferenceNode()) + items[i] = NewSchemaProxy(n) } - doneChan <- struct{}{} + return items } - // props async buildProps := func(k lowmodel.KeyReference[string], v lowmodel.ValueReference[*base.SchemaProxy], props *orderedmap.Map[string, *SchemaProxy], sw int, ) { @@ -427,15 +401,23 @@ func NewSchema(schema *base.Schema) *Schema { s.DependentSchemas = props case 2: s.PatternProperties = props + case 3: + s.Defs = props } } props := orderedmap.New[string, *SchemaProxy]() + if !schema.Properties.IsEmpty() { + s.Properties = props + } for name, schemaProxy := range schema.Properties.Value.FromOldest() { buildProps(name, schemaProxy, props, 0) } dependents := orderedmap.New[string, *SchemaProxy]() + if !schema.DependentSchemas.IsEmpty() { + s.DependentSchemas = dependents + } for name, schemaProxy := range schema.DependentSchemas.Value.FromOldest() { buildProps(name, schemaProxy, dependents, 1) } @@ -450,10 +432,20 @@ func NewSchema(schema *base.Schema) *Schema { } patternProps := orderedmap.New[string, *SchemaProxy]() + if !schema.PatternProperties.IsEmpty() { + s.PatternProperties = patternProps + } for name, schemaProxy := range schema.PatternProperties.Value.FromOldest() { buildProps(name, schemaProxy, patternProps, 2) } + if !schema.Defs.IsEmpty() { + defs := orderedmap.New[string, *SchemaProxy]() + for name, schemaProxy := range schema.Defs.Value.FromOldest() { + buildProps(name, schemaProxy, defs, 3) + } + } + var allOf []*SchemaProxy var oneOf []*SchemaProxy var anyOf []*SchemaProxy @@ -461,21 +453,14 @@ func NewSchema(schema *base.Schema) *Schema { var items *DynamicValue[*SchemaProxy, bool] var prefixItems []*SchemaProxy - children := 0 if !schema.AllOf.IsEmpty() { - children++ - allOf = make([]*SchemaProxy, len(schema.AllOf.Value)) - go buildOutSchemas(schema.AllOf.Value, &allOf, polyCompletedChan, errChan) + allOf = buildOutSchemas(schema.AllOf.Value) } if !schema.AnyOf.IsEmpty() { - children++ - anyOf = make([]*SchemaProxy, len(schema.AnyOf.Value)) - go buildOutSchemas(schema.AnyOf.Value, &anyOf, polyCompletedChan, errChan) + anyOf = buildOutSchemas(schema.AnyOf.Value) } if !schema.OneOf.IsEmpty() { - children++ - oneOf = make([]*SchemaProxy, len(schema.OneOf.Value)) - go buildOutSchemas(schema.OneOf.Value, &oneOf, polyCompletedChan, errChan) + oneOf = buildOutSchemas(schema.OneOf.Value) } if !schema.Not.IsEmpty() { not = NewSchemaProxy(&schema.Not) @@ -495,21 +480,7 @@ func NewSchema(schema *base.Schema) *Schema { } } if !schema.PrefixItems.IsEmpty() { - children++ - prefixItems = make([]*SchemaProxy, len(schema.PrefixItems.Value)) - go buildOutSchemas(schema.PrefixItems.Value, &prefixItems, polyCompletedChan, errChan) - } - - completeChildren := 0 - if children > 0 { - allDone: - for { - <-polyCompletedChan - completeChildren++ - if children == completeChildren { - break allDone - } - } + prefixItems = buildOutSchemas(schema.PrefixItems.Value) } s.OneOf = oneOf s.AnyOf = anyOf @@ -559,6 +530,12 @@ func (s *Schema) RenderInline() ([]byte, error) { // MarshalYAML will create a ready to render YAML representation of the Schema object. func (s *Schema) MarshalYAML() (interface{}, error) { + if s.ParentProxy != nil { + if node, ok, err := s.ParentProxy.renderTransformedRefWithSiblings(s); ok || err != nil { + return node, err + } + } + nb := high.NewNodeBuilder(s, s.low) // determine index version @@ -573,6 +550,14 @@ func (s *Schema) MarshalYAML() (interface{}, error) { // MarshalJSON will create a ready to render JSON representation of the Schema object. func (s *Schema) MarshalJSON() ([]byte, error) { + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + node, err := s.ParentProxy.referenceYAMLNodeForSchema(s) + if err != nil { + return nil, err + } + return marshalYAMLNodeJSON(node) + } + nb := high.NewNodeBuilder(s, s.low) // determine index version @@ -584,13 +569,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) { } // render node node := nb.Render() - var renderedJSON map[string]interface{} - - // marshal into struct - _ = node.Decode(&renderedJSON) - - // return JSON bytes - return json.Marshal(renderedJSON) + return marshalYAMLNodeJSON(node) } // MarshalYAMLInlineWithContext will render out the Schema pointer as YAML using the provided @@ -606,6 +585,9 @@ func (s *Schema) MarshalYAMLInlineWithContext(ctx any) (interface{}, error) { renderCtx = NewInlineRenderContext() ctx = renderCtx } + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + return s.ParentProxy.marshalParsedRefWithSiblingsInline(renderCtx, s) + } // determine if we should preserve discriminator refs based on rendering mode. // in validation mode, we need to fully inline all refs for the JSON schema compiler. @@ -647,6 +629,14 @@ func (s *Schema) MarshalYAMLInline() (interface{}, error) { // MarshalJSONInline will render out the Schema pointer as JSON, and all refs will be inlined fully func (s *Schema) MarshalJSONInline() ([]byte, error) { + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + rendered, err := s.MarshalYAMLInline() + if err != nil { + return nil, err + } + return marshalYAMLRenderJSON(rendered) + } + nb := high.NewNodeBuilder(s, s.low) nb.Resolve = true // determine index version @@ -658,11 +648,21 @@ func (s *Schema) MarshalJSONInline() ([]byte, error) { } // render node node := nb.Render() - var renderedJSON map[string]interface{} + return marshalYAMLNodeJSON(node) +} - // marshal into struct - _ = node.Decode(&renderedJSON) +func marshalYAMLRenderJSON(rendered interface{}) ([]byte, error) { + node, ok := yamlNodeFromRender(rendered) + if !ok { + return nil, errors.New("unable to render schema as JSON: YAML render was not a node") + } + return marshalYAMLNodeJSON(node) +} - // return JSON bytes +func marshalYAMLNodeJSON(node *yaml.Node) ([]byte, error) { + var renderedJSON map[string]interface{} + if err := node.Decode(&renderedJSON); err != nil { + return nil, err + } return json.Marshal(renderedJSON) } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema_proxy.go b/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema_proxy.go index 84b4ce2e2..53ecc1f5c 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema_proxy.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/high/base/schema_proxy.go @@ -175,12 +175,12 @@ type SchemaProxy struct { buildError error rendered *Schema refStr string - lock *sync.Mutex + lock sync.Mutex } // NewSchemaProxy creates a new high-level SchemaProxy from a low-level one. func NewSchemaProxy(schema *low.NodeReference[*base.SchemaProxy]) *SchemaProxy { - return &SchemaProxy{schema: schema, lock: &sync.Mutex{}} + return &SchemaProxy{schema: schema} } // copySchemaWithParentProxy creates a shallow copy of a schema and sets the ParentProxy @@ -193,13 +193,13 @@ func (sp *SchemaProxy) copySchemaWithParentProxy(schema *Schema) *Schema { // CreateSchemaProxy will create a new high-level SchemaProxy from a high-level Schema, this acts the same // as if the SchemaProxy is pre-rendered. func CreateSchemaProxy(schema *Schema) *SchemaProxy { - return &SchemaProxy{rendered: schema, lock: &sync.Mutex{}} + return &SchemaProxy{rendered: schema} } // CreateSchemaProxyRef will create a new high-level SchemaProxy from a reference string, this is used only when // building out new models from scratch that require a reference rather than a schema implementation. func CreateSchemaProxyRef(ref string) *SchemaProxy { - return &SchemaProxy{refStr: ref, lock: &sync.Mutex{}} + return &SchemaProxy{refStr: ref} } // CreateSchemaProxyRefWithSchema creates a SchemaProxy that carries both a $ref and sibling schema @@ -208,7 +208,7 @@ func CreateSchemaProxyRef(ref string) *SchemaProxy { // // If schema is nil, the result behaves identically to CreateSchemaProxyRef. func CreateSchemaProxyRefWithSchema(ref string, schema *Schema) *SchemaProxy { - return &SchemaProxy{refStr: ref, rendered: schema, lock: &sync.Mutex{}} + return &SchemaProxy{refStr: ref, rendered: schema} } // GetValueNode returns the value node of the SchemaProxy. @@ -228,7 +228,7 @@ func (sp *SchemaProxy) GetValueNode() *yaml.Node { // instead for proxies created using NewSchemaProxy or CreateSchema* methods. // https://github.com/pb33f/libopenapi/issues/403 func (sp *SchemaProxy) Schema() *Schema { - if sp == nil || sp.lock == nil { + if sp == nil { return nil } @@ -243,6 +243,11 @@ func (sp *SchemaProxy) Schema() *Schema { return nil } + if sp.isParsedRefWithSiblings() { + sp.rendered = sp.buildSiblingOnlySchemaView() + return sp.rendered + } + // check the high-level cache first. idx := sp.schema.Value.GetIndex() if idx != nil && sp.schema.Value != nil { @@ -294,6 +299,8 @@ func (sp *SchemaProxy) Schema() *Schema { } // IsReference returns true if the SchemaProxy is a reference to another Schema. +// For parsed OpenAPI 3.1 $ref-with-siblings schemas, the low proxy is backed by +// an internal allOf node, but the high-level API reflects the authored $ref. func (sp *SchemaProxy) IsReference() bool { if sp == nil { return false @@ -302,6 +309,9 @@ func (sp *SchemaProxy) IsReference() bool { if sp.refStr != "" { return true } + if sp.isParsedRefWithSiblings() { + return true + } if sp.schema != nil && sp.schema.Value != nil { return sp.schema.Value.IsReference() } @@ -313,11 +323,17 @@ func (sp *SchemaProxy) GetReference() string { if sp.refStr != "" { return sp.refStr } + if sp.isParsedRefWithSiblings() { + return sp.schema.Value.GetTransformedRefReference() + } if refNode := sp.GetReferenceNode(); refNode != nil { if refValNode := utils.GetRefValueNode(refNode); refValNode != nil { return refValNode.Value } } + if sp.schema == nil || sp.schema.Value == nil { + return "" + } return sp.schema.GetValue().GetReference() } @@ -332,6 +348,12 @@ func (sp *SchemaProxy) GetReferenceNode() *yaml.Node { if sp.refStr != "" { return utils.CreateRefNode(sp.refStr) } + if sp.isParsedRefWithSiblings() { + return sp.schema.Value.TransformedRef + } + if sp.schema == nil || sp.schema.Value == nil { + return nil + } return sp.schema.GetValue().GetReferenceNode() } @@ -354,9 +376,6 @@ func (sp *SchemaProxy) BuildSchema() (*Schema, error) { return nil, nil } schema := sp.Schema() - if sp.lock == nil { - return schema, sp.buildError - } sp.lock.Lock() er := sp.buildError sp.lock.Unlock() @@ -368,9 +387,6 @@ func (sp *SchemaProxy) GetBuildError() error { if sp == nil { return nil } - if sp.lock == nil { - return sp.buildError - } sp.lock.Lock() err := sp.buildError sp.lock.Unlock() @@ -397,6 +413,69 @@ func (sp *SchemaProxy) isRefWithSiblings() bool { return sp.refStr != "" && sp.rendered != nil && sp.schema == nil } +// IsTransformedRefWithSiblings reports whether this high-level proxy represents +// an authored OpenAPI 3.1 $ref with sibling schema keywords. +func (sp *SchemaProxy) IsTransformedRefWithSiblings() bool { + return sp != nil && + sp.schema != nil && + sp.schema.Value != nil && + sp.schema.Value.IsTransformedRefWithSiblings() && + sp.shouldCollapseTransformedRefWithSiblings() +} + +func (sp *SchemaProxy) isParsedRefWithSiblings() bool { + return sp.IsTransformedRefWithSiblings() +} + +func (sp *SchemaProxy) buildSiblingOnlySchemaView() *Schema { + if sp == nil || sp.schema == nil || sp.schema.Value == nil { + return nil + } + lowProxy := sp.schema.Value + siblingNode := lowProxy.GetTransformedRefSiblingSchema() + if siblingNode == nil { + return nil + } + + lowSchema := new(base.Schema) + if err := lowSchema.Build(lowProxy.GetContext(), siblingNode, lowProxy.GetIndex()); err != nil { + sp.buildError = err + return nil + } + lowSchema.ParentProxy = lowProxy + + schema := NewSchema(lowSchema) + schema.ParentProxy = sp + return schema +} + +// BuildTransformedRefSemanticSchema returns the internal semantic allOf view for +// an authored $ref-with-siblings proxy, using current high-level sibling values. +func (sp *SchemaProxy) BuildTransformedRefSemanticSchema(currentSibling *Schema) (*Schema, error) { + return sp.buildSemanticAllOfSchemaView(currentSibling) +} + +func (sp *SchemaProxy) buildSemanticAllOfSchemaView(currentSibling *Schema) (*Schema, error) { + if sp == nil || sp.schema == nil || sp.schema.Value == nil { + return nil, nil + } + lowSchema := sp.schema.Value.Schema() + if lowSchema == nil { + return nil, sp.schema.Value.GetBuildError() + } + schema := NewSchema(lowSchema) + schema.ParentProxy = nil + if currentSibling == nil { + currentSibling = sp.Schema() + } + if currentSibling != nil && len(schema.AllOf) == 2 { + siblingCopy := *currentSibling + siblingCopy.ParentProxy = nil + schema.AllOf[0] = CreateSchemaProxy(&siblingCopy) + } + return schema, nil +} + // renderRefWithSiblings builds a YAML mapping node containing $ref as the // first key followed by all rendered schema sibling properties. func (sp *SchemaProxy) renderRefWithSiblings() *yaml.Node { @@ -412,6 +491,150 @@ func (sp *SchemaProxy) renderRefWithSiblings() *yaml.Node { return node } +func (sp *SchemaProxy) renderTransformedRefWithSiblings(s *Schema) (*yaml.Node, bool, error) { + if sp == nil || sp.schema == nil || sp.schema.Value == nil || sp.schema.Value.TransformedRef == nil || s == nil { + return nil, false, nil + } + if !sp.shouldCollapseTransformedRefWithSiblings() { + return nil, false, nil + } + + var siblingNode *yaml.Node + ref := sp.schema.Value.GetTransformedRefReference() + + if !sp.schemaIsTransformedSiblingView(s) { + if len(s.AllOf) != 2 || s.AllOf[0] == nil || s.AllOf[1] == nil || !s.AllOf[1].IsReference() { + return nil, false, nil + } + + // Only collapse the synthetic allOf created by the sibling-ref transformer. + // If callers add fields to the outer schema or change its composition, keep + // the explicit allOf so no mutations are hidden. + outerNode := high.NewNodeBuilder(s, s.low).Render() + if len(outerNode.Content) != 2 || outerNode.Content[0].Value != "allOf" { + return nil, false, nil + } + + siblingRender, err := s.AllOf[0].MarshalYAML() + if err != nil { + return nil, true, err + } + var ok bool + siblingNode, ok = yamlNodeFromRender(siblingRender) + if !ok || !utils.IsNodeMap(siblingNode) { + return nil, false, nil + } + ref = s.AllOf[1].GetReference() + } else { + siblingNode = high.NewNodeBuilder(s, s.low).Render() + } + + original := sp.schema.Value.TransformedRef + result := utils.CreateEmptyMapNode() + consumed := make(map[string]struct{}, len(siblingNode.Content)/2) + + for i := 0; i+1 < len(original.Content); i += 2 { + keyNode := original.Content[i] + valueNode := original.Content[i+1] + if keyNode == nil { + continue + } + if keyNode.Value == "$ref" { + refKey := cloneYAMLNode(keyNode) + refValue := cloneYAMLNode(valueNode) + if refValue == nil { + refValue = utils.CreateStringNode(ref) + } + refValue.Value = ref + result.Content = append(result.Content, refKey, refValue) + continue + } + if _, siblingValue := findYAMLPair(siblingNode, keyNode.Value); siblingValue != nil { + renderKey := cloneYAMLNode(keyNode) + result.Content = append(result.Content, renderKey, cloneYAMLNode(siblingValue)) + consumed[keyNode.Value] = struct{}{} + } + } + + for i := 0; i+1 < len(siblingNode.Content); i += 2 { + keyNode := siblingNode.Content[i] + valueNode := siblingNode.Content[i+1] + if _, ok := consumed[keyNode.Value]; ok { + continue + } + result.Content = append(result.Content, cloneYAMLNode(keyNode), cloneYAMLNode(valueNode)) + } + + return result, true, nil +} + +func (sp *SchemaProxy) schemaIsTransformedSiblingView(s *Schema) bool { + if sp == nil || sp.schema == nil || sp.schema.Value == nil || s == nil { + return false + } + return s.low != nil && s.low.RootNode == sp.schema.Value.GetTransformedRefSiblingSchema() +} + +func (sp *SchemaProxy) shouldCollapseTransformedRefWithSiblings() bool { + if sp == nil || sp.schema == nil || sp.schema.Value == nil { + return false + } + idx := sp.schema.Value.GetIndex() + if idx == nil || idx.GetConfig() == nil || idx.GetConfig().SpecInfo == nil { + return true + } + return idx.GetConfig().SpecInfo.VersionNumeric >= 3.1 +} + +func yamlNodeFromRender(rendered interface{}) (*yaml.Node, bool) { + switch node := rendered.(type) { + case *yaml.Node: + return node, node != nil + case yaml.Node: + return &node, true + default: + return nil, false + } +} + +func findYAMLPair(node *yaml.Node, key string) (*yaml.Node, *yaml.Node) { + if node == nil || !utils.IsNodeMap(node) { + return nil, nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i] != nil && node.Content[i].Value == key { + return node.Content[i], node.Content[i+1] + } + } + return nil, nil +} + +func cloneYAMLNode(node *yaml.Node) *yaml.Node { + if node == nil { + return nil + } + clone := &yaml.Node{ + Kind: node.Kind, + Style: node.Style, + Tag: node.Tag, + Value: node.Value, + Anchor: node.Anchor, + Alias: node.Alias, + Line: node.Line, + Column: node.Column, + HeadComment: node.HeadComment, + LineComment: node.LineComment, + FootComment: node.FootComment, + } + if len(node.Content) > 0 { + clone.Content = make([]*yaml.Node, len(node.Content)) + for i, child := range node.Content { + clone.Content[i] = cloneYAMLNode(child) + } + } + return clone +} + // Render will return a YAML representation of the Schema object as a byte slice. func (sp *SchemaProxy) Render() ([]byte, error) { return yaml.Marshal(sp) @@ -419,11 +642,17 @@ func (sp *SchemaProxy) Render() ([]byte, error) { // MarshalYAML will create a ready to render YAML representation of the SchemaProxy object. func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { + if sp.isParsedRefWithSiblings() { + return sp.referenceYAMLNodeForSchema(nil) + } if !sp.IsReference() { s, err := sp.BuildSchema() if err != nil { return nil, err } + if node, ok, renderErr := sp.renderTransformedRefWithSiblings(s); ok || renderErr != nil { + return node, renderErr + } nb := high.NewNodeBuilder(s, s.low) return nb.Render(), nil } @@ -433,6 +662,29 @@ func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { return sp.GetReferenceNode(), nil } +func (sp *SchemaProxy) referenceYAMLNode() (*yaml.Node, error) { + return sp.referenceYAMLNodeForSchema(nil) +} + +func (sp *SchemaProxy) referenceYAMLNodeForSchema(s *Schema) (*yaml.Node, error) { + if sp.isRefWithSiblings() { + return sp.renderRefWithSiblings(), nil + } + if sp.isParsedRefWithSiblings() { + if s == nil { + var err error + s, err = sp.BuildSchema() + if err != nil { + return nil, err + } + } + if node, ok, renderErr := sp.renderTransformedRefWithSiblings(s); ok || renderErr != nil { + return node, renderErr + } + } + return sp.GetReferenceNode(), nil +} + // getInlineRenderKey generates a unique key for tracking this schema during inline rendering. // This prevents infinite recursion when schemas reference each other circularly. func (sp *SchemaProxy) getInlineRenderKey() string { @@ -444,6 +696,22 @@ func (sp *SchemaProxy) getInlineRenderKey() string { } return "" } + if sp.isParsedRefWithSiblings() && sp.schema.ValueNode != nil { + node := sp.schema.ValueNode + idx := sp.schema.Value.GetIndex() + if node.Line > 0 && node.Column > 0 { + source := "inline" + if idx != nil { + source = idx.GetSpecAbsolutePath() + } + return fmt.Sprintf("%s:%d:%d", source, node.Line, node.Column) + } + source := "inline" + if idx != nil { + source = fmt.Sprintf("%s:inline", idx.GetSpecAbsolutePath()) + } + return fmt.Sprintf("%s:%p", source, node) + } // Use the reference string if available if sp.IsReference() { ref := sp.GetReference() @@ -501,13 +769,8 @@ func (sp *SchemaProxy) MarshalYAMLInline() (interface{}, error) { } func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (interface{}, error) { - // refNode returns the correct reference YAML node — with sibling - // properties when this proxy carries both a $ref and schema data. - refNode := func() *yaml.Node { - if sp.isRefWithSiblings() { - return sp.renderRefWithSiblings() - } - return sp.GetReferenceNode() + refNode := func() (*yaml.Node, error) { + return sp.referenceYAMLNode() } // check if this reference should be preserved (set via context by discriminator handling). @@ -516,7 +779,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte if sp.IsReference() { ref := sp.GetReference() if ref != "" && ctx.ShouldPreserveRef(ref) { - return refNode(), nil + return refNode() } } @@ -537,7 +800,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte rootIdx := rolodex.GetRootIndex() // If the schema is in the root index, preserve the ref if rootIdx != nil && schemaIdx == rootIdx { - return refNode(), nil + return refNode() } } } @@ -553,8 +816,9 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte if ctx.StartRendering(renderKey) { // We're already rendering this schema in THIS call chain - return ref to break the cycle if sp.IsReference() { - return refNode(), - fmt.Errorf("schema render failure, circular reference: `%s`", sp.GetReference()) + node, refErr := refNode() + return node, errors.Join(refErr, + fmt.Errorf("schema render failure, circular reference: `%s`", sp.GetReference())) } // For inline schemas, return an empty map to avoid infinite recursion return &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, @@ -585,7 +849,8 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte for _, c := range circ { if sp.IsReference() { if sp.GetReference() == c.LoopPoint.Definition { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } basePath := idx.GetSpecAbsolutePath() @@ -594,7 +859,8 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte } if basePath == c.LoopPoint.FullDefinition { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } a := utils.ReplaceWindowsDriveWithLinuxPath(strings.Replace(c.LoopPoint.FullDefinition, basePath, "", 1)) b := sp.GetReference() @@ -616,14 +882,16 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte bBase, bFragment := index.SplitRefFragment(b) if aFragment != "" && bFragment != "" && aFragment == bFragment { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } if aFragment == "" && bFragment == "" { aNorm := strings.TrimPrefix(strings.TrimPrefix(aBase, "./"), "/") bNorm := strings.TrimPrefix(strings.TrimPrefix(bBase, "./"), "/") if aNorm != "" && bNorm != "" && aNorm == bNorm { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } } } @@ -634,6 +902,9 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte return nil, err } if s != nil { + if sp.isParsedRefWithSiblings() { + return sp.marshalParsedRefWithSiblingsInline(ctx, s) + } // For programmatic ref+siblings proxies, render directly to avoid nil-deref // in Schema.MarshalYAMLInlineWithContext which assumes s.GoLow() is non-nil. if sp.isRefWithSiblings() { @@ -647,3 +918,14 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte } return nil, errors.New("unable to render schema") } + +func (sp *SchemaProxy) marshalParsedRefWithSiblingsInline(ctx *InlineRenderContext, currentSibling *Schema) (interface{}, error) { + s, err := sp.buildSemanticAllOfSchemaView(currentSibling) + if err != nil { + return nil, err + } + if s == nil { + return nil, errors.New("unable to render transformed schema reference") + } + return s.MarshalYAMLInlineWithContext(ctx) +} diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/high/node_builder.go b/vendor/github.com/pb33f/libopenapi/datamodel/high/node_builder.go index dc1a991cb..378eac817 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/high/node_builder.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/high/node_builder.go @@ -5,6 +5,7 @@ package high import ( "fmt" + "math" "reflect" "sort" "strconv" @@ -39,6 +40,32 @@ type RenderableInlineWithContext interface { const renderZero = "renderZero" +func originalFloatLexeme(value float64, lowValue any) (string, bool) { + vnut, ok := lowValue.(low.HasValueNodeUntyped) + if !ok { + return "", false + } + + valueNode := vnut.GetValueNode() + if valueNode == nil || !utils.IsNodeNumberValue(valueNode) { + return "", false + } + + parsed, err := strconv.ParseFloat(valueNode.Value, 64) + if err != nil { + return "", false + } + + if parsed != value { + return "", false + } + if value == 0 && math.Signbit(parsed) != math.Signbit(value) { + return "", false + } + + return valueNode.Value, true +} + // NewNodeBuilder will create a new NodeBuilder instance, this is the only way to create a NodeBuilder. // The function accepts a high level object and a low level object (need to be siblings/same type). // @@ -321,6 +348,36 @@ func (n *NodeBuilder) Render() *yaml.Node { return m } +// encodeSafeValue returns a value safe to pass to (*yaml.Node).Encode. When the +// value is a *yaml.Node it returns a deep copy: Encode desolves the represented +// graph in place (Desolve rewrites Tag/Style), and the representer aliases input +// nodes, so encoding a model-owned node would mutate it. With concurrent renders +// (e.g. linters running rules in parallel) that mutation races with readers of +// the same node. Encoding a copy keeps shared nodes immutable. +func encodeSafeValue(value any) any { + if vn, ok := value.(*yaml.Node); ok { + return deepCopyYAMLNode(vn) + } + return value +} + +func deepCopyYAMLNode(n *yaml.Node) *yaml.Node { + if n == nil { + return nil + } + c := *n + if n.Alias != nil { + c.Alias = deepCopyYAMLNode(n.Alias) + } + if n.Content != nil { + c.Content = make([]*yaml.Node, len(n.Content)) + for i, child := range n.Content { + c.Content[i] = deepCopyYAMLNode(child) + } + } + return &c +} + // AddYAMLNode will add a new *yaml.Node to the parent node, using the tag, key and value provided. // If the value is nil, then the node will not be added. This method is recursive, so it will dig down // into any non-scalar types. @@ -384,6 +441,9 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *ya precision = len(strings.Split(fmt.Sprint(entry.StringValue), ".")[1]) } val := strconv.FormatFloat(value.(float64), 'f', precision, 64) + if original, ok := originalFloatLexeme(value.(float64), entry.LowValue); ok { + val = original + } // Always create float node for float64 values, even if they don't contain decimal points // This handles cases like negative zero (-0.0) which formats as "-0" but should remain float valueNode = utils.CreateFloatNode(val) @@ -462,7 +522,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *ya break } - err := rawNode.Encode(value) + err := rawNode.Encode(encodeSafeValue(value)) if err != nil { return parent } else { @@ -589,6 +649,9 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *ya encodeSkip = true if *b != 0 || entry.RenderZero { formatFloat := strconv.FormatFloat(*b, 'f', -1, 64) + if original, ok := originalFloatLexeme(*b, entry.LowValue); ok { + formatFloat = original + } // Always create float node for float64 values, even if they're whole numbers // This handles cases like negative zero (-0.0) and ensures type consistency @@ -612,7 +675,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *ya } } - err := rawNode.Encode(value) + err := rawNode.Encode(encodeSafeValue(value)) if err != nil { return parent } else { diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/high/shared.go b/vendor/github.com/pb33f/libopenapi/datamodel/high/shared.go index bc72d0853..4e5ee01b7 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/high/shared.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/high/shared.go @@ -20,6 +20,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "go.yaml.in/yaml/v4" ) @@ -147,6 +148,10 @@ func ResolveExternalRef[H any, L any]( return result, nil } + if isResolvedExternalRefModel(lowObj) { + return result, nil + } + ref := lowObj.GetReference() resolved := idx.FindComponent(context.Background(), ref) if resolved == nil || resolved.Node == nil { @@ -168,6 +173,22 @@ func ResolveExternalRef[H any, L any]( return result, nil } +func isResolvedExternalRefModel(lowObj ExternalRefResolver) bool { + if !utils.IsExternalRef(lowObj.GetReference()) { + return false + } + rooted, ok := lowObj.(low.HasRootNode) + if !ok { + return false + } + root := rooted.GetRootNode() + if root == nil { + return false + } + isRef, _, _ := utils.IsNodeRefValue(root) + return !isRef +} + // RenderExternalRef is a convenience function that resolves an external reference and renders it inline. // This combines ResolveExternalRef with RenderInline for the common case where you want to // resolve and immediately render an external reference. diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/high/v3/document.go b/vendor/github.com/pb33f/libopenapi/datamodel/high/v3/document.go index 5bbd231ee..3cea08622 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/high/v3/document.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/high/v3/document.go @@ -173,9 +173,10 @@ func (d *Document) Render() ([]byte, error) { // the rendering will use the original indention of the document. func (d *Document) RenderWithIndention(indent int) []byte { var buf bytes.Buffer - yamlEncoder := yaml.NewEncoder(&buf) - yamlEncoder.SetIndent(indent) - _ = yamlEncoder.Encode(d) + yamlDumper, _ := yaml.NewDumper(&buf, yaml.WithV3Defaults(), yaml.WithLineWidth(-1)) + yamlDumper.SetIndent(indent) + _ = yamlDumper.Dump(d) + _ = yamlDumper.Close() return buf.Bytes() } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/constants.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/constants.go index 078ac9a99..ed20339da 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/constants.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/constants.go @@ -31,6 +31,7 @@ const ( DependentSchemasLabel = "dependentSchemas" DependentRequiredLabel = "dependentRequired" PatternPropertiesLabel = "patternProperties" + DefsLabel = "$defs" IfLabel = "if" ElseLabel = "else" ThenLabel = "then" diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/discriminator.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/discriminator.go index 77157492b..936af1471 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/discriminator.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/discriminator.go @@ -4,12 +4,14 @@ package base import ( + "fmt" "hash/maphash" "go.yaml.in/yaml/v4" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" ) // Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas @@ -31,6 +33,68 @@ type Discriminator struct { low.NodeMap } +// ValidateDiscriminatorMappingValueNodes checks that discriminator mapping values are scalar strings. +func ValidateDiscriminatorMappingValueNodes(discriminatorNode *yaml.Node) error { + discriminatorNode = utils.NodeAlias(discriminatorNode) + if discriminatorNode == nil || discriminatorNode.Kind != yaml.MappingNode { + return nil + } + utils.CheckForMergeNodes(discriminatorNode) + + for i := 0; i < len(discriminatorNode.Content); i += 2 { + keyNode := utils.NodeAlias(discriminatorNode.Content[i]) + if keyNode == nil { + continue + } + if keyNode.Value != "mapping" { + continue + } + + mappingNode := utils.NodeAlias(discriminatorNode.Content[i+1]) + if mappingNode == nil || mappingNode.Kind != yaml.MappingNode { + return fmt.Errorf("discriminator.mapping must be an object") + } + utils.CheckForMergeNodes(mappingNode) + + for j := 0; j < len(mappingNode.Content); j += 2 { + keyNode := utils.NodeAlias(mappingNode.Content[j]) + if keyNode == nil { + continue + } + mappingName := keyNode.Value + valueNode := utils.NodeAlias(mappingNode.Content[j+1]) + if valueNode == nil || valueNode.Kind != yaml.ScalarNode || valueNode.Tag != "!!str" { + return fmt.Errorf("discriminator.mapping.%s must be a string, found %s", mappingName, describeDiscriminatorMappingNode(valueNode)) + } + } + return nil + } + + return nil +} + +func describeDiscriminatorMappingNode(node *yaml.Node) string { + if node == nil { + return "nil" + } + if node.Kind == yaml.ScalarNode { + return node.Tag + } + + switch node.Kind { + case yaml.MappingNode: + return "object" + case yaml.SequenceNode: + return "array" + case yaml.DocumentNode: + return "document" + case yaml.AliasNode: + return "alias" + default: + return fmt.Sprintf("kind %d", node.Kind) + } +} + // GetRootNode will return the root yaml node of the Discriminator object func (d *Discriminator) GetRootNode() *yaml.Node { return d.RootNode diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema.go index 6f81c0171..4b18f6683 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema.go @@ -2,8 +2,6 @@ package base import ( "context" - "errors" - "fmt" "sync" "github.com/pb33f/libopenapi/datamodel/low" @@ -90,7 +88,9 @@ type Schema struct { DependentSchemas low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] DependentRequired low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[[]string]]] - PatternProperties low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] + PatternProperties low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] + // Defs holds reusable JSON Schema definitions declared under the $defs keyword. + Defs low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] PropertyNames low.NodeReference[*SchemaProxy] UnevaluatedItems low.NodeReference[*SchemaProxy] UnevaluatedProperties low.NodeReference[*SchemaDynamicValue[*SchemaProxy, bool]] @@ -153,81 +153,33 @@ type Schema struct { // will specifically look for a key node named 'schema' and extract the value mapped to that key. If the operation // fails then no NodeReference is returned and an error is returned instead. func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*SchemaProxy], error) { - var schLabel, schNode *yaml.Node errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d" - refLocation := "" - var refNode *yaml.Node - - foundIndex := idx - foundCtx := ctx - if rf, rl, rv := utils.IsNodeRefValue(root); rf { - // locate reference in index. - ref, fIdx, err, nCtx := low.LocateRefNodeWithContext(ctx, root, idx) - if ref != nil { - schNode = ref - schLabel = rl - foundCtx = nCtx - foundIndex = fIdx - } else if errors.Is(err, low.ErrExternalRefSkipped) { - refLocation = rv - schema := &SchemaProxy{kn: root, vn: root, idx: idx, ctx: ctx} - _ = schema.Build(ctx, root, root, idx) - n := &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: root, ValueNode: root} - n.SetReference(refLocation, root) - schema.SetReference(refLocation, root) - return n, nil - } else { - v := root.Content[1].Value - if root.Content[1].Value == "" { - v = "[empty]" - } - return nil, fmt.Errorf(errStr, - v, root.Content[1].Line, root.Content[1].Column) - } - } else { - _, schLabel, schNode = utils.FindKeyNodeFull(SchemaLabel, root.Content) - if schNode != nil { - h := false - if h, _, refLocation = utils.IsNodeRefValue(schNode); h { - ref, fIdx, lerr, nCtx := low.LocateRefNodeWithContext(foundCtx, schNode, foundIndex) - if ref != nil { - refNode = schNode - schNode = ref - if fIdx != nil { - foundIndex = fIdx - } - foundCtx = nCtx - } else if errors.Is(lerr, low.ErrExternalRefSkipped) { - refNode = schNode - } else { - v := schNode.Content[1].Value - if schNode.Content[1].Value == "" { - v = "[empty]" - } - return nil, fmt.Errorf(errStr, - v, schNode.Content[1].Line, schNode.Content[1].Column) - } - } - } + if rf, refLabel, _ := utils.IsNodeRefValue(root); rf { + return extractSchemaProxy(ctx, idx, refLabel, root, errStr) } + _, schLabel, schNode := utils.FindKeyNodeFull(SchemaLabel, root.Content) if schNode != nil { - // check if schema has already been built. - schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: foundIndex, ctx: foundCtx} - - // call Build to ensure transformation happens - _ = schema.Build(foundCtx, schLabel, schNode, foundIndex) + return extractSchemaProxy(ctx, idx, schLabel, schNode, errStr) + } + return nil, nil +} - schema.SetReference(refLocation, refNode) +func extractSchemaProxy(ctx context.Context, idx *index.SpecIndex, keyNode, valueNode *yaml.Node, errFormat string) (*low.NodeReference[*SchemaProxy], error) { + resolved, err := resolveSchemaBuildInput(ctx, valueNode, idx, errFormat) + if err != nil { + return nil, err + } - n := &low.NodeReference[*SchemaProxy]{ - Value: schema, - KeyNode: schLabel, - ValueNode: schema.vn, // use transformed node - } - n.SetReference(refLocation, refNode) - return n, nil + built := buildSchemaProxy(resolved.ctx, resolved.idx, keyNode, resolved.valueNode, resolved.scopeNode, resolved.refNode, resolved.transformed, resolved.refLocation) + n := &low.NodeReference[*SchemaProxy]{ + Value: built.Value, + KeyNode: keyNode, + ValueNode: built.ValueNode, } - return nil, nil + if resolved.refLocation != "" { + n.SetReference(resolved.refLocation, resolved.refNode) + } + return n, nil } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build.go index 6e944f58f..597763a73 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build.go @@ -318,6 +318,9 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde _, discLabel, discNode := utils.FindKeyNodeFullTop(DiscriminatorLabel, root.Content) if discNode != nil { + if err := ValidateDiscriminatorMappingValueNodes(discNode); err != nil { + return err + } var discriminator Discriminator _ = low.BuildModel(discNode, &discriminator) discriminator.KeyNode = discLabel @@ -377,6 +380,14 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde s.PatternProperties = *props } + props, err = buildPropertyMap(ctx, s, root, idx, DefsLabel) + if err != nil { + return err + } + if props != nil { + s.Defs = *props + } + itemsIsBool := false itemsBoolValue := false _, itemsLabel, itemsValue := utils.FindKeyNodeFullTop(ItemsLabel, root.Content) diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build_helpers.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build_helpers.go index 661a1c23a..03328453f 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build_helpers.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_build_helpers.go @@ -19,7 +19,9 @@ type resolvedSchemaBuildInput struct { ctx context.Context idx *index.SpecIndex valueNode *yaml.Node + scopeNode *yaml.Node refNode *yaml.Node + transformed *transformedSiblingRef refLocation string } @@ -41,7 +43,7 @@ func buildPropertyMap(ctx context.Context, parent *Schema, root *yaml.Node, idx propertyMap.Set(low.KeyReference[string]{ KeyNode: currentProp, Value: currentProp.Value, - }, buildSchemaProxy(resolved.ctx, resolved.idx, currentProp, resolved.valueNode, resolved.refNode, resolved.refLocation != "", resolved.refLocation)) + }, buildSchemaProxy(resolved.ctx, resolved.idx, currentProp, resolved.valueNode, resolved.scopeNode, resolved.refNode, resolved.transformed, resolved.refLocation)) } return &low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]]{ @@ -100,13 +102,9 @@ func (s *Schema) extractExtensions(root *yaml.Node) { } // buildSchemaProxy builds out a SchemaProxy for a single node. -func buildSchemaProxy(ctx context.Context, idx *index.SpecIndex, kn, vn *yaml.Node, rf *yaml.Node, isRef bool, refLocation string) low.ValueReference[*SchemaProxy] { +func buildSchemaProxy(ctx context.Context, idx *index.SpecIndex, kn, vn, scopeNode, rf *yaml.Node, transformed *transformedSiblingRef, refLocation string) low.ValueReference[*SchemaProxy] { sp := new(SchemaProxy) - if isRef { - sp.prepareForResolvedBuild(ctx, kn, vn, idx, refLocation, rf) - } else { - sp.prepareForResolvedBuild(ctx, kn, vn, idx, "", nil) - } + sp.prepareForResolvedBuild(ctx, kn, vn, scopeNode, idx, refLocation, rf, transformed) return low.ValueReference[*SchemaProxy]{ Value: sp, ValueNode: sp.vn, @@ -130,7 +128,7 @@ func buildSchema(ctx context.Context, labelNode, valueNode *yaml.Node, idx *inde return low.ValueReference[*SchemaProxy]{}, err } - return buildSchemaProxy(resolved.ctx, resolved.idx, labelNode, resolved.valueNode, resolved.refNode, resolved.refLocation != "", resolved.refLocation), nil + return buildSchemaProxy(resolved.ctx, resolved.idx, labelNode, resolved.valueNode, resolved.scopeNode, resolved.refNode, resolved.transformed, resolved.refLocation), nil } // buildSchemaList builds out child schemas for a parent schema. Expected to be an array of schema objects. @@ -152,7 +150,7 @@ func buildSchemaList(ctx context.Context, labelNode, valueNode *yaml.Node, idx * if err != nil { return nil, err } - r := buildSchemaProxy(resolved.ctx, resolved.idx, resolved.valueNode, resolved.valueNode, resolved.refNode, resolved.refLocation != "", resolved.refLocation) + r := buildSchemaProxy(resolved.ctx, resolved.idx, resolved.valueNode, resolved.valueNode, resolved.scopeNode, resolved.refNode, resolved.transformed, resolved.refLocation) results = append(results, r) } @@ -190,17 +188,25 @@ func resolveSchemaBuildInput(ctx context.Context, valueNode *yaml.Node, idx *ind ctx: ctx, idx: idx, valueNode: valueNode, + scopeNode: valueNode, } if valueNode == nil { return resolved, nil } + if transformedValue, transformedRef, wasTransformed := transformSiblingRefNode(valueNode, idx); wasTransformed { + resolved.valueNode = transformedValue + resolved.transformed = transformedRef + return resolved, nil + } + if hasRef, _, refLocation := utils.IsNodeRefValue(valueNode); hasRef { ref, foundIdx, err, foundCtx := low.LocateRefNodeWithContext(ctx, valueNode, idx) if ref != nil { resolved.refNode = valueNode resolved.valueNode = ref + resolved.scopeNode = ref resolved.refLocation = refLocation resolved.ctx = foundCtx resolved.idx = foundIdx @@ -211,8 +217,16 @@ func resolveSchemaBuildInput(ctx context.Context, valueNode *yaml.Node, idx *ind resolved.refLocation = refLocation return resolved, nil } - return resolved, fmt.Errorf(errFormat, valueNode.Content[1].Value, valueNode.Content[1].Line, valueNode.Content[1].Column) + return resolved, schemaReferenceBuildError(errFormat, valueNode) } return resolved, nil } + +func schemaReferenceBuildError(errFormat string, valueNode *yaml.Node) error { + refValue := valueNode.Content[1].Value + if refValue == "" { + refValue = "[empty]" + } + return fmt.Errorf(errFormat, refValue, valueNode.Content[1].Line, valueNode.Content[1].Column) +} diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_hash.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_hash.go index f091e54ca..34064b2c7 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_hash.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_hash.go @@ -324,6 +324,8 @@ func (s *Schema) hash(quick bool) uint64 { writeSchemaMapHashes(sb, s.PatternProperties.Value) + writeSchemaMapHashes(sb, s.Defs.Value) + if len(s.PrefixItems.Value) > 0 { scratch = resizeSchemaHashScratch(scratch, len(s.PrefixItems.Value)) for i := range s.PrefixItems.Value { diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_proxy.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_proxy.go index 1f3d76733..17f1fbeda 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_proxy.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/schema_proxy.go @@ -70,6 +70,7 @@ type SchemaProxy struct { nodeStore sync.Map nodeMap low.NodeMap TransformedRef *yaml.Node // Original node that contained the ref before transformation + transformedRef *transformedSiblingRef *low.NodeMap } @@ -85,18 +86,9 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in // transform sibling refs to allOf structure if enabled and applicable // this ensures sp.vn contains the pre-transformed YAML as the source of truth - transformedValue := value - wasTransformed := false - if idx != nil && idx.GetConfig() != nil && idx.GetConfig().TransformSiblingRefs { - transformer := NewSiblingRefTransformer(idx) - if transformer.ShouldTransform(value) { - transformed, _ := transformer.TransformSiblingRef(value) - if transformed != nil { - transformedValue = transformed - wasTransformed = true - sp.TransformedRef = value // store original node that had the ref - } - } + transformedValue, transformedRef, wasTransformed := transformSiblingRefNode(value, idx) + if wasTransformed { + sp.setTransformedRef(transformedRef) } sp.vn = transformedValue @@ -117,14 +109,27 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in return nil } +func transformSiblingRefNode(value *yaml.Node, idx *index.SpecIndex) (*yaml.Node, *transformedSiblingRef, bool) { + if idx == nil || idx.GetConfig() == nil || !idx.GetConfig().TransformSiblingRefs { + return value, nil, false + } + transformer := NewSiblingRefTransformer(idx) + transformed := transformer.transformSiblingRefWithMetadata(value) + if transformed == nil { + return value, nil, false + } + return transformed.allOfNode, transformed, true +} + // prepareForResolvedBuild initializes proxy state when the caller has already resolved any reference metadata. // This avoids re-running the full Build ref-detection path for child-schema helpers that already did that work. -func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value *yaml.Node, idx *index.SpecIndex, refLocation string, refNode *yaml.Node) { +func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value, scopeNode *yaml.Node, idx *index.SpecIndex, refLocation string, refNode *yaml.Node, transformed *transformedSiblingRef) { sp.kn = key sp.idx = idx sp.vn = value - sp.ctx = applySchemaIdScope(ctx, value, idx) + sp.ctx = applySchemaIdScope(ctx, scopeNode, idx) sp.Reference = low.Reference{} + sp.setTransformedRef(transformed) if refLocation != "" { sp.SetReference(refLocation, refNode) } @@ -133,6 +138,47 @@ func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value * sp.NodeMap = &sp.nodeMap } +func (sp *SchemaProxy) setTransformedRef(transformed *transformedSiblingRef) { + sp.transformedRef = transformed + sp.TransformedRef = nil + if transformed != nil { + sp.TransformedRef = transformed.referenceNode + } +} + +// IsTransformedRefWithSiblings reports whether this proxy was authored as a +// schema-level $ref with sibling keywords and internally normalized to allOf. +func (sp *SchemaProxy) IsTransformedRefWithSiblings() bool { + return sp != nil && sp.transformedRef != nil && sp.transformedRef.reference != "" +} + +// GetTransformedRefSiblingSchema returns the sibling-only schema for an +// internally transformed $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefSiblingSchema() *yaml.Node { + if !sp.IsTransformedRefWithSiblings() { + return nil + } + return sp.transformedRef.siblingNode +} + +// GetTransformedRefReference returns the original reference value for an +// internally transformed $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefReference() string { + if !sp.IsTransformedRefWithSiblings() { + return "" + } + return sp.transformedRef.reference +} + +// GetTransformedRefAllOfSchema returns the internal allOf schema for an +// authored $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefAllOfSchema() *yaml.Node { + if !sp.IsTransformedRefWithSiblings() { + return nil + } + return sp.transformedRef.allOfNode +} + func applySchemaIdScope(ctx context.Context, node *yaml.Node, idx *index.SpecIndex) context.Context { if node == nil { return ctx diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/sibling_ref_transformer.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/sibling_ref_transformer.go index 5173becd8..69c9cd48e 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/base/sibling_ref_transformer.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/base/sibling_ref_transformer.go @@ -4,6 +4,8 @@ package base import ( + "sort" + "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "go.yaml.in/yaml/v4" @@ -15,6 +17,13 @@ type SiblingRefTransformer struct { index *index.SpecIndex } +type transformedSiblingRef struct { + allOfNode *yaml.Node + siblingNode *yaml.Node + referenceNode *yaml.Node + reference string +} + // NewSiblingRefTransformer creates a new transformer instance func NewSiblingRefTransformer(idx *index.SpecIndex) *SiblingRefTransformer { return &SiblingRefTransformer{ @@ -28,17 +37,51 @@ func NewSiblingRefTransformer(idx *index.SpecIndex) *SiblingRefTransformer { // Input: {title: "MySchema", $ref: "#/components/schemas/Base"} // Output: {allOf: [{title: "MySchema"}, {$ref: "#/components/schemas/Base"}]} func (srt *SiblingRefTransformer) TransformSiblingRef(node *yaml.Node) (*yaml.Node, error) { - if !srt.ShouldTransform(node) { + transformed := srt.transformSiblingRefWithMetadata(node) + if transformed == nil { return node, nil // no transformation needed } + return transformed.allOfNode, nil +} +func (srt *SiblingRefTransformer) transformSiblingRefWithMetadata(node *yaml.Node) *transformedSiblingRef { + if srt.index == nil || srt.index.GetConfig() == nil || !srt.index.GetConfig().TransformSiblingRefs { + return nil + } siblings, refValue := srt.ExtractSiblingProperties(node) - return srt.CreateAllOfStructure(refValue, siblings), nil + if len(siblings) == 0 || refValue == "" { + return nil + } + siblingNode := srt.createSiblingSchemaNode(node) + return &transformedSiblingRef{ + allOfNode: srt.createAllOfStructureWithSiblingNode(refValue, siblingNode), + siblingNode: siblingNode, + referenceNode: node, + reference: refValue, + } } // CreateAllOfStructure creates an allOf node structure from ref value and sibling properties func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings map[string]*yaml.Node) *yaml.Node { + var siblingSchemaNode *yaml.Node + if len(siblings) > 0 { + siblingSchemaNode = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + keys := make([]string, 0, len(siblings)) + for key := range siblings { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + valueNode := siblings[key] + keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key} + copiedValueNode := srt.copyNode(valueNode) + siblingSchemaNode.Content = append(siblingSchemaNode.Content, keyNode, copiedValueNode) + } + } + return srt.createAllOfStructureWithSiblingNode(refValue, siblingSchemaNode) +} +func (srt *SiblingRefTransformer) createAllOfStructureWithSiblingNode(refValue string, siblingSchemaNode *yaml.Node) *yaml.Node { allOfNode := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", @@ -50,19 +93,10 @@ func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings allOfArrayNode := allOfNode.Content[1] - // first element: schema with sibling properties (excluding $ref) - if len(siblings) > 0 { - siblingSchemaNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - for key, valueNode := range siblings { - keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key} - // create a copy of the value node to avoid modifying original - copiedValueNode := srt.copyNode(valueNode) - siblingSchemaNode.Content = append(siblingSchemaNode.Content, keyNode, copiedValueNode) - } + if siblingSchemaNode != nil && len(siblingSchemaNode.Content) > 0 { allOfArrayNode.Content = append(allOfArrayNode.Content, siblingSchemaNode) } - // second element: the reference schema refSchemaNode := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", @@ -76,6 +110,22 @@ func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings return allOfNode } +func (srt *SiblingRefTransformer) createSiblingSchemaNode(node *yaml.Node) *yaml.Node { + if !utils.IsNodeMap(node) { + return nil + } + siblingNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for i := 0; i+1 < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + if keyNode == nil || keyNode.Value == "$ref" { + continue + } + siblingNode.Content = append(siblingNode.Content, srt.copyNode(keyNode), srt.copyNode(valueNode)) + } + return siblingNode +} + // ExtractSiblingProperties extracts sibling properties from a node containing $ref // returns a map of sibling properties and the $ref value func (srt *SiblingRefTransformer) ExtractSiblingProperties(node *yaml.Node) (map[string]*yaml.Node, string) { diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/extraction_functions.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/extraction_functions.go index f8f86d030..222aeec47 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/extraction_functions.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/extraction_functions.go @@ -1100,13 +1100,17 @@ func ExtractExtensions(root *yaml.Node) *orderedmap.Map[KeyReference[string], Va if root == nil { return nil } - extensions := utils.FindExtensionNodes(root.Content) extensionMap := orderedmap.New[KeyReference[string], ValueReference[*yaml.Node]]() - for _, ext := range extensions { - extensionMap.Set(KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }, ValueReference[*yaml.Node]{Value: ext.Value, ValueNode: ext.Value}) + content := root.Content + for i := 0; i+1 < len(content); i += 2 { + key := content[i] + if strings.HasPrefix(key.Value, "x-") { + value := utils.NodeAlias(content[i+1]) + extensionMap.Set(KeyReference[string]{ + Value: key.Value, + KeyNode: key, + }, ValueReference[*yaml.Node]{Value: value, ValueNode: value}) + } } return extensionMap } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/model_builder.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/model_builder.go index 4cebe9057..7b721b8f7 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/model_builder.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/model_builder.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" "sync" + "unicode/utf8" "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" @@ -48,6 +49,19 @@ func buildModelFields(modelType reflect.Type) []buildModelField { return actual.([]buildModelField) } +// lowerIfNeeded returns the input unchanged when it contains no uppercase ASCII and no +// multibyte runes (the overwhelmingly common case for OpenAPI keys), avoiding the +// per-key allocation of strings.ToLower on the BuildModel hot path. +func lowerIfNeeded(s string) string { + for i := 0; i < len(s); i++ { + c := s[i] + if c >= utf8.RuneSelf || ('A' <= c && c <= 'Z') { + return strings.ToLower(s) + } + } + return s +} + // BuildModel accepts a yaml.Node pointer and a model, which can be any struct. Using reflection, the model is // analyzed and the names of all the properties are extracted from the model and subsequently looked up from within // the yaml.Node.Content value. @@ -70,7 +84,7 @@ func BuildModel(node *yaml.Node, model interface{}) error { content := node.Content keyMap := make(map[string]int, len(content)/2) for j := 0; j < len(content)-1; j += 2 { - k := strings.ToLower(utils.NodeAlias(content[j]).Value) + k := lowerIfNeeded(utils.NodeAlias(content[j]).Value) if _, exists := keyMap[k]; !exists { keyMap[k] = j } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/v2/swagger.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/v2/swagger.go index 6d079cb9b..7e5211b0a 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/v2/swagger.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/v2/swagger.go @@ -152,6 +152,7 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur idxConfig.BasePath = config.BasePath idxConfig.Logger = config.Logger idxConfig.ExcludeExtensionRefs = config.ExcludeExtensionRefs + idxConfig.SkipMetadataCollection = config.SkipMetadataCollection rolodex := index.NewRolodex(idxConfig) rolodex.SetRootNode(info.RootNode) doc.Rolodex = rolodex @@ -195,8 +196,8 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur } } - // if base url is provided, add a remote filesystem to the rolodex. - if idxConfig.BaseURL != nil { + // Only create a remote filesystem when the caller explicitly allows remote references. + if config.AllowRemoteReferences { // create a remote filesystem remoteFS, _ := index.NewRemoteFSWithConfig(idxConfig) @@ -206,7 +207,11 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur idxConfig.AllowRemoteLookup = true // add to the rolodex - rolodex.AddRemoteFS(config.BaseURL.String(), remoteFS) + u := "default" + if config.BaseURL != nil { + u = config.BaseURL.String() + } + rolodex.AddRemoteFS(u, remoteFS) } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/components.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/components.go index 2f792c2d4..ce6dbdb06 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/components.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/components.go @@ -5,6 +5,7 @@ package v3 import ( "context" + "errors" "fmt" "hash/maphash" "reflect" @@ -50,6 +51,7 @@ type Components struct { type componentBuildResult[T any] struct { key low.KeyReference[string] value low.ValueReference[T] + err error } type componentInput struct { @@ -302,14 +304,55 @@ func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, labe translateFunc := func(_ int, value componentInput) (componentBuildResult[T], error) { var n T = new(N) currentLabel := value.currentLabel - node := value.node + node := utils.NodeAlias(value.node) + foundIndex := idx + foundContext := ctx + var localCircErr error + var refNode *yaml.Node + var referenceValue string + _, isSchemaProxy := any(n).(*base.SchemaProxy) + + if h, _, rv := utils.IsNodeRefValue(node); h && rv != "" && !isSchemaProxy && foundIndex != nil { + ref, fIdx, err, nCtx := low.LocateRefNodeWithContext(foundContext, node, foundIndex) + if ref != nil { + refNode = node + node = ref + referenceValue = rv + if fIdx != nil { + foundIndex = fIdx + } + foundContext = nCtx + if err != nil { + localCircErr = err + } + } else if errors.Is(err, low.ErrExternalRefSkipped) { + low.SetReference(n, rv, node) + v := low.ValueReference[T]{ + Value: n, + ValueNode: node, + } + v.SetReference(rv, node) + return componentBuildResult[T]{ + key: low.KeyReference[string]{ + KeyNode: currentLabel, + Value: currentLabel.Value, + }, + value: v, + }, nil + } else if err != nil { + return componentBuildResult[T]{}, fmt.Errorf("component build failed: reference cannot be found: %s", err.Error()) + } + } // build. _ = low.BuildModel(node, n) - err := n.Build(ctx, currentLabel, node, idx) + err := n.Build(foundContext, currentLabel, node, foundIndex) if err != nil { return componentBuildResult[T]{}, err } + if referenceValue != "" { + low.SetReference(n, referenceValue, refNode) + } nType := reflect.TypeOf(n) nValue := reflect.ValueOf(n) @@ -333,18 +376,27 @@ func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, labe } } + valueRef := low.ValueReference[T]{ + Value: n, + ValueNode: finalValueNode, // use transformed node if available + } + if referenceValue != "" { + valueRef.SetReference(referenceValue, refNode) + } return componentBuildResult[T]{ key: low.KeyReference[string]{ KeyNode: currentLabel, Value: currentLabel.Value, }, - value: low.ValueReference[T]{ - Value: n, - ValueNode: finalValueNode, // use transformed node if available - }, + value: valueRef, + err: localCircErr, }, nil } + var circError error err := datamodel.TranslateSliceParallel(inputs, translateFunc, func(result componentBuildResult[T]) error { + if result.err != nil { + circError = result.err + } componentValues.Set(result.key, result.value) return nil }) @@ -357,5 +409,8 @@ func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, labe ValueNode: nodeValue, Value: componentValues, } + if circError != nil && (idx == nil || !idx.AllowCircularReferenceResolving()) { + return results, circError + } return results, nil } diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/create_document.go b/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/create_document.go index 6d037846a..90e228eb3 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/create_document.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/low/v3/create_document.go @@ -151,6 +151,7 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur idxConfig.SpecInfo = info idxConfig.UseSchemaQuickHash = config.UseSchemaQuickHash idxConfig.ExcludeExtensionRefs = config.ExcludeExtensionRefs + idxConfig.SkipMetadataCollection = config.SkipMetadataCollection idxConfig.IgnoreArrayCircularReferences = config.IgnoreArrayCircularReferences idxConfig.IgnorePolymorphicCircularReferences = config.IgnorePolymorphicCircularReferences idxConfig.AllowUnknownExtensionContentDetection = config.AllowUnknownExtensionContentDetection @@ -247,8 +248,8 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur rolodex.AddLocalFS(cwd, fileFS) } } - // if base url is provided, add a remote filesystem to the rolodex. - if idxConfig.BaseURL != nil || config.AllowRemoteReferences { + // Only create a remote filesystem when the caller explicitly allows remote references. + if config.AllowRemoteReferences { // create a remote filesystem remoteFS, _ := index.NewRemoteFSWithConfig(idxConfig) @@ -483,6 +484,11 @@ func extractPaths(ctx context.Context, root *yaml.Node, nodes documentTopLevelNo } func extractWebhooks(ctx context.Context, root *yaml.Node, nodes documentTopLevelNodes, doc *Document, idx *index.SpecIndex) error { + // without a genuine top-level key, ExtractMap can match a same-named scalar + // (e.g. "webhooks" in an extension value) and create an empty webhooks map. + if nodes.webhooks.value == nil { + return nil + } hooks, hooksL, hooksN, err := low.ExtractMap[*PathItem](ctx, WebhooksLabel, root, idx) if err != nil { return err diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/spec_info.go b/vendor/github.com/pb33f/libopenapi/datamodel/spec_info.go index f5200f042..ab947fd04 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/spec_info.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/spec_info.go @@ -9,7 +9,10 @@ import ( "errors" "fmt" "strings" + "sync" "time" + "unicode/utf16" + "unicode/utf8" "github.com/pb33f/libopenapi/utils" "go.yaml.in/yaml/v4" @@ -23,24 +26,105 @@ const ( // SpecInfo represents a 'ready-to-process' OpenAPI Document. The RootNode is the most important property // used by the library, this contains the top of the document tree that every single low model is based off. type SpecInfo struct { - SpecType string `json:"type"` - NumLines int `json:"numLines"` - Version string `json:"version"` - VersionNumeric float32 `json:"versionNumeric"` - SpecFormat string `json:"format"` - SpecFileType string `json:"fileType"` - SpecBytes *[]byte `json:"bytes"` // the original byte array - RootNode *yaml.Node `json:"-"` // reference to the root node of the spec. - SpecJSONBytes *[]byte `json:"-"` // original bytes converted to JSON - SpecJSON *map[string]interface{} `json:"-"` // standard JSON map of original bytes - Error error `json:"-"` // something go wrong? - APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3) - Generated time.Time `json:"-"` - OriginalIndentation int `json:"-"` // the original whitespace - Self string `json:"-"` // the $self field for OpenAPI 3.2+ documents (base URI) + SpecType string `json:"type"` + NumLines int `json:"numLines"` + Version string `json:"version"` + VersionNumeric float32 `json:"versionNumeric"` + SpecFormat string `json:"format"` + SpecFileType string `json:"fileType"` + SpecBytes *[]byte `json:"bytes"` // the original byte array + RootNode *yaml.Node `json:"-"` // reference to the root node of the spec. + + // SpecJSONBytes is the original document converted to JSON. It is populated lazily. + // + // Deprecated: read via GetSpecJSONBytes(), which builds the JSON representation on + // first use. This field is nil until GetSpecJSON or GetSpecJSONBytes is called. + // Concurrent readers must use the accessors: a direct field read racing the first + // accessor call is a data race. + SpecJSONBytes *[]byte `json:"-"` + + // SpecJSON is the original document as a standard JSON map. It is populated lazily. + // + // Deprecated: read via GetSpecJSON(), which builds the JSON representation on + // first use. This field is nil until GetSpecJSON or GetSpecJSONBytes is called. + // Concurrent readers must use the accessors: a direct field read racing the first + // accessor call is a data race. + SpecJSON *map[string]interface{} `json:"-"` + + Error error `json:"-"` // something go wrong? + APISchema string `json:"-"` // API Schema for supplied spec type (2 or 3) + Generated time.Time `json:"-"` + OriginalIndentation int `json:"-"` // the original whitespace + Self string `json:"-"` // the $self field for OpenAPI 3.2+ documents (base URI) + + jsonOnce sync.Once // guards the lazy JSON build + jsonErr error // error from the lazy JSON build, if any + skipJSONBuild bool // set by SkipJSONConversion: accessors return nil, preserving the flag's contract +} + +// GetSpecJSON returns the document as a standard JSON map, building it on first call. +// Returns nil if the document cannot be converted, or if the SpecInfo has been +// released (the node tree and original bytes are required to build the JSON view). +func (s *SpecInfo) GetSpecJSON() *map[string]interface{} { + s.jsonOnce.Do(s.buildJSON) + return s.SpecJSON +} + +// GetSpecJSONBytes returns the document converted to JSON bytes, building the +// representation on first call. Returns nil if the document cannot be converted, or +// if the SpecInfo has been released. +func (s *SpecInfo) GetSpecJSONBytes() *[]byte { + s.jsonOnce.Do(s.buildJSON) + return s.SpecJSONBytes +} + +// GetSpecJSONError returns the error from building the JSON view, building it first +// if needed. It distinguishes a failed conversion (non-nil error) from a released +// SpecInfo (nil error, nil JSON: there is nothing left to build, so nothing failed). +func (s *SpecInfo) GetSpecJSONError() error { + s.jsonOnce.Do(s.buildJSON) + return s.jsonErr +} + +// buildJSON populates SpecJSON and SpecJSONBytes from the parsed node tree (YAML) or +// the original bytes (JSON). This is the same conversion the library used to perform +// eagerly on every parse; it now runs only when the JSON view is requested. +func (s *SpecInfo) buildJSON() { + if s.skipJSONBuild { + return + } + var jsonSpec map[string]interface{} + if s.SpecFileType == YAMLFileType { + if s.RootNode == nil { + return + } + if err := s.RootNode.Decode(&jsonSpec); err != nil { + s.jsonErr = fmt.Errorf("failed to decode YAML to JSON: %w", err) + return + } + // Marshal to JSON - if this fails due to unsupported types (e.g. map[interface{}]interface{}), + // we tolerate it as it doesn't indicate spec invalidity, just YAML/JSON incompatibility + b, err := json.Marshal(&jsonSpec) + if err == nil { + s.SpecJSONBytes = &b + } + s.SpecJSON = &jsonSpec + return + } + if s.SpecBytes == nil { + return + } + if err := json.Unmarshal(*s.SpecBytes, &jsonSpec); err != nil { + s.jsonErr = fmt.Errorf("failed to unmarshal JSON: %w", err) + return + } + s.SpecJSONBytes = s.SpecBytes + s.SpecJSON = &jsonSpec } // Release nils fields that pin the YAML node tree and large byte arrays in memory. +// After release, GetSpecJSON and GetSpecJSONBytes return nil: the inputs needed to +// build the JSON view are gone and will not be resurrected. func (s *SpecInfo) Release() { if s == nil { return @@ -79,7 +163,7 @@ func ExtractSpecInfoWithDocumentCheck(spec []byte, bypass bool) (*SpecInfo, erro func extractSpecInfoInternal(spec []byte, bypass bool, skipJSON bool) (*SpecInfo, error) { var parsedSpec yaml.Node - specInfo := &SpecInfo{} + specInfo := &SpecInfo{skipJSONBuild: skipJSON} // set original bytes specInfo.SpecBytes = &spec @@ -97,12 +181,11 @@ func extractSpecInfoInternal(spec []byte, bypass bool, skipJSON bool) (*SpecInfo specInfo.NumLines = bytes.Count(spec, []byte{'\n'}) + 1 - // Pre-process JSON to handle \/ escape sequences that YAML parser doesn't recognize. - // JSON (RFC 8259) allows \/ as an optional escape for forward slash, but YAML does not. - // See: https://github.com/pb33f/libopenapi/issues/479 + // Pre-process JSON escapes that YAML parsers do not accept even though + // they are valid JSON, while preserving the existing YAML-node parse path. parseBytes := spec if specInfo.SpecFileType == JSONFileType { - parseBytes = unescapeJSONSlashes(spec) + parseBytes = normalizeJSONForYAMLParser(spec) } err := yaml.Unmarshal(parseBytes, &parsedSpec) @@ -131,28 +214,41 @@ func extractSpecInfoInternal(spec []byte, bypass bool, skipJSON bool) (*SpecInfo _, openAPI2 := utils.FindKeyNode(utils.OpenApi2, parsedSpec.Content) _, asyncAPI := utils.FindKeyNode(utils.AsyncApi, parsedSpec.Content) + // validate document structure without building the JSON view: the root must be a + // mapping, YAML mappings must not contain duplicate keys (matching the yaml + // decoder's checks), and JSON input must be syntactically valid. The full JSON + // conversion is built lazily by GetSpecJSON/GetSpecJSONBytes. parseJSON := func(bytes []byte, spec *SpecInfo, parsedNode *yaml.Node) error { - var jsonSpec map[string]interface{} if spec.SpecFileType == YAMLFileType { - // Decode YAML to map - this is critical to catch structural errors like duplicate keys - if err := parsedNode.Decode(&jsonSpec); err != nil { - return fmt.Errorf("failed to decode YAML to JSON: %w", err) + root := parsedNode + if root.Kind == yaml.DocumentNode && len(root.Content) > 0 { + root = root.Content[0] } - // Marshal to JSON - if this fails due to unsupported types (e.g. map[interface{}]interface{}), - // we tolerate it as it doesn't indicate spec invalidity, just YAML/JSON incompatibility - b, err := json.Marshal(&jsonSpec) - if err == nil { - spec.SpecJSONBytes = &b + if root.Kind != yaml.MappingNode && root.Tag != "!!null" { + // the document cannot construct into a map: run the decoder purely to + // surface its exact construct error (garbage input is the rare path). + var jsonSpec map[string]interface{} + if err := parsedNode.Decode(&jsonSpec); err != nil { + return fmt.Errorf("failed to decode YAML to JSON: %w", err) + } + if jsonSpec == nil { + return fmt.Errorf("failed to decode YAML to JSON: YAML document root is %v, not a mapping", root.Kind) + } } - spec.SpecJSON = &jsonSpec - } else { - if err := json.Unmarshal(bytes, &jsonSpec); err != nil { - return fmt.Errorf("failed to unmarshal JSON: %w", err) + if err := checkDuplicateMappingKeys(parsedNode); err != nil { + return fmt.Errorf("failed to decode YAML to JSON: %w", err) } - spec.SpecJSONBytes = &bytes - spec.SpecJSON = &jsonSpec + return nil } - return nil + if json.Valid(bytes) { + return nil + } + // invalid JSON is the rare path: run the decoder purely to surface the same + // detailed error message the eager conversion produced. json.Valid and + // json.Unmarshal share the same scanner, so the decode always errors here. + var jsonSpec map[string]interface{} + err := json.Unmarshal(bytes, &jsonSpec) + return fmt.Errorf("failed to unmarshal JSON: %w", err) } // if !bypass { @@ -294,10 +390,8 @@ func extractSpecInfoInternal(spec []byte, bypass bool, skipJSON bool) (*SpecInfo // parseJSON(spec, specInfo, &parsedSpec) //} - if !parsed && !skipJSON { - if err := parseJSON(spec, specInfo, &parsedSpec); err != nil && !bypass { - return nil, err - } + if !parsed && !skipJSON && bypass { + _ = parseJSON(spec, specInfo, &parsedSpec) } // detect the original whitespace indentation @@ -315,6 +409,58 @@ func ExtractSpecInfo(spec []byte) (*SpecInfo, error) { return ExtractSpecInfoWithDocumentCheck(spec, false) } +// checkDuplicateMappingKeys walks a parsed node tree and reports duplicate mapping +// keys using the exact equality semantics of the go.yaml.in/yaml/v4 decoder: two keys +// in the same mapping collide when their node Kind and raw Value match (no tag or +// alias resolution). The collected construct errors are normalized onto the +// public one-line error shape, and children of an offending mapping are not +// descended into, mirroring the decoder halting construction of that mapping. +// +// Known divergence: an anchored mapping with duplicate keys that is aliased +// elsewhere is reported ONCE here, while the decoder re-reports it on every +// construction visit (anchor + each alias). The decoder's repeat count is an +// artifact of construction order, not extra information, so the walker does +// not replicate it. TestCheckDuplicateMappingKeys_MatchesDecoder pins parity +// for everything else (and TestCheckDuplicateMappingKeys_AliasedAnchorDivergence +// pins this exception); revisit both when go.yaml.in/yaml/v4 leaves rc. +func checkDuplicateMappingKeys(node *yaml.Node) error { + var errs []string + walkDuplicateMappingKeys(node, &errs) + if len(errs) == 0 { + return nil + } + return errors.New("yaml: construct errors: " + strings.Join(errs, "; ")) +} + +func walkDuplicateMappingKeys(node *yaml.Node, errs *[]string) { + switch node.Kind { + case yaml.DocumentNode, yaml.SequenceNode: + for _, child := range node.Content { + walkDuplicateMappingKeys(child, errs) + } + case yaml.MappingNode: + l := len(node.Content) + found := false + for i := 0; i < l; i += 2 { + ni := node.Content[i] + for j := i + 2; j < l; j += 2 { + nj := node.Content[j] + if ni.Kind == nj.Kind && ni.Value == nj.Value { + *errs = append(*errs, + fmt.Sprintf("line %d: mapping key %#v already defined at line %d", nj.Line, nj.Value, ni.Line)) + found = true + } + } + } + if found { + return + } + for _, child := range node.Content { + walkDuplicateMappingKeys(child, errs) + } + } +} + // extract version number from specification func parseVersionTypeData(d interface{}) (string, int, error) { r := []rune(strings.TrimSpace(fmt.Sprintf("%v", d))) @@ -324,39 +470,126 @@ func parseVersionTypeData(d interface{}) (string, int, error) { return string(r), int(r[0]) - '0', nil } -// unescapeJSONSlashes replaces the optional \/ escape sequence in JSON with / -// JSON (RFC 8259) allows \/ as an optional escape for forward slash, but YAML -// parsers (including go.yaml.in/yaml/v4) do not recognize it. -// This handles escaped backslashes correctly: \\/ becomes \/ not // -// Returns the original slice if no transformation is needed (zero allocation). -func unescapeJSONSlashes(jsonBytes []byte) []byte { - // fast path: check if transformation is needed - if !bytes.Contains(jsonBytes, []byte(`\/`)) { +// normalizeJSONForYAMLParser rewrites the small set of JSON escapes accepted by +// RFC 8259 but rejected by go.yaml.in/yaml/v4. It returns the original slice +// without allocation unless a rewrite is required. +func normalizeJSONForYAMLParser(jsonBytes []byte) []byte { + if bytes.IndexByte(jsonBytes, '\\') < 0 { return jsonBytes } - result := make([]byte, 0, len(jsonBytes)) - i := 0 - for i < len(jsonBytes) { - if jsonBytes[i] == '\\' && i+1 < len(jsonBytes) { - switch jsonBytes[i+1] { - case '/': - // \/ -> / (json optional escape for solidus) - result = append(result, '/') - i += 2 - case '\\': - // preserve escaped backslash to prevent \\/ becoming // - result = append(result, '\\', '\\') - i += 2 - default: - // preserve other escape sequences (\n, \t, \", etc.) - result = append(result, jsonBytes[i]) - i++ - } - } else { - result = append(result, jsonBytes[i]) - i++ + var result []byte + var runeBytes [utf8.UTFMax]byte + last := 0 + scan := 0 + + for scan < len(jsonBytes) { + rel := bytes.IndexByte(jsonBytes[scan:], '\\') + if rel < 0 { + break + } + + escape := scan + rel + replacement, consumed, ok := jsonEscapeReplacement(jsonBytes, escape, &runeBytes) + if !ok { + scan = nextJSONEscapeScanOffset(jsonBytes, escape) + continue } + + if result == nil { + result = make([]byte, 0, len(jsonBytes)) + } + result = append(result, jsonBytes[last:escape]...) + result = append(result, replacement...) + scan = escape + consumed + last = scan + } + + if result == nil { + return jsonBytes } + + result = append(result, jsonBytes[last:]...) return result } + +func jsonEscapeReplacement(jsonBytes []byte, escape int, runeBytes *[utf8.UTFMax]byte) ([]byte, int, bool) { + if escape+1 >= len(jsonBytes) { + return nil, 0, false + } + + switch jsonBytes[escape+1] { + case '/': + runeBytes[0] = '/' + return runeBytes[:1], 2, true + case 'u': + if escape+12 > len(jsonBytes) { + return nil, 0, false + } + + high, ok := decodeJSONUnicodeEscape(jsonBytes[escape+2 : escape+6]) + if !ok || !isHighSurrogate(high) { + return nil, 0, false + } + + lowEscape := escape + 6 + if jsonBytes[lowEscape] != '\\' || jsonBytes[lowEscape+1] != 'u' { + return nil, 0, false + } + + low, ok := decodeJSONUnicodeEscape(jsonBytes[lowEscape+2 : lowEscape+6]) + if !ok || !isLowSurrogate(low) { + return nil, 0, false + } + + r := utf16.DecodeRune(rune(high), rune(low)) + n := utf8.EncodeRune(runeBytes[:], r) + return runeBytes[:n], 12, true + default: + return nil, 0, false + } +} + +func nextJSONEscapeScanOffset(jsonBytes []byte, escape int) int { + if escape+1 >= len(jsonBytes) { + return escape + 1 + } + return escape + 2 +} + +func decodeJSONUnicodeEscape(hexBytes []byte) (uint16, bool) { + if len(hexBytes) != 4 { + return 0, false + } + + var value uint16 + for _, b := range hexBytes { + hex, ok := jsonHexValue(b) + if !ok { + return 0, false + } + value = value<<4 | uint16(hex) + } + return value, true +} + +func jsonHexValue(b byte) (byte, bool) { + switch { + case b >= '0' && b <= '9': + return b - '0', true + case b >= 'a' && b <= 'f': + return b - 'a' + 10, true + case b >= 'A' && b <= 'F': + return b - 'A' + 10, true + default: + return 0, false + } +} + +func isHighSurrogate(value uint16) bool { + return value >= 0xD800 && value <= 0xDBFF +} + +func isLowSurrogate(value uint16) bool { + return value >= 0xDC00 && value <= 0xDFFF +} diff --git a/vendor/github.com/pb33f/libopenapi/datamodel/translate.go b/vendor/github.com/pb33f/libopenapi/datamodel/translate.go index a5245c0cb..7e8ce6446 100644 --- a/vendor/github.com/pb33f/libopenapi/datamodel/translate.go +++ b/vendor/github.com/pb33f/libopenapi/datamodel/translate.go @@ -38,6 +38,11 @@ type pipelineResult[OUT any] struct { err error } +// parallelTranslateThreshold is the collection size below which TranslateSliceParallel +// and TranslateMapParallel run sequentially: worker pool setup (goroutines, channels, +// pending map) costs more than translating a handful of items inline. +const parallelTranslateThreshold = 16 + // TranslateSliceParallel iterates a slice in parallel and calls translate() // asynchronously. // translate() may return `datamodel.Continue` to continue iteration. @@ -48,6 +53,33 @@ func TranslateSliceParallel[IN any, OUT any](in []IN, translate TranslateSliceFu return nil } + // small collections run inline: same observable semantics, none of the + // worker pool overhead. + if len(in) <= parallelTranslateThreshold { + for i := range in { + out, err := translate(i, in[i]) + if err == Continue { + continue + } + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if result == nil { + continue + } + if err = result(out); err != nil { + if err == io.EOF { + return nil + } + return err + } + } + return nil + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -149,14 +181,33 @@ func TranslateMapParallel[K comparable, V any, RV any](m *orderedmap.Map[K, V], return nil } - // Snapshot pairs for indexed access. + // small maps run inline: same observable semantics, none of the worker + // pool or pair snapshot overhead. + if m.Len() <= parallelTranslateThreshold { + for pair := orderedmap.First(m); pair != nil; pair = pair.Next() { + rv, err := translate(pair) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if err = result(rv); err != nil { + if err == io.EOF { + return nil + } + return err + } + } + return nil + } + + // Snapshot pairs for indexed access. The map is larger than the sequential + // threshold, so pairs is never empty here. pairs := make([]orderedmap.Pair[K, V], 0, m.Len()) for pair := orderedmap.First(m); pair != nil; pair = pair.Next() { pairs = append(pairs, pair) } - if len(pairs) == 0 { - return nil - } ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/vendor/github.com/pb33f/libopenapi/document.go b/vendor/github.com/pb33f/libopenapi/document.go index 65b571a77..9c9bf3e40 100644 --- a/vendor/github.com/pb33f/libopenapi/document.go +++ b/vendor/github.com/pb33f/libopenapi/document.go @@ -137,7 +137,7 @@ type DocumentModel[T v2high.Swagger | v3high.Document] struct { // This function will NOT automatically follow (meaning load) any file or remote references that are found. // // If this isn't the behavior you want, then you can use the NewDocumentWithConfiguration() function instead, which allows you to set a configuration that -// will allow you to control if file or remote references are allowed. In particular the `AllowFileReferences` and `FollowRemoteReferences` +// will allow you to control if file or remote references are allowed. In particular the `AllowFileReferences` and `AllowRemoteReferences` // properties. func NewDocument(specByteArray []byte) (Document, error) { return NewDocumentWithTypeCheck(specByteArray, false) diff --git a/vendor/github.com/pb33f/libopenapi/index/extract_refs.go b/vendor/github.com/pb33f/libopenapi/index/extract_refs.go index e0176ee41..da55e843d 100644 --- a/vendor/github.com/pb33f/libopenapi/index/extract_refs.go +++ b/vendor/github.com/pb33f/libopenapi/index/extract_refs.go @@ -20,7 +20,7 @@ func isSchemaContainingNode(v string) bool { func isMapOfSchemaContainingNode(v string) bool { switch v { - case "properties", "patternProperties": + case "properties", "patternProperties", "$defs": return true } return false diff --git a/vendor/github.com/pb33f/libopenapi/index/extract_refs_inline.go b/vendor/github.com/pb33f/libopenapi/index/extract_refs_inline.go index 3f80fe033..643c83739 100644 --- a/vendor/github.com/pb33f/libopenapi/index/extract_refs_inline.go +++ b/vendor/github.com/pb33f/libopenapi/index/extract_refs_inline.go @@ -11,6 +11,39 @@ import ( "go.yaml.in/yaml/v4" ) +// buildDefinitionPath assembles absPath + "#/" + seenPath + extra segments joined by +// '/' in a single allocation. The "#/..." definition is a zero-copy suffix slice of +// the result, so callers derive both strings from one build. +func buildDefinitionPath(absPath string, seenPath []string, extra ...string) string { + size := len(absPath) + 2 + for _, s := range seenPath { + size += len(s) + 1 + } + for _, s := range extra { + size += len(s) + 1 + } + var b strings.Builder + b.Grow(size) + b.WriteString(absPath) + b.WriteString("#/") + first := true + for _, s := range seenPath { + if !first { + b.WriteByte('/') + } + b.WriteString(s) + first = false + } + for _, s := range extra { + if !first { + b.WriteByte('/') + } + b.WriteString(s) + first = false + } + return b.String() +} + func (index *SpecIndex) collectInlineSchemaDefinition(parent, node *yaml.Node, seenPath []string, keyIndex int) { if keyIndex+1 >= len(node.Content) { return @@ -21,11 +54,11 @@ func (index *SpecIndex) collectInlineSchemaDefinition(parent, node *yaml.Node, s var jsonPath, definitionPath, fullDefinitionPath string if len(seenPath) > 0 || keyNode.Value != "" { - loc := append(seenPath, keyNode.Value) - locPath := strings.Join(loc, "/") - definitionPath = "#/" + locPath - fullDefinitionPath = index.specAbsolutePath + "#/" + locPath - _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) + fullDefinitionPath = buildDefinitionPath(index.specAbsolutePath, seenPath, keyNode.Value) + definitionPath = fullDefinitionPath[len(index.specAbsolutePath):] + if !index.skipMetadataCollection() { + _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) + } } ref := &Reference{ @@ -68,6 +101,7 @@ func (index *SpecIndex) collectMapSchemaDefinitions(parent, node *yaml.Node, see } label := "" + prefix := "" for h, prop := range propertiesNode.Content { if h%2 == 0 { label = prop.Value @@ -76,11 +110,14 @@ func (index *SpecIndex) collectMapSchemaDefinitions(parent, node *yaml.Node, see var jsonPath, definitionPath, fullDefinitionPath string if len(seenPath) > 0 || keyNode.Value != "" && label != "" { - loc := append(seenPath, keyNode.Value, label) - locPath := strings.Join(loc, "/") - definitionPath = "#/" + locPath - fullDefinitionPath = index.specAbsolutePath + "#/" + locPath - _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) + if prefix == "" { + prefix = buildDefinitionPath(index.specAbsolutePath, seenPath, keyNode.Value) + } + fullDefinitionPath = prefix + "/" + label + definitionPath = fullDefinitionPath[len(index.specAbsolutePath):] + if !index.skipMetadataCollection() { + _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) + } } ref := &Reference{ @@ -111,17 +148,20 @@ func (index *SpecIndex) collectArraySchemaDefinitions(parent, node *yaml.Node, s keyNode := node.Content[keyIndex] arrayNode := node.Content[keyIndex+1] + prefix := "" for h, element := range arrayNode.Content { var jsonPath, definitionPath, fullDefinitionPath string if len(seenPath) > 0 { - loc := append(seenPath, keyNode.Value, strconv.Itoa(h)) - locPath := strings.Join(loc, "/") - definitionPath = "#/" + locPath - fullDefinitionPath = index.specAbsolutePath + "#/" + locPath - _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) + if prefix == "" { + prefix = buildDefinitionPath(index.specAbsolutePath, seenPath, keyNode.Value) + } + fullDefinitionPath = prefix + "/" + strconv.Itoa(h) + definitionPath = fullDefinitionPath[len(index.specAbsolutePath):] } else { definitionPath = "#/" + keyNode.Value fullDefinitionPath = index.specAbsolutePath + "#/" + keyNode.Value + } + if !index.skipMetadataCollection() { _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) } diff --git a/vendor/github.com/pb33f/libopenapi/index/extract_refs_lookup.go b/vendor/github.com/pb33f/libopenapi/index/extract_refs_lookup.go index 4062daca8..288ef7682 100644 --- a/vendor/github.com/pb33f/libopenapi/index/extract_refs_lookup.go +++ b/vendor/github.com/pb33f/libopenapi/index/extract_refs_lookup.go @@ -197,8 +197,9 @@ func (index *SpecIndex) ExtractComponentsFromRefs(ctx context.Context, refs []*R } func (index *SpecIndex) locateRef(ctx context.Context, ref *Reference) *Reference { - uri := strings.Split(ref.FullDefinition, "#/") - isExternalRef := len(uri) == 2 && len(uri[0]) > 0 + // match strings.Split len==2 semantics: exactly one "#/" with a non-empty file part. + refFile, refFragment, refCut := strings.Cut(ref.FullDefinition, "#/") + isExternalRef := refCut && refFile != "" && !strings.Contains(refFragment, "#/") if isExternalRef { index.refLock.Lock() } @@ -221,11 +222,11 @@ func (index *SpecIndex) locateRef(ctx context.Context, ref *Reference) *Referenc } if located.Node != nil { + index.awaitNodeMap() index.nodeMapLock.RLock() - if located.Node.Line > 1 && len(index.nodeMap[located.Node.Line-1]) > 0 { - for _, v := range index.nodeMap[located.Node.Line-1] { - located.KeyNode = v - break + if prevLine := located.Node.Line - 1; prevLine > 0 && prevLine < len(index.nodeLines) { + if entries := index.nodeLines[prevLine]; len(entries) > 0 { + located.KeyNode = entries[0].node } } index.nodeMapLock.RUnlock() diff --git a/vendor/github.com/pb33f/libopenapi/index/extract_refs_metadata.go b/vendor/github.com/pb33f/libopenapi/index/extract_refs_metadata.go index ce9b82f78..085237792 100644 --- a/vendor/github.com/pb33f/libopenapi/index/extract_refs_metadata.go +++ b/vendor/github.com/pb33f/libopenapi/index/extract_refs_metadata.go @@ -16,6 +16,13 @@ type metadataPathAction struct { stop bool } +// skipMetadataCollection reports whether diagnostic metadata collection is disabled. +// Only collection is skipped: the path actions extractNodeMetadata returns drive +// seenPath handling for every downstream ref, so that logic always runs. +func (index *SpecIndex) skipMetadataCollection() bool { + return index.config != nil && index.config.SkipMetadataCollection +} + func (index *SpecIndex) extractNodeMetadata(node, parent *yaml.Node, seenPath []string, keyIndex int) metadataPathAction { keyNode := node.Content[keyIndex] if keyNode == nil || keyNode.Value == "" || keyNode.Value == "$ref" || keyNode.Value == "$id" { @@ -31,8 +38,7 @@ func (index *SpecIndex) extractNodeMetadata(node, parent *yaml.Node, seenPath [] var jsonPathComputed bool computeJSONPath := func() string { if !jsonPathComputed { - loc := append(seenPath, segment) - definitionPath := "#/" + strings.Join(loc, "/") + definitionPath := buildDefinitionPath("", seenPath, segment) _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) jsonPathComputed = true } @@ -47,7 +53,7 @@ func (index *SpecIndex) extractNodeMetadata(node, parent *yaml.Node, seenPath [] if isMetadataPropertyNamePath(seenPath) { return metadataPathAction{appendSegment: true, stop: true} } - if !metadataPathContainsExamples(seenPath) { + if !metadataPathContainsExamples(seenPath) && !index.skipMetadataCollection() { refNode := metadataValueNode(node, keyIndex) ref := &DescriptionReference{ ParentNode: parent, @@ -69,25 +75,33 @@ func (index *SpecIndex) extractNodeMetadata(node, parent *yaml.Node, seenPath [] if metadataPathContainsExamples(seenPath) { return metadataPathAction{stop: true} } - refNode := metadataValueNode(node, keyIndex) - index.allSummaries = append(index.allSummaries, &DescriptionReference{ - ParentNode: parent, - Content: refNode.Value, - Path: computeJSONPath(), - Node: refNode, - KeyNode: keyNode, - IsSummary: true, - }) - index.summaryCount++ + if !index.skipMetadataCollection() { + refNode := metadataValueNode(node, keyIndex) + index.allSummaries = append(index.allSummaries, &DescriptionReference{ + ParentNode: parent, + Content: refNode.Value, + Path: computeJSONPath(), + Node: refNode, + KeyNode: keyNode, + IsSummary: true, + }) + index.summaryCount++ + } case "security": - index.collectSecurityRequirementMetadata(node, keyIndex, computeJSONPath()) + if !index.skipMetadataCollection() { + index.collectSecurityRequirementMetadata(node, keyIndex, computeJSONPath()) + } case "enum": if len(seenPath) > 0 && seenPath[len(seenPath)-1] == "properties" { return metadataPathAction{appendSegment: true, stop: true} } - index.collectEnumMetadata(node, parent, keyIndex, computeJSONPath()) + if !index.skipMetadataCollection() { + index.collectEnumMetadata(node, parent, keyIndex, computeJSONPath()) + } case "properties": - index.collectObjectWithPropertiesMetadata(node, parent, keyNode, computeJSONPath()) + if !index.skipMetadataCollection() { + index.collectObjectWithPropertiesMetadata(node, parent, keyNode, computeJSONPath()) + } } return metadataPathAction{appendSegment: true} diff --git a/vendor/github.com/pb33f/libopenapi/index/extract_refs_ref.go b/vendor/github.com/pb33f/libopenapi/index/extract_refs_ref.go index 850c120e4..2aeef7f0d 100644 --- a/vendor/github.com/pb33f/libopenapi/index/extract_refs_ref.go +++ b/vendor/github.com/pb33f/libopenapi/index/extract_refs_ref.go @@ -80,6 +80,7 @@ func (index *SpecIndex) extractReferenceAt( Node: node, KeyNode: node.Content[keyIndex+1], Path: path, + SourcePath: append([]string(nil), seenPath...), Index: index, IsExtensionRef: isExtensionPath, HasSiblingProperties: len(siblingProps) > 0, @@ -334,6 +335,7 @@ func (index *SpecIndex) storeReferenceWithSiblings( Node: &copiedNode, KeyNode: node.Content[keyIndex], Path: path, + SourcePath: append([]string(nil), ref.SourcePath...), Index: index, IsExtensionRef: isExtensionPath, HasSiblingProperties: len(siblingProps) > 0, diff --git a/vendor/github.com/pb33f/libopenapi/index/find_component_build.go b/vendor/github.com/pb33f/libopenapi/index/find_component_build.go index 5c027c28f..7c537d2a1 100644 --- a/vendor/github.com/pb33f/libopenapi/index/find_component_build.go +++ b/vendor/github.com/pb33f/libopenapi/index/find_component_build.go @@ -47,6 +47,7 @@ func buildResolvedComponentReference( HasSiblingProperties: source.HasSiblingProperties, In: source.In, } + ref.SourcePath = append([]string(nil), source.SourcePath...) ref.ParentNodeTypes = append([]string(nil), source.ParentNodeTypes...) ref.SiblingKeys = append([]*yaml.Node(nil), source.SiblingKeys...) ref.SiblingProperties = cloneSiblingProperties(source.SiblingProperties) diff --git a/vendor/github.com/pb33f/libopenapi/index/index_model.go b/vendor/github.com/pb33f/libopenapi/index/index_model.go index 46a5c386f..5e159d31f 100644 --- a/vendor/github.com/pb33f/libopenapi/index/index_model.go +++ b/vendor/github.com/pb33f/libopenapi/index/index_model.go @@ -40,6 +40,7 @@ type Reference struct { Index *SpecIndex `json:"-"` // index that contains this reference. RemoteLocation string `json:"remoteLocation,omitempty"` Path string `json:"path,omitempty"` // this won't always be available. + SourcePath []string `json:"-"` // OpenAPI path to the source $ref location. RequiredRefProperties map[string][]string `json:"requiredProperties,omitempty"` // definition names (eg, #/definitions/One) to a list of required properties on this definition which reference that definition HasSiblingProperties bool `json:"-"` // indicates if ref has sibling properties SiblingProperties map[string]*yaml.Node `json:"-"` // stores sibling property nodes @@ -226,6 +227,20 @@ type SpecIndexConfig struct { // PropertyMergeStrategy defines how to handle conflicts when merging properties. PropertyMergeStrategy datamodel.PropertyMergeStrategy + // SkipMetadataCollection disables the collection of diagnostic metadata during indexing: + // descriptions, summaries, enums, objects-with-properties, security requirement + // references, and the JSONPath `Path` values on inline schema references. Skipping + // them significantly reduces allocations and retained memory when parsing large + // documents. Reference extraction and resolution are unaffected. + // + // -- UNSAFE FOR DIAGNOSTIC, RULE, OR PATH CONSUMERS -- + // When enabled, GetAllDescriptions, GetAllSummaries, GetAllEnums, + // GetAllObjectsWithProperties, GetSecurityRequirementReferences and the related + // counts are intentionally empty/zero, and inline schema Reference.Path values are + // empty strings. vacuum and any other tool that consumes index metadata or Path + // values must NOT enable this. Defaults to false (everything is collected). + SkipMetadataCollection bool + // private fields uri []string id string @@ -278,6 +293,7 @@ func (s *SpecIndexConfig) ToDocumentConfiguration() *datamodel.DocumentConfigura ResolveNestedRefsWithDocumentContext: s.ResolveNestedRefsWithDocumentContext, PropertyMergeStrategy: strategy, SkipExternalRefResolution: s.SkipExternalRefResolution, + SkipMetadataCollection: s.SkipMetadataCollection, Logger: s.Logger, } } @@ -415,7 +431,8 @@ type SpecIndex struct { built bool uri []string logger *slog.Logger - nodeMap map[int]map[int]*yaml.Node + nodeLines [][]nodeLineEntry + legacyNodeMap map[int]map[int]*yaml.Node // materialized on demand by GetNodeMap only nodeMapCompleted chan struct{} pendingResolve []refMap highModelCache Cache @@ -443,8 +460,30 @@ func (index *SpecIndex) GetConfig() *SpecIndexConfig { } // GetNodeMap returns the line-to-column-to-node map built during indexing. +// The map is materialized from the internal line index on first call and cached. +// +// Deprecated: use GetNode for single lookups; this method exists for API +// compatibility and allocates a full legacy map on first use. func (index *SpecIndex) GetNodeMap() map[int]map[int]*yaml.Node { - return index.nodeMap + index.awaitNodeMap() + index.nodeMapLock.Lock() + defer index.nodeMapLock.Unlock() + if index.legacyNodeMap != nil || index.nodeLines == nil { + return index.legacyNodeMap + } + legacy := make(map[int]map[int]*yaml.Node) + for line, entries := range index.nodeLines { + if len(entries) == 0 { + continue + } + cols := make(map[int]*yaml.Node, len(entries)) + for _, e := range entries { + cols[int(e.column)] = e.node + } + legacy[line] = cols + } + index.legacyNodeMap = legacy + return legacy } // GetCache returns the reference lookup cache used during resolution. @@ -542,7 +581,12 @@ func (index *SpecIndex) releaseComponentIndexes() { } func (index *SpecIndex) releaseDerivedState() { - index.nodeMap = nil + // node-map state is read concurrently via awaitNodeMap/GetNode; nil it + // under the same lock those readers use. + index.nodeMapLock.Lock() + index.nodeLines = nil + index.legacyNodeMap = nil + index.nodeMapLock.Unlock() index.allDescriptions = nil index.allSummaries = nil index.allEnums = nil @@ -602,7 +646,9 @@ func (index *SpecIndex) resetRuntimeState() { index.built = false index.componentIndexChan = nil index.polyComponentIndexChan = nil - index.nodeMapCompleted = nil + // nodeMapCompleted is deliberately NOT nilled: it is closed (retaining + // nothing) and awaitNodeMap reads the field without a lock on the GetNode + // hot path - writing nil here would race every reader for zero benefit. } // SetAbsolutePath sets the absolute path to the spec file for the index. Will be absolute, either as a http link or a file. diff --git a/vendor/github.com/pb33f/libopenapi/index/map_index_nodes.go b/vendor/github.com/pb33f/libopenapi/index/map_index_nodes.go index 4f8ecde50..84cb1f101 100644 --- a/vendor/github.com/pb33f/libopenapi/index/map_index_nodes.go +++ b/vendor/github.com/pb33f/libopenapi/index/map_index_nodes.go @@ -41,44 +41,96 @@ type NodeOrigin struct { Index *SpecIndex `json:"-" yaml:"-"` } +// nodeLineEntry is a single (column, node) pair on one line of the spec. Lines hold very +// few nodes, so a small slice scanned linearly is far cheaper than a per-line map. +type nodeLineEntry struct { + column int32 + node *yaml.Node +} + // GetNode returns a node from the spec based on a line and column. The second return var bool is true -// if the node was found, false if not. +// if the node was found, false if not. Blocks until the node line index has been fully built. func (index *SpecIndex) GetNode(line int, column int) (*yaml.Node, bool) { + index.awaitNodeMap() index.nodeMapLock.RLock() defer index.nodeMapLock.RUnlock() - if index.nodeMap[line] == nil { - return nil, false - } - node := index.nodeMap[line][column] + node := lookupNodeLines(index.nodeLines, line, column) return node, node != nil } -// MapNodes maps all nodes in the document to a map of line/column to node. -// Writes directly to index.nodeMap with lock protection (concurrent reads -// may happen from ExtractRefs running in parallel). +// awaitNodeMap blocks until MapNodes has published the node line index. It is a no-op +// once the index has been built or released (the completion channel is close-only). +func (index *SpecIndex) awaitNodeMap() { + if ch := index.nodeMapCompleted; ch != nil { + <-ch + } +} + +// lookupNodeLines returns the node stored at line/column, or nil if absent. +func lookupNodeLines(lines [][]nodeLineEntry, line, column int) *yaml.Node { + if line < 0 || line >= len(lines) { + return nil + } + for _, e := range lines[line] { + if int(e.column) == column { + return e.node + } + } + return nil +} + +// MapNodes maps all nodes in the document by line and column. The index is built into a +// local structure without locking, published under a single lock, and completion is +// signalled by closing nodeMapCompleted (close-only: supports any number of waiters). func (index *SpecIndex) MapNodes(rootNode *yaml.Node) { - mapNodesRecursive(rootNode, index, true) - index.nodeMapCompleted <- struct{}{} + sizeHint := 0 + if index.config != nil && index.config.SpecInfo != nil { + sizeHint = index.config.SpecInfo.NumLines + } + // lines are 1-based; +1 so line NumLines is directly addressable. + lines := make([][]nodeLineEntry, sizeHint+1) + lines = mapNodesRecursive(rootNode, lines) + index.nodeMapLock.Lock() + index.nodeLines = lines + index.nodeMapLock.Unlock() close(index.nodeMapCompleted) } -func mapNodesRecursive(node *yaml.Node, index *SpecIndex, root bool) { +func mapNodesRecursive(node *yaml.Node, lines [][]nodeLineEntry) [][]nodeLineEntry { if node.Kind == yaml.DocumentNode { node = node.Content[0] } for _, child := range node.Content { - index.nodeMapLock.Lock() - if index.nodeMap[child.Line] == nil { - index.nodeMap[child.Line] = make(map[int]*yaml.Node) + lines = addNodeLineEntry(lines, child) + lines = mapNodesRecursive(child, lines) + } + return addNodeLineEntry(lines, node) +} + +// addNodeLineEntry records node at its line/column, preserving the previous map +// semantics: a later write to the same line/column replaces the earlier one +// (parents are written after their children, so parents win collisions). +func addNodeLineEntry(lines [][]nodeLineEntry, node *yaml.Node) [][]nodeLineEntry { + line := node.Line + if line < 0 { + return lines + } + if line >= len(lines) { + grown := len(lines) * 2 + if grown <= line { + grown = line + 1 } - index.nodeMap[child.Line][child.Column] = child - index.nodeMapLock.Unlock() - mapNodesRecursive(child, index, false) + expanded := make([][]nodeLineEntry, grown) + copy(expanded, lines) + lines = expanded } - index.nodeMapLock.Lock() - if index.nodeMap[node.Line] == nil { - index.nodeMap[node.Line] = make(map[int]*yaml.Node) + entries := lines[line] + for i := range entries { + if int(entries[i].column) == node.Column { + entries[i].node = node + return lines + } } - index.nodeMap[node.Line][node.Column] = node - index.nodeMapLock.Unlock() + lines[line] = append(entries, nodeLineEntry{column: int32(node.Column), node: node}) + return lines } diff --git a/vendor/github.com/pb33f/libopenapi/index/resolver_relatives.go b/vendor/github.com/pb33f/libopenapi/index/resolver_relatives.go index ca33a60d9..4fee92f3a 100644 --- a/vendor/github.com/pb33f/libopenapi/index/resolver_relatives.go +++ b/vendor/github.com/pb33f/libopenapi/index/resolver_relatives.go @@ -144,6 +144,7 @@ func (resolver *Resolver) extractRelativeReference( RemoteLocation: ref.RemoteLocation, IsRemote: true, Index: ref.Index, + SourcePath: append([]string(nil), ref.SourcePath...), } locatedRef, _, _ := resolver.searchReferenceWithContext(ref, searchRef) diff --git a/vendor/github.com/pb33f/libopenapi/index/search_index.go b/vendor/github.com/pb33f/libopenapi/index/search_index.go index 9e0c92d17..e03171163 100644 --- a/vendor/github.com/pb33f/libopenapi/index/search_index.go +++ b/vendor/github.com/pb33f/libopenapi/index/search_index.go @@ -171,47 +171,49 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex absPath = index.config.BasePath } var roloLookup string - uri := strings.Split(ref, "#/") - if len(uri) == 2 { - if uri[0] != "" { - if strings.HasPrefix(uri[0], "http") { + uriFile, uriFragment, uriCut := strings.Cut(ref, "#/") + // match strings.Split(ref, "#/") len==2 semantics: exactly one separator. + singleFragment := uriCut && !strings.Contains(uriFragment, "#/") + if singleFragment { + if uriFile != "" { + if strings.HasPrefix(uriFile, "http") { roloLookup = searchRef.FullDefinition } else { - if filepath.IsAbs(uri[0]) { - roloLookup = uri[0] + if filepath.IsAbs(uriFile) { + roloLookup = uriFile } else { if filepath.Ext(absPath) != "" { absPath = filepath.Dir(absPath) } - roloLookup = index.resolveRelativeFilePath(absPath, uri[0]) + roloLookup = index.resolveRelativeFilePath(absPath, uriFile) } } } else { - if filepath.Ext(uri[1]) != "" { + if filepath.Ext(uriFragment) != "" { roloLookup = absPath } else { roloLookup = "" } - ref = fmt.Sprintf("#/%s", uri[1]) - refAlt = fmt.Sprintf("%s#/%s", absPath, uri[1]) + ref = fmt.Sprintf("#/%s", uriFragment) + refAlt = fmt.Sprintf("%s#/%s", absPath, uriFragment) } } else { - if filepath.IsAbs(uri[0]) { - roloLookup = uri[0] + if filepath.IsAbs(uriFile) { + roloLookup = uriFile } else { - if strings.HasPrefix(uri[0], "http") { + if strings.HasPrefix(uriFile, "http") { roloLookup = ref } else { if filepath.Ext(absPath) != "" { absPath = filepath.Dir(absPath) } - roloLookup = index.resolveRelativeFilePath(absPath, uri[0]) + roloLookup = index.resolveRelativeFilePath(absPath, uriFile) } } - ref = uri[0] + ref = uriFile } if strings.Contains(ref, "%") { // decode the url. @@ -258,9 +260,7 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex // component-tree walking inside the remote file. if roloLookup != "" { - if strings.Contains(roloLookup, "#") { - roloLookup = strings.Split(roloLookup, "#")[0] - } + roloLookup, _, _ = strings.Cut(roloLookup, "#") b := filepath.Base(roloLookup) sfn := index.GetSpecFileName() @@ -270,8 +270,8 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex if b == sfn && roloLookup == abp { // if the reference is the same as the spec file name, we should look through the index for the component var r *Reference - if len(uri) == 2 { - r = index.FindComponentInRoot(ctx, fmt.Sprintf("#/%s", uri[1])) + if singleFragment { + r = index.FindComponentInRoot(ctx, fmt.Sprintf("#/%s", uriFragment)) } return r, index, ctx } @@ -352,12 +352,12 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex node, _ := rFile.GetContentAsYAMLNode() if node != nil { var found *Reference - exp := strings.Split(ref, "#/") + expFile, expFragment, expCut := strings.Cut(ref, "#/") compId := ref - if len(exp) == 2 { - compId = fmt.Sprintf("#/%s", exp[1]) - found = FindComponent(ctx, node, compId, exp[0], idx) + if expCut && !strings.Contains(expFragment, "#/") { + compId = fmt.Sprintf("#/%s", expFragment) + found = FindComponent(ctx, node, compId, expFile, idx) } if found == nil { found = idx.FindComponent(ctx, ref) diff --git a/vendor/github.com/pb33f/libopenapi/index/search_rolodex.go b/vendor/github.com/pb33f/libopenapi/index/search_rolodex.go index 6eb3f4bb4..d61c6c50f 100644 --- a/vendor/github.com/pb33f/libopenapi/index/search_rolodex.go +++ b/vendor/github.com/pb33f/libopenapi/index/search_rolodex.go @@ -97,51 +97,49 @@ func (r *Rolodex) FindNodeOrigin(node *yaml.Node) *NodeOrigin { // is returned, otherwise nil is returned. func (index *SpecIndex) FindNodeOrigin(node *yaml.Node) *NodeOrigin { if node != nil { + index.awaitNodeMap() index.nodeMapLock.RLock() - if index.nodeMap[node.Line] != nil { - if index.nodeMap[node.Line][node.Column] != nil { - foundNode := index.nodeMap[node.Line][node.Column] - match := false + if foundNode := lookupNodeLines(index.nodeLines, node.Line, node.Column); foundNode != nil { + match := false - if foundNode == node { - match = true + if foundNode == node { + match = true + } + + // if the found node is a map. iterate through the content until we locate the node at that position + if !match && (utils.IsNodeMap(foundNode) || + utils.IsNodeArray(foundNode)) && (utils.IsNodeMap(node) || utils.IsNodeArray(node)) { + if len(node.Content) == len(foundNode.Content) { + // hash node and found node + match = checkHash(node, foundNode) } + } else { + if !match { + // hash node and found node + match = checkHash(node, foundNode) - // if the found node is a map. iterate through the content until we locate the node at that position - if !match && (utils.IsNodeMap(foundNode) || - utils.IsNodeArray(foundNode)) && (utils.IsNodeMap(node) || utils.IsNodeArray(node)) { - if len(node.Content) == len(foundNode.Content) { - // hash node and found node - match = checkHash(node, foundNode) - } - } else { if !match { - // hash node and found node - match = checkHash(node, foundNode) - - if !match { - // check if the found node is a map and if the first item in the map - // has the same line and column, as well as the same value - if utils.IsNodeMap(foundNode) && len(foundNode.Content) > 0 { - if foundNode.Content[0].Line == node.Line && - foundNode.Content[0].Column == node.Column && - foundNode.Content[0].Value == node.Value { - match = true - } + // check if the found node is a map and if the first item in the map + // has the same line and column, as well as the same value + if utils.IsNodeMap(foundNode) && len(foundNode.Content) > 0 { + if foundNode.Content[0].Line == node.Line && + foundNode.Content[0].Column == node.Column && + foundNode.Content[0].Value == node.Value { + match = true } } } } + } - if match { - index.nodeMapLock.RUnlock() - return &NodeOrigin{ - Node: foundNode, - Line: node.Line, - Column: node.Column, - AbsoluteLocation: index.specAbsolutePath, - Index: index, - } + if match { + index.nodeMapLock.RUnlock() + return &NodeOrigin{ + Node: foundNode, + Line: node.Line, + Column: node.Column, + AbsoluteLocation: index.specAbsolutePath, + Index: index, } } } diff --git a/vendor/github.com/pb33f/libopenapi/index/spec_index_build.go b/vendor/github.com/pb33f/libopenapi/index/spec_index_build.go index 9df192ecd..544696d41 100644 --- a/vendor/github.com/pb33f/libopenapi/index/spec_index_build.go +++ b/vendor/github.com/pb33f/libopenapi/index/spec_index_build.go @@ -62,7 +62,6 @@ func createNewIndex(ctx context.Context, rootNode *yaml.Node, index *SpecIndex, return index } index.nodeMapCompleted = make(chan struct{}) - index.nodeMap = make(map[int]map[int]*yaml.Node) go index.MapNodes(rootNode) index.cache = new(sync.Map) diff --git a/vendor/github.com/pb33f/libopenapi/index/utility_methods.go b/vendor/github.com/pb33f/libopenapi/index/utility_methods.go index 1e8fc191f..d201dcac5 100644 --- a/vendor/github.com/pb33f/libopenapi/index/utility_methods.go +++ b/vendor/github.com/pb33f/libopenapi/index/utility_methods.go @@ -555,16 +555,7 @@ func findIndex(index *SpecIndex, i *yaml.Node) *SpecIndex { } allIndexes := rolodex.GetIndexes() for _, searchIndex := range allIndexes { - nodeMap := searchIndex.GetNodeMap() - line, ok := nodeMap[i.Line] - if !ok { - continue - } - node, ok := line[i.Column] - if !ok { - continue - } - if node == i { + if node, ok := searchIndex.GetNode(i.Line, i.Column); ok && node == i { return searchIndex } } diff --git a/vendor/github.com/pb33f/libopenapi/overlay.go b/vendor/github.com/pb33f/libopenapi/overlay.go index 2f475f0bd..ed37aff74 100644 --- a/vendor/github.com/pb33f/libopenapi/overlay.go +++ b/vendor/github.com/pb33f/libopenapi/overlay.go @@ -4,6 +4,7 @@ package libopenapi import ( + "bytes" gocontext "context" "github.com/pb33f/libopenapi/datamodel" @@ -32,6 +33,10 @@ type OverlayResult struct { // NewOverlayDocument creates a new overlay document from the provided bytes. // The overlay document can then be applied to a target OpenAPI document using ApplyOverlay. func NewOverlayDocument(overlayBytes []byte) (*highoverlay.Overlay, error) { + if len(bytes.TrimSpace(overlayBytes)) == 0 { + return nil, overlay.ErrInvalidOverlay + } + var node yaml.Node if err := yaml.Unmarshal(overlayBytes, &node); err != nil { return nil, err diff --git a/vendor/github.com/pb33f/libopenapi/utils/utils.go b/vendor/github.com/pb33f/libopenapi/utils/utils.go index a7eb2ee8b..f903ce9a4 100644 --- a/vendor/github.com/pb33f/libopenapi/utils/utils.go +++ b/vendor/github.com/pb33f/libopenapi/utils/utils.go @@ -521,10 +521,10 @@ func FindKeyNode(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode } return NodeAlias(v), NodeAlias(nodes[i+1]) // next node is what we need. } - for x, j := range mergedNodeContent(v) { + content := mergedNodeContent(v) + for x, j := range content { if key == j.Value { if IsNodeMap(v) { - content := mergedNodeContent(v) if x+1 == len(content) { return NodeAlias(v), NodeAlias(content[x]) } @@ -532,7 +532,7 @@ func FindKeyNode(key string, nodes []*yaml.Node) (keyNode *yaml.Node, valueNode } if IsNodeArray(v) { - return NodeAlias(v), NodeAlias(mergedNodeContent(v)[x]) + return NodeAlias(v), NodeAlias(content[x]) } } } @@ -960,43 +960,6 @@ func isPathChar(s string) bool { return true } -func appendSegment(sb *strings.Builder, segs []string, cleaned []string, i int, wrapInQuotes bool) { - sb.Reset() - if wrapInQuotes { - sb.WriteString("['") - sb.WriteString(segs[i]) - sb.WriteString("']") - } else { - sb.WriteString("[") - sb.WriteString(segs[i]) - sb.WriteString("]") - } - c := sb.String() - sb.Reset() - sb.WriteString(cleaned[len(cleaned)-1]) - sb.WriteString(c) - cleaned[len(cleaned)-1] = sb.String() -} - -// appendSegmentOptimized uses strings.Builder more efficiently to avoid allocations -func appendSegmentOptimized(segs []string, cleaned []string, i int, wrapInQuotes bool) { - var builder strings.Builder - if wrapInQuotes { - builder.Grow(len(cleaned[len(cleaned)-1]) + len(segs[i]) + 4) // existing + [''] + segment - builder.WriteString(cleaned[len(cleaned)-1]) - builder.WriteString("['") - builder.WriteString(segs[i]) - builder.WriteString("']") - } else { - builder.Grow(len(cleaned[len(cleaned)-1]) + len(segs[i]) + 2) // existing + [] + segment - builder.WriteString(cleaned[len(cleaned)-1]) - builder.WriteByte('[') - builder.WriteString(segs[i]) - builder.WriteByte(']') - } - cleaned[len(cleaned)-1] = builder.String() -} - // parseSmallUint returns the unsigned integer value and true if s is a string of // digits representing a non-negative integer. Returns 0, false otherwise. func parseSmallUint(s string) (int, bool) { @@ -1016,15 +979,16 @@ func parseSmallUint(s string) (int, bool) { // ConvertComponentIdIntoFriendlyPathSearch will convert a JSON Path into a friendly path search string. // the friendliness comes from it being suitable for use with any JSON Path parser. // -// This function was re-written in v0.18.0 in order to fix a number of performance issues with the original -// implementation. Allocations were high and this function is used a lot, this new implementation is much -// lighter on string allocations by using a string builder. +// Rewritten as a single pass over the input with one output builder: no segment slice, +// no per-segment builders. Output is byte-identical to the previous implementation and +// is pinned by the golden corpus in testdata/component_id_golden.txt. func ConvertComponentIdIntoFriendlyPathSearch(id string) (string, string) { if id == "" || id == "#/" { return "", "$." } - segs := strings.Split(id, "/") - lastSeg := segs[len(segs)-1] + + // the name is the raw final segment, JSON-Pointer unescaped and URL decoded. + lastSeg := id[strings.LastIndexByte(id, '/')+1:] if strings.Contains(lastSeg, "~1") { lastSeg = strings.ReplaceAll(lastSeg, "~1", "/") } @@ -1033,151 +997,125 @@ func ConvertComponentIdIntoFriendlyPathSearch(id string) (string, string) { } name := lastSeg - // Pre-allocate with estimated capacity - estimatedCap := len(segs) + (len(segs) / 2) - cleaned := make([]string, 0, estimatedCap) + idContainsHash := strings.Contains(id, "#") + lastIndex := strings.Count(id, "/") // absolute index of the final segment - // check for strange spaces, chars and if found, wrap them up, clean them and create a new cleaned path. - for i := range segs { - if segs[i] == "" { + // note: we do NOT replace # with $ here. the leading # from JSON Pointer notation + // (e.g., "#/components/...") is dropped by the leading-segment rule, and any # + // characters within component names (e.g., "async_search.submit#wait_for_completion_timeout") + // should be preserved literally in the JSONPath query. see issue #485. + var b strings.Builder + b.Grow(len(id) + 16) + b.WriteString("$.") + elements := 0 + + // the plural-parent rule reads the previous segment as transformed by its own + // iteration (bracket-wrapped segments end in ']', stripped segments may shrink), + // so track what the previous segment ended up as, not what it started as. + prevNonEmpty := false + prevEndsInS := false + + pos := 0 + for i := 0; pos <= len(id); i++ { + var seg string + if next := strings.IndexByte(id[pos:], '/'); next >= 0 { + seg = id[pos : pos+next] + pos += next + 1 + } else { + seg = id[pos:] + pos = len(id) + 1 + } + + if seg == "" { + prevNonEmpty = false + prevEndsInS = false continue } - if !isPathChar(segs[i]) { + isLast := i == lastIndex - if strings.Contains(segs[i], "~1") { - segs[i] = strings.ReplaceAll(segs[i], "~1", "/") + if !isPathChar(seg) { + t := seg + if strings.Contains(t, "~1") { + t = strings.ReplaceAll(t, "~1", "/") } - if strings.ContainsRune(segs[i], '%') { - segs[i], _ = url.QueryUnescape(segs[i]) + if strings.ContainsRune(t, '%') { + t, _ = url.QueryUnescape(t) } - - // Use string builder for bracket wrapping - var bracketBuilder strings.Builder - bracketBuilder.Grow(len(segs[i]) + 4) - bracketBuilder.WriteString("['") - bracketBuilder.WriteString(segs[i]) - bracketBuilder.WriteString("']") - segs[i] = bracketBuilder.String() - - if len(cleaned) > 0 && i < len(segs)-1 { - // Use string builder for concatenation with last cleaned element - var concatBuilder strings.Builder - concatBuilder.Grow(len(cleaned[len(cleaned)-1]) + len(segs[i])) - concatBuilder.WriteString(cleaned[len(cleaned)-1]) - concatBuilder.WriteString(segs[i]) - cleaned[len(cleaned)-1] = concatBuilder.String() + // the transformed segment is bracket-wrapped: never empty, ends in ']'. + prevNonEmpty = true + prevEndsInS = false + if i == 0 && !isLast { + // a leading non-path segment (like the '#' of a JSON Pointer) is dropped. continue - } else { - if i > 0 && i < len(segs)-1 { - cleaned = append(cleaned, segs[i]) - continue - } - if i == len(segs)-1 { - l := len(cleaned) - if l > 0 { - // Use string builder for concatenation - var endBuilder strings.Builder - endBuilder.Grow(len(cleaned[l-1]) + len(segs[i])) - endBuilder.WriteString(cleaned[l-1]) - endBuilder.WriteString(segs[i]) - cleaned[l-1] = endBuilder.String() - } else { - cleaned = append(cleaned, segs[i]) - } - } } - } else { + if elements == 0 { + elements++ // becomes the first element, no separator needed + } + b.WriteString("['") + b.WriteString(t) + b.WriteString("']") + continue + } - // strip out any backslashes - if strings.Contains(id, "#") && strings.Contains(segs[i], `\`) { - segs[i] = strings.ReplaceAll(segs[i], `\`, "") - cleaned = append(cleaned, segs[i]) - continue + // strip out any backslashes + if idContainsHash && strings.Contains(seg, `\`) { + t := strings.ReplaceAll(seg, `\`, "") + if elements > 0 { + b.WriteByte('.') } + b.WriteString(t) + elements++ + prevNonEmpty = t != "" + prevEndsInS = t != "" && t[len(t)-1] == 's' + continue + } - intVal, isNum := parseSmallUint(segs[i]) - if isNum { + if intVal, isNum := parseSmallUint(seg); isNum { + // an index with no preceding element is dropped. + if elements > 0 { if intVal <= 99 { - if len(cleaned) > 0 { - appendSegmentOptimized(segs, cleaned, i, false) - } + b.WriteByte('[') + b.WriteString(seg) + b.WriteByte(']') } else { - if len(cleaned) > 0 { - appendSegmentOptimized(segs, cleaned, i, true) - } + b.WriteString("['") + b.WriteString(seg) + b.WriteString("']") } - continue } + prevNonEmpty = true + prevEndsInS = false + continue + } - // if we have a plural parent, wrap it in quotes. - if i > 0 && segs[i-1] != "" && segs[i-1][len(segs[i-1])-1] == 's' { - if i == 2 { // ignore first segment. - cleaned = append(cleaned, segs[i]) - continue + // if we have a plural parent, wrap it in quotes. + if i > 0 && prevNonEmpty && prevEndsInS { + prevNonEmpty = true + prevEndsInS = seg[len(seg)-1] == 's' + if i == 2 { // ignore first segment. + if elements > 0 { + b.WriteByte('.') } - - // Use string builder for plural wrapping - var pluralBuilder strings.Builder - pluralBuilder.Grow(len(cleaned[len(cleaned)-1]) + len(segs[i]) + 4) - pluralBuilder.WriteString(cleaned[len(cleaned)-1]) - pluralBuilder.WriteString("['") - pluralBuilder.WriteString(segs[i]) - pluralBuilder.WriteString("']") - cleaned[len(cleaned)-1] = pluralBuilder.String() + b.WriteString(seg) + elements++ continue } - - cleaned = append(cleaned, segs[i]) - } - } - - // use single string builder for final assembly. - // note: we do NOT replace # with $ here. the leading # from JSON Pointer notation - // (e.g., "#/components/...") is already stripped when we split by "/", and any # - // characters within component names (e.g., "async_search.submit#wait_for_completion_timeout") - // should be preserved literally in the JSONPath query. see issue #485. - var finalBuilder strings.Builder - if len(cleaned) > 1 { - // Estimate final size - totalLen := 0 - for _, seg := range cleaned { - totalLen += len(seg) + b.WriteString("['") + b.WriteString(seg) + b.WriteString("']") + continue } - finalBuilder.Grow(totalLen + len(cleaned) + 5) // segments + dots + $ + potential extra . - finalBuilder.WriteByte('$') - for i, segment := range cleaned { - if i > 0 { - finalBuilder.WriteByte('.') - } - finalBuilder.WriteString(segment) - } - } else { - // Handle single segment case - if len(cleaned) == 1 { - finalBuilder.Grow(len(cleaned[0]) + 5) - finalBuilder.WriteString("$.") - finalBuilder.WriteString(cleaned[0]) - } else { - finalBuilder.WriteString("$.") + if elements > 0 { + b.WriteByte('.') } + b.WriteString(seg) + elements++ + prevNonEmpty = true + prevEndsInS = seg[len(seg)-1] == 's' } - replaced := finalBuilder.String() - - // Ensure proper format - if len(replaced) > 0 { - if len(replaced) > 1 && replaced[1] != '.' { - // Insert period after $ - var dotBuilder strings.Builder - dotBuilder.Grow(len(replaced) + 1) - dotBuilder.WriteByte(replaced[0]) // $ - dotBuilder.WriteByte('.') // . - dotBuilder.WriteString(replaced[1:]) - replaced = dotBuilder.String() - } - } - return name, replaced + return name, b.String() } // ConvertComponentIdIntoPath will convert a JSON Path into a component ID diff --git a/vendor/github.com/pb33f/libopenapi/what-changed/model/breaking_rules_config.go b/vendor/github.com/pb33f/libopenapi/what-changed/model/breaking_rules_config.go index b18834718..e673d4187 100644 --- a/vendor/github.com/pb33f/libopenapi/what-changed/model/breaking_rules_config.go +++ b/vendor/github.com/pb33f/libopenapi/what-changed/model/breaking_rules_config.go @@ -246,6 +246,10 @@ func buildValidComponentSet() map[string]bool { // just "discriminator" at the top level) and returns all validation errors found. // Returns nil if the configuration is valid. func ValidateBreakingRulesConfigYAML(yamlBytes []byte) *ConfigValidationResult { + if len(strings.TrimSpace(string(yamlBytes))) == 0 { + return nil + } + var rootNode yaml.Node if err := yaml.Unmarshal(yamlBytes, &rootNode); err != nil { return &ConfigValidationResult{ diff --git a/vendor/github.com/pb33f/libopenapi/what-changed/model/comparison_functions.go b/vendor/github.com/pb33f/libopenapi/what-changed/model/comparison_functions.go index 824e13848..30361154b 100644 --- a/vendor/github.com/pb33f/libopenapi/what-changed/model/comparison_functions.go +++ b/vendor/github.com/pb33f/libopenapi/what-changed/model/comparison_functions.go @@ -4,6 +4,7 @@ package model import ( + "bytes" "fmt" "reflect" "strings" @@ -412,10 +413,9 @@ func checkForAdditionInternal[T any](l, r *yaml.Node, label string, changes *[]* if withEncoding { createFn = CreateChangeWithEncoding } - // left doesn't exist if: nil OR (empty scalar AND not a map/array) OR (empty map/array) + // left doesn't exist if: nil OR an empty scalar. Empty maps and arrays are real values. leftDoesNotExist := l == nil || - (l.Value == EMPTY_STR && !utils.IsNodeMap(l) && !utils.IsNodeArray(l)) || - ((utils.IsNodeMap(l) || utils.IsNodeArray(l)) && len(l.Content) == 0) + (l.Value == EMPTY_STR && !utils.IsNodeMap(l) && !utils.IsNodeArray(l)) // right exists if: not nil AND (has value OR is array OR is map) rightExists := r != nil && (r.Value != EMPTY_STR || utils.IsNodeArray(r) || utils.IsNodeMap(r)) @@ -441,7 +441,7 @@ func checkForModificationInternal[T any](l, r *yaml.Node, label string, changes createFn = CreateChangeWithEncoding } if l != nil && l.Value != EMPTY_STR && r != nil && r.Value != EMPTY_STR { - if !low.CompareYAMLNodes(l, r) { + if !compareYAMLNodesForChanges(l, r) { createFn(changes, Modified, label, l, r, breaking, orig, new) } return @@ -469,20 +469,60 @@ func checkForModificationInternal[T any](l, r *yaml.Node, label string, changes } // Compare the YAML node trees directly without marshaling - if !low.CompareYAMLNodes(l, r) { + if !compareYAMLNodesForChanges(l, r) { createFn(changes, Modified, label, l, r, breaking, orig, new) } return } if l != nil && utils.IsNodeMap(l) && r != nil && utils.IsNodeMap(r) { // Compare the YAML node trees directly without marshaling - if !low.CompareYAMLNodes(l, r) { + if !compareYAMLNodesForChanges(l, r) { createFn(changes, Modified, label, l, r, breaking, orig, new) } return } } +func compareYAMLNodesForChanges(left, right *yaml.Node) bool { + if low.CompareYAMLNodes(left, right) { + return true + } + leftClone := cloneYAMLNodeWithoutAnchors(left, nil) + rightClone := cloneYAMLNodeWithoutAnchors(right, nil) + if low.CompareYAMLNodes(leftClone, rightClone) { + return true + } + + leftBytes, leftErr := yaml.Marshal(leftClone) + rightBytes, rightErr := yaml.Marshal(rightClone) + return leftErr == nil && rightErr == nil && bytes.Equal(leftBytes, rightBytes) +} + +func cloneYAMLNodeWithoutAnchors(node *yaml.Node, seen map[*yaml.Node]*yaml.Node) *yaml.Node { + if node == nil { + return nil + } + if seen == nil { + seen = make(map[*yaml.Node]*yaml.Node) + } + if clone, ok := seen[node]; ok { + return clone + } + + clone := *node + clone.Anchor = "" + seen[node] = &clone + + if len(node.Content) > 0 { + clone.Content = make([]*yaml.Node, len(node.Content)) + for i, child := range node.Content { + clone.Content[i] = cloneYAMLNodeWithoutAnchors(child, seen) + } + } + clone.Alias = cloneYAMLNodeWithoutAnchors(node.Alias, seen) + return &clone +} + // CheckForModification will check left and right yaml.Node instances for changes. Anything that is found in both // sides, but vary in value is considered a modification. // diff --git a/vendor/github.com/pb33f/libopenapi/what-changed/model/example.go b/vendor/github.com/pb33f/libopenapi/what-changed/model/example.go index 8e6ea7306..35a8cf223 100644 --- a/vendor/github.com/pb33f/libopenapi/what-changed/model/example.go +++ b/vendor/github.com/pb33f/libopenapi/what-changed/model/example.go @@ -84,61 +84,63 @@ func CompareExamples(l, r *base.Example) *ExampleChanges { // Value if utils.IsNodeMap(l.Value.ValueNode) && utils.IsNodeMap(r.Value.ValueNode) { - lKeys := make([]string, len(l.Value.ValueNode.Content)/2) - rKeys := make([]string, len(r.Value.ValueNode.Content)/2) - z := 0 - for k := range l.Value.ValueNode.Content { - if k%2 == 0 { - // if there is no value (value is another map or something else), render the node into yaml and hash it. - // https://github.com/pb33f/libopenapi/issues/61 - val := l.Value.ValueNode.Content[k+1].Value - if val == "" { - val = low.HashYAMLNodeSlice(l.Value.ValueNode.Content[k+1].Content) + if !compareYAMLNodesForChanges(l.Value.ValueNode, r.Value.ValueNode) { + lKeys := make([]string, len(l.Value.ValueNode.Content)/2) + rKeys := make([]string, len(r.Value.ValueNode.Content)/2) + z := 0 + for k := range l.Value.ValueNode.Content { + if k%2 == 0 { + // if there is no value (value is another map or something else), render the node into yaml and hash it. + // https://github.com/pb33f/libopenapi/issues/61 + val := l.Value.ValueNode.Content[k+1].Value + if val == "" { + val = low.HashYAMLNodeSlice(l.Value.ValueNode.Content[k+1].Content) + } + lKeys[z] = fmt.Sprintf("%v-%v-%v", + l.Value.ValueNode.Content[k].Value, + l.Value.ValueNode.Content[k+1].Tag, + fmt.Sprintf("%x", val)) + z++ + } else { + continue } - lKeys[z] = fmt.Sprintf("%v-%v-%v", - l.Value.ValueNode.Content[k].Value, - l.Value.ValueNode.Content[k+1].Tag, - fmt.Sprintf("%x", val)) - z++ - } else { - continue } - } - z = 0 - for k := range r.Value.ValueNode.Content { - if k%2 == 0 { - // if there is no value (value is another map or something else), render the node into yaml and hash it. - // https://github.com/pb33f/libopenapi/issues/61 - val := r.Value.ValueNode.Content[k+1].Value - if val == "" { - val = low.HashYAMLNodeSlice(r.Value.ValueNode.Content[k+1].Content) + z = 0 + for k := range r.Value.ValueNode.Content { + if k%2 == 0 { + // if there is no value (value is another map or something else), render the node into yaml and hash it. + // https://github.com/pb33f/libopenapi/issues/61 + val := r.Value.ValueNode.Content[k+1].Value + if val == "" { + val = low.HashYAMLNodeSlice(r.Value.ValueNode.Content[k+1].Content) + } + rKeys[z] = fmt.Sprintf("%v-%v-%v", + r.Value.ValueNode.Content[k].Value, + r.Value.ValueNode.Content[k+1].Tag, + fmt.Sprintf("%x", val)) + z++ + } else { + continue } - rKeys[z] = fmt.Sprintf("%v-%v-%v", - r.Value.ValueNode.Content[k].Value, - r.Value.ValueNode.Content[k+1].Tag, - fmt.Sprintf("%x", val)) - z++ - } else { - continue - } - } - sort.Strings(lKeys) - sort.Strings(rKeys) - for k := range lKeys { - if k < len(rKeys) && lKeys[k] != rKeys[k] { - CreateChangeWithEncoding(&changes, Modified, v3.ValueLabel, - l.Value.GetValueNode(), r.Value.GetValueNode(), BreakingModified(CompExample, PropValue), l.Value.GetValue(), r.Value.GetValue()) - continue } - if k >= len(rKeys) { - CreateChangeWithEncoding(&changes, PropertyRemoved, v3.ValueLabel, - l.Value.ValueNode, r.Value.ValueNode, BreakingRemoved(CompExample, PropValue), l.Value.Value, r.Value.Value) + sort.Strings(lKeys) + sort.Strings(rKeys) + for k := range lKeys { + if k < len(rKeys) && lKeys[k] != rKeys[k] { + CreateChangeWithEncoding(&changes, Modified, v3.ValueLabel, + l.Value.GetValueNode(), r.Value.GetValueNode(), BreakingModified(CompExample, PropValue), l.Value.GetValue(), r.Value.GetValue()) + continue + } + if k >= len(rKeys) { + CreateChangeWithEncoding(&changes, PropertyRemoved, v3.ValueLabel, + l.Value.ValueNode, r.Value.ValueNode, BreakingRemoved(CompExample, PropValue), l.Value.Value, r.Value.Value) + } } - } - for k := range rKeys { - if k >= len(lKeys) { - CreateChangeWithEncoding(&changes, PropertyAdded, v3.ValueLabel, - l.Value.ValueNode, r.Value.ValueNode, BreakingAdded(CompExample, PropValue), l.Value.Value, r.Value.Value) + for k := range rKeys { + if k >= len(lKeys) { + CreateChangeWithEncoding(&changes, PropertyAdded, v3.ValueLabel, + l.Value.ValueNode, r.Value.ValueNode, BreakingAdded(CompExample, PropValue), l.Value.Value, r.Value.Value) + } } } } else { diff --git a/vendor/go.yaml.in/yaml/v4/.gitignore b/vendor/go.yaml.in/yaml/v4/.gitignore index ae2880f5f..5bdcb9a09 100644 --- a/vendor/go.yaml.in/yaml/v4/.gitignore +++ b/vendor/go.yaml.in/yaml/v4/.gitignore @@ -3,8 +3,9 @@ /.cache/ /.claude/ -/CLAUDE.md +/note/ +/worktree/ /yts/testdata/ /go-yaml -/note/ +/*.md /*.yaml diff --git a/vendor/go.yaml.in/yaml/v4/.golangci.yaml b/vendor/go.yaml.in/yaml/v4/.golangci.yaml index ef97b235e..6eb649792 100644 --- a/vendor/go.yaml.in/yaml/v4/.golangci.yaml +++ b/vendor/go.yaml.in/yaml/v4/.golangci.yaml @@ -6,13 +6,14 @@ version: '2' linters: enable: - dupword + - godoclint - govet - mirror - misspell - - nolintlint - - staticcheck - modernize - nilnesserr + - nolintlint + - staticcheck - thelper - unconvert disable: @@ -25,17 +26,21 @@ linters: - 'NULL' - DOCUMENT-START - BLOCK-END + godoclint: + default: none + enable: + - require-stdlib-doclink + govet: + enable-all: true + disable: + - fieldalignment + - shadow misspell: locale: US nolintlint: allow-unused: false require-specific: true require-explanation: true - govet: - enable-all: true - disable: - - fieldalignment - - shadow staticcheck: checks: # enable all rules diff --git a/vendor/go.yaml.in/yaml/v4/.typos.toml b/vendor/go.yaml.in/yaml/v4/.typos.toml index 59f933115..e0842f9a3 100644 --- a/vendor/go.yaml.in/yaml/v4/.typos.toml +++ b/vendor/go.yaml.in/yaml/v4/.typos.toml @@ -31,3 +31,6 @@ extend-ignore-re = [ [default.extend-words] caf = "caf" # part of "café" shown as "caf\u00e9" in Unicode escape examples deprecat = "deprecat" # Used as part of a command in a docs/ file +Desolver = "Desolver" # Type name: inverse of Resolver (removes tags) +Desolve = "Desolve" # Function name: inverse of Resolve +desolve = "desolve" # Function name variants diff --git a/vendor/go.yaml.in/yaml/v4/doc.go b/vendor/go.yaml.in/yaml/v4/doc.go index 9e9ac7283..3aa872d27 100644 --- a/vendor/go.yaml.in/yaml/v4/doc.go +++ b/vendor/go.yaml.in/yaml/v4/doc.go @@ -1,7 +1,5 @@ -// -// Copyright (c) 2025 The go-yaml Project Contributors +// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 -// // Package yaml implements YAML 1.1/1.2 encoding and decoding for Go programs. // @@ -81,14 +79,14 @@ // // Or use version-specific option presets for consistent formatting: // -// yaml.NewDumper(w, yaml.V3) +// yaml.NewDumper(w, yaml.WithV3Defaults()) // // Options can be combined and later options override earlier ones: // // // Start with v3 defaults, then override indent // yaml.NewDumper(w, -// yaml.V3, -// yaml.WithIndent(4), +// yaml.WithV3Defaults(), +// yaml.WithIndent(2), // ) // // Load options from YAML configuration files: diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/README.md b/vendor/go.yaml.in/yaml/v4/internal/libyaml/README.md index 62dbeac3e..cfcbcc984 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/README.md +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/README.md @@ -23,7 +23,7 @@ The `internal/libyaml` package implements the core YAML processing stages: - **yaml.go** - Core types and constants (Event, Token, enums) - **reader.go** - Input handling and encoding detection - **writer.go** - Output handling -- **yamlprivate.go** - Internal types and helper functions +- **util.go** - Internal types and helper functions ### Test Files @@ -34,10 +34,11 @@ The `internal/libyaml` package implements the core YAML processing stages: - **yaml_test.go** - Utility function tests - **reader_test.go** - Reader tests - **writer_test.go** - Writer tests -- **yamlprivate_test.go** - Character classification tests +- **util_test.go** - Character classification tests - **loader_test.go** - Data loader scalar resolution tests - **yamldatatest_test.go** - YAML test data loading framework -- **yamldatatest_loader.go** - YAML test data loader with scalar type resolution (exported for reuse) +- **yamldatatest_loader.go** - YAML test data loader with scalar type + resolution (exported for reuse) ### Test Data Files (in `testdata/`) @@ -48,7 +49,7 @@ The `internal/libyaml` package implements the core YAML processing stages: - **yaml.yaml** - Utility function test cases - **reader.yaml** - Reader test cases - **writer.yaml** - Writer test cases -- **yamlprivate.yaml** - Character classification test cases +- **util.yaml** - Character classification test cases - **loader.yaml** - Data loader scalar resolution test cases ## Processing Pipeline @@ -126,7 +127,8 @@ The testing framework uses a data-driven approach: 1. **Test data** is stored in YAML files in the `testdata/` directory 2. **Test logic** is implemented in Go files (`*_test.go`) -3. **One-to-one pairing**: Each `testdata/foo.yaml` has a corresponding `foo_test.go` +3. **One-to-one pairing**: Each `testdata/foo.yaml` has a corresponding + `foo_test.go` **Benefits**: - Easy to add new test cases without writing Go code @@ -175,7 +177,7 @@ Each YAML file contains test cases for a specific component: - Output handlers (string, io.Writer) - Error conditions -- **yamlprivate.yaml** - Character classification tests +- **util.yaml** - Character classification tests - Character type predicates (isAlpha, isDigit, isHex, etc.) - Character conversion functions (asDigit, asHex, width) - Unicode handling @@ -188,13 +190,15 @@ Each YAML file contains test cases for a specific component: ### Test Framework Implementation -The test framework is implemented in `yamldatatest_loader.go` and `yamldatatest_test.go`: +The test framework is implemented in `testdata_test.go`: **Core functions**: -- `LoadYAML(data []byte) (interface{}, error)` - Parses YAML using libyaml parser with scalar type resolution (exported) -- `UnmarshalStruct(target interface{}, data map[string]interface{}) error` - Populates structs (exported) -- `LoadTestCases(filename string) ([]TestCase, error)` - Loads and parses test YAML files -- `coerceScalar(value string) interface{}` - Resolves scalar strings to appropriate Go types (int, float64, bool, nil, string) +- `LoadAny(data []byte) (interface{}, error)` - Parses YAML using production + loader with scalar type resolution (exported from loader.go) +- `UnmarshalStruct(target interface{}, data map[string]interface{}) error` - + Populates structs (exported) +- `LoadTestCases(filename string) ([]TestCase, error)` - Loads and parses + test YAML files **Core types**: - `TestCase` struct - Umbrella structure containing fields for all test types @@ -203,8 +207,10 @@ The test framework is implemented in `yamldatatest_loader.go` and `yamldatatest_ **Post-processing**: After loading, the framework processes test data: -- Converts `Want` (interface{}) to `WantEvents`, `WantTokens`, or `WantSpecs` based on test type -- Converts `Want` (interface{}) to `WantContains` (handles both scalar and sequence) +- Converts `Want` (interface{}) to `WantEvents`, `WantTokens`, or `WantSpecs` + based on test type +- Converts `Want` (interface{}) to `WantContains` (handles both scalar and + sequence) - Converts `Checks` to field validation specifications ### Test Types @@ -490,7 +496,8 @@ Test cases use a **type-as-key** format where the test type is the map key: - **yaml** - Input YAML string to test - **want** - Expected result (format varies by test type) - For api-panic: string containing expected panic message substring - - For scan-error/parse-error: boolean (defaults to true if omitted; set to false if no error expected) + - For scan-error/parse-error: boolean (defaults to true if omitted; set to + false if no error expected) - For enum-string: string representing expected String() output - For other types: varies (may be sequence or scalar) - **data** - For emitter tests: list of event specifications to emit @@ -499,11 +506,20 @@ Test cases use a **type-as-key** format where the test type is the map key: - **call** - For API tests: method call [MethodName, arg1, arg2, ...] - **init** - For API panic tests: setup method call before main method - **byte** - For API tests: boolean flag to convert string args to []byte -- **test** - For API tests: list of field validation checks in format `operator: [field, value]` where operator is one of: nil, cap, len, eq, gte, len-gt. -- **test** - For style-accessor tests: array of [Method, STYLE] where Method is the accessor method (e.g., ScalarStyle) and STYLE is the style constant (e.g., DOUBLE_QUOTED_SCALAR_STYLE). -- **enum** - For enum tests: array of [Type, Value] where Type is the enum type (e.g., ScalarStyle) and Value is the constant (e.g., PLAIN_SCALAR_STYLE) - -**Note on scalar type resolution**: Unquoted scalar values in test data are automatically resolved to appropriate Go types (int, float64, bool, nil) by the `LoadYAML` function. Quoted scalars remain as strings. +- **test** - For API tests: list of field validation checks in format + `operator: [field, value]` where operator is one of: nil, cap, len, eq, gte, + len-gt. +- **test** - For style-accessor tests: array of [Method, STYLE] where Method + is the accessor method (e.g., ScalarStyle) and STYLE is the style constant + (e.g., DOUBLE_QUOTED_SCALAR_STYLE). +- **enum** - For enum tests: array of [Type, Value] where Type is the enum + type (e.g., ScalarStyle) and Value is the constant (e.g., + PLAIN_SCALAR_STYLE) + +**Note on scalar type resolution**: Unquoted scalar values in test data are +automatically resolved to appropriate Go types (int, float64, bool, nil) by the +`LoadAny` function. +Quoted scalars remain as strings. ### Running Tests diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/api.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/api.go deleted file mode 100644 index f0807f5b0..000000000 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/api.go +++ /dev/null @@ -1,733 +0,0 @@ -// Copyright 2006-2010 Kirill Simonov -// Copyright 2011-2019 Canonical Ltd -// Copyright 2025 The go-yaml Project Contributors -// SPDX-License-Identifier: Apache-2.0 AND MIT - -// High-level API helpers for parser and emitter initialization and -// configuration. -// Provides convenience functions for token insertion and stream management. - -package libyaml - -import ( - "io" -) - -func (parser *Parser) insertToken(pos int, token *Token) { - // fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens)) - - // Check if we can move the queue at the beginning of the buffer. - if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { - if parser.tokens_head != len(parser.tokens) { - copy(parser.tokens, parser.tokens[parser.tokens_head:]) - } - parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] - parser.tokens_head = 0 - } - parser.tokens = append(parser.tokens, *token) - if pos < 0 { - return - } - copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) - parser.tokens[parser.tokens_head+pos] = *token -} - -// NewParser creates a new parser object. -func NewParser() Parser { - return Parser{ - raw_buffer: make([]byte, 0, input_raw_buffer_size), - buffer: make([]byte, 0, input_buffer_size), - } -} - -// Delete a parser object. -func (parser *Parser) Delete() { - *parser = Parser{} -} - -// String read handler. -func yamlStringReadHandler(parser *Parser, buffer []byte) (n int, err error) { - if parser.input_pos == len(parser.input) { - return 0, io.EOF - } - n = copy(buffer, parser.input[parser.input_pos:]) - parser.input_pos += n - return n, nil -} - -// Reader read handler. -func yamlReaderReadHandler(parser *Parser, buffer []byte) (n int, err error) { - return parser.input_reader.Read(buffer) -} - -// SetInputString sets a string input. -func (parser *Parser) SetInputString(input []byte) { - if parser.read_handler != nil { - panic("must set the input source only once") - } - parser.read_handler = yamlStringReadHandler - parser.input = input - parser.input_pos = 0 -} - -// SetInputReader sets a file input. -func (parser *Parser) SetInputReader(r io.Reader) { - if parser.read_handler != nil { - panic("must set the input source only once") - } - parser.read_handler = yamlReaderReadHandler - parser.input_reader = r -} - -// SetEncoding sets the source encoding. -func (parser *Parser) SetEncoding(encoding Encoding) { - if parser.encoding != ANY_ENCODING { - panic("must set the encoding only once") - } - parser.encoding = encoding -} - -// GetPendingComments returns the parser's comment queue for CLI access. -func (parser *Parser) GetPendingComments() []Comment { - return parser.comments -} - -// GetCommentsHead returns the current position in the comment queue. -func (parser *Parser) GetCommentsHead() int { - return parser.comments_head -} - -// NewEmitter creates a new emitter object. -func NewEmitter() Emitter { - return Emitter{ - buffer: make([]byte, output_buffer_size), - states: make([]EmitterState, 0, initial_stack_size), - events: make([]Event, 0, initial_queue_size), - best_width: -1, - } -} - -// Delete an emitter object. -func (emitter *Emitter) Delete() { - *emitter = Emitter{} -} - -// String write handler. -func yamlStringWriteHandler(emitter *Emitter, buffer []byte) error { - *emitter.output_buffer = append(*emitter.output_buffer, buffer...) - return nil -} - -// yamlWriterWriteHandler uses emitter.output_writer to write the -// emitted text. -func yamlWriterWriteHandler(emitter *Emitter, buffer []byte) error { - _, err := emitter.output_writer.Write(buffer) - return err -} - -// SetOutputString sets a string output. -func (emitter *Emitter) SetOutputString(output_buffer *[]byte) { - if emitter.write_handler != nil { - panic("must set the output target only once") - } - emitter.write_handler = yamlStringWriteHandler - emitter.output_buffer = output_buffer -} - -// SetOutputWriter sets a file output. -func (emitter *Emitter) SetOutputWriter(w io.Writer) { - if emitter.write_handler != nil { - panic("must set the output target only once") - } - emitter.write_handler = yamlWriterWriteHandler - emitter.output_writer = w -} - -// SetEncoding sets the output encoding. -func (emitter *Emitter) SetEncoding(encoding Encoding) { - if emitter.encoding != ANY_ENCODING { - panic("must set the output encoding only once") - } - emitter.encoding = encoding -} - -// SetCanonical sets the canonical output style. -func (emitter *Emitter) SetCanonical(canonical bool) { - emitter.canonical = canonical -} - -// SetIndent sets the indentation increment. -func (emitter *Emitter) SetIndent(indent int) { - if indent < 2 || indent > 9 { - indent = 2 - } - emitter.BestIndent = indent -} - -// SetWidth sets the preferred line width. -func (emitter *Emitter) SetWidth(width int) { - if width < 0 { - width = -1 - } - emitter.best_width = width -} - -// SetUnicode sets if unescaped non-ASCII characters are allowed. -func (emitter *Emitter) SetUnicode(unicode bool) { - emitter.unicode = unicode -} - -// SetLineBreak sets the preferred line break character. -func (emitter *Emitter) SetLineBreak(line_break LineBreak) { - emitter.line_break = line_break -} - -///* -// * Destroy a token object. -// */ -// -//YAML_DECLARE(void) -//yaml_token_delete(yaml_token_t *token) -//{ -// assert(token); // Non-NULL token object expected. -// -// switch (token.type) -// { -// case YAML_TAG_DIRECTIVE_TOKEN: -// yaml_free(token.data.tag_directive.handle); -// yaml_free(token.data.tag_directive.prefix); -// break; -// -// case YAML_ALIAS_TOKEN: -// yaml_free(token.data.alias.value); -// break; -// -// case YAML_ANCHOR_TOKEN: -// yaml_free(token.data.anchor.value); -// break; -// -// case YAML_TAG_TOKEN: -// yaml_free(token.data.tag.handle); -// yaml_free(token.data.tag.suffix); -// break; -// -// case YAML_SCALAR_TOKEN: -// yaml_free(token.data.scalar.value); -// break; -// -// default: -// break; -// } -// -// memset(token, 0, sizeof(yaml_token_t)); -//} -// -///* -// * Check if a string is a valid UTF-8 sequence. -// * -// * Check 'reader.c' for more details on UTF-8 encoding. -// */ -// -//static int -//yaml_check_utf8(yaml_char_t *start, size_t length) -//{ -// yaml_char_t *end = start+length; -// yaml_char_t *pointer = start; -// -// while (pointer < end) { -// unsigned char octet; -// unsigned int width; -// unsigned int value; -// size_t k; -// -// octet = pointer[0]; -// width = (octet & 0x80) == 0x00 ? 1 : -// (octet & 0xE0) == 0xC0 ? 2 : -// (octet & 0xF0) == 0xE0 ? 3 : -// (octet & 0xF8) == 0xF0 ? 4 : 0; -// value = (octet & 0x80) == 0x00 ? octet & 0x7F : -// (octet & 0xE0) == 0xC0 ? octet & 0x1F : -// (octet & 0xF0) == 0xE0 ? octet & 0x0F : -// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0; -// if (!width) return 0; -// if (pointer+width > end) return 0; -// for (k = 1; k < width; k ++) { -// octet = pointer[k]; -// if ((octet & 0xC0) != 0x80) return 0; -// value = (value << 6) + (octet & 0x3F); -// } -// if (!((width == 1) || -// (width == 2 && value >= 0x80) || -// (width == 3 && value >= 0x800) || -// (width == 4 && value >= 0x10000))) return 0; -// -// pointer += width; -// } -// -// return 1; -//} -// - -// NewStreamStartEvent creates a new STREAM-START event. -func NewStreamStartEvent(encoding Encoding) Event { - return Event{ - Type: STREAM_START_EVENT, - encoding: encoding, - } -} - -// NewStreamEndEvent creates a new STREAM-END event. -func NewStreamEndEvent() Event { - return Event{ - Type: STREAM_END_EVENT, - } -} - -// NewDocumentStartEvent creates a new DOCUMENT-START event. -func NewDocumentStartEvent(version_directive *VersionDirective, tag_directives []TagDirective, implicit bool) Event { - return Event{ - Type: DOCUMENT_START_EVENT, - versionDirective: version_directive, - tagDirectives: tag_directives, - Implicit: implicit, - } -} - -// NewDocumentEndEvent creates a new DOCUMENT-END event. -func NewDocumentEndEvent(implicit bool) Event { - return Event{ - Type: DOCUMENT_END_EVENT, - Implicit: implicit, - } -} - -// NewAliasEvent creates a new ALIAS event. -func NewAliasEvent(anchor []byte) Event { - return Event{ - Type: ALIAS_EVENT, - Anchor: anchor, - } -} - -// NewScalarEvent creates a new SCALAR event. -func NewScalarEvent(anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style ScalarStyle) Event { - return Event{ - Type: SCALAR_EVENT, - Anchor: anchor, - Tag: tag, - Value: value, - Implicit: plain_implicit, - quoted_implicit: quoted_implicit, - Style: Style(style), - } -} - -// NewSequenceStartEvent creates a new SEQUENCE-START event. -func NewSequenceStartEvent(anchor, tag []byte, implicit bool, style SequenceStyle) Event { - return Event{ - Type: SEQUENCE_START_EVENT, - Anchor: anchor, - Tag: tag, - Implicit: implicit, - Style: Style(style), - } -} - -// NewSequenceEndEvent creates a new SEQUENCE-END event. -func NewSequenceEndEvent() Event { - return Event{ - Type: SEQUENCE_END_EVENT, - } -} - -// NewMappingStartEvent creates a new MAPPING-START event. -func NewMappingStartEvent(anchor, tag []byte, implicit bool, style MappingStyle) Event { - return Event{ - Type: MAPPING_START_EVENT, - Anchor: anchor, - Tag: tag, - Implicit: implicit, - Style: Style(style), - } -} - -// NewMappingEndEvent creates a new MAPPING-END event. -func NewMappingEndEvent() Event { - return Event{ - Type: MAPPING_END_EVENT, - } -} - -// Delete an event object. -func (e *Event) Delete() { - *e = Event{} -} - -///* -// * Create a document object. -// */ -// -//YAML_DECLARE(int) -//yaml_document_initialize(document *yaml_document_t, -// version_directive *yaml_version_directive_t, -// tag_directives_start *yaml_tag_directive_t, -// tag_directives_end *yaml_tag_directive_t, -// start_implicit int, end_implicit int) -//{ -// struct { -// error yaml_error_type_t -// } context -// struct { -// start *yaml_node_t -// end *yaml_node_t -// top *yaml_node_t -// } nodes = { NULL, NULL, NULL } -// version_directive_copy *yaml_version_directive_t = NULL -// struct { -// start *yaml_tag_directive_t -// end *yaml_tag_directive_t -// top *yaml_tag_directive_t -// } tag_directives_copy = { NULL, NULL, NULL } -// value yaml_tag_directive_t = { NULL, NULL } -// mark yaml_mark_t = { 0, 0, 0 } -// -// assert(document) // Non-NULL document object is expected. -// assert((tag_directives_start && tag_directives_end) || -// (tag_directives_start == tag_directives_end)) -// // Valid tag directives are expected. -// -// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error -// -// if (version_directive) { -// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t)) -// if (!version_directive_copy) goto error -// version_directive_copy.major = version_directive.major -// version_directive_copy.minor = version_directive.minor -// } -// -// if (tag_directives_start != tag_directives_end) { -// tag_directive *yaml_tag_directive_t -// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE)) -// goto error -// for (tag_directive = tag_directives_start -// tag_directive != tag_directives_end; tag_directive ++) { -// assert(tag_directive.handle) -// assert(tag_directive.prefix) -// if (!yaml_check_utf8(tag_directive.handle, -// strlen((char *)tag_directive.handle))) -// goto error -// if (!yaml_check_utf8(tag_directive.prefix, -// strlen((char *)tag_directive.prefix))) -// goto error -// value.handle = yaml_strdup(tag_directive.handle) -// value.prefix = yaml_strdup(tag_directive.prefix) -// if (!value.handle || !value.prefix) goto error -// if (!PUSH(&context, tag_directives_copy, value)) -// goto error -// value.handle = NULL -// value.prefix = NULL -// } -// } -// -// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy, -// tag_directives_copy.start, tag_directives_copy.top, -// start_implicit, end_implicit, mark, mark) -// -// return 1 -// -//error: -// STACK_DEL(&context, nodes) -// yaml_free(version_directive_copy) -// while (!STACK_EMPTY(&context, tag_directives_copy)) { -// value yaml_tag_directive_t = POP(&context, tag_directives_copy) -// yaml_free(value.handle) -// yaml_free(value.prefix) -// } -// STACK_DEL(&context, tag_directives_copy) -// yaml_free(value.handle) -// yaml_free(value.prefix) -// -// return 0 -//} -// -///* -// * Destroy a document object. -// */ -// -//YAML_DECLARE(void) -//yaml_document_delete(document *yaml_document_t) -//{ -// struct { -// error yaml_error_type_t -// } context -// tag_directive *yaml_tag_directive_t -// -// context.error = YAML_NO_ERROR // Eliminate a compiler warning. -// -// assert(document) // Non-NULL document object is expected. -// -// while (!STACK_EMPTY(&context, document.nodes)) { -// node yaml_node_t = POP(&context, document.nodes) -// yaml_free(node.tag) -// switch (node.type) { -// case YAML_SCALAR_NODE: -// yaml_free(node.data.scalar.value) -// break -// case YAML_SEQUENCE_NODE: -// STACK_DEL(&context, node.data.sequence.items) -// break -// case YAML_MAPPING_NODE: -// STACK_DEL(&context, node.data.mapping.pairs) -// break -// default: -// assert(0) // Should not happen. -// } -// } -// STACK_DEL(&context, document.nodes) -// -// yaml_free(document.version_directive) -// for (tag_directive = document.tag_directives.start -// tag_directive != document.tag_directives.end -// tag_directive++) { -// yaml_free(tag_directive.handle) -// yaml_free(tag_directive.prefix) -// } -// yaml_free(document.tag_directives.start) -// -// memset(document, 0, sizeof(yaml_document_t)) -//} -// -///** -// * Get a document node. -// */ -// -//YAML_DECLARE(yaml_node_t *) -//yaml_document_get_node(document *yaml_document_t, index int) -//{ -// assert(document) // Non-NULL document object is expected. -// -// if (index > 0 && document.nodes.start + index <= document.nodes.top) { -// return document.nodes.start + index - 1 -// } -// return NULL -//} -// -///** -// * Get the root object. -// */ -// -//YAML_DECLARE(yaml_node_t *) -//yaml_document_get_root_node(document *yaml_document_t) -//{ -// assert(document) // Non-NULL document object is expected. -// -// if (document.nodes.top != document.nodes.start) { -// return document.nodes.start -// } -// return NULL -//} -// -///* -// * Add a scalar node to a document. -// */ -// -//YAML_DECLARE(int) -//yaml_document_add_scalar(document *yaml_document_t, -// tag *yaml_char_t, value *yaml_char_t, length int, -// style yaml_scalar_style_t) -//{ -// struct { -// error yaml_error_type_t -// } context -// mark yaml_mark_t = { 0, 0, 0 } -// tag_copy *yaml_char_t = NULL -// value_copy *yaml_char_t = NULL -// node yaml_node_t -// -// assert(document) // Non-NULL document object is expected. -// assert(value) // Non-NULL value is expected. -// -// if (!tag) { -// tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG -// } -// -// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error -// tag_copy = yaml_strdup(tag) -// if (!tag_copy) goto error -// -// if (length < 0) { -// length = strlen((char *)value) -// } -// -// if (!yaml_check_utf8(value, length)) goto error -// value_copy = yaml_malloc(length+1) -// if (!value_copy) goto error -// memcpy(value_copy, value, length) -// value_copy[length] = '\0' -// -// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark) -// if (!PUSH(&context, document.nodes, node)) goto error -// -// return document.nodes.top - document.nodes.start -// -//error: -// yaml_free(tag_copy) -// yaml_free(value_copy) -// -// return 0 -//} -// -///* -// * Add a sequence node to a document. -// */ -// -//YAML_DECLARE(int) -//yaml_document_add_sequence(document *yaml_document_t, -// tag *yaml_char_t, style yaml_sequence_style_t) -//{ -// struct { -// error yaml_error_type_t -// } context -// mark yaml_mark_t = { 0, 0, 0 } -// tag_copy *yaml_char_t = NULL -// struct { -// start *yaml_node_item_t -// end *yaml_node_item_t -// top *yaml_node_item_t -// } items = { NULL, NULL, NULL } -// node yaml_node_t -// -// assert(document) // Non-NULL document object is expected. -// -// if (!tag) { -// tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG -// } -// -// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error -// tag_copy = yaml_strdup(tag) -// if (!tag_copy) goto error -// -// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error -// -// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end, -// style, mark, mark) -// if (!PUSH(&context, document.nodes, node)) goto error -// -// return document.nodes.top - document.nodes.start -// -//error: -// STACK_DEL(&context, items) -// yaml_free(tag_copy) -// -// return 0 -//} -// -///* -// * Add a mapping node to a document. -// */ -// -//YAML_DECLARE(int) -//yaml_document_add_mapping(document *yaml_document_t, -// tag *yaml_char_t, style yaml_mapping_style_t) -//{ -// struct { -// error yaml_error_type_t -// } context -// mark yaml_mark_t = { 0, 0, 0 } -// tag_copy *yaml_char_t = NULL -// struct { -// start *yaml_node_pair_t -// end *yaml_node_pair_t -// top *yaml_node_pair_t -// } pairs = { NULL, NULL, NULL } -// node yaml_node_t -// -// assert(document) // Non-NULL document object is expected. -// -// if (!tag) { -// tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG -// } -// -// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error -// tag_copy = yaml_strdup(tag) -// if (!tag_copy) goto error -// -// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error -// -// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end, -// style, mark, mark) -// if (!PUSH(&context, document.nodes, node)) goto error -// -// return document.nodes.top - document.nodes.start -// -//error: -// STACK_DEL(&context, pairs) -// yaml_free(tag_copy) -// -// return 0 -//} -// -///* -// * Append an item to a sequence node. -// */ -// -//YAML_DECLARE(int) -//yaml_document_append_sequence_item(document *yaml_document_t, -// sequence int, item int) -//{ -// struct { -// error yaml_error_type_t -// } context -// -// assert(document) // Non-NULL document is required. -// assert(sequence > 0 -// && document.nodes.start + sequence <= document.nodes.top) -// // Valid sequence id is required. -// assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE) -// // A sequence node is required. -// assert(item > 0 && document.nodes.start + item <= document.nodes.top) -// // Valid item id is required. -// -// if (!PUSH(&context, -// document.nodes.start[sequence-1].data.sequence.items, item)) -// return 0 -// -// return 1 -//} -// -///* -// * Append a pair of a key and a value to a mapping node. -// */ -// -//YAML_DECLARE(int) -//yaml_document_append_mapping_pair(document *yaml_document_t, -// mapping int, key int, value int) -//{ -// struct { -// error yaml_error_type_t -// } context -// -// pair yaml_node_pair_t -// -// assert(document) // Non-NULL document is required. -// assert(mapping > 0 -// && document.nodes.start + mapping <= document.nodes.top) -// // Valid mapping id is required. -// assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE) -// // A mapping node is required. -// assert(key > 0 && document.nodes.start + key <= document.nodes.top) -// // Valid key id is required. -// assert(value > 0 && document.nodes.start + value <= document.nodes.top) -// // Valid value id is required. -// -// pair.key = key -// pair.value = value -// -// if (!PUSH(&context, -// document.nodes.start[mapping-1].data.mapping.pairs, pair)) -// return 0 -// -// return 1 -//} -// -// diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/composer.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/composer.go index 57dd7f1e6..59d64cb03 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/composer.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/composer.go @@ -24,112 +24,48 @@ type Composer struct { returnStream bool // flag to return stream node next atStreamEnd bool // at stream end encoding Encoding // stream encoding from STREAM_START + opts *Options // options for loading } // NewComposer creates a new composer from a byte slice. -func NewComposer(b []byte) *Composer { +func NewComposer(b []byte, opts *Options) *Composer { p := Composer{ Parser: NewParser(), + opts: opts, } if len(b) == 0 { b = []byte{'\n'} } p.Parser.SetInputString(b) + if opts != nil { + p.Parser.depthCheck = opts.DepthCheck + } return &p } -// NewComposerFromReader creates a new composer from an io.Reader. -func NewComposerFromReader(r io.Reader) *Composer { +// NewComposerFromReader creates a new composer from an [io.Reader]. +func NewComposerFromReader(r io.Reader, opts *Options) *Composer { p := Composer{ Parser: NewParser(), + opts: opts, } p.Parser.SetInputReader(r) - return &p -} - -func (c *Composer) init() { - if c.doneInit { - return - } - c.anchors = make(map[string]*Node) - // Peek to get the encoding from STREAM_START_EVENT - if c.peek() == STREAM_START_EVENT { - c.encoding = c.event.GetEncoding() - } - c.expect(STREAM_START_EVENT) - c.doneInit = true - - // If stream nodes are enabled, prepare to return the first stream node - if c.streamNodes { - c.returnStream = true - } -} - -func (c *Composer) Destroy() { - if c.event.Type != NO_EVENT { - c.event.Delete() - } - c.Parser.Delete() -} - -// SetStreamNodes enables or disables stream node emission. -func (c *Composer) SetStreamNodes(enable bool) { - c.streamNodes = enable -} - -// expect consumes an event from the event stream and -// checks that it's of the expected type. -func (c *Composer) expect(e EventType) { - if c.event.Type == NO_EVENT { - if err := c.Parser.Parse(&c.event); err != nil { - c.fail(err) - } - } - if c.event.Type == STREAM_END_EVENT { - failf("attempted to go past the end of stream; corrupted value?") - } - if c.event.Type != e { - c.fail(fmt.Errorf("expected %s event but got %s", e, c.event.Type)) - } - c.event.Delete() - c.event.Type = NO_EVENT -} - -// peek peeks at the next event in the event stream, -// puts the results into c.event and returns the event type. -func (c *Composer) peek() EventType { - if c.event.Type != NO_EVENT { - return c.event.Type - } - // It's curious choice from the underlying API to generally return a - // positive result on success, but on this case return true in an error - // scenario. This was the source of bugs in the past (issue #666). - if err := c.Parser.Parse(&c.event); err != nil { - c.fail(err) - } - return c.event.Type -} - -func (c *Composer) fail(err error) { - Fail(err) -} - -func (c *Composer) anchor(n *Node, anchor []byte) { - if anchor != nil { - n.Anchor = string(anchor) - c.anchors[n.Anchor] = n + if opts != nil { + p.Parser.depthCheck = opts.DepthCheck } + return &p } -// Parse parses the next YAML node from the event stream. -func (c *Composer) Parse() *Node { +// Compose composes the next YAML node from the event stream. +func (c *Composer) Compose() *Node { c.init() // Handle stream nodes if enabled if c.streamNodes { // Check for stream end first if c.peek() == STREAM_END_EVENT { - // If we haven't returned the final stream node yet, return it now + // If we haven't returned the final stream node yet, + // return it now if !c.atStreamEnd { c.atStreamEnd = true return c.createStreamNode() @@ -138,7 +74,8 @@ func (c *Composer) Parse() *Node { return nil } - // Check if we should return a stream node before the next document + // Check if we should return a stream node before the next + // document if c.returnStream { c.returnStream = false n := c.createStreamNode() @@ -160,7 +97,8 @@ func (c *Composer) Parse() *Node { case DOCUMENT_START_EVENT: return c.document() case STREAM_END_EVENT: - // Happens when attempting to decode an empty buffer (when not using stream nodes). + // Happens when attempting to decode an empty buffer (when not + // using stream nodes). return nil case TAIL_COMMENT_EVENT: panic("internal error: unexpected tail comment event (please report)") @@ -169,18 +107,17 @@ func (c *Composer) Parse() *Node { } } -func (c *Composer) node(kind Kind, defaultTag, tag, value string) *Node { +// node creates a new node with the given kind, tag, and value, and attaches +// position and comment information from the current event. +func (c *Composer) node(kind Kind, tag, value string) *Node { var style Style if tag != "" && tag != "!" { // Normalize tag to short form (e.g., tag:yaml.org,2002:str -> !!str) tag = shortTag(tag) style = TaggedStyle - } else if defaultTag != "" { - tag = defaultTag - } else if kind == ScalarNode { - // Delegate to resolver to determine tag from value - tag, _ = resolve("", value) } + // Note: Nodes without explicit tags are left with empty tags. + // Tag defaulting happens in a separate stage via Resolver. n := &Node{ Kind: kind, Tag: tag, @@ -188,8 +125,8 @@ func (c *Composer) node(kind Kind, defaultTag, tag, value string) *Node { Style: style, } if !c.Textless { - n.Line = c.event.StartMark.Line + 1 - n.Column = c.event.StartMark.Column + 1 + n.Line = c.event.StartMark.Line + n.Column = c.event.StartMark.Column n.HeadComment = string(c.event.HeadComment) n.LineComment = string(c.event.LineComment) n.FootComment = string(c.event.FootComment) @@ -197,14 +134,10 @@ func (c *Composer) node(kind Kind, defaultTag, tag, value string) *Node { return n } -func (c *Composer) parseChild(parent *Node) *Node { - child := c.Parse() - parent.Content = append(parent.Content, child) - return child -} - +// document composes a document node by parsing its content between +// DOCUMENT_START and DOCUMENT_END events. func (c *Composer) document() *Node { - n := c.node(DocumentNode, "", "", "") + n := c.node(DocumentNode, "", "") c.doc = n c.expect(DOCUMENT_START_EVENT) c.parseChild(n) @@ -221,56 +154,40 @@ func (c *Composer) document() *Node { return n } +// createStreamNode creates a stream node with encoding information. func (c *Composer) createStreamNode() *Node { n := &Node{ - Kind: StreamNode, - Encoding: c.encoding, + Kind: StreamNode, + Stream: &Stream{Encoding: c.encoding}, } if !c.Textless && c.event.Type != NO_EVENT { - n.Line = c.event.StartMark.Line + 1 - n.Column = c.event.StartMark.Column + 1 - } - return n -} - -// captureDirectives captures version and tag directives from upcoming DOCUMENT_START. -func (c *Composer) captureDirectives(n *Node) { - if c.peek() == DOCUMENT_START_EVENT { - if vd := c.event.GetVersionDirective(); vd != nil { - n.Version = &StreamVersionDirective{ - Major: vd.Major(), - Minor: vd.Minor(), - } - } - if tds := c.event.GetTagDirectives(); len(tds) > 0 { - n.TagDirectives = make([]StreamTagDirective, len(tds)) - for i, td := range tds { - n.TagDirectives[i] = StreamTagDirective{ - Handle: td.GetHandle(), - Prefix: td.GetPrefix(), - } - } + n.Line = c.event.StartMark.Line + n.Column = c.event.StartMark.Column + if c.event.Type == STREAM_END_EVENT { + n.HeadComment = string(c.event.HeadComment) + n.LineComment = string(c.event.LineComment) + n.FootComment = string(c.event.FootComment) } } + return n } +// alias composes an alias node by resolving the referenced anchor. func (c *Composer) alias() *Node { - n := c.node(AliasNode, "", "", string(c.event.Anchor)) + n := c.node(AliasNode, "", string(c.event.Anchor)) n.Alias = c.anchors[n.Value] if n.Alias == nil { msg := fmt.Sprintf("unknown anchor '%s' referenced", n.Value) - Fail(&ParserError{ - Message: msg, - Mark: Mark{ - Line: n.Line, - Column: n.Column, - }, - }) + Fail(formatComposerError(msg, Mark{ + Line: n.Line, + Column: n.Column, + })) } c.expect(ALIAS_EVENT) return n } +// scalar composes a scalar node with value, tag, and style information. func (c *Composer) scalar() *Node { parsedStyle := c.event.ScalarStyle() var nodeStyle Style @@ -286,19 +203,17 @@ func (c *Composer) scalar() *Node { } nodeValue := string(c.event.Value) nodeTag := string(c.event.Tag) - var defaultTag string - if nodeStyle != 0 { - defaultTag = strTag - } - n := c.node(ScalarNode, defaultTag, nodeTag, nodeValue) + n := c.node(ScalarNode, nodeTag, nodeValue) n.Style |= nodeStyle c.anchor(n, c.event.Anchor) c.expect(SCALAR_EVENT) return n } +// sequence composes a sequence node by parsing elements between +// SEQUENCE_START and SEQUENCE_END events. func (c *Composer) sequence() *Node { - n := c.node(SequenceNode, seqTag, string(c.event.Tag), "") + n := c.node(SequenceNode, string(c.event.Tag), "") if c.event.SequenceStyle()&FLOW_SEQUENCE_STYLE != 0 { n.Style |= FlowStyle } @@ -313,8 +228,10 @@ func (c *Composer) sequence() *Node { return n } +// mapping composes a mapping node by parsing key-value pairs between +// MAPPING_START and MAPPING_END events, handling foot comments appropriately. func (c *Composer) mapping() *Node { - n := c.node(MappingNode, mapTag, string(c.event.Tag), "") + n := c.node(MappingNode, string(c.event.Tag), "") block := true if c.event.MappingStyle()&FLOW_MAPPING_STYLE != 0 { block = false @@ -353,10 +270,149 @@ func (c *Composer) mapping() *Node { return n } +// init initializes the composer by setting up the anchor map and consuming +// the STREAM_START event. +func (c *Composer) init() { + if c.doneInit { + return + } + c.anchors = make(map[string]*Node) + // Peek to get the encoding from STREAM_START_EVENT + if c.peek() == STREAM_START_EVENT { + c.encoding = c.event.GetEncoding() + } + c.expect(STREAM_START_EVENT) + c.doneInit = true + + // If stream nodes are enabled, prepare to return the first stream node + if c.streamNodes { + c.returnStream = true + } +} + +// Destroy cleans up the composer by deleting any pending event and the +// underlying parser. +func (c *Composer) Destroy() { + if c.event.Type != NO_EVENT { + c.event.Delete() + } + c.Parser.Delete() +} + +// SetStreamNodes enables or disables stream node emission. +func (c *Composer) SetStreamNodes(enable bool) { + c.streamNodes = enable +} + +// expect consumes an event from the event stream and +// checks that it's of the expected type. +func (c *Composer) expect(e EventType) { + if c.event.Type == NO_EVENT { + if err := c.Parser.Parse(&c.event); err != nil { + c.fail(err) + } + } + if c.event.Type == STREAM_END_EVENT { + Fail(formatComposerError( + "attempted to go past the end of stream; corrupted value?", + Mark{Line: c.event.StartMark.Line, Column: c.event.StartMark.Column}, + )) + } + if c.event.Type != e { + Fail(formatComposerError( + fmt.Sprintf("expected %s event but got %s", e, c.event.Type), + Mark{Line: c.event.StartMark.Line, Column: c.event.StartMark.Column}, + )) + } + c.event.Delete() + c.event.Type = NO_EVENT +} + +// peek peeks at the next event in the event stream, +// puts the results into c.event and returns the event type. +func (c *Composer) peek() EventType { + if c.event.Type != NO_EVENT { + return c.event.Type + } + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if err := c.Parser.Parse(&c.event); err != nil { + c.fail(err) + } + return c.event.Type +} + +// fail panics with the given error. +func (c *Composer) fail(err error) { + Fail(err) +} + +// anchor sets the anchor name on a node and records it in the anchor map. +func (c *Composer) anchor(n *Node, anchor []byte) { + if anchor != nil { + n.Anchor = string(anchor) + c.anchors[n.Anchor] = n + } +} + +// parseChild composes the next node and adds it as a child to the parent. +func (c *Composer) parseChild(parent *Node) *Node { + child := c.Compose() + parent.Content = append(parent.Content, child) + return child +} + +// captureDirectives captures version and tag directives from upcoming +// DOCUMENT_START. +// The node n must have Stream initialized (as created by createStreamNode). +func (c *Composer) captureDirectives(n *Node) { + if c.peek() == DOCUMENT_START_EVENT { + if vd := c.event.GetVersionDirective(); vd != nil { + n.Stream.Version = &StreamVersionDirective{ + Major: vd.Major(), + Minor: vd.Minor(), + } + } + if tds := c.event.GetTagDirectives(); len(tds) > 0 { + n.Stream.TagDirectives = make([]StreamTagDirective, len(tds)) + for i, td := range tds { + n.Stream.TagDirectives[i] = StreamTagDirective{ + Handle: td.GetHandle(), + Prefix: td.GetPrefix(), + } + } + } + } +} + +// Fail panics with a YAMLError wrapping the given error. func Fail(err error) { panic(&YAMLError{err}) } +// failf panics with a YAMLError containing a formatted error message. func failf(format string, args ...any) { panic(&YAMLError{fmt.Errorf("yaml: "+format, args...)}) } + +// formatComposerError creates a LoadError for composer-stage errors. +func formatComposerError(message string, mark Mark) *LoadError { + return &LoadError{ + Stage: ComposerStage, + Mark: mark, + Message: message, + } +} + +// formatComposerErrorContext creates a LoadError with both context and +// problem information for composer-stage errors. +func formatComposerErrorContext(context string, contextMark Mark, message string, mark Mark) *LoadError { + return &LoadError{ + Stage: ComposerStage, + ContextMark: contextMark, + ContextMsg: context, + Mark: mark, + Message: message, + } +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/constructor.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/constructor.go index 66f209a59..1d96e914a 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/constructor.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/constructor.go @@ -10,303 +10,40 @@ package libyaml import ( "encoding" "encoding/base64" - "errors" "fmt" "math" "reflect" - "strings" - "sync" "time" ) // -------------------------------------------------------------------------- -// Interfaces and types needed by constructor +// Types and Interfaces -// constructor interface may be implemented by types to customize their -// behavior when being constructed from a YAML document. -type constructor interface { - UnmarshalYAML(value *Node) error -} - -type obsoleteConstructor interface { +// legacyConstructor is the old-style unmarshaler interface. +// It's kept for backwards compatibility. +type legacyConstructor interface { UnmarshalYAML(construct func(any) error) error } -// Marshaler interface may be implemented by types to customize their -// behavior when being marshaled into a YAML document. -type Marshaler interface { - MarshalYAML() (any, error) -} - -// IsZeroer is used to check whether an object is zero to determine whether -// it should be omitted when marshaling with the ,omitempty flag. One notable -// implementation is time.Time. -type IsZeroer interface { - IsZero() bool -} - -// handleErr recovers from panics caused by yaml errors -func handleErr(err *error) { - if v := recover(); v != nil { - if e, ok := v.(*YAMLError); ok { - *err = e.Err - } else { - panic(v) - } - } -} - -// -------------------------------------------------------------------------- -// Struct field information - -type structInfo struct { - FieldsMap map[string]fieldInfo - FieldsList []fieldInfo - - // InlineMap is the number of the field in the struct that - // contains an ,inline map, or -1 if there's none. - InlineMap int - - // InlineConstructors holds indexes to inlined fields that - // contain constructor values. - InlineConstructors [][]int -} - -type fieldInfo struct { - Key string - Num int - OmitEmpty bool - Flow bool - // Id holds the unique field identifier, so we can cheaply - // check for field duplicates without maintaining an extra map. - Id int - - // Inline holds the field index if the field is part of an inlined struct. - Inline []int -} - -var ( - structMap = make(map[reflect.Type]*structInfo) - fieldMapMutex sync.RWMutex - constructorType reflect.Type -) - -func init() { - var v constructor - constructorType = reflect.ValueOf(&v).Elem().Type() -} - -// hasConstructYAMLMethod checks if a type has an UnmarshalYAML method -// that looks like it implements yaml.Unmarshaler (from root package). -// This is needed because we can't directly check for the interface type -// since it's in a different package that we can't import. -func hasConstructYAMLMethod(t reflect.Type) bool { - method, found := t.MethodByName("UnmarshalYAML") - if !found { - return false - } - - // Check signature: func(*T) UnmarshalYAML(*Node) error - mtype := method.Type - if mtype.NumIn() != 2 || mtype.NumOut() != 1 { - return false - } - - // First param is receiver (already checked by MethodByName) - // Second param should be a pointer to a Node-like struct - paramType := mtype.In(1) - if paramType.Kind() != reflect.Ptr { - return false - } - - elemType := paramType.Elem() - if elemType.Kind() != reflect.Struct || elemType.Name() != "Node" { - return false - } - - // Return type should be error - retType := mtype.Out(0) - if retType.Kind() != reflect.Interface || retType.Name() != "error" { - return false - } - - return true -} - -func getStructInfo(st reflect.Type) (*structInfo, error) { - fieldMapMutex.RLock() - sinfo, found := structMap[st] - fieldMapMutex.RUnlock() - if found { - return sinfo, nil - } - - n := st.NumField() - fieldsMap := make(map[string]fieldInfo) - fieldsList := make([]fieldInfo, 0, n) - inlineMap := -1 - inlineConstructors := [][]int(nil) - for i := 0; i != n; i++ { - field := st.Field(i) - if field.PkgPath != "" && !field.Anonymous { - continue // Private field - } - - info := fieldInfo{Num: i} - - tag := field.Tag.Get("yaml") - if tag == "" && !strings.Contains(string(field.Tag), ":") { - tag = string(field.Tag) - } - if tag == "-" { - continue - } - - inline := false - fields := strings.Split(tag, ",") - if len(fields) > 1 { - for _, flag := range fields[1:] { - switch flag { - case "omitempty": - info.OmitEmpty = true - case "flow": - info.Flow = true - case "inline": - inline = true - default: - return nil, fmt.Errorf("unsupported flag %q in tag %q of type %s", flag, tag, st) - } - } - tag = fields[0] - } - - if inline { - switch field.Type.Kind() { - case reflect.Map: - if inlineMap >= 0 { - return nil, errors.New("multiple ,inline maps in struct " + st.String()) - } - if field.Type.Key() != reflect.TypeOf("") { - return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) - } - inlineMap = info.Num - case reflect.Struct, reflect.Pointer: - ftype := field.Type - for ftype.Kind() == reflect.Pointer { - ftype = ftype.Elem() - } - if ftype.Kind() != reflect.Struct { - return nil, errors.New("option ,inline may only be used on a struct or map field") - } - // Check for both libyaml.constructor and yaml.Unmarshaler (by method name) - if reflect.PointerTo(ftype).Implements(constructorType) || hasConstructYAMLMethod(reflect.PointerTo(ftype)) { - inlineConstructors = append(inlineConstructors, []int{i}) - } else { - sinfo, err := getStructInfo(ftype) - if err != nil { - return nil, err - } - for _, index := range sinfo.InlineConstructors { - inlineConstructors = append(inlineConstructors, append([]int{i}, index...)) - } - for _, finfo := range sinfo.FieldsList { - if _, found := fieldsMap[finfo.Key]; found { - msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() - return nil, errors.New(msg) - } - if finfo.Inline == nil { - finfo.Inline = []int{i, finfo.Num} - } else { - finfo.Inline = append([]int{i}, finfo.Inline...) - } - finfo.Id = len(fieldsList) - fieldsMap[finfo.Key] = finfo - fieldsList = append(fieldsList, finfo) - } - } - default: - return nil, errors.New("option ,inline may only be used on a struct or map field") - } - continue - } - - if tag != "" { - info.Key = tag - } else { - info.Key = strings.ToLower(field.Name) - } - - if _, found = fieldsMap[info.Key]; found { - msg := "duplicated key '" + info.Key + "' in struct " + st.String() - return nil, errors.New(msg) - } - - info.Id = len(fieldsList) - fieldsList = append(fieldsList, info) - fieldsMap[info.Key] = info - } - - sinfo = &structInfo{ - FieldsMap: fieldsMap, - FieldsList: fieldsList, - InlineMap: inlineMap, - InlineConstructors: inlineConstructors, - } - - fieldMapMutex.Lock() - structMap[st] = sinfo - fieldMapMutex.Unlock() - return sinfo, nil +// constructorAdapter is an interface that wraps the root package's Unmarshaler +// interface. +// This allows the constructor to call constructors that expect *yaml.Node +// instead of *libyaml.Node. +type constructorAdapter interface { + CallRootConstructor(n *Node) error } -// isZero reports whether v represents the zero value for its type. -// If v implements the IsZeroer interface, IsZero() is called. -// Otherwise, zero is determined by checking type-specific conditions. -// This is used to determine omitempty behavior when marshaling. -func isZero(v reflect.Value) bool { - kind := v.Kind() - if z, ok := v.Interface().(IsZeroer); ok { - if (kind == reflect.Pointer || kind == reflect.Interface) && v.IsNil() { - return true - } - return z.IsZero() - } - switch kind { - case reflect.String: - return len(v.String()) == 0 - case reflect.Interface, reflect.Pointer: - return v.IsNil() - case reflect.Slice: - return v.Len() == 0 - case reflect.Map: - return v.Len() == 0 - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Struct: - vt := v.Type() - for i := v.NumField() - 1; i >= 0; i-- { - if vt.Field(i).PkgPath != "" { - continue // Private field - } - if !isZero(v.Field(i)) { - return false - } - } - return true - } - return false -} +// ScalarConstructFunc is the signature for tag-specific scalar constructor +// functions. +// Each function handles construction of a specific YAML tag to various Go +// types. +type ScalarConstructFunc func(c *Constructor, n *Node, resolved any, out reflect.Value) bool +// Constructor state type Constructor struct { doc *Node aliases map[*Node]bool - TypeErrors []*ConstructError + TypeErrors []*LoadError stringMapType reflect.Type generalMapType reflect.Type @@ -316,18 +53,13 @@ type Constructor struct { constructCount int aliasCount int aliasDepth int + aliasCheck func(aliasCount, constructCount int) error mergedFields map[any]bool } -var ( - nodeType = reflect.TypeOf(Node{}) - durationType = reflect.TypeOf(time.Duration(0)) - stringMapType = reflect.TypeOf(map[string]any{}) - generalMapType = reflect.TypeOf(map[any]any{}) - ifaceType = generalMapType.Elem() -) - +// NewConstructor creates a new Constructor initialized with the provided +// options. func NewConstructor(opts *Options) *Constructor { return &Constructor{ stringMapType: stringMapType, @@ -335,434 +67,143 @@ func NewConstructor(opts *Options) *Constructor { KnownFields: opts.KnownFields, UniqueKeys: opts.UniqueKeys, aliases: make(map[*Node]bool), + aliasCheck: opts.AliasCheck, } } -// Construct decodes YAML input into the provided output value. -// The out parameter must be a pointer to the value to decode into. -// Returns a [LoadErrors] if type mismatches occur during decoding. -func Construct(in []byte, out any, opts *Options) error { - d := NewConstructor(opts) - p := NewComposer(in) - defer p.Destroy() - node := p.Parse() - if node != nil { - v := reflect.ValueOf(out) - if v.Kind() == reflect.Pointer && !v.IsNil() { - v = v.Elem() - } - d.Construct(node, v) - } - if len(d.TypeErrors) > 0 { - return &LoadErrors{Errors: d.TypeErrors} - } - return nil -} - -func (c *Constructor) tagError(n *Node, tag string, out reflect.Value) { - if n.Tag != "" { - tag = n.Tag - } - value := n.Value - if tag != seqTag && tag != mapTag { - if len(value) > 10 { - value = " `" + value[:7] + "...`" - } else { - value = " `" + value + "`" - } - } - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: fmt.Errorf("cannot construct %s%s into %s", shortTag(tag), value, out.Type()), - Line: n.Line, - Column: n.Column, - }) -} - -func (c *Constructor) callConstructor(n *Node, u constructor) (good bool) { - err := u.UnmarshalYAML(n) - switch e := err.(type) { - case nil: - return true - case *LoadErrors: - c.TypeErrors = append(c.TypeErrors, e.Errors...) - return false - default: - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: err, - Line: n.Line, - Column: n.Column, - }) - return false - } -} - -func (c *Constructor) callObsoleteConstructor(n *Node, u obsoleteConstructor) (good bool) { - terrlen := len(c.TypeErrors) - err := u.UnmarshalYAML(func(v any) (err error) { - defer handleErr(&err) - c.Construct(n, reflect.ValueOf(v)) - if len(c.TypeErrors) > terrlen { - issues := c.TypeErrors[terrlen:] - c.TypeErrors = c.TypeErrors[:terrlen] - return &LoadErrors{issues} - } - return nil - }) - switch e := err.(type) { - case nil: - return true - case *LoadErrors: - c.TypeErrors = append(c.TypeErrors, e.Errors...) - return false - default: - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: err, - Line: n.Line, - Column: n.Column, - }) - return false - } -} - -func isTextUnmarshaler(out reflect.Value) bool { - // Dereference pointers to check the underlying type, - // similar to how prepare() handles Constructor checks. - for out.Kind() == reflect.Pointer { - if out.IsNil() { - // Create a new instance to check the type - out = reflect.New(out.Type().Elem()).Elem() - } else { - out = out.Elem() - } - } - if out.CanAddr() { - _, ok := out.Addr().Interface().(encoding.TextUnmarshaler) - return ok - } - return false -} - -// prepare initializes and dereferences pointers and calls UnmarshalYAML -// if a value is found to implement it. -// It returns the initialized and dereferenced out value, whether -// construction was already done by UnmarshalYAML, and if so whether -// its types constructed appropriately. -// -// If n holds a null value, prepare returns before doing anything. -func (c *Constructor) prepare(n *Node, out reflect.Value) (newout reflect.Value, constructed, good bool) { - if n.ShortTag() == nullTag { - return out, false, false - } - again := true - for again { - again = false - if out.Kind() == reflect.Pointer { - if out.IsNil() { - out.Set(reflect.New(out.Type().Elem())) - } - out = out.Elem() - again = true - } - if out.CanAddr() { - // Try yaml.Unmarshaler (from root package) first - if called, good := c.tryCallYAMLConstructor(n, out); called { - return out, true, good - } - - outi := out.Addr().Interface() - // Check for libyaml.constructor - if u, ok := outi.(constructor); ok { - good = c.callConstructor(n, u) - return out, true, good - } - if u, ok := outi.(obsoleteConstructor); ok { - good = c.callObsoleteConstructor(n, u) - return out, true, good - } - } - } - return out, false, false -} - -func (c *Constructor) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) { - if n.ShortTag() == nullTag { - return reflect.Value{} - } - for _, num := range index { - for { - if v.Kind() == reflect.Pointer { - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - v = v.Elem() - continue - } - break - } - v = v.Field(num) - } - return v -} - -const ( - // 400,000 decode operations is ~500kb of dense object declarations, or - // ~5kb of dense object declarations with 10000% alias expansion - alias_ratio_range_low = 400000 - - // 4,000,000 decode operations is ~5MB of dense object declarations, or - // ~4.5MB of dense object declarations with 10% alias expansion - alias_ratio_range_high = 4000000 - - // alias_ratio_range is the range over which we scale allowed alias ratios - alias_ratio_range = float64(alias_ratio_range_high - alias_ratio_range_low) -) - -func allowedAliasRatio(constructCount int) float64 { - switch { - case constructCount <= alias_ratio_range_low: - // allow 99% to come from alias expansion for small-to-medium documents - return 0.99 - case constructCount >= alias_ratio_range_high: - // allow 10% to come from alias expansion for very large documents - return 0.10 - default: - // scale smoothly from 99% down to 10% over the range. - // this maps to 396,000 - 400,000 allowed alias-driven decodes over the range. - // 400,000 decode operations is ~100MB of allocations in worst-case scenarios (single-item maps). - return 0.99 - 0.89*(float64(constructCount-alias_ratio_range_low)/alias_ratio_range) - } -} - -// constructorAdapter is an interface that wraps the root package's Unmarshaler interface. -// This allows the constructor to call constructors that expect *yaml.Node instead of *libyaml.Node. -type constructorAdapter interface { - CallRootConstructor(n *Node) error -} - -// tryCallYAMLConstructor checks if the value has an UnmarshalYAML method that takes -// a *yaml.Node (from the root package) and calls it if found. -// This handles the case where user types implement yaml.Unmarshaler instead of libyaml.constructor. -func (c *Constructor) tryCallYAMLConstructor(n *Node, out reflect.Value) (called bool, good bool) { - if !out.CanAddr() { - return false, false - } - - addr := out.Addr() - // Check for UnmarshalYAML method - method := addr.MethodByName("UnmarshalYAML") - if !method.IsValid() { - return false, false - } - - // Check method signature: func(*yaml.Node) error - mtype := method.Type() - if mtype.NumIn() != 1 || mtype.NumOut() != 1 { - return false, false - } - - // Check if parameter is a pointer to a Node-like struct - paramType := mtype.In(0) - if paramType.Kind() != reflect.Ptr { - return false, false - } - - elemType := paramType.Elem() - if elemType.Kind() != reflect.Struct { - return false, false - } - - // Check if it's the same underlying type as our Node - // Both yaml.Node and libyaml.Node have the same structure - if elemType.Name() != "Node" { - return false, false - } - - // Call the method with a converted node - // Since yaml.Node and libyaml.Node have the same structure, - // we can convert using unsafe pointer cast - nodeValue := reflect.NewAt(elemType, reflect.ValueOf(n).UnsafePointer()) - - results := method.Call([]reflect.Value{nodeValue}) - err := results[0].Interface() - - if err == nil { - return true, true - } - - switch e := err.(type) { - case *LoadErrors: - c.TypeErrors = append(c.TypeErrors, e.Errors...) - return true, false - default: - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: e.(error), - Line: n.Line, - Column: n.Column, - }) - return true, false - } -} +// -------------------------------------------------------------------------- +// Main Entry Point +// Construct converts a YAML node into the Go value represented by out. +// It dispatches to the appropriate handler based on the node kind and +// handles alias expansion, custom unmarshalers, and type resolution. +// Returns true if the construction was successful. func (c *Constructor) Construct(n *Node, out reflect.Value) (good bool) { c.constructCount++ if c.aliasDepth > 0 { c.aliasCount++ } - if c.aliasCount > 100 && c.constructCount > 1000 && float64(c.aliasCount)/float64(c.constructCount) > allowedAliasRatio(c.constructCount) { - failf("document contains excessive aliasing") + if c.aliasCheck != nil { + if err := c.aliasCheck(c.aliasCount, c.constructCount); err != nil { + Fail(formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) + } } if out.Type() == nodeType { out.Set(reflect.ValueOf(n).Elem()) return true } - // When out type implements [encoding.TextUnmarshaler], ensure the node is - // a scalar. Otherwise, for example, constructing a YAML mapping into - // a struct having no exported fields, but implementing TextUnmarshaler - // would silently succeed, but do nothing. - // - // Note that this matches the behavior of both encoding/json and encoding/json/v2. - if n.Kind != ScalarNode && isTextUnmarshaler(out) { - err := fmt.Errorf("cannot construct %s into %s (TextUnmarshaler)", shortTag(n.Tag), out.Type()) - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: err, - Line: n.Line, - Column: n.Column, - }) - return false - } switch n.Kind { case DocumentNode: return c.document(n, out) - case AliasNode: - return c.alias(n, out) - } - out, constructed, good := c.prepare(n, out) - if constructed { - return good - } - switch n.Kind { - case ScalarNode: - good = c.scalar(n, out) - case MappingNode: - good = c.mapping(n, out) - case SequenceNode: - good = c.sequence(n, out) - case 0: - if n.IsZero() { - return c.null(out) - } - fallthrough - default: - failf("cannot construct node with unknown kind %d", n.Kind) - } - return good -} - -func (c *Constructor) document(n *Node, out reflect.Value) (good bool) { - if len(n.Content) == 1 { - c.doc = n - c.Construct(n.Content[0], out) - return true - } - return false -} - -func (c *Constructor) alias(n *Node, out reflect.Value) (good bool) { - if c.aliases[n] { - // TODO this could actually be allowed in some circumstances. - failf("anchor '%s' value contains itself", n.Value) - } - c.aliases[n] = true - c.aliasDepth++ - good = c.Construct(n.Alias, out) - c.aliasDepth-- - delete(c.aliases, n) - return good -} - -func (c *Constructor) null(out reflect.Value) bool { - if out.CanAddr() { - switch out.Kind() { - case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: - out.Set(reflect.Zero(out.Type())) - return true - } - } - return false -} - -func (c *Constructor) scalar(n *Node, out reflect.Value) bool { - var tag string - var resolved any - if n.indicatedString() { - tag = strTag - resolved = n.Value - } else { - tag, resolved = resolve(n.Tag, n.Value) - if tag == binaryTag { - data, err := base64.StdEncoding.DecodeString(resolved.(string)) - if err != nil { - failf("!!binary value contains invalid base64 data") - } - resolved = string(data) - } + case AliasNode: + return c.alias(n, out) } - if resolved == nil { - return c.null(out) + + out, constructed, good := c.prepare(n, out) + if constructed { + return good } - if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { - // We've resolved to exactly the type we want, so use that. - out.Set(resolvedv) - return true + + // When out type implements [encoding.TextUnmarshaler], ensure the node + // is a scalar. Otherwise, for example, constructing a YAML mapping + // into a struct having no exported fields, but implementing + // TextUnmarshaler would silently succeed, but do nothing. + // + // Note that this matches the behavior of both encoding/json and + // encoding/json/v2. + if n.Kind != ScalarNode && isTextUnmarshaler(out) { + err := fmt.Errorf("cannot construct %s into %s (TextUnmarshaler)", shortTag(n.Tag), out.Type()) + c.TypeErrors = append(c.TypeErrors, + formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) + return false } - // Perhaps we can use the value as a TextUnmarshaler to - // set its value. - if out.CanAddr() { - u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) - if ok { - var text []byte - if tag == binaryTag { - text = []byte(resolved.(string)) - } else { - // We let any value be constructed into TextUnmarshaler. - // That might be more lax than we'd like, but the - // TextUnmarshaler itself should bowl out any dubious values. - text = []byte(n.Value) - } - err := u.UnmarshalText(text) - if err != nil { - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: err, - Line: n.Line, - Column: n.Column, - }) - return false - } - return true + + switch n.Kind { + case ScalarNode: + good = c.scalar(n, out) + case MappingNode: + good = c.mapping(n, out) + case SequenceNode: + good = c.sequence(n, out) + case 0: + if n.IsZero() { + return c.null(out) } + fallthrough + default: + Fail(formatConstructorError( + fmt.Errorf("cannot construct node with unknown kind: '%d'", n.Kind), + Mark{Line: n.Line, Column: n.Column}, + )) } + return good +} + +// -------------------------------------------------------------------------- +// Package-level Variables and Constants +var ( + nodeType = reflect.TypeOf(Node{}) + durationType = reflect.TypeOf(time.Duration(0)) + stringMapType = reflect.TypeOf(map[string]any{}) + generalMapType = reflect.TypeOf(map[any]any{}) + ifaceType = generalMapType.Elem() +) + +// scalarConstructors maps YAML scalar tags to their constructor functions. +var scalarConstructors = map[string]ScalarConstructFunc{ + strTag: (*Constructor).constructStr, + intTag: (*Constructor).constructInt, + boolTag: (*Constructor).constructBool, + floatTag: (*Constructor).constructFloat, + nullTag: (*Constructor).constructNull, + timestampTag: (*Constructor).constructTimestamp, + binaryTag: (*Constructor).constructBinary, + mergeTag: (*Constructor).constructMerge, +} + +// -------------------------------------------------------------------------- +// Scalar tag constructors + +// constructStr constructs a !!str tagged value into various Go types. +func (c *Constructor) constructStr(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.String: - if tag == binaryTag { - out.SetString(resolved.(string)) - return true - } out.SetString(n.Value) return true - case reflect.Slice: - // allow decoding !!binary-tagged value into []byte specifically - if out.Type().Elem().Kind() == reflect.Uint8 { - if tag == binaryTag { - out.SetBytes([]byte(resolved.(string))) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // Handle time.Duration parsing from strings like "3s", "1m", + // etc. + if out.Type() == durationType { + d, err := time.ParseDuration(n.Value) + if err == nil { + out.SetInt(int64(d)) return true } } + case reflect.Bool: + // YAML 1.1 compatibility: allow string values like "y", "on", + // "Off" as bools + switch n.Value { + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": + out.SetBool(true) + return true + case "n", "N", "no", "No", "NO", "off", "Off", "OFF": + out.SetBool(false) + return true + } case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true + } + c.tagError(n, strTag, out) + return false +} + +// constructInt constructs a !!int tagged value into various Go types. +func (c *Constructor) constructInt(n *Node, resolved any, out reflect.Value) bool { + switch out.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - // This used to work in v2, but it's very unfriendly. isDuration := out.Type() == durationType switch resolved := resolved.(type) { @@ -790,7 +231,8 @@ func (c *Constructor) scalar(n *Node, out reflect.Value) bool { case float64: if !isDuration && resolved >= math.MinInt64 && resolved <= math.MaxInt64 { intVal := int64(resolved) - // Verify conversion is lossless (handles floating-point precision) + // Verify conversion is lossless (handles + // floating-point precision) if float64(intVal) == resolved && !out.OverflowInt(intVal) { out.SetInt(intVal) return true @@ -825,21 +267,52 @@ func (c *Constructor) scalar(n *Node, out reflect.Value) bool { case float64: if resolved >= 0 && resolved <= math.MaxUint64 { uintVal := uint64(resolved) - // Verify conversion is lossless (handles floating-point precision) + // Verify conversion is lossless (handles + // floating-point precision) if float64(uintVal) == resolved && !out.OverflowUint(uintVal) { out.SetUint(uintVal) return true } } } + case reflect.Float32, reflect.Float64: + // Allow int to float conversion + switch resolved := resolved.(type) { + case int: + out.SetFloat(float64(resolved)) + return true + case int64: + out.SetFloat(float64(resolved)) + return true + case uint64: + out.SetFloat(float64(resolved)) + return true + } + case reflect.String: + // Allow int to string conversion + out.SetString(n.Value) + return true + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + } + c.tagError(n, intTag, out) + return false +} + +// constructBool constructs a !!bool tagged value into various Go types. +func (c *Constructor) constructBool(n *Node, resolved any, out reflect.Value) bool { + switch out.Kind() { case reflect.Bool: switch resolved := resolved.(type) { case bool: out.SetBool(resolved) return true case string: - // This offers some compatibility with the 1.1 spec (https://yaml.org/type/bool.html). - // It only works if explicitly attempting to construct into a typed bool value. + // This offers some compatibility with the 1.1 spec + // (https://yaml.org/type/bool.html). + // It only works if explicitly attempting to construct + // into a typed bool value. switch resolved { case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": out.SetBool(true) @@ -849,6 +322,21 @@ func (c *Constructor) scalar(n *Node, out reflect.Value) bool { return true } } + case reflect.String: + // Allow bool to be constructed as string (e.g., true -> "true") + out.SetString(n.Value) + return true + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + } + c.tagError(n, boolTag, out) + return false +} + +// constructFloat constructs a !!float tagged value into various Go types. +func (c *Constructor) constructFloat(n *Node, resolved any, out reflect.Value) bool { + switch out.Kind() { case reflect.Float32, reflect.Float64: switch resolved := resolved.(type) { case int: @@ -864,25 +352,198 @@ func (c *Constructor) scalar(n *Node, out reflect.Value) bool { out.SetFloat(resolved) return true } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // Allow float to int conversion if lossless + if fval, ok := resolved.(float64); ok { + if fval >= math.MinInt64 && fval <= math.MaxInt64 { + intVal := int64(fval) + if float64(intVal) == fval && !out.OverflowInt(intVal) { + out.SetInt(intVal) + return true + } + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + // Allow float to uint conversion if lossless + if fval, ok := resolved.(float64); ok { + if fval >= 0 && fval <= math.MaxUint64 { + uintVal := uint64(fval) + if float64(uintVal) == fval && !out.OverflowUint(uintVal) { + out.SetUint(uintVal) + return true + } + } + } + case reflect.String: + out.SetString(n.Value) + return true + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + } + c.tagError(n, floatTag, out) + return false +} + +// constructTimestamp constructs a !!timestamp tagged value into various Go +// types. +func (c *Constructor) constructTimestamp(n *Node, resolved any, out reflect.Value) bool { + switch out.Kind() { case reflect.Struct: if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { out.Set(resolvedv) return true } - case reflect.Pointer: - panic("yaml internal error: please report the issue") + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true } - c.tagError(n, tag, out) + c.tagError(n, timestampTag, out) return false } -func settableValueOf(i any) reflect.Value { - v := reflect.ValueOf(i) - sv := reflect.New(v.Type()).Elem() - sv.Set(v) - return sv +// constructBinary constructs a !!binary tagged value into various Go types. +func (c *Constructor) constructBinary(n *Node, resolved any, out reflect.Value) bool { + switch out.Kind() { + case reflect.String: + out.SetString(resolved.(string)) + return true + case reflect.Slice: + // allow decoding !!binary-tagged value into []byte specifically + if out.Type().Elem().Kind() == reflect.Uint8 { + out.SetBytes([]byte(resolved.(string))) + return true + } + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + } + c.tagError(n, binaryTag, out) + return false +} + +// constructNull constructs a !!null tagged value into various Go types. +func (c *Constructor) constructNull(n *Node, resolved any, out reflect.Value) bool { + return c.null(out) +} + +// constructMerge handles !!merge tagged keys. +// Merge keys are directives, not values, so construction always fails. +// They are handled specially by the mapping() function. +func (c *Constructor) constructMerge(n *Node, resolved any, out reflect.Value) bool { + return false +} + +// -------------------------------------------------------------------------- +// Node Kind Handlers + +// document constructs a DocumentNode by processing its single content node. +func (c *Constructor) document(n *Node, out reflect.Value) (good bool) { + if len(n.Content) == 1 { + c.doc = n + c.Construct(n.Content[0], out) + return true + } + return false +} + +// alias constructs an AliasNode by following the alias reference and +// tracking alias depth to detect circular references. +func (c *Constructor) alias(n *Node, out reflect.Value) (good bool) { + if c.aliases[n] { + // TODO this could actually be allowed in some circumstances. + Fail(formatComposerError( + fmt.Sprintf("anchor '%s' value contains itself", n.Value), + Mark{Line: n.Line, Column: n.Column}, + )) + } + c.aliases[n] = true + c.aliasDepth++ + good = c.Construct(n.Alias, out) + c.aliasDepth-- + delete(c.aliases, n) + return good +} + +// scalar constructs a ScalarNode by resolving its tag and value, then +// dispatching to the appropriate tag-specific constructor or using +// TextUnmarshaler if available. +func (c *Constructor) scalar(n *Node, out reflect.Value) bool { + // Resolve the tag and value + var tag string + var resolved any + if n.indicatedString() { + tag = strTag + resolved = n.Value + } else { + tag, resolved = resolve(n.Tag, n.Value) + if tag == binaryTag { + data, err := base64.StdEncoding.DecodeString(resolved.(string)) + if err != nil { + Fail(formatConstructorError( + fmt.Errorf("!!binary value contains invalid base64 data"), + Mark{Line: n.Line, Column: n.Column}, + )) + } + resolved = string(data) + } + } + + // Handle null + if resolved == nil { + return c.null(out) + } + + // Fast path: exact type match + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + out.Set(resolvedv) + return true + } + + // Handle TextUnmarshaler interface + if out.CanAddr() { + u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) + if ok { + var text []byte + if tag == binaryTag { + text = []byte(resolved.(string)) + } else { + text = []byte(n.Value) + } + err := u.UnmarshalText(text) + if err != nil { + c.TypeErrors = append(c.TypeErrors, formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) + return false + } + return true + } + } + + // Dispatch to tag-specific constructor + if constructor, ok := scalarConstructors[tag]; ok { + return constructor(c, n, resolved, out) + } + + // Unknown tag - try some fallback behaviors + switch out.Kind() { + case reflect.Interface: + // For interface{} targets, accept any resolved value + out.Set(reflect.ValueOf(resolved)) + return true + case reflect.Struct: + // For struct targets with matching types, try direct assignment + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + out.Set(resolvedv) + return true + } + } + + // No constructor and no fallback worked + c.tagError(n, tag, out) + return false } +// sequence constructs a SequenceNode into a Go slice, array, or interface. func (c *Constructor) sequence(n *Node, out reflect.Value) (good bool) { l := len(n.Content) @@ -892,7 +553,10 @@ func (c *Constructor) sequence(n *Node, out reflect.Value) (good bool) { out.Set(reflect.MakeSlice(out.Type(), l, l)) case reflect.Array: if l != out.Len() { - failf("invalid array: want %d elements but got %d", out.Len(), l) + Fail(formatConstructorError( + fmt.Errorf("invalid array: want %d elements but got %d", out.Len(), l), + Mark{Line: n.Line, Column: n.Column}, + )) } case reflect.Interface: // No type hints. Will have to use a generic sequence. @@ -921,6 +585,9 @@ func (c *Constructor) sequence(n *Node, out reflect.Value) (good bool) { return true } +// mapping constructs a MappingNode into a Go map, struct, or interface. +// It handles key uniqueness checking, merge keys, and type-appropriate +// map construction (string-keyed vs general). func (c *Constructor) mapping(n *Node, out reflect.Value) (good bool) { l := len(n.Content) if c.UniqueKeys { @@ -930,11 +597,10 @@ func (c *Constructor) mapping(n *Node, out reflect.Value) (good bool) { for j := i + 2; j < l; j += 2 { nj := n.Content[j] if ni.Kind == nj.Kind && ni.Value == nj.Value { - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: fmt.Errorf("mapping key %#v already defined at line %d", nj.Value, ni.Line), - Line: nj.Line, - Column: nj.Column, - }) + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + fmt.Errorf("mapping key %#v already defined at line %d", nj.Value, ni.Line), + Mark{Line: nj.Line, Column: nj.Column}, + )) } } } @@ -993,17 +659,20 @@ func (c *Constructor) mapping(n *Node, out reflect.Value) (good bool) { if c.Construct(n.Content[i], k) { if mergedFields != nil { ki := k.Interface() - if c.getPossiblyUnhashableKey(mergedFields, ki) { + if c.getPossiblyUnhashableKey(mergedFields, ki, n.Content[i]) { continue } - c.setPossiblyUnhashableKey(mergedFields, ki, true) + c.setPossiblyUnhashableKey(mergedFields, ki, true, n.Content[i]) } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() } if kkind == reflect.Map || kkind == reflect.Slice { - failf("cannot use '%#v' as a map key; try decoding into yaml.Node", k.Interface()) + Fail(formatConstructorError( + fmt.Errorf("cannot use '%#v' as a map key; try decoding into yaml.Node", k.Interface()), + Mark{Line: n.Content[i].Line, Column: n.Content[i].Column}, + )) } e := reflect.New(et).Elem() if c.Construct(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) { @@ -1022,20 +691,12 @@ func (c *Constructor) mapping(n *Node, out reflect.Value) (good bool) { return true } -func isStringMap(n *Node) bool { - if n.Kind != MappingNode { - return false - } - l := len(n.Content) - for i := 0; i < l; i += 2 { - shortTag := n.Content[i].ShortTag() - if shortTag != strTag && shortTag != mergeTag { - return false - } - } - return true -} - +// -------------------------------------------------------------------------- +// Mapping/Struct Support + +// mappingStruct constructs a MappingNode into a struct value. +// It handles field matching by name, inline fields, inline maps, merge keys, +// and enforces known fields and unique keys when configured. func (c *Constructor) mappingStruct(n *Node, out reflect.Value) (good bool) { sinfo, err := getStructInfo(out.Type()) if err != nil { @@ -1082,11 +743,10 @@ func (c *Constructor) mappingStruct(n *Node, out reflect.Value) (good bool) { if info, ok := sinfo.FieldsMap[sname]; ok { if c.UniqueKeys { if doneFields[info.Id] { - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: fmt.Errorf("field %s already set in type %s", name.String(), out.Type()), - Line: ni.Line, - Column: ni.Column, - }) + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + fmt.Errorf("field %s already set in type %s", name.String(), out.Type()), + Mark{Line: ni.Line, Column: ni.Column}, + )) continue } doneFields[info.Id] = true @@ -1106,11 +766,10 @@ func (c *Constructor) mappingStruct(n *Node, out reflect.Value) (good bool) { c.Construct(n.Content[i+1], value) inlineMap.SetMapIndex(name, value) } else if c.KnownFields { - c.TypeErrors = append(c.TypeErrors, &ConstructError{ - Err: fmt.Errorf("field %s not found in type %s", name.String(), out.Type()), - Line: ni.Line, - Column: ni.Column, - }) + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + fmt.Errorf("field %s not found in type %s", name.String(), out.Type()), + Mark{Line: ni.Line, Column: ni.Column}, + )) } } @@ -1121,28 +780,10 @@ func (c *Constructor) mappingStruct(n *Node, out reflect.Value) (good bool) { return true } -func failWantMap() { - failf("map merge requires map or sequence of maps as the value") -} - -func (c *Constructor) setPossiblyUnhashableKey(m map[any]bool, key any, value bool) { - defer func() { - if err := recover(); err != nil { - failf("%v", err) - } - }() - m[key] = value -} - -func (c *Constructor) getPossiblyUnhashableKey(m map[any]bool, key any) bool { - defer func() { - if err := recover(); err != nil { - failf("%v", err) - } - }() - return m[key] -} - +// merge processes a merge key (<<) by constructing the merge value into out. +// The merge value can be a single mapping, an alias to a mapping, or a +// sequence of mappings. +// Fields from the parent mapping take precedence over merged fields. func (c *Constructor) merge(parent *Node, merge *Node, out reflect.Value) { mergedFields := c.mergedFields if mergedFields == nil { @@ -1150,7 +791,7 @@ func (c *Constructor) merge(parent *Node, merge *Node, out reflect.Value) { for i := 0; i < len(parent.Content); i += 2 { k := reflect.New(ifaceType).Elem() if c.Construct(parent.Content[i], k) { - c.setPossiblyUnhashableKey(c.mergedFields, k.Interface(), true) + c.setPossiblyUnhashableKey(c.mergedFields, k.Interface(), true, parent.Content[i]) } } } @@ -1160,7 +801,7 @@ func (c *Constructor) merge(parent *Node, merge *Node, out reflect.Value) { c.Construct(merge, out) case AliasNode: if merge.Alias != nil && merge.Alias.Kind != MappingNode { - failWantMap() + failWantMap(merge.Alias) } c.Construct(merge, out) case SequenceNode: @@ -1168,20 +809,344 @@ func (c *Constructor) merge(parent *Node, merge *Node, out reflect.Value) { ni := merge.Content[i] if ni.Kind == AliasNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode { - failWantMap() + failWantMap(ni.Alias) } } else if ni.Kind != MappingNode { - failWantMap() + failWantMap(ni) } c.Construct(ni, out) } default: - failWantMap() + failWantMap(merge) } c.mergedFields = mergedFields } +// isStringMap checks if a MappingNode has only string or merge keys. +// This determines whether to use map[string]any or map[any]any when +// constructing into an interface{}. +func isStringMap(n *Node) bool { + if n.Kind != MappingNode { + return false + } + l := len(n.Content) + for i := 0; i < l; i += 2 { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { + return false + } + } + return true +} + +// isMerge checks if a node is a merge key (!!merge tag). func isMerge(n *Node) bool { return n.Kind == ScalarNode && shortTag(n.Tag) == mergeTag } + +// failWantMap panics with an error message for invalid merge key values. +func failWantMap(n *Node) { + Fail(formatConstructorError( + fmt.Errorf("map merge requires map or sequence of maps as the value"), + Mark{Line: n.Line, Column: n.Column}, + )) +} + +// -------------------------------------------------------------------------- +// Utility Methods + +// prepare initializes and dereferences pointers and calls UnmarshalYAML +// if a value is found to implement it. +// It returns the initialized and dereferenced out value, whether +// construction was already done by UnmarshalYAML, and if so whether +// its types constructed appropriately. +// +// If n holds a null value, prepare returns before doing anything. +func (c *Constructor) prepare(n *Node, out reflect.Value) (newout reflect.Value, constructed, good bool) { + if n.ShortTag() == nullTag { + return out, false, false + } + again := true + for again { + again = false + if out.Kind() == reflect.Pointer { + if out.IsNil() { + out.Set(reflect.New(out.Type().Elem())) + } + out = out.Elem() + again = true + } + if out.CanAddr() { + // Try yaml.Unmarshaler (from root package) first + if called, good := c.tryCallYAMLConstructor(n, out); called { + return out, true, good + } + + outi := out.Addr().Interface() + // Check for libyaml.constructor + if u, ok := outi.(constructor); ok { + good = c.callConstructor(n, u) + return out, true, good + } + if u, ok := outi.(legacyConstructor); ok { + good = c.callLegacyConstructor(n, u) + return out, true, good + } + } + } + return out, false, false +} + +// fieldByIndex returns the struct field at the given index path, initializing +// any nil pointers along the way. +func (c *Constructor) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) { + if n.ShortTag() == nullTag { + return reflect.Value{} + } + for _, num := range index { + for { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + continue + } + break + } + v = v.Field(num) + } + return v +} + +// tryCallYAMLConstructor checks if the value has an UnmarshalYAML method that +// takes a *Node from an allowlisted v3 yaml package and calls it if found. +// This handles backward compatibility with types that implement the v3 +// yaml.Unmarshaler interface instead of the native libyaml.constructor. +func (c *Constructor) tryCallYAMLConstructor(n *Node, out reflect.Value) (called bool, good bool) { + if !out.CanAddr() { + return false, false + } + + addr := out.Addr() + // Check for UnmarshalYAML method + method := addr.MethodByName("UnmarshalYAML") + if !method.IsValid() { + return false, false + } + + // Check method signature: func(*yaml.Node) error + mtype := method.Type() + if mtype.NumIn() != 1 || mtype.NumOut() != 1 { + return false, false + } + + // Check if parameter is a pointer to a Node-like struct + paramType := mtype.In(0) + if paramType.Kind() != reflect.Ptr { + return false, false + } + + elemType := paramType.Elem() + if elemType.Kind() != reflect.Struct { + return false, false + } + + // Only accept *Node from allowlisted v3 yaml packages whose Node type + // is assumed to have a compatible memory layout with libyaml.Node. + // The unsafe pointer cast below is only safe for these packages. + if elemType.Name() != "Node" || !isYAMLNodePkg(elemType.PkgPath()) { + return false, false + } + + // Return type must be error + retType := mtype.Out(0) + if retType.Kind() != reflect.Interface || retType.Name() != "error" { + return false, false + } + + // Call the method with a converted node. + // The allowlisted v3 packages define their own Node type that is + // assumed to have a compatible memory layout with libyaml.Node. + nodeValue := reflect.NewAt(elemType, reflect.ValueOf(n).UnsafePointer()) + + results := method.Call([]reflect.Value{nodeValue}) + err := results[0].Interface() + + if err == nil { + return true, true + } + + switch e := err.(type) { + case *LoadErrors: + c.TypeErrors = append(c.TypeErrors, e.Errors...) + return true, false + default: + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + err.(error), + Mark{Line: n.Line, Column: n.Column}, + )) + return true, false + } +} + +// callConstructor invokes the UnmarshalYAML method on a value implementing +// the constructor interface, handling errors appropriately. +func (c *Constructor) callConstructor(n *Node, u constructor) (good bool) { + err := u.UnmarshalYAML(n) + switch e := err.(type) { + case nil: + return true + case *LoadErrors: + c.TypeErrors = append(c.TypeErrors, e.Errors...) + return false + default: + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + err, + Mark{Line: n.Line, Column: n.Column}, + )) + return false + } +} + +// callLegacyConstructor invokes the UnmarshalYAML method on a value +// implementing the old-style legacyConstructor interface. +func (c *Constructor) callLegacyConstructor(n *Node, u legacyConstructor) (good bool) { + terrlen := len(c.TypeErrors) + err := u.UnmarshalYAML(func(v any) (err error) { + defer handleErr(&err) + c.Construct(n, reflect.ValueOf(v)) + if len(c.TypeErrors) > terrlen { + issues := c.TypeErrors[terrlen:] + c.TypeErrors = c.TypeErrors[:terrlen] + return &LoadErrors{issues} + } + return nil + }) + switch e := err.(type) { + case nil: + return true + case *LoadErrors: + c.TypeErrors = append(c.TypeErrors, e.Errors...) + return false + default: + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + err, + Mark{Line: n.Line, Column: n.Column}, + )) + return false + } +} + +// tagError records a type construction error indicating that a node with a +// given tag cannot be constructed into the target type. +func (c *Constructor) tagError(n *Node, tag string, out reflect.Value) { + if n.Tag != "" { + tag = n.Tag + } + value := n.Value + if tag != seqTag && tag != mapTag { + if len(value) > 10 { + value = " `" + value[:7] + "...`" + } else { + value = " `" + value + "`" + } + } + c.TypeErrors = append(c.TypeErrors, formatConstructorError( + fmt.Errorf("cannot construct %s%s into %s", shortTag(tag), value, out.Type()), + Mark{Line: n.Line, Column: n.Column}, + )) +} + +// null constructs a null value by setting the target to its zero value. +// Only works for nillable types (interface, pointer, map, slice). +func (c *Constructor) null(out reflect.Value) bool { + if out.CanAddr() { + switch out.Kind() { + case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: + out.Set(reflect.Zero(out.Type())) + return true + } + } + return false +} + +// isTextUnmarshaler checks if a value implements [encoding.TextUnmarshaler]. +// It dereferences pointers to check the underlying type. +func isTextUnmarshaler(out reflect.Value) bool { + // Dereference pointers to check the underlying type, + // similar to how prepare() handles Constructor checks. + for out.Kind() == reflect.Pointer { + if out.IsNil() { + // Create a new instance to check the type + out = reflect.New(out.Type().Elem()).Elem() + } else { + out = out.Elem() + } + } + if out.CanAddr() { + _, ok := out.Addr().Interface().(encoding.TextUnmarshaler) + return ok + } + return false +} + +// settableValueOf returns a settable [reflect.Value] for the given value. +func settableValueOf(i any) reflect.Value { + v := reflect.ValueOf(i) + sv := reflect.New(v.Type()).Elem() + sv.Set(v) + return sv +} + +// setPossiblyUnhashableKey sets a map key, recovering from panics if the key +// type is unhashable. +func (c *Constructor) setPossiblyUnhashableKey(m map[any]bool, key any, value bool, n *Node) { + defer func() { + if err := recover(); err != nil { + Fail(formatConstructorError( + fmt.Errorf("%v", err), + Mark{Line: n.Line, Column: n.Column}, + )) + } + }() + m[key] = value +} + +// getPossiblyUnhashableKey gets a map key value, recovering from panics if +// the key type is unhashable. +func (c *Constructor) getPossiblyUnhashableKey(m map[any]bool, key any, n *Node) bool { + defer func() { + if err := recover(); err != nil { + Fail(formatConstructorError( + fmt.Errorf("%v", err), + Mark{Line: n.Line, Column: n.Column}, + )) + } + }() + return m[key] +} + +// formatConstructorError creates a LoadError for constructor-stage errors. +func formatConstructorError(err error, mark Mark) *LoadError { + return &LoadError{ + Stage: ConstructorStage, + Mark: mark, + Message: err.Error(), + err: err, + } +} + +// formatConstructorErrorContext creates a LoadError with both context and +// problem information for constructor-stage errors. +func formatConstructorErrorContext(context string, contextMark Mark, err error, mark Mark) *LoadError { + return &LoadError{ + Stage: ConstructorStage, + ContextMark: contextMark, + ContextMsg: context, + Mark: mark, + Message: err.Error(), + err: err, + } +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/desolver.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/desolver.go new file mode 100644 index 000000000..9f406eebc --- /dev/null +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/desolver.go @@ -0,0 +1,152 @@ +// Copyright 2025 The go-yaml Project Contributors +// SPDX-License-Identifier: Apache-2.0 + +// Desolver stage: Removes inferable tags from YAML nodes. +// This is the inverse of the Resolver - it walks a tagged node tree and +// removes tags that can be inferred during parsing, producing cleaner YAML +// output without unnecessary type annotations. + +package libyaml + +// Desolver handles tag removal for YAML nodes during serialization. +// It removes tags that would be automatically resolved to the same type +// during parsing, making the output cleaner and more readable. +type Desolver struct { + opts *Options +} + +// NewDesolver creates a new Desolver with the given options. +func NewDesolver(opts *Options) *Desolver { + return &Desolver{opts: opts} +} + +// Desolve walks the node tree and removes tags that can be inferred. +// This is the inverse of Resolver - it takes a fully-tagged node tree +// (from Representer) and removes unnecessary tags to produce clean output. +// +// For scalar nodes: if the value would resolve to the same tag when parsed, +// the tag is removed. For strings that would resolve differently, the tag is +// removed and quoting style is set to preserve the string type. +// +// For collection nodes (maps/sequences): default tags (!!map, !!seq) are +// removed since they're implied by the structure. +func (d *Desolver) Desolve(n *Node) { + if n == nil { + return + } + + switch n.Kind { + case ScalarNode: + d.desolveScalar(n) + case DocumentNode, SequenceNode, MappingNode: + d.desolveCollection(n) + // Recursively desolve children + for _, child := range n.Content { + d.Desolve(child) + } + case AliasNode: + // Alias nodes don't have tags to remove + } +} + +// desolveScalar removes tags from scalar nodes when they can be inferred. +func (d *Desolver) desolveScalar(n *Node) { + // If explicitly tagged by user (TaggedStyle), keep it + if n.Style&TaggedStyle != 0 { + return + } + + // Empty tag means it's already untagged - nothing to do + if n.Tag == "" { + return + } + + stag := shortTag(n.Tag) + + // Check if this is a standard scalar tag that we can potentially remove + isStandardTag := false + switch stag { + case nullTag, boolTag, strTag, intTag, floatTag, timestampTag: + isStandardTag = true + case binaryTag: + // Binary scalars are not implicitly resolvable - never remove. + return + case mergeTag: + // Elide the implicit !!merge tag when the value is the canonical + // merge key marker. The TaggedStyle early-return above already + // preserves !!merge when it was explicit in the source. + if n.Value == "<<" { + n.Tag = "" + } + return + default: + // Custom tag - preserve it + return + } + + // Only process standard tags from here + if !isStandardTag { + return + } + + // What tag would this value resolve to? + rtag, _ := resolve("", n.Value) + + // If resolved tag matches current tag, we can elide the tag + if rtag == stag { + // Tag can be inferred - remove it + n.Tag = "" + } else if stag == strTag { + // This is a string type, but would resolve to something else. + // Remove the tag and force quoting to preserve string type. + n.Tag = "" + // If not already quoted, set quote style based on content + if n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) == 0 { + // Determine quote style based on options or default to single quotes + if d.opts != nil { + // Convert ScalarStyle to Style + switch d.opts.QuotePreference.ScalarStyle() { + case DOUBLE_QUOTED_SCALAR_STYLE: + n.Style |= DoubleQuotedStyle + default: + n.Style |= SingleQuotedStyle + } + } else { + n.Style |= SingleQuotedStyle + } + } + } else if stag == floatTag || stag == intTag { + // For numeric type mismatches (like float64(1) → "1" with !!float tag): + // Elide the tag and let YAML resolve naturally. + // Without the tag, "1" resolves as !!int, which may change the type, + // but that's acceptable for cleaner output (and matches old behavior). + n.Tag = "" + } + // For other standard tags with mismatches, keep the tag to preserve type +} + +// desolveCollection removes default tags from collection nodes. +func (d *Desolver) desolveCollection(n *Node) { + // If explicitly tagged by user, keep it + if n.Style&TaggedStyle != 0 { + return + } + + stag := shortTag(n.Tag) + switch n.Kind { + case MappingNode: + // !!map is the default for mappings - remove it + if stag == mapTag { + n.Tag = "" + } + case SequenceNode: + // !!seq is the default for sequences - remove it + if stag == seqTag { + n.Tag = "" + } + case DocumentNode: + // Documents don't have tags in YAML output + n.Tag = "" + } + // For other tags, keep them - they're explicit type information +} diff --git a/vendor/go.yaml.in/yaml/v4/dumper.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/dumper.go similarity index 61% rename from vendor/go.yaml.in/yaml/v4/dumper.go rename to vendor/go.yaml.in/yaml/v4/internal/libyaml/dumper.go index 318289279..b65e81d7e 100644 --- a/vendor/go.yaml.in/yaml/v4/dumper.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/dumper.go @@ -7,17 +7,42 @@ // - Dump: Encode value(s) to YAML (use WithAll for multi-doc) // - NewDumper: Create a streaming dumper to io.Writer -package yaml +package libyaml import ( "bytes" - "errors" "io" "reflect" - - "go.yaml.in/yaml/v4/internal/libyaml" ) +// A Dumper writes YAML values to an output stream with configurable options. +// It uses a 3-stage pipeline mirroring the Loader: +// 1. Representer: Go values → Tagged Node tree +// 2. Desolver: Remove inferable tags +// 3. Serializer: Node tree → Events → YAML +type Dumper struct { + representer *Representer + desolver *Desolver + serializer *Serializer + options *Options +} + +// NewDumper returns a new Dumper that writes to w with the given options. +// +// The Dumper should be closed after use to flush all data to w. +func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { + o, err := ApplyOptions(opts...) + if err != nil { + return nil, err + } + return &Dumper{ + representer: NewRepresenter(o), // No writer - builds nodes + desolver: NewDesolver(o), + serializer: NewSerializer(w, o), // Writer here - emits YAML + options: o, + }, nil +} + // Dump encodes a value to YAML with the given options. // // By default, Dump encodes a single value as a single YAML document. @@ -34,13 +59,13 @@ import ( func Dump(in any, opts ...Option) (out []byte, err error) { defer handleErr(&err) - o, err := libyaml.ApplyOptions(opts...) + o, err := ApplyOptions(opts...) if err != nil { return nil, err } var buf bytes.Buffer - d, err := NewDumper(&buf, func(opts *libyaml.Options) error { + d, err := NewDumper(&buf, func(opts *Options) error { *opts = *o // Copy options return nil }) @@ -52,9 +77,10 @@ func Dump(in any, opts ...Option) (out []byte, err error) { // Multi-document mode: in must be a slice inVal := reflect.ValueOf(in) if inVal.Kind() != reflect.Slice { - return nil, &LoadErrors{Errors: []*libyaml.ConstructError{{ - Err: errors.New("yaml: WithAllDocuments requires a slice input"), - }}} + return nil, &DumpError{ + Stage: RepresenterStage, + Message: "WithAllDocuments requires a slice input", + } } // Dump each element as a separate document @@ -76,26 +102,6 @@ func Dump(in any, opts ...Option) (out []byte, err error) { return buf.Bytes(), nil } -// A Dumper writes YAML values to an output stream with configurable options. -type Dumper struct { - encoder *libyaml.Representer - opts *libyaml.Options -} - -// NewDumper returns a new Dumper that writes to w with the given options. -// -// The Dumper should be closed after use to flush all data to w. -func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { - o, err := libyaml.ApplyOptions(opts...) - if err != nil { - return nil, err - } - return &Dumper{ - encoder: libyaml.NewRepresenter(w, o), - opts: o, - }, nil -} - // Dump writes the YAML encoding of v to the stream. // // If multiple values are dumped to the stream, the second and subsequent @@ -105,7 +111,16 @@ func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { // values to YAML. func (d *Dumper) Dump(v any) (err error) { defer handleErr(&err) - d.encoder.MarshalDoc("", reflect.ValueOf(v)) + + // Stage 1: Represent - Go values → Tagged Node tree + node := d.representer.Represent("", reflect.ValueOf(v)) + + // Stage 2: Desolve - Remove inferable tags + d.desolver.Desolve(node) + + // Stage 3: Serialize - Node tree → Events → YAML + d.serializer.Serialize(node) + return nil } @@ -113,6 +128,22 @@ func (d *Dumper) Dump(v any) (err error) { // It does not write a stream terminating string "...". func (d *Dumper) Close() (err error) { defer handleErr(&err) - d.encoder.Finish() + d.serializer.Finish() return nil } + +// SetIndent changes the indentation used when encoding. +// This is used by the legacy Encoder.SetIndent() method. +func (d *Dumper) SetIndent(spaces int) { + if spaces < 0 { + failDumpf(SerializerStage, "cannot indent to a negative number of spaces") + } + // Set on serializer's emitter + d.serializer.Emitter.BestIndent = spaces +} + +// SetCompactSeqIndent controls whether '- ' is considered part of the indentation. +// This is used by the legacy Encoder methods. +func (d *Dumper) SetCompactSeqIndent(compact bool) { + d.serializer.Emitter.CompactSequenceIndent = compact +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/emitter.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/emitter.go index 7ea83e89c..e603a196c 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/emitter.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/emitter.go @@ -11,120 +11,158 @@ package libyaml import ( "bytes" "fmt" + "io" ) -// Flush the buffer if needed. -func (emitter *Emitter) flushIfNeeded() error { - if emitter.buffer_pos+5 >= len(emitter.buffer) { - return emitter.flush() - } - return nil -} +// WriteHandler is called when the [Emitter] needs to flush the accumulated +// characters to the output. The handler should write @a size bytes of the +// @a buffer to the output. +// +// @param[in,out] data A pointer to an application data specified by +// yamlEmitter.setOutput(). +// @param[in] buffer The buffer with bytes to be written. +// @param[in] size The size of the buffer. +// +// @returns On success, the handler should return @c 1. If the handler failed, +// the returned value should be @c 0. +type WriteHandler func(emitter *Emitter, buffer []byte) error + +// EmitterState represents the current state of the emitter. +type EmitterState int + +// The emitter states. +const ( + // Expect STREAM-START. + EMIT_STREAM_START_STATE EmitterState = iota + + EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END. + EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END. + EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document. + EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END. + EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence. + EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out + EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence. + EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out + EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping. + EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence. + EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence. + EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping. + EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping. + EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping. + EMIT_END_STATE // Expect nothing. +) -// Put a character to the output buffer. -func (emitter *Emitter) put(value byte) error { - if emitter.buffer_pos+5 >= len(emitter.buffer) { - if err := emitter.flush(); err != nil { - return err - } - } - emitter.buffer[emitter.buffer_pos] = value - emitter.buffer_pos++ - emitter.column++ - return nil -} +// Emitter holds all information about the current state of the emitter. +type Emitter struct { + // Writer stuff -// Put a line break to the output buffer. -func (emitter *Emitter) putLineBreak() error { - if emitter.buffer_pos+5 >= len(emitter.buffer) { - if err := emitter.flush(); err != nil { - return err - } - } - switch emitter.line_break { - case CR_BREAK: - emitter.buffer[emitter.buffer_pos] = '\r' - emitter.buffer_pos += 1 - case LN_BREAK: - emitter.buffer[emitter.buffer_pos] = '\n' - emitter.buffer_pos += 1 - case CRLN_BREAK: - emitter.buffer[emitter.buffer_pos+0] = '\r' - emitter.buffer[emitter.buffer_pos+1] = '\n' - emitter.buffer_pos += 2 - default: - panic("unknown line break setting") - } - if emitter.column == 0 { - emitter.space_above = true + write_handler WriteHandler // Write handler. + + output_buffer *[]byte // String output data. + output_writer io.Writer // File output data. + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + encoding Encoding // The stream encoding. + + // Emitter stuff + + canonical bool // If the output is in the canonical style? + BestIndent int // The number of indentation spaces. + best_width int // The preferred width of the output lines. + unicode bool // Allow unescaped non-ASCII characters? + line_break LineBreak // The preferred line break. + quotePreference QuoteStyle // Preferred quote style when quoting is required. + + state EmitterState // The current emitter state. + states []EmitterState // The stack of states. + + events []Event // The event queue. + events_head int // The head of the event queue. + + indents []int // The stack of indentation levels. + + tag_directives []TagDirective // The list of tag directives. + + indent int // The current indentation level. + + CompactSequenceIndent bool // Is '- ' is considered part of the indentation for sequence elements? + + flow_level int // The current flow level. + + root_context bool // Is it the document root context? + sequence_context bool // Is it a sequence context? + mapping_context bool // Is it a mapping context? + simple_key_context bool // Is it a simple mapping key context? + + line int // The current line. + column int // The current column. + whitespace bool // If the last character was a whitespace? + indention bool // If the last character was an indentation character (' ', '-', '?', ':')? + OpenEnded bool // If an explicit document end is required? + + space_above bool // Is there's an empty line above? + foot_indent int // The indent used to write the foot comment above, or -1 if none. + + // Anchor analysis. + anchor_data struct { + anchor []byte // The anchor value. + alias bool // Is it an alias? } - emitter.column = 0 - emitter.line++ - // [Go] Do this here and below and drop from everywhere else (see commented lines). - emitter.indention = true - return nil -} -// Copy a character from a string into buffer. -func (emitter *Emitter) write(s []byte, i *int) error { - if emitter.buffer_pos+5 >= len(emitter.buffer) { - if err := emitter.flush(); err != nil { - return err - } + // Tag analysis. + tag_data struct { + handle []byte // The tag handle. + suffix []byte // The tag suffix. } - p := emitter.buffer_pos - w := width(s[*i]) - switch w { - case 4: - emitter.buffer[p+3] = s[*i+3] - fallthrough - case 3: - emitter.buffer[p+2] = s[*i+2] - fallthrough - case 2: - emitter.buffer[p+1] = s[*i+1] - fallthrough - case 1: - emitter.buffer[p+0] = s[*i+0] - default: - panic("unknown character width") + + // Scalar analysis. + scalar_data struct { + value []byte // The scalar value. + multiline bool // Does the scalar contain line breaks? + flow_plain_allowed bool // Can the scalar be expressed in the flow plain style? + block_plain_allowed bool // Can the scalar be expressed in the block plain style? + single_quoted_allowed bool // Can the scalar be expressed in the single quoted style? + block_allowed bool // Can the scalar be expressed in the literal or folded styles? + style ScalarStyle // The output style. } - emitter.column++ - emitter.buffer_pos += w - *i += w - return nil -} -// Write a whole string into buffer. -func (emitter *Emitter) writeAll(s []byte) error { - for i := 0; i < len(s); { - if err := emitter.write(s, &i); err != nil { - return err - } + // Comments + HeadComment []byte + LineComment []byte + FootComment []byte + TailComment []byte + + key_line_comment []byte + + // Representer stuff + + opened bool // If the stream was already opened? + closed bool // If the stream was already closed? + + // The information associated with the document nodes. + anchors *struct { + references int // The number of references. + anchor int // The anchor id. + serialized bool // If the node has been emitted? } - return nil + + last_anchor_id int // The last assigned anchor id. } -// Copy a line break character from a string into buffer. -func (emitter *Emitter) writeLineBreak(s []byte, i *int) error { - if s[*i] == '\n' { - if err := emitter.putLineBreak(); err != nil { - return err - } - *i++ - } else { - if err := emitter.write(s, i); err != nil { - return err - } - if emitter.column == 0 { - emitter.space_above = true - } - emitter.column = 0 - emitter.line++ - // [Go] Do this here and above and drop from everywhere else (see commented lines). - emitter.indention = true +// NewEmitter creates a new emitter object. +func NewEmitter() Emitter { + return Emitter{ + buffer: make([]byte, output_buffer_size), + states: make([]EmitterState, 0, initial_stack_size), + events: make([]Event, 0, initial_queue_size), + best_width: -1, } - return nil } // Emit an event. @@ -144,97 +182,79 @@ func (emitter *Emitter) Emit(event *Event) error { return nil } -// Check if we need to accumulate more events before emitting. -// -// We accumulate extra -// - 1 event for DOCUMENT-START -// - 2 events for SEQUENCE-START -// - 3 events for MAPPING-START -func (emitter *Emitter) needMoreEvents() bool { - if emitter.events_head == len(emitter.events) { - return true - } - var accumulate int - switch emitter.events[emitter.events_head].Type { - case DOCUMENT_START_EVENT: - accumulate = 1 - case SEQUENCE_START_EVENT: - accumulate = 2 - case MAPPING_START_EVENT: - accumulate = 3 - default: - return false - } - if len(emitter.events)-emitter.events_head > accumulate { - return false +// Delete an emitter object. +func (emitter *Emitter) Delete() { + *emitter = Emitter{} +} + +// String write handler. +func yamlStringWriteHandler(emitter *Emitter, buffer []byte) error { + *emitter.output_buffer = append(*emitter.output_buffer, buffer...) + return nil +} + +// yamlWriterWriteHandler uses emitter.output_writer to write the +// emitted text. +func yamlWriterWriteHandler(emitter *Emitter, buffer []byte) error { + _, err := emitter.output_writer.Write(buffer) + return err +} + +// SetOutputString sets a string output. +func (emitter *Emitter) SetOutputString(output_buffer *[]byte) { + if emitter.write_handler != nil { + panic("must set the output target only once") } - var level int - for i := emitter.events_head; i < len(emitter.events); i++ { - switch emitter.events[i].Type { - case STREAM_START_EVENT, DOCUMENT_START_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT: - level++ - case STREAM_END_EVENT, DOCUMENT_END_EVENT, SEQUENCE_END_EVENT, MAPPING_END_EVENT: - level-- - } - if level == 0 { - return false - } + emitter.write_handler = yamlStringWriteHandler + emitter.output_buffer = output_buffer +} + +// SetOutputWriter sets a file output. +func (emitter *Emitter) SetOutputWriter(w io.Writer) { + if emitter.write_handler != nil { + panic("must set the output target only once") } - return true + emitter.write_handler = yamlWriterWriteHandler + emitter.output_writer = w } -// Append a directive to the directives stack. -func (emitter *Emitter) appendTagDirective(value *TagDirective, allow_duplicates bool) error { - for i := 0; i < len(emitter.tag_directives); i++ { - if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { - if allow_duplicates { - return nil - } - return EmitterError{ - Message: "duplicate %TAG directive", - } - } +// SetEncoding sets the output encoding. +func (emitter *Emitter) SetEncoding(encoding Encoding) { + if emitter.encoding != ANY_ENCODING { + panic("must set the output encoding only once") } + emitter.encoding = encoding +} - // [Go] Do we actually need to copy this given garbage collection - // and the lack of deallocating destructors? - tag_copy := TagDirective{ - handle: make([]byte, len(value.handle)), - prefix: make([]byte, len(value.prefix)), +// SetCanonical sets the canonical output style. +func (emitter *Emitter) SetCanonical(canonical bool) { + emitter.canonical = canonical +} + +// SetIndent sets the indentation increment. +func (emitter *Emitter) SetIndent(indent int) { + if indent < 2 || indent > 9 { + indent = 2 } - copy(tag_copy.handle, value.handle) - copy(tag_copy.prefix, value.prefix) - emitter.tag_directives = append(emitter.tag_directives, tag_copy) - return nil + emitter.BestIndent = indent } -// Increase the indentation level. -func (emitter *Emitter) increaseIndentCompact(flow, indentless bool, compact_seq bool) error { - emitter.indents = append(emitter.indents, emitter.indent) - if emitter.indent < 0 { - if flow { - emitter.indent = emitter.BestIndent - } else { - emitter.indent = 0 - } - } else if !indentless { - // [Go] This was changed so that indentations are more regular. - if emitter.states[len(emitter.states)-1] == EMIT_BLOCK_SEQUENCE_ITEM_STATE { - // The first indent inside a sequence will just skip the "- " indicator. - emitter.indent += 2 - } else { - // Everything else aligns to the chosen indentation. - emitter.indent = emitter.BestIndent * ((emitter.indent + emitter.BestIndent) / emitter.BestIndent) - if compact_seq { - // The value compact_seq passed in is almost always set to `false` when this function is called, - // except when we are dealing with sequence nodes. So this gets triggered to subtract 2 only when we - // are increasing the indent to account for sequence nodes, which will be correct because we need to - // subtract 2 to account for the - at the beginning of the sequence node. - emitter.indent = emitter.indent - 2 - } - } +// SetWidth sets the preferred line width. +func (emitter *Emitter) SetWidth(width int) { + if width < 0 { + width = -1 } - return nil + emitter.best_width = width +} + +// SetUnicode sets if unescaped non-ASCII characters are allowed. +func (emitter *Emitter) SetUnicode(unicode bool) { + emitter.unicode = unicode +} + +// SetLineBreak sets the preferred line break character. +func (emitter *Emitter) SetLineBreak(line_break LineBreak) { + emitter.line_break = line_break } // State dispatcher. @@ -306,6 +326,45 @@ func (emitter *Emitter) stateMachine(event *Event) error { panic("invalid emitter state") } +// Check if we need to accumulate more events before emitting. +// +// We accumulate extra +// - 1 event for DOCUMENT-START +// - 2 events for SEQUENCE-START +// - 3 events for MAPPING-START +func (emitter *Emitter) needMoreEvents() bool { + if emitter.events_head == len(emitter.events) { + return true + } + var accumulate int + switch emitter.events[emitter.events_head].Type { + case DOCUMENT_START_EVENT: + accumulate = 1 + case SEQUENCE_START_EVENT: + accumulate = 2 + case MAPPING_START_EVENT: + accumulate = 3 + default: + return false + } + if len(emitter.events)-emitter.events_head > accumulate { + return false + } + var level int + for i := emitter.events_head; i < len(emitter.events); i++ { + switch emitter.events[i].Type { + case STREAM_START_EVENT, DOCUMENT_START_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT: + level++ + case STREAM_END_EVENT, DOCUMENT_END_EVENT, SEQUENCE_END_EVENT, MAPPING_END_EVENT: + level-- + } + if level == 0 { + return false + } + } + return true +} + // Expect STREAM-START. func (emitter *Emitter) emitStreamStart(event *Event) error { if event.Type != STREAM_START_EVENT { @@ -473,18 +532,6 @@ func (emitter *Emitter) emitDocumentStart(event *Event, first bool) error { } } -// emitter preserves the original signature and delegates to -// increaseIndentCompact without compact-sequence indentation -func (emitter *Emitter) increaseIndent(flow, indentless bool) error { - return emitter.increaseIndentCompact(flow, indentless, false) -} - -// processLineComment preserves the original signature and delegates to -// processLineCommentLinebreak passing false for linebreak -func (emitter *Emitter) processLineComment() error { - return emitter.processLineCommentLinebreak(false) -} - // Expect the root node. func (emitter *Emitter) emitDocumentContent(event *Event) error { emitter.states = append(emitter.states, EMIT_DOCUMENT_END_STATE) @@ -633,7 +680,9 @@ func (emitter *Emitter) emitFlowMappingKey(event *Event, first, trail bool) erro } if event.Type == MAPPING_END_EVENT { - if (emitter.canonical || len(emitter.HeadComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0) && !first && !trail { + if (emitter.canonical || + len(emitter.HeadComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0) && + !first && !trail { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } @@ -737,13 +786,17 @@ func (emitter *Emitter) emitFlowMappingValue(event *Event, simple bool) error { // Expect a block item node. func (emitter *Emitter) emitBlockSequenceItem(event *Event, first bool) error { if first { - // emitter.mapping context tells us if we are currently in a mapping context. - // emitter.column tells us which column we are in the yaml output. 0 is the first char of the column. - // emitter.indentation tells us if the last character was an indentation character. - // emitter.compact_sequence_indent tells us if '- ' is considered part of the indentation for sequence elements. - // So, `seq` means that we are in a mapping context, and we are either at the first char of the column or - // the last character was not an indentation character, and we consider '- ' part of the indentation - // for sequence elements. + // emitter.mapping context tells us if we are currently in a + // mapping context. emitter.column tells us which column we + // are in the yaml output. 0 is the first char of the column. + // emitter.indentation tells us if the last character was an + // indentation character. + // emitter.compact_sequence_indent tells us if '- ' is + // considered part of the indentation for sequence elements. + // So, `seq` means that we are in a mapping context, and we are + // either at the first char of the column or the last character + // was not an indentation character, and we consider '- ' part + // of the indentation for sequence elements. seq := emitter.mapping_context && (emitter.column == 0 || !emitter.indention) && emitter.CompactSequenceIndent if err := emitter.increaseIndentCompact(false, false, seq); err != nil { @@ -841,19 +894,23 @@ func (emitter *Emitter) emitBlockMappingValue(event *Event, simple bool) error { } } if len(emitter.key_line_comment) > 0 { - // [Go] Line comments are generally associated with the value, but when there's - // no value on the same line as a mapping key they end up attached to the - // key itself. + // [Go] Line comments are generally associated with the value, + // but when there's no value on the same line as a mapping key + // they end up attached to the key itself. if event.Type == SCALAR_EVENT { if len(emitter.LineComment) == 0 { - // A scalar is coming and it has no line comments by itself yet, - // so just let it handle the line comment as usual. If it has a - // line comment, we can't have both so the one from the key is lost. + // A scalar is coming and it has no line + // comments by itself yet, so just let it + // handle the line comment as usual. If it has + // a line comment, we can't have both so the + // one from the key is lost. emitter.LineComment = emitter.key_line_comment emitter.key_line_comment = nil } - } else if event.SequenceStyle() != FLOW_SEQUENCE_STYLE && (event.Type == MAPPING_START_EVENT || event.Type == SEQUENCE_START_EVENT) { - // An indented block follows, so write the comment right now. + } else if event.SequenceStyle() != FLOW_SEQUENCE_STYLE && + (event.Type == MAPPING_START_EVENT || event.Type == SEQUENCE_START_EVENT) { + // An indented block follows, so write the comment + // right now. emitter.LineComment, emitter.key_line_comment = emitter.key_line_comment, emitter.LineComment if err := emitter.processLineComment(); err != nil { return err @@ -874,10 +931,6 @@ func (emitter *Emitter) emitBlockMappingValue(event *Event, simple bool) error { return nil } -func (emitter *Emitter) silentNilEvent(event *Event) bool { - return event.Type == SCALAR_EVENT && event.Implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 -} - // Expect a node. func (emitter *Emitter) emitNode(event *Event, root bool, sequence bool, mapping bool, simple_key bool, @@ -913,15 +966,6 @@ func (emitter *Emitter) emitAlias(event *Event) error { return nil } -// requiredQuoteStyle returns the appropriate quote style based on the -// emitter's quotePreference setting. -func (emitter *Emitter) requiredQuoteStyle() ScalarStyle { - if emitter.quotePreference == QuoteDouble { - return DOUBLE_QUOTED_SCALAR_STYLE - } - return SINGLE_QUOTED_SCALAR_STYLE -} - // Expect SCALAR. func (emitter *Emitter) emitScalar(event *Event) error { if err := emitter.selectScalarStyle(event); err != nil { @@ -954,7 +998,8 @@ func (emitter *Emitter) emitSequenceStart(event *Event) error { if err := emitter.processTag(); err != nil { return err } - if emitter.flow_level > 0 || emitter.canonical || event.SequenceStyle() == FLOW_SEQUENCE_STYLE || + if emitter.flow_level > 0 || emitter.canonical || + event.SequenceStyle() == FLOW_SEQUENCE_STYLE || emitter.checkEmptySequence() { emitter.state = EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE } else { @@ -971,7 +1016,8 @@ func (emitter *Emitter) emitMappingStart(event *Event) error { if err := emitter.processTag(); err != nil { return err } - if emitter.flow_level > 0 || emitter.canonical || event.MappingStyle() == FLOW_MAPPING_STYLE || + if emitter.flow_level > 0 || emitter.canonical || + event.MappingStyle() == FLOW_MAPPING_STYLE || emitter.checkEmptyMapping() { emitter.state = EMIT_FLOW_MAPPING_FIRST_KEY_STATE } else { @@ -1018,73 +1064,23 @@ func (emitter *Emitter) checkSimpleKey() bool { len(emitter.tag_data.suffix) + len(emitter.scalar_data.value) case SEQUENCE_START_EVENT: - if !emitter.checkEmptySequence() { - return false - } - length += len(emitter.anchor_data.anchor) + - len(emitter.tag_data.handle) + - len(emitter.tag_data.suffix) - case MAPPING_START_EVENT: - if !emitter.checkEmptyMapping() { - return false - } - length += len(emitter.anchor_data.anchor) + - len(emitter.tag_data.handle) + - len(emitter.tag_data.suffix) - default: - return false - } - return length <= 128 -} - -// Determine an acceptable scalar style. -func (emitter *Emitter) selectScalarStyle(event *Event) error { - no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 - if no_tag && !event.Implicit && !event.quoted_implicit { - return EmitterError{ - Message: "neither tag nor implicit flags are specified", - } - } - - style := event.ScalarStyle() - if style == ANY_SCALAR_STYLE { - style = PLAIN_SCALAR_STYLE - } - if emitter.canonical { - style = DOUBLE_QUOTED_SCALAR_STYLE - } - if emitter.simple_key_context && emitter.scalar_data.multiline { - style = DOUBLE_QUOTED_SCALAR_STYLE - } - - if style == PLAIN_SCALAR_STYLE { - if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed || - emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed { - style = emitter.requiredQuoteStyle() - } - if len(emitter.scalar_data.value) == 0 && (emitter.flow_level > 0 || emitter.simple_key_context) { - style = emitter.requiredQuoteStyle() - } - if no_tag && !event.Implicit { - style = emitter.requiredQuoteStyle() - } - } - if style == SINGLE_QUOTED_SCALAR_STYLE { - if !emitter.scalar_data.single_quoted_allowed { - style = DOUBLE_QUOTED_SCALAR_STYLE + if !emitter.checkEmptySequence() { + return false } - } - if style == LITERAL_SCALAR_STYLE || style == FOLDED_SCALAR_STYLE { - if !emitter.scalar_data.block_allowed || emitter.flow_level > 0 || emitter.simple_key_context { - style = DOUBLE_QUOTED_SCALAR_STYLE + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + case MAPPING_START_EVENT: + if !emitter.checkEmptyMapping() { + return false } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + default: + return false } - - if no_tag && !event.quoted_implicit && style != PLAIN_SCALAR_STYLE { - emitter.tag_data.handle = []byte{'!'} - } - emitter.scalar_data.style = style - return nil + return length <= 128 } // Write an anchor. @@ -1181,7 +1177,13 @@ func (emitter *Emitter) processHeadComment() error { return nil } -// Write an line comment. +// processLineComment preserves the original signature and delegates to +// processLineCommentLinebreak passing false for linebreak +func (emitter *Emitter) processLineComment() error { + return emitter.processLineCommentLinebreak(false) +} + +// Write a line comment. func (emitter *Emitter) processLineCommentLinebreak(linebreak bool) error { if len(emitter.LineComment) == 0 { // The next 3 lines are needed to resolve an issue with leading newlines @@ -1347,7 +1349,9 @@ func (emitter *Emitter) analyzeScalar(value []byte) error { return nil } - if len(value) >= 3 && ((value[0] == '-' && value[1] == '-' && value[2] == '-') || (value[0] == '.' && value[1] == '.' && value[2] == '.')) { + if len(value) >= 3 && + ((value[0] == '-' && value[1] == '-' && value[2] == '-') || + (value[0] == '.' && value[1] == '.' && value[2] == '.')) { block_indicators = true flow_indicators = true } @@ -1359,7 +1363,8 @@ func (emitter *Emitter) analyzeScalar(value []byte) error { if i == 0 { switch value[i] { - case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`': + case '#', ',', '[', ']', '{', '}', '&', '*', '!', + '|', '>', '\'', '"', '%', '@', '`': flow_indicators = true block_indicators = true case '?', ':': @@ -1425,7 +1430,8 @@ func (emitter *Emitter) analyzeScalar(value []byte) error { previous_break = false } - // [Go]: Why 'z'? Couldn't be the end of the string as that's the loop condition. + // [Go]: Why 'z'? Couldn't be the end of the string as that's + // the loop condition. preceded_by_whitespace = isBlankOrZero(value, i) } @@ -1500,7 +1506,8 @@ func (emitter *Emitter) analyzeEvent(event *Event) error { return err } } - if len(event.Tag) > 0 && (emitter.canonical || (!event.Implicit && !event.quoted_implicit)) { + if len(event.Tag) > 0 && (emitter.canonical || + (!event.Implicit && !event.quoted_implicit)) { if err := emitter.analyzeTag(event.Tag); err != nil { return err } @@ -1536,6 +1543,58 @@ func (emitter *Emitter) analyzeEvent(event *Event) error { return nil } +// Determine an acceptable scalar style. +func (emitter *Emitter) selectScalarStyle(event *Event) error { + no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 + if no_tag && !event.Implicit && !event.quoted_implicit { + return EmitterError{ + Message: "neither tag nor implicit flags are specified", + } + } + + style := event.ScalarStyle() + if style == ANY_SCALAR_STYLE { + style = PLAIN_SCALAR_STYLE + } + if emitter.canonical { + style = DOUBLE_QUOTED_SCALAR_STYLE + } + if emitter.simple_key_context && emitter.scalar_data.multiline { + style = DOUBLE_QUOTED_SCALAR_STYLE + } + + if style == PLAIN_SCALAR_STYLE { + if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed || + emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed { + style = emitter.requiredQuoteStyle() + } + if len(emitter.scalar_data.value) == 0 && + (emitter.flow_level > 0 || emitter.simple_key_context) { + style = emitter.requiredQuoteStyle() + } + if no_tag && !event.Implicit { + style = emitter.requiredQuoteStyle() + } + } + if style == SINGLE_QUOTED_SCALAR_STYLE { + if !emitter.scalar_data.single_quoted_allowed { + style = DOUBLE_QUOTED_SCALAR_STYLE + } + } + if style == LITERAL_SCALAR_STYLE || style == FOLDED_SCALAR_STYLE { + if !emitter.scalar_data.block_allowed || + emitter.flow_level > 0 || emitter.simple_key_context { + style = DOUBLE_QUOTED_SCALAR_STYLE + } + } + + if no_tag && !event.quoted_implicit && style != PLAIN_SCALAR_STYLE { + emitter.tag_data.handle = []byte{'!'} + } + emitter.scalar_data.style = style + return nil +} + // Write the BOM character. func (emitter *Emitter) writeBom() error { if err := emitter.flushIfNeeded(); err != nil { @@ -1549,12 +1608,14 @@ func (emitter *Emitter) writeBom() error { return nil } +// writeIndent writes the appropriate indentation to the output. func (emitter *Emitter) writeIndent() error { indent := emitter.indent if indent < 0 { indent = 0 } - if !emitter.indention || emitter.column > indent || (emitter.column == indent && !emitter.whitespace) { + if !emitter.indention || emitter.column > indent || + (emitter.column == indent && !emitter.whitespace) { if err := emitter.putLineBreak(); err != nil { return err } @@ -1575,6 +1636,7 @@ func (emitter *Emitter) writeIndent() error { return nil } +// writeIndicator writes a YAML indicator (like ':', '-', '?') to the output. func (emitter *Emitter) writeIndicator(indicator []byte, need_whitespace, is_whitespace, is_indention bool) error { if need_whitespace && !emitter.whitespace { if err := emitter.put(' '); err != nil { @@ -1590,6 +1652,7 @@ func (emitter *Emitter) writeIndicator(indicator []byte, need_whitespace, is_whi return nil } +// writeAnchor writes an anchor name to the output. func (emitter *Emitter) writeAnchor(value []byte) error { if err := emitter.writeAll(value); err != nil { return err @@ -1599,6 +1662,7 @@ func (emitter *Emitter) writeAnchor(value []byte) error { return nil } +// writeTagHandle writes a tag handle to the output. func (emitter *Emitter) writeTagHandle(value []byte) error { if !emitter.whitespace { if err := emitter.put(' '); err != nil { @@ -1613,6 +1677,8 @@ func (emitter *Emitter) writeTagHandle(value []byte) error { return nil } +// writeTagContent writes a tag URI to the output, URL-encoding special +// characters as needed. func (emitter *Emitter) writeTagContent(value []byte, need_whitespace bool) error { if need_whitespace && !emitter.whitespace { if err := emitter.put(' '); err != nil { @@ -1622,7 +1688,8 @@ func (emitter *Emitter) writeTagContent(value []byte, need_whitespace bool) erro for i := 0; i < len(value); { var must_write bool switch value[i] { - case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\'', '(', ')', '[', ']': + case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', + '_', '.', '~', '*', '\'', '(', ')', '[', ']': must_write = true default: must_write = isAlpha(value, i) @@ -1667,6 +1734,8 @@ func (emitter *Emitter) writeTagContent(value []byte, need_whitespace bool) erro return nil } +// writePlainScalar writes a plain (unquoted) scalar to the output, handling +// line breaks and wrapping as needed. func (emitter *Emitter) writePlainScalar(value []byte, allow_breaks bool) error { if len(value) > 0 && !emitter.whitespace { if err := emitter.put(' '); err != nil { @@ -1678,7 +1747,9 @@ func (emitter *Emitter) writePlainScalar(value []byte, allow_breaks bool) error breaks := false for i := 0; i < len(value); { if isSpace(value, i) { - if allow_breaks && !spaces && emitter.column > emitter.best_width && !isSpace(value, i+1) { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + !isSpace(value, i+1) { if err := emitter.writeIndent(); err != nil { return err } @@ -1725,6 +1796,8 @@ func (emitter *Emitter) writePlainScalar(value []byte, allow_breaks bool) error return nil } +// writeSingleQuotedScalar writes a single-quoted scalar to the output, +// escaping single quotes and handling line breaks. func (emitter *Emitter) writeSingleQuotedScalar(value []byte, allow_breaks bool) error { if err := emitter.writeIndicator([]byte{'\''}, true, false, false); err != nil { return err @@ -1734,7 +1807,9 @@ func (emitter *Emitter) writeSingleQuotedScalar(value []byte, allow_breaks bool) breaks := false for i := 0; i < len(value); { if isSpace(value, i) { - if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 && !isSpace(value, i+1) { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + i > 0 && i < len(value)-1 && !isSpace(value, i+1) { if err := emitter.writeIndent(); err != nil { return err } @@ -1782,6 +1857,8 @@ func (emitter *Emitter) writeSingleQuotedScalar(value []byte, allow_breaks bool) return nil } +// writeDoubleQuotedScalar writes a double-quoted scalar to the output, +// escaping special characters and handling Unicode. func (emitter *Emitter) writeDoubleQuotedScalar(value []byte, allow_breaks bool) error { spaces := false if err := emitter.writeIndicator([]byte{'"'}, true, false, false); err != nil { @@ -1874,7 +1951,9 @@ func (emitter *Emitter) writeDoubleQuotedScalar(value []byte, allow_breaks bool) } spaces = false } else if isSpace(value, i) { - if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 { + if allow_breaks && !spaces && + emitter.column > emitter.best_width && + i > 0 && i < len(value)-1 { if err := emitter.writeIndent(); err != nil { return err } @@ -1903,10 +1982,13 @@ func (emitter *Emitter) writeDoubleQuotedScalar(value []byte, allow_breaks bool) return nil } +// writeBlockScalarHints writes the indentation and chomping indicators for +// block scalars. func (emitter *Emitter) writeBlockScalarHints(value []byte) error { if isSpace(value, 0) { // https://github.com/yaml/go-yaml/issues/65 - // isLineBreak(value, 0) removed as the linebreak will only write the indentation value. + // isLineBreak(value, 0) removed as the linebreak will only + // write the indentation value. indent_hint := []byte{'0' + byte(emitter.BestIndent)} if err := emitter.writeIndicator(indent_hint, false, false, false); err != nil { return err @@ -1947,6 +2029,8 @@ func (emitter *Emitter) writeBlockScalarHints(value []byte) error { return nil } +// writeLiteralScalar writes a literal block scalar (|) to the output, +// preserving line breaks exactly. func (emitter *Emitter) writeLiteralScalar(value []byte) error { if err := emitter.writeIndicator([]byte{'|'}, true, false, false); err != nil { return err @@ -1982,6 +2066,8 @@ func (emitter *Emitter) writeLiteralScalar(value []byte) error { return nil } +// writeFoldedScalar writes a folded block scalar (>) to the output, folding +// long lines at appropriate breaks. func (emitter *Emitter) writeFoldedScalar(value []byte) error { if err := emitter.writeIndicator([]byte{'>'}, true, false, false); err != nil { return err @@ -2021,7 +2107,9 @@ func (emitter *Emitter) writeFoldedScalar(value []byte) error { } leading_spaces = isBlank(value, i) } - if !breaks && isSpace(value, i) && !isSpace(value, i+1) && emitter.column > emitter.best_width { + if !breaks && isSpace(value, i) && + !isSpace(value, i+1) && + emitter.column > emitter.best_width { if err := emitter.writeIndent(); err != nil { return err } @@ -2038,6 +2126,8 @@ func (emitter *Emitter) writeFoldedScalar(value []byte) error { return nil } +// writeComment writes a comment to the output, ensuring each line starts +// with '#' and handling line breaks appropriately. func (emitter *Emitter) writeComment(comment []byte) error { breaks := false pound := false @@ -2081,3 +2171,201 @@ func (emitter *Emitter) writeComment(comment []byte) error { emitter.whitespace = true return nil } + +// Flush the buffer if needed. +func (emitter *Emitter) flushIfNeeded() error { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + return emitter.flush() + } + return nil +} + +// Put a character to the output buffer. +func (emitter *Emitter) put(value byte) error { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + if err := emitter.flush(); err != nil { + return err + } + } + emitter.buffer[emitter.buffer_pos] = value + emitter.buffer_pos++ + emitter.column++ + return nil +} + +// Put a line break to the output buffer. +func (emitter *Emitter) putLineBreak() error { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + if err := emitter.flush(); err != nil { + return err + } + } + switch emitter.line_break { + case CR_BREAK: + emitter.buffer[emitter.buffer_pos] = '\r' + emitter.buffer_pos += 1 + case LN_BREAK: + emitter.buffer[emitter.buffer_pos] = '\n' + emitter.buffer_pos += 1 + case CRLN_BREAK: + emitter.buffer[emitter.buffer_pos+0] = '\r' + emitter.buffer[emitter.buffer_pos+1] = '\n' + emitter.buffer_pos += 2 + default: + panic("unknown line break setting") + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and below and drop from everywhere else (see + // commented lines). + emitter.indention = true + return nil +} + +// Copy a character from a string into buffer. +func (emitter *Emitter) write(s []byte, i *int) error { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + if err := emitter.flush(); err != nil { + return err + } + } + p := emitter.buffer_pos + w := width(s[*i]) + switch w { + case 4: + emitter.buffer[p+3] = s[*i+3] + fallthrough + case 3: + emitter.buffer[p+2] = s[*i+2] + fallthrough + case 2: + emitter.buffer[p+1] = s[*i+1] + fallthrough + case 1: + emitter.buffer[p+0] = s[*i+0] + default: + panic("unknown character width") + } + emitter.column++ + emitter.buffer_pos += w + *i += w + return nil +} + +// Write a whole string into buffer. +func (emitter *Emitter) writeAll(s []byte) error { + for i := 0; i < len(s); { + if err := emitter.write(s, &i); err != nil { + return err + } + } + return nil +} + +// Copy a line break character from a string into buffer. +func (emitter *Emitter) writeLineBreak(s []byte, i *int) error { + if s[*i] == '\n' { + if err := emitter.putLineBreak(); err != nil { + return err + } + *i++ + } else { + if err := emitter.write(s, i); err != nil { + return err + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and above and drop from everywhere else + // (see commented lines). + emitter.indention = true + } + return nil +} + +// Append a directive to the directives stack. +func (emitter *Emitter) appendTagDirective(value *TagDirective, allow_duplicates bool) error { + for i := 0; i < len(emitter.tag_directives); i++ { + if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { + if allow_duplicates { + return nil + } + return EmitterError{ + Message: "duplicate %TAG directive", + } + } + } + + // [Go] Do we actually need to copy this given garbage collection + // and the lack of deallocating destructors? + tag_copy := TagDirective{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(tag_copy.handle, value.handle) + copy(tag_copy.prefix, value.prefix) + emitter.tag_directives = append(emitter.tag_directives, tag_copy) + return nil +} + +// Increase the indentation level. +func (emitter *Emitter) increaseIndentCompact(flow, indentless bool, compact_seq bool) error { + emitter.indents = append(emitter.indents, emitter.indent) + if emitter.indent < 0 { + if flow { + emitter.indent = emitter.BestIndent + } else { + emitter.indent = 0 + } + } else if !indentless { + // [Go] This was changed so that indentations are more regular. + if emitter.states[len(emitter.states)-1] == EMIT_BLOCK_SEQUENCE_ITEM_STATE { + // The first indent inside a sequence will just skip + // the "- " indicator. + emitter.indent += 2 + } else { + // Everything else aligns to the chosen indentation. + emitter.indent = emitter.BestIndent * + ((emitter.indent + emitter.BestIndent) / emitter.BestIndent) + if compact_seq { + // The value compact_seq passed in is almost + // always set to `false` when this function is + // called, except when we are dealing with + // sequence nodes. So this gets triggered to + // subtract 2 only when we are increasing the + // indent to account for sequence nodes, which + // will be correct because we need to subtract + // 2 to account for the - at the beginning of + // the sequence node. + emitter.indent = emitter.indent - 2 + } + } + } + return nil +} + +// emitter preserves the original signature and delegates to +// increaseIndentCompact without compact-sequence indentation +func (emitter *Emitter) increaseIndent(flow, indentless bool) error { + return emitter.increaseIndentCompact(flow, indentless, false) +} + +// silentNilEvent checks if an event represents an implicit null scalar that +// can be omitted in non-canonical mode. +func (emitter *Emitter) silentNilEvent(event *Event) bool { + return event.Type == SCALAR_EVENT && event.Implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 +} + +// requiredQuoteStyle returns the appropriate quote style based on the +// emitter's quotePreference setting. +func (emitter *Emitter) requiredQuoteStyle() ScalarStyle { + if emitter.quotePreference == QuoteDouble { + return DOUBLE_QUOTED_SCALAR_STYLE + } + return SINGLE_QUOTED_SCALAR_STYLE +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/errors.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/errors.go index 8b279bfda..a33cc7767 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/errors.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/errors.go @@ -12,111 +12,185 @@ import ( "strings" ) -type MarkedYAMLError struct { - // optional context - ContextMark Mark - ContextMessage string +// Stage identifies the processing stage where an error occurred during YAML +// loading or dumping. +type Stage string - Mark Mark - Message string +const ( + // Load stages + ReaderStage Stage = "reader" // Input reading and encoding + ScannerStage Stage = "scanner" // Tokenization + ParserStage Stage = "parser" // Event stream parsing + ComposerStage Stage = "composer" // Node tree construction + ResolverStage Stage = "resolver" // Tag resolution + ConstructorStage Stage = "constructor" // Go value construction + + // Dump stages + RepresenterStage Stage = "representer" // Go value to Node tree + SerializerStage Stage = "serializer" // Node tree to events + EmitterStage Stage = "emitter" // Events to YAML bytes + WriterStage Stage = "writer" // Output writing +) + +// LoadError represents an error that occurred while loading a YAML document. +// +// It provides detailed location information and identifies the processing +// stage where the error occurred. +type LoadError struct { + Stage Stage // Processing stage where error occurred + Message string // Error description + + // Position information + Mark Mark // Primary error position + ContextMark Mark // Optional context position (e.g., start of construct) + ContextMsg string // Optional context message + + // Error chaining + err error // Underlying error (for Unwrap support) } -func (e MarkedYAMLError) Error() string { +// Error returns the error message with stage and position information. +// Format: "go-yaml load error in at L:C: " +// Or with context: "go-yaml load error in () at L:C-L:C: " +func (e *LoadError) Error() string { + if len(e.ContextMsg) > 0 { + return fmt.Sprintf("go-yaml load error in %s (%s) at %s: %s", + e.Stage, e.ContextMsg, e.ContextMark.rangeString(e.Mark), e.Message) + } + return fmt.Sprintf("go-yaml load error in %s at %s: %s", + e.Stage, e.Mark.shortString(), e.Message) +} + +// simpleError returns the error message without the "yaml: Load error (in stage)" prefix. +// Used for formatting errors within LoadErrors collections. +// Format: "line L: " (backwards compatible - no column info) +func (e *LoadError) simpleError() string { var builder strings.Builder - builder.WriteString("yaml: ") - if len(e.ContextMessage) > 0 { - fmt.Fprintf(&builder, "%s at %s: ", e.ContextMessage, e.ContextMark) + if len(e.ContextMsg) > 0 { + fmt.Fprintf(&builder, "%s at %s: ", e.ContextMsg, e.ContextMark) } - if len(e.ContextMessage) == 0 || e.ContextMark != e.Mark { - fmt.Fprintf(&builder, "%s: ", e.Mark) + if len(e.ContextMsg) == 0 || e.ContextMark != e.Mark { + if e.Mark.Line > 0 { + fmt.Fprintf(&builder, "line %d: ", e.Mark.Line) + } else { + builder.WriteString(": ") + } } builder.WriteString(e.Message) return builder.String() } -type ParserError MarkedYAMLError +// Unwrap returns the underlying error. +func (e *LoadError) Unwrap() error { + return e.err +} -func (e ParserError) Error() string { - return MarkedYAMLError(e).Error() +// NewLoadError creates a LoadError with an underlying cause. +// The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. +func NewLoadError(stage Stage, message string, mark Mark, cause error) *LoadError { + return &LoadError{ + Stage: stage, + Message: message, + Mark: mark, + err: cause, + } } -type ScannerError MarkedYAMLError +// DumpError represents an error that occurred while dumping a YAML document. +// +// It identifies the processing stage where the error occurred and provides +// an optional underlying cause via Unwrap. +type DumpError struct { + Stage Stage // Processing stage where error occurred + Message string // Error description -func (e ScannerError) Error() string { - return MarkedYAMLError(e).Error() + // Error chaining + err error // Underlying error (for Unwrap support) } -type ReaderError struct { - Offset int - Value int - Err error +// Error returns the error message with stage information. +// Format: "go-yaml dump error in : " +func (e *DumpError) Error() string { + return fmt.Sprintf("go-yaml dump error in %s: %s", e.Stage, e.Message) } -func (e ReaderError) Error() string { - return fmt.Sprintf("yaml: offset %d: %s", e.Offset, e.Err) +// Unwrap returns the underlying error. +func (e *DumpError) Unwrap() error { + return e.err } -func (e ReaderError) Unwrap() error { - return e.Err +// NewDumpError creates a DumpError with an underlying cause. +// The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. +func NewDumpError(stage Stage, message string, cause error) *DumpError { + return &DumpError{Stage: stage, Message: message, err: cause} } +// failDump panics with a YAMLError wrapping a DumpError for the given stage. +// If err is exactly a *DumpError it is passed through unchanged to avoid +// double-wrapping (e.g. a user MarshalYAML that returns yaml.NewDumpError). +// Errors that merely wrap a *DumpError are treated as ordinary errors so that +// the outer wrapper's message and context are preserved. +func failDump(stage Stage, err error) { + if de, ok := err.(*DumpError); ok { + panic(&YAMLError{de}) + } + panic(&YAMLError{&DumpError{Stage: stage, Message: err.Error(), err: err}}) +} + +// failDumpf panics with a YAMLError wrapping a formatted DumpError. +func failDumpf(stage Stage, format string, args ...any) { + panic(&YAMLError{&DumpError{Stage: stage, Message: fmt.Sprintf(format, args...)}}) +} + +// EmitterError represents an error that occurred during emitting. type EmitterError struct { Message string } +// Error returns the error message. func (e EmitterError) Error() string { return fmt.Sprintf("yaml: %s", e.Message) } +// WriterError represents an error that occurred while writing output. type WriterError struct { Err error } +// Error returns the error message. func (e WriterError) Error() string { return fmt.Sprintf("yaml: %s", e.Err) } +// Unwrap returns the underlying error. func (e WriterError) Unwrap() error { return e.Err } -// ConstructError represents a single, non-fatal error that occurred during -// the constructing of a YAML document into a Go value. -type ConstructError struct { - Err error - Line int - Column int -} - -func (e *ConstructError) Error() string { - return fmt.Sprintf("line %d: %s", e.Line, e.Err.Error()) -} - -func (e *ConstructError) Unwrap() error { - return e.Err -} - // LoadErrors is returned when one or more fields cannot be properly decoded. type LoadErrors struct { - Errors []*ConstructError + Errors []*LoadError } +// Error returns a formatted error message listing all construct errors. func (e *LoadErrors) Error() string { var b strings.Builder - b.WriteString("yaml: construct errors:") - for _, err := range e.Errors { - b.WriteString("\n ") - b.WriteString(err.Error()) + b.WriteString("yaml: construct errors: ") + for i, err := range e.Errors { + if i > 0 { + b.WriteString("; ") + } + b.WriteString(err.simpleError()) } return b.String() } -// As implements errors.As for Go versions prior to 1.20 that don't support +// As implements [errors.As] for Go versions prior to 1.20 that don't support // the Unwrap() []error interface. It allows [LoadErrors] to match against -// *ConstructError targets by returning the first error in the list. +// *LoadError or *TypeError targets. func (e *LoadErrors) As(target any) bool { switch t := target.(type) { - case **ConstructError: + case **LoadError: if len(e.Errors) == 0 { return false } @@ -125,7 +199,7 @@ func (e *LoadErrors) As(target any) bool { case **TypeError: var msgs []string for _, err := range e.Errors { - msgs = append(msgs, err.Error()) + msgs = append(msgs, err.simpleError()) } *t = &TypeError{Errors: msgs} return true @@ -133,7 +207,7 @@ func (e *LoadErrors) As(target any) bool { return false } -// Is implements errors.Is for Go versions prior to 1.20 that don't support +// Is implements [errors.Is] for Go versions prior to 1.20 that don't support // the Unwrap() []error interface. It checks if any wrapped error matches // the target error. func (e *LoadErrors) Is(target error) bool { @@ -145,7 +219,7 @@ func (e *LoadErrors) Is(target error) bool { return false } -// TypeError is an obsolete error type retained for compatibility. +// TypeError is a legacy error type retained for compatibility. // // A TypeError is returned by Unmarshal when one or more fields in // the YAML document cannot be properly decoded into the requested @@ -157,8 +231,9 @@ type TypeError struct { Errors []string } +// Error returns a formatted error message listing all unmarshal errors. func (e *TypeError) Error() string { - return fmt.Sprintf("yaml: unmarshal errors:\n %s", strings.Join(e.Errors, "\n ")) + return fmt.Sprintf("yaml: unmarshal errors: %s", strings.Join(e.Errors, "; ")) } // YAMLError is an internal error wrapper type. @@ -166,6 +241,19 @@ type YAMLError struct { Err error } +// Error returns the error message. func (e *YAMLError) Error() string { return e.Err.Error() } + +// handleErr recovers from panics caused by yaml errors. +// It's used in defer statements to convert YAMLError panics into regular errors. +func handleErr(err *error) { + if v := recover(); v != nil { + if e, ok := v.(*YAMLError); ok { + *err = e.Err + } else { + panic(v) + } + } +} diff --git a/vendor/go.yaml.in/yaml/v4/loader.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/loader.go similarity index 55% rename from vendor/go.yaml.in/yaml/v4/loader.go rename to vendor/go.yaml.in/yaml/v4/internal/libyaml/loader.go index aa9757fff..6b8081245 100644 --- a/vendor/go.yaml.in/yaml/v4/loader.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/loader.go @@ -7,18 +7,45 @@ // - Load: Decode YAML document(s) into a value (use WithAll for multi-doc) // - NewLoader: Create a streaming loader from io.Reader -package yaml +package libyaml import ( "bytes" "errors" "io" "reflect" - - "go.yaml.in/yaml/v4/internal/libyaml" ) -// Load decodes YAML document(s) with the given options. +// A Loader reads and loads YAML values from an input stream with configurable +// options. +type Loader struct { + composer *Composer + resolver *Resolver + constructor *Constructor + options *Options + docCount int +} + +// NewLoader returns a new Loader that reads from r with the given options. +// +// The Loader introduces its own buffering and may read data from r beyond the +// YAML values requested. +func NewLoader(r io.Reader, opts ...Option) (*Loader, error) { + o, err := ApplyOptions(opts...) + if err != nil { + return nil, err + } + c := NewComposerFromReader(r, o) + c.SetStreamNodes(o.StreamNodes) + return &Loader{ + composer: c, + resolver: NewResolver(o), + constructor: NewConstructor(o), + options: o, + }, nil +} + +// Load loads YAML document(s) with the given options. // // By default, Load requires exactly one document in the input. // If zero documents are found, it returns an error. @@ -30,7 +57,7 @@ import ( // yaml.Load(multiDocYAML, &configs, yaml.WithAllDocuments()) // // When WithAllDocuments is used, out must be a pointer to a slice. -// Each document is decoded into the slice element type. +// Each document is loaded into the slice element type. // Zero documents results in an empty slice (no error). // // Maps and pointers (to a struct, string, int, etc) are accepted as out @@ -38,8 +65,8 @@ import ( // the yaml package will initialize it if necessary. The out parameter // must not be nil. // -// The type of the decoded values should be compatible with the respective -// values in out. If one or more values cannot be decoded due to type +// The type of the loaded values should be compatible with the respective +// values in out. If one or more values cannot be loaded due to type // mismatches, decoding continues partially until the end of the YAML // content, and a *yaml.LoadErrors is returned with details for all // missed values. @@ -62,7 +89,7 @@ import ( // See the documentation of Dump for the format of tags and a list of // supported tag options. func Load(in []byte, out any, opts ...Option) error { - o, err := libyaml.ApplyOptions(opts...) + o, err := ApplyOptions(opts...) if err != nil { return err } @@ -76,26 +103,84 @@ func Load(in []byte, out any, opts ...Option) error { return loadSingle(in, out, o) } -// loadAll loads all documents into a slice -func loadAll(in []byte, out any, opts *libyaml.Options) error { +// Load reads the next YAML-encoded document from its input and stores it +// in the value pointed to by v. +// +// Returns [io.EOF] when there are no more documents to read. +// If WithSingleDocument option was set and a document was already read, +// subsequent calls return [io.EOF]. +// +// Maps and pointers (to a struct, string, int, etc) are accepted as v +// values. If an internal pointer within a struct is not initialized, +// the yaml package will initialize it if necessary. The v parameter +// must not be nil. +// +// Struct fields are only loaded if they are exported (have an upper case +// first letter), and are loaded using the field name lowercased as the +// default key. Custom keys may be defined via the "yaml" name in the field +// tag: the content preceding the first comma is used as the key, and the +// following comma-separated options control the loading and dumping behavior. +// +// See the documentation of the package-level Load function for more details +// about YAML to Go conversion and tag options. +func (l *Loader) Load(v any) (err error) { + defer handleErr(&err) + if l.options.SingleDocument && l.docCount > 0 { + return io.EOF + } + + // Stage 1: Compose - parse events into node tree (unresolved tags) + node := l.composer.Compose() // *Node + if node == nil { + return io.EOF + } + l.docCount++ + + // Stage 2: Resolve - determine implicit types for untagged scalars + l.resolver.Resolve(node) + + // Stage 3: Construct - convert node tree to Go values + out := reflect.ValueOf(v) + if out.Kind() == reflect.Pointer && !out.IsNil() { + out = out.Elem() + } + l.constructor.Construct(node, out) + if len(l.constructor.TypeErrors) > 0 { + typeErrors := l.constructor.TypeErrors + l.constructor.TypeErrors = nil + return &LoadErrors{Errors: typeErrors} + } + return nil +} + +// loadAll loads all documents from the input into a slice. +// The out parameter must be a non-nil pointer to a slice. +// Each document is appended to the slice as an element. +func loadAll(in []byte, out any, opts *Options) error { outVal := reflect.ValueOf(out) if outVal.Kind() != reflect.Pointer || outVal.IsNil() { - return &LoadErrors{Errors: []*libyaml.ConstructError{{ - Err: errors.New("yaml: WithAllDocuments requires a non-nil pointer to a slice"), + msg := "yaml: WithAllDocuments requires a non-nil pointer to a slice" + return &LoadErrors{Errors: []*LoadError{{ + Stage: ConstructorStage, + Message: msg, + err: errors.New(msg), }}} } sliceVal := outVal.Elem() if sliceVal.Kind() != reflect.Slice { - return &LoadErrors{Errors: []*libyaml.ConstructError{{ - Err: errors.New("yaml: WithAllDocuments requires a pointer to a slice"), + msg := "yaml: WithAllDocuments requires a pointer to a slice" + return &LoadErrors{Errors: []*LoadError{{ + Stage: ConstructorStage, + Message: msg, + err: errors.New(msg), }}} } // Create a new slice (clear existing content) sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), 0, 0)) - l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.Options) error { + l, err := NewLoader(bytes.NewReader(in), func(o *Options) error { *o = *opts // Copy options return nil }) @@ -114,16 +199,18 @@ func loadAll(in []byte, out any, opts *libyaml.Options) error { if err != nil { return err } - // Append decoded element to slice + // Append loaded element to slice sliceVal.Set(reflect.Append(sliceVal, elemPtr.Elem())) } return nil } -// loadSingle loads exactly one document (strict) -func loadSingle(in []byte, out any, opts *libyaml.Options) error { - l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.Options) error { +// loadSingle loads exactly one document from the input. +// Returns an error if the input contains zero or multiple documents +// (unless FromLegacy option is set for backward compatibility). +func loadSingle(in []byte, out any, opts *Options) error { + l, err := NewLoader(bytes.NewReader(in), func(o *Options) error { *o = *opts // Copy options return nil }) @@ -134,14 +221,25 @@ func loadSingle(in []byte, out any, opts *libyaml.Options) error { // Load first document err = l.Load(out) if err == io.EOF { - return &LoadErrors{Errors: []*libyaml.ConstructError{{ - Err: errors.New("yaml: no documents in stream"), + if opts.FromLegacy { + return nil + } + msg := "yaml: no documents in stream" + return &LoadErrors{Errors: []*LoadError{{ + Stage: ConstructorStage, + Message: msg, + err: errors.New(msg), }}} } if err != nil { return err } + // Skip trailing document check for legacy Unmarshal() compatibility + if opts.FromLegacy { + return nil + } + // Check for additional documents var dummy any err = l.Load(&dummy) @@ -151,81 +249,53 @@ func loadSingle(in []byte, out any, opts *libyaml.Options) error { return err } // Successfully loaded a second document - this is an error in strict mode - return &LoadErrors{Errors: []*libyaml.ConstructError{{ - Err: errors.New("yaml: expected single document, found multiple"), + msg := "yaml: expected single document, found multiple" + return &LoadErrors{Errors: []*LoadError{{ + Stage: ConstructorStage, + Message: msg, + err: errors.New(msg), }}} } return nil } -// A Loader reads and decodes YAML values from an input stream with configurable -// options. -type Loader struct { - composer *libyaml.Composer - decoder *libyaml.Constructor - opts *libyaml.Options - docCount int +// SetKnownFields enables or disables strict field checking for subsequent Load +// calls. +// This is used by the legacy Decoder.KnownFields() method. +func (l *Loader) SetKnownFields(enable bool) { + l.constructor.KnownFields = enable } -// NewLoader returns a new Loader that reads from r with the given options. -// -// The Loader introduces its own buffering and may read data from r beyond the -// YAML values requested. -func NewLoader(r io.Reader, opts ...Option) (*Loader, error) { - o, err := libyaml.ApplyOptions(opts...) - if err != nil { - return nil, err +// ComposeAndResolve composes and resolves the next document from the input +// and returns the node without constructing Go values. This is used by +// Unmarshal() to support the Unmarshaler interface. +func (l *Loader) ComposeAndResolve() *Node { + if l.options.SingleDocument && l.docCount > 0 { + return nil } - c := libyaml.NewComposerFromReader(r) - c.SetStreamNodes(o.StreamNodes) - return &Loader{ - composer: c, - decoder: libyaml.NewConstructor(o), - opts: o, - }, nil -} -// Load reads the next YAML-encoded document from its input and stores it -// in the value pointed to by v. -// -// Returns io.EOF when there are no more documents to read. -// If WithSingleDocument option was set and a document was already read, -// subsequent calls return io.EOF. -// -// Maps and pointers (to a struct, string, int, etc) are accepted as v -// values. If an internal pointer within a struct is not initialized, -// the yaml package will initialize it if necessary. The v parameter -// must not be nil. -// -// Struct fields are only loaded if they are exported (have an upper case -// first letter), and are loaded using the field name lowercased as the -// default key. Custom keys may be defined via the "yaml" name in the field -// tag: the content preceding the first comma is used as the key, and the -// following comma-separated options control the loading and dumping behavior. -// -// See the documentation of the package-level Load function for more details -// about YAML to Go conversion and tag options. -func (l *Loader) Load(v any) (err error) { - defer handleErr(&err) - if l.opts.SingleDocument && l.docCount > 0 { - return io.EOF - } - node := l.composer.Parse() // *libyaml.Node + // Stage 1: Compose - parse events into node tree (unresolved tags) + node := l.composer.Compose() if node == nil { - return io.EOF + return nil } l.docCount++ - out := reflect.ValueOf(v) - if out.Kind() == reflect.Pointer && !out.IsNil() { - out = out.Elem() - } - l.decoder.Construct(node, out) // Pass libyaml.Node directly - if len(l.decoder.TypeErrors) > 0 { - typeErrors := l.decoder.TypeErrors - l.decoder.TypeErrors = nil - return &LoadErrors{Errors: typeErrors} + // Stage 2: Resolve - determine implicit types for untagged scalars + l.resolver.Resolve(node) + + return node +} + +// LoadAny parses YAML data into generic Go structures (map[string]any, []any). +// +// Useful for test data loading where the structure is unknown at compile time. +// This is a convenience wrapper around Load with an any target. +func LoadAny(data []byte) (any, error) { + var result any + if err := Load(data, &result); err != nil { + return nil, err } - return nil + return result, nil } diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/node.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/node.go index 48b258225..4fee73b39 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/node.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/node.go @@ -28,13 +28,17 @@ const ( mergeTag = "!!merge" ) +// longTagPrefix is the standard YAML tag prefix for core types. const longTagPrefix = "tag:yaml.org,2002:" +// longTags maps short tags to their long form representations. +// shortTags maps long tags to their short form representations. var ( longTags = make(map[string]string) shortTags = make(map[string]string) ) +// init initializes the tag conversion maps. func init() { for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} { ltag := longTag(stag) @@ -43,6 +47,7 @@ func init() { } } +// shortTag converts a long-form tag to its short form (e.g., "tag:yaml.org,2002:str" to "!!str"). func shortTag(tag string) string { if strings.HasPrefix(tag, longTagPrefix) { if stag, ok := shortTags[tag]; ok { @@ -53,6 +58,7 @@ func shortTag(tag string) string { return tag } +// longTag converts a short-form tag to its long form (e.g., "!!str" to "tag:yaml.org,2002:str"). func longTag(tag string) string { if strings.HasPrefix(tag, "!!") { if ltag, ok := longTags[tag]; ok { @@ -66,6 +72,7 @@ func longTag(tag string) string { // Kind represents the type of YAML node type Kind uint32 +// Kind constants define the different types of YAML nodes. const ( DocumentNode Kind = 1 << iota SequenceNode @@ -78,6 +85,7 @@ const ( // Style represents the formatting style of a YAML node type Style uint32 +// Style constants define different formatting styles for YAML nodes. const ( TaggedStyle Style = 1 << iota DoubleQuotedStyle @@ -99,6 +107,14 @@ type StreamTagDirective struct { Prefix string } +// Stream holds stream-level metadata for StreamNode. +// This includes encoding, version directive, and tag directives. +type Stream struct { + Encoding Encoding + Version *StreamVersionDirective + TagDirectives []StreamTagDirective +} + // Node represents an element in the YAML document hierarchy. While documents // are typically encoded and decoded into higher level types, such as structs // and maps, Node is an intermediate representation that allows detailed @@ -171,26 +187,15 @@ type Node struct { Line int Column int - // StreamNode-specific fields (only valid when Kind == StreamNode) - - // Encoding holds the stream encoding (UTF-8, UTF-16LE, UTF-16BE). - // Only valid for StreamNode. - Encoding Encoding - - // Version holds the YAML version directive (%YAML). - // Only valid for StreamNode. - Version *StreamVersionDirective - - // TagDirectives holds the %TAG directives. - // Only valid for StreamNode. - TagDirectives []StreamTagDirective + // Stream holds stream metadata (non-nil only when Kind == StreamNode). + Stream *Stream } // IsZero returns whether the node has all of its fields unset. func (n *Node) IsZero() bool { return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil && n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 && - n.Encoding == 0 && n.Version == nil && n.TagDirectives == nil + n.Stream == nil } // LongTag returns the long form of the tag that indicates the data type for @@ -230,6 +235,7 @@ func (n *Node) ShortTag() string { return shortTag(n.Tag) } +// indicatedString returns true if the node's style explicitly indicates a string type. func (n *Node) indicatedString() bool { return n.Kind == ScalarNode && (shortTag(n.Tag) == strTag || @@ -324,14 +330,22 @@ func (n *Node) Load(v any, opts ...Option) (err error) { // conversion of Go values into YAML. func (n *Node) Encode(v any) (err error) { defer handleErr(&err) - e := NewRepresenter(noWriter, DefaultOptions) - defer e.Destroy() - e.MarshalDoc("", reflect.ValueOf(v)) - e.Finish() - p := NewComposer(e.Out) + // Use the 3-stage dump pipeline with round-trip to preserve styles + r := NewRepresenter(DefaultOptions) + node := r.Represent("", reflect.ValueOf(v)) + d := NewDesolver(DefaultOptions) + d.Desolve(node) + s := NewSerializer(nil, DefaultOptions) + var out []byte + s.Emitter.SetOutputString(&out) + s.Serialize(node) + s.Finish() + // Parse back to get styles + p := NewComposer(out, nil) p.Textless = true defer p.Destroy() - doc := p.Parse() + doc := p.Compose() + NewResolver(nil).Resolve(doc) *n = *doc.Content[0] return nil } @@ -350,14 +364,106 @@ func (n *Node) Dump(v any, opts ...Option) (err error) { if err != nil { return err } - e := NewRepresenter(noWriter, o) - defer e.Destroy() - e.MarshalDoc("", reflect.ValueOf(v)) - e.Finish() - p := NewComposer(e.Out) + // Use the 3-stage dump pipeline with round-trip to preserve styles + r := NewRepresenter(o) + node := r.Represent("", reflect.ValueOf(v)) + d := NewDesolver(o) + d.Desolve(node) + s := NewSerializer(nil, o) + var out []byte + s.Emitter.SetOutputString(&out) + s.Serialize(node) + s.Finish() + // Parse back to get styles + p := NewComposer(out, nil) p.Textless = true defer p.Destroy() - doc := p.Parse() + doc := p.Compose() + NewResolver(nil).Resolve(doc) *n = *doc.Content[0] return nil } + +// Marshaler interface may be implemented by types to customize their +// behavior when being marshaled into a YAML document. +type Marshaler interface { + MarshalYAML() (any, error) +} + +// Unmarshaler is the interface implemented by types that can unmarshal +// a YAML description of themselves. +type Unmarshaler interface { + UnmarshalYAML(node *Node) error +} + +// IsZeroer is used to check whether an object is zero to determine whether +// it should be omitted when marshaling with the ,omitempty flag. One notable +// implementation is [time.Time]. +type IsZeroer interface { + IsZero() bool +} + +// FromYAMLNode is a new interface that types can implement to customize +// their unmarshaling behavior. It receives a Node directly and modifies +// the receiver in place. +// This is the preferred interface for new code. +// +// Note: This interface is reserved for the v4 API and is not yet fully +// integrated into the current implementation. +type FromYAMLNode interface { + FromYAMLNode(*Node) error +} + +// ToYAMLNode is a new interface that types can implement to customize +// their marshaling behavior. It returns a Node directly. +// This is the preferred interface for new code. +// +// Note: This interface is reserved for the v4 API and is not yet fully +// integrated into the current implementation. +type ToYAMLNode interface { + ToYAMLNode() (*Node, error) +} + +// isZero reports whether v represents the zero value for its type. +// If v implements the IsZeroer interface, IsZero() is called. +// Otherwise, zero is determined by checking type-specific conditions. +// This is used to determine omitempty behavior when marshaling. +func isZero(v reflect.Value) bool { + kind := v.Kind() + if z, ok := v.Interface().(IsZeroer); ok { + if (kind == reflect.Pointer || kind == reflect.Interface) && v.IsNil() { + return true + } + return z.IsZero() + } + switch kind { + case reflect.String: + return len(v.String()) == 0 + case reflect.Interface, reflect.Pointer: + return v.IsNil() + case reflect.Slice: + return v.Len() == 0 + case reflect.Map: + return v.Len() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Struct: + vt := v.Type() + for i := v.NumField() - 1; i >= 0; i-- { + if vt.Field(i).PkgPath != "" { + continue // Private field + } + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/options.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/options.go index a21c53c06..c2a3ac4db 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/options.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/options.go @@ -1,7 +1,5 @@ -// -// Copyright (c) 2025 The go-yaml Project Contributors +// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 -// // Options configuration for loading and dumping YAML. // Provides centralized control for indentation, line width, strictness, and @@ -34,11 +32,68 @@ type Options struct { ExplicitEnd bool // Always emit ... FlowSimpleCollections bool // Use flow style for simple collections QuotePreference QuoteStyle // Preferred quote style when quoting is required + + // Safety limit checks (set by ApplyOptions or WithPlugin(limit.New(...))) + DepthCheck func(depth int, ctx *DepthContext) error + AliasCheck func(aliasCount, constructCount int) error + + // Private options (not exported, used internally) + FromLegacy bool // Indicates legacy Unmarshal()/Decoder path (check Unmarshaler, allow trailing content) } // Option allows configuring YAML loading and dumping operations. type Option func(*Options) error +// DepthKind represents the type of nesting (flow or block). +type DepthKind string + +// DepthKindFlow and DepthKindBlock are the possible values of DepthContext.Kind. +const ( + DepthKindFlow DepthKind = "flow" + DepthKindBlock DepthKind = "block" +) + +// DepthContext holds context about a nesting depth check. +type DepthContext struct { + Kind DepthKind +} + +// DefaultDepthCheck is the default depth check function. +// It returns an error when depth exceeds 10000. +func DefaultDepthCheck(depth int, ctx *DepthContext) error { + const maxDepth = 10000 + if depth > maxDepth { + return fmt.Errorf("exceeded max depth of %d", maxDepth) + } + return nil +} + +// DefaultAliasCheck is the default alias check function. +// It uses a ratio-based heuristic to prevent DoS attacks via excessive aliasing. +func DefaultAliasCheck(aliasCount, constructCount int) error { + const ( + aliasRatioRangeLow = 400000 + aliasRatioRangeHigh = 4000000 + aliasRatioRange = float64(aliasRatioRangeHigh - aliasRatioRangeLow) + ) + if aliasCount <= 100 || constructCount <= 1000 { + return nil + } + var allowed float64 + switch { + case constructCount <= aliasRatioRangeLow: + allowed = 0.99 + case constructCount >= aliasRatioRangeHigh: + allowed = 0.10 + default: + allowed = 0.99 - 0.89*(float64(constructCount-aliasRatioRangeLow)/aliasRatioRange) + } + if float64(aliasCount)/float64(constructCount) > allowed { + return errors.New("document contains excessive aliasing") + } + return nil +} + // WithIndent sets the number of spaces to use for indentation when // dumping YAML content. // @@ -92,7 +147,7 @@ func WithKnownFields(knownFields ...bool) Option { // WithSingleDocument configures the Loader to only process the first document // in a YAML stream. After the first document is loaded, subsequent calls to -// Load will return io.EOF. +// Load will return [io.EOF]. // // When called without arguments, defaults to true. // @@ -371,6 +426,10 @@ func ApplyOptions(opts ...Option) (*Options, error) { LineWidth: 80, Unicode: true, UniqueKeys: true, + + // Default safety limits + DepthCheck: DefaultDepthCheck, + AliasCheck: DefaultAliasCheck, } for _, opt := range opts { if err := opt(o); err != nil { diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/parser.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/parser.go index 34152d695..64daaaf24 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/parser.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/parser.go @@ -4,7 +4,8 @@ // SPDX-License-Identifier: Apache-2.0 AND MIT // Parser stage: Transforms token stream into event stream. -// Implements a recursive-descent parser (LL(1)) following the YAML grammar specification. +// Implements a recursive-descent parser (LL(1)) following the YAML grammar +// specification. // // The parser implements the following grammar: // @@ -52,59 +53,213 @@ import ( "strings" ) -// Peek the next token in the token queue. -func (parser *Parser) peekToken(out **Token) error { - if !parser.token_available { - if err := parser.fetchMoreTokens(); err != nil { - return err - } +// ReadHandler is called by the [Parser] when it needs to read more bytes +// from the input source. The handler should fill the provided buffer with +// up to len(buffer) bytes from the input source. +// +// The arguments are as follows: +// +// [in] parser The parser object. +// [out] buffer The buffer for reading. +// [out] size_read The actual number of bytes read from the source. +// +// On success, the handler should return 1. If the handler failed, +// the returned value should be 0. On EOF, the handler should set the +// size_read to 0 and return 1. +type ReadHandler func(parser *Parser, buffer []byte) (n int, err error) + +// SimpleKey holds information about a potential simple key. +type SimpleKey struct { + flow_level int // What flow level is the key at? + required bool // Is a simple key required? + token_number int // The number of the token. + mark Mark // The position mark. +} + +// ParserState represents the state of the parser. +type ParserState int + +// Parser state constants define the different states the parser can be in. +const ( + PARSE_STREAM_START_STATE ParserState = iota + + PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document. + PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START. + PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document. + PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END. + PARSE_BLOCK_NODE_STATE // Expect a block node. + PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence. + PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence. + PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence. + PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key. + PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value. + PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence. + PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence. + PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping. + PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping. + PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry. + PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping. + PARSE_END_STATE // Expect nothing. +) + +// String returns a string representation of the parser state. +func (ps ParserState) String() string { + switch ps { + case PARSE_STREAM_START_STATE: + return "PARSE_STREAM_START_STATE" + case PARSE_IMPLICIT_DOCUMENT_START_STATE: + return "PARSE_IMPLICIT_DOCUMENT_START_STATE" + case PARSE_DOCUMENT_START_STATE: + return "PARSE_DOCUMENT_START_STATE" + case PARSE_DOCUMENT_CONTENT_STATE: + return "PARSE_DOCUMENT_CONTENT_STATE" + case PARSE_DOCUMENT_END_STATE: + return "PARSE_DOCUMENT_END_STATE" + case PARSE_BLOCK_NODE_STATE: + return "PARSE_BLOCK_NODE_STATE" + case PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return "PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE" + case PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return "PARSE_BLOCK_SEQUENCE_ENTRY_STATE" + case PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return "PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE" + case PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return "PARSE_BLOCK_MAPPING_FIRST_KEY_STATE" + case PARSE_BLOCK_MAPPING_KEY_STATE: + return "PARSE_BLOCK_MAPPING_KEY_STATE" + case PARSE_BLOCK_MAPPING_VALUE_STATE: + return "PARSE_BLOCK_MAPPING_VALUE_STATE" + case PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return "PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE" + case PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return "PARSE_FLOW_SEQUENCE_ENTRY_STATE" + case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE" + case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE" + case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE" + case PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return "PARSE_FLOW_MAPPING_FIRST_KEY_STATE" + case PARSE_FLOW_MAPPING_KEY_STATE: + return "PARSE_FLOW_MAPPING_KEY_STATE" + case PARSE_FLOW_MAPPING_VALUE_STATE: + return "PARSE_FLOW_MAPPING_VALUE_STATE" + case PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return "PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE" + case PARSE_END_STATE: + return "PARSE_END_STATE" } + return "" +} - token := &parser.tokens[parser.tokens_head] - parser.UnfoldComments(token) - *out = token - return nil +// AliasData holds information about aliases. +type AliasData struct { + anchor []byte // The anchor. + index int // The node id. + mark Mark // The anchor mark. } -// UnfoldComments walks through the comments queue and joins all -// comments behind the position of the provided token into the respective -// top-level comment slices in the parser. -func (parser *Parser) UnfoldComments(token *Token) { - for parser.comments_head < len(parser.comments) && token.StartMark.Index >= parser.comments[parser.comments_head].TokenMark.Index { - comment := &parser.comments[parser.comments_head] - if len(comment.Head) > 0 { - if token.Type == BLOCK_END_TOKEN { - // No heads on ends, so keep comment.Head for a follow up token. - break - } - if len(parser.HeadComment) > 0 { - parser.HeadComment = append(parser.HeadComment, '\n') - } - parser.HeadComment = append(parser.HeadComment, comment.Head...) - } - if len(comment.Foot) > 0 { - if len(parser.FootComment) > 0 { - parser.FootComment = append(parser.FootComment, '\n') - } - parser.FootComment = append(parser.FootComment, comment.Foot...) - } - if len(comment.Line) > 0 { - if len(parser.LineComment) > 0 { - parser.LineComment = append(parser.LineComment, '\n') - } - parser.LineComment = append(parser.LineComment, comment.Line...) - } - *comment = Comment{} - parser.comments_head++ - } +// Comment holds information about a comment in the YAML stream. +type Comment struct { + ScanMark Mark // Position where scanning for comments started + TokenMark Mark // Position after which tokens will be associated with this comment + StartMark Mark // Position of '#' comment mark + EndMark Mark // Position where comment terminated + + Head []byte + Line []byte + Foot []byte } -// Remove the next token from the queue (must be called after peek_token). -func (parser *Parser) skipToken() { - parser.token_available = false - parser.tokens_parsed++ - parser.stream_end_produced = parser.tokens[parser.tokens_head].Type == STREAM_END_TOKEN - parser.tokens_head++ +// Parser structure holds all information about the current +// state of the parser. +type Parser struct { + lastError error + + // Reader stuff + read_handler ReadHandler // Read handler. + + input_reader io.Reader // File input data. + input []byte // String input data. + input_pos int + + eof bool // EOF flag + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + unread int // The number of unread characters in the buffer. + + newlines int // The number of line breaks since last non-break/non-blank character + + raw_buffer []byte // The raw buffer. + raw_buffer_pos int // The current position of the buffer. + + encoding Encoding // The input encoding. + + offset int // The offset of the current position (in bytes). + mark Mark // The mark of the current position. + + // Comments + + HeadComment []byte // The current head comments + LineComment []byte // The current line comments + FootComment []byte // The current foot comments + tail_comment []byte // Foot comment that happens at the end of a block. + stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc) + + comments []Comment // The folded comments for all parsed tokens + comments_head int + + skip_comments bool // Skip comment scanning for performance + + // Scanner stuff + + stream_start_produced bool // Have we started to scan the input stream? + stream_end_produced bool // Have we reached the end of the input stream? + + flow_level int // The number of unclosed '[' and '{' indicators. + + tokens []Token // The tokens queue. + tokens_head int // The head of the tokens queue. + tokens_parsed int // The number of tokens fetched from the queue. + token_available bool // Does the tokens queue contain a token ready for dequeueing. + + indent int // The current indentation level. + indents []int // The indentation levels stack. + + simple_key_allowed bool // May a simple key occur at the current position? + simple_key_possible bool // Is the current simple key possible? + simple_key SimpleKey // The current simple key. + simple_key_stack []SimpleKey // The stack of simple keys. + + depthCheck func(int, *DepthContext) error // Depth limit check function + + // Parser stuff + + state ParserState // The current parser state. + states []ParserState // The parser states stack. + marks []Mark // The stack of marks. + tag_directives []TagDirective // The list of TAG directives. + + // Representer stuff + + aliases []AliasData // The alias data. +} + +// NewParser creates a new parser object. +func NewParser() Parser { + return Parser{ + raw_buffer: make([]byte, 0, input_raw_buffer_size), + buffer: make([]byte, 0, input_buffer_size), + mark: Mark{Line: 1, Column: 1}, + depthCheck: DefaultDepthCheck, + } } // Parse gets the next event. @@ -130,21 +285,74 @@ func (parser *Parser) Parse(event *Event) error { return nil } -func formatParserError(problem string, problem_mark Mark) error { - return ParserError{ - Mark: problem_mark, - Message: problem, +// Delete a parser object. +func (parser *Parser) Delete() { + *parser = Parser{} +} + +// String read handler. +func yamlStringReadHandler(parser *Parser, buffer []byte) (n int, err error) { + if parser.input_pos == len(parser.input) { + return 0, io.EOF } + n = copy(buffer, parser.input[parser.input_pos:]) + parser.input_pos += n + return n, nil } -func formatParserErrorContext(context string, context_mark Mark, problem string, problem_mark Mark) error { - return ParserError{ - ContextMark: context_mark, - ContextMessage: context, +// Reader read handler. +func yamlReaderReadHandler(parser *Parser, buffer []byte) (n int, err error) { + return parser.input_reader.Read(buffer) +} - Mark: problem_mark, - Message: problem, +// SetInputString sets a string input. +func (parser *Parser) SetInputString(input []byte) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yamlStringReadHandler + parser.input = input + parser.input_pos = 0 +} + +// SetInputReader sets a file input. +func (parser *Parser) SetInputReader(r io.Reader) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yamlReaderReadHandler + parser.input_reader = r +} + +// SetEncoding sets the source encoding. +func (parser *Parser) SetEncoding(encoding Encoding) { + if parser.encoding != ANY_ENCODING { + panic("must set the encoding only once") } + parser.encoding = encoding +} + +// GetPendingComments returns the parser's comment queue for CLI access. +func (parser *Parser) GetPendingComments() []Comment { + return parser.comments +} + +// GetCommentsHead returns the current position in the comment queue. +func (parser *Parser) GetCommentsHead() int { + return parser.comments_head +} + +// SetSkipComments enables or disables comment scanning. +// When enabled, the scanner skips comment tokens for better performance. +func (parser *Parser) SetSkipComments(skip bool) { + parser.skip_comments = skip +} + +// default_tag_directives defines the standard tag directives (! and !!) +// that are implicitly available in all YAML documents. +var default_tag_directives = []TagDirective{ + {[]byte("!"), []byte("!")}, + {[]byte("!!"), []byte("tag:yaml.org,2002:")}, } // State dispatcher. @@ -221,9 +429,9 @@ func (parser *Parser) stateMachine(event *Event) error { } // Parse the production: -// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END // -// ************ +// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +// ************ func (parser *Parser) parseStreamStart(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -244,13 +452,11 @@ func (parser *Parser) parseStreamStart(event *Event) error { } // Parse the productions: -// implicit_document ::= block_node DOCUMENT-END* // -// * -// -// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* -// -// ************************* +// implicit_document ::= block_node DOCUMENT-END* +// * +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// ************************* func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -258,12 +464,10 @@ func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { } // Parse extra document end indicators. - if !implicit { - for token.Type == DOCUMENT_END_TOKEN { - parser.skipToken() - if err := parser.peekToken(&token); err != nil { - return err - } + for token.Type == DOCUMENT_END_TOKEN { + parser.skipToken() + if err := parser.peekToken(&token); err != nil { + return err } } @@ -280,9 +484,11 @@ func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { var head_comment []byte if len(parser.HeadComment) > 0 { - // [Go] Scan the header comment backwards, and if an empty line is found, break - // the header so the part before the last empty line goes into the - // document header, while the bottom of it goes into a follow up event. + // [Go] Scan the header comment backwards, and if an + // empty line is found, break the header so the part + // before the last empty line goes into the document + // header, while the bottom of it goes into a follow up + // event. for i := len(parser.HeadComment) - 1; i > 0; i-- { if parser.HeadComment[i] == '\n' { if i == len(parser.HeadComment)-1 { @@ -344,6 +550,7 @@ func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { StartMark: token.StartMark, EndMark: token.EndMark, } + parser.setEventComments(event) parser.skipToken() } @@ -351,9 +558,9 @@ func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { } // Parse the productions: -// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // -// *********** +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// *********** func (parser *Parser) parseDocumentContent(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -374,11 +581,10 @@ func (parser *Parser) parseDocumentContent(event *Event) error { } // Parse the productions: -// implicit_document ::= block_node DOCUMENT-END* // -// ************* -// -// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// implicit_document ::= block_node DOCUMENT-END* +// ************* +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* func (parser *Parser) parseDocumentEnd(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -412,54 +618,113 @@ func (parser *Parser) parseDocumentEnd(event *Event) error { return nil } -func (parser *Parser) setEventComments(event *Event) { - event.HeadComment = parser.HeadComment - event.LineComment = parser.LineComment - event.FootComment = parser.FootComment - parser.HeadComment = nil - parser.LineComment = nil - parser.FootComment = nil - parser.tail_comment = nil - parser.stem_comment = nil +// Parse directives. +func (parser *Parser) processDirectives(version_directive_ref **VersionDirective, tag_directives_ref *[]TagDirective) error { + var version_directive *VersionDirective + var tag_directives []TagDirective + + var token *Token + if err := parser.peekToken(&token); err != nil { + return err + } + + for token.Type == VERSION_DIRECTIVE_TOKEN || token.Type == TAG_DIRECTIVE_TOKEN { + switch token.Type { + case VERSION_DIRECTIVE_TOKEN: + if version_directive != nil { + return formatParserError( + "found duplicate %YAML directive", token.StartMark) + } + if token.major != 1 || token.minor != 1 { + return formatParserError( + "found incompatible YAML document", token.StartMark) + } + version_directive = &VersionDirective{ + major: token.major, + minor: token.minor, + } + case TAG_DIRECTIVE_TOKEN: + value := TagDirective{ + handle: token.Value, + prefix: token.prefix, + } + if err := parser.appendTagDirective(value, false, token.StartMark); err != nil { + return err + } + tag_directives = append(tag_directives, value) + } + + parser.skipToken() + if err := parser.peekToken(&token); err != nil { + return err + } + } + + for i := range default_tag_directives { + if err := parser.appendTagDirective(default_tag_directives[i], true, token.StartMark); err != nil { + return err + } + } + + if version_directive_ref != nil { + *version_directive_ref = version_directive + } + if tag_directives_ref != nil { + *tag_directives_ref = tag_directives + } + return nil +} + +// Append a tag directive to the directives stack. +func (parser *Parser) appendTagDirective(value TagDirective, allow_duplicates bool, mark Mark) error { + for i := range parser.tag_directives { + if bytes.Equal(value.handle, parser.tag_directives[i].handle) { + if allow_duplicates { + return nil + } + return formatParserError("found duplicate %TAG directive", mark) + } + } + + // [Go] I suspect the copy is unnecessary. This was likely done + // because there was no way to track ownership of the data. + value_copy := TagDirective{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(value_copy.handle, value.handle) + copy(value_copy.prefix, value.prefix) + parser.tag_directives = append(parser.tag_directives, value_copy) + return nil } // Parse the productions: -// block_node_or_indentless_sequence ::= // -// ALIAS -// ***** -// | properties (block_content | indentless_block_sequence)? -// ********** * -// | block_content | indentless_block_sequence -// * -// -// block_node ::= ALIAS -// -// ***** -// | properties block_content? -// ********** * -// | block_content -// * -// -// flow_node ::= ALIAS -// -// ***** -// | properties flow_content? -// ********** * -// | flow_content -// * -// -// properties ::= TAG ANCHOR? | ANCHOR TAG? -// -// ************************* -// -// block_content ::= block_collection | flow_collection | SCALAR -// -// ****** -// -// flow_content ::= flow_collection | SCALAR -// -// ****** +// block_node_or_indentless_sequence ::= +// ALIAS +// ***** +// | properties (block_content | indentless_block_sequence)? +// ********** * +// | block_content | indentless_block_sequence +// * +// block_node ::= ALIAS +// ***** +// | properties block_content? +// ********** * +// | block_content +// * +// flow_node ::= ALIAS +// ***** +// | properties flow_content? +// ********** * +// | flow_content +// * +// properties ::= TAG ANCHOR? | ANCHOR TAG? +// ************************* +// block_content ::= block_collection | flow_collection | SCALAR +// ****** +// flow_content ::= flow_collection | SCALAR +// ****** func (parser *Parser) parseNode(event *Event, block, indentless_sequence bool) error { // defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() @@ -683,9 +948,9 @@ func (parser *Parser) parseNode(event *Event, block, indentless_sequence bool) e } // Parse the productions: -// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END // -// ******************** *********** * ********* +// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// ******************** *********** * ********* func (parser *Parser) parseBlockSequenceEntry(event *Event, first bool) error { if first { var token *Token @@ -742,9 +1007,9 @@ func (parser *Parser) parseBlockSequenceEntry(event *Event, first bool) error { } // Parse the productions: -// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ // -// *********** * +// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +// *********** * func (parser *Parser) parseIndentlessSequenceEntry(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -784,10 +1049,11 @@ func (parser *Parser) parseIndentlessSequenceEntry(event *Event) error { // Split stem comment from head comment. // -// When a sequence or map is found under a sequence entry, the former head comment -// is assigned to the underlying sequence or map as a whole, not the individual -// sequence or map entry as would be expected otherwise. To handle this case the -// previous head comment is moved aside as the stem comment. +// When a sequence or map is found under a sequence entry, the former head +// comment is assigned to the underlying sequence or map as a whole, not the +// individual sequence or map entry as would be expected otherwise. +// To handle this case the previous head comment is moved aside as the stem +// comment. func (parser *Parser) splitStemComment(stem_len int) error { if stem_len == 0 { return nil @@ -813,15 +1079,15 @@ func (parser *Parser) splitStemComment(stem_len int) error { } // Parse the productions: -// block_mapping ::= BLOCK-MAPPING_START // -// ******************* -// ((KEY block_node_or_indentless_sequence?)? -// *** * -// (VALUE block_node_or_indentless_sequence?)?)* +// block_mapping ::= BLOCK-MAPPING_START +// ******************* +// ((KEY block_node_or_indentless_sequence?)? +// *** * +// (VALUE block_node_or_indentless_sequence?)?)* // -// BLOCK-END -// ********* +// BLOCK-END +// ********* func (parser *Parser) parseBlockMappingKey(event *Event, first bool) error { if first { var token *Token @@ -837,8 +1103,9 @@ func (parser *Parser) parseBlockMappingKey(event *Event, first bool) error { return err } - // [Go] A tail comment was left from the prior mapping value processed. Emit an event - // as it needs to be processed with that value and not the following key. + // [Go] A tail comment was left from the prior mapping value processed. + // Emit an event as it needs to be processed with that value and not + // the following key. if len(parser.tail_comment) > 0 { *event = Event{ Type: TAIL_COMMENT_EVENT, @@ -888,13 +1155,14 @@ func (parser *Parser) parseBlockMappingKey(event *Event, first bool) error { } // Parse the productions: -// block_mapping ::= BLOCK-MAPPING_START // -// ((KEY block_node_or_indentless_sequence?)? +// block_mapping ::= BLOCK-MAPPING_START +// +// ((KEY block_node_or_indentless_sequence?)? // -// (VALUE block_node_or_indentless_sequence?)?)* -// ***** * -// BLOCK-END +// (VALUE block_node_or_indentless_sequence?)?)* +// ***** * +// BLOCK-END func (parser *Parser) parseBlockMappingValue(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -920,19 +1188,17 @@ func (parser *Parser) parseBlockMappingValue(event *Event) error { } // Parse the productions: -// flow_sequence ::= FLOW-SEQUENCE-START -// -// ******************* -// (flow_sequence_entry FLOW-ENTRY)* -// * ********** -// flow_sequence_entry? -// * -// FLOW-SEQUENCE-END -// ***************** -// -// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // -// * +// flow_sequence ::= FLOW-SEQUENCE-START +// ******************* +// (flow_sequence_entry FLOW-ENTRY)* +// * ********** +// flow_sequence_entry? +// * +// FLOW-SEQUENCE-END +// ***************** +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * func (parser *Parser) parseFlowSequenceEntry(event *Event, first bool) error { if first { var token *Token @@ -995,9 +1261,9 @@ func (parser *Parser) parseFlowSequenceEntry(event *Event, first bool) error { } // Parse the productions: -// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // -// *** * +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// *** * func (parser *Parser) parseFlowSequenceEntryMappingKey(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -1016,9 +1282,9 @@ func (parser *Parser) parseFlowSequenceEntryMappingKey(event *Event) error { } // Parse the productions: -// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // -// ***** * +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// ***** * func (parser *Parser) parseFlowSequenceEntryMappingValue(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -1040,9 +1306,9 @@ func (parser *Parser) parseFlowSequenceEntryMappingValue(event *Event) error { } // Parse the productions: -// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // -// * +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * func (parser *Parser) parseFlowSequenceEntryMappingEnd(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -1058,18 +1324,17 @@ func (parser *Parser) parseFlowSequenceEntryMappingEnd(event *Event) error { } // Parse the productions: -// flow_mapping ::= FLOW-MAPPING-START // -// ****************** -// (flow_mapping_entry FLOW-ENTRY)* -// * ********** -// flow_mapping_entry? -// ****************** -// FLOW-MAPPING-END -// **************** -// -// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -// - *** * +// flow_mapping ::= FLOW-MAPPING-START +// ****************** +// (flow_mapping_entry FLOW-ENTRY)* +// * ********** +// flow_mapping_entry? +// ****************** +// FLOW-MAPPING-END +// **************** +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * *** * func (parser *Parser) parseFlowMappingKey(event *Event, first bool) error { if first { var token *Token @@ -1135,8 +1400,9 @@ func (parser *Parser) parseFlowMappingKey(event *Event, first bool) error { } // Parse the productions: -// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -// - ***** * +// +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * ***** * func (parser *Parser) parseFlowMappingValue(event *Event, empty bool) error { var token *Token if err := parser.peekToken(&token); err != nil { @@ -1160,107 +1426,112 @@ func (parser *Parser) parseFlowMappingValue(event *Event, empty bool) error { return parser.processEmptyScalar(event, token.StartMark) } -// Generate an empty scalar event. -func (parser *Parser) processEmptyScalar(event *Event, mark Mark) error { - *event = Event{ - Type: SCALAR_EVENT, - StartMark: mark, - EndMark: mark, - Value: nil, // Empty - Implicit: true, - Style: Style(PLAIN_SCALAR_STYLE), +// Peek the next token in the token queue. +func (parser *Parser) peekToken(out **Token) error { + if !parser.token_available { + if err := parser.fetchMoreTokens(); err != nil { + return err + } } - return nil -} -var default_tag_directives = []TagDirective{ - {[]byte("!"), []byte("!")}, - {[]byte("!!"), []byte("tag:yaml.org,2002:")}, + token := &parser.tokens[parser.tokens_head] + parser.UnfoldComments(token) + *out = token + return nil } -// Parse directives. -func (parser *Parser) processDirectives(version_directive_ref **VersionDirective, tag_directives_ref *[]TagDirective) error { - var version_directive *VersionDirective - var tag_directives []TagDirective - - var token *Token - if err := parser.peekToken(&token); err != nil { - return err - } - - for token.Type == VERSION_DIRECTIVE_TOKEN || token.Type == TAG_DIRECTIVE_TOKEN { - switch token.Type { - case VERSION_DIRECTIVE_TOKEN: - if version_directive != nil { - return formatParserError( - "found duplicate %YAML directive", token.StartMark) - } - if token.major != 1 || token.minor != 1 { - return formatParserError( - "found incompatible YAML document", token.StartMark) - } - version_directive = &VersionDirective{ - major: token.major, - minor: token.minor, +// UnfoldComments walks through the comments queue and joins all +// comments behind the position of the provided token into the respective +// top-level comment slices in the parser. +func (parser *Parser) UnfoldComments(token *Token) { + for parser.comments_head < len(parser.comments) && token.StartMark.Index >= parser.comments[parser.comments_head].TokenMark.Index { + comment := &parser.comments[parser.comments_head] + if len(comment.Head) > 0 { + if token.Type == BLOCK_END_TOKEN { + // No heads on ends, so keep comment.Head for a follow up token. + break } - case TAG_DIRECTIVE_TOKEN: - value := TagDirective{ - handle: token.Value, - prefix: token.prefix, + if len(parser.HeadComment) > 0 { + parser.HeadComment = append(parser.HeadComment, '\n') } - if err := parser.appendTagDirective(value, false, token.StartMark); err != nil { - return err + parser.HeadComment = append(parser.HeadComment, comment.Head...) + } + if len(comment.Foot) > 0 { + if len(parser.FootComment) > 0 { + parser.FootComment = append(parser.FootComment, '\n') } - tag_directives = append(tag_directives, value) + parser.FootComment = append(parser.FootComment, comment.Foot...) } - - parser.skipToken() - if err := parser.peekToken(&token); err != nil { - return err + if len(comment.Line) > 0 { + if len(parser.LineComment) > 0 { + parser.LineComment = append(parser.LineComment, '\n') + } + parser.LineComment = append(parser.LineComment, comment.Line...) } + *comment = Comment{} + parser.comments_head++ } +} - for i := range default_tag_directives { - if err := parser.appendTagDirective(default_tag_directives[i], true, token.StartMark); err != nil { - return err - } - } +// Remove the next token from the queue (must be called after peek_token). +func (parser *Parser) skipToken() { + parser.token_available = false + parser.tokens_parsed++ + parser.stream_end_produced = parser.tokens[parser.tokens_head].Type == STREAM_END_TOKEN + parser.tokens_head++ +} - if version_directive_ref != nil { - *version_directive_ref = version_directive - } - if tag_directives_ref != nil { - *tag_directives_ref = tag_directives +// formatParserError creates a LoadError with the given problem message +// and mark position. +func formatParserError(problem string, problemMark Mark) *LoadError { + return &LoadError{ + Stage: ParserStage, + Mark: problemMark, + Message: problem, } - return nil } -// Append a tag directive to the directives stack. -func (parser *Parser) appendTagDirective(value TagDirective, allow_duplicates bool, mark Mark) error { - for i := range parser.tag_directives { - if bytes.Equal(value.handle, parser.tag_directives[i].handle) { - if allow_duplicates { - return nil - } - return formatParserError("found duplicate %TAG directive", mark) - } +// formatParserErrorContext creates a LoadError with both context and +// problem information, each with their own mark positions. +func formatParserErrorContext(context string, contextMark Mark, problem string, problemMark Mark) *LoadError { + return &LoadError{ + Stage: ParserStage, + ContextMark: contextMark, + ContextMsg: context, + Mark: problemMark, + Message: problem, } +} - // [Go] I suspect the copy is unnecessary. This was likely done - // because there was no way to track ownership of the data. - value_copy := TagDirective{ - handle: make([]byte, len(value.handle)), - prefix: make([]byte, len(value.prefix)), +// setEventComments transfers accumulated comments from the parser to the +// event and clears the parser's comment state. +func (parser *Parser) setEventComments(event *Event) { + event.HeadComment = parser.HeadComment + event.LineComment = parser.LineComment + event.FootComment = parser.FootComment + parser.HeadComment = nil + parser.LineComment = nil + parser.FootComment = nil + parser.tail_comment = nil + parser.stem_comment = nil +} + +// Generate an empty scalar event. +func (parser *Parser) processEmptyScalar(event *Event, mark Mark) error { + *event = Event{ + Type: SCALAR_EVENT, + StartMark: mark, + EndMark: mark, + Value: nil, // Empty + Implicit: true, + Style: Style(PLAIN_SCALAR_STYLE), } - copy(value_copy.handle, value.handle) - copy(value_copy.prefix, value.prefix) - parser.tag_directives = append(parser.tag_directives, value_copy) return nil } // ParserGetEvents parses the YAML input and returns the generated event stream. func ParserGetEvents(in []byte) (string, error) { - p := NewComposer(in) + p := NewComposer(in, nil) defer p.Destroy() var events strings.Builder var event Event @@ -1280,6 +1551,8 @@ func ParserGetEvents(in []byte) (string, error) { return events.String(), nil } +// formatEvent formats an event as a human-readable string for debugging +// and testing purposes. func formatEvent(e *Event) string { var b strings.Builder switch e.Type { diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/reader.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/reader.go index ecc00fe28..b2c1a53bd 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/reader.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/reader.go @@ -15,89 +15,6 @@ import ( "io" ) -func formatReaderError(problem string, offset int, value int) error { - return ReaderError{ - Offset: offset, - Value: value, - Err: errors.New(problem), - } -} - -// Byte order marks. -const ( - bom_UTF8 = "\xef\xbb\xbf" - bom_UTF16LE = "\xff\xfe" - bom_UTF16BE = "\xfe\xff" -) - -// Determine the input stream encoding by checking the BOM symbol. If no BOM is -// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure. -func (parser *Parser) determineEncoding() error { - // Ensure that we had enough bytes in the raw buffer. - for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { - if err := parser.updateRawBuffer(); err != nil { - return err - } - } - - // Determine the encoding. - buf := parser.raw_buffer - pos := parser.raw_buffer_pos - avail := len(buf) - pos - if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] { - parser.encoding = UTF16LE_ENCODING - parser.raw_buffer_pos += 2 - parser.offset += 2 - } else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] { - parser.encoding = UTF16BE_ENCODING - parser.raw_buffer_pos += 2 - parser.offset += 2 - } else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] { - parser.encoding = UTF8_ENCODING - parser.raw_buffer_pos += 3 - parser.offset += 3 - } else { - parser.encoding = UTF8_ENCODING - } - return nil -} - -// Update the raw buffer. -func (parser *Parser) updateRawBuffer() error { - size_read := 0 - - // Return if the raw buffer is full. - if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { - return nil - } - - // Return on EOF. - if parser.eof { - return nil - } - - // Move the remaining bytes in the raw buffer to the beginning. - if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { - copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) - } - parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] - parser.raw_buffer_pos = 0 - - // Call the read handler to fill the buffer. - size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) - parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] - if err == io.EOF { - parser.eof = true - } else if err != nil { - return ReaderError{ - Offset: parser.offset, - Value: -1, - Err: fmt.Errorf("input error: %w", err), - } - } - return nil -} - // Ensure that the buffer contains at least `length` characters. // Return true on success, false on failure. // @@ -107,9 +24,10 @@ func (parser *Parser) updateBuffer(length int) error { panic("read handler must be set") } - // [Go] This function was changed to guarantee the requested length size at EOF. - // The fact we need to do this is pretty awful, but the description above implies - // for that to be the case, and there are tests + // [Go] This function was changed to guarantee the requested length + // size at EOF. + // The fact we need to do this is pretty awful, but the description + // above implies for that to be the case, and there are tests // If the EOF flag is set and the raw buffer is empty, do nothing. // @@ -205,8 +123,8 @@ func (parser *Parser) updateBuffer(length int) error { default: // The leading octet is invalid. return formatReaderError( - "invalid leading UTF-8 octet", - parser.offset, int(octet)) + fmt.Sprintf("invalid leading UTF-8 octet (value: %d)", octet), + Mark{Index: parser.offset}) } // Check if the raw buffer contains an incomplete character. @@ -214,7 +132,7 @@ func (parser *Parser) updateBuffer(length int) error { if parser.eof { return formatReaderError( "incomplete UTF-8 octet sequence", - parser.offset, -1) + Mark{Index: parser.offset}) } break inner } @@ -240,8 +158,8 @@ func (parser *Parser) updateBuffer(length int) error { // Check if the octet is valid. if (octet & 0xC0) != 0x80 { return formatReaderError( - "invalid trailing UTF-8 octet", - parser.offset+k, int(octet)) + fmt.Sprintf("invalid trailing UTF-8 octet (value: %d)", octet), + Mark{Index: parser.offset + k}) } // Decode the octet. @@ -257,14 +175,14 @@ func (parser *Parser) updateBuffer(length int) error { default: return formatReaderError( "invalid length of a UTF-8 sequence", - parser.offset, -1) + Mark{Index: parser.offset}) } // Check the range of the value. if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF { return formatReaderError( - "invalid Unicode character", - parser.offset, int(value)) + fmt.Sprintf("invalid Unicode character (value: %d)", value), + Mark{Index: parser.offset}) } case UTF16LE_ENCODING, UTF16BE_ENCODING: @@ -304,7 +222,7 @@ func (parser *Parser) updateBuffer(length int) error { if parser.eof { return formatReaderError( "incomplete UTF-16 character", - parser.offset, -1) + Mark{Index: parser.offset}) } break inner } @@ -316,8 +234,8 @@ func (parser *Parser) updateBuffer(length int) error { // Check for unexpected low surrogate area. if value&0xFC00 == 0xDC00 { return formatReaderError( - "unexpected low surrogate area", - parser.offset, int(value)) + fmt.Sprintf("unexpected low surrogate area (value: %d)", value), + Mark{Index: parser.offset}) } // Check for a high surrogate area. @@ -329,7 +247,7 @@ func (parser *Parser) updateBuffer(length int) error { if parser.eof { return formatReaderError( "incomplete UTF-16 surrogate pair", - parser.offset, -1) + Mark{Index: parser.offset}) } break inner } @@ -341,8 +259,8 @@ func (parser *Parser) updateBuffer(length int) error { // Check for a low surrogate area. if value2&0xFC00 != 0xDC00 { return formatReaderError( - "expected low surrogate area", - parser.offset+2, int(value2)) + fmt.Sprintf("expected low surrogate area (value: %d)", value2), + Mark{Index: parser.offset + 2}) } // Generate the value of the surrogate pair. @@ -383,8 +301,8 @@ func (parser *Parser) updateBuffer(length int) error { case value >= 0x10000 && value <= 0x10FFFF: default: return formatReaderError( - "control characters are not allowed", - parser.offset, int(value)) + fmt.Sprintf("control characters are not allowed (value: %d)", value), + Mark{Index: parser.offset}) } // Move the raw pointers. @@ -439,3 +357,90 @@ func (parser *Parser) updateBuffer(length int) error { parser.buffer = parser.buffer[:buffer_len] return nil } + +// Byte order marks for UTF-8, UTF-16LE, and UTF-16BE encodings. +const ( + bom_UTF8 = "\xef\xbb\xbf" + bom_UTF16LE = "\xff\xfe" + bom_UTF16BE = "\xfe\xff" +) + +// Determine the input stream encoding by checking the BOM symbol. +// If no BOM is found, the UTF-8 encoding is assumed. +// Return 1 on success, 0 on failure. +func (parser *Parser) determineEncoding() error { + // Ensure that we had enough bytes in the raw buffer. + for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { + if err := parser.updateRawBuffer(); err != nil { + return err + } + } + + // Determine the encoding. + buf := parser.raw_buffer + pos := parser.raw_buffer_pos + avail := len(buf) - pos + if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] { + parser.encoding = UTF16LE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] { + parser.encoding = UTF16BE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] { + parser.encoding = UTF8_ENCODING + parser.raw_buffer_pos += 3 + parser.offset += 3 + } else { + parser.encoding = UTF8_ENCODING + } + return nil +} + +// Update the raw buffer. +func (parser *Parser) updateRawBuffer() error { + size_read := 0 + + // Return if the raw buffer is full. + if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { + return nil + } + + // Return on EOF. + if parser.eof { + return nil + } + + // Move the remaining bytes in the raw buffer to the beginning. + if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { + copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) + } + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] + parser.raw_buffer_pos = 0 + + // Call the read handler to fill the buffer. + size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] + if err == io.EOF { + parser.eof = true + } else if err != nil { + return &LoadError{ + Stage: ReaderStage, + Message: fmt.Sprintf("input error: %v", err), + Mark: Mark{Index: parser.offset}, + err: err, + } + } + return nil +} + +// formatReaderError creates a LoadError for reader-stage errors. +func formatReaderError(message string, mark Mark) *LoadError { + return &LoadError{ + Stage: ReaderStage, + Message: message, + Mark: mark, + err: errors.New(message), + } +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/representer.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/representer.go index 2f451949f..c9f1b21f7 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/representer.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/representer.go @@ -3,14 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 // Representer stage: Converts Go values to YAML nodes. -// Handles marshaling from Go types to the intermediate node representation. +// Handles representing from Go types to the intermediate node representation. package libyaml import ( "encoding" - "fmt" - "io" "reflect" "regexp" "sort" @@ -21,10 +19,399 @@ import ( "unicode/utf8" ) +// keyList is a sortable slice of reflect.Values used for sorting map keys +// in a natural order (numeric, then lexicographic). type keyList []reflect.Value -func (l keyList) Len() int { return len(l) } +// Representer converts Go values to YAML node trees with configurable +// formatting options. +type Representer struct { + flow bool + Indent int + lineWidth int + explicitStart bool + explicitEnd bool + flowSimpleCollections bool + quotePreference QuoteStyle +} + +// NewRepresenter creates a new YAML representer with the given options. +func NewRepresenter(opts *Options) *Representer { + return &Representer{ + Indent: opts.Indent, + lineWidth: opts.LineWidth, + explicitStart: opts.ExplicitStart, + explicitEnd: opts.ExplicitEnd, + flowSimpleCollections: opts.FlowSimpleCollections, + quotePreference: opts.QuotePreference, + } +} + +// Represent converts a Go value to a YAML node tree. +// This is the primary method for the Representer stage in the dump pipeline. +func (r *Representer) Represent(tag string, in reflect.Value) *Node { + var node *Node + if in.IsValid() { + node, _ = in.Interface().(*Node) + } + if node != nil && node.Kind == DocumentNode { + // Already a document node, return as-is + return node + } else { + // Wrap the represented value in a document node + contentNode := r.represent(tag, in) + return &Node{ + Kind: DocumentNode, + Content: []*Node{contentNode}, + } + } +} + +// From http://yaml.org/type/float.html, except the regular expression there +// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix. +var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`) + +// represent is the core conversion method that handles the actual +// type-specific conversion from Go values to YAML nodes. +func (r *Representer) represent(tag string, in reflect.Value) *Node { + tag = shortTag(tag) + if !in.IsValid() || in.Kind() == reflect.Pointer && in.IsNil() { + return r.nilv() + } + iface := in.Interface() + switch value := iface.(type) { + case *Node: + return r.nodev(in) + case Node: + if !in.CanAddr() { + n := reflect.New(in.Type()).Elem() + n.Set(in) + in = n + } + return r.nodev(in.Addr()) + case time.Time: + return r.timev(tag, in) + case *time.Time: + return r.timev(tag, in.Elem()) + case time.Duration: + return r.stringv(tag, reflect.ValueOf(value.String())) + case Marshaler: + v, err := value.MarshalYAML() + if err != nil { + failDump(RepresenterStage, err) + } + if v == nil { + return r.nilv() + } + return r.represent(tag, reflect.ValueOf(v)) + case encoding.TextMarshaler: + text, err := value.MarshalText() + if err != nil { + failDump(RepresenterStage, err) + } + in = reflect.ValueOf(string(text)) + case nil: + return r.nilv() + } + switch in.Kind() { + case reflect.Interface: + return r.represent(tag, in.Elem()) + case reflect.Map: + return r.mapv(tag, in) + case reflect.Pointer: + return r.represent(tag, in.Elem()) + case reflect.Struct: + return r.structv(tag, in) + case reflect.Slice, reflect.Array: + return r.slicev(tag, in) + case reflect.String: + return r.stringv(tag, in) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return r.intv(tag, in) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return r.uintv(tag, in) + case reflect.Float32, reflect.Float64: + return r.floatv(tag, in) + case reflect.Bool: + return r.boolv(tag, in) + default: + failDumpf(RepresenterStage, "cannot represent type: %s", in.Type().String()) + return nil // unreachable; failDumpf always panics + } +} + +// mapv converts a Go map to a YAML mapping node with sorted keys. +func (r *Representer) mapv(tag string, in reflect.Value) *Node { + if tag == "" { + tag = mapTag + } + var style Style + if r.flow { + r.flow = false + style = FlowStyle + } + + keys := keyList(in.MapKeys()) + sort.Sort(keys) + content := make([]*Node, 0, len(keys)*2) + for _, k := range keys { + content = append(content, r.represent("", k)) + content = append(content, r.represent("", in.MapIndex(k))) + } + + return &Node{ + Kind: MappingNode, + Tag: tag, + Content: content, + Style: style, + } +} + +// structv converts a Go struct to a YAML mapping node, handling field tags, +// omitempty, inline fields, and inline maps. +func (r *Representer) structv(tag string, in reflect.Value) *Node { + sinfo, err := getStructInfo(in.Type()) + if err != nil { + failDump(RepresenterStage, err) + } + + if tag == "" { + tag = mapTag + } + var style Style + if r.flow { + r.flow = false + style = FlowStyle + } + + content := make([]*Node, 0) + for _, info := range sinfo.FieldsList { + var value reflect.Value + if info.Inline == nil { + value = in.Field(info.Num) + } else { + value = r.fieldByIndex(in, info.Inline) + if !value.IsValid() { + continue + } + } + if info.OmitEmpty && isZero(value) { + continue + } + content = append(content, r.represent("", reflect.ValueOf(info.Key))) + r.flow = info.Flow + content = append(content, r.represent("", value)) + } + if sinfo.InlineMap >= 0 { + m := in.Field(sinfo.InlineMap) + if m.Len() > 0 { + r.flow = false + keys := keyList(m.MapKeys()) + sort.Sort(keys) + for _, k := range keys { + if _, found := sinfo.FieldsMap[k.String()]; found { + failDumpf(RepresenterStage, "cannot have key %q in inlined map: conflicts with struct field", k.String()) + } + content = append(content, r.represent("", k)) + r.flow = false + content = append(content, r.represent("", m.MapIndex(k))) + } + } + } + + return &Node{ + Kind: MappingNode, + Tag: tag, + Content: content, + Style: style, + } +} + +// slicev converts a Go slice or array to a YAML sequence node. +func (r *Representer) slicev(tag string, in reflect.Value) *Node { + if tag == "" { + tag = seqTag + } + var style Style + if r.flow { + r.flow = false + style = FlowStyle + } + + n := in.Len() + content := make([]*Node, n) + for i := 0; i < n; i++ { + content[i] = r.represent("", in.Index(i)) + } + + return &Node{ + Kind: SequenceNode, + Tag: tag, + Content: content, + Style: style, + } +} + +// stringv converts a Go string to a YAML scalar node, handling quoting, +// binary data (base64 encoding), and special string values. +func (r *Representer) stringv(tag string, in reflect.Value) *Node { + var style Style + s := in.String() + needsQuoting := false + + switch { + case !utf8.ValidString(s): + if tag == binaryTag { + failDumpf(RepresenterStage, "explicitly tagged !!binary data must be base64-encoded") + } + if tag != "" { + failDumpf(RepresenterStage, "cannot represent invalid UTF-8 data as %s", shortTag(tag)) + } + // It can't be represented directly as YAML so use a binary tag + // and represent it as base64. + tag = binaryTag + s = encodeBase64(s) + case tag == "": + tag = strTag + // Check if this string needs quoting for compatibility + // even though it would resolve as !!str + needsQuoting = isBase60Float(s) || isOldBool(s) || looksLikeMerge(s) + } + + // Set the style based on content + switch { + case strings.Contains(s, "\n"): + if r.flow || !shouldUseLiteralStyle(s) { + style = DoubleQuotedStyle + } else { + style = LiteralStyle + } + case needsQuoting: + // Force quoting for YAML 1.1 compatibility values + style = SingleQuotedStyle + default: + // Plain style by default - Desolver will add quotes if type mismatch + style = 0 + } + + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + Style: style, + } +} + +// boolv converts a Go bool to a YAML scalar node. +func (r *Representer) boolv(tag string, in reflect.Value) *Node { + var s string + if in.Bool() { + s = "true" + } else { + s = "false" + } + if tag == "" { + tag = boolTag + } + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + } +} + +// intv converts a Go signed integer to a YAML scalar node. +func (r *Representer) intv(tag string, in reflect.Value) *Node { + s := strconv.FormatInt(in.Int(), 10) + if tag == "" { + tag = intTag + } + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + } +} + +// uintv converts a Go unsigned integer to a YAML scalar node. +func (r *Representer) uintv(tag string, in reflect.Value) *Node { + s := strconv.FormatUint(in.Uint(), 10) + if tag == "" { + tag = intTag + } + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + } +} + +// timev converts a Go [time.Time] to a YAML scalar node in RFC3339Nano format. +func (r *Representer) timev(tag string, in reflect.Value) *Node { + t := in.Interface().(time.Time) + s := t.Format(time.RFC3339Nano) + if tag == "" { + tag = timestampTag + } + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + } +} + +// floatv converts a Go float to a YAML scalar node, handling special values +// like infinity and NaN. +func (r *Representer) floatv(tag string, in reflect.Value) *Node { + // Issue #352: When formatting, use the precision of the underlying value + precision := 64 + if in.Kind() == reflect.Float32 { + precision = 32 + } + + s := strconv.FormatFloat(in.Float(), 'g', -1, precision) + switch s { + case "+Inf": + s = ".inf" + case "-Inf": + s = "-.inf" + case "NaN": + s = ".nan" + } + if tag == "" { + tag = floatTag + } + return &Node{ + Kind: ScalarNode, + Tag: tag, + Value: s, + } +} + +// nilv creates a YAML null node. +func (r *Representer) nilv() *Node { + return &Node{ + Kind: ScalarNode, + Tag: nullTag, + Value: "null", + } +} + +// nodev returns a node value as-is without conversion. +func (r *Representer) nodev(in reflect.Value) *Node { + // Return the node as-is - no conversion needed + return in.Interface().(*Node) +} + +// Len returns the number of keys in the list. +func (l keyList) Len() int { return len(l) } + +// Swap exchanges the positions of two keys in the list. func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +// Less implements a natural sort order for map keys: numeric values sort +// numerically, strings sort with natural number ordering, and mixed types +// sort by kind. func (l keyList) Less(i, j int) bool { a := l[i] b := l[j] @@ -134,198 +521,8 @@ func numLess(a, b reflect.Value) bool { panic("not a number") } -// Sentinel values for newRepresenter parameters. -// These provide clarity at call sites, similar to http.NoBody. -var ( - noWriter io.Writer = nil - noVersionDirective *VersionDirective = nil - noTagDirective []TagDirective = nil -) - -type Representer struct { - Emitter Emitter - Out []byte - flow bool - Indent int - lineWidth int - doneInit bool - explicitStart bool - explicitEnd bool - flowSimpleCollections bool - quotePreference QuoteStyle -} - -// NewRepresenter creates a new YAML representr with the given options. -// -// The writer parameter specifies the output destination for the representr. -// If writer is nil, the representr will write to an internal buffer. -func NewRepresenter(writer io.Writer, opts *Options) *Representer { - emitter := NewEmitter() - emitter.CompactSequenceIndent = opts.CompactSeqIndent - emitter.quotePreference = opts.QuotePreference - emitter.SetWidth(opts.LineWidth) - emitter.SetUnicode(opts.Unicode) - emitter.SetCanonical(opts.Canonical) - emitter.SetLineBreak(opts.LineBreak) - - r := &Representer{ - Emitter: emitter, - Indent: opts.Indent, - lineWidth: opts.LineWidth, - explicitStart: opts.ExplicitStart, - explicitEnd: opts.ExplicitEnd, - flowSimpleCollections: opts.FlowSimpleCollections, - quotePreference: opts.QuotePreference, - } - - if writer != nil { - r.Emitter.SetOutputWriter(writer) - } else { - r.Emitter.SetOutputString(&r.Out) - } - - return r -} - -func (r *Representer) init() { - if r.doneInit { - return - } - if r.Indent == 0 { - r.Indent = 4 - } - r.Emitter.BestIndent = r.Indent - r.emit(NewStreamStartEvent(UTF8_ENCODING)) - r.doneInit = true -} - -func (r *Representer) Finish() { - r.Emitter.OpenEnded = false - r.emit(NewStreamEndEvent()) -} - -func (r *Representer) Destroy() { - r.Emitter.Delete() -} - -func (r *Representer) emit(event Event) { - // This will internally delete the event value. - r.must(r.Emitter.Emit(&event)) -} - -func (r *Representer) must(err error) { - if err != nil { - msg := err.Error() - if msg == "" { - msg = "unknown problem generating YAML content" - } - failf("%s", msg) - } -} - -func (r *Representer) MarshalDoc(tag string, in reflect.Value) { - r.init() - var node *Node - if in.IsValid() { - node, _ = in.Interface().(*Node) - } - if node != nil && node.Kind == DocumentNode { - r.nodev(in) - } else { - // Use !explicitStart for implicit flag (true = implicit/no marker) - r.emit(NewDocumentStartEvent(noVersionDirective, noTagDirective, !r.explicitStart)) - r.marshal(tag, in) - // Use !explicitEnd for implicit flag - r.emit(NewDocumentEndEvent(!r.explicitEnd)) - } -} - -func (r *Representer) marshal(tag string, in reflect.Value) { - tag = shortTag(tag) - if !in.IsValid() || in.Kind() == reflect.Pointer && in.IsNil() { - r.nilv() - return - } - iface := in.Interface() - switch value := iface.(type) { - case *Node: - r.nodev(in) - return - case Node: - if !in.CanAddr() { - n := reflect.New(in.Type()).Elem() - n.Set(in) - in = n - } - r.nodev(in.Addr()) - return - case time.Time: - r.timev(tag, in) - return - case *time.Time: - r.timev(tag, in.Elem()) - return - case time.Duration: - r.stringv(tag, reflect.ValueOf(value.String())) - return - case Marshaler: - v, err := value.MarshalYAML() - if err != nil { - Fail(err) - } - if v == nil { - r.nilv() - return - } - r.marshal(tag, reflect.ValueOf(v)) - return - case encoding.TextMarshaler: - text, err := value.MarshalText() - if err != nil { - Fail(err) - } - in = reflect.ValueOf(string(text)) - case nil: - r.nilv() - return - } - switch in.Kind() { - case reflect.Interface: - r.marshal(tag, in.Elem()) - case reflect.Map: - r.mapv(tag, in) - case reflect.Pointer: - r.marshal(tag, in.Elem()) - case reflect.Struct: - r.structv(tag, in) - case reflect.Slice, reflect.Array: - r.slicev(tag, in) - case reflect.String: - r.stringv(tag, in) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - r.intv(tag, in) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - r.uintv(tag, in) - case reflect.Float32, reflect.Float64: - r.floatv(tag, in) - case reflect.Bool: - r.boolv(tag, in) - default: - panic("cannot marshal type: " + in.Type().String()) - } -} - -func (r *Representer) mapv(tag string, in reflect.Value) { - r.mappingv(tag, func() { - keys := keyList(in.MapKeys()) - sort.Sort(keys) - for _, k := range keys { - r.marshal("", k) - r.marshal("", in.MapIndex(k)) - } - }) -} - +// fieldByIndex navigates through struct fields using the given index path, +// dereferencing pointers as needed. func (r *Representer) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) { for _, num := range index { for { @@ -343,79 +540,10 @@ func (r *Representer) fieldByIndex(v reflect.Value, index []int) (field reflect. return v } -func (r *Representer) structv(tag string, in reflect.Value) { - sinfo, err := getStructInfo(in.Type()) - if err != nil { - panic(err) - } - r.mappingv(tag, func() { - for _, info := range sinfo.FieldsList { - var value reflect.Value - if info.Inline == nil { - value = in.Field(info.Num) - } else { - value = r.fieldByIndex(in, info.Inline) - if !value.IsValid() { - continue - } - } - if info.OmitEmpty && isZero(value) { - continue - } - r.marshal("", reflect.ValueOf(info.Key)) - r.flow = info.Flow - r.marshal("", value) - } - if sinfo.InlineMap >= 0 { - m := in.Field(sinfo.InlineMap) - if m.Len() > 0 { - r.flow = false - keys := keyList(m.MapKeys()) - sort.Sort(keys) - for _, k := range keys { - if _, found := sinfo.FieldsMap[k.String()]; found { - panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String())) - } - r.marshal("", k) - r.flow = false - r.marshal("", m.MapIndex(k)) - } - } - } - }) -} - -func (r *Representer) mappingv(tag string, f func()) { - implicit := tag == "" - style := BLOCK_MAPPING_STYLE - if r.flow { - r.flow = false - style = FLOW_MAPPING_STYLE - } - r.emit(NewMappingStartEvent(nil, []byte(tag), implicit, style)) - f() - r.emit(NewMappingEndEvent()) -} - -func (r *Representer) slicev(tag string, in reflect.Value) { - implicit := tag == "" - style := BLOCK_SEQUENCE_STYLE - if r.flow { - r.flow = false - style = FLOW_SEQUENCE_STYLE - } - r.emit(NewSequenceStartEvent(nil, []byte(tag), implicit, style)) - n := in.Len() - for i := 0; i < n; i++ { - r.marshal("", in.Index(i)) - } - r.emit(NewSequenceEndEvent()) -} - // isBase60 returns whether s is in base 60 notation as defined in YAML 1.1. // // The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported -// in YAML 1.2 and by this package, but these should be marshaled quoted for +// in YAML 1.2 and by this package, but these should be represented quoted for // the time being for compatibility with other parsers. func isBase60Float(s string) (result bool) { // Fast path. @@ -430,14 +558,10 @@ func isBase60Float(s string) (result bool) { return base60float.MatchString(s) } -// From http://yaml.org/type/float.html, except the regular expression there -// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix. -var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`) - // isOldBool returns whether s is bool notation as defined in YAML 1.1. // // We continue to force strings that YAML 1.1 would interpret as booleans to be -// rendered as quotes strings so that the marshaled output valid for YAML 1.1 +// rendered as quotes strings so that the represented output valid for YAML 1.1 // parsing. func isOldBool(s string) (result bool) { switch s { @@ -456,116 +580,3 @@ func isOldBool(s string) (result bool) { func looksLikeMerge(s string) (result bool) { return s == "<<" } - -func (r *Representer) stringv(tag string, in reflect.Value) { - var style ScalarStyle - s := in.String() - canUsePlain := true - switch { - case !utf8.ValidString(s): - if tag == binaryTag { - failf("explicitly tagged !!binary data must be base64-encoded") - } - if tag != "" { - failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag)) - } - // It can't be represented directly as YAML so use a binary tag - // and represent it as base64. - tag = binaryTag - s = encodeBase64(s) - case tag == "": - // Check to see if it would resolve to a specific - // tag when represented unquoted. If it doesn't, - // there's no need to quote it. - rtag, _ := resolve("", s) - canUsePlain = rtag == strTag && - !(isBase60Float(s) || - isOldBool(s) || - looksLikeMerge(s)) - } - // Note: it's possible for user code to emit invalid YAML - // if they explicitly specify a tag and a string containing - // text that's incompatible with that tag. - switch { - case strings.Contains(s, "\n"): - if r.flow || !shouldUseLiteralStyle(s) { - style = DOUBLE_QUOTED_SCALAR_STYLE - } else { - style = LITERAL_SCALAR_STYLE - } - case canUsePlain: - style = PLAIN_SCALAR_STYLE - default: - style = r.quotePreference.ScalarStyle() - } - r.emitScalar(s, "", tag, style, nil, nil, nil, nil) -} - -func (r *Representer) boolv(tag string, in reflect.Value) { - var s string - if in.Bool() { - s = "true" - } else { - s = "false" - } - r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) intv(tag string, in reflect.Value) { - s := strconv.FormatInt(in.Int(), 10) - r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) uintv(tag string, in reflect.Value) { - s := strconv.FormatUint(in.Uint(), 10) - r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) timev(tag string, in reflect.Value) { - t := in.Interface().(time.Time) - s := t.Format(time.RFC3339Nano) - r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) floatv(tag string, in reflect.Value) { - // Issue #352: When formatting, use the precision of the underlying value - precision := 64 - if in.Kind() == reflect.Float32 { - precision = 32 - } - - s := strconv.FormatFloat(in.Float(), 'g', -1, precision) - switch s { - case "+Inf": - s = ".inf" - case "-Inf": - s = "-.inf" - case "NaN": - s = ".nan" - } - r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) nilv() { - r.emitScalar("null", "", "", PLAIN_SCALAR_STYLE, nil, nil, nil, nil) -} - -func (r *Representer) emitScalar( - value, anchor, tag string, style ScalarStyle, head, line, foot, tail []byte, -) { - // TODO Kill this function. Replace all initialize calls by their underlining Go literals. - implicit := tag == "" - if !implicit { - tag = longTag(tag) - } - event := NewScalarEvent([]byte(anchor), []byte(tag), []byte(value), implicit, implicit, style) - event.HeadComment = head - event.LineComment = line - event.FootComment = foot - event.TailComment = tail - r.emit(event) -} - -func (r *Representer) nodev(in reflect.Value) { - r.node(in.Interface().(*Node), "") -} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/resolver.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/resolver.go index d78a20e8e..5256b77fc 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/resolver.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/resolver.go @@ -10,6 +10,7 @@ package libyaml import ( "encoding/base64" + "fmt" "math" "regexp" "strconv" @@ -17,67 +18,77 @@ import ( "time" ) +// resolveMapItem holds a resolved value and its YAML tag for exact string +// matches in the resolution table. type resolveMapItem struct { value any tag string } -var ( - resolveTable = make([]byte, 256) - resolveMap = make(map[string]resolveMapItem) -) +// Resolver handles tag resolution for YAML nodes. +type Resolver struct { + opts *Options +} -// negativeZero represents -0.0 for YAML encoding/decoding -// this is needed because Go constants cannot express -0.0 -// https://staticcheck.dev/docs/checks/#SA4026 -var negativeZero = math.Copysign(0.0, -1.0) +// NewResolver creates a new Resolver with the given options. +func NewResolver(opts *Options) *Resolver { + return &Resolver{opts: opts} +} -func init() { - t := resolveTable - t[int('+')] = 'S' // Sign - t[int('-')] = 'S' - for _, c := range "0123456789" { - t[int(c)] = 'D' // Digit +// Resolve walks the node tree and resolves tags for untagged nodes. +// This is called after composition to: +// - Default quoted scalars to !!str +// - Default sequences to !!seq +// - Default mappings to !!map +// - Resolve plain scalars to implicit types (int, float, bool, null, timestamp) +func (r *Resolver) Resolve(n *Node) { + if n == nil { + return } - for _, c := range "yYnNtTfFoO~<" { // < for merge key << - t[int(c)] = 'M' // In map - } - t[int('.')] = '.' // Float (potentially in map) - resolveMapList := []struct { - v any - tag string - l []string - }{ - {true, boolTag, []string{"true", "True", "TRUE"}}, - {false, boolTag, []string{"false", "False", "FALSE"}}, - {nil, nullTag, []string{"", "~", "null", "Null", "NULL"}}, - {math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}}, - {math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}}, - {math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}}, - {math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}}, - {negativeZero, floatTag, []string{"-0", "-0.0"}}, - {"<<", mergeTag, []string{"<<"}}, - } + switch n.Kind { + case ScalarNode: + if n.Tag == "" { + if n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { + // Quoted scalars default to !!str without value resolution + n.Tag = strTag + } else { + // Plain scalars: resolve type from value + n.Tag, _ = resolve("", n.Value) + } + } - m := resolveMap - for _, item := range resolveMapList { - for _, s := range item.l { - m[s] = resolveMapItem{item.v, item.tag} + case SequenceNode: + if n.Tag == "" { + n.Tag = seqTag + } + for _, child := range n.Content { + r.Resolve(child) } - } -} -func resolvableTag(tag string) bool { - switch tag { - case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: - return true + case MappingNode: + if n.Tag == "" { + n.Tag = mapTag + } + for _, child := range n.Content { + r.Resolve(child) + } + + case DocumentNode: + for _, child := range n.Content { + r.Resolve(child) + } + + case AliasNode: + // Alias nodes point to already-resolved nodes } - return false } -var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`) - +// resolve determines the YAML tag and Go value for a scalar string. +// It takes a tag hint and the scalar string value, and returns the resolved +// tag and the corresponding Go value (int, float, bool, [time.Time], etc.). +// If the tag is already specified and non-resolvable, it returns the input +// unchanged. func resolve(tag string, in string) (rtag string, out any) { tag = shortTag(tag) if !resolvableTag(tag) { @@ -102,7 +113,10 @@ func resolve(tag string, in string) (rtag string, out any) { } } } - failf("cannot construct %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)) + Fail(formatResolverError( + fmt.Sprintf("cannot construct %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)), + Mark{}, + )) }() // Any data is accepted as a !!str or !!binary. @@ -134,8 +148,8 @@ func resolve(tag string, in string) (rtag string, out any) { case 'D', 'S': // Int, float, or timestamp. - // Only try values as a timestamp if the value is unquoted or there's an explicit - // !!timestamp tag. + // Only try values as a timestamp if the value is + // unquoted or there's an explicit !!timestamp tag. if tag == "" || tag == timestampTag { t, ok := parseTimestamp(in) if ok { @@ -169,6 +183,83 @@ func resolve(tag string, in string) (rtag string, out any) { return strTag, in } +// resolveTable provides a fast lookup table for initial character-based +// classification during tag resolution. +// resolveMap maps specific scalar strings to their resolved values and tags. +var ( + resolveTable = make([]byte, 256) + resolveMap = make(map[string]resolveMapItem) +) + +// negativeZero represents -0.0 for YAML encoding/decoding +// this is needed because Go constants cannot express -0.0 +// https://staticcheck.dev/docs/checks/#SA4026 +var negativeZero = math.Copysign(0.0, -1.0) + +// yamlStyleFloat matches floating-point numbers in YAML style (including +// scientific notation and numbers starting with a dot). +var yamlStyleFloat = regexp.MustCompile(`^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?$`) + +// allowedTimestampFormats lists the timestamp formats supported by the +// resolver. +// This is a subset of the formats allowed by the regular expression +// defined at http://yaml.org/type/timestamp.html. +var allowedTimestampFormats = []string{ + "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // space separated with no time zone + "2006-1-2", // date only + // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" + // from the set of examples. +} + +// init initializes the resolveTable with character class mappings for tag resolution. +func init() { + t := resolveTable + t[int('+')] = 'S' // Sign + t[int('-')] = 'S' + for _, c := range "0123456789" { + t[int(c)] = 'D' // Digit + } + for _, c := range "yYnNtTfFoO~<" { // < for merge key << + t[int(c)] = 'M' // In map + } + t[int('.')] = '.' // Float (potentially in map) + + resolveMapList := []struct { + v any + tag string + l []string + }{ + {true, boolTag, []string{"true", "True", "TRUE"}}, + {false, boolTag, []string{"false", "False", "FALSE"}}, + {nil, nullTag, []string{"", "~", "null", "Null", "NULL"}}, + {math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}}, + {math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}}, + {math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}}, + {math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}}, + {negativeZero, floatTag, []string{"-0", "-0.0"}}, + {"<<", mergeTag, []string{"<<"}}, + } + + m := resolveMap + for _, item := range resolveMapList { + for _, s := range item.l { + m[s] = resolveMapItem{item.v, item.tag} + } + } +} + +// resolvableTag checks if a tag can be automatically resolved from a scalar +// value. +func resolvableTag(tag string) bool { + switch tag { + case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: + return true + } + return false +} + // encodeBase64 encodes s as base64 that is broken up into multiple lines // as appropriate for the resulting length. func encodeBase64(s string) string { @@ -194,17 +285,6 @@ func encodeBase64(s string) string { return string(out[:k]) } -// This is a subset of the formats allowed by the regular expression -// defined at http://yaml.org/type/timestamp.html. -var allowedTimestampFormats = []string{ - "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. - "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". - "2006-1-2 15:4:5.999999999", // space separated with no time zone - "2006-1-2", // date only - // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" - // from the set of examples. -} - // parseTimestamp parses s as a timestamp string and // returns the timestamp and reports whether it succeeded. // Timestamp formats are defined at http://yaml.org/type/timestamp.html @@ -229,3 +309,24 @@ func parseTimestamp(s string) (time.Time, bool) { } return time.Time{}, false } + +// formatResolverError creates a LoadError for resolver-stage errors. +func formatResolverError(message string, mark Mark) *LoadError { + return &LoadError{ + Stage: ResolverStage, + Mark: mark, + Message: message, + } +} + +// formatResolverErrorContext creates a LoadError with both context and +// problem information for resolver-stage errors. +func formatResolverErrorContext(context string, contextMark Mark, message string, mark Mark) *LoadError { + return &LoadError{ + Stage: ResolverStage, + ContextMark: contextMark, + ContextMsg: context, + Mark: mark, + Message: message, + } +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/scanner.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/scanner.go index 36f62b0ea..1cbcc32f5 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/scanner.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/scanner.go @@ -490,96 +490,267 @@ import ( // BLOCK-END // -// Advance the buffer pointer. -func (parser *Parser) skip() { - if !isBlank(parser.buffer, parser.buffer_pos) { - parser.newlines = 0 - } - parser.mark.Index++ - parser.mark.Column++ - parser.unread-- - parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) +// Buffer sizes and internal constants +const ( + // The size of the input raw buffer. + input_raw_buffer_size = 512 + + // The size of the input buffer. + // It should be possible to decode the whole raw buffer. + input_buffer_size = input_raw_buffer_size * 3 + + // The size of the output buffer. + output_buffer_size = 128 + + // The size of other stacks and queues. + initial_stack_size = 16 + initial_queue_size = 16 + initial_string_size = 16 +) + +// Character classification functions + +// Check if the character at the specified position is an alphabetical +// character, a digit, '_', or '-'. +func isAlpha(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || + b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' } -func (parser *Parser) skipLine() { - if isCRLF(parser.buffer, parser.buffer_pos) { - parser.mark.Index += 2 - parser.mark.Column = 0 - parser.mark.Line++ - parser.unread -= 2 - parser.buffer_pos += 2 - parser.newlines++ - } else if isLineBreak(parser.buffer, parser.buffer_pos) { - parser.mark.Index++ - parser.mark.Column = 0 - parser.mark.Line++ - parser.unread-- - parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) - parser.newlines++ +// Check if the character at the specified position is a flow indicator as +// defined by spec production [23] c-flow-indicator ::= +// c-collect-entry | c-sequence-start | c-sequence-end | +// c-mapping-start | c-mapping-end +func isFlowIndicator(b []byte, i int) bool { + return b[i] == '[' || b[i] == ']' || + b[i] == '{' || b[i] == '}' || b[i] == ',' +} + +// Check if the character at the specified position is valid for anchor names +// as defined by spec production [102] ns-anchor-char ::= ns-char - +// c-flow-indicator. +// This includes all printable characters except: CR, LF, BOM, space, tab, '[', +// ']', '{', '}', ','. +// 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) && + !isBOM(b, i) && + !isFlowIndicator(b, i) && + isASCII(b, i) } -// Copy a character to a string buffer and advance pointers. -func (parser *Parser) read(s []byte) []byte { - if !isBlank(parser.buffer, parser.buffer_pos) { - parser.newlines = 0 +// 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 valid in a tag URI. +// +// The set of valid characters is: +// +// '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', +// '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. +// +// If verbatim is true, flow indicators (',', '[', ']', '{', '}') are also +// allowed. +func isTagURIChar(b []byte, i int, verbatim bool) bool { + c := b[i] + // isAlpha covers: 0-9, A-Z, a-z, _, - + if isAlpha(b, i) { + return true } - w := width(parser.buffer[parser.buffer_pos]) - if w == 0 { - panic("invalid character sequence") + // Check special URI characters + switch c { + case ';', '/', '?', ':', '@', '&', '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%': + return true + case ',', '[', ']', '{', '}': + return verbatim } - if len(s) == 0 { - s = make([]byte, 0, 32) + return false +} + +// 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' +} + +// Get the value of a digit. +func asDigit(b []byte, i int) int { + return int(b[i]) - '0' +} + +// Check if the character at the specified position is a hex-digit. +func isHex(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || + b[i] >= 'a' && b[i] <= 'f' +} + +// Get the value of a hex-digit. +func asHex(b []byte, i int) int { + bi := b[i] + if bi >= 'A' && bi <= 'F' { + return int(bi) - 'A' + 10 } - if w == 1 && len(s)+w <= cap(s) { - s = s[:len(s)+1] - s[len(s)-1] = parser.buffer[parser.buffer_pos] - parser.buffer_pos++ - } else { - s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) - parser.buffer_pos += w + if bi >= 'a' && bi <= 'f' { + return int(bi) - 'a' + 10 } - parser.mark.Index++ - parser.mark.Column++ - parser.unread-- - return s + return int(bi) - '0' } -// Copy a line break character to a string buffer and advance pointers. -func (parser *Parser) readLine(s []byte) []byte { - buf := parser.buffer - pos := parser.buffer_pos - switch { - case buf[pos] == '\r' && buf[pos+1] == '\n': - // CR LF . LF - s = append(s, '\n') - parser.buffer_pos += 2 - parser.mark.Index++ - parser.unread-- - case buf[pos] == '\r' || buf[pos] == '\n': - // CR|LF . LF - s = append(s, '\n') - parser.buffer_pos += 1 - case buf[pos] == '\xC2' && buf[pos+1] == '\x85': - // NEL . LF - s = append(s, '\n') - parser.buffer_pos += 2 - case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'): - // LS|PS . LS|PS - s = append(s, buf[parser.buffer_pos:pos+3]...) - parser.buffer_pos += 3 +// Check if the character is ASCII. +func isASCII(b []byte, i int) bool { + return b[i] <= 0x7F +} + +// Check if the character at the start of the buffer can be printed unescaped. +func isPrintable(b []byte, i int) bool { + return ((b[i] == 0x0A) || // . == #x0A + (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E + (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF + (b[i] > 0xC2 && b[i] < 0xED) || + (b[i] == 0xED && b[i+1] < 0xA0) || + (b[i] == 0xEE) || + (b[i] == 0xEF && // #xE000 <= . <= #xFFFD + !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF + !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) +} + +// Check if the character at the specified position is NUL. +func isZeroChar(b []byte, i int) bool { + return b[i] == 0x00 +} + +// Check if the beginning of the buffer is a BOM. +func isBOM(b []byte, i int) bool { + return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF +} + +// Check if the character at the specified position is space. +func isSpace(b []byte, i int) bool { + return b[i] == ' ' +} + +// Check if the character at the specified position is tab. +func isTab(b []byte, i int) bool { + return b[i] == '\t' +} + +// Check if the character at the specified position is blank (space or tab). +func isBlank(b []byte, i int) bool { + // return isSpace(b, i) || isTab(b, i) + return b[i] == ' ' || b[i] == '\t' +} + +// Check if the character at the specified position is a line break. +func isLineBreak(b []byte, i int) bool { + return (b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) +} + +// isCRLF checks if the position contains a CR LF sequence. +func isCRLF(b []byte, i int) bool { + return b[i] == '\r' && b[i+1] == '\n' +} + +// Check if the character is a line break or NUL. +func isBreakOrZero(b []byte, i int) bool { + // return isLineBreak(b, i) || isZeroChar(b, i) + return ( + // isBreak: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + // isZeroChar: + b[i] == 0) +} + +// Check if the character is a line break, space, or NUL. +func isSpaceOrZero(b []byte, i int) bool { + // return isSpace(b, i) || isBreakOrZero(b, i) + return ( + // isSpace: + b[i] == ' ' || + // isBreakOrZero: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +// Check if the character is a line break, space, tab, or NUL. +func isBlankOrZero(b []byte, i int) bool { + // return isBlank(b, i) || isBreakOrZero(b, i) + return ( + // isBlank: + b[i] == ' ' || b[i] == '\t' || + // isBreakOrZero: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +func isEndOfScalarInFlowContentChar(b []byte, i int) bool { + switch b[i] { + // ",", "[", "]", "{" and "}" + case ',', '[', ']', '{', '}': + return true + // "? " + case '?': + return isBlankOrZero(b, i+1) + // ": ", ":,", ":]" and ":}" + case ':': + return b[i+1] == ' ' || b[i+1] == ',' || + b[i+1] == ']' || b[i+1] == '}' default: - return s + return false } - parser.mark.Index++ - parser.mark.Column = 0 - parser.mark.Line++ - parser.unread-- - parser.newlines++ - return s } -// Scan gets the next token. +// Determine the width of the character. +func width(b byte) int { + // Don't replace these by a switch without first + // confirming that it is being inlined. + if b&0x80 == 0x00 { + return 1 + } + if b&0xE0 == 0xC0 { + return 2 + } + if b&0xF0 == 0xE0 { + return 3 + } + if b&0xF8 == 0xF0 { + return 4 + } + return 0 +} + +// Scan advances the buffer pointer and reads the next token. func (parser *Parser) Scan(token *Token) error { // Erase the token object. *token = Token{} // [Go] Is this necessary? @@ -613,45 +784,13 @@ func (parser *Parser) Scan(token *Token) error { return nil } -func formatScannerError(problem string, problem_mark Mark) error { - problem_mark.Line += 1 - - return ScannerError{ - Mark: problem_mark, - Message: problem, - } -} - -func formatScannerErrorContext(context string, context_mark Mark, problem string, problem_mark Mark) error { - context_mark.Line += 1 - problem_mark.Line += 1 +// formatScannerError creates a ScannerError with the given problem message +// and mark position. - return ScannerError{ - ContextMark: context_mark, - ContextMessage: context, - - Mark: problem_mark, - Message: problem, - } -} - -func (parser *Parser) setScannerTagError(directive bool, context_mark Mark, problem string) error { - context := "while parsing a tag" - if directive { - context = "while parsing a %TAG directive" - } - return formatScannerErrorContext(context, context_mark, problem, parser.mark) -} - -func trace(args ...any) func() { - pargs := append([]any{"+++"}, args...) - fmt.Println(pargs...) - pargs = append([]any{"---"}, args...) - return func() { fmt.Println(pargs...) } -} +// max_number_length is the maximum length of a number suffix in a scalar tag. +const max_number_length = 2 -// Ensure that the tokens queue contains at least one token which can be -// returned to the Parser. +// fetchMoreTokens ensures the token queue has at least one token for lookahead. func (parser *Parser) fetchMoreTokens() error { // While we need more tokens to fetch, do it. for { @@ -738,7 +877,7 @@ func (parser *Parser) fetchNextToken() (err error) { } // Is it a directive? - if parser.mark.Column == 0 && parser.buffer[parser.buffer_pos] == '%' { + if parser.mark.Column == 1 && parser.buffer[parser.buffer_pos] == '%' { return parser.fetchDirective() } @@ -746,12 +885,12 @@ func (parser *Parser) fetchNextToken() (err error) { pos := parser.buffer_pos // Is it the document start indicator? - if parser.mark.Column == 0 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && isBlankOrZero(buf, pos+3) { + if parser.mark.Column == 1 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && isBlankOrZero(buf, pos+3) { return parser.fetchDocumentIndicator(DOCUMENT_START_TOKEN) } // Is it the document end indicator? - if parser.mark.Column == 0 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && isBlankOrZero(buf, pos+3) { + if parser.mark.Column == 1 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && isBlankOrZero(buf, pos+3) { return parser.fetchDocumentIndicator(DOCUMENT_END_TOKEN) } @@ -764,6 +903,9 @@ func (parser *Parser) fetchNextToken() (err error) { if err != nil { return } + if parser.skip_comments { + return + } if len(parser.tokens) > 0 && parser.tokens[len(parser.tokens)-1].Type == BLOCK_ENTRY_TOKEN { // Sequence indicators alone have no line comments. It becomes // a head comment for whatever follows. @@ -810,7 +952,8 @@ func (parser *Parser) fetchNextToken() (err error) { } // Is it the value indicator? - if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 && !parser.isFlowSequence() || isBlankOrZero(parser.buffer, parser.buffer_pos+1)) { + if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 && + !parser.isFlowSequence() || isBlankOrZero(parser.buffer, parser.buffer_pos+1)) { return parser.fetchValue() } @@ -891,249 +1034,88 @@ func (parser *Parser) fetchNextToken() (err error) { "found character that cannot start any token", parser.mark) } -func (parser *Parser) isFlowSequence() bool { - if len(parser.tokens) == 0 { - return false +// isFlowSequence checks if the previous token indicates we're in a flow +// sequence context. +func (parser *Parser) fetchAnchor(typ TokenType) error { + // An anchor or an alias could be a simple key. + if err := parser.saveSimpleKey(); err != nil { + return err } - previousToken := parser.tokens[len(parser.tokens)-1] - return previousToken.Type == FLOW_ENTRY_TOKEN || previousToken.Type == FLOW_SEQUENCE_START_TOKEN -} -// Check if a simple key may start at the current position and add it if -// needed. -func (parser *Parser) saveSimpleKey() error { - // A simple key is required at the current position if the scanner is in - // the block context and the current column coincides with the indentation - // level. + // A simple key cannot follow an anchor or an alias. + parser.simple_key_allowed = false - required := parser.flow_level == 0 && parser.indent == parser.mark.Column + // Create the ALIAS or ANCHOR token and append it to the queue. + var token Token + if err := parser.scanAnchor(&token, typ); err != nil { + return err + } + parser.insertToken(-1, &token) + return nil +} - // - // If the current position may start a simple key, save it. - // - if parser.simple_key_allowed { - if err := parser.removeSimpleKey(); err != nil { +// Produce the TAG token. +func (parser *Parser) fetchBlockEntry() error { + // Check if the scanner is in the block context. + if parser.flow_level == 0 { + // Check if we are allowed to start a new entry. + if !parser.simple_key_allowed { + return formatScannerError("block sequence entries are not allowed in this context", parser.mark) + } + // Add the BLOCK-SEQUENCE-START token if needed. + if err := parser.rollIndent(parser.mark.Column, -1, BLOCK_SEQUENCE_START_TOKEN, parser.mark); err != nil { return err } + } else { //nolint:staticcheck // there is no problem with this empty branch as it's documentation. - parser.simple_key_possible = true - parser.simple_key = SimpleKey{ - required: required, - flow_level: parser.flow_level, - token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), - mark: parser.mark, - } + // It is an error for the '-' indicator to occur in the flow context, + // but we let the Parser detect and report about it because the Parser + // is able to point to the context. } - return nil -} -// Remove a potential simple key at the current flow level. -func (parser *Parser) removeSimpleKey() error { - // If the key is required, it is an error. - if parser.simple_key.required { - return formatScannerErrorContext( - "while scanning a simple key", parser.simple_key.mark, - "could not find expected ':'", parser.mark) + // Reset any potential simple keys on the current flow level. + if err := parser.removeSimpleKey(); err != nil { + return err } - parser.simple_key_possible = false // disable the key - return nil -} + // Simple keys are allowed after '-'. + parser.simple_key_allowed = true -// max_flow_level limits the flow_level -const max_flow_level = 10000 + // Consume the token. + start_mark := parser.mark + parser.skip() + end_mark := parser.mark -// Increase the flow level and resize the simple key list if needed. -func (parser *Parser) increaseFlowLevel() error { - // Increase the flow level. - parser.flow_level++ - if parser.flow_level > max_flow_level { - return formatScannerErrorContext( - "while increasing flow level", parser.simple_key.mark, - fmt.Sprintf("exceeded max depth of %d", max_flow_level), parser.mark) + // Create the BLOCK-ENTRY token and append it to the queue. + token := Token{ + Type: BLOCK_ENTRY_TOKEN, + StartMark: start_mark, + EndMark: end_mark, } + parser.insertToken(-1, &token) + return nil +} - // If a simple key was possible, push it to the stack before resetting the key. - if parser.simple_key_possible { - parser.simple_key_stack = append(parser.simple_key_stack, parser.simple_key) +// Produce the KEY token. +func (parser *Parser) fetchBlockScalar(literal bool) error { + // Remove any potential simple keys. + if err := parser.removeSimpleKey(); err != nil { + return err } - // Reset the simple key for the new flow level. - parser.simple_key = SimpleKey{} + // A simple key may follow a block scalar. + parser.simple_key_allowed = true + // Create the SCALAR token and append it to the queue. + var token Token + if err := parser.scanBlockScalar(&token, literal); err != nil { + return err + } + parser.insertToken(-1, &token) return nil } -// Decrease the flow level. -func (parser *Parser) decreaseFlowLevel() error { - if parser.flow_level > 0 { - parser.flow_level-- - - if len(parser.simple_key_stack) == 0 { - return nil - } - - last := len(parser.simple_key_stack) - 1 - if parser.simple_key_stack[last].flow_level == parser.flow_level { - parser.simple_key = parser.simple_key_stack[last] // use last item - parser.simple_key_stack = parser.simple_key_stack[:last] // remove last item - parser.simple_key_possible = true // enable the key - } - } - return nil -} - -// max_indents limits the indents stack size -const max_indents = 10000 - -// Push the current indentation level to the stack and set the new level -// the current column is greater than the indentation level. In this case, -// append or insert the specified token into the token queue. -func (parser *Parser) rollIndent(column, number int, typ TokenType, mark Mark) error { - // In the flow context, do nothing. - if parser.flow_level > 0 { - return nil - } - - if parser.indent < column { - // Push the current indentation level to the stack and set the new - // indentation level. - parser.indents = append(parser.indents, parser.indent) - parser.indent = column - if len(parser.indents) > max_indents { - return formatScannerErrorContext( - "while increasing indent level", parser.simple_key.mark, - fmt.Sprintf("exceeded max depth of %d", max_indents), parser.mark) - } - - // Create a token and insert it into the queue. - token := Token{ - Type: typ, - StartMark: mark, - EndMark: mark, - } - if number > -1 { - number -= parser.tokens_parsed - } - parser.insertToken(number, &token) - } - return nil -} - -// Pop indentation levels from the indents stack until the current level -// becomes less or equal to the column. For each indentation level, append -// the BLOCK-END token. -func (parser *Parser) unrollIndent(column int, scan_mark Mark) error { - // In the flow context, do nothing. - if parser.flow_level > 0 { - return nil - } - - block_mark := scan_mark - block_mark.Index-- - - // Loop through the indentation levels in the stack. - for parser.indent > column { - - // [Go] Reposition the end token before potential following - // foot comments of parent blocks. For that, search - // backwards for recent comments that were at the same - // indent as the block that is ending now. - stop_index := block_mark.Index - for i := len(parser.comments) - 1; i >= 0; i-- { - comment := &parser.comments[i] - - if comment.EndMark.Index < stop_index { - // Don't go back beyond the start of the comment/whitespace scan, unless column < 0. - // If requested indent column is < 0, then the document is over and everything else - // is a foot anyway. - break - } - if comment.StartMark.Column == parser.indent+1 { - // This is a good match. But maybe there's a former comment - // at that same indent level, so keep searching. - block_mark = comment.StartMark - } - - // While the end of the former comment matches with - // the start of the following one, we know there's - // nothing in between and scanning is still safe. - stop_index = comment.ScanMark.Index - } - - // Create a token and append it to the queue. - token := Token{ - Type: BLOCK_END_TOKEN, - StartMark: block_mark, - EndMark: block_mark, - } - parser.insertToken(-1, &token) - - // Pop the indentation level. - parser.indent = parser.indents[len(parser.indents)-1] - parser.indents = parser.indents[:len(parser.indents)-1] - } - return nil -} - -// Initialize the scanner and produce the STREAM-START token. -func (parser *Parser) fetchStreamStart() error { - // Set the initial indentation. - parser.indent = -1 - - // Initialize the simple key stack. - parser.simple_key = SimpleKey{} - parser.simple_key_stack = []SimpleKey{} - - // A simple key is allowed at the beginning of the stream. - parser.simple_key_allowed = true - - // We have started. - parser.stream_start_produced = true - - // Create the STREAM-START token and append it to the queue. - token := Token{ - Type: STREAM_START_TOKEN, - StartMark: parser.mark, - EndMark: parser.mark, - encoding: parser.encoding, - } - parser.insertToken(-1, &token) - return nil -} - -// Produce the STREAM-END token and shut down the scanner. -func (parser *Parser) fetchStreamEnd() error { - // Force new line. - if parser.mark.Column != 0 { - parser.mark.Column = 0 - parser.mark.Line++ - } - - // Reset the indentation level. - if err := parser.unrollIndent(-1, parser.mark); err != nil { - return err - } - - // Reset simple keys. - if err := parser.removeSimpleKey(); err != nil { - return err - } - parser.simple_key = SimpleKey{} - parser.simple_key_stack = []SimpleKey{} - parser.simple_key_allowed = false - - // Create the STREAM-END token and append it to the queue. - token := Token{ - Type: STREAM_END_TOKEN, - StartMark: parser.mark, - EndMark: parser.mark, - } - parser.insertToken(-1, &token) - return nil -} - -// Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. +// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. func (parser *Parser) fetchDirective() error { // Reset the indentation level. if err := parser.unrollIndent(-1, parser.mark); err != nil { @@ -1192,26 +1174,27 @@ func (parser *Parser) fetchDocumentIndicator(typ TokenType) error { } // Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. -func (parser *Parser) fetchFlowCollectionStart(typ TokenType) error { - // The indicators '[' and '{' may start a simple key. - if err := parser.saveSimpleKey(); err != nil { +func (parser *Parser) fetchFlowCollectionEnd(typ TokenType) error { + // Reset any potential simple key on the current flow level. + if err := parser.removeSimpleKey(); err != nil { return err } - // Increase the flow level. - if err := parser.increaseFlowLevel(); err != nil { + // Decrease the flow level. + if err := parser.decreaseFlowLevel(); err != nil { return err } - // A simple key may follow the indicators '[' and '{'. - parser.simple_key_allowed = true + // No simple keys after the indicators ']' and '}'. + parser.simple_key_allowed = false // Consume the token. + start_mark := parser.mark parser.skip() end_mark := parser.mark - // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. + // Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. token := Token{ Type: typ, StartMark: start_mark, @@ -1222,28 +1205,27 @@ func (parser *Parser) fetchFlowCollectionStart(typ TokenType) error { return nil } -// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. -func (parser *Parser) fetchFlowCollectionEnd(typ TokenType) error { - // Reset any potential simple key on the current flow level. - if err := parser.removeSimpleKey(); err != nil { +// Produce the FLOW-ENTRY token. +func (parser *Parser) fetchFlowCollectionStart(typ TokenType) error { + // The indicators '[' and '{' may start a simple key. + if err := parser.saveSimpleKey(); err != nil { return err } - // Decrease the flow level. - if err := parser.decreaseFlowLevel(); err != nil { + // Increase the flow level. + if err := parser.increaseFlowLevel(); err != nil { return err } - // No simple keys after the indicators ']' and '}'. - parser.simple_key_allowed = false + // A simple key may follow the indicators '[' and '{'. + parser.simple_key_allowed = true // Consume the token. - start_mark := parser.mark parser.skip() end_mark := parser.mark - // Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. + // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. token := Token{ Type: typ, StartMark: start_mark, @@ -1254,7 +1236,7 @@ func (parser *Parser) fetchFlowCollectionEnd(typ TokenType) error { return nil } -// Produce the FLOW-ENTRY token. +// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. func (parser *Parser) fetchFlowEntry() error { // Reset any potential simple keys on the current flow level. if err := parser.removeSimpleKey(); err != nil { @@ -1280,48 +1262,25 @@ func (parser *Parser) fetchFlowEntry() error { } // Produce the BLOCK-ENTRY token. -func (parser *Parser) fetchBlockEntry() error { - // Check if the scanner is in the block context. - if parser.flow_level == 0 { - // Check if we are allowed to start a new entry. - if !parser.simple_key_allowed { - return formatScannerError("block sequence entries are not allowed in this context", parser.mark) - } - // Add the BLOCK-SEQUENCE-START token if needed. - if err := parser.rollIndent(parser.mark.Column, -1, BLOCK_SEQUENCE_START_TOKEN, parser.mark); err != nil { - return err - } - } else { //nolint:staticcheck // there is no problem with this empty branch as it's documentation. - - // It is an error for the '-' indicator to occur in the flow context, - // but we let the Parser detect and report about it because the Parser - // is able to point to the context. - } - - // Reset any potential simple keys on the current flow level. - if err := parser.removeSimpleKey(); err != nil { +func (parser *Parser) fetchFlowScalar(single bool) error { + // A plain scalar could be a simple key. + if err := parser.saveSimpleKey(); err != nil { return err } - // Simple keys are allowed after '-'. - parser.simple_key_allowed = true - - // Consume the token. - start_mark := parser.mark - parser.skip() - end_mark := parser.mark + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false - // Create the BLOCK-ENTRY token and append it to the queue. - token := Token{ - Type: BLOCK_ENTRY_TOKEN, - StartMark: start_mark, - EndMark: end_mark, + // Create the SCALAR token and append it to the queue. + var token Token + if err := parser.scanFlowScalar(&token, single); err != nil { + return err } parser.insertToken(-1, &token) return nil } -// Produce the KEY token. +// Produce the SCALAR(...,plain) token. func (parser *Parser) fetchKey() error { // In the block context, additional checks are required. if parser.flow_level == 0 { @@ -1359,20 +1318,115 @@ func (parser *Parser) fetchKey() error { } // Produce the VALUE token. -func (parser *Parser) fetchValue() error { - simple_key := &parser.simple_key +func (parser *Parser) fetchPlainScalar() error { + // A plain scalar could be a simple key. + if err := parser.saveSimpleKey(); err != nil { + return err + } - // Have we found a simple key? - if parser.simple_key_possible && simple_key.mark.Line == parser.mark.Line { - // Create the KEY token and insert it into the queue. - token := Token{ - Type: KEY_TOKEN, - StartMark: simple_key.mark, - EndMark: simple_key.mark, - } - parser.insertToken(simple_key.token_number-parser.tokens_parsed, &token) + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false - // In the block context, we may need to add the BLOCK-MAPPING-START token. + // Create the SCALAR token and append it to the queue. + var token Token + if err := parser.scanPlainScalar(&token); err != nil { + return err + } + parser.insertToken(-1, &token) + return nil +} + +// Eat whitespaces and comments until the next token is found. +func (parser *Parser) fetchStreamEnd() error { + // Force new line. + if parser.mark.Column != 1 { + parser.mark.Column = 1 + parser.mark.Line++ + } + + // Reset the indentation level. + if err := parser.unrollIndent(-1, parser.mark); err != nil { + return err + } + + // Reset simple keys. + if err := parser.removeSimpleKey(); err != nil { + return err + } + parser.simple_key = SimpleKey{} + parser.simple_key_stack = []SimpleKey{} + parser.simple_key_allowed = false + + // Create the STREAM-END token and append it to the queue. + token := Token{ + Type: STREAM_END_TOKEN, + StartMark: parser.mark, + EndMark: parser.mark, + } + parser.insertToken(-1, &token) + return nil +} + +// Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. +func (parser *Parser) fetchStreamStart() error { + // Set the initial indentation. + parser.indent = -1 + + // Initialize the simple key stack. + parser.simple_key = SimpleKey{} + parser.simple_key_stack = []SimpleKey{} + + // A simple key is allowed at the beginning of the stream. + parser.simple_key_allowed = true + + // We have started. + parser.stream_start_produced = true + + // Create the STREAM-START token and append it to the queue. + token := Token{ + Type: STREAM_START_TOKEN, + StartMark: parser.mark, + EndMark: parser.mark, + encoding: parser.encoding, + } + parser.insertToken(-1, &token) + return nil +} + +// Produce the STREAM-END token and shut down the scanner. +func (parser *Parser) fetchTag() error { + // A tag could be a simple key. + if err := parser.saveSimpleKey(); err != nil { + return err + } + + // A simple key cannot follow a tag. + parser.simple_key_allowed = false + + // Create the TAG token and append it to the queue. + var token Token + if err := parser.scanTag(&token); err != nil { + return err + } + parser.insertToken(-1, &token) + return nil +} + +// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. +func (parser *Parser) fetchValue() error { + simple_key := &parser.simple_key + + // Have we found a simple key? + if parser.simple_key_possible && simple_key.mark.Line == parser.mark.Line { + // Create the KEY token and insert it into the queue. + token := Token{ + Type: KEY_TOKEN, + StartMark: simple_key.mark, + EndMark: simple_key.mark, + } + parser.insertToken(simple_key.token_number-parser.tokens_parsed, &token) + + // In the block context, we may need to add the BLOCK-MAPPING-START token. if err := parser.rollIndent(simple_key.mark.Column, simple_key.token_number, BLOCK_MAPPING_START_TOKEN, simple_key.mark); err != nil { @@ -1423,255 +1477,136 @@ func (parser *Parser) fetchValue() error { } // Produce the ALIAS or ANCHOR token. -func (parser *Parser) fetchAnchor(typ TokenType) error { - // An anchor or an alias could be a simple key. - if err := parser.saveSimpleKey(); err != nil { - return err - } - - // A simple key cannot follow an anchor or an alias. - parser.simple_key_allowed = false +func (parser *Parser) scanAnchor(token *Token, typ TokenType) error { + var s []byte - // Create the ALIAS or ANCHOR token and append it to the queue. - var token Token - if err := parser.scanAnchor(&token, typ); err != nil { - return err - } - parser.insertToken(-1, &token) - return nil -} + // Eat the indicator character. + start_mark := parser.mark + parser.skip() -// Produce the TAG token. -func (parser *Parser) fetchTag() error { - // A tag could be a simple key. - if err := parser.saveSimpleKey(); err != nil { - return err + // Consume the value. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } } - // A simple key cannot follow a tag. - parser.simple_key_allowed = false - - // Create the TAG token and append it to the queue. - var token Token - if err := parser.scanTag(&token); err != nil { - return err + for isAnchorChar(parser.buffer, parser.buffer_pos) { + s = parser.read(s) + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } } - parser.insertToken(-1, &token) - return nil -} -// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. -func (parser *Parser) fetchBlockScalar(literal bool) error { - // Remove any potential simple keys. - if err := parser.removeSimpleKey(); err != nil { - return err - } + end_mark := parser.mark - // A simple key may follow a block scalar. - parser.simple_key_allowed = true + /* + * Check if length of the anchor is greater than 0 and it is followed by + * a whitespace character or one of the indicators: + * + * '?', ':', ',', ']', '}', '%', '@', '`'. + */ - // Create the SCALAR token and append it to the queue. - var token Token - if err := parser.scanBlockScalar(&token, literal); err != nil { - return err + if len(s) == 0 || + !(isBlankOrZero(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' || + parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' || + parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || + parser.buffer[parser.buffer_pos] == '`') { + context := "while scanning an alias" + if typ == ANCHOR_TOKEN { + context = "while scanning an anchor" + } + return formatScannerErrorContext(context, start_mark, + "did not find expected alphabetic or numeric character", parser.mark) } - parser.insertToken(-1, &token) - return nil -} -// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. -func (parser *Parser) fetchFlowScalar(single bool) error { - // A plain scalar could be a simple key. - if err := parser.saveSimpleKey(); err != nil { - return err + // Create a token. + *token = Token{ + Type: typ, + StartMark: start_mark, + EndMark: end_mark, + Value: s, } - // A simple key cannot follow a flow scalar. - parser.simple_key_allowed = false - - // Create the SCALAR token and append it to the queue. - var token Token - if err := parser.scanFlowScalar(&token, single); err != nil { - return err - } - parser.insertToken(-1, &token) return nil } -// Produce the SCALAR(...,plain) token. -func (parser *Parser) fetchPlainScalar() error { - // A plain scalar could be a simple key. - if err := parser.saveSimpleKey(); err != nil { - return err - } - - // A simple key cannot follow a flow scalar. - parser.simple_key_allowed = false +// scanTag scans a TAG token. +func (parser *Parser) scanBlockScalar(token *Token, literal bool) error { + // Eat the indicator '|' or '>'. + start_mark := parser.mark + parser.skip() - // Create the SCALAR token and append it to the queue. - var token Token - if err := parser.scanPlainScalar(&token); err != nil { - return err + // Scan the additional block scalar indicators. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } } - parser.insertToken(-1, &token) - return nil -} -// Eat whitespaces and comments until the next token is found. -func (parser *Parser) scanToNextToken() error { - scan_mark := parser.mark + // Check for a chomping indicator. + var chomping, increment int + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + // Set the chomping method and eat the indicator. + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + parser.skip() - // Until the next token is not found. - for { - // Allow the BOM mark to start a line. + // Check for an indentation indicator. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - if parser.mark.Column == 0 && isBOM(parser.buffer, parser.buffer_pos) { + if isDigit(parser.buffer, parser.buffer_pos) { + // Check that the indentation is greater than 0. + if parser.buffer[parser.buffer_pos] == '0' { + return formatScannerErrorContext("while scanning a block scalar", start_mark, + "found an indentation indicator equal to 0", parser.mark) + } + + // Get the indentation level and eat the indicator. + increment = asDigit(parser.buffer, parser.buffer_pos) parser.skip() } - // Eat whitespaces. - // Tabs are allowed: - // - in the flow context - // - in the block context, but not at the beginning of the line or - // after '-', '?', or ':' (complex value). - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - - for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } + } else if isDigit(parser.buffer, parser.buffer_pos) { + // Do the same as above, but in the opposite order. - // Check if we just had a line comment under a sequence entry that - // looks more like a header to the following content. Similar to this: - // - // - # The comment - // - Some data - // - // If so, transform the line comment to a head comment and reposition. - if len(parser.comments) > 0 && len(parser.tokens) > 1 { - tokenA := parser.tokens[len(parser.tokens)-2] - tokenB := parser.tokens[len(parser.tokens)-1] - comment := &parser.comments[len(parser.comments)-1] - if tokenA.Type == BLOCK_SEQUENCE_START_TOKEN && tokenB.Type == BLOCK_ENTRY_TOKEN && len(comment.Line) > 0 && !isLineBreak(parser.buffer, parser.buffer_pos) { - // If it was in the prior line, reposition so it becomes a - // header of the follow up token. Otherwise, keep it in place - // so it becomes a header of the former. - comment.Head = comment.Line - comment.Line = nil - if comment.StartMark.Line == parser.mark.Line-1 { - comment.TokenMark = parser.mark - } - } + if parser.buffer[parser.buffer_pos] == '0' { + return formatScannerErrorContext("while scanning a block scalar", start_mark, + "found an indentation indicator equal to 0", parser.mark) } + increment = asDigit(parser.buffer, parser.buffer_pos) + parser.skip() - // Eat a comment until a line break. - if parser.buffer[parser.buffer_pos] == '#' { - if err := parser.scanComments(scan_mark); err != nil { + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { return err } } - - // If it is a line break, eat it. - if isLineBreak(parser.buffer, parser.buffer_pos) { - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err - } - } - parser.skipLine() - - // In the block context, a new line may start a simple key. - if parser.flow_level == 0 { - parser.simple_key_allowed = true + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 } - } else { - break // We have found a token. - } - } - - return nil -} - -// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. -// -// Scope: -// -// %YAML 1.1 # a comment \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -func (parser *Parser) scanDirective(token *Token) error { - // Eat '%'. - start_mark := parser.mark - parser.skip() - - // Scan the directive name. - var name []byte - if err := parser.scanDirectiveName(start_mark, &name); err != nil { - return err - } - - // Is it a YAML directive? - if bytes.Equal(name, []byte("YAML")) { - // Scan the VERSION directive value. - var major, minor int8 - if err := parser.scanVersionDirectiveValue(start_mark, &major, &minor); err != nil { - return err - } - end_mark := parser.mark - - // Create a VERSION-DIRECTIVE token. - *token = Token{ - Type: VERSION_DIRECTIVE_TOKEN, - StartMark: start_mark, - EndMark: end_mark, - major: major, - minor: minor, - } - - // Is it a TAG directive? - } else if bytes.Equal(name, []byte("TAG")) { - // Scan the TAG directive value. - var handle, prefix []byte - if err := parser.scanTagDirectiveValue(start_mark, &handle, &prefix); err != nil { - return err - } - end_mark := parser.mark - - // Create a TAG-DIRECTIVE token. - *token = Token{ - Type: TAG_DIRECTIVE_TOKEN, - StartMark: start_mark, - EndMark: end_mark, - Value: handle, - prefix: prefix, + parser.skip() } - - // Unknown directive. - } else { - return formatScannerErrorContext("while scanning a directive", start_mark, - "found unknown directive name", parser.mark) } - // Eat the rest of the line including any comments. + // Eat whitespaces and comments to the end of the line. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { @@ -1680,12 +1615,10 @@ func (parser *Parser) scanDirective(token *Token) error { } } } - if parser.buffer[parser.buffer_pos] == '#' { - // [Go] Discard this inline comment for the time being. - //if !parser.ScanLineComment(start_mark) { - // return false - //} + if err := parser.scanLineComment(start_mark); err != nil { + return err + } for !isBreakOrZero(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { @@ -1698,7 +1631,7 @@ func (parser *Parser) scanDirective(token *Token) error { // Check if we are at the end of the line. if !isBreakOrZero(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a directive", start_mark, + return formatScannerErrorContext("while scanning a block scalar", start_mark, "did not find expected comment or line break", parser.mark) } @@ -1712,380 +1645,391 @@ func (parser *Parser) scanDirective(token *Token) error { parser.skipLine() } - return nil -} + end_mark := parser.mark -// Scan the directive name. -// -// Scope: -// -// %YAML 1.1 # a comment \n -// ^^^^ -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^ -func (parser *Parser) scanDirectiveName(start_mark Mark, name *[]byte) error { - // Consume the directive name. + // Set the indentation level if it was specified. + var indent int + if increment > 0 { + if parser.indent >= 0 { + indent = parser.indent + increment + } else { + // With 1-based columns, the line start is column 1, so an + // explicit indent indicator of n means column n+1. + indent = increment + 1 + } + } + + // Scan the leading line breaks and determine the indentation level if + // needed. + var s, leading_break, trailing_breaks []byte + if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { + return err + } + + // Scan the block scalar content. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } + var leading_blank, trailing_blank bool + for parser.mark.Column == indent && !isZeroChar(parser.buffer, parser.buffer_pos) { + // We are at the beginning of a non-empty line. - var s []byte - for isAlpha(parser.buffer, parser.buffer_pos) { - s = parser.read(s) - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Is it a trailing whitespace? + trailing_blank = isBlank(parser.buffer, parser.buffer_pos) + + // Check if we need to fold the leading line break. + if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' { + // Do we need to join the lines by space? + if len(trailing_breaks) == 0 { + s = append(s, ' ') } + } else { + s = append(s, leading_break...) } - } + leading_break = leading_break[:0] - // Check if the name is empty. - if len(s) == 0 { - return formatScannerErrorContext("while scanning a directive", start_mark, - "could not find expected directive name", parser.mark) - } + // Append the remaining line breaks. + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] - // Check for an blank character after the name. - if !isBlankOrZero(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a directive", start_mark, - "found unexpected non-alphabetical character", parser.mark) - } - *name = s - return nil -} + // Is it a leading whitespace? + leading_blank = isBlank(parser.buffer, parser.buffer_pos) -// Scan the value of VERSION-DIRECTIVE. -// -// Scope: -// -// %YAML 1.1 # a comment \n -// ^^^^^^ -func (parser *Parser) scanVersionDirectiveValue(start_mark Mark, major, minor *int8) error { - // Eat whitespaces. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Consume the current line. + for !isBreakOrZero(parser.buffer, parser.buffer_pos) { + s = parser.read(s) + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } } - } - for isBlank(parser.buffer, parser.buffer_pos) { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { + + // Consume the line break. + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { return err } } - } - // Consume the major version number. - if err := parser.scanVersionDirectiveNumber(start_mark, major); err != nil { - return err - } + leading_break = parser.readLine(leading_break) - // Eat '.'. - if parser.buffer[parser.buffer_pos] != '.' { - return formatScannerErrorContext("while scanning a %YAML directive", start_mark, - "did not find expected digit or '.' character", parser.mark) + // Eat the following indentation spaces and line breaks. + if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { + return err + } } - parser.skip() - - // Consume the minor version number. - if err := parser.scanVersionDirectiveNumber(start_mark, minor); err != nil { - return err - } - return nil -} - -const max_number_length = 2 - -// Scan the version number of VERSION-DIRECTIVE. -// -// Scope: -// -// %YAML 1.1 # a comment \n -// ^ -// %YAML 1.1 # a comment \n -// ^ -func (parser *Parser) scanVersionDirectiveNumber(start_mark Mark, number *int8) error { - // Repeat while the next character is digit. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } + // Chomp the tail. + if chomping != -1 { + s = append(s, leading_break...) } - var value, length int8 - for isDigit(parser.buffer, parser.buffer_pos) { - // Check if the number is too long. - length++ - if length > max_number_length { - return formatScannerErrorContext("while scanning a %YAML directive", start_mark, - "found extremely long version number", parser.mark) - } - value = value*10 + int8(asDigit(parser.buffer, parser.buffer_pos)) - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } + if chomping == 1 { + s = append(s, trailing_breaks...) } - // Check if the number was present. - if length == 0 { - return formatScannerErrorContext("while scanning a %YAML directive", start_mark, - "did not find expected version number", parser.mark) + // Create a token. + *token = Token{ + Type: SCALAR_TOKEN, + StartMark: start_mark, + EndMark: end_mark, + Value: s, + Style: LITERAL_SCALAR_STYLE, + } + if !literal { + token.Style = FOLDED_SCALAR_STYLE } - *number = value return nil } -// Scan the value of a TAG-DIRECTIVE token. -// -// Scope: -// -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -func (parser *Parser) scanTagDirectiveValue(start_mark Mark, handle, prefix *[]byte) error { - var handle_value, prefix_value []byte - - // Eat whitespaces. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } +// Scan indentation spaces and line breaks for a block scalar. +// Determine the indentation level if needed. +func (parser *Parser) scanBlockScalarBreaks(indent *int, breaks *[]byte, start_mark Mark, end_mark *Mark) error { + *end_mark = parser.mark - for isBlank(parser.buffer, parser.buffer_pos) { - parser.skip() + // Eat the indentation spaces and line breaks. + max_indent := 0 + for { + // Eat the indentation spaces. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - } + for (*indent == 0 || parser.mark.Column < *indent) && isSpace(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + if parser.mark.Column > max_indent { + max_indent = parser.mark.Column + } - // Scan a handle. - if err := parser.scanTagHandle(true, start_mark, &handle_value); err != nil { - return err - } + // Check for a tab character messing the indentation. + if (*indent == 0 || parser.mark.Column < *indent) && isTab(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a block scalar", start_mark, + "found a tab character where an indentation space is expected", parser.mark) + } - // Expect a whitespace. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Have we found a non-empty line? + if !isLineBreak(parser.buffer, parser.buffer_pos) { + break } - } - if !isBlank(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a %TAG directive", start_mark, - "did not find expected whitespace", parser.mark) - } - // Eat whitespaces. - for isBlank(parser.buffer, parser.buffer_pos) { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { + // Consume the line break. + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { return err } } + // [Go] Should really be returning breaks instead. + *breaks = parser.readLine(*breaks) + *end_mark = parser.mark } - // Scan a prefix (TAG directive URI - flow indicators allowed). - if err := parser.scanTagURI(true, true, nil, start_mark, &prefix_value); err != nil { - return err - } - - // Expect a whitespace or line break. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Determine the indentation level if needed. + if *indent == 0 { + *indent = max_indent + if *indent < parser.indent+1 { + *indent = parser.indent + 1 + } + if *indent < 1 { + *indent = 1 } } - if !isBlankOrZero(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a %TAG directive", start_mark, - "did not find expected whitespace or line break", parser.mark) - } - - *handle = handle_value - *prefix = prefix_value return nil } -func (parser *Parser) scanAnchor(token *Token, typ TokenType) error { - var s []byte - - // Eat the indicator character. - start_mark := parser.mark - parser.skip() - - // Consume the value. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - - for isAnchorChar(parser.buffer, parser.buffer_pos) { - s = parser.read(s) - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } - - end_mark := parser.mark - - /* - * Check if length of the anchor is greater than 0 and it is followed by - * a whitespace character or one of the indicators: - * - * '?', ':', ',', ']', '}', '%', '@', '`'. - */ +// Scan a quoted scalar. +func (parser *Parser) scanComments(scan_mark Mark) error { + token := parser.tokens[len(parser.tokens)-1] - if len(s) == 0 || - !(isBlankOrZero(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' || - parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || - parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' || - parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || - parser.buffer[parser.buffer_pos] == '`') { - context := "while scanning an alias" - if typ == ANCHOR_TOKEN { - context = "while scanning an anchor" - } - return formatScannerErrorContext(context, start_mark, - "did not find expected alphabetic or numeric character", parser.mark) + if token.Type == FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 { + token = parser.tokens[len(parser.tokens)-2] } - // Create a token. - *token = Token{ - Type: typ, - StartMark: start_mark, - EndMark: end_mark, - Value: s, + token_mark := token.StartMark + var start_mark Mark + next_indent := parser.indent + if next_indent < 0 { + next_indent = 0 } - return nil -} - -/* - * Scan a TAG token. - */ + recent_empty := false + first_empty := parser.newlines <= 1 -func (parser *Parser) scanTag(token *Token) error { - var handle, suffix []byte + line := parser.mark.Line + column := parser.mark.Column - start_mark := parser.mark + var text []byte - // Check if the tag is in the canonical form. - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err + // The foot line is the place where a comment must start to + // still be considered as a foot of the prior content. + // If there's some content in the currently parsed line, then + // the foot is the line below it. + foot_line := -1 + if scan_mark.Line > 0 { + foot_line = parser.mark.Line - parser.newlines + 1 + if parser.newlines == 0 && parser.mark.Column > 1 { + foot_line++ } } - if parser.buffer[parser.buffer_pos+1] == '<' { - // Keep the handle as '' - - // Eat '!<' - parser.skip() - parser.skip() - - // Consume the tag value (verbatim tag - flow indicators allowed). - if err := parser.scanTagURI(false, true, nil, start_mark, &suffix); err != nil { - return err + peek := 0 + for ; peek < 512; peek++ { + if parser.unread < peek+1 { + if parser.updateBuffer(peek+1) != nil { + break + } } - - // Check for '>' and eat it. - if parser.buffer[parser.buffer_pos] != '>' { - return formatScannerErrorContext("while scanning a tag", start_mark, - "did not find the expected '>'", parser.mark) + column++ + if isBlank(parser.buffer, parser.buffer_pos+peek) { + continue + } + c := parser.buffer[parser.buffer_pos+peek] + close_flow := parser.flow_level > 0 && (c == ']' || c == '}') + if close_flow || isBreakOrZero(parser.buffer, parser.buffer_pos+peek) { + // Got line break or terminator. + if close_flow || !recent_empty { + if close_flow || first_empty && (start_mark.Line == foot_line && token.Type != VALUE_TOKEN || start_mark.Column-1 < next_indent) { + // This is the first empty line and there were no empty lines before, + // so this initial part of the comment is a foot of the prior token + // instead of being a head for the following one. Split it up. + // Alternatively, this might also be the last comment inside a flow + // scope, so it must be a footer. + if len(text) > 0 { + if start_mark.Column-1 < next_indent { + // If dedented it's unrelated to the prior token. + token_mark = start_mark + } + parser.comments = append(parser.comments, Comment{ + ScanMark: scan_mark, + TokenMark: token_mark, + StartMark: start_mark, + EndMark: Mark{parser.mark.Index + peek, line, column}, + Foot: text, + }) + scan_mark = Mark{parser.mark.Index + peek, line, column} + token_mark = scan_mark + text = nil + } + } else { + if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 { + text = append(text, '\n') + } + } + } + if !isLineBreak(parser.buffer, parser.buffer_pos+peek) { + break + } + first_empty = false + recent_empty = true + column = 1 + line++ + continue } - parser.skip() - } else { - // The tag has either the '!suffix' or the '!handle!suffix' form. - - // First, try to scan a handle. - if err := parser.scanTagHandle(false, start_mark, &handle); err != nil { - return err + if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.Column) { + // The comment at the different indentation is a foot of the + // preceding data rather than a head of the upcoming one. + parser.comments = append(parser.comments, Comment{ + ScanMark: scan_mark, + TokenMark: token_mark, + StartMark: start_mark, + EndMark: Mark{parser.mark.Index + peek, line, column}, + Foot: text, + }) + scan_mark = Mark{parser.mark.Index + peek, line, column} + token_mark = scan_mark + text = nil } - // Check if it is, indeed, handle. - if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' { - // Scan the suffix now (short form - flow indicators not allowed). - if err := parser.scanTagURI(false, false, nil, start_mark, &suffix); err != nil { - return err - } + if parser.buffer[parser.buffer_pos+peek] != '#' { + break + } + + if len(text) == 0 { + start_mark = Mark{parser.mark.Index + peek, line, column} } else { - // It wasn't a handle after all. Scan the rest of the tag (short form). - if err := parser.scanTagURI(false, false, handle, start_mark, &suffix); err != nil { - return err - } + text = append(text, '\n') + } - // Set the handle to '!'. - handle = []byte{'!'} + recent_empty = false - // A special case: the '!' tag. Set the handle to '' and the - // suffix to '!'. - if len(suffix) == 0 { - handle, suffix = suffix, handle + // Consume until after the consumed comment line. + seen := parser.mark.Index + peek + for { + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if isBreakOrZero(parser.buffer, parser.buffer_pos) { + if parser.mark.Index >= seen { + break + } + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { + return err + } + } + parser.skipLine() + } else if parser.mark.Index >= seen { + text = parser.read(text) + } else { + parser.skip() } } - } - // Check the character which ends the tag. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + peek = 0 + column = 1 + line = parser.mark.Line + next_indent = parser.indent + if next_indent < 0 { + next_indent = 0 } } - if !isBlankOrZero(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a tag", start_mark, - "did not find expected whitespace or line break", parser.mark) - } - - end_mark := parser.mark - // Create a token. - *token = Token{ - Type: TAG_TOKEN, - StartMark: start_mark, - EndMark: end_mark, - Value: handle, - suffix: suffix, + if len(text) > 0 { + parser.comments = append(parser.comments, Comment{ + ScanMark: scan_mark, + TokenMark: start_mark, + StartMark: start_mark, + EndMark: Mark{parser.mark.Index + peek - 1, line, column}, + Head: text, + }) } return nil } -// Scan a tag handle. -func (parser *Parser) scanTagHandle(directive bool, start_mark Mark, handle *[]byte) error { - // Check the initial '!' character. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { +// scanDirective scans a directive (%YAML or %TAG). +func (parser *Parser) scanDirective(token *Token) error { + // Eat '%'. + start_mark := parser.mark + parser.skip() + + // Scan the directive name. + var name []byte + if err := parser.scanDirectiveName(start_mark, &name); err != nil { + return err + } + + // Is it a YAML directive? + if bytes.Equal(name, []byte("YAML")) { + // Scan the VERSION directive value. + var major, minor int8 + if err := parser.scanVersionDirectiveValue(start_mark, &major, &minor); err != nil { return err } - } - if parser.buffer[parser.buffer_pos] != '!' { - return parser.setScannerTagError(directive, - start_mark, "did not find expected '!'") - } + end_mark := parser.mark - var s []byte + // Create a VERSION-DIRECTIVE token. + *token = Token{ + Type: VERSION_DIRECTIVE_TOKEN, + StartMark: start_mark, + EndMark: end_mark, + major: major, + minor: minor, + } - // Copy the '!' character. - s = parser.read(s) + // Is it a TAG directive? + } else if bytes.Equal(name, []byte("TAG")) { + // Scan the TAG directive value. + var handle, prefix []byte + if err := parser.scanTagDirectiveValue(start_mark, &handle, &prefix); err != nil { + return err + } + end_mark := parser.mark - // Copy all subsequent alphabetical and numerical characters. + // Create a TAG-DIRECTIVE token. + *token = Token{ + Type: TAG_DIRECTIVE_TOKEN, + StartMark: start_mark, + EndMark: end_mark, + Value: handle, + prefix: prefix, + } + + // Unknown directive. + } else { + return formatScannerErrorContext("while scanning a directive", start_mark, + "found unknown directive name", parser.mark) + } + + // Eat the rest of the line including any comments. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - for isAlpha(parser.buffer, parser.buffer_pos) { - s = parser.read(s) + + for isBlank(parser.buffer, parser.buffer_pos) { + parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err @@ -2093,445 +2037,129 @@ func (parser *Parser) scanTagHandle(directive bool, start_mark Mark, handle *[]b } } - // Check if the trailing character is '!' and copy it. - if parser.buffer[parser.buffer_pos] == '!' { - s = parser.read(s) - } else { - // It's either the '!' tag or not really a tag handle. If it's a %TAG - // directive, it's an error. If it's a tag token, it must be a part of URI. - if directive && string(s) != "!" { - return parser.setScannerTagError(directive, - start_mark, "did not find expected '!'") + if parser.buffer[parser.buffer_pos] == '#' { + // [Go] Discard this inline comment for the time being. + //if !parser.ScanLineComment(start_mark) { + // return false + //} + for !isBreakOrZero(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } } } - *handle = s - return nil -} - -// Scan a tag URI. -// directive: true if scanning a %TAG directive URI -// verbatim: true if scanning a verbatim tag !<...> or TAG directive (flow indicators allowed) -func (parser *Parser) scanTagURI(directive bool, verbatim bool, head []byte, start_mark Mark, uri *[]byte) error { - // size_t length = head ? strlen((char *)head) : 0 - var s []byte - hasTag := len(head) > 0 - - // Copy the head if needed. - // - // Note that we don't copy the leading '!' character. - if len(head) > 1 { - s = append(s, head[1:]...) + // Check if we are at the end of the line. + if !isBreakOrZero(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a directive", start_mark, + "did not find expected comment or line break", parser.mark) } - // Scan the tag. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Eat a line break. + if isLineBreak(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { + return err + } } + parser.skipLine() } - // The set of characters that may appear in URI is as follows: - // - // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', - // '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. - // - // Note: Flow indicators (',', '[', ']', '{', '}') are only allowed in verbatim tags. - for isTagURIChar(parser.buffer, parser.buffer_pos, verbatim) { - // Check if it is a URI-escape sequence. - if parser.buffer[parser.buffer_pos] == '%' { - if err := parser.scanURIEscapes(directive, start_mark, &s); err != nil { - return err - } - } else { - s = parser.read(s) - } - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - hasTag = true - } - - // Check for characters which are not allowed in tags. - // For non-verbatim tags, if we stopped at a printable character that isn't whitespace, - // it's an invalid tag character - give a specific error. - // For verbatim tags, the caller will check for the expected '>' delimiter. - if !verbatim { - c := parser.buffer[parser.buffer_pos] - if !isBlankOrZero(parser.buffer, parser.buffer_pos) && - c >= 0x20 && c <= 0x7E { - return parser.setScannerTagError(directive, start_mark, - fmt.Sprintf("found character '%c' that is not allowed in a YAML tag", c)) - } - } - - if !hasTag { - return parser.setScannerTagError(directive, - start_mark, "did not find expected tag URI") - } - *uri = s - return nil -} - -// Decode an URI-escape sequence corresponding to a single UTF-8 character. -func (parser *Parser) scanURIEscapes(directive bool, start_mark Mark, s *[]byte) error { - // Decode the required number of characters. - w := 1024 - for w > 0 { - // Check for a URI-escaped octet. - if parser.unread < 3 { - if err := parser.updateBuffer(3); err != nil { - return err - } - } - - if !(parser.buffer[parser.buffer_pos] == '%' && - isHex(parser.buffer, parser.buffer_pos+1) && - isHex(parser.buffer, parser.buffer_pos+2)) { - return parser.setScannerTagError(directive, - start_mark, "did not find URI escaped octet") - } - - // Get the octet. - octet := byte((asHex(parser.buffer, parser.buffer_pos+1) << 4) + asHex(parser.buffer, parser.buffer_pos+2)) - - // If it is the leading octet, determine the length of the UTF-8 sequence. - if w == 1024 { - w = width(octet) - if w == 0 { - return parser.setScannerTagError(directive, - start_mark, "found an incorrect leading UTF-8 octet") - } - } else { - // Check if the trailing octet is correct. - if octet&0xC0 != 0x80 { - return parser.setScannerTagError(directive, - start_mark, "found an incorrect trailing UTF-8 octet") - } - } - - // Copy the octet and move the pointers. - *s = append(*s, octet) - parser.skip() - parser.skip() - parser.skip() - w-- - } return nil } -// Scan a block scalar. -func (parser *Parser) scanBlockScalar(token *Token, literal bool) error { - // Eat the indicator '|' or '>'. - start_mark := parser.mark - parser.skip() - - // Scan the additional block scalar indicators. +// Scan the directive name. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^ +func (parser *Parser) scanDirectiveName(start_mark Mark, name *[]byte) error { + // Consume the directive name. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - // Check for a chomping indicator. - var chomping, increment int - if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { - // Set the chomping method and eat the indicator. - if parser.buffer[parser.buffer_pos] == '+' { - chomping = +1 - } else { - chomping = -1 - } - parser.skip() - - // Check for an indentation indicator. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - if isDigit(parser.buffer, parser.buffer_pos) { - // Check that the indentation is greater than 0. - if parser.buffer[parser.buffer_pos] == '0' { - return formatScannerErrorContext("while scanning a block scalar", start_mark, - "found an indentation indicator equal to 0", parser.mark) - } - - // Get the indentation level and eat the indicator. - increment = asDigit(parser.buffer, parser.buffer_pos) - parser.skip() - } - - } else if isDigit(parser.buffer, parser.buffer_pos) { - // Do the same as above, but in the opposite order. - - if parser.buffer[parser.buffer_pos] == '0' { - return formatScannerErrorContext("while scanning a block scalar", start_mark, - "found an indentation indicator equal to 0", parser.mark) - } - increment = asDigit(parser.buffer, parser.buffer_pos) - parser.skip() - + var s []byte + for isAlpha(parser.buffer, parser.buffer_pos) { + s = parser.read(s) if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } - if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { - if parser.buffer[parser.buffer_pos] == '+' { - chomping = +1 - } else { - chomping = -1 - } - parser.skip() - } } - // Eat whitespaces and comments to the end of the line. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - for isBlank(parser.buffer, parser.buffer_pos) { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } - if parser.buffer[parser.buffer_pos] == '#' { - if err := parser.scanLineComment(start_mark); err != nil { - return err - } - for !isBreakOrZero(parser.buffer, parser.buffer_pos) { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } + // Check if the name is empty. + if len(s) == 0 { + return formatScannerErrorContext("while scanning a directive", start_mark, + "could not find expected directive name", parser.mark) } - // Check if we are at the end of the line. - if !isBreakOrZero(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a block scalar", start_mark, - "did not find expected comment or line break", parser.mark) + // Check for an blank character after the name. + if !isBlankOrZero(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a directive", start_mark, + "found unexpected non-alphabetical character", parser.mark) } + *name = s + return nil +} - // Eat a line break. - if isLineBreak(parser.buffer, parser.buffer_pos) { - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { +// Scan the value of VERSION-DIRECTIVE. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^^^ +func (parser *Parser) scanFlowScalar(token *Token, single bool) error { + // Eat the left quote. + start_mark := parser.mark + parser.skip() + + // Consume the content of the quoted scalar. + var s, leading_break, trailing_breaks, whitespaces []byte + for { + // Check that there are no document indicators at the beginning + // of the line. + if parser.unread < 4 { + if err := parser.updateBuffer(4); err != nil { return err } } - parser.skipLine() - } - - end_mark := parser.mark - // Set the indentation level if it was specified. - var indent int - if increment > 0 { - if parser.indent >= 0 { - indent = parser.indent + increment - } else { - indent = increment + if parser.mark.Column == 1 && + ((parser.buffer[parser.buffer_pos+0] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos+0] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + isBlankOrZero(parser.buffer, parser.buffer_pos+3) { + return formatScannerErrorContext("while scanning a quoted scalar", start_mark, + "found unexpected document indicator", parser.mark) } - } - - // Scan the leading line breaks and determine the indentation level if needed. - var s, leading_break, trailing_breaks []byte - if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { - return err - } - // Scan the block scalar content. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err + // Check for EOF. + if isZeroChar(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a quoted scalar", start_mark, + "found unexpected end of stream", parser.mark) } - } - var leading_blank, trailing_blank bool - for parser.mark.Column == indent && !isZeroChar(parser.buffer, parser.buffer_pos) { - // We are at the beginning of a non-empty line. - // Is it a trailing whitespace? - trailing_blank = isBlank(parser.buffer, parser.buffer_pos) - - // Check if we need to fold the leading line break. - if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' { - // Do we need to join the lines by space? - if len(trailing_breaks) == 0 { - s = append(s, ' ') - } - } else { - s = append(s, leading_break...) - } - leading_break = leading_break[:0] - - // Append the remaining line breaks. - s = append(s, trailing_breaks...) - trailing_breaks = trailing_breaks[:0] - - // Is it a leading whitespace? - leading_blank = isBlank(parser.buffer, parser.buffer_pos) - - // Consume the current line. - for !isBreakOrZero(parser.buffer, parser.buffer_pos) { - s = parser.read(s) - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } - - // Consume the line break. - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err - } - } - - leading_break = parser.readLine(leading_break) - - // Eat the following indentation spaces and line breaks. - if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { - return err - } - } - - // Chomp the tail. - if chomping != -1 { - s = append(s, leading_break...) - } - if chomping == 1 { - s = append(s, trailing_breaks...) - } - - // Create a token. - *token = Token{ - Type: SCALAR_TOKEN, - StartMark: start_mark, - EndMark: end_mark, - Value: s, - Style: LITERAL_SCALAR_STYLE, - } - if !literal { - token.Style = FOLDED_SCALAR_STYLE - } - return nil -} - -// Scan indentation spaces and line breaks for a block scalar. Determine the -// indentation level if needed. -func (parser *Parser) scanBlockScalarBreaks(indent *int, breaks *[]byte, start_mark Mark, end_mark *Mark) error { - *end_mark = parser.mark - - // Eat the indentation spaces and line breaks. - max_indent := 0 - for { - // Eat the indentation spaces. - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - for (*indent == 0 || parser.mark.Column < *indent) && isSpace(parser.buffer, parser.buffer_pos) { - parser.skip() - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - } - if parser.mark.Column > max_indent { - max_indent = parser.mark.Column - } - - // Check for a tab character messing the indentation. - if (*indent == 0 || parser.mark.Column < *indent) && isTab(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a block scalar", start_mark, - "found a tab character where an indentation space is expected", parser.mark) - } - - // Have we found a non-empty line? - if !isLineBreak(parser.buffer, parser.buffer_pos) { - break - } - - // Consume the line break. - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err - } - } - // [Go] Should really be returning breaks instead. - *breaks = parser.readLine(*breaks) - *end_mark = parser.mark - } - - // Determine the indentation level if needed. - if *indent == 0 { - *indent = max_indent - if *indent < parser.indent+1 { - *indent = parser.indent + 1 - } - if *indent < 1 { - *indent = 1 - } - } - return nil -} - -// Scan a quoted scalar. -func (parser *Parser) scanFlowScalar(token *Token, single bool) error { - // Eat the left quote. - start_mark := parser.mark - parser.skip() - - // Consume the content of the quoted scalar. - var s, leading_break, trailing_breaks, whitespaces []byte - for { - // Check that there are no document indicators at the beginning of the line. - if parser.unread < 4 { - if err := parser.updateBuffer(4); err != nil { - return err - } - } - - if parser.mark.Column == 0 && - ((parser.buffer[parser.buffer_pos+0] == '-' && - parser.buffer[parser.buffer_pos+1] == '-' && - parser.buffer[parser.buffer_pos+2] == '-') || - (parser.buffer[parser.buffer_pos+0] == '.' && - parser.buffer[parser.buffer_pos+1] == '.' && - parser.buffer[parser.buffer_pos+2] == '.')) && - isBlankOrZero(parser.buffer, parser.buffer_pos+3) { - return formatScannerErrorContext("while scanning a quoted scalar", start_mark, - "found unexpected document indicator", parser.mark) - } - - // Check for EOF. - if isZeroChar(parser.buffer, parser.buffer_pos) { - return formatScannerErrorContext("while scanning a quoted scalar", start_mark, - "found unexpected end of stream", parser.mark) - } - - // Consume non-blank characters. - leading_blanks := false - for !isBlankOrZero(parser.buffer, parser.buffer_pos) { - if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' { - // Is is an escaped single quote. - s = append(s, '\'') - parser.skip() - parser.skip() + // Consume non-blank characters. + leading_blanks := false + for !isBlankOrZero(parser.buffer, parser.buffer_pos) { + if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' { + // Is is an escaped single quote. + s = append(s, '\'') + parser.skip() + parser.skip() } else if single && parser.buffer[parser.buffer_pos] == '\'' { // It is a right single quote. @@ -2755,6 +2383,67 @@ func (parser *Parser) scanFlowScalar(token *Token, single bool) error { } // Scan a plain scalar. +func (parser *Parser) scanLineComment(token_mark Mark) error { + if parser.newlines > 0 { + return nil + } + + var start_mark Mark + var text []byte + + for peek := 0; peek < 512; peek++ { + if parser.unread < peek+1 { + if parser.updateBuffer(peek+1) != nil { + break + } + } + if isBlank(parser.buffer, parser.buffer_pos+peek) { + continue + } + if parser.buffer[parser.buffer_pos+peek] == '#' { + seen := parser.mark.Index + peek + for { + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if isBreakOrZero(parser.buffer, parser.buffer_pos) { + if parser.mark.Index >= seen { + break + } + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { + return err + } + } + parser.skipLine() + } else if parser.mark.Index >= seen { + if len(text) == 0 { + start_mark = parser.mark + } + text = parser.read(text) + } else { + parser.skip() + } + } + } + break + } + if len(text) > 0 { + parser.comments = append(parser.comments, Comment{ + ScanMark: token_mark, + TokenMark: token_mark, + StartMark: start_mark, + EndMark: parser.mark, + Line: text, + }) + } + return nil +} + +// scanComments scans and associates comments with tokens, handling head, line, +// and foot comments based on their position relative to tokens. func (parser *Parser) scanPlainScalar(token *Token) error { var s, leading_break, trailing_breaks, whitespaces []byte var leading_blanks bool @@ -2771,7 +2460,7 @@ func (parser *Parser) scanPlainScalar(token *Token) error { return err } } - if parser.mark.Column == 0 && + if parser.mark.Column == 1 && ((parser.buffer[parser.buffer_pos+0] == '-' && parser.buffer[parser.buffer_pos+1] == '-' && parser.buffer[parser.buffer_pos+2] == '-') || @@ -2791,13 +2480,9 @@ func (parser *Parser) scanPlainScalar(token *Token) error { for !isBlankOrZero(parser.buffer, parser.buffer_pos) { // Check for indicators that may end a plain scalar. + // Check for ": " or isEndOfScalarInFlowContentChar in flow context. if (parser.buffer[parser.buffer_pos] == ':' && isBlankOrZero(parser.buffer, parser.buffer_pos+1)) || - (parser.flow_level > 0 && - (parser.buffer[parser.buffer_pos] == ',' || - (parser.buffer[parser.buffer_pos] == '?' && isBlankOrZero(parser.buffer, parser.buffer_pos+1)) || - parser.buffer[parser.buffer_pos] == '[' || - parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || - parser.buffer[parser.buffer_pos] == '}')) { + (parser.flow_level > 0 && isEndOfScalarInFlowContentChar(parser.buffer, parser.buffer_pos)) { break } @@ -2907,222 +2592,839 @@ func (parser *Parser) scanPlainScalar(token *Token) error { return nil } -func (parser *Parser) scanLineComment(token_mark Mark) error { - if parser.newlines > 0 { - return nil +// scanLineComment scans a comment on the same line as a token. +func (parser *Parser) scanTag(token *Token) error { + var handle, suffix []byte + + start_mark := parser.mark + + // Check if the tag is in the canonical form. + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { + return err + } } - var start_mark Mark - var text []byte + if parser.buffer[parser.buffer_pos+1] == '<' { + // Keep the handle as '' - for peek := 0; peek < 512; peek++ { - if parser.unread < peek+1 { - if parser.updateBuffer(peek+1) != nil { - break - } + // Eat '!<' + parser.skip() + parser.skip() + + // Consume the tag value (verbatim tag - flow indicators allowed). + if err := parser.scanTagURI(false, true, nil, start_mark, &suffix); err != nil { + return err } - if isBlank(parser.buffer, parser.buffer_pos+peek) { - continue + + // Check for '>' and eat it. + if parser.buffer[parser.buffer_pos] != '>' { + return formatScannerErrorContext("while scanning a tag", start_mark, + "did not find the expected '>'", parser.mark) } - if parser.buffer[parser.buffer_pos+peek] == '#' { - seen := parser.mark.Index + peek - for { - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - if isBreakOrZero(parser.buffer, parser.buffer_pos) { - if parser.mark.Index >= seen { - break - } - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err - } - } - parser.skipLine() - } else if parser.mark.Index >= seen { - if len(text) == 0 { - start_mark = parser.mark - } - text = parser.read(text) - } else { - parser.skip() - } - } + + parser.skip() + } else { + // The tag has either the '!suffix' or the '!handle!suffix' form. + + // First, try to scan a handle. + if err := parser.scanTagHandle(false, start_mark, &handle); err != nil { + return err } - break - } - if len(text) > 0 { - parser.comments = append(parser.comments, Comment{ - ScanMark: token_mark, - TokenMark: token_mark, - StartMark: start_mark, - EndMark: parser.mark, - Line: text, - }) - } - return nil -} -func (parser *Parser) scanComments(scan_mark Mark) error { - token := parser.tokens[len(parser.tokens)-1] + // Check if it is, indeed, handle. + if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' { + // Scan the suffix now (short form - flow indicators + // not allowed). + if err := parser.scanTagURI(false, false, nil, start_mark, &suffix); err != nil { + return err + } + } else { + // It wasn't a handle after all. Scan the rest of the + // tag (short form). + if err := parser.scanTagURI(false, false, handle, start_mark, &suffix); err != nil { + return err + } - if token.Type == FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 { - token = parser.tokens[len(parser.tokens)-2] + // Set the handle to '!'. + handle = []byte{'!'} + + // A special case: the '!' tag. Set the handle to '' + // and the suffix to '!'. + if len(suffix) == 0 { + handle, suffix = suffix, handle + } + } } - token_mark := token.StartMark - var start_mark Mark - next_indent := parser.indent - if next_indent < 0 { - next_indent = 0 + // Check the character which ends the tag. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if !isBlankOrZero(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a tag", start_mark, + "did not find expected whitespace or line break", parser.mark) } - recent_empty := false - first_empty := parser.newlines <= 1 + end_mark := parser.mark - line := parser.mark.Line - column := parser.mark.Column + // Create a token. + *token = Token{ + Type: TAG_TOKEN, + StartMark: start_mark, + EndMark: end_mark, + Value: handle, + suffix: suffix, + } + return nil +} - var text []byte +// Scan a tag handle. +func (parser *Parser) scanTagDirectiveValue(start_mark Mark, handle, prefix *[]byte) error { + var handle_value, prefix_value []byte - // The foot line is the place where a comment must start to - // still be considered as a foot of the prior content. - // If there's some content in the currently parsed line, then - // the foot is the line below it. - foot_line := -1 - if scan_mark.Line > 0 { - foot_line = parser.mark.Line - parser.newlines + 1 - if parser.newlines == 0 && parser.mark.Column > 1 { - foot_line++ + // Eat whitespaces. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err } } - peek := 0 - for ; peek < 512; peek++ { - if parser.unread < peek+1 { - if parser.updateBuffer(peek+1) != nil { - break - } - } - column++ - if isBlank(parser.buffer, parser.buffer_pos+peek) { - continue - } - c := parser.buffer[parser.buffer_pos+peek] - close_flow := parser.flow_level > 0 && (c == ']' || c == '}') - if close_flow || isBreakOrZero(parser.buffer, parser.buffer_pos+peek) { - // Got line break or terminator. - if close_flow || !recent_empty { - if close_flow || first_empty && (start_mark.Line == foot_line && token.Type != VALUE_TOKEN || start_mark.Column-1 < next_indent) { - // This is the first empty line and there were no empty lines before, - // so this initial part of the comment is a foot of the prior token - // instead of being a head for the following one. Split it up. - // Alternatively, this might also be the last comment inside a flow - // scope, so it must be a footer. - if len(text) > 0 { - if start_mark.Column-1 < next_indent { - // If dedented it's unrelated to the prior token. - token_mark = start_mark - } - parser.comments = append(parser.comments, Comment{ - ScanMark: scan_mark, - TokenMark: token_mark, - StartMark: start_mark, - EndMark: Mark{parser.mark.Index + peek, line, column}, - Foot: text, - }) - scan_mark = Mark{parser.mark.Index + peek, line, column} - token_mark = scan_mark - text = nil - } - } else { - if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 { - text = append(text, '\n') - } - } - } - if !isLineBreak(parser.buffer, parser.buffer_pos+peek) { - break + for isBlank(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err } - first_empty = false - recent_empty = true - column = 0 - line++ - continue - } - - if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.Column) { - // The comment at the different indentation is a foot of the - // preceding data rather than a head of the upcoming one. - parser.comments = append(parser.comments, Comment{ - ScanMark: scan_mark, - TokenMark: token_mark, - StartMark: start_mark, - EndMark: Mark{parser.mark.Index + peek, line, column}, - Foot: text, - }) - scan_mark = Mark{parser.mark.Index + peek, line, column} - token_mark = scan_mark - text = nil } + } - if parser.buffer[parser.buffer_pos+peek] != '#' { - break - } + // Scan a handle. + if err := parser.scanTagHandle(true, start_mark, &handle_value); err != nil { + return err + } - if len(text) == 0 { - start_mark = Mark{parser.mark.Index + peek, line, column} - } else { - text = append(text, '\n') + // Expect a whitespace. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err } + } + if !isBlank(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a %TAG directive", start_mark, + "did not find expected whitespace", parser.mark) + } - recent_empty = false - - // Consume until after the consumed comment line. - seen := parser.mark.Index + peek - for { - if parser.unread < 1 { - if err := parser.updateBuffer(1); err != nil { - return err - } - } - if isBreakOrZero(parser.buffer, parser.buffer_pos) { - if parser.mark.Index >= seen { - break - } - if parser.unread < 2 { - if err := parser.updateBuffer(2); err != nil { - return err - } - } - parser.skipLine() - } else if parser.mark.Index >= seen { - text = parser.read(text) - } else { - parser.skip() + // Eat whitespaces. + for isBlank(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err } } + } - peek = 0 - column = 0 - line = parser.mark.Line - next_indent = parser.indent - if next_indent < 0 { - next_indent = 0 - } + // Scan a prefix (TAG directive URI - flow indicators allowed). + if err := parser.scanTagURI(true, true, nil, start_mark, &prefix_value); err != nil { + return err } - if len(text) > 0 { - parser.comments = append(parser.comments, Comment{ - ScanMark: scan_mark, - TokenMark: start_mark, - StartMark: start_mark, - EndMark: Mark{parser.mark.Index + peek - 1, line, column}, - Head: text, - }) + // Expect a whitespace or line break. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if !isBlankOrZero(parser.buffer, parser.buffer_pos) { + return formatScannerErrorContext("while scanning a %TAG directive", start_mark, + "did not find expected whitespace or line break", parser.mark) + } + + *handle = handle_value + *prefix = prefix_value + return nil +} + +// scanAnchor scans an ANCHOR or ALIAS token. +func (parser *Parser) scanTagHandle(directive bool, start_mark Mark, handle *[]byte) error { + // Check the initial '!' character. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if parser.buffer[parser.buffer_pos] != '!' { + return parser.setScannerTagError(directive, + start_mark, "did not find expected '!'") + } + + var s []byte + + // Copy the '!' character. + s = parser.read(s) + + // Copy all subsequent alphabetical and numerical characters. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + for isAlpha(parser.buffer, parser.buffer_pos) { + s = parser.read(s) + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + + // Check if the trailing character is '!' and copy it. + if parser.buffer[parser.buffer_pos] == '!' { + s = parser.read(s) + } else { + // It's either the '!' tag or not really a tag handle. + // If it's a %TAG directive, it's an error. + // If it's a tag token, it must be a part of URI. + if directive && string(s) != "!" { + return parser.setScannerTagError(directive, + start_mark, "did not find expected '!'") + } + } + + *handle = s + return nil +} + +// Scan a tag URI. +// directive: true if scanning a %TAG directive URI +// verbatim: true if scanning a verbatim tag !<...> or TAG directive (flow indicators allowed) +func (parser *Parser) scanTagURI(directive bool, verbatim bool, head []byte, start_mark Mark, uri *[]byte) error { + // size_t length = head ? strlen((char *)head) : 0 + var s []byte + hasTag := len(head) > 0 + + // Copy the head if needed. + // + // Note that we don't copy the leading '!' character. + if len(head) > 1 { + s = append(s, head[1:]...) + } + + // Scan the tag. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + + // The set of characters that may appear in URI is as follows: + // + // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', + // '&', '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. + // + // Note: Flow indicators (',', '[', ']', '{', '}') are only allowed in + // verbatim tags. + for isTagURIChar(parser.buffer, parser.buffer_pos, verbatim) { + // Check if it is a URI-escape sequence. + if parser.buffer[parser.buffer_pos] == '%' { + if err := parser.scanURIEscapes(directive, start_mark, &s); err != nil { + return err + } + } else { + s = parser.read(s) + } + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + hasTag = true + } + + // Check for characters which are not allowed in tags. + // For non-verbatim tags, if we stopped at a printable character that + // isn't whitespace, it's an invalid tag character - give a specific + // error. + // For verbatim tags, the caller will check for the expected '>' + // delimiter. + if !verbatim { + c := parser.buffer[parser.buffer_pos] + if !isBlankOrZero(parser.buffer, parser.buffer_pos) && + c >= 0x20 && c <= 0x7E { + return parser.setScannerTagError(directive, start_mark, + fmt.Sprintf("found character '%c' that is not allowed in a YAML tag", c)) + } + } + + if !hasTag { + return parser.setScannerTagError(directive, + start_mark, "did not find expected tag URI") + } + *uri = s + return nil +} + +// Decode an URI-escape sequence corresponding to a single UTF-8 character. +func (parser *Parser) scanToNextToken() error { + scan_mark := parser.mark + + // Until the next token is not found. + for { + // Allow the BOM mark to start a line. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + if parser.mark.Column == 1 && isBOM(parser.buffer, parser.buffer_pos) { + parser.skip() + } + + // Eat whitespaces. + // Tabs are allowed: + // - in the flow context + // - in the block context, but not at the beginning of the line or + // after '-', '?', or ':' (complex value). + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + + for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + + // Check if we just had a line comment under a sequence entry that + // looks more like a header to the following content. Similar to this: + // + // - # The comment + // - Some data + // + // If so, transform the line comment to a head comment and reposition. + if len(parser.comments) > 0 && len(parser.tokens) > 1 { + tokenA := parser.tokens[len(parser.tokens)-2] + tokenB := parser.tokens[len(parser.tokens)-1] + comment := &parser.comments[len(parser.comments)-1] + if tokenA.Type == BLOCK_SEQUENCE_START_TOKEN && tokenB.Type == BLOCK_ENTRY_TOKEN && len(comment.Line) > 0 && !isLineBreak(parser.buffer, parser.buffer_pos) { + // If it was in the prior line, reposition so it becomes a + // header of the follow up token. Otherwise, keep it in place + // so it becomes a header of the former. + comment.Head = comment.Line + comment.Line = nil + if comment.StartMark.Line == parser.mark.Line-1 { + comment.TokenMark = parser.mark + } + } + } + + // Eat a comment until a line break. + if parser.buffer[parser.buffer_pos] == '#' { + if parser.skip_comments { + // Skip comment without storing it + for !isLineBreak(parser.buffer, parser.buffer_pos) && !isZeroChar(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + } else { + if err := parser.scanComments(scan_mark); err != nil { + return err + } + } + } + + // If it is a line break, eat it. + if isLineBreak(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 { + if err := parser.updateBuffer(2); err != nil { + return err + } + } + parser.skipLine() + + // In the block context, a new line may start a simple key. + if parser.flow_level == 0 { + parser.simple_key_allowed = true + } + } else { + break // We have found a token. + } + } + + return nil +} + +// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. +// +// Scope: +// +// %YAML 1.1 # a comment \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +func (parser *Parser) scanURIEscapes(directive bool, start_mark Mark, s *[]byte) error { + // Decode the required number of characters. + w := 1024 + for w > 0 { + // Check for a URI-escaped octet. + if parser.unread < 3 { + if err := parser.updateBuffer(3); err != nil { + return err + } + } + + if !(parser.buffer[parser.buffer_pos] == '%' && + isHex(parser.buffer, parser.buffer_pos+1) && + isHex(parser.buffer, parser.buffer_pos+2)) { + return parser.setScannerTagError(directive, + start_mark, "did not find URI escaped octet") + } + + // Get the octet. + octet := byte((asHex(parser.buffer, parser.buffer_pos+1) << 4) + asHex(parser.buffer, parser.buffer_pos+2)) + + // If it is the leading octet, determine the length of the + // UTF-8 sequence. + if w == 1024 { + w = width(octet) + if w == 0 { + return parser.setScannerTagError(directive, + start_mark, "found an incorrect leading UTF-8 octet") + } + } else { + // Check if the trailing octet is correct. + if octet&0xC0 != 0x80 { + return parser.setScannerTagError(directive, + start_mark, "found an incorrect trailing UTF-8 octet") + } + } + + // Copy the octet and move the pointers. + *s = append(*s, octet) + parser.skip() + parser.skip() + parser.skip() + w-- + } + return nil +} + +// Scan a block scalar. +func (parser *Parser) scanVersionDirectiveNumber(start_mark Mark, number *int8) error { + // Repeat while the next character is digit. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + var value, length int8 + for isDigit(parser.buffer, parser.buffer_pos) { + // Check if the number is too long. + length++ + if length > max_number_length { + return formatScannerErrorContext("while scanning a %YAML directive", start_mark, + "found extremely long version number", parser.mark) + } + value = value*10 + int8(asDigit(parser.buffer, parser.buffer_pos)) + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + + // Check if the number was present. + if length == 0 { + return formatScannerErrorContext("while scanning a %YAML directive", start_mark, + "did not find expected version number", parser.mark) + } + *number = value + return nil +} + +// Scan the value of a TAG-DIRECTIVE token. +// +// Scope: +// +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +func (parser *Parser) scanVersionDirectiveValue(start_mark Mark, major, minor *int8) error { + // Eat whitespaces. + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + for isBlank(parser.buffer, parser.buffer_pos) { + parser.skip() + if parser.unread < 1 { + if err := parser.updateBuffer(1); err != nil { + return err + } + } + } + + // Consume the major version number. + if err := parser.scanVersionDirectiveNumber(start_mark, major); err != nil { + return err + } + + // Eat '.'. + if parser.buffer[parser.buffer_pos] != '.' { + return formatScannerErrorContext("while scanning a %YAML directive", start_mark, + "did not find expected digit or '.' character", parser.mark) + } + + parser.skip() + + // Consume the minor version number. + if err := parser.scanVersionDirectiveNumber(start_mark, minor); err != nil { + return err + } + return nil +} + +// max_number_length limits the length of version number components in +// %YAML directives. +func (parser *Parser) saveSimpleKey() error { + // A simple key is required at the current position if the scanner is in + // the block context and the current column coincides with the indentation + // level. + + required := parser.flow_level == 0 && parser.indent == parser.mark.Column + + // + // If the current position may start a simple key, save it. + // + if parser.simple_key_allowed { + if err := parser.removeSimpleKey(); err != nil { + return err + } + + parser.simple_key_possible = true + parser.simple_key = SimpleKey{ + required: required, + flow_level: parser.flow_level, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + mark: parser.mark, + } + } + return nil +} + +// Remove a potential simple key at the current flow level. +func (parser *Parser) removeSimpleKey() error { + // If the key is required, it is an error. + if parser.simple_key.required { + return formatScannerErrorContext( + "while scanning a simple key", parser.simple_key.mark, + "could not find expected ':'", parser.mark) + } + + parser.simple_key_possible = false // disable the key + return nil +} + +func (parser *Parser) isFlowSequence() bool { + if len(parser.tokens) == 0 { + return false + } + previousToken := parser.tokens[len(parser.tokens)-1] + return previousToken.Type == FLOW_ENTRY_TOKEN || previousToken.Type == FLOW_SEQUENCE_START_TOKEN +} + +// Check if a simple key may start at the current position and add it if +// needed. +func (parser *Parser) increaseFlowLevel() error { + // Increase the flow level. + parser.flow_level++ + if err := parser.depthCheck(parser.flow_level, &DepthContext{Kind: DepthKindFlow}); err != nil { + return formatScannerErrorContext( + "while increasing flow level", parser.simple_key.mark, + err.Error(), parser.mark) + } + + // If a simple key was possible, push it to the stack before resetting the key. + if parser.simple_key_possible { + parser.simple_key_stack = append(parser.simple_key_stack, parser.simple_key) + } + + // Reset the simple key for the new flow level. + parser.simple_key = SimpleKey{} + + return nil +} + +// Decrease the flow level. +func (parser *Parser) decreaseFlowLevel() error { + if parser.flow_level > 0 { + parser.flow_level-- + + if len(parser.simple_key_stack) == 0 { + return nil + } + + last := len(parser.simple_key_stack) - 1 + if parser.simple_key_stack[last].flow_level == parser.flow_level { + parser.simple_key = parser.simple_key_stack[last] // use last item + parser.simple_key_stack = parser.simple_key_stack[:last] // remove last item + parser.simple_key_possible = true // enable the key + } + } + return nil +} + +func (parser *Parser) rollIndent(column, number int, typ TokenType, mark Mark) error { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return nil + } + + if parser.indent < column { + // Push the current indentation level to the stack and set the new + // indentation level. + parser.indents = append(parser.indents, parser.indent) + parser.indent = column + if err := parser.depthCheck(len(parser.indents), &DepthContext{Kind: DepthKindBlock}); err != nil { + return formatScannerErrorContext( + "while increasing indent level", parser.simple_key.mark, + err.Error(), parser.mark) + } + + // Create a token and insert it into the queue. + token := Token{ + Type: typ, + StartMark: mark, + EndMark: mark, + } + if number > -1 { + number -= parser.tokens_parsed + } + parser.insertToken(number, &token) + } + return nil +} + +// Pop indentation levels from the indents stack until the current level +// becomes less or equal to the column. For each indentation level, append +// the BLOCK-END token. +func (parser *Parser) unrollIndent(column int, scan_mark Mark) error { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return nil + } + + block_mark := scan_mark + block_mark.Index-- + + // Loop through the indentation levels in the stack. + for parser.indent > column { + + // [Go] Reposition the end token before potential following + // foot comments of parent blocks. For that, search + // backwards for recent comments that were at the same + // indent as the block that is ending now. + stop_index := block_mark.Index + for i := len(parser.comments) - 1; i >= 0; i-- { + comment := &parser.comments[i] + + if comment.EndMark.Index < stop_index { + // Don't go back beyond the start of the + // comment/whitespace scan, unless column < 0. + // If requested indent column is < 0, then the + // document is over and everything else is a + // foot anyway. + break + } + if comment.StartMark.Column == parser.indent+1 { + // This is a good match. + // But maybe there's a former comment at that + // same indent level, so keep searching. + block_mark = comment.StartMark + } + + // While the end of the former comment matches with + // the start of the following one, we know there's + // nothing in between and scanning is still safe. + stop_index = comment.ScanMark.Index + } + + // Create a token and append it to the queue. + token := Token{ + Type: BLOCK_END_TOKEN, + StartMark: block_mark, + EndMark: block_mark, + } + parser.insertToken(-1, &token) + + // Pop the indentation level. + parser.indent = parser.indents[len(parser.indents)-1] + parser.indents = parser.indents[:len(parser.indents)-1] } return nil } + +// insertToken inserts a token into the queue at the specified position. +func (parser *Parser) insertToken(pos int, token *Token) { + // fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens)) + + // Check if we can move the queue at the beginning of the buffer. + if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { + if parser.tokens_head != len(parser.tokens) { + copy(parser.tokens, parser.tokens[parser.tokens_head:]) + } + parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] + parser.tokens_head = 0 + } + parser.tokens = append(parser.tokens, *token) + if pos < 0 { + return + } + copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) + parser.tokens[parser.tokens_head+pos] = *token +} + +// Initialize the scanner and produce the STREAM-START token. +func (parser *Parser) skip() { + if !isBlank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + parser.mark.Index++ + parser.mark.Column++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) +} + +// skipLine advances the parser position past the current line break. +func (parser *Parser) skipLine() { + if isCRLF(parser.buffer, parser.buffer_pos) { + parser.mark.Index += 2 + parser.mark.Column = 1 + parser.mark.Line++ + parser.unread -= 2 + parser.buffer_pos += 2 + parser.newlines++ + } else if isLineBreak(parser.buffer, parser.buffer_pos) { + parser.mark.Index++ + parser.mark.Column = 1 + parser.mark.Line++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) + parser.newlines++ + } +} + +// Copy a character to a string buffer and advance pointers. +func (parser *Parser) read(s []byte) []byte { + if !isBlank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + w := width(parser.buffer[parser.buffer_pos]) + if w == 0 { + panic("invalid character sequence") + } + if len(s) == 0 { + s = make([]byte, 0, 32) + } + if w == 1 && len(s)+w <= cap(s) { + s = s[:len(s)+1] + s[len(s)-1] = parser.buffer[parser.buffer_pos] + parser.buffer_pos++ + } else { + s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) + parser.buffer_pos += w + } + parser.mark.Index++ + parser.mark.Column++ + parser.unread-- + return s +} + +// Copy a line break character to a string buffer and advance pointers. +func (parser *Parser) readLine(s []byte) []byte { + buf := parser.buffer + pos := parser.buffer_pos + switch { + case buf[pos] == '\r' && buf[pos+1] == '\n': + // CR LF . LF + s = append(s, '\n') + parser.buffer_pos += 2 + parser.mark.Index++ + parser.unread-- + case buf[pos] == '\r' || buf[pos] == '\n': + // CR|LF . LF + s = append(s, '\n') + parser.buffer_pos += 1 + case buf[pos] == '\xC2' && buf[pos+1] == '\x85': + // NEL . LF + s = append(s, '\n') + parser.buffer_pos += 2 + case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'): + // LS|PS . LS|PS + s = append(s, buf[parser.buffer_pos:pos+3]...) + parser.buffer_pos += 3 + default: + return s + } + parser.mark.Index++ + parser.mark.Column = 1 + parser.mark.Line++ + parser.unread-- + parser.newlines++ + return s +} + +// formatScannerError creates a LoadError for scanner-stage errors. +func formatScannerError(problem string, problemMark Mark) *LoadError { + return &LoadError{ + Stage: ScannerStage, + Mark: problemMark, + Message: problem, + } +} + +// formatScannerErrorContext creates a LoadError with both context and +// problem information, each with their own mark positions. +func formatScannerErrorContext(context string, contextMark Mark, problem string, problemMark Mark) *LoadError { + return &LoadError{ + Stage: ScannerStage, + ContextMark: contextMark, + ContextMsg: context, + Mark: problemMark, + Message: problem, + } +} + +// setScannerTagError creates a tag-related scanner error with appropriate +// context based on whether it's from a directive or tag parsing. +func (parser *Parser) setScannerTagError(directive bool, contextMark Mark, problem string) error { + context := "while parsing a tag" + if directive { + context = "while parsing a %TAG directive" + } + return formatScannerErrorContext(context, contextMark, problem, parser.mark) +} + +// trace is a debug utility that prints entry/exit messages for function calls. +func trace(args ...any) func() { + pargs := append([]any{"+++"}, args...) + fmt.Println(pargs...) + pargs = append([]any{"---"}, args...) + return func() { fmt.Println(pargs...) } +} + +// Ensure that the tokens queue contains at least one token which can be +// returned to the Parser. diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/serializer.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/serializer.go index 7ec160dc8..4914dce08 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/serializer.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/serializer.go @@ -8,99 +8,158 @@ package libyaml import ( + "errors" + "fmt" + "io" "strings" "unicode/utf8" ) +// Serializer handles serialization of YAML nodes to event stream. +type Serializer struct { + Emitter Emitter + out []byte + lineWidth int + explicitStart bool + explicitEnd bool + flowSimpleCollections bool + quotePreference QuoteStyle + doneInit bool +} + +// NewSerializer creates a new Serializer with the given options. +func NewSerializer(w io.Writer, opts *Options) *Serializer { + emitter := NewEmitter() + emitter.CompactSequenceIndent = opts.CompactSeqIndent + emitter.quotePreference = opts.QuotePreference + emitter.SetWidth(opts.LineWidth) + emitter.SetUnicode(opts.Unicode) + emitter.SetCanonical(opts.Canonical) + emitter.SetLineBreak(opts.LineBreak) + + // Set indentation (defaults to 2 if not specified) + indent := opts.Indent + if indent == 0 { + indent = 2 + } + emitter.BestIndent = indent + + if w != nil { + emitter.SetOutputWriter(w) + } + + return &Serializer{ + Emitter: emitter, + lineWidth: opts.LineWidth, + explicitStart: opts.ExplicitStart, + explicitEnd: opts.ExplicitEnd, + flowSimpleCollections: opts.FlowSimpleCollections, + quotePreference: opts.QuotePreference, + } +} + +// Serialize walks a Node tree and emits events to produce YAML output. +// This is the primary method for the Serializer stage. +func (s *Serializer) Serialize(node *Node) { + s.init() + s.node(node, "") +} + +// Sentinel values for event creation. +// These provide clarity at call sites, similar to http.NoBody. +var ( + noVersionDirective *VersionDirective = nil + noTagDirective []TagDirective = nil +) + +// init initializes the serializer by emitting a STREAM_START event. +func (s *Serializer) init() { + if s.doneInit { + return + } + s.emit(NewStreamStartEvent(UTF8_ENCODING)) + s.doneInit = true +} + +// Finish completes serialization by emitting a STREAM_END event. +func (s *Serializer) Finish() { + s.Emitter.OpenEnded = false + s.emit(NewStreamEndEvent()) +} + // node serializes a Node tree into YAML events. -// This is the core of the serializer stage - it walks the tree and produces events. -func (r *Representer) node(node *Node, tail string) { +// This is the core of the serializer stage - it walks the tree and produces +// events. +func (s *Serializer) node(node *Node, tail string) { // Zero nodes behave as nil. if node.Kind == 0 && node.IsZero() { - r.nilv() + s.emitScalar("null", "", "", PLAIN_SCALAR_STYLE, nil, nil, nil, nil) return } - // If the tag was not explicitly requested, and dropping it won't change the - // implicit tag of the value, don't include it in the presentation. + // Tags have been processed by Desolver: + // - Empty tag = can be inferred or style handles it + // - Non-empty tag = emit explicitly + // Style has also been set by Desolver for quoting needs tag := node.Tag - stag := shortTag(tag) var forceQuoting bool - if tag != "" && node.Style&TaggedStyle == 0 { - if node.Kind == ScalarNode { - if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { - tag = "" - } else { - rtag, _ := resolve("", node.Value) - if rtag == stag && stag != mergeTag { - tag = "" - } else if stag == strTag { - tag = "" - forceQuoting = true - } - } - } else { - var rtag string - switch node.Kind { - case MappingNode: - rtag = mapTag - case SequenceNode: - rtag = seqTag - } - if rtag == stag { - tag = "" - } + if tag == "" && node.Kind == ScalarNode { + // Empty tag with quoting style means the string type needs to + // be preserved + if node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { + forceQuoting = true } } switch node.Kind { case DocumentNode: - event := NewDocumentStartEvent(noVersionDirective, noTagDirective, !r.explicitStart) + event := NewDocumentStartEvent(noVersionDirective, noTagDirective, !s.explicitStart) event.HeadComment = []byte(node.HeadComment) - r.emit(event) + s.emit(event) for _, node := range node.Content { - r.node(node, "") + s.node(node, "") } - event = NewDocumentEndEvent(!r.explicitEnd) + event = NewDocumentEndEvent(!s.explicitEnd) event.FootComment = []byte(node.FootComment) - r.emit(event) + s.emit(event) case SequenceNode: style := BLOCK_SEQUENCE_STYLE // Use flow style if explicitly requested or if it's a simple // collection (scalar-only contents that fit within line width, // enabled via WithFlowSimpleCollections) - if node.Style&FlowStyle != 0 || r.isSimpleCollection(node) { + if node.Style&FlowStyle != 0 || s.isSimpleCollection(node) { style = FLOW_SEQUENCE_STYLE } event := NewSequenceStartEvent([]byte(node.Anchor), []byte(longTag(tag)), tag == "", style) event.HeadComment = []byte(node.HeadComment) - r.emit(event) + s.emit(event) for _, node := range node.Content { - r.node(node, "") + s.node(node, "") } event = NewSequenceEndEvent() event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) - r.emit(event) + s.emit(event) case MappingNode: style := BLOCK_MAPPING_STYLE // Use flow style if explicitly requested or if it's a simple // collection (scalar-only contents that fit within line width, // enabled via WithFlowSimpleCollections) - if node.Style&FlowStyle != 0 || r.isSimpleCollection(node) { + if node.Style&FlowStyle != 0 || s.isSimpleCollection(node) { style = FLOW_MAPPING_STYLE } event := NewMappingStartEvent([]byte(node.Anchor), []byte(longTag(tag)), tag == "", style) event.TailComment = []byte(tail) event.HeadComment = []byte(node.HeadComment) - r.emit(event) + s.emit(event) - // The tail logic below moves the foot comment of prior keys to the following key, - // since the value for each key may be a nested structure and the foot needs to be - // processed only the entirety of the value is streamed. The last tail is processed - // with the mapping end event. + // The tail logic below moves the foot comment of prior keys to + // the following key, since the value for each key may be a + // nested structure and the foot needs to be processed only the + // entirety of the value is streamed. The last tail is + // processed with the mapping end event. var tail string for i := 0; i+1 < len(node.Content); i += 2 { k := node.Content[i] @@ -110,34 +169,35 @@ func (r *Representer) node(node *Node, tail string) { kopy.FootComment = "" k = &kopy } - r.node(k, tail) + s.node(k, tail) tail = foot v := node.Content[i+1] - r.node(v, "") + s.node(v, "") } event = NewMappingEndEvent() event.TailComment = []byte(tail) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) - r.emit(event) + s.emit(event) case AliasNode: event := NewAliasEvent([]byte(node.Value)) event.HeadComment = []byte(node.HeadComment) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) - r.emit(event) + s.emit(event) case ScalarNode: value := node.Value if !utf8.ValidString(value) { + stag := shortTag(tag) if stag == binaryTag { - failf("explicitly tagged !!binary data must be base64-encoded") + failDumpf(SerializerStage, "explicitly tagged !!binary data must be base64-encoded") } if stag != "" { - failf("cannot marshal invalid UTF-8 data as %s", stag) + failDumpf(SerializerStage, "cannot marshal invalid UTF-8 data as %s", stag) } // It can't be represented directly as YAML so use a binary tag // and represent it as base64. @@ -158,19 +218,67 @@ func (r *Representer) node(node *Node, tail string) { case strings.Contains(value, "\n"): style = LITERAL_SCALAR_STYLE case forceQuoting: - style = r.quotePreference.ScalarStyle() + style = s.quotePreference.ScalarStyle() } - r.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) + s.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) default: - failf("cannot represent node with unknown kind %d", node.Kind) + failDumpf(SerializerStage, "cannot represent node with unknown kind %d", node.Kind) + } +} + +// emit sends an event to the underlying emitter. +func (s *Serializer) emit(event Event) { + s.must(s.Emitter.Emit(&event)) +} + +// must panics if the given error is non-nil, routing to the appropriate stage. +func (s *Serializer) must(err error) { + if err == nil { + return + } + var ee EmitterError + if errors.As(err, &ee) { + failDumpf(EmitterStage, "%s", ee.Message) + } + var we WriterError + if errors.As(err, &we) { + // Unwrap to get the original I/O error, stripping the + // "write error: " prefix that WriterError adds internally. + cause := we.Err + if unwrapped := errors.Unwrap(we.Err); unwrapped != nil { + cause = unwrapped + } + failDump(WriterStage, cause) + } + msg := err.Error() + if msg == "" { + msg = fmt.Sprintf("unknown problem generating YAML content with %T", err) + } + failDumpf(SerializerStage, "%s", msg) +} + +// emitScalar emits a scalar event with the given value, anchor, tag, style, +// and associated comments. +func (s *Serializer) emitScalar( + value, anchor, tag string, style ScalarStyle, head, line, foot, tail []byte, +) { + implicit := tag == "" + if !implicit { + tag = longTag(tag) } + event := NewScalarEvent([]byte(anchor), []byte(tag), []byte(value), implicit, implicit, style) + event.HeadComment = head + event.LineComment = line + event.FootComment = foot + event.TailComment = tail + s.emit(event) } // isSimpleCollection checks if a node contains only scalar values and would // fit within the line width when rendered in flow style. -func (r *Representer) isSimpleCollection(node *Node) bool { - if !r.flowSimpleCollections { +func (s *Serializer) isSimpleCollection(node *Node) bool { + if !s.flowSimpleCollections { return false } if node.Kind != SequenceNode && node.Kind != MappingNode { @@ -183,8 +291,8 @@ func (r *Representer) isSimpleCollection(node *Node) bool { } } // Estimate flow style length - estimatedLen := r.estimateFlowLength(node) - width := r.lineWidth + estimatedLen := s.estimateFlowLength(node) + width := s.lineWidth if width <= 0 { width = 80 // Default width if not set } @@ -192,7 +300,7 @@ func (r *Representer) isSimpleCollection(node *Node) bool { } // estimateFlowLength estimates the character length of a node in flow style. -func (r *Representer) estimateFlowLength(node *Node) int { +func (s *Serializer) estimateFlowLength(node *Node) int { if node.Kind == SequenceNode { // [item1, item2, ...] = 2 + sum(len(items)) + 2*(len-1) length := 2 // [] @@ -217,3 +325,98 @@ func (r *Representer) estimateFlowLength(node *Node) int { } return 0 } + +// NewStreamStartEvent creates a new STREAM-START event. +func NewStreamStartEvent(encoding Encoding) Event { + return Event{ + Type: STREAM_START_EVENT, + encoding: encoding, + } +} + +// NewStreamEndEvent creates a new STREAM-END event. +func NewStreamEndEvent() Event { + return Event{ + Type: STREAM_END_EVENT, + } +} + +// NewDocumentStartEvent creates a new DOCUMENT-START event. +func NewDocumentStartEvent(version_directive *VersionDirective, tag_directives []TagDirective, implicit bool) Event { + return Event{ + Type: DOCUMENT_START_EVENT, + versionDirective: version_directive, + tagDirectives: tag_directives, + Implicit: implicit, + } +} + +// NewDocumentEndEvent creates a new DOCUMENT-END event. +func NewDocumentEndEvent(implicit bool) Event { + return Event{ + Type: DOCUMENT_END_EVENT, + Implicit: implicit, + } +} + +// NewAliasEvent creates a new ALIAS event. +func NewAliasEvent(anchor []byte) Event { + return Event{ + Type: ALIAS_EVENT, + Anchor: anchor, + } +} + +// NewScalarEvent creates a new SCALAR event. +func NewScalarEvent(anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style ScalarStyle) Event { + return Event{ + Type: SCALAR_EVENT, + Anchor: anchor, + Tag: tag, + Value: value, + Implicit: plain_implicit, + quoted_implicit: quoted_implicit, + Style: Style(style), + } +} + +// NewSequenceStartEvent creates a new SEQUENCE-START event. +func NewSequenceStartEvent(anchor, tag []byte, implicit bool, style SequenceStyle) Event { + return Event{ + Type: SEQUENCE_START_EVENT, + Anchor: anchor, + Tag: tag, + Implicit: implicit, + Style: Style(style), + } +} + +// NewSequenceEndEvent creates a new SEQUENCE-END event. +func NewSequenceEndEvent() Event { + return Event{ + Type: SEQUENCE_END_EVENT, + } +} + +// NewMappingStartEvent creates a new MAPPING-START event. +func NewMappingStartEvent(anchor, tag []byte, implicit bool, style MappingStyle) Event { + return Event{ + Type: MAPPING_START_EVENT, + Anchor: anchor, + Tag: tag, + Implicit: implicit, + Style: Style(style), + } +} + +// NewMappingEndEvent creates a new MAPPING-END event. +func NewMappingEndEvent() Event { + return Event{ + Type: MAPPING_END_EVENT, + } +} + +// Delete an event object. +func (e *Event) Delete() { + *e = Event{} +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/structmeta.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/structmeta.go new file mode 100644 index 000000000..70c663097 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/structmeta.go @@ -0,0 +1,250 @@ +// Copyright 2011-2019 Canonical Ltd +// Copyright 2025 The go-yaml Project Contributors +// SPDX-License-Identifier: Apache-2.0 + +// Struct metadata extraction for YAML marshaling/unmarshaling. +// +// This file analyzes Go struct types to build mappings between YAML keys and +// struct fields. It parses struct tags like `yaml:"name,omitempty,flow,inline"` +// and caches the results for efficient repeated access. +// +// Used by: +// - Constructor: maps YAML keys to struct fields when unmarshaling +// - Representer: maps struct fields to YAML keys when marshaling +// +// Key types: +// - structInfo: cached metadata about a struct type +// - fieldInfo: metadata about a single struct field +// - getStructInfo(): analyzes a struct type and returns cached metadata + +package libyaml + +import ( + "errors" + "fmt" + "reflect" + "strings" + "sync" +) + +// structInfo holds cached information about a struct's YAML-relevant fields. +type structInfo struct { + FieldsMap map[string]fieldInfo + FieldsList []fieldInfo + + // InlineMap is the number of the field in the struct that + // contains an ,inline map, or -1 if there's none. + InlineMap int + + // InlineConstructors holds indexes to inlined fields that + // contain constructor values. + InlineConstructors [][]int +} + +// fieldInfo holds information about a single struct field. +type fieldInfo struct { + Key string + Num int + OmitEmpty bool + Flow bool + // Id holds the unique field identifier, so we can cheaply + // check for field duplicates without maintaining an extra map. + Id int + + // Inline holds the field index if the field is part of an inlined struct. + Inline []int +} + +// structMap caches struct reflection information. +// fieldMapMutex protects access to structMap. +// constructorType holds the [reflect.Type] for the constructor interface. +var ( + structMap = make(map[reflect.Type]*structInfo) + fieldMapMutex sync.RWMutex + constructorType reflect.Type +) + +// constructor interface is defined here to detect types that implement +// UnmarshalYAML during struct reflection. +type constructor interface { + UnmarshalYAML(value *Node) error +} + +// init initializes the constructorType variable with the [reflect.Type] of constructor interface. +func init() { + var v constructor + constructorType = reflect.ValueOf(&v).Elem().Type() +} + +// hasConstructYAMLMethod checks if a type has an UnmarshalYAML method +// that takes a *Node from an allowlisted v3 yaml package. This detects +// v3 backward-compatible Unmarshaler implementations whose Node type +// can't be checked via interface assertion from this package. +func hasConstructYAMLMethod(t reflect.Type) bool { + method, found := t.MethodByName("UnmarshalYAML") + if !found { + return false + } + + // Check signature: func(*T) UnmarshalYAML(*Node) error + mtype := method.Type + if mtype.NumIn() != 2 || mtype.NumOut() != 1 { + return false + } + + // First param is receiver (already checked by MethodByName) + // Second param should be a pointer to a Node-like struct + paramType := mtype.In(1) + if paramType.Kind() != reflect.Ptr { + return false + } + + elemType := paramType.Elem() + if elemType.Kind() != reflect.Struct || elemType.Name() != "Node" || !isYAMLNodePkg(elemType.PkgPath()) { + return false + } + + // Return type should be error + retType := mtype.Out(0) + if retType.Kind() != reflect.Interface || retType.Name() != "error" { + return false + } + + return true +} + +func isYAMLNodePkg(pkg string) bool { + switch pkg { + case "gopkg.in/yaml.v3", "go.yaml.in/yaml/v3": + return true + } + return false +} + +// getStructInfo returns cached information about a struct type's fields. +// It parses struct tags and builds a map of field names to field info. +func getStructInfo(st reflect.Type) (*structInfo, error) { + fieldMapMutex.RLock() + sinfo, found := structMap[st] + fieldMapMutex.RUnlock() + if found { + return sinfo, nil + } + + n := st.NumField() + fieldsMap := make(map[string]fieldInfo) + fieldsList := make([]fieldInfo, 0, n) + inlineMap := -1 + inlineConstructors := [][]int(nil) + for i := 0; i != n; i++ { + field := st.Field(i) + if field.PkgPath != "" && !field.Anonymous { + continue // Private field + } + + info := fieldInfo{Num: i} + + tag := field.Tag.Get("yaml") + if tag == "" && !strings.Contains(string(field.Tag), ":") { + tag = string(field.Tag) + } + if tag == "-" { + continue + } + + inline := false + fields := strings.Split(tag, ",") + if len(fields) > 1 { + for _, flag := range fields[1:] { + switch flag { + case "omitempty": + info.OmitEmpty = true + case "flow": + info.Flow = true + case "inline": + inline = true + default: + return nil, fmt.Errorf("unsupported flag %q in tag %q of type %s", flag, tag, st) + } + } + tag = fields[0] + } + + if inline { + switch field.Type.Kind() { + case reflect.Map: + if inlineMap >= 0 { + return nil, errors.New("multiple ,inline maps in struct " + st.String()) + } + if field.Type.Key() != reflect.TypeOf("") { + return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) + } + inlineMap = info.Num + case reflect.Struct, reflect.Pointer: + ftype := field.Type + for ftype.Kind() == reflect.Pointer { + ftype = ftype.Elem() + } + if ftype.Kind() != reflect.Struct { + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + // Check for both libyaml.constructor and yaml.Unmarshaler (by method name) + if reflect.PointerTo(ftype).Implements(constructorType) || hasConstructYAMLMethod(reflect.PointerTo(ftype)) { + inlineConstructors = append(inlineConstructors, []int{i}) + } else { + sinfo, err := getStructInfo(ftype) + if err != nil { + return nil, err + } + for _, index := range sinfo.InlineConstructors { + inlineConstructors = append(inlineConstructors, append([]int{i}, index...)) + } + for _, finfo := range sinfo.FieldsList { + if _, found := fieldsMap[finfo.Key]; found { + msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + if finfo.Inline == nil { + finfo.Inline = []int{i, finfo.Num} + } else { + finfo.Inline = append([]int{i}, finfo.Inline...) + } + finfo.Id = len(fieldsList) + fieldsMap[finfo.Key] = finfo + fieldsList = append(fieldsList, finfo) + } + } + default: + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + continue + } + + if tag != "" { + info.Key = tag + } else { + info.Key = strings.ToLower(field.Name) + } + + if _, found = fieldsMap[info.Key]; found { + msg := "duplicated key '" + info.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + + info.Id = len(fieldsList) + fieldsList = append(fieldsList, info) + fieldsMap[info.Key] = info + } + + sinfo = &structInfo{ + FieldsMap: fieldsMap, + FieldsList: fieldsList, + InlineMap: inlineMap, + InlineConstructors: inlineConstructors, + } + + fieldMapMutex.Lock() + structMap[st] = sinfo + fieldMapMutex.Unlock() + return sinfo, nil +} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yaml.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/yaml.go index f9f4f1f49..04a401a33 100644 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yaml.go +++ b/vendor/go.yaml.in/yaml/v4/internal/libyaml/yaml.go @@ -11,7 +11,6 @@ package libyaml import ( "fmt" - "io" "strings" ) @@ -39,6 +38,7 @@ func (t *TagDirective) GetHandle() string { return string(t.handle) } // GetPrefix returns the tag prefix. func (t *TagDirective) GetPrefix() string { return string(t.prefix) } +// Encoding represents the character encoding of a YAML stream. type Encoding int // The stream encoding. @@ -51,6 +51,7 @@ const ( UTF16BE_ENCODING // The UTF-16-BE encoding with BOM. ) +// LineBreak represents the line break style used in YAML output. type LineBreak int // Line break types. @@ -63,6 +64,7 @@ const ( CRLN_BREAK // Use CR LN for line breaks (DOS style). ) +// QuoteStyle represents the preferred quote style for scalar values. type QuoteStyle int // Quote style types for required quoting. @@ -82,6 +84,7 @@ func (q QuoteStyle) ScalarStyle() ScalarStyle { return SINGLE_QUOTED_SCALAR_STYLE } +// ErrorType represents the category of error that occurred during processing. type ErrorType int // Many bad things could happen with the parser and emitter. @@ -101,10 +104,11 @@ const ( // Mark holds the pointer position. type Mark struct { Index int // The position index. - Line int // The position line (1-indexed). - Column int // The position column (0-indexed internally, displayed as 1-indexed). + Line int // The position line (1-indexed; 0 means unknown). + Column int // The position column (1-indexed; 0 means unknown). } +// String returns a human-readable string representation of the position mark. func (m Mark) String() string { var builder strings.Builder if m.Line == 0 { @@ -112,17 +116,60 @@ func (m Mark) String() string { } fmt.Fprintf(&builder, "line %d", m.Line) - if m.Column != 0 { - fmt.Fprintf(&builder, ", column %d", m.Column+1) + if m.Column > 0 { + fmt.Fprintf(&builder, ", column %d", m.Column) } return builder.String() } +// shortString returns a compact position string. +// Returns "" when Line is 0 (position not known). +// When Column is 0 (unknown), it is omitted from output ("L{line}"); +// otherwise it is displayed as "L{line}.C{col}". +func (m Mark) shortString() string { + if m.Line == 0 { + return "" + } + if m.Column > 0 { + return fmt.Sprintf("L%d.C%d", m.Line, m.Column) + } + return fmt.Sprintf("L%d", m.Line) +} + +// rangeString formats a position range from start mark m to end mark. +// Both marks use shortString for their individual display. +// When marks are on the same line: +// - Both Column==0: just "L2" (unknown columns, no range shown) +// - Both Column>0: "L2.C6-C7" (compact column range) +// - Mixed columns: "L1.C4-L1" (full start with line-only end) +// +// When marks are on different lines: "L1.C8-L2.C3" +func (m Mark) rangeString(end Mark) string { + start := m.shortString() + if m.Line == end.Line { + if m.Column == 0 && end.Column == 0 { + // Same line, unknown columns: just "L2" + return start + } + if m.Column > 0 && end.Column > 0 { + if m.Column == end.Column { + // Same position: just "L2.C6" + return start + } + // Same line with columns: "L2.C6-C7" + return fmt.Sprintf("%s-C%d", start, end.Column) + } + } + return fmt.Sprintf("%s-%s", start, end.shortString()) +} + // Node Styles +// styleInt is the underlying type for style constants. type styleInt int8 +// ScalarStyle represents the formatting style of a scalar value. type ScalarStyle styleInt // Scalar styles. @@ -155,6 +202,7 @@ func (style ScalarStyle) String() string { } } +// SequenceStyle represents the formatting style of a sequence node. type SequenceStyle styleInt // Sequence styles. @@ -166,6 +214,7 @@ const ( FLOW_SEQUENCE_STYLE // The flow sequence style. ) +// MappingStyle represents the formatting style of a mapping node. type MappingStyle styleInt // Mapping styles. @@ -179,6 +228,7 @@ const ( // Tokens +// TokenType represents the type of a scanned token. type TokenType int // Token types. @@ -215,6 +265,7 @@ const ( COMMENT_TOKEN // A COMMENT token. ) +// String returns a string representation of the token type. func (tt TokenType) String() string { switch tt { case NO_TOKEN: @@ -297,6 +348,7 @@ type Token struct { // Events +// EventType represents the type of a parsing or emitting event. type EventType int8 // Event types. @@ -317,6 +369,7 @@ const ( TAIL_COMMENT_EVENT ) +// eventStrings maps EventType constants to their string representations. var eventStrings = []string{ NO_EVENT: "none", STREAM_START_EVENT: "stream start", @@ -332,6 +385,7 @@ var eventStrings = []string{ TAIL_COMMENT_EVENT: "tail comment", } +// String returns a string representation of the event type. func (e EventType) String() string { if e < 0 || int(e) >= len(eventStrings) { return fmt.Sprintf("unknown event %d", e) @@ -382,9 +436,14 @@ type Event struct { Style Style } -func (e *Event) ScalarStyle() ScalarStyle { return ScalarStyle(e.Style) } +// ScalarStyle returns the style of a scalar event. +func (e *Event) ScalarStyle() ScalarStyle { return ScalarStyle(e.Style) } + +// SequenceStyle returns the style of a sequence event. func (e *Event) SequenceStyle() SequenceStyle { return SequenceStyle(e.Style) } -func (e *Event) MappingStyle() MappingStyle { return MappingStyle(e.Style) } + +// MappingStyle returns the style of a mapping event. +func (e *Event) MappingStyle() MappingStyle { return MappingStyle(e.Style) } // GetEncoding returns the stream encoding (for STREAM_START_EVENT). func (e *Event) GetEncoding() Encoding { return e.encoding } @@ -396,7 +455,6 @@ func (e *Event) GetVersionDirective() *VersionDirective { return e.versionDirect func (e *Event) GetTagDirectives() []TagDirective { return e.tagDirectives } // Nodes - const ( NULL_TAG = "tag:yaml.org,2002:null" // The tag !!null with the only possible value: null. BOOL_TAG = "tag:yaml.org,2002:bool" // The tag !!bool with the values: true and false. @@ -416,419 +474,3 @@ const ( DEFAULT_SEQUENCE_TAG = SEQ_TAG // The default sequence tag is !!seq. DEFAULT_MAPPING_TAG = MAP_TAG // The default mapping tag is !!map. ) - -type NodeType int - -// Node types. -const ( - // An empty node. - NO_NODE NodeType = iota - - SCALAR_NODE // A scalar node. - SEQUENCE_NODE // A sequence node. - MAPPING_NODE // A mapping node. -) - -// NodeItem represents an element of a sequence node. -type NodeItem int - -// NodePair represents an element of a mapping node. -type NodePair struct { - key int // The key of the element. - value int // The value of the element. -} - -// parserNode represents a single node in the YAML document tree. -type parserNode struct { - typ NodeType // The node type. - tag []byte // The node tag. - - // The node data. - - // The scalar parameters (for SCALAR_NODE). - scalar struct { - value []byte // The scalar value. - length int // The length of the scalar value. - style ScalarStyle // The scalar style. - } - - // The sequence parameters (for YAML_SEQUENCE_NODE). - sequence struct { - items_data []NodeItem // The stack of sequence items. - style SequenceStyle // The sequence style. - } - - // The mapping parameters (for MAPPING_NODE). - mapping struct { - pairs_data []NodePair // The stack of mapping pairs (key, value). - pairs_start *NodePair // The beginning of the stack. - pairs_end *NodePair // The end of the stack. - pairs_top *NodePair // The top of the stack. - style MappingStyle // The mapping style. - } - - start_mark Mark // The beginning of the node. - end_mark Mark // The end of the node. -} - -// Document structure. -type Document struct { - // The document nodes. - nodes []parserNode - - // The version directive. - version_directive *VersionDirective - - // The list of tag directives. - tag_directives_data []TagDirective - tag_directives_start int // The beginning of the tag directives list. - tag_directives_end int // The end of the tag directives list. - - start_implicit int // Is the document start indicator implicit? - end_implicit int // Is the document end indicator implicit? - - // The start/end of the document. - start_mark, end_mark Mark -} - -// ReadHandler is called when the [Parser] needs to read more bytes from the -// source. The handler should write not more than size bytes to the buffer. -// The number of written bytes should be set to the size_read variable. -// -// [in,out] data A pointer to an application data specified by -// -// yamlParser.setInput(). -// -// [out] buffer The buffer to write the data from the source. -// [in] size The size of the buffer. -// [out] size_read The actual number of bytes read from the source. -// -// On success, the handler should return 1. If the handler failed, -// the returned value should be 0. On EOF, the handler should set the -// size_read to 0 and return 1. -type ReadHandler func(parser *Parser, buffer []byte) (n int, err error) - -// SimpleKey holds information about a potential simple key. -type SimpleKey struct { - flow_level int // What flow level is the key at? - required bool // Is a simple key required? - token_number int // The number of the token. - mark Mark // The position mark. -} - -// ParserState represents the state of the parser. -type ParserState int - -const ( - PARSE_STREAM_START_STATE ParserState = iota - - PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document. - PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START. - PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document. - PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END. - PARSE_BLOCK_NODE_STATE // Expect a block node. - PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence. - PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence. - PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence. - PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. - PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key. - PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value. - PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence. - PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence. - PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping. - PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping. - PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry. - PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. - PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. - PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. - PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping. - PARSE_END_STATE // Expect nothing. -) - -func (ps ParserState) String() string { - switch ps { - case PARSE_STREAM_START_STATE: - return "PARSE_STREAM_START_STATE" - case PARSE_IMPLICIT_DOCUMENT_START_STATE: - return "PARSE_IMPLICIT_DOCUMENT_START_STATE" - case PARSE_DOCUMENT_START_STATE: - return "PARSE_DOCUMENT_START_STATE" - case PARSE_DOCUMENT_CONTENT_STATE: - return "PARSE_DOCUMENT_CONTENT_STATE" - case PARSE_DOCUMENT_END_STATE: - return "PARSE_DOCUMENT_END_STATE" - case PARSE_BLOCK_NODE_STATE: - return "PARSE_BLOCK_NODE_STATE" - case PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: - return "PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE" - case PARSE_BLOCK_SEQUENCE_ENTRY_STATE: - return "PARSE_BLOCK_SEQUENCE_ENTRY_STATE" - case PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: - return "PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE" - case PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: - return "PARSE_BLOCK_MAPPING_FIRST_KEY_STATE" - case PARSE_BLOCK_MAPPING_KEY_STATE: - return "PARSE_BLOCK_MAPPING_KEY_STATE" - case PARSE_BLOCK_MAPPING_VALUE_STATE: - return "PARSE_BLOCK_MAPPING_VALUE_STATE" - case PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: - return "PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE" - case PARSE_FLOW_SEQUENCE_ENTRY_STATE: - return "PARSE_FLOW_SEQUENCE_ENTRY_STATE" - case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: - return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE" - case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: - return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE" - case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: - return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE" - case PARSE_FLOW_MAPPING_FIRST_KEY_STATE: - return "PARSE_FLOW_MAPPING_FIRST_KEY_STATE" - case PARSE_FLOW_MAPPING_KEY_STATE: - return "PARSE_FLOW_MAPPING_KEY_STATE" - case PARSE_FLOW_MAPPING_VALUE_STATE: - return "PARSE_FLOW_MAPPING_VALUE_STATE" - case PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: - return "PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE" - case PARSE_END_STATE: - return "PARSE_END_STATE" - } - return "" -} - -// AliasData holds information about aliases. -type AliasData struct { - anchor []byte // The anchor. - index int // The node id. - mark Mark // The anchor mark. -} - -// Parser structure holds all information about the current -// state of the parser. -type Parser struct { - lastError error - - // Reader stuff - read_handler ReadHandler // Read handler. - - input_reader io.Reader // File input data. - input []byte // String input data. - input_pos int - - eof bool // EOF flag - - buffer []byte // The working buffer. - buffer_pos int // The current position of the buffer. - - unread int // The number of unread characters in the buffer. - - newlines int // The number of line breaks since last non-break/non-blank character - - raw_buffer []byte // The raw buffer. - raw_buffer_pos int // The current position of the buffer. - - encoding Encoding // The input encoding. - - offset int // The offset of the current position (in bytes). - mark Mark // The mark of the current position. - - // Comments - - HeadComment []byte // The current head comments - LineComment []byte // The current line comments - FootComment []byte // The current foot comments - tail_comment []byte // Foot comment that happens at the end of a block. - stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc) - - comments []Comment // The folded comments for all parsed tokens - comments_head int - - // Scanner stuff - - stream_start_produced bool // Have we started to scan the input stream? - stream_end_produced bool // Have we reached the end of the input stream? - - flow_level int // The number of unclosed '[' and '{' indicators. - - tokens []Token // The tokens queue. - tokens_head int // The head of the tokens queue. - tokens_parsed int // The number of tokens fetched from the queue. - token_available bool // Does the tokens queue contain a token ready for dequeueing. - - indent int // The current indentation level. - indents []int // The indentation levels stack. - - simple_key_allowed bool // May a simple key occur at the current position? - simple_key_possible bool // Is the current simple key possible? - simple_key SimpleKey // The current simple key. - simple_key_stack []SimpleKey // The stack of simple keys. - - // Parser stuff - - state ParserState // The current parser state. - states []ParserState // The parser states stack. - marks []Mark // The stack of marks. - tag_directives []TagDirective // The list of TAG directives. - - // Representer stuff - - aliases []AliasData // The alias data. - - document *Document // The currently parsed document. -} - -type Comment struct { - ScanMark Mark // Position where scanning for comments started - TokenMark Mark // Position after which tokens will be associated with this comment - StartMark Mark // Position of '#' comment mark - EndMark Mark // Position where comment terminated - - Head []byte - Line []byte - Foot []byte -} - -// Emitter Definitions - -// WriteHandler is called when the [Emitter] needs to flush the accumulated -// characters to the output. The handler should write @a size bytes of the -// @a buffer to the output. -// -// @param[in,out] data A pointer to an application data specified by -// -// yamlEmitter.setOutput(). -// -// @param[in] buffer The buffer with bytes to be written. -// @param[in] size The size of the buffer. -// -// @returns On success, the handler should return @c 1. If the handler failed, -// the returned value should be @c 0. -type WriteHandler func(emitter *Emitter, buffer []byte) error - -type EmitterState int - -// The emitter states. -const ( - // Expect STREAM-START. - EMIT_STREAM_START_STATE EmitterState = iota - - EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END. - EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END. - EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document. - EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END. - EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence. - EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out - EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence. - EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. - EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out - EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. - EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping. - EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. - EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence. - EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence. - EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. - EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping. - EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping. - EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping. - EMIT_END_STATE // Expect nothing. -) - -// Emitter holds all information about the current state of the emitter. -type Emitter struct { - // Writer stuff - - write_handler WriteHandler // Write handler. - - output_buffer *[]byte // String output data. - output_writer io.Writer // File output data. - - buffer []byte // The working buffer. - buffer_pos int // The current position of the buffer. - - encoding Encoding // The stream encoding. - - // Emitter stuff - - canonical bool // If the output is in the canonical style? - BestIndent int // The number of indentation spaces. - best_width int // The preferred width of the output lines. - unicode bool // Allow unescaped non-ASCII characters? - line_break LineBreak // The preferred line break. - quotePreference QuoteStyle // Preferred quote style when quoting is required. - - state EmitterState // The current emitter state. - states []EmitterState // The stack of states. - - events []Event // The event queue. - events_head int // The head of the event queue. - - indents []int // The stack of indentation levels. - - tag_directives []TagDirective // The list of tag directives. - - indent int // The current indentation level. - - CompactSequenceIndent bool // Is '- ' is considered part of the indentation for sequence elements? - - flow_level int // The current flow level. - - root_context bool // Is it the document root context? - sequence_context bool // Is it a sequence context? - mapping_context bool // Is it a mapping context? - simple_key_context bool // Is it a simple mapping key context? - - line int // The current line. - column int // The current column. - whitespace bool // If the last character was a whitespace? - indention bool // If the last character was an indentation character (' ', '-', '?', ':')? - OpenEnded bool // If an explicit document end is required? - - space_above bool // Is there's an empty line above? - foot_indent int // The indent used to write the foot comment above, or -1 if none. - - // Anchor analysis. - anchor_data struct { - anchor []byte // The anchor value. - alias bool // Is it an alias? - } - - // Tag analysis. - tag_data struct { - handle []byte // The tag handle. - suffix []byte // The tag suffix. - } - - // Scalar analysis. - scalar_data struct { - value []byte // The scalar value. - multiline bool // Does the scalar contain line breaks? - flow_plain_allowed bool // Can the scalar be expressed in the flow plain style? - block_plain_allowed bool // Can the scalar be expressed in the block plain style? - single_quoted_allowed bool // Can the scalar be expressed in the single quoted style? - block_allowed bool // Can the scalar be expressed in the literal or folded styles? - style ScalarStyle // The output style. - } - - // Comments - HeadComment []byte - LineComment []byte - FootComment []byte - TailComment []byte - - key_line_comment []byte - - // Representer stuff - - opened bool // If the stream was already opened? - closed bool // If the stream was already closed? - - // The information associated with the document nodes. - anchors *struct { - references int // The number of references. - anchor int // The anchor id. - serialized bool // If the node has been emitted? - } - - last_anchor_id int // The last assigned anchor id. - - document *Document // The currently emitted document. -} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamldatatest_loader.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamldatatest_loader.go deleted file mode 100644 index 5ca2ff0ea..000000000 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamldatatest_loader.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2025 The go-yaml Project Contributors -// SPDX-License-Identifier: Apache-2.0 - -// YAML test data loading utilities. -// Provides helper functions for loading and processing YAML test data, -// including scalar coercion. - -package libyaml - -import ( - "errors" - "fmt" - "io" - "strings" -) - -// coerceScalar converts a YAML scalar string to an appropriate Go type -func coerceScalar(value string) any { - // Try bool and null - switch value { - case "true": - return true - case "false": - return false - case "null": - return nil - } - - // Try hex int (0x or 0X prefix) - needed for test data byte arrays - var intVal int - if _, err := fmt.Sscanf(strings.ToLower(value), "0x%x", &intVal); err == nil { - return intVal - } - - // Try float (must check before int because %d will parse "1.5" as "1") - if strings.Contains(value, ".") { - var floatVal float64 - if _, err := fmt.Sscanf(value, "%f", &floatVal); err == nil { - return floatVal - } - } - - // Try decimal int - use int64 to handle large values on 32-bit systems - var int64Val int64 - if _, err := fmt.Sscanf(value, "%d", &int64Val); err == nil { - // Return as int if it fits, otherwise int64 - if int64Val == int64(int(int64Val)) { - return int(int64Val) - } - return int64Val - } - - // Default to string - return value -} - -// LoadYAML parses YAML data using the native libyaml Parser. -// This function is exported so it can be used by other packages for data-driven testing. -// It returns a generic interface{} which is typically: -// - map[string]interface{} for YAML mappings -// - []interface{} for YAML sequences -// - scalar values, resolved according to the following rules: -// - Booleans: "true" and "false" are returned as bool (true/false). -// - Nulls: "null" is returned as nil. -// - Floats: values containing "." are parsed as float64. -// - Decimal integers: values matching integer format are parsed as int. -// - All other values are returned as string. -// -// This scalar resolution behavior matches the implementation in coerceScalar. -func LoadYAML(data []byte) (any, error) { - parser := NewParser() - parser.SetInputString(data) - defer parser.Delete() - - type stackEntry struct { - container any // map[string]interface{} or []interface{} - key string // for maps: current key waiting for value - } - - var stack []stackEntry - var root any - - for { - var event Event - if err := parser.Parse(&event); err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, err - } - - switch event.Type { - case STREAM_END_EVENT: - // End of stream, we're done - return root, nil - - case STREAM_START_EVENT, DOCUMENT_START_EVENT: - // Structural markers, no action needed - - case MAPPING_START_EVENT: - newMap := make(map[string]any) - stack = append(stack, stackEntry{container: newMap}) - - case MAPPING_END_EVENT: - if len(stack) > 0 { - popped := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - // Add completed map to parent or set as root - if len(stack) == 0 { - root = popped.container - } else { - parent := &stack[len(stack)-1] - if m, ok := parent.container.(map[string]any); ok { - m[parent.key] = popped.container - parent.key = "" // Reset key after use - } else if s, ok := parent.container.([]any); ok { - parent.container = append(s, popped.container) - } - } - } - - case SEQUENCE_START_EVENT: - newSlice := make([]any, 0) - stack = append(stack, stackEntry{container: newSlice}) - - case SEQUENCE_END_EVENT: - if len(stack) > 0 { - popped := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - // Add completed slice to parent or set as root - if len(stack) == 0 { - root = popped.container - } else { - parent := &stack[len(stack)-1] - if m, ok := parent.container.(map[string]any); ok { - m[parent.key] = popped.container - parent.key = "" // Reset key after use - } else if s, ok := parent.container.([]any); ok { - parent.container = append(s, popped.container) - } - } - } - - case SCALAR_EVENT: - value := string(event.Value) - // Only coerce plain (unquoted) scalars - isQuoted := ScalarStyle(event.Style) != PLAIN_SCALAR_STYLE - - if len(stack) == 0 { - // Scalar at root level - if isQuoted { - root = value - } else { - root = coerceScalar(value) - } - } else { - parent := &stack[len(stack)-1] - if m, ok := parent.container.(map[string]any); ok { - if parent.key == "" { - // This scalar is a key - keep as string, don't coerce - parent.key = value - } else { - // This scalar is a value - if isQuoted { - m[parent.key] = value - } else { - m[parent.key] = coerceScalar(value) - } - parent.key = "" - } - } else if s, ok := parent.container.([]any); ok { - // Add to sequence - if isQuoted { - parent.container = append(s, value) - } else { - parent.container = append(s, coerceScalar(value)) - } - } - } - - case DOCUMENT_END_EVENT: - // Document end marker, continue processing - - case ALIAS_EVENT, TAIL_COMMENT_EVENT: - // For now, skip aliases and comments (not used in test data) - } - } - - return root, nil -} diff --git a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamlprivate.go b/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamlprivate.go deleted file mode 100644 index d23af7d9f..000000000 --- a/vendor/go.yaml.in/yaml/v4/internal/libyaml/yamlprivate.go +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2006-2010 Kirill Simonov -// Copyright 2011-2019 Canonical Ltd -// Copyright 2025 The go-yaml Project Contributors -// SPDX-License-Identifier: Apache-2.0 AND MIT - -// Internal constants and buffer sizes. -// Defines buffer sizes, stack sizes, and other internal configuration -// constants for libyaml. - -package libyaml - -const ( - // The size of the input raw buffer. - input_raw_buffer_size = 512 - - // The size of the input buffer. - // It should be possible to decode the whole raw buffer. - input_buffer_size = input_raw_buffer_size * 3 - - // The size of the output buffer. - output_buffer_size = 128 - - // The size of other stacks and queues. - initial_stack_size = 16 - initial_queue_size = 16 - initial_string_size = 16 -) - -// Check if the character at the specified position is an alphabetical -// character, a digit, '_', or '-'. -func isAlpha(b []byte, i int) bool { - return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || - b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' -} - -// Check if the character at the specified position is a flow indicator as -// defined by spec production [23] c-flow-indicator ::= -// c-collect-entry | c-sequence-start | c-sequence-end | -// c-mapping-start | c-mapping-end -func isFlowIndicator(b []byte, i int) bool { - return b[i] == '[' || b[i] == ']' || - b[i] == '{' || b[i] == '}' || b[i] == ',' -} - -// Check if the character at the specified position is valid for anchor names -// as defined by spec production [102] ns-anchor-char ::= ns-char - -// c-flow-indicator. -// This includes all printable characters except: CR, LF, BOM, space, tab, '[', -// ']', '{', '}', ','. -// 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) && - !isBOM(b, i) && - !isFlowIndicator(b, i) && - 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 valid in a tag URI. -// -// The set of valid characters is: -// -// '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', -// '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. -// -// If verbatim is true, flow indicators (',', '[', ']', '{', '}') are also -// allowed. -func isTagURIChar(b []byte, i int, verbatim bool) bool { - c := b[i] - // isAlpha covers: 0-9, A-Z, a-z, _, - - if isAlpha(b, i) { - return true - } - // Check special URI characters - switch c { - case ';', '/', '?', ':', '@', '&', '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%': - return true - case ',', '[', ']', '{', '}': - return verbatim - } - return false -} - -// 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' -} - -// Get the value of a digit. -func asDigit(b []byte, i int) int { - return int(b[i]) - '0' -} - -// Check if the character at the specified position is a hex-digit. -func isHex(b []byte, i int) bool { - return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || - b[i] >= 'a' && b[i] <= 'f' -} - -// Get the value of a hex-digit. -func asHex(b []byte, i int) int { - bi := b[i] - if bi >= 'A' && bi <= 'F' { - return int(bi) - 'A' + 10 - } - if bi >= 'a' && bi <= 'f' { - return int(bi) - 'a' + 10 - } - return int(bi) - '0' -} - -// Check if the character is ASCII. -func isASCII(b []byte, i int) bool { - return b[i] <= 0x7F -} - -// Check if the character at the start of the buffer can be printed unescaped. -func isPrintable(b []byte, i int) bool { - return ((b[i] == 0x0A) || // . == #x0A - (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E - (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF - (b[i] > 0xC2 && b[i] < 0xED) || - (b[i] == 0xED && b[i+1] < 0xA0) || - (b[i] == 0xEE) || - (b[i] == 0xEF && // #xE000 <= . <= #xFFFD - !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF - !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) -} - -// Check if the character at the specified position is NUL. -func isZeroChar(b []byte, i int) bool { - return b[i] == 0x00 -} - -// Check if the beginning of the buffer is a BOM. -func isBOM(b []byte, i int) bool { - return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF -} - -// Check if the character at the specified position is space. -func isSpace(b []byte, i int) bool { - return b[i] == ' ' -} - -// Check if the character at the specified position is tab. -func isTab(b []byte, i int) bool { - return b[i] == '\t' -} - -// Check if the character at the specified position is blank (space or tab). -func isBlank(b []byte, i int) bool { - // return isSpace(b, i) || isTab(b, i) - return b[i] == ' ' || b[i] == '\t' -} - -// Check if the character at the specified position is a line break. -func isLineBreak(b []byte, i int) bool { - return (b[i] == '\r' || // CR (#xD) - b[i] == '\n' || // LF (#xA) - b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) -} - -func isCRLF(b []byte, i int) bool { - return b[i] == '\r' && b[i+1] == '\n' -} - -// Check if the character is a line break or NUL. -func isBreakOrZero(b []byte, i int) bool { - // return isLineBreak(b, i) || isZeroChar(b, i) - return ( - // isBreak: - b[i] == '\r' || // CR (#xD) - b[i] == '\n' || // LF (#xA) - b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) - // isZeroChar: - b[i] == 0) -} - -// Check if the character is a line break, space, or NUL. -func isSpaceOrZero(b []byte, i int) bool { - // return isSpace(b, i) || isBreakOrZero(b, i) - return ( - // isSpace: - b[i] == ' ' || - // isBreakOrZero: - b[i] == '\r' || // CR (#xD) - b[i] == '\n' || // LF (#xA) - b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) - b[i] == 0) -} - -// Check if the character is a line break, space, tab, or NUL. -func isBlankOrZero(b []byte, i int) bool { - // return isBlank(b, i) || isBreakOrZero(b, i) - return ( - // isBlank: - b[i] == ' ' || b[i] == '\t' || - // isBreakOrZero: - b[i] == '\r' || // CR (#xD) - b[i] == '\n' || // LF (#xA) - b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) - b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) - b[i] == 0) -} - -// Determine the width of the character. -func width(b byte) int { - // Don't replace these by a switch without first - // confirming that it is being inlined. - if b&0x80 == 0x00 { - return 1 - } - if b&0xE0 == 0xC0 { - return 2 - } - if b&0xF0 == 0xE0 { - return 3 - } - if b&0xF8 == 0xF0 { - return 4 - } - return 0 -} diff --git a/vendor/go.yaml.in/yaml/v4/node.go b/vendor/go.yaml.in/yaml/v4/node.go new file mode 100644 index 000000000..b78a50ee1 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v4/node.go @@ -0,0 +1,98 @@ +package yaml + +import "go.yaml.in/yaml/v4/internal/libyaml" + +// ----------------------------------------------------------------------------- +// Node-related type aliases and constants +// ----------------------------------------------------------------------------- +type ( + // Node represents an element in the YAML document hierarchy. + // While documents are typically encoded and decoded into higher level + // types, such as structs and maps, Node is an intermediate representation + // that allows detailed control over the content being decoded or encoded. + // + // It's worth noting that although Node offers access into details such as + // line numbers, columns, and comments, the content when re-encoded will + // not have its original textual representation preserved. + // An effort is made to render the data pleasantly, and to preserve + // comments near the data they describe, though. + // + // Values that make use of the Node type interact with the yaml package in + // the same way any other type would do, by encoding and decoding yaml data + // directly or indirectly into them. + // + // For example: + // + // var person struct { + // Name string + // Address yaml.Node + // } + // err := yaml.Unmarshal(data, &person) + // + // Or by itself: + // + // var person Node + // err := yaml.Unmarshal(data, &person) + Node = libyaml.Node + + // Kind represents the type of YAML node. + Kind = libyaml.Kind + + // Style represents the formatting style of a YAML node. + Style = libyaml.Style + + // Marshaler interface may be implemented by types to customize their + // behavior when being marshaled into a YAML document. + Marshaler = libyaml.Marshaler + + // Unmarshaler is the interface implemented by types that can unmarshal + // a YAML description of themselves. + Unmarshaler = libyaml.Unmarshaler + + // IsZeroer is used to check whether an object is zero to determine whether + // it should be omitted when marshaling with the ,omitempty flag. + // One notable implementation is [time.Time]. + IsZeroer = libyaml.IsZeroer +) + +// Kind constants define the different types of YAML nodes. +const ( + // DocumentNode represents the root of a YAML document. + DocumentNode = libyaml.DocumentNode + + // SequenceNode represents a YAML sequence (list). + SequenceNode = libyaml.SequenceNode + + // MappingNode represents a YAML mapping (dictionary). + MappingNode = libyaml.MappingNode + + // ScalarNode represents a YAML scalar value. + ScalarNode = libyaml.ScalarNode + + // AliasNode represents a reference to an anchored node. + AliasNode = libyaml.AliasNode + + // StreamNode represents a container for multiple YAML documents. + StreamNode = libyaml.StreamNode +) + +// Style constants define different formatting styles for YAML nodes. +const ( + // TaggedStyle explicitly shows the tag on the node. + TaggedStyle = libyaml.TaggedStyle + + // DoubleQuotedStyle uses double quotes for scalar values. + DoubleQuotedStyle = libyaml.DoubleQuotedStyle + + // SingleQuotedStyle uses single quotes for scalar values. + SingleQuotedStyle = libyaml.SingleQuotedStyle + + // LiteralStyle uses literal block scalar style (|). + LiteralStyle = libyaml.LiteralStyle + + // FoldedStyle uses folded block scalar style (>). + FoldedStyle = libyaml.FoldedStyle + + // FlowStyle uses flow style (inline) formatting. + FlowStyle = libyaml.FlowStyle +) diff --git a/vendor/go.yaml.in/yaml/v4/plugin.go b/vendor/go.yaml.in/yaml/v4/plugin.go new file mode 100644 index 000000000..4f5b4a3b1 --- /dev/null +++ b/vendor/go.yaml.in/yaml/v4/plugin.go @@ -0,0 +1,25 @@ +// Copyright 2026 The go-yaml Project Contributors +// SPDX-License-Identifier: Apache-2.0 + +package yaml + +// LimitPlugin configures safety limits for YAML parsing. +// +// When registered, CheckDepth is called on each nesting depth increase, +// and CheckAlias is called on each alias expansion to detect excessive +// aliasing. +// +// Example usage: +// +// import "go.yaml.in/yaml/v4/plugin/limit" +// loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) +type LimitPlugin interface { + // CheckDepth is called when the parser increases nesting depth. + // depth is the current nesting level; ctx.Kind is "flow" or "block". + // Return an error to abort parsing. + CheckDepth(depth int, ctx *DepthContext) error + + // CheckAlias is called during alias expansion. + // Return an error to abort construction. + CheckAlias(aliasCount, constructCount int) error +} diff --git a/vendor/go.yaml.in/yaml/v4/plugin/limit/plugin.go b/vendor/go.yaml.in/yaml/v4/plugin/limit/plugin.go new file mode 100644 index 000000000..be9703eaa --- /dev/null +++ b/vendor/go.yaml.in/yaml/v4/plugin/limit/plugin.go @@ -0,0 +1,191 @@ +// Copyright 2026 The go-yaml Project Contributors +// SPDX-License-Identifier: Apache-2.0 + +// Package limit provides a configurable safety limit plugin for go-yaml. +// +// The limit plugin controls the maximum nesting depth and alias expansion +// ratio during YAML parsing. +// By default, go-yaml enforces conservative limits to prevent DoS attacks. +// This plugin lets you relax or tighten those limits for your use case. +// +// # Usage +// +// import ( +// "go.yaml.in/yaml/v4" +// "go.yaml.in/yaml/v4/plugin/limit" +// ) +// +// // Default limits +// loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New())) +// +// // Disable alias checking (e.g. for 11,000 programmatic aliases) +// loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) +// +// // Custom depth limit +// loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.DepthValue(50)))) +// +// # Third-Party Plugins +// +// You can implement [yaml.LimitPlugin] directly instead of using this package: +// +// type StrictLimit struct{} +// func (s *StrictLimit) CheckDepth(depth int, ctx *yaml.DepthContext) error { ... } +// func (s *StrictLimit) CheckAlias(aliasCount, constructCount int) error { ... } +// yaml.NewLoader(data, yaml.WithPlugin(&StrictLimit{})) +package limit + +import ( + "fmt" + + "go.yaml.in/yaml/v4/internal/libyaml" +) + +// DepthContext is an alias for the type used in depth check callbacks. +// See [yaml.DepthContext] for field documentation. +type DepthContext = libyaml.DepthContext + +// Plugin implements configurable safety limits for YAML parsing. +type Plugin struct { + depthLimit *int + depthDisabled bool + depthFn func(int, *DepthContext) error + aliasLimit *int + aliasDisabled bool + aliasFn func(int, int) error +} + +// Option configures a [Plugin]. +type Option func(*Plugin) + +// New creates a limit plugin with the given options. +// With no options, it uses the same defaults as the bare go-yaml library. +func New(opts ...Option) *Plugin { + p := &Plugin{} + for _, o := range opts { + o(p) + } + return p +} + +// DepthValue sets a maximum nesting depth (both flow and block). +func DepthValue(n int) Option { + return func(p *Plugin) { + p.depthLimit = &n + p.depthDisabled = false + p.depthFn = nil + } +} + +// DepthNone disables depth checking entirely. +func DepthNone() Option { + return func(p *Plugin) { + p.depthDisabled = true + p.depthLimit = nil + p.depthFn = nil + } +} + +// DepthFunc sets a custom depth check function. +func DepthFunc(fn func(depth int, ctx *DepthContext) error) Option { + return func(p *Plugin) { + p.depthFn = fn + p.depthLimit = nil + p.depthDisabled = false + } +} + +// AliasValue sets a simple alias expansion count threshold. +func AliasValue(n int) Option { + return func(p *Plugin) { + p.aliasLimit = &n + p.aliasDisabled = false + p.aliasFn = nil + } +} + +// AliasNone disables alias checking entirely. +func AliasNone() Option { + return func(p *Plugin) { + p.aliasDisabled = true + p.aliasLimit = nil + p.aliasFn = nil + } +} + +// AliasFunc sets a custom alias check function. +func AliasFunc(fn func(aliasCount, constructCount int) error) Option { + return func(p *Plugin) { + p.aliasFn = fn + p.aliasLimit = nil + p.aliasDisabled = false + } +} + +// CheckDepth implements [yaml.LimitPlugin]. +func (p *Plugin) CheckDepth(depth int, ctx *DepthContext) error { + if p.depthFn != nil { + return p.depthFn(depth, ctx) + } + if p.depthDisabled { + return nil + } + if p.depthLimit != nil { + if depth > *p.depthLimit { + return fmt.Errorf("exceeded max depth of %d", *p.depthLimit) + } + return nil + } + return libyaml.DefaultDepthCheck(depth, ctx) +} + +// CheckAlias implements [yaml.LimitPlugin]. +func (p *Plugin) CheckAlias(aliasCount, constructCount int) error { + if p.aliasFn != nil { + return p.aliasFn(aliasCount, constructCount) + } + if p.aliasDisabled { + return nil + } + if p.aliasLimit != nil { + if aliasCount > *p.aliasLimit { + return fmt.Errorf("exceeded max alias count of %d", *p.aliasLimit) + } + return nil + } + return libyaml.DefaultAliasCheck(aliasCount, constructCount) +} + +// NewFromYAML creates a limit plugin from a YAML config map. +// Keys: "depth" (int or null), "alias" (int or null). +// Null values disable the corresponding check. +// Omitted keys use defaults. +func NewFromYAML(cfg map[string]any) (*Plugin, error) { + var opts []Option + for key, val := range cfg { + switch key { + case "depth": + if val == nil { + opts = append(opts, DepthNone()) + } else { + n, ok := val.(int) + if !ok { + return nil, fmt.Errorf("limit: depth must be int or null, got %T", val) + } + opts = append(opts, DepthValue(n)) + } + case "alias": + if val == nil { + opts = append(opts, AliasNone()) + } else { + n, ok := val.(int) + if !ok { + return nil, fmt.Errorf("limit: alias must be int or null, got %T", val) + } + opts = append(opts, AliasValue(n)) + } + default: + return nil, fmt.Errorf("limit: unknown key %q", key) + } + } + return New(opts...), nil +} diff --git a/vendor/go.yaml.in/yaml/v4/yaml.go b/vendor/go.yaml.in/yaml/v4/yaml.go index 684d660d2..b003171db 100644 --- a/vendor/go.yaml.in/yaml/v4/yaml.go +++ b/vendor/go.yaml.in/yaml/v4/yaml.go @@ -9,15 +9,12 @@ // https://github.com/yaml/go-yaml // // This file contains: -// - Version presets (V2, V3, V4) +// - Version preset functions (WithV2Defaults, WithV3Defaults, WithV4Defaults) // - Options API (WithIndent, WithKnownFields, etc.) // - Type and constant re-exports from internal/libyaml // - Helper functions for struct field handling +// - Load/Dump API (Load, Dump, Loader, Dumper) // - Classic APIs (Decoder, Encoder, Unmarshal, Marshal) -// -// For the main API, see: -// - loader.go: Load, Loader -// - dumper.go: Dump, Dumper package yaml @@ -25,11 +22,9 @@ import ( "errors" "fmt" "io" - "reflect" - "strings" - "sync" "go.yaml.in/yaml/v4/internal/libyaml" + "go.yaml.in/yaml/v4/plugin/limit" ) //----------------------------------------------------------------------------- @@ -37,92 +32,218 @@ import ( //----------------------------------------------------------------------------- // Usage: -// yaml.Dump(&data, yaml.V3) -// yaml.Dump(&data, yaml.V3, yaml.WithIndent(2), yaml.WithCompactSeqIndent()) - -// V2 defaults: -var V2 = Options( - WithIndent(2), - WithCompactSeqIndent(false), - WithLineWidth(80), - WithUnicode(true), - WithUniqueKeys(true), - WithQuotePreference(QuoteLegacy), -) +// yaml.Dump(&data, yaml.WithV3Defaults()) +// yaml.Dump(&data, yaml.WithV3Defaults(), yaml.WithIndent(2), yaml.WithCompactSeqIndent()) + +// WithV2Defaults returns V2-compatible default options. +func WithV2Defaults() Option { + return Options( + WithIndent(2), + WithCompactSeqIndent(false), + WithLineWidth(80), + WithUnicode(true), + WithUniqueKeys(true), + WithQuotePreference(QuoteLegacy), + WithPlugin(limit.New()), + ) +} -// V3 defaults: -var V3 = Options( - WithIndent(4), - WithCompactSeqIndent(false), - WithLineWidth(80), - WithUnicode(true), - WithUniqueKeys(true), - WithQuotePreference(QuoteLegacy), -) +// WithV3Defaults returns V3-compatible default options. +func WithV3Defaults() Option { + return Options( + WithIndent(4), + WithCompactSeqIndent(false), + WithLineWidth(80), + WithUnicode(true), + WithUniqueKeys(true), + WithQuotePreference(QuoteLegacy), + WithPlugin(limit.New()), + ) +} -// V4 defaults: -var V4 = Options( - WithIndent(2), - WithCompactSeqIndent(true), - WithLineWidth(80), - WithUnicode(true), - WithUniqueKeys(true), - WithQuotePreference(QuoteSingle), -) +// WithV4Defaults returns the current V4 default options. +func WithV4Defaults() Option { + return Options( + WithIndent(2), + WithCompactSeqIndent(true), + WithLineWidth(80), + WithUnicode(true), + WithUniqueKeys(true), + WithQuotePreference(QuoteSingle), + WithPlugin(limit.New()), + ) +} //----------------------------------------------------------------------------- // Options //----------------------------------------------------------------------------- // Option allows configuring YAML loading and dumping operations. -// Re-exported from internal/libyaml. type Option = libyaml.Option +// Option configuration functions var ( - // WithIndent sets indentation spaces (2-9). - // See internal/libyaml.WithIndent. + // WithIndent sets the number of spaces to use for indentation when + // dumping YAML content. + // + // Valid values are 2-9. Common choices: 2 (compact), 4 (readable). WithIndent = libyaml.WithIndent - // WithCompactSeqIndent configures '- ' as part of indentation. - // See internal/libyaml.WithCompactSeqIndent. + + // WithCompactSeqIndent configures whether the sequence indicator '- ' is + // considered part of the indentation when dumping YAML content. + // + // If compact is true, '- ' is treated as part of the indentation. + // If compact is false, '- ' is not treated as part of the indentation. + // When called without arguments, defaults to true. WithCompactSeqIndent = libyaml.WithCompactSeqIndent - // WithKnownFields enables strict field checking during loading. - // See internal/libyaml.WithKnownFields. + + // WithKnownFields enables or disables strict field checking during YAML + // loading. + // + // When enabled, loading will return an error if the YAML input contains + // fields that do not correspond to any fields in the target struct. + // When called without arguments, defaults to true. WithKnownFields = libyaml.WithKnownFields - // WithSingleDocument only processes first document in stream. - // See internal/libyaml.WithSingleDocument. + + // WithSingleDocument configures the Loader to only process the first + // document in a YAML stream. After the first document is loaded, + // subsequent calls to Load will return [io.EOF]. + // + // When called without arguments, defaults to true. + // + // This is useful when you expect exactly one document and want behavior + // similar to Unmarshal. WithSingleDocument = libyaml.WithSingleDocument - // WithStreamNodes enables stream boundary nodes when loading. - // See internal/libyaml.WithStreamNodes. + + // WithStreamNodes enables returning stream boundary nodes when loading + // YAML. + // + // When enabled, Loader.Load returns an interleaved sequence of + // StreamNode and DocumentNode values: + // + // [StreamNode, DocNode, StreamNode, DocNode, ..., StreamNode] + // + // StreamNodes contain metadata about the stream including: + // - Encoding (UTF-8, UTF-16LE, UTF-16BE) + // - YAML version directive (%YAML) + // - Tag directives (%TAG) + // - Position information (Line, Column) + // + // An empty YAML stream returns a single StreamNode. + // When called without arguments, defaults to true. + // + // The default is false. WithStreamNodes = libyaml.WithStreamNodes - // WithAllDocuments enables multi-document mode for Load and Dump. - // See internal/libyaml.WithAllDocuments. + + // WithAllDocuments enables multi-document mode for Load and Dump + // operations. + // + // When used with Load, the target must be a pointer to a slice. + // All documents in the YAML stream will be decoded into the slice. + // Zero documents results in an empty slice (no error). + // + // When used with Dump, the input must be a slice. + // Each element will be encoded as a separate YAML document + // with "---" separators. + // + // When called without arguments, defaults to true. + // + // The default is false (single-document mode). WithAllDocuments = libyaml.WithAllDocuments - // WithLineWidth sets preferred line width for output. - // See internal/libyaml.WithLineWidth. + + // WithLineWidth sets the preferred line width for YAML output. + // + // When encoding long strings, the encoder will attempt to wrap them at + // this width using literal block style (|). Set to -1 or 0 for unlimited + // width. + // + // The default is 80 characters. WithLineWidth = libyaml.WithLineWidth - // WithUnicode controls non-ASCII characters in output. - // See internal/libyaml.WithUnicode. + + // WithUnicode controls whether non-ASCII characters are allowed in YAML + // output. + // + // When true, non-ASCII characters appear as-is (e.g., "café"). + // When false, non-ASCII characters are escaped (e.g., "caf\u00e9"). + // When called without arguments, defaults to true. + // + // The default is true. WithUnicode = libyaml.WithUnicode - // WithUniqueKeys enables duplicate key detection. - // See internal/libyaml.WithUniqueKeys. + + // WithUniqueKeys enables or disables duplicate key detection during YAML + // loading. + // + // When enabled, loading will return an error if the YAML input contains + // duplicate keys in any mapping. This is a security feature that prevents + // key override attacks. + // When called without arguments, defaults to true. + // + // The default is true. WithUniqueKeys = libyaml.WithUniqueKeys + // WithCanonical forces canonical YAML output format. - // See internal/libyaml.WithCanonical. + // + // When enabled, the encoder outputs strictly canonical YAML with explicit + // tags for all values. This produces verbose output primarily useful for + // debugging and YAML spec compliance testing. + // When called without arguments, defaults to true. + // + // The default is false. WithCanonical = libyaml.WithCanonical - // WithLineBreak sets line ending style for output. - // See internal/libyaml.WithLineBreak. + + // WithLineBreak sets the line ending style for YAML output. + // + // Available options: + // - LineBreakLN: Unix-style \n (default) + // - LineBreakCR: Old Mac-style \r + // - LineBreakCRLN: Windows-style \r\n + // + // The default is LineBreakLN. WithLineBreak = libyaml.WithLineBreak - // WithExplicitStart controls document start markers (---). - // See internal/libyaml.WithExplicitStart. + + // WithExplicitStart controls whether document start markers (---) are + // always emitted. + // + // When true, every document begins with an explicit "---" marker. + // When false (default), the marker is omitted for the first document. + // When called without arguments, defaults to true. WithExplicitStart = libyaml.WithExplicitStart - // WithExplicitEnd controls document end markers (...). - // See internal/libyaml.WithExplicitEnd. + + // WithExplicitEnd controls whether document end markers (...) are always + // emitted. + // + // When true, every document ends with an explicit "..." marker. + // When false (default), the marker is omitted. + // When called without arguments, defaults to true. WithExplicitEnd = libyaml.WithExplicitEnd - // WithFlowSimpleCollections controls flow style for simple collections. - // See internal/libyaml.WithFlowSimpleCollections. + + // WithFlowSimpleCollections controls whether simple collections use flow + // style. + // + // When true, sequences and mappings containing only scalar values (no + // nested collections) are rendered in flow style if they fit within the + // line width. + // Example: {name: test, count: 42} or [a, b, c] + // When called without arguments, defaults to true. + // + // When false (default), all collections use block style. WithFlowSimpleCollections = libyaml.WithFlowSimpleCollections - // WithQuotePreference sets preferred quote style when quoting is required. - // See internal/libyaml.WithQuotePreference. + + // WithQuotePreference sets the preferred quote style for strings that + // require quoting. + // + // This option only affects strings that require quoting per the YAML spec. + // Plain strings that don't need quoting remain unquoted regardless of this + // setting. Quoting is required for: + // - Strings that look like other YAML types (true, false, null, 123, etc.) + // - Strings with leading/trailing whitespace + // - Strings containing special YAML syntax characters + // - Empty strings in certain contexts + // + // Quote styles: + // - QuoteSingle: Use single quotes (v4 default) + // - QuoteDouble: Use double quotes + // - QuoteLegacy: Legacy v2/v3 behavior (mixed quoting) WithQuotePreference = libyaml.WithQuotePreference ) @@ -132,12 +253,55 @@ var ( // // Example: // -// opts := yaml.Options(yaml.V4, yaml.WithIndent(3)) +// opts := yaml.Options(yaml.WithV4Defaults(), yaml.WithIndent(3)) // yaml.Dump(&data, opts) func Options(opts ...Option) Option { return libyaml.CombineOptions(opts...) } +// DepthKind represents the type of nesting (flow or block). +type DepthKind = libyaml.DepthKind + +// DepthKind constants for nesting depth checks. +const ( + DepthKindFlow = libyaml.DepthKindFlow + DepthKindBlock = libyaml.DepthKindBlock +) + +// DepthContext holds context about a nesting depth check. +type DepthContext = libyaml.DepthContext + +// WithPlugin registers one or more plugins for YAML processing. +// +// Plugins extend the YAML library with custom processing logic. +// Each plugin implements one or more plugin interfaces. +// Currently supported plugin types: +// - LimitPlugin: Controls depth and alias expansion limits +// +// Example: +// +// import "go.yaml.in/yaml/v4/plugin/limit" +// loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) +// +// Plugins use public types and can be implemented by external packages. +func WithPlugin(plugins ...any) Option { + return func(o *libyaml.Options) error { + for _, p := range plugins { + registered := false + if lp, ok := p.(LimitPlugin); ok { + o.DepthCheck = lp.CheckDepth + o.AliasCheck = lp.CheckAlias + registered = true + } + // Future plugin types add cases here (non-exclusive if) + if !registered { + return fmt.Errorf("yaml: unsupported plugin type: %T", p) + } + } + return nil + } +} + // OptsYAML parses a YAML string containing option settings and returns // an Option that can be combined with other options using Options(). // @@ -154,6 +318,12 @@ func Options(opts ...Option) Option { // - known-fields (bool) // - single-document (bool) // - unique-keys (bool) +// - plugin (map of plugin name to config) +// +// The plugin field configures plugins by name. Each key is a plugin +// name and the value is its configuration map (or null for defaults). +// Currently supported: "limit" with keys "depth" and "alias" (int +// or null to disable). // // Only fields specified in the YAML will override other options when // combined. Unspecified fields won't affect other options. @@ -163,22 +333,26 @@ func Options(opts ...Option) Option { // opts, err := yaml.OptsYAML(` // indent: 3 // known-fields: true +// plugin: +// limit: +// depth: 50 // `) // yaml.Dump(&data, yaml.Options(V4, opts)) func OptsYAML(yamlStr string) (Option, error) { var cfg struct { - Indent *int `yaml:"indent"` - CompactSeqIndent *bool `yaml:"compact-seq-indent"` - LineWidth *int `yaml:"line-width"` - Unicode *bool `yaml:"unicode"` - Canonical *bool `yaml:"canonical"` - LineBreak *string `yaml:"line-break"` - ExplicitStart *bool `yaml:"explicit-start"` - ExplicitEnd *bool `yaml:"explicit-end"` - FlowSimpleCollections *bool `yaml:"flow-simple-coll"` - KnownFields *bool `yaml:"known-fields"` - SingleDocument *bool `yaml:"single-document"` - UniqueKeys *bool `yaml:"unique-keys"` + Indent *int `yaml:"indent"` + CompactSeqIndent *bool `yaml:"compact-seq-indent"` + LineWidth *int `yaml:"line-width"` + Unicode *bool `yaml:"unicode"` + Canonical *bool `yaml:"canonical"` + LineBreak *string `yaml:"line-break"` + ExplicitStart *bool `yaml:"explicit-start"` + ExplicitEnd *bool `yaml:"explicit-end"` + FlowSimpleCollections *bool `yaml:"flow-simple-coll"` + KnownFields *bool `yaml:"known-fields"` + SingleDocument *bool `yaml:"single-document"` + UniqueKeys *bool `yaml:"unique-keys"` + Plugin map[string]any `yaml:"plugin"` } if err := Load([]byte(yamlStr), &cfg, WithKnownFields()); err != nil { return nil, err @@ -232,6 +406,28 @@ func OptsYAML(yamlStr string) (Option, error) { } } + for name, val := range cfg.Plugin { + switch name { + case "limit": + var cfgMap map[string]any + switch v := val.(type) { + case nil: + cfgMap = map[string]any{} + case map[string]any: + cfgMap = v + default: + return nil, fmt.Errorf("yaml: plugin %q value must be a mapping or null", name) + } + p, err := limit.NewFromYAML(cfgMap) + if err != nil { + return nil, err + } + optList = append(optList, WithPlugin(p)) + default: + return nil, fmt.Errorf("yaml: unknown plugin %q", name) + } + } + return Options(optList...), nil } @@ -239,60 +435,77 @@ func OptsYAML(yamlStr string) (Option, error) { // Type and constant re-exports //----------------------------------------------------------------------------- -type ( - // Node represents a YAML node in the document tree. - // See internal/libyaml.Node. - Node = libyaml.Node - // Kind identifies the type of a YAML node. - // See internal/libyaml.Kind. - Kind = libyaml.Kind - // Style controls the presentation of a YAML node. - // See internal/libyaml.Style. - Style = libyaml.Style - // Marshaler is implemented by types with custom YAML marshaling. - // See internal/libyaml.Marshaler. - Marshaler = libyaml.Marshaler - // IsZeroer is implemented by types that can report if they're zero. - // See internal/libyaml.IsZeroer. - IsZeroer = libyaml.IsZeroer -) - -// Unmarshaler is the interface implemented by types -// that can unmarshal a YAML description of themselves. -type Unmarshaler interface { - UnmarshalYAML(node *Node) error -} - // Re-export stream-related types type ( + Stream = libyaml.Stream VersionDirective = libyaml.StreamVersionDirective - TagDirective = libyaml.StreamTagDirective - Encoding = libyaml.Encoding + + // TagDirective represents a YAML %TAG directive for stream nodes. + TagDirective = libyaml.StreamTagDirective + + // Encoding represents the character encoding of a YAML stream. + Encoding = libyaml.Encoding ) -// Re-export encoding constants +// Encoding constants for YAML stream encoding const ( - EncodingAny = libyaml.ANY_ENCODING - EncodingUTF8 = libyaml.UTF8_ENCODING + // EncodingAny lets the parser choose the encoding. + EncodingAny = libyaml.ANY_ENCODING + + // EncodingUTF8 is the default UTF-8 encoding. + EncodingUTF8 = libyaml.UTF8_ENCODING + + // EncodingUTF16LE is UTF-16-LE encoding with BOM. EncodingUTF16LE = libyaml.UTF16LE_ENCODING + + // EncodingUTF16BE is UTF-16-BE encoding with BOM. EncodingUTF16BE = libyaml.UTF16BE_ENCODING ) -// Re-export error types -type ( +// Stage identifies the processing stage where an error occurred during YAML +// loading or dumping. +type Stage = libyaml.Stage +// Stage constants for YAML processing pipeline. +const ( + // Load stages + ReaderStage = libyaml.ReaderStage // Input reading and encoding + ScannerStage = libyaml.ScannerStage // Tokenization + ParserStage = libyaml.ParserStage // Event stream parsing + ComposerStage = libyaml.ComposerStage // Node tree construction + ResolverStage = libyaml.ResolverStage // Tag resolution + ConstructorStage = libyaml.ConstructorStage // Go value construction + + // Dump stages + RepresenterStage = libyaml.RepresenterStage // Go value to Node tree + SerializerStage = libyaml.SerializerStage // Node tree to events + EmitterStage = libyaml.EmitterStage // Events to YAML bytes + WriterStage = libyaml.WriterStage // Output writing +) + +// Mark represents a position in the YAML document. +type Mark = libyaml.Mark + +// Error types for YAML loading and dumping +type ( // LoadError represents an error encountered while decoding a YAML document. // // It contains details about the location in the document where the error - // occurred, as well as a descriptive message. - LoadError = libyaml.ConstructError + // occurred, as well as the processing stage that generated it. + LoadError = libyaml.LoadError // LoadErrors is returned when one or more fields cannot be properly decoded. // // It contains multiple *[LoadError] instances with details about each error. LoadErrors = libyaml.LoadErrors - // TypeError is an obsolete error type retained for compatibility. + // DumpError represents an error that occurred while dumping a YAML document. + // + // It identifies the processing stage where the error occurred and provides + // an optional underlying cause via Unwrap. + DumpError = libyaml.DumpError + + // TypeError is a legacy error type retained for compatibility. // // Deprecated: Use [LoadErrors] instead. // @@ -300,25 +513,13 @@ type ( TypeError = libyaml.TypeError ) -// Re-export Kind constants -const ( - DocumentNode = libyaml.DocumentNode - SequenceNode = libyaml.SequenceNode - MappingNode = libyaml.MappingNode - ScalarNode = libyaml.ScalarNode - AliasNode = libyaml.AliasNode - StreamNode = libyaml.StreamNode -) +// NewLoadError creates a LoadError with an underlying cause error. +// The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. +var NewLoadError = libyaml.NewLoadError -// Re-export Style constants -const ( - TaggedStyle = libyaml.TaggedStyle - DoubleQuotedStyle = libyaml.DoubleQuotedStyle - SingleQuotedStyle = libyaml.SingleQuotedStyle - LiteralStyle = libyaml.LiteralStyle - FoldedStyle = libyaml.FoldedStyle - FlowStyle = libyaml.FlowStyle -) +// NewDumpError creates a DumpError with an underlying cause error. +// The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. +var NewDumpError = libyaml.NewDumpError // LineBreak represents the line ending style for YAML output. type LineBreak = libyaml.LineBreak @@ -341,180 +542,37 @@ const ( ) //----------------------------------------------------------------------------- -// Helper functions +// Load/Dump API //----------------------------------------------------------------------------- -// The code in this section was copied from mgo/bson. +// Advanced streaming API types +type ( + // Loader reads and loads YAML values from an input stream with + // configurable options. + Loader = libyaml.Loader -var ( - structMap = make(map[reflect.Type]*structInfo) - fieldMapMutex sync.RWMutex - unmarshalerType reflect.Type + // Dumper writes YAML values to an output stream with configurable options. + Dumper = libyaml.Dumper ) -// structInfo holds details for the serialization of fields of -// a given struct. -type structInfo struct { - FieldsMap map[string]fieldInfo - FieldsList []fieldInfo - - // InlineMap is the number of the field in the struct that - // contains an ,inline map, or -1 if there's none. - InlineMap int - - // InlineUnmarshalers holds indexes to inlined fields that - // contain unmarshaler values. - InlineUnmarshalers [][]int +// NewLoader returns a new Loader that reads from r with the given options. +func NewLoader(r io.Reader, opts ...Option) (*Loader, error) { + return libyaml.NewLoader(r, opts...) } -type fieldInfo struct { - Key string - Num int - OmitEmpty bool - Flow bool - // Id holds the unique field identifier, so we can cheaply - // check for field duplicates without maintaining an extra map. - Id int - - // Inline holds the field index if the field is part of an inlined struct. - Inline []int +// NewDumper returns a new Dumper that writes to w with the given options. +func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { + return libyaml.NewDumper(w, opts...) } -func getStructInfo(st reflect.Type) (*structInfo, error) { - fieldMapMutex.RLock() - sinfo, found := structMap[st] - fieldMapMutex.RUnlock() - if found { - return sinfo, nil - } - - n := st.NumField() - fieldsMap := make(map[string]fieldInfo) - fieldsList := make([]fieldInfo, 0, n) - inlineMap := -1 - inlineUnmarshalers := [][]int(nil) - for i := 0; i != n; i++ { - field := st.Field(i) - if field.PkgPath != "" && !field.Anonymous { - continue // Private field - } - - info := fieldInfo{Num: i} - - tag := field.Tag.Get("yaml") - if tag == "" && !strings.Contains(string(field.Tag), ":") { - tag = string(field.Tag) - } - if tag == "-" { - continue - } - - inline := false - fields := strings.Split(tag, ",") - if len(fields) > 1 { - for _, flag := range fields[1:] { - switch flag { - case "omitempty": - info.OmitEmpty = true - case "flow": - info.Flow = true - case "inline": - inline = true - default: - return nil, fmt.Errorf("unsupported flag %q in tag %q of type %s", flag, tag, st) - } - } - tag = fields[0] - } - - if inline { - switch field.Type.Kind() { - case reflect.Map: - if inlineMap >= 0 { - return nil, errors.New("multiple ,inline maps in struct " + st.String()) - } - if field.Type.Key() != reflect.TypeOf("") { - return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) - } - inlineMap = info.Num - case reflect.Struct, reflect.Pointer: - ftype := field.Type - for ftype.Kind() == reflect.Pointer { - ftype = ftype.Elem() - } - if ftype.Kind() != reflect.Struct { - return nil, errors.New("option ,inline may only be used on a struct or map field") - } - if reflect.PointerTo(ftype).Implements(unmarshalerType) { - inlineUnmarshalers = append(inlineUnmarshalers, []int{i}) - } else { - sinfo, err := getStructInfo(ftype) - if err != nil { - return nil, err - } - for _, index := range sinfo.InlineUnmarshalers { - inlineUnmarshalers = append(inlineUnmarshalers, append([]int{i}, index...)) - } - for _, finfo := range sinfo.FieldsList { - if _, found := fieldsMap[finfo.Key]; found { - msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() - return nil, errors.New(msg) - } - if finfo.Inline == nil { - finfo.Inline = []int{i, finfo.Num} - } else { - finfo.Inline = append([]int{i}, finfo.Inline...) - } - finfo.Id = len(fieldsList) - fieldsMap[finfo.Key] = finfo - fieldsList = append(fieldsList, finfo) - } - } - default: - return nil, errors.New("option ,inline may only be used on a struct or map field") - } - continue - } - - if tag != "" { - info.Key = tag - } else { - info.Key = strings.ToLower(field.Name) - } - - if _, found = fieldsMap[info.Key]; found { - msg := "duplicated key '" + info.Key + "' in struct " + st.String() - return nil, errors.New(msg) - } - - info.Id = len(fieldsList) - fieldsList = append(fieldsList, info) - fieldsMap[info.Key] = info - } - - sinfo = &structInfo{ - FieldsMap: fieldsMap, - FieldsList: fieldsList, - InlineMap: inlineMap, - InlineUnmarshalers: inlineUnmarshalers, - } - - fieldMapMutex.Lock() - structMap[st] = sinfo - fieldMapMutex.Unlock() - return sinfo, nil +// Load loads YAML document(s) with the given options. +func Load(in []byte, out any, opts ...Option) error { + return libyaml.Load(in, out, opts...) } -var noWriter io.Writer - -func handleErr(err *error) { - if v := recover(); v != nil { - if e, ok := v.(*libyaml.YAMLError); ok { - *err = e.Err - } else { - panic(v) - } - } +// Dump encodes a value to YAML with the given options. +func Dump(in any, opts ...Option) (out []byte, err error) { + return libyaml.Dump(in, opts...) } //----------------------------------------------------------------------------- @@ -523,8 +581,7 @@ func handleErr(err *error) { // A Decoder reads and decodes YAML values from an input stream. type Decoder struct { - composer *libyaml.Composer - knownFields bool + loader *Loader } // NewDecoder returns a new decoder that reads from r. @@ -532,15 +589,15 @@ type Decoder struct { // The decoder introduces its own buffering and may read // data from r beyond the YAML values requested. func NewDecoder(r io.Reader) *Decoder { - return &Decoder{ - composer: libyaml.NewComposerFromReader(r), - } + // NewLoader won't return error with WithV3Defaults() and withFromLegacy + loader, _ := NewLoader(r, WithV3Defaults(), withFromLegacy()) + return &Decoder{loader: loader} } // KnownFields ensures that the keys in decoded mappings to // exist as fields in the struct being decoded into. func (dec *Decoder) KnownFields(enable bool) { - dec.knownFields = enable + dec.loader.SetKnownFields(enable) } // Decode reads the next YAML-encoded value from its input @@ -548,37 +605,22 @@ func (dec *Decoder) KnownFields(enable bool) { // // See the documentation for Unmarshal for details about the // conversion of YAML into a Go value. -func (dec *Decoder) Decode(v any) (err error) { - d := libyaml.NewConstructor(libyaml.DefaultOptions) - d.KnownFields = dec.knownFields - defer handleErr(&err) - node := dec.composer.Parse() - if node == nil { - return io.EOF - } - out := reflect.ValueOf(v) - if out.Kind() == reflect.Pointer && !out.IsNil() { - out = out.Elem() - } - d.Construct(node, out) - if len(d.TypeErrors) > 0 { - return &LoadErrors{Errors: d.TypeErrors} - } - return nil +func (dec *Decoder) Decode(v any) error { + return dec.loader.Load(v) } // An Encoder writes YAML values to an output stream. type Encoder struct { - encoder *libyaml.Representer + dumper *Dumper } // NewEncoder returns a new encoder that writes to w. // The Encoder should be closed after use to flush all data // to w. func NewEncoder(w io.Writer) *Encoder { - return &Encoder{ - encoder: libyaml.NewRepresenter(w, libyaml.DefaultOptions), - } + // NewDumper won't return an error when using WithV3Defaults() + dumper, _ := NewDumper(w, WithV3Defaults()) + return &Encoder{dumper: dumper} } // Encode writes the YAML encoding of v to the stream. @@ -588,36 +630,29 @@ func NewEncoder(w io.Writer) *Encoder { // // See the documentation for Marshal for details about the conversion of Go // values to YAML. -func (e *Encoder) Encode(v any) (err error) { - defer handleErr(&err) - e.encoder.MarshalDoc("", reflect.ValueOf(v)) - return nil +func (e *Encoder) Encode(v any) error { + return e.dumper.Dump(v) } // SetIndent changes the used indentation used when encoding. func (e *Encoder) SetIndent(spaces int) { - if spaces < 0 { - panic("yaml: cannot indent to a negative number of spaces") - } - e.encoder.Indent = spaces + e.dumper.SetIndent(spaces) } // CompactSeqIndent makes it so that '- ' is considered part of the indentation. func (e *Encoder) CompactSeqIndent() { - e.encoder.Emitter.CompactSequenceIndent = true + e.dumper.SetCompactSeqIndent(true) } // DefaultSeqIndent makes it so that '- ' is not considered part of the indentation. func (e *Encoder) DefaultSeqIndent() { - e.encoder.Emitter.CompactSequenceIndent = false + e.dumper.SetCompactSeqIndent(false) } // Close closes the encoder by writing any remaining data. // It does not write a stream terminating string "...". -func (e *Encoder) Close() (err error) { - defer handleErr(&err) - e.encoder.Finish() - return nil +func (e *Encoder) Close() error { + return e.dumper.Close() } // Unmarshal decodes the first document found within the in byte slice @@ -654,28 +689,17 @@ func (e *Encoder) Close() (err error) { // See the documentation of Marshal for the format of tags and a list of // supported tag options. func Unmarshal(in []byte, out any) (err error) { - return unmarshal(in, out, V3) + return Load(in, out, WithV3Defaults(), withFromLegacy()) } -func unmarshal(in []byte, out any, opts ...Option) (err error) { - defer handleErr(&err) - o, err := libyaml.ApplyOptions(opts...) - if err != nil { - return err - } - - // Check if out implements yaml.Unmarshaler - if u, ok := out.(Unmarshaler); ok { - p := libyaml.NewComposer(in) - defer p.Destroy() - node := p.Parse() - if node != nil { - return u.UnmarshalYAML(node) - } +// withFromLegacy is a private option that indicates this call is from +// a legacy API (Unmarshal/Decoder). It enables Unmarshaler interface +// checking and allows trailing content for backward compatibility. +func withFromLegacy() Option { + return func(o *libyaml.Options) error { + o.FromLegacy = true return nil } - - return libyaml.Construct(in, out, o) } // Marshal serializes the value provided into a YAML document. The structure @@ -722,11 +746,6 @@ func unmarshal(in []byte, out any, opts ...Option) (err error) { // yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" // yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" func Marshal(in any) (out []byte, err error) { - defer handleErr(&err) - e := libyaml.NewRepresenter(noWriter, libyaml.DefaultOptions) - defer e.Destroy() - e.MarshalDoc("", reflect.ValueOf(in)) - e.Finish() - out = e.Out - return out, err + // Use WithV3Defaults() with unlimited line width to match legacy DefaultOptions + return Dump(in, WithV3Defaults(), WithLineWidth(-1)) } diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index f69fd7546..c261a8ebb 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -109,7 +109,7 @@ func (g *Group) TryGo(f func() error) bool { if g.sem != nil { select { case g.sem <- token{}: - // Note: this allows barging iff channels in general allow barging. + // Note: this allows barging if and only if channels in general allow barging. default: return false } diff --git a/vendor/golang.org/x/sync/semaphore/semaphore.go b/vendor/golang.org/x/sync/semaphore/semaphore.go index b618162aa..040c5bc50 100644 --- a/vendor/golang.org/x/sync/semaphore/semaphore.go +++ b/vendor/golang.org/x/sync/semaphore/semaphore.go @@ -83,7 +83,7 @@ func (s *Weighted) Acquire(ctx context.Context, n int64) error { default: isFront := s.waiters.Front() == elem s.waiters.Remove(elem) - // If we're at the front and there're extra tokens left, notify other waiters. + // If we're at the front and there are extra tokens left, notify other waiters. if isFront && s.size > s.cur { s.notifyWaiters() } @@ -139,15 +139,15 @@ func (s *Weighted) notifyWaiters() { w := next.Value.(waiter) if s.size-s.cur < w.n { - // Not enough tokens for the next waiter. We could keep going (to try to + // Not enough tokens for the next waiter. We could keep going (to try to // find a waiter with a smaller request), but under load that could cause // starvation for large requests; instead, we leave all remaining waiters // blocked. // // Consider a semaphore used as a read-write lock, with N tokens, N - // readers, and one writer. Each reader can Acquire(1) to obtain a read - // lock. The writer can Acquire(N) to obtain a write lock, excluding all - // of the readers. If we allow the readers to jump ahead in the queue, + // readers, and one writer. Each reader can Acquire(1) to obtain a read + // lock. The writer can Acquire(N) to obtain a write lock, excluding all + // of the readers. If we allow the readers to jump ahead in the queue, // the writer will starve — there is always one token available for every // reader. break diff --git a/vendor/golang.org/x/sync/singleflight/singleflight.go b/vendor/golang.org/x/sync/singleflight/singleflight.go index 90ca138af..32e40bdc8 100644 --- a/vendor/golang.org/x/sync/singleflight/singleflight.go +++ b/vendor/golang.org/x/sync/singleflight/singleflight.go @@ -15,12 +15,12 @@ import ( "sync" ) -// errGoexit indicates the runtime.Goexit was called in -// the user given function. +// errGoexit indicates runtime.Goexit was called in +// the user-given function. var errGoexit = errors.New("runtime.Goexit was called") // A panicError is an arbitrary value recovered from a panic -// with the stack trace during the execution of given function. +// with the stack trace during the execution of the given function. type panicError struct { value any stack []byte @@ -204,7 +204,7 @@ func (g *Group) doCall(c *call, key string, fn func() (any, error)) { } } -// Forget tells the singleflight to forget about a key. Future calls +// Forget tells the singleflight to forget about a key. Future calls // to Do for this key will call the function rather than waiting for // an earlier call to complete. func (g *Group) Forget(key string) { diff --git a/vendor/modules.txt b/vendor/modules.txt index b203638da..28c77709d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -647,8 +647,8 @@ github.com/openshift/machine-config-operator/pkg/daemon/constants github.com/pb33f/jsonpath/pkg/jsonpath github.com/pb33f/jsonpath/pkg/jsonpath/config github.com/pb33f/jsonpath/pkg/jsonpath/token -# github.com/pb33f/libopenapi v0.36.1 -## explicit; go 1.25.0 +# github.com/pb33f/libopenapi v0.38.3 +## explicit; go 1.25.7 github.com/pb33f/libopenapi github.com/pb33f/libopenapi/datamodel github.com/pb33f/libopenapi/datamodel/high @@ -1005,10 +1005,11 @@ go.yaml.in/yaml/v2 # go.yaml.in/yaml/v3 v3.0.4 ## explicit; go 1.16 go.yaml.in/yaml/v3 -# go.yaml.in/yaml/v4 v4.0.0-rc.4 +# go.yaml.in/yaml/v4 v4.0.0-rc.6 ## explicit; go 1.18 go.yaml.in/yaml/v4 go.yaml.in/yaml/v4/internal/libyaml +go.yaml.in/yaml/v4/plugin/limit # golang.org/x/crypto v0.50.0 ## explicit; go 1.25.0 golang.org/x/crypto/chacha20 @@ -1059,7 +1060,7 @@ golang.org/x/oauth2/google/internal/stsexchange golang.org/x/oauth2/internal golang.org/x/oauth2/jws golang.org/x/oauth2/jwt -# golang.org/x/sync v0.20.0 +# golang.org/x/sync v0.21.0 ## explicit; go 1.25.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore