251 lines
6.9 KiB
Go
251 lines
6.9 KiB
Go
// 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
|
|
}
|