Files
dualfroz f87ec7522c Make SetHeaderNormalizer safe to call while decoding
SetHeaderNormalizer reassigned the normalizer and copied a fresh sync.Map
over the struct info cache while other goroutines were using it. Keep both
in one value swapped atomically instead. Fixes #237.
2026-10-02 16:20:31 +02:00

288 lines
8.8 KiB
Go

package gocsv
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
)
// --------------------------------------------------------------------------
// Reflection helpers
type structInfo struct {
Fields []fieldInfo
Err error
}
// fieldInfo is a struct field that should be mapped to a CSV column, or vice-versa
// Each IndexChain element before the last is the index of an the embedded struct field
// that defines Key as a tag
type fieldInfo struct {
keys []string
omitEmpty bool
IndexChain []int
defaultValue string
partial bool
inline bool
}
func (f fieldInfo) getFirstKey() string {
return f.keys[0]
}
func (f fieldInfo) matchesKey(key string) bool {
for _, k := range f.keys {
if key == k || strings.TrimSpace(key) == k || (f.partial && strings.Contains(key, k)) || removeZeroWidthChars(key) == k {
return true
}
}
return false
}
// zwchs is Zero Width Characters map
var zwchs = map[rune]struct{}{
'\u200B': {}, // zero width space (U+200B)
'\uFEFF': {}, // zero width no-break space (U+FEFF)
'\u200D': {}, // zero width joiner (U+200D)
'\u200C': {}, // zero width non-joiner (U+200C)
}
func removeZeroWidthChars(s string) string {
return strings.Map(func(r rune) rune {
if _, ok := zwchs[r]; ok {
return -1
}
return r
}, s)
}
var structMap = make(map[reflect.Type]*structInfo)
var structMapMutex sync.RWMutex
func getStructInfo(rType reflect.Type) *structInfo {
structInfos := &loadHeaderNormalization().structInfos
stInfo, ok := structInfos.Load(rType)
if ok {
return stInfo.(*structInfo)
}
fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool))
stInfo = &structInfo{Fields: fieldsList, Err: err}
structInfos.Store(rType, stInfo)
return stInfo.(*structInfo)
}
func getFieldInfos(rType reflect.Type, parentIndexChain []int, parentKeys []string, ancestors map[reflect.Type]bool) ([]fieldInfo, error) {
if ancestors[rType] {
return nil, fmt.Errorf("cyclic struct type %s: exclude the recursive field with csv:\"-\"", rType)
}
ancestors[rType] = true
defer delete(ancestors, rType)
fieldsCount := rType.NumField()
fieldsList := make([]fieldInfo, 0, fieldsCount)
for i := 0; i < fieldsCount; i++ {
field := rType.Field(i)
if field.PkgPath != "" {
continue
}
var cpy = make([]int, len(parentIndexChain))
copy(cpy, parentIndexChain)
indexChain := append(cpy, i)
// Determine whether it's a struct we can expand into nested columns.
// Structs that implement any of the text or CSV marshaling methods
// should result in one value and not have their fields exposed
fieldType := field.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
isExpandableStruct := fieldType.Kind() == reflect.Struct && !canMarshal(fieldType)
var currFieldInfo *fieldInfo
if !field.Anonymous {
filteredTags := []string{}
currFieldInfo, filteredTags = filterTags(TagName, indexChain, field)
if len(filteredTags) == 1 && filteredTags[0] == "-" {
// ignore nested structs with - tag
continue
} else if isExpandableStruct && len(filteredTags) == 1 && filteredTags[0] == "." {
// inline nested structs with . tag
currFieldInfo.inline = true
currFieldInfo.keys = parentKeys
} else if len(filteredTags) > 0 && filteredTags[0] != "" {
currFieldInfo.keys = filteredTags
} else {
currFieldInfo.keys = []string{normalizeName(field.Name)}
}
if len(parentKeys) > 0 && currFieldInfo != nil && !currFieldInfo.inline {
// create cartesian product of keys
// eg: parent keys x field keys
keys := make([]string, 0, len(parentKeys)*len(currFieldInfo.keys))
for _, pkey := range parentKeys {
for _, ckey := range currFieldInfo.keys {
keys = append(keys, normalizeName(fmt.Sprintf("%s%s%s", pkey, FieldsCombiner, ckey)))
}
currFieldInfo.keys = keys
}
}
}
// handle struct
if isExpandableStruct {
// if the field is an embedded struct, pass along parent keys
keys := parentKeys
if currFieldInfo != nil {
keys = currFieldInfo.keys
}
childFields, err := getFieldInfos(fieldType, indexChain, keys, ancestors)
if err != nil {
return nil, err
}
fieldsList = append(fieldsList, childFields...)
continue
}
// if the field is an embedded struct, ignore the csv tag
if currFieldInfo == nil {
continue
}
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
var arrayLength = -1
// if the field is a slice or an array, see if it has a `csv[n]` tag
if arrayTag, ok := field.Tag.Lookup(TagName + "[]"); ok {
arrayLength, _ = strconv.Atoi(arrayTag)
}
// slices or arrays of Struct get special handling
if field.Type.Elem().Kind() == reflect.Struct {
fieldInfos, err := getFieldInfos(field.Type.Elem(), []int{}, []string{}, ancestors)
if err != nil {
return nil, err
}
// if no special csv[] tag was supplied, just include the field directly
if arrayLength == -1 {
fieldsList = append(fieldsList, *currFieldInfo)
} else {
// When the field is a slice/array of structs, create a fieldInfo for each index and each field
for idx := 0; idx < arrayLength; idx++ {
// copy index chain and append array index
var cpy2 = make([]int, len(indexChain))
copy(cpy2, indexChain)
arrayIndexChain := append(cpy2, idx)
for _, childFieldInfo := range fieldInfos {
// copy array index chain and append array index
var cpy3 = make([]int, len(arrayIndexChain))
copy(cpy3, arrayIndexChain)
arrayFieldInfo := fieldInfo{
IndexChain: append(cpy3, childFieldInfo.IndexChain...),
omitEmpty: childFieldInfo.omitEmpty,
defaultValue: childFieldInfo.defaultValue,
partial: childFieldInfo.partial,
}
// create cartesian product of keys
// eg: array field keys x struct field keys
for _, akey := range currFieldInfo.keys {
for _, fkey := range childFieldInfo.keys {
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d].%s", akey, idx, fkey)))
}
}
fieldsList = append(fieldsList, arrayFieldInfo)
}
}
}
} else if arrayLength > 0 {
// When the field is a slice/array of primitives, create a fieldInfo for each index
for idx := 0; idx < arrayLength; idx++ {
// copy index chain and append array index
var cpy2 = make([]int, len(indexChain))
copy(cpy2, indexChain)
arrayFieldInfo := fieldInfo{
IndexChain: append(cpy2, idx),
omitEmpty: currFieldInfo.omitEmpty,
defaultValue: currFieldInfo.defaultValue,
partial: currFieldInfo.partial,
}
for _, akey := range currFieldInfo.keys {
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d]", akey, idx)))
}
fieldsList = append(fieldsList, arrayFieldInfo)
}
} else {
fieldsList = append(fieldsList, *currFieldInfo)
}
} else {
fieldsList = append(fieldsList, *currFieldInfo)
}
}
return fieldsList, nil
}
func filterTags(tagName string, indexChain []int, field reflect.StructField) (*fieldInfo, []string) {
currFieldInfo := fieldInfo{IndexChain: indexChain}
fieldTag := field.Tag.Get(tagName)
fieldTags := strings.Split(fieldTag, TagSeparator)
filteredTags := []string{}
for _, fieldTagEntry := range fieldTags {
trimmedFieldTagEntry := strings.TrimSpace(fieldTagEntry) // handles cases like `csv:"foo, omitempty, default=test"`
if trimmedFieldTagEntry == "omitempty" {
currFieldInfo.omitEmpty = true
} else if trimmedFieldTagEntry == "partial" {
// Must be an exact match: using HasPrefix here would mistake a column name that
// happens to start with "partial" (e.g. `csv:"partial_delivery_number"`) for the
// "partial" option, dropping the real column name. See issue #274.
currFieldInfo.partial = true
} else if strings.HasPrefix(trimmedFieldTagEntry, "default=") {
currFieldInfo.defaultValue = strings.TrimPrefix(trimmedFieldTagEntry, "default=")
} else {
filteredTags = append(filteredTags, normalizeName(trimmedFieldTagEntry))
}
}
return &currFieldInfo, filteredTags
}
func getConcreteContainerInnerType(in reflect.Type) (inInnerWasPointer bool, inInnerType reflect.Type) {
inInnerType = in.Elem()
inInnerWasPointer = false
if inInnerType.Kind() == reflect.Ptr {
inInnerWasPointer = true
inInnerType = inInnerType.Elem()
}
return inInnerWasPointer, inInnerType
}
func getConcreteReflectValueAndType(in interface{}) (reflect.Value, reflect.Type) {
value := reflect.ValueOf(in)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
return value, value.Type()
}
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
func isErrorType(outType reflect.Type) bool {
if outType.Kind() != reflect.Interface {
return false
}
return outType.Implements(errorInterface)
}