mirror of
https://github.com/vxcontrol/cloud.git
synced 2026-07-19 19:53:42 -04:00
199 lines
4.3 KiB
Go
199 lines
4.3 KiB
Go
package anonymizer
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
// defined anonymizer decorator name for struct tag
|
|
anonymizerTagName = "anonymizer"
|
|
|
|
// specifies if the field should be skipped from anonymization
|
|
skipField = "skip"
|
|
)
|
|
|
|
// structField represents a single field found in a struct
|
|
type structField struct {
|
|
// field name
|
|
Name string
|
|
// field index path
|
|
Index []int
|
|
// field type
|
|
Type reflect.Type
|
|
// whether to skip anonymization
|
|
Skip bool
|
|
}
|
|
|
|
// structInfo holds information about a struct for anonymization
|
|
type structInfo struct {
|
|
Fields []structField
|
|
Type reflect.Type
|
|
}
|
|
|
|
var (
|
|
// cache for struct information
|
|
structCache = make(map[reflect.Type]*structInfo)
|
|
cacheMutex sync.RWMutex
|
|
)
|
|
|
|
// ErrObjectImmutable is returned when trying to anonymize an immutable object
|
|
var ErrObjectImmutable = fmt.Errorf("object is immutable and cannot be anonymized")
|
|
|
|
// getStructInfo returns struct information from cache or parses it
|
|
func getStructInfo(t reflect.Type) (*structInfo, error) {
|
|
cacheMutex.RLock()
|
|
info, exists := structCache[t]
|
|
cacheMutex.RUnlock()
|
|
|
|
if exists {
|
|
return info, nil
|
|
}
|
|
|
|
// parse struct and cache result
|
|
cacheMutex.Lock()
|
|
defer cacheMutex.Unlock()
|
|
|
|
// double-check after acquiring write lock
|
|
if info, exists := structCache[t]; exists {
|
|
return info, nil
|
|
}
|
|
|
|
info, err := parseStruct(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
structCache[t] = info
|
|
return info, nil
|
|
}
|
|
|
|
// parseStruct parses struct fields and returns StructInfo
|
|
func parseStruct(t reflect.Type) (*structInfo, error) {
|
|
if t.Kind() != reflect.Struct {
|
|
return nil, fmt.Errorf("expected struct type, got %s", t.Kind())
|
|
}
|
|
|
|
fields, err := getStructFields(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &structInfo{
|
|
Fields: fields,
|
|
Type: t,
|
|
}, nil
|
|
}
|
|
|
|
// getStructFields returns a list of fields for the given struct type
|
|
func getStructFields(t reflect.Type) ([]structField, error) {
|
|
// anonymous fields to explore at current and next level
|
|
current := []structField{}
|
|
next := []structField{{Type: t}}
|
|
|
|
// count of queued names for current level and the next
|
|
var count, nextCount map[reflect.Type]int
|
|
|
|
// types already visited at an earlier level
|
|
visited := map[reflect.Type]bool{}
|
|
|
|
// fields found
|
|
var fields []structField
|
|
|
|
for len(next) > 0 {
|
|
current, next = next, current[:0]
|
|
count, nextCount = nextCount, map[reflect.Type]int{}
|
|
|
|
for _, f := range current {
|
|
if visited[f.Type] {
|
|
continue
|
|
}
|
|
visited[f.Type] = true
|
|
|
|
// scan f.Type for fields to include
|
|
for i := 0; i < f.Type.NumField(); i++ {
|
|
sf := f.Type.Field(i)
|
|
if sf.Anonymous {
|
|
t := sf.Type
|
|
if t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if !sf.IsExported() && t.Kind() != reflect.Struct {
|
|
// ignore embedded fields of unexported non-struct types
|
|
continue
|
|
}
|
|
} else if !sf.IsExported() {
|
|
// ignore unexported non-embedded fields
|
|
continue
|
|
}
|
|
|
|
// parse anonymizer tag
|
|
var skip bool
|
|
tagAnonymizer := sf.Tag.Get(anonymizerTagName)
|
|
if tagAnonymizer != "" {
|
|
opts := strings.Split(tagAnonymizer, ",")
|
|
for _, opt := range opts {
|
|
if strings.TrimSpace(opt) == skipField {
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
index := make([]int, len(f.Index)+1)
|
|
copy(index, f.Index)
|
|
index[len(f.Index)] = i
|
|
|
|
ft := sf.Type
|
|
for ft.Name() == "" && ft.Kind() == reflect.Pointer {
|
|
// follow pointer
|
|
ft = ft.Elem()
|
|
}
|
|
|
|
// record found field and index sequence
|
|
if !sf.Anonymous || ft.Kind() != reflect.Struct {
|
|
field := structField{
|
|
Name: sf.Name,
|
|
Index: index,
|
|
Type: ft,
|
|
Skip: skip,
|
|
}
|
|
fields = append(fields, field)
|
|
if count[f.Type] > 1 {
|
|
// if there were multiple instances, add a second
|
|
fields = append(fields, fields[len(fields)-1])
|
|
}
|
|
continue
|
|
}
|
|
|
|
// record new anonymous struct to explore in next round
|
|
nextCount[ft]++
|
|
if nextCount[ft] == 1 {
|
|
next = append(next, structField{Name: ft.Name(), Index: index, Type: ft})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return fields, nil
|
|
}
|
|
|
|
// isMutable checks if the given value can be modified
|
|
func isMutable(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Pointer:
|
|
return !v.IsNil()
|
|
case reflect.Map, reflect.Slice:
|
|
return true
|
|
case reflect.Interface:
|
|
if !v.IsNil() {
|
|
return isMutable(v.Elem())
|
|
}
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|