Files
go-omni/copyx/graph.go

271 lines
6.5 KiB
Go

package copyx
import (
"fmt"
"reflect"
"sort"
"sync"
"time"
)
const maxDepth = 256
const maxNodes = 200000
const maxCopyBytes = 64 << 20
var timeType = reflect.TypeFor[time.Time]()
// Immutable type metadata is shared, never values or converter registrations.
var valueTypes sync.Map
func simpleValue(t reflect.Type) bool {
if known, ok := valueTypes.Load(t); ok {
return known.(bool)
}
result := t == timeType
if !result {
switch t.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
result = true
case reflect.Array:
result = simpleValue(t.Elem())
case reflect.Struct:
result = !forbidden(t)
for i := 0; result && i < t.NumField(); i++ {
result = simpleValue(t.Field(i).Type)
}
}
}
valueTypes.Store(t, result)
return result
}
type reference struct {
kind reflect.Kind
typ reflect.Type
ptr uintptr
length, capacity int
}
func identity(v reflect.Value) reference {
r := reference{kind: v.Kind(), typ: v.Type(), ptr: uintptr(v.UnsafePointer())}
if v.Kind() == reflect.Slice {
r.length = v.Len()
r.capacity = v.Cap()
}
return r
}
type span struct{ start, end uintptr }
type graph struct {
maps map[uintptr]bool
spans []span
maxEnd []uintptr
}
// References are inspected without reading private values through Interface or
// using unsafe writes. Slice capacity matters: returning source[:0] is an alias.
func scanGraph(v reflect.Value, visit func(reflect.Value, string) error) error {
seen := map[reference]bool{}
nodes := 0
var walk func(reflect.Value, string, int) error
walk = func(v reflect.Value, path string, depth int) error {
nodes++
if depth > maxDepth || nodes > maxNodes {
return at(path, ErrLimit)
}
if !v.IsValid() {
return nil
}
if err := visit(v, path); err != nil {
return err
}
if simpleValue(v.Type()) {
return nil
} // time.Time is a documented immutable value.
switch v.Kind() {
case reflect.Interface:
if !v.IsNil() {
return walk(v.Elem(), path, depth+1)
}
case reflect.Pointer, reflect.Map, reflect.Slice:
if v.IsNil() {
return nil
}
id := identity(v)
if seen[id] {
return nil
}
seen[id] = true
switch v.Kind() {
case reflect.Pointer:
return walk(v.Elem(), path, depth+1)
case reflect.Map:
it := v.MapRange()
for it.Next() {
if err := walk(it.Key(), path+"[key]", depth+1); err != nil {
return err
}
if err := walk(it.Value(), path+"[value]", depth+1); err != nil {
return err
}
}
case reflect.Slice:
if simpleValue(v.Type().Elem()) {
return nil
}
all := v.Slice(0, v.Cap())
for i := 0; i < all.Len(); i++ {
if err := walk(all.Index(i), fmt.Sprintf("%s[%d]", path, i), depth+1); err != nil {
return err
}
}
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if err := walk(v.Field(i), path+"."+v.Type().Field(i).Name, depth+1); err != nil {
return err
}
}
case reflect.Array:
for i := 0; i < v.Len(); i++ {
if err := walk(v.Index(i), fmt.Sprintf("%s[%d]", path, i), depth+1); err != nil {
return err
}
}
}
return nil
}
return walk(v, "$", 0)
}
func mutableSpan(v reflect.Value) (span, bool) {
var n uintptr
switch v.Kind() {
case reflect.Pointer:
if v.IsNil() {
return span{}, false
}
n = v.Type().Elem().Size()
case reflect.Slice:
if v.IsNil() {
return span{}, false
}
n = uintptr(v.Cap()) * v.Type().Elem().Size()
default:
return span{}, false
}
if n == 0 {
return span{}, false
}
start := uintptr(v.UnsafePointer())
return span{start, start + n}, true
}
func sourceGraph(v reflect.Value) (*graph, error) {
g := &graph{maps: map[uintptr]bool{}}
err := scanGraph(v, func(v reflect.Value, _ string) error {
if r, ok := mutableSpan(v); ok {
g.spans = append(g.spans, r)
}
if v.Kind() == reflect.Map && !v.IsNil() {
g.maps[uintptr(v.UnsafePointer())] = true
}
return nil
})
sort.Slice(g.spans, func(i, j int) bool { return g.spans[i].start < g.spans[j].start })
var end uintptr
for _, r := range g.spans {
if r.end > end {
end = r.end
}
g.maxEnd = append(g.maxEnd, end)
}
return g, err
}
func (g *graph) audit(v reflect.Value) error {
return scanGraph(v, func(v reflect.Value, path string) error {
if forbidden(v.Type()) {
return at(path, ErrUnsupported)
}
switch v.Kind() {
case reflect.Func, reflect.Chan:
if !v.IsNil() {
return at(path, ErrUnsupported)
}
case reflect.UnsafePointer:
if v.Pointer() != 0 {
return at(path, ErrUnsupported)
}
}
if v.Kind() == reflect.Map && !v.IsNil() && g.maps[uintptr(v.UnsafePointer())] {
return at(path, ErrSharedReference)
}
if r, ok := mutableSpan(v); ok {
i := sort.Search(len(g.spans), func(i int) bool { return g.spans[i].start >= r.end })
if i > 0 && g.maxEnd[i-1] > r.start {
return at(path, ErrSharedReference)
}
}
return nil
})
}
func forbidden(t reflect.Type) bool {
// Lock/resource implementations are not data. A custom converter may turn
// them into a different data type but may not manufacture cloned handles.
switch t.PkgPath() {
case "sync", "sync/atomic", "os", "net", "database/sql":
return t.Kind() == reflect.Struct
}
return false
}
// Shallow copies may share handles through pointers, but must not copy lock or
// resource state by value, including state inside a containing struct/array.
var safeValueTypes sync.Map
func shallowSafe(t reflect.Type) bool {
if known, ok := safeValueTypes.Load(t); ok {
return known.(bool)
}
result := true
if forbidden(t) {
safeValueTypes.Store(t, false)
return false
}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
if !shallowSafe(t.Field(i).Type) {
result = false
break
}
}
case reflect.Array:
result = shallowSafe(t.Elem())
}
safeValueTypes.Store(t, result)
return result
}
func privateMutable(t reflect.Type) bool {
if t == timeType {
return false
}
switch t.Kind() {
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan, reflect.UnsafePointer:
return true
case reflect.Array:
return privateMutable(t.Elem())
case reflect.Struct:
if forbidden(t) {
return true
}
for i := 0; i < t.NumField(); i++ {
if privateMutable(t.Field(i).Type) {
return true
}
}
}
return false
}