aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lunixbochs/struc/parse.go
blob: 43c5875f50e4534374d4925001ead59c57e91227 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package struc

import (
	"encoding/binary"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

// struc:"int32,big,sizeof=Data,skip,sizefrom=Len"

type strucTag struct {
	Type     string
	Order    binary.ByteOrder
	Sizeof   string
	Skip     bool
	Sizefrom string
}

func parseStrucTag(tag reflect.StructTag) *strucTag {
	t := &strucTag{
		Order: binary.BigEndian,
	}
	tagStr := tag.Get("struc")
	if tagStr == "" {
		// someone's going to typo this (I already did once)
		// sorry if you made a module actually using this tag
		// and you're mad at me now
		tagStr = tag.Get("struct")
	}
	for _, s := range strings.Split(tagStr, ",") {
		if strings.HasPrefix(s, "sizeof=") {
			tmp := strings.SplitN(s, "=", 2)
			t.Sizeof = tmp[1]
		} else if strings.HasPrefix(s, "sizefrom=") {
			tmp := strings.SplitN(s, "=", 2)
			t.Sizefrom = tmp[1]
		} else if s == "big" {
			t.Order = binary.BigEndian
		} else if s == "little" {
			t.Order = binary.LittleEndian
		} else if s == "skip" {
			t.Skip = true
		} else {
			t.Type = s
		}
	}
	return t
}

var typeLenRe = regexp.MustCompile(`^\[(\d*)\]`)

func parseField(f reflect.StructField) (fd *Field, tag *strucTag, err error) {
	tag = parseStrucTag(f.Tag)
	var ok bool
	fd = &Field{
		Name:  f.Name,
		Len:   1,
		Order: tag.Order,
		Slice: false,
		kind:  f.Type.Kind(),
	}
	switch fd.kind {
	case reflect.Array:
		fd.Slice = true
		fd.Array = true
		fd.Len = f.Type.Len()
		fd.kind = f.Type.Elem().Kind()
	case reflect.Slice:
		fd.Slice = true
		fd.Len = -1
		fd.kind = f.Type.Elem().Kind()
	case reflect.Ptr:
		fd.Ptr = true
		fd.kind = f.Type.Elem().Kind()
	}
	// check for custom types
	tmp := reflect.New(f.Type)
	if _, ok := tmp.Interface().(Custom); ok {
		fd.Type = CustomType
		return
	}
	var defTypeOk bool
	fd.defType, defTypeOk = reflectTypeMap[fd.kind]
	// find a type in the struct tag
	pureType := typeLenRe.ReplaceAllLiteralString(tag.Type, "")
	if fd.Type, ok = typeLookup[pureType]; ok {
		fd.Len = 1
		match := typeLenRe.FindAllStringSubmatch(tag.Type, -1)
		if len(match) > 0 && len(match[0]) > 1 {
			fd.Slice = true
			first := match[0][1]
			// Field.Len = -1 indicates a []slice
			if first == "" {
				fd.Len = -1
			} else {
				fd.Len, err = strconv.Atoi(first)
			}
		}
		return
	}
	// the user didn't specify a type
	switch f.Type {
	case reflect.TypeOf(Size_t(0)):
		fd.Type = SizeType
	case reflect.TypeOf(Off_t(0)):
		fd.Type = OffType
	default:
		if defTypeOk {
			fd.Type = fd.defType
		} else {
			err = errors.New("struc: Could not find field type.")
		}
	}
	return
}

func parseFieldsLocked(v reflect.Value) (Fields, error) {
	// we need to repeat this logic because parseFields() below can't be recursively called due to locking
	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	t := v.Type()
	if v.NumField() < 1 {
		return nil, errors.New("struc: Struct has no fields.")
	}
	sizeofMap := make(map[string][]int)
	fields := make(Fields, v.NumField())
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		f, tag, err := parseField(field)
		if tag.Skip {
			continue
		}
		if err != nil {
			return nil, err
		}
		if !v.Field(i).CanSet() {
			continue
		}
		f.Index = i
		if tag.Sizeof != "" {
			target, ok := t.FieldByName(tag.Sizeof)
			if !ok {
				return nil, fmt.Errorf("struc: `sizeof=%s` field does not exist", tag.Sizeof)
			}
			f.Sizeof = target.Index
			sizeofMap[tag.Sizeof] = field.Index
		}
		if sizefrom, ok := sizeofMap[field.Name]; ok {
			f.Sizefrom = sizefrom
		}
		if tag.Sizefrom != "" {
			source, ok := t.FieldByName(tag.Sizefrom)
			if !ok {
				return nil, fmt.Errorf("struc: `sizefrom=%s` field does not exist", tag.Sizefrom)
			}
			f.Sizefrom = source.Index
		}
		if f.Len == -1 && f.Sizefrom == nil {
			return nil, fmt.Errorf("struc: field `%s` is a slice with no length or sizeof field", field.Name)
		}
		// recurse into nested structs
		// TODO: handle loops (probably by indirecting the []Field and putting pointer in cache)
		if f.Type == Struct {
			typ := field.Type
			if f.Ptr {
				typ = typ.Elem()
			}
			if f.Slice {
				typ = typ.Elem()
			}
			f.Fields, err = parseFieldsLocked(reflect.New(typ))
			if err != nil {
				return nil, err
			}
		}
		fields[i] = f
	}
	return fields, nil
}

var fieldCache = make(map[reflect.Type]Fields)
var fieldCacheLock sync.RWMutex
var parseLock sync.Mutex

func fieldCacheLookup(t reflect.Type) Fields {
	fieldCacheLock.RLock()
	defer fieldCacheLock.RUnlock()
	if cached, ok := fieldCache[t]; ok {
		return cached
	}
	return nil
}

func parseFields(v reflect.Value) (Fields, error) {
	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	t := v.Type()

	// fast path: hopefully the field parsing is already cached
	if cached := fieldCacheLookup(t); cached != nil {
		return cached, nil
	}

	// hold a global lock so multiple goroutines can't parse (the same) fields at once
	parseLock.Lock()
	defer parseLock.Unlock()

	// check cache a second time, in case parseLock was just released by
	// another thread who filled the cache for us
	if cached := fieldCacheLookup(t); cached != nil {
		return cached, nil
	}

	// no luck, time to parse and fill the cache ourselves
	fields, err := parseFieldsLocked(v)
	if err != nil {
		return nil, err
	}
	fieldCacheLock.Lock()
	fieldCache[t] = fields
	fieldCacheLock.Unlock()
	return fields, nil
}