├── .travis.yml ├── LICENSE ├── README.md ├── bench_test.go ├── binary.go ├── custom.go ├── custom_float16.go ├── custom_float16_test.go ├── custom_test.go ├── field.go ├── field_test.go ├── fields.go ├── fields_test.go ├── go.mod ├── legacy.go ├── packable_test.go ├── packer.go ├── parse.go ├── parse_test.go ├── struc.go ├── struc_test.go ├── test_pack_init ├── doc.go └── pack_init_test.go ├── types.go └── types_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | script: go test -v 5 | 6 | go: 7 | - 1.3 8 | - 1.12 9 | - 1.13 10 | - tip 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ryan Hileman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/lunixbochs/struc.svg?branch=master)](https://travis-ci.org/lunixbochs/struc) [![GoDoc](https://godoc.org/github.com/lunixbochs/struc?status.svg)](https://godoc.org/github.com/lunixbochs/struc) 2 | 3 | struc 4 | ==== 5 | 6 | Struc exists to pack and unpack C-style structures from bytes, which is useful for binary files and network protocols. It could be considered an alternative to `encoding/binary`, which requires massive boilerplate for some similar operations. 7 | 8 | Take a look at an [example comparing `struc` and `encoding/binary`](https://bochs.info/p/cxvm9) 9 | 10 | Struc considers usability first. That said, it does cache reflection data and aims to be competitive with `encoding/binary` struct packing in every way, including performance. 11 | 12 | Example struct 13 | ---- 14 | 15 | ```Go 16 | type Example struct { 17 | Var int `struc:"int32,sizeof=Str"` 18 | Str string 19 | Weird []byte `struc:"[8]int64"` 20 | Var []int `struc:"[]int32,little"` 21 | } 22 | ``` 23 | 24 | Struct tag format 25 | ---- 26 | 27 | - ```Var []int `struc:"[]int32,little,sizeof=StringField"` ``` will pack Var as a slice of little-endian int32, and link it as the size of `StringField`. 28 | - `sizeof=`: Indicates this field is a number used to track the length of a another field. `sizeof` fields are automatically updated on `Pack()` based on the current length of the tracked field, and are used to size the target field during `Unpack()`. 29 | - Bare values will be parsed as type and endianness. 30 | 31 | Endian formats 32 | ---- 33 | 34 | - `big` (default) 35 | - `little` 36 | 37 | Recognized types 38 | ---- 39 | 40 | - `pad` - this type ignores field contents and is backed by a `[length]byte` containing nulls 41 | - `bool` 42 | - `byte` 43 | - `int8`, `uint8` 44 | - `int16`, `uint16` 45 | - `int32`, `uint32` 46 | - `int64`, `uint64` 47 | - `float32` 48 | - `float64` 49 | 50 | Types can be indicated as arrays/slices using `[]` syntax. Example: `[]int64`, `[8]int32`. 51 | 52 | Bare slice types (those with no `[size]`) must have a linked `Sizeof` field. 53 | 54 | Private fields are ignored when packing and unpacking. 55 | 56 | Example code 57 | ---- 58 | 59 | ```Go 60 | package main 61 | 62 | import ( 63 | "bytes" 64 | "github.com/lunixbochs/struc" 65 | ) 66 | 67 | type Example struct { 68 | A int `struc:"big"` 69 | 70 | // B will be encoded/decoded as a 16-bit int (a "short") 71 | // but is stored as a native int in the struct 72 | B int `struc:"int16"` 73 | 74 | // the sizeof key links a buffer's size to any int field 75 | Size int `struc:"int8,little,sizeof=Str"` 76 | Str string 77 | 78 | // you can get freaky if you want 79 | Str2 string `struc:"[5]int64"` 80 | } 81 | 82 | func main() { 83 | var buf bytes.Buffer 84 | t := &Example{1, 2, 0, "test", "test2"} 85 | err := struc.Pack(&buf, t) 86 | o := &Example{} 87 | err = struc.Unpack(&buf, o) 88 | } 89 | ``` 90 | 91 | Benchmark 92 | ---- 93 | 94 | `BenchmarkEncode` uses struc. `Stdlib` benchmarks use equivalent `encoding/binary` code. `Manual` encodes without any reflection, and should be considered an upper bound on performance (which generated code based on struc definitions should be able to achieve). 95 | 96 | ``` 97 | BenchmarkEncode 1000000 1265 ns/op 98 | BenchmarkStdlibEncode 1000000 1855 ns/op 99 | BenchmarkManualEncode 5000000 284 ns/op 100 | BenchmarkDecode 1000000 1259 ns/op 101 | BenchmarkStdlibDecode 1000000 1656 ns/op 102 | BenchmarkManualDecode 20000000 89.0 ns/op 103 | ``` 104 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | ) 8 | 9 | type BenchExample struct { 10 | Test [5]byte 11 | A int32 12 | B, C, D int16 13 | Test2 [4]byte 14 | Length int32 15 | } 16 | 17 | func BenchmarkArrayEncode(b *testing.B) { 18 | for i := 0; i < b.N; i++ { 19 | var buf bytes.Buffer 20 | if err := Pack(&buf, arrayReference); err != nil { 21 | b.Fatal(err) 22 | } 23 | } 24 | } 25 | 26 | func BenchmarkSliceEncode(b *testing.B) { 27 | for i := 0; i < b.N; i++ { 28 | var buf bytes.Buffer 29 | if err := Pack(&buf, sliceReference); err != nil { 30 | b.Fatal(err) 31 | } 32 | } 33 | } 34 | 35 | func BenchmarkArrayDecode(b *testing.B) { 36 | var out ExampleArray 37 | for i := 0; i < b.N; i++ { 38 | buf := bytes.NewBuffer(arraySliceReferenceBytes) 39 | if err := Unpack(buf, &out); err != nil { 40 | b.Fatal(err) 41 | } 42 | } 43 | } 44 | 45 | func BenchmarkSliceDecode(b *testing.B) { 46 | var out ExampleSlice 47 | for i := 0; i < b.N; i++ { 48 | buf := bytes.NewBuffer(arraySliceReferenceBytes) 49 | if err := Unpack(buf, &out); err != nil { 50 | b.Fatal(err) 51 | } 52 | } 53 | } 54 | 55 | type BenchStrucExample struct { 56 | Test [5]byte `struc:"[5]byte"` 57 | A int `struc:"int32"` 58 | B, C, D int `struc:"int16"` 59 | Test2 [4]byte `struc:"[4]byte"` 60 | Length int `struc:"int32,sizeof=Data"` 61 | Data []byte 62 | } 63 | 64 | var benchRef = &BenchExample{ 65 | [5]byte{1, 2, 3, 4, 5}, 66 | 1, 2, 3, 4, 67 | [4]byte{1, 2, 3, 4}, 68 | 8, 69 | } 70 | 71 | var eightBytes = []byte("8bytestr") 72 | 73 | var benchStrucRef = &BenchStrucExample{ 74 | [5]byte{1, 2, 3, 4, 5}, 75 | 1, 2, 3, 4, 76 | [4]byte{1, 2, 3, 4}, 77 | 8, eightBytes, 78 | } 79 | 80 | func BenchmarkEncode(b *testing.B) { 81 | for i := 0; i < b.N; i++ { 82 | var buf bytes.Buffer 83 | err := Pack(&buf, benchStrucRef) 84 | if err != nil { 85 | b.Fatal(err) 86 | } 87 | } 88 | } 89 | 90 | func BenchmarkStdlibEncode(b *testing.B) { 91 | for i := 0; i < b.N; i++ { 92 | var buf bytes.Buffer 93 | err := binary.Write(&buf, binary.BigEndian, benchRef) 94 | if err != nil { 95 | b.Fatal(err) 96 | } 97 | _, err = buf.Write(eightBytes) 98 | if err != nil { 99 | b.Fatal(err) 100 | } 101 | } 102 | } 103 | 104 | func BenchmarkManualEncode(b *testing.B) { 105 | order := binary.BigEndian 106 | s := benchStrucRef 107 | for i := 0; i < b.N; i++ { 108 | var buf bytes.Buffer 109 | tmp := make([]byte, 29) 110 | copy(tmp[0:5], s.Test[:]) 111 | order.PutUint32(tmp[5:9], uint32(s.A)) 112 | order.PutUint16(tmp[9:11], uint16(s.B)) 113 | order.PutUint16(tmp[11:13], uint16(s.C)) 114 | order.PutUint16(tmp[13:15], uint16(s.D)) 115 | copy(tmp[15:19], s.Test2[:]) 116 | order.PutUint32(tmp[19:23], uint32(s.Length)) 117 | copy(tmp[23:], s.Data) 118 | _, err := buf.Write(tmp) 119 | if err != nil { 120 | b.Fatal(err) 121 | } 122 | } 123 | } 124 | 125 | func BenchmarkDecode(b *testing.B) { 126 | var out BenchStrucExample 127 | var buf bytes.Buffer 128 | if err := Pack(&buf, benchStrucRef); err != nil { 129 | b.Fatal(err) 130 | } 131 | bufBytes := buf.Bytes() 132 | for i := 0; i < b.N; i++ { 133 | buf := bytes.NewReader(bufBytes) 134 | err := Unpack(buf, &out) 135 | if err != nil { 136 | b.Fatal(err) 137 | } 138 | out.Data = nil 139 | } 140 | } 141 | 142 | func BenchmarkStdlibDecode(b *testing.B) { 143 | var out BenchExample 144 | var buf bytes.Buffer 145 | binary.Write(&buf, binary.BigEndian, *benchRef) 146 | _, err := buf.Write(eightBytes) 147 | if err != nil { 148 | b.Fatal(err) 149 | } 150 | bufBytes := buf.Bytes() 151 | for i := 0; i < b.N; i++ { 152 | buf := bytes.NewReader(bufBytes) 153 | err := binary.Read(buf, binary.BigEndian, &out) 154 | if err != nil { 155 | b.Fatal(err) 156 | } 157 | tmp := make([]byte, out.Length) 158 | _, err = buf.Read(tmp) 159 | if err != nil { 160 | b.Fatal(err) 161 | } 162 | } 163 | } 164 | 165 | func BenchmarkManualDecode(b *testing.B) { 166 | var o BenchStrucExample 167 | var buf bytes.Buffer 168 | if err := Pack(&buf, benchStrucRef); err != nil { 169 | b.Fatal(err) 170 | } 171 | tmp := buf.Bytes() 172 | order := binary.BigEndian 173 | for i := 0; i < b.N; i++ { 174 | copy(o.Test[:], tmp[0:5]) 175 | o.A = int(order.Uint32(tmp[5:9])) 176 | o.B = int(order.Uint16(tmp[9:11])) 177 | o.C = int(order.Uint16(tmp[11:13])) 178 | o.D = int(order.Uint16(tmp[13:15])) 179 | copy(o.Test2[:], tmp[15:19]) 180 | o.Length = int(order.Uint32(tmp[19:23])) 181 | o.Data = make([]byte, o.Length) 182 | copy(o.Data, tmp[23:]) 183 | } 184 | } 185 | 186 | func BenchmarkFullEncode(b *testing.B) { 187 | for i := 0; i < b.N; i++ { 188 | var buf bytes.Buffer 189 | if err := Pack(&buf, reference); err != nil { 190 | b.Fatal(err) 191 | } 192 | } 193 | } 194 | 195 | func BenchmarkFullDecode(b *testing.B) { 196 | var out Example 197 | for i := 0; i < b.N; i++ { 198 | buf := bytes.NewBuffer(referenceBytes) 199 | if err := Unpack(buf, &out); err != nil { 200 | b.Fatal(err) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /binary.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "reflect" 7 | ) 8 | 9 | type byteWriter struct { 10 | buf []byte 11 | pos int 12 | } 13 | 14 | func (b byteWriter) Write(p []byte) (int, error) { 15 | capacity := len(b.buf) - b.pos 16 | if capacity < len(p) { 17 | p = p[:capacity] 18 | } 19 | if len(p) > 0 { 20 | copy(b.buf[b.pos:], p) 21 | b.pos += len(p) 22 | } 23 | return len(p), nil 24 | } 25 | 26 | type binaryFallback reflect.Value 27 | 28 | func (b binaryFallback) String() string { 29 | return b.String() 30 | } 31 | 32 | func (b binaryFallback) Sizeof(val reflect.Value, options *Options) int { 33 | return binary.Size(val.Interface()) 34 | } 35 | 36 | func (b binaryFallback) Pack(buf []byte, val reflect.Value, options *Options) (int, error) { 37 | tmp := byteWriter{buf: buf} 38 | var order binary.ByteOrder = binary.BigEndian 39 | if options.Order != nil { 40 | order = options.Order 41 | } 42 | err := binary.Write(tmp, order, val.Interface()) 43 | return tmp.pos, err 44 | } 45 | 46 | func (b binaryFallback) Unpack(r io.Reader, val reflect.Value, options *Options) error { 47 | var order binary.ByteOrder = binary.BigEndian 48 | if options.Order != nil { 49 | order = options.Order 50 | } 51 | return binary.Read(r, order, val.Interface()) 52 | } 53 | -------------------------------------------------------------------------------- /custom.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | ) 7 | 8 | type Custom interface { 9 | Pack(p []byte, opt *Options) (int, error) 10 | Unpack(r io.Reader, length int, opt *Options) error 11 | Size(opt *Options) int 12 | String() string 13 | } 14 | 15 | type customFallback struct { 16 | custom Custom 17 | } 18 | 19 | func (c customFallback) Pack(p []byte, val reflect.Value, opt *Options) (int, error) { 20 | return c.custom.Pack(p, opt) 21 | } 22 | 23 | func (c customFallback) Unpack(r io.Reader, val reflect.Value, opt *Options) error { 24 | return c.custom.Unpack(r, 1, opt) 25 | } 26 | 27 | func (c customFallback) Sizeof(val reflect.Value, opt *Options) int { 28 | return c.custom.Size(opt) 29 | } 30 | 31 | func (c customFallback) String() string { 32 | return c.custom.String() 33 | } 34 | -------------------------------------------------------------------------------- /custom_float16.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "math" 7 | "strconv" 8 | ) 9 | 10 | type Float16 float64 11 | 12 | func (f *Float16) Pack(p []byte, opt *Options) (int, error) { 13 | order := opt.Order 14 | if order == nil { 15 | order = binary.BigEndian 16 | } 17 | sign := uint16(0) 18 | if *f < 0 { 19 | sign = 1 20 | } 21 | var frac, exp uint16 22 | if math.IsInf(float64(*f), 0) { 23 | exp = 0x1f 24 | frac = 0 25 | } else if math.IsNaN(float64(*f)) { 26 | exp = 0x1f 27 | frac = 1 28 | } else { 29 | bits := math.Float64bits(float64(*f)) 30 | exp64 := (bits >> 52) & 0x7ff 31 | if exp64 != 0 { 32 | exp = uint16((exp64 - 1023 + 15) & 0x1f) 33 | } 34 | frac = uint16((bits >> 42) & 0x3ff) 35 | } 36 | var out uint16 37 | out |= sign << 15 38 | out |= exp << 10 39 | out |= frac & 0x3ff 40 | order.PutUint16(p, out) 41 | return 2, nil 42 | } 43 | func (f *Float16) Unpack(r io.Reader, length int, opt *Options) error { 44 | order := opt.Order 45 | if order == nil { 46 | order = binary.BigEndian 47 | } 48 | var tmp [2]byte 49 | if _, err := r.Read(tmp[:]); err != nil { 50 | return err 51 | } 52 | val := order.Uint16(tmp[:2]) 53 | sign := (val >> 15) & 1 54 | exp := int16((val >> 10) & 0x1f) 55 | frac := val & 0x3ff 56 | if exp == 0x1f { 57 | if frac != 0 { 58 | *f = Float16(math.NaN()) 59 | } else { 60 | *f = Float16(math.Inf(int(sign)*-2 + 1)) 61 | } 62 | } else { 63 | var bits uint64 64 | bits |= uint64(sign) << 63 65 | bits |= uint64(frac) << 42 66 | if exp > 0 { 67 | bits |= uint64(exp-15+1023) << 52 68 | } 69 | *f = Float16(math.Float64frombits(bits)) 70 | } 71 | return nil 72 | } 73 | func (f *Float16) Size(opt *Options) int { 74 | return 2 75 | } 76 | func (f *Float16) String() string { 77 | return strconv.FormatFloat(float64(*f), 'g', -1, 32) 78 | } 79 | -------------------------------------------------------------------------------- /custom_float16_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "math" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestFloat16(t *testing.T) { 14 | // test cases from https://en.wikipedia.org/wiki/Half-precision_floating-point_format#Half_precision_examples 15 | tests := []struct { 16 | B string 17 | F float64 18 | }{ 19 | //s expnt significand 20 | {"0 01111 0000000000", 1}, 21 | {"0 01111 0000000001", 1.0009765625}, 22 | {"1 10000 0000000000", -2}, 23 | {"0 11110 1111111111", 65504}, 24 | // {"0 00001 0000000000", 0.0000610352}, 25 | // {"0 00000 1111111111", 0.0000609756}, 26 | // {"0 00000 0000000001", 0.0000000596046}, 27 | {"0 00000 0000000000", 0}, 28 | // {"1 00000 0000000000", -0}, 29 | {"0 11111 0000000000", math.Inf(1)}, 30 | {"1 11111 0000000000", math.Inf(-1)}, 31 | {"0 01101 0101010101", 0.333251953125}, 32 | } 33 | for _, test := range tests { 34 | var buf bytes.Buffer 35 | f := Float16(test.F) 36 | if err := Pack(&buf, &f); err != nil { 37 | t.Error("pack failed:", err) 38 | continue 39 | } 40 | bitval, _ := strconv.ParseUint(strings.Replace(test.B, " ", "", -1), 2, 16) 41 | tmp := binary.BigEndian.Uint16(buf.Bytes()) 42 | if tmp != uint16(bitval) { 43 | t.Errorf("incorrect pack: %s != %016b (%f)", test.B, tmp, test.F) 44 | continue 45 | } 46 | var f2 Float16 47 | if err := Unpack(&buf, &f2); err != nil { 48 | t.Error("unpack failed:", err) 49 | continue 50 | } 51 | // let sprintf deal with (im)precision for me here 52 | if fmt.Sprintf("%f", f) != fmt.Sprintf("%f", f2) { 53 | t.Errorf("incorrect unpack: %016b %f != %f", bitval, f, f2) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /custom_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | "testing" 10 | ) 11 | 12 | // Custom Type 13 | type Int3 uint32 14 | 15 | // newInt3 returns a pointer to an Int3 16 | func newInt3(in int) *Int3 { 17 | i := Int3(in) 18 | return &i 19 | } 20 | 21 | type Int3Struct struct { 22 | I Int3 23 | } 24 | 25 | func (i *Int3) Pack(p []byte, opt *Options) (int, error) { 26 | var tmp [4]byte 27 | binary.BigEndian.PutUint32(tmp[:], uint32(*i)) 28 | copy(p, tmp[1:]) 29 | return 3, nil 30 | } 31 | func (i *Int3) Unpack(r io.Reader, length int, opt *Options) error { 32 | var tmp [4]byte 33 | if _, err := r.Read(tmp[1:]); err != nil { 34 | return err 35 | } 36 | *i = Int3(binary.BigEndian.Uint32(tmp[:])) 37 | return nil 38 | } 39 | func (i *Int3) Size(opt *Options) int { 40 | return 3 41 | } 42 | func (i *Int3) String() string { 43 | return strconv.FormatUint(uint64(*i), 10) 44 | } 45 | 46 | // Array of custom type 47 | type ArrayInt3Struct struct { 48 | I [2]Int3 49 | } 50 | 51 | // Custom type of array of standard type 52 | type DoubleUInt8 [2]uint8 53 | 54 | type DoubleUInt8Struct struct { 55 | I DoubleUInt8 56 | } 57 | 58 | func (di *DoubleUInt8) Pack(p []byte, opt *Options) (int, error) { 59 | for i, value := range *di { 60 | p[i] = value 61 | } 62 | 63 | return 2, nil 64 | } 65 | 66 | func (di *DoubleUInt8) Unpack(r io.Reader, length int, opt *Options) error { 67 | for i := 0; i < 2; i++ { 68 | var value uint8 69 | if err := binary.Read(r, binary.LittleEndian, &value); err != nil { 70 | if err == io.EOF { 71 | return io.ErrUnexpectedEOF 72 | } 73 | return err 74 | } 75 | di[i] = value 76 | } 77 | return nil 78 | } 79 | 80 | func (di *DoubleUInt8) Size(opt *Options) int { 81 | return 2 82 | } 83 | 84 | func (di *DoubleUInt8) String() string { 85 | panic("not implemented") 86 | } 87 | 88 | // Custom type of array of custom type 89 | type DoubleInt3 [2]Int3 90 | 91 | type DoubleInt3Struct struct { 92 | D DoubleInt3 93 | } 94 | 95 | func (di *DoubleInt3) Pack(p []byte, opt *Options) (int, error) { 96 | var out []byte 97 | for _, value := range *di { 98 | tmp := make([]byte, 3) 99 | if _, err := value.Pack(tmp, opt); err != nil { 100 | return 0, err 101 | } 102 | out = append(out, tmp...) 103 | } 104 | copy(p, out) 105 | 106 | return 6, nil 107 | } 108 | 109 | func (di *DoubleInt3) Unpack(r io.Reader, length int, opt *Options) error { 110 | for i := 0; i < 2; i++ { 111 | di[i].Unpack(r, 0, opt) 112 | } 113 | return nil 114 | } 115 | 116 | func (di *DoubleInt3) Size(opt *Options) int { 117 | return 6 118 | } 119 | 120 | func (di *DoubleInt3) String() string { 121 | panic("not implemented") 122 | } 123 | 124 | // Custom type of slice of standard type 125 | // Slice of uint8, stored in a zero terminated list. 126 | type SliceUInt8 []uint8 127 | 128 | type SliceUInt8Struct struct { 129 | I SliceUInt8 130 | N uint8 // A field after to ensure the length is correct. 131 | } 132 | 133 | func (ia *SliceUInt8) Pack(p []byte, opt *Options) (int, error) { 134 | for i, value := range *ia { 135 | p[i] = value 136 | } 137 | 138 | return len(*ia) + 1, nil 139 | } 140 | 141 | func (ia *SliceUInt8) Unpack(r io.Reader, length int, opt *Options) error { 142 | for { 143 | var value uint8 144 | if err := binary.Read(r, binary.LittleEndian, &value); err != nil { 145 | if err == io.EOF { 146 | return io.ErrUnexpectedEOF 147 | } 148 | return err 149 | } 150 | if value == 0 { 151 | break 152 | } 153 | *ia = append(*ia, value) 154 | } 155 | return nil 156 | } 157 | 158 | func (ia *SliceUInt8) Size(opt *Options) int { 159 | return len(*ia) + 1 160 | } 161 | 162 | func (ia *SliceUInt8) String() string { 163 | panic("not implemented") 164 | } 165 | 166 | type ArrayOfUInt8Struct struct { 167 | I [2]uint8 168 | } 169 | 170 | // Custom 4-character fixed string, similar to CHAR(4) in SQL. 171 | type Char4 string 172 | 173 | func (*Char4) Size(opt *Options) int { 174 | return 4 175 | } 176 | 177 | func (c *Char4) Pack(p []byte, opt *Options) (int, error) { 178 | buf := []byte(*c) 179 | buf = append(buf, make([]byte, c.Size(nil)-len(buf))...) 180 | 181 | copy(p, buf) 182 | return len(buf), nil 183 | } 184 | 185 | func (c *Char4) Unpack(r io.Reader, length int, opt *Options) error { 186 | buf := bytes.Buffer{} 187 | if _, err := buf.ReadFrom(r); err != nil { 188 | if err == io.EOF { 189 | return io.ErrUnexpectedEOF 190 | } 191 | return err 192 | } 193 | 194 | *c = Char4(buf.String()) 195 | return nil 196 | } 197 | 198 | func (c *Char4) String() string { 199 | return string(*c) 200 | } 201 | 202 | type Char4Struct struct { 203 | C Char4 204 | } 205 | 206 | func TestCustomTypes(t *testing.T) { 207 | testCases := []struct { 208 | name string 209 | packObj interface{} 210 | emptyObj interface{} 211 | expectBytes []byte 212 | skip bool // Skip the test, because it fails. 213 | // Switch to expectFail when possible: 214 | // https://github.com/golang/go/issues/25951 215 | }{ 216 | // Start tests with unimplemented non-custom types. 217 | { 218 | name: "ArrayOfUInt8", 219 | packObj: [2]uint8{32, 64}, 220 | emptyObj: [2]uint8{0, 0}, 221 | expectBytes: []byte{32, 64}, 222 | skip: true, // Not implemented. 223 | }, 224 | { 225 | name: "PointerToArrayOfUInt8", 226 | packObj: &[2]uint8{32, 64}, 227 | emptyObj: &[2]uint8{0, 0}, 228 | expectBytes: []byte{32, 64}, 229 | skip: true, // Not implemented. 230 | }, 231 | { 232 | name: "ArrayOfUInt8Struct", 233 | packObj: &ArrayOfUInt8Struct{I: [2]uint8{32, 64}}, 234 | emptyObj: &ArrayOfUInt8Struct{}, 235 | expectBytes: []byte{32, 64}, 236 | }, 237 | { 238 | name: "CustomType", 239 | packObj: newInt3(3), 240 | emptyObj: newInt3(0), 241 | expectBytes: []byte{0, 0, 3}, 242 | }, 243 | { 244 | name: "CustomType-Big", 245 | packObj: newInt3(4000), 246 | emptyObj: newInt3(0), 247 | expectBytes: []byte{0, 15, 160}, 248 | }, 249 | { 250 | name: "CustomTypeStruct", 251 | packObj: &Int3Struct{3}, 252 | emptyObj: &Int3Struct{}, 253 | expectBytes: []byte{0, 0, 3}, 254 | }, 255 | { 256 | name: "ArrayOfCustomType", 257 | packObj: [2]Int3{3, 4}, 258 | emptyObj: [2]Int3{}, 259 | expectBytes: []byte{0, 0, 3, 0, 0, 4}, 260 | skip: true, // Not implemented. 261 | }, 262 | { 263 | name: "PointerToArrayOfCustomType", 264 | packObj: &[2]Int3{3, 4}, 265 | emptyObj: &[2]Int3{}, 266 | expectBytes: []byte{0, 0, 3, 0, 0, 4}, 267 | skip: true, // Not implemented. 268 | }, 269 | { 270 | name: "ArrayOfCustomTypeStruct", 271 | packObj: &ArrayInt3Struct{[2]Int3{3, 4}}, 272 | emptyObj: &ArrayInt3Struct{}, 273 | expectBytes: []byte{0, 0, 3, 0, 0, 4}, 274 | skip: true, // Not implemented. 275 | }, 276 | { 277 | name: "CustomTypeOfArrayOfUInt8", 278 | packObj: &DoubleUInt8{32, 64}, 279 | emptyObj: &DoubleUInt8{}, 280 | expectBytes: []byte{32, 64}, 281 | }, 282 | { 283 | name: "CustomTypeOfArrayOfUInt8Struct", 284 | packObj: &DoubleUInt8Struct{I: DoubleUInt8{32, 64}}, 285 | emptyObj: &DoubleUInt8Struct{}, 286 | expectBytes: []byte{32, 64}, 287 | skip: true, // Not implemented. 288 | }, 289 | { 290 | name: "CustomTypeOfArrayOfCustomType", 291 | packObj: &DoubleInt3{Int3(128), Int3(256)}, 292 | emptyObj: &DoubleInt3{}, 293 | expectBytes: []byte{0, 0, 128, 0, 1, 0}, 294 | }, 295 | { 296 | name: "CustomTypeOfArrayOfCustomTypeStruct", 297 | packObj: &DoubleInt3Struct{D: DoubleInt3{Int3(128), Int3(256)}}, 298 | emptyObj: &DoubleInt3Struct{}, 299 | expectBytes: []byte{0, 0, 128, 0, 1, 0}, 300 | skip: true, // Not implemented. 301 | }, 302 | { 303 | name: "CustomTypeOfSliceOfUInt8", 304 | packObj: &SliceUInt8{128, 64, 32}, 305 | emptyObj: &SliceUInt8{}, 306 | expectBytes: []byte{128, 64, 32, 0}, 307 | }, 308 | { 309 | name: "CustomTypeOfSliceOfUInt8-Empty", 310 | packObj: &SliceUInt8{}, 311 | emptyObj: &SliceUInt8{}, 312 | expectBytes: []byte{0}, 313 | }, 314 | { 315 | name: "CustomTypeOfSliceOfUInt8Struct", 316 | packObj: &SliceUInt8Struct{I: SliceUInt8{128, 64, 32}, N: 192}, 317 | emptyObj: &SliceUInt8Struct{}, 318 | expectBytes: []byte{128, 64, 32, 0, 192}, 319 | skip: true, // Not implemented. 320 | }, 321 | { 322 | name: "CustomTypeOfChar4Struct", 323 | packObj: &Char4Struct{C: Char4("foo\x00")}, 324 | emptyObj: &Char4Struct{}, 325 | expectBytes: []byte{102, 111, 111, 0}, 326 | }, 327 | } 328 | 329 | for _, test := range testCases { 330 | // TODO: Switch to t.Run() when Go 1.7 is the minimum supported version. 331 | t.Log("RUN ", test.name) 332 | runner := func(t *testing.T) { 333 | defer func() { 334 | if r := recover(); r != nil { 335 | t.Log("unexpected panic:", r) 336 | t.Error(r) 337 | } 338 | }() 339 | if test.skip { 340 | // TODO: Switch to t.Skip() when Go 1.7 is supported 341 | t.Log("skipped unimplemented") 342 | return 343 | } 344 | var buf bytes.Buffer 345 | if err := Pack(&buf, test.packObj); err != nil { 346 | t.Fatal(err) 347 | } 348 | if !bytes.Equal(buf.Bytes(), test.expectBytes) { 349 | t.Fatal("error packing, expect:", test.expectBytes, "found:", buf.Bytes()) 350 | } 351 | if err := Unpack(&buf, test.emptyObj); err != nil { 352 | t.Fatal(err) 353 | } 354 | if !reflect.DeepEqual(test.packObj, test.emptyObj) { 355 | t.Fatal("error unpacking, expect:", test.packObj, "found:", test.emptyObj) 356 | } 357 | } 358 | runner(t) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "math" 8 | "reflect" 9 | ) 10 | 11 | type Field struct { 12 | Name string 13 | Ptr bool 14 | Index int 15 | Type Type 16 | defType Type 17 | Array bool 18 | Slice bool 19 | Len int 20 | Order binary.ByteOrder 21 | Sizeof []int 22 | Sizefrom []int 23 | Fields Fields 24 | kind reflect.Kind 25 | } 26 | 27 | func (f *Field) String() string { 28 | var out string 29 | if f.Type == Pad { 30 | return fmt.Sprintf("{type: Pad, len: %d}", f.Len) 31 | } else { 32 | out = fmt.Sprintf("type: %s, order: %v", f.Type.String(), f.Order) 33 | } 34 | if f.Sizefrom != nil { 35 | out += fmt.Sprintf(", sizefrom: %v", f.Sizefrom) 36 | } else if f.Len > 0 { 37 | out += fmt.Sprintf(", len: %d", f.Len) 38 | } 39 | if f.Sizeof != nil { 40 | out += fmt.Sprintf(", sizeof: %v", f.Sizeof) 41 | } 42 | return "{" + out + "}" 43 | } 44 | 45 | func (f *Field) Size(val reflect.Value, options *Options) int { 46 | typ := f.Type.Resolve(options) 47 | size := 0 48 | if typ == Struct { 49 | vals := []reflect.Value{val} 50 | if f.Slice { 51 | vals = make([]reflect.Value, val.Len()) 52 | for i := 0; i < val.Len(); i++ { 53 | vals[i] = val.Index(i) 54 | } 55 | } 56 | for _, val := range vals { 57 | size += f.Fields.Sizeof(val, options) 58 | } 59 | } else if typ == Pad { 60 | size = f.Len 61 | } else if typ == CustomType { 62 | return val.Addr().Interface().(Custom).Size(options) 63 | } else if f.Slice || f.kind == reflect.String { 64 | length := val.Len() 65 | if f.Len > 1 { 66 | length = f.Len 67 | } 68 | size = length * typ.Size() 69 | } else { 70 | size = typ.Size() 71 | } 72 | align := options.ByteAlign 73 | if align > 0 && size < align { 74 | size = align 75 | } 76 | return size 77 | } 78 | 79 | func (f *Field) packVal(buf []byte, val reflect.Value, length int, options *Options) (size int, err error) { 80 | order := f.Order 81 | if options.Order != nil { 82 | order = options.Order 83 | } 84 | if f.Ptr { 85 | val = val.Elem() 86 | } 87 | typ := f.Type.Resolve(options) 88 | switch typ { 89 | case Struct: 90 | return f.Fields.Pack(buf, val, options) 91 | case Bool, Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64: 92 | size = typ.Size() 93 | var n uint64 94 | switch f.kind { 95 | case reflect.Bool: 96 | if val.Bool() { 97 | n = 1 98 | } else { 99 | n = 0 100 | } 101 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 102 | n = uint64(val.Int()) 103 | default: 104 | n = val.Uint() 105 | } 106 | switch typ { 107 | case Bool: 108 | if n != 0 { 109 | buf[0] = 1 110 | } else { 111 | buf[0] = 0 112 | } 113 | case Int8, Uint8: 114 | buf[0] = byte(n) 115 | case Int16, Uint16: 116 | order.PutUint16(buf, uint16(n)) 117 | case Int32, Uint32: 118 | order.PutUint32(buf, uint32(n)) 119 | case Int64, Uint64: 120 | order.PutUint64(buf, uint64(n)) 121 | } 122 | case Float32, Float64: 123 | size = typ.Size() 124 | n := val.Float() 125 | switch typ { 126 | case Float32: 127 | order.PutUint32(buf, math.Float32bits(float32(n))) 128 | case Float64: 129 | order.PutUint64(buf, math.Float64bits(n)) 130 | } 131 | case String: 132 | switch f.kind { 133 | case reflect.String: 134 | size = val.Len() 135 | copy(buf, []byte(val.String())) 136 | default: 137 | // TODO: handle kind != bytes here 138 | size = val.Len() 139 | copy(buf, val.Bytes()) 140 | } 141 | case CustomType: 142 | return val.Addr().Interface().(Custom).Pack(buf, options) 143 | default: 144 | panic(fmt.Sprintf("no pack handler for type: %s", typ)) 145 | } 146 | return 147 | } 148 | 149 | func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options) (int, error) { 150 | typ := f.Type.Resolve(options) 151 | if typ == Pad { 152 | for i := 0; i < length; i++ { 153 | buf[i] = 0 154 | } 155 | return length, nil 156 | } 157 | if f.Slice { 158 | // special case strings and byte slices for performance 159 | end := val.Len() 160 | if !f.Array && typ == Uint8 && (f.defType == Uint8 || f.kind == reflect.String) { 161 | var tmp []byte 162 | if f.kind == reflect.String { 163 | tmp = []byte(val.String()) 164 | } else { 165 | tmp = val.Bytes() 166 | } 167 | copy(buf, tmp) 168 | if end < length { 169 | // TODO: allow configuring pad byte? 170 | rep := bytes.Repeat([]byte{0}, length-end) 171 | copy(buf[end:], rep) 172 | return length, nil 173 | } 174 | return val.Len(), nil 175 | } 176 | pos := 0 177 | var zero reflect.Value 178 | if end < length { 179 | zero = reflect.Zero(val.Type().Elem()) 180 | } 181 | for i := 0; i < length; i++ { 182 | cur := zero 183 | if i < end { 184 | cur = val.Index(i) 185 | } 186 | if n, err := f.packVal(buf[pos:], cur, 1, options); err != nil { 187 | return pos, err 188 | } else { 189 | pos += n 190 | } 191 | } 192 | return pos, nil 193 | } else { 194 | return f.packVal(buf, val, length, options) 195 | } 196 | } 197 | 198 | func (f *Field) unpackVal(buf []byte, val reflect.Value, length int, options *Options) error { 199 | order := f.Order 200 | if options.Order != nil { 201 | order = options.Order 202 | } 203 | if f.Ptr { 204 | val = val.Elem() 205 | } 206 | typ := f.Type.Resolve(options) 207 | switch typ { 208 | case Float32, Float64: 209 | var n float64 210 | switch typ { 211 | case Float32: 212 | n = float64(math.Float32frombits(order.Uint32(buf))) 213 | case Float64: 214 | n = math.Float64frombits(order.Uint64(buf)) 215 | } 216 | switch f.kind { 217 | case reflect.Float32, reflect.Float64: 218 | val.SetFloat(n) 219 | default: 220 | return fmt.Errorf("struc: refusing to unpack float into field %s of type %s", f.Name, f.kind.String()) 221 | } 222 | case Bool, Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64: 223 | var n uint64 224 | switch typ { 225 | case Int8: 226 | n = uint64(int64(int8(buf[0]))) 227 | case Int16: 228 | n = uint64(int64(int16(order.Uint16(buf)))) 229 | case Int32: 230 | n = uint64(int64(int32(order.Uint32(buf)))) 231 | case Int64: 232 | n = uint64(int64(order.Uint64(buf))) 233 | case Bool, Uint8: 234 | n = uint64(buf[0]) 235 | case Uint16: 236 | n = uint64(order.Uint16(buf)) 237 | case Uint32: 238 | n = uint64(order.Uint32(buf)) 239 | case Uint64: 240 | n = uint64(order.Uint64(buf)) 241 | } 242 | switch f.kind { 243 | case reflect.Bool: 244 | val.SetBool(n != 0) 245 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 246 | val.SetInt(int64(n)) 247 | default: 248 | val.SetUint(n) 249 | } 250 | default: 251 | panic(fmt.Sprintf("no unpack handler for type: %s", typ)) 252 | } 253 | return nil 254 | } 255 | 256 | func (f *Field) Unpack(buf []byte, val reflect.Value, length int, options *Options) error { 257 | typ := f.Type.Resolve(options) 258 | if typ == Pad || f.kind == reflect.String { 259 | if typ == Pad { 260 | return nil 261 | } else { 262 | val.SetString(string(buf)) 263 | return nil 264 | } 265 | } else if f.Slice { 266 | if val.Cap() < length { 267 | val.Set(reflect.MakeSlice(val.Type(), length, length)) 268 | } else if val.Len() < length { 269 | val.Set(val.Slice(0, length)) 270 | } 271 | // special case byte slices for performance 272 | if !f.Array && typ == Uint8 && f.defType == Uint8 { 273 | copy(val.Bytes(), buf[:length]) 274 | return nil 275 | } 276 | pos := 0 277 | size := typ.Size() 278 | for i := 0; i < length; i++ { 279 | if err := f.unpackVal(buf[pos:pos+size], val.Index(i), 1, options); err != nil { 280 | return err 281 | } 282 | pos += size 283 | } 284 | return nil 285 | } else { 286 | return f.unpackVal(buf, val, length, options) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /field_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | type badFloat struct { 9 | BadFloat int `struc:"float64"` 10 | } 11 | 12 | func TestBadFloatField(t *testing.T) { 13 | buf := bytes.NewReader([]byte("00000000")) 14 | err := Unpack(buf, &badFloat{}) 15 | if err == nil { 16 | t.Fatal("failed to error on bad float unpack") 17 | } 18 | } 19 | 20 | type emptyLengthField struct { 21 | Strlen int `struc:"sizeof=Str"` 22 | Str []byte 23 | } 24 | 25 | func TestEmptyLengthField(t *testing.T) { 26 | var buf bytes.Buffer 27 | s := &emptyLengthField{0, []byte("test")} 28 | o := &emptyLengthField{} 29 | if err := Pack(&buf, s); err != nil { 30 | t.Fatal(err) 31 | } 32 | if err := Unpack(&buf, o); err != nil { 33 | t.Fatal(err) 34 | } 35 | if !bytes.Equal(s.Str, o.Str) { 36 | t.Fatal("empty length field encode failed") 37 | } 38 | } 39 | 40 | type fixedSlicePad struct { 41 | Field []byte `struc:"[4]byte"` 42 | } 43 | 44 | func TestFixedSlicePad(t *testing.T) { 45 | var buf bytes.Buffer 46 | ref := []byte{0, 0, 0, 0} 47 | s := &fixedSlicePad{} 48 | if err := Pack(&buf, s); err != nil { 49 | t.Fatal(err) 50 | } 51 | if !bytes.Equal(buf.Bytes(), ref) { 52 | t.Fatal("implicit fixed slice pack failed") 53 | } 54 | if err := Unpack(&buf, s); err != nil { 55 | t.Fatal(err) 56 | } 57 | if !bytes.Equal(s.Field, ref) { 58 | t.Fatal("implicit fixed slice unpack failed") 59 | } 60 | } 61 | 62 | type sliceCap struct { 63 | Len int `struc:"sizeof=Field"` 64 | Field []byte 65 | } 66 | 67 | func TestSliceCap(t *testing.T) { 68 | var buf bytes.Buffer 69 | tmp := &sliceCap{0, []byte("1234")} 70 | if err := Pack(&buf, tmp); err != nil { 71 | t.Fatal(err) 72 | } 73 | tmp.Field = make([]byte, 0, 4) 74 | if err := Unpack(&buf, tmp); err != nil { 75 | t.Fatal(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /fields.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | type Fields []*Field 12 | 13 | func (f Fields) SetByteOrder(order binary.ByteOrder) { 14 | for _, field := range f { 15 | if field != nil { 16 | field.Order = order 17 | } 18 | } 19 | } 20 | 21 | func (f Fields) String() string { 22 | fields := make([]string, len(f)) 23 | for i, field := range f { 24 | if field != nil { 25 | fields[i] = field.String() 26 | } 27 | } 28 | return "{" + strings.Join(fields, ", ") + "}" 29 | } 30 | 31 | func (f Fields) Sizeof(val reflect.Value, options *Options) int { 32 | for val.Kind() == reflect.Ptr { 33 | val = val.Elem() 34 | } 35 | size := 0 36 | for i, field := range f { 37 | if field != nil { 38 | size += field.Size(val.Field(i), options) 39 | } 40 | } 41 | return size 42 | } 43 | 44 | func (f Fields) sizefrom(val reflect.Value, index []int) int { 45 | field := val.FieldByIndex(index) 46 | switch field.Kind() { 47 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 48 | return int(field.Int()) 49 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 50 | n := int(field.Uint()) 51 | // all the builtin array length types are native int 52 | // so this guards against weird truncation 53 | if n < 0 { 54 | return 0 55 | } 56 | return n 57 | default: 58 | name := val.Type().FieldByIndex(index).Name 59 | panic(fmt.Sprintf("sizeof field %T.%s not an integer type", val.Interface(), name)) 60 | } 61 | } 62 | 63 | func (f Fields) Pack(buf []byte, val reflect.Value, options *Options) (int, error) { 64 | for val.Kind() == reflect.Ptr { 65 | val = val.Elem() 66 | } 67 | pos := 0 68 | for i, field := range f { 69 | if field == nil { 70 | continue 71 | } 72 | v := val.Field(i) 73 | length := field.Len 74 | if field.Sizefrom != nil { 75 | length = f.sizefrom(val, field.Sizefrom) 76 | } 77 | if length <= 0 && field.Slice { 78 | length = v.Len() 79 | } 80 | if field.Sizeof != nil { 81 | length := val.FieldByIndex(field.Sizeof).Len() 82 | switch field.kind { 83 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 84 | // allocating a new int here has fewer side effects (doesn't update the original struct) 85 | // but it's a wasteful allocation 86 | // the old method might work if we just cast the temporary int/uint to the target type 87 | v = reflect.New(v.Type()).Elem() 88 | v.SetInt(int64(length)) 89 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 90 | v = reflect.New(v.Type()).Elem() 91 | v.SetUint(uint64(length)) 92 | default: 93 | panic(fmt.Sprintf("sizeof field is not int or uint type: %s, %s", field.Name, v.Type())) 94 | } 95 | } 96 | if n, err := field.Pack(buf[pos:], v, length, options); err != nil { 97 | return n, err 98 | } else { 99 | pos += n 100 | } 101 | } 102 | return pos, nil 103 | } 104 | 105 | func (f Fields) Unpack(r io.Reader, val reflect.Value, options *Options) error { 106 | for val.Kind() == reflect.Ptr { 107 | val = val.Elem() 108 | } 109 | var tmp [8]byte 110 | var buf []byte 111 | for i, field := range f { 112 | if field == nil { 113 | continue 114 | } 115 | v := val.Field(i) 116 | length := field.Len 117 | if field.Sizefrom != nil { 118 | length = f.sizefrom(val, field.Sizefrom) 119 | } 120 | if v.Kind() == reflect.Ptr && !v.Elem().IsValid() { 121 | v.Set(reflect.New(v.Type().Elem())) 122 | } 123 | if field.Type == Struct { 124 | if field.Slice { 125 | vals := v 126 | if !field.Array { 127 | vals = reflect.MakeSlice(v.Type(), length, length) 128 | } 129 | for i := 0; i < length; i++ { 130 | v := vals.Index(i) 131 | fields, err := parseFields(v) 132 | if err != nil { 133 | return err 134 | } 135 | if err := fields.Unpack(r, v, options); err != nil { 136 | return err 137 | } 138 | } 139 | if !field.Array { 140 | v.Set(vals) 141 | } 142 | } else { 143 | // TODO: DRY (we repeat the inner loop above) 144 | fields, err := parseFields(v) 145 | if err != nil { 146 | return err 147 | } 148 | if err := fields.Unpack(r, v, options); err != nil { 149 | return err 150 | } 151 | } 152 | continue 153 | } else { 154 | typ := field.Type.Resolve(options) 155 | if typ == CustomType { 156 | if err := v.Addr().Interface().(Custom).Unpack(r, length, options); err != nil { 157 | return err 158 | } 159 | } else { 160 | size := length * field.Type.Resolve(options).Size() 161 | if size < 8 { 162 | buf = tmp[:size] 163 | } else { 164 | buf = make([]byte, size) 165 | } 166 | if _, err := io.ReadFull(r, buf); err != nil { 167 | return err 168 | } 169 | err := field.Unpack(buf[:size], v, length, options) 170 | if err != nil { 171 | return err 172 | } 173 | } 174 | } 175 | } 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /fields_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var refVal = reflect.ValueOf(reference) 10 | 11 | func TestFieldsParse(t *testing.T) { 12 | if _, err := parseFields(refVal); err != nil { 13 | t.Fatal(err) 14 | } 15 | } 16 | 17 | func TestFieldsString(t *testing.T) { 18 | fields, _ := parseFields(refVal) 19 | fields.String() 20 | } 21 | 22 | type sizefromStruct struct { 23 | Size1 uint `struc:"sizeof=Var1"` 24 | Var1 []byte 25 | Size2 int `struc:"sizeof=Var2"` 26 | Var2 []byte 27 | } 28 | 29 | func TestFieldsSizefrom(t *testing.T) { 30 | var test = sizefromStruct{ 31 | Var1: []byte{1, 2, 3}, 32 | Var2: []byte{4, 5, 6}, 33 | } 34 | var buf bytes.Buffer 35 | err := Pack(&buf, &test) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | err = Unpack(&buf, &test) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | 45 | type sizefromStructBad struct { 46 | Size1 string `struc:"sizeof=Var1"` 47 | Var1 []byte 48 | } 49 | 50 | func TestFieldsSizefromBad(t *testing.T) { 51 | var test = &sizefromStructBad{Var1: []byte{1, 2, 3}} 52 | var buf bytes.Buffer 53 | defer func() { 54 | if err := recover(); err == nil { 55 | t.Fatal("failed to panic on bad sizeof type") 56 | } 57 | }() 58 | Pack(&buf, &test) 59 | } 60 | 61 | type StructWithinArray struct { 62 | a uint32 63 | } 64 | 65 | type StructHavingArray struct { 66 | Props [1]StructWithinArray `struc:"[1]StructWithinArray"` 67 | } 68 | 69 | func TestStrucArray(t *testing.T) { 70 | var buf bytes.Buffer 71 | a := &StructHavingArray{[1]StructWithinArray{}} 72 | err := Pack(&buf, a) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | b := &StructHavingArray{} 77 | err = Unpack(&buf, b) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lunixbochs/struc 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /legacy.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Deprecated. Use PackWithOptions. 9 | func PackWithOrder(w io.Writer, data interface{}, order binary.ByteOrder) error { 10 | return PackWithOptions(w, data, &Options{Order: order}) 11 | } 12 | 13 | // Deprecated. Use UnpackWithOptions. 14 | func UnpackWithOrder(r io.Reader, data interface{}, order binary.ByteOrder) error { 15 | return UnpackWithOptions(r, data, &Options{Order: order}) 16 | } 17 | -------------------------------------------------------------------------------- /packable_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | var packableReference = []byte{ 10 | 1, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 5, 0, 6, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 8, 11 | 9, 10, 11, 12, 13, 14, 15, 16, 12 | 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, 23, 0, 24, 13 | } 14 | 15 | func TestPackable(t *testing.T) { 16 | var ( 17 | buf bytes.Buffer 18 | 19 | i8 int8 = 1 20 | i16 int16 = 2 21 | i32 int32 = 3 22 | i64 int64 = 4 23 | u8 uint8 = 5 24 | u16 uint16 = 6 25 | u32 uint32 = 7 26 | u64 uint64 = 8 27 | 28 | u8a = [8]uint8{9, 10, 11, 12, 13, 14, 15, 16} 29 | u16a = [8]uint16{17, 18, 19, 20, 21, 22, 23, 24} 30 | ) 31 | // pack tests 32 | if err := Pack(&buf, i8); err != nil { 33 | t.Fatal(err) 34 | } 35 | if err := Pack(&buf, i16); err != nil { 36 | t.Fatal(err) 37 | } 38 | if err := Pack(&buf, i32); err != nil { 39 | t.Fatal(err) 40 | } 41 | if err := Pack(&buf, i64); err != nil { 42 | t.Fatal(err) 43 | } 44 | if err := Pack(&buf, u8); err != nil { 45 | t.Fatal(err) 46 | } 47 | if err := Pack(&buf, u16); err != nil { 48 | t.Fatal(err) 49 | } 50 | if err := Pack(&buf, u32); err != nil { 51 | t.Fatal(err) 52 | } 53 | if err := Pack(&buf, u64); err != nil { 54 | t.Fatal(err) 55 | } 56 | if err := Pack(&buf, u8a[:]); err != nil { 57 | t.Fatal(err) 58 | } 59 | if err := Pack(&buf, u16a[:]); err != nil { 60 | t.Fatal(err) 61 | } 62 | if !bytes.Equal(buf.Bytes(), packableReference) { 63 | fmt.Println(buf.Bytes()) 64 | fmt.Println(packableReference) 65 | t.Fatal("Packable Pack() did not match reference.") 66 | } 67 | // unpack tests 68 | i8 = 0 69 | i16 = 0 70 | i32 = 0 71 | i64 = 0 72 | u8 = 0 73 | u16 = 0 74 | u32 = 0 75 | u64 = 0 76 | if err := Unpack(&buf, &i8); err != nil { 77 | t.Fatal(err) 78 | } 79 | if err := Unpack(&buf, &i16); err != nil { 80 | t.Fatal(err) 81 | } 82 | if err := Unpack(&buf, &i32); err != nil { 83 | t.Fatal(err) 84 | } 85 | if err := Unpack(&buf, &i64); err != nil { 86 | t.Fatal(err) 87 | } 88 | if err := Unpack(&buf, &u8); err != nil { 89 | t.Fatal(err) 90 | } 91 | if err := Unpack(&buf, &u16); err != nil { 92 | t.Fatal(err) 93 | } 94 | if err := Unpack(&buf, &u32); err != nil { 95 | t.Fatal(err) 96 | } 97 | if err := Unpack(&buf, &u64); err != nil { 98 | t.Fatal(err) 99 | } 100 | if err := Unpack(&buf, u8a[:]); err != nil { 101 | t.Fatal(err) 102 | } 103 | if err := Unpack(&buf, u16a[:]); err != nil { 104 | t.Fatal(err) 105 | } 106 | // unpack checks 107 | if i8 != 1 || i16 != 2 || i32 != 3 || i64 != 4 { 108 | t.Fatal("Signed integer unpack failed.") 109 | } 110 | if u8 != 5 || u16 != 6 || u32 != 7 || u64 != 8 { 111 | t.Fatal("Unsigned integer unpack failed.") 112 | } 113 | for i := 0; i < 8; i++ { 114 | if u8a[i] != uint8(i+9) { 115 | t.Fatal("uint8 array unpack failed.") 116 | } 117 | } 118 | for i := 0; i < 8; i++ { 119 | if u16a[i] != uint16(i+17) { 120 | t.Fatal("uint16 array unpack failed.") 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packer.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | ) 7 | 8 | type Packer interface { 9 | Pack(buf []byte, val reflect.Value, options *Options) (int, error) 10 | Unpack(r io.Reader, val reflect.Value, options *Options) error 11 | Sizeof(val reflect.Value, options *Options) int 12 | String() string 13 | } 14 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | // struc:"int32,big,sizeof=Data,skip,sizefrom=Len" 15 | 16 | type strucTag struct { 17 | Type string 18 | Order binary.ByteOrder 19 | Sizeof string 20 | Skip bool 21 | Sizefrom string 22 | } 23 | 24 | func parseStrucTag(tag reflect.StructTag) *strucTag { 25 | t := &strucTag{ 26 | Order: binary.BigEndian, 27 | } 28 | tagStr := tag.Get("struc") 29 | if tagStr == "" { 30 | // someone's going to typo this (I already did once) 31 | // sorry if you made a module actually using this tag 32 | // and you're mad at me now 33 | tagStr = tag.Get("struct") 34 | } 35 | for _, s := range strings.Split(tagStr, ",") { 36 | if strings.HasPrefix(s, "sizeof=") { 37 | tmp := strings.SplitN(s, "=", 2) 38 | t.Sizeof = tmp[1] 39 | } else if strings.HasPrefix(s, "sizefrom=") { 40 | tmp := strings.SplitN(s, "=", 2) 41 | t.Sizefrom = tmp[1] 42 | } else if s == "big" { 43 | t.Order = binary.BigEndian 44 | } else if s == "little" { 45 | t.Order = binary.LittleEndian 46 | } else if s == "skip" { 47 | t.Skip = true 48 | } else { 49 | t.Type = s 50 | } 51 | } 52 | return t 53 | } 54 | 55 | var typeLenRe = regexp.MustCompile(`^\[(\d*)\]`) 56 | 57 | func parseField(f reflect.StructField) (fd *Field, tag *strucTag, err error) { 58 | tag = parseStrucTag(f.Tag) 59 | var ok bool 60 | fd = &Field{ 61 | Name: f.Name, 62 | Len: 1, 63 | Order: tag.Order, 64 | Slice: false, 65 | kind: f.Type.Kind(), 66 | } 67 | switch fd.kind { 68 | case reflect.Array: 69 | fd.Slice = true 70 | fd.Array = true 71 | fd.Len = f.Type.Len() 72 | fd.kind = f.Type.Elem().Kind() 73 | case reflect.Slice: 74 | fd.Slice = true 75 | fd.Len = -1 76 | fd.kind = f.Type.Elem().Kind() 77 | case reflect.Ptr: 78 | fd.Ptr = true 79 | fd.kind = f.Type.Elem().Kind() 80 | } 81 | // check for custom types 82 | tmp := reflect.New(f.Type) 83 | if _, ok := tmp.Interface().(Custom); ok { 84 | fd.Type = CustomType 85 | return 86 | } 87 | var defTypeOk bool 88 | fd.defType, defTypeOk = reflectTypeMap[fd.kind] 89 | // find a type in the struct tag 90 | pureType := typeLenRe.ReplaceAllLiteralString(tag.Type, "") 91 | if fd.Type, ok = typeLookup[pureType]; ok { 92 | fd.Len = 1 93 | match := typeLenRe.FindAllStringSubmatch(tag.Type, -1) 94 | if len(match) > 0 && len(match[0]) > 1 { 95 | fd.Slice = true 96 | first := match[0][1] 97 | // Field.Len = -1 indicates a []slice 98 | if first == "" { 99 | fd.Len = -1 100 | } else { 101 | fd.Len, err = strconv.Atoi(first) 102 | } 103 | } 104 | return 105 | } 106 | // the user didn't specify a type 107 | switch f.Type { 108 | case reflect.TypeOf(Size_t(0)): 109 | fd.Type = SizeType 110 | case reflect.TypeOf(Off_t(0)): 111 | fd.Type = OffType 112 | default: 113 | if defTypeOk { 114 | fd.Type = fd.defType 115 | } else { 116 | err = errors.New(fmt.Sprintf("struc: Could not resolve field '%v' type '%v'.", f.Name, f.Type)) 117 | } 118 | } 119 | return 120 | } 121 | 122 | func parseFieldsLocked(v reflect.Value) (Fields, error) { 123 | // we need to repeat this logic because parseFields() below can't be recursively called due to locking 124 | for v.Kind() == reflect.Ptr { 125 | v = v.Elem() 126 | } 127 | t := v.Type() 128 | if v.NumField() < 1 { 129 | return nil, errors.New("struc: Struct has no fields.") 130 | } 131 | sizeofMap := make(map[string][]int) 132 | fields := make(Fields, v.NumField()) 133 | for i := 0; i < t.NumField(); i++ { 134 | field := t.Field(i) 135 | f, tag, err := parseField(field) 136 | if tag.Skip { 137 | continue 138 | } 139 | if err != nil { 140 | return nil, err 141 | } 142 | if !v.Field(i).CanSet() { 143 | continue 144 | } 145 | f.Index = i 146 | if tag.Sizeof != "" { 147 | target, ok := t.FieldByName(tag.Sizeof) 148 | if !ok { 149 | return nil, fmt.Errorf("struc: `sizeof=%s` field does not exist", tag.Sizeof) 150 | } 151 | f.Sizeof = target.Index 152 | sizeofMap[tag.Sizeof] = field.Index 153 | } 154 | if sizefrom, ok := sizeofMap[field.Name]; ok { 155 | f.Sizefrom = sizefrom 156 | } 157 | if tag.Sizefrom != "" { 158 | source, ok := t.FieldByName(tag.Sizefrom) 159 | if !ok { 160 | return nil, fmt.Errorf("struc: `sizefrom=%s` field does not exist", tag.Sizefrom) 161 | } 162 | f.Sizefrom = source.Index 163 | } 164 | if f.Len == -1 && f.Sizefrom == nil { 165 | return nil, fmt.Errorf("struc: field `%s` is a slice with no length or sizeof field", field.Name) 166 | } 167 | // recurse into nested structs 168 | // TODO: handle loops (probably by indirecting the []Field and putting pointer in cache) 169 | if f.Type == Struct { 170 | typ := field.Type 171 | if f.Ptr { 172 | typ = typ.Elem() 173 | } 174 | if f.Slice { 175 | typ = typ.Elem() 176 | } 177 | f.Fields, err = parseFieldsLocked(reflect.New(typ)) 178 | if err != nil { 179 | return nil, err 180 | } 181 | } 182 | fields[i] = f 183 | } 184 | return fields, nil 185 | } 186 | 187 | var fieldCache = make(map[reflect.Type]Fields) 188 | var fieldCacheLock sync.RWMutex 189 | var parseLock sync.Mutex 190 | 191 | func fieldCacheLookup(t reflect.Type) Fields { 192 | fieldCacheLock.RLock() 193 | defer fieldCacheLock.RUnlock() 194 | if cached, ok := fieldCache[t]; ok { 195 | return cached 196 | } 197 | return nil 198 | } 199 | 200 | func parseFields(v reflect.Value) (Fields, error) { 201 | for v.Kind() == reflect.Ptr { 202 | v = v.Elem() 203 | } 204 | t := v.Type() 205 | 206 | // fast path: hopefully the field parsing is already cached 207 | if cached := fieldCacheLookup(t); cached != nil { 208 | return cached, nil 209 | } 210 | 211 | // hold a global lock so multiple goroutines can't parse (the same) fields at once 212 | parseLock.Lock() 213 | defer parseLock.Unlock() 214 | 215 | // check cache a second time, in case parseLock was just released by 216 | // another thread who filled the cache for us 217 | if cached := fieldCacheLookup(t); cached != nil { 218 | return cached, nil 219 | } 220 | 221 | // no luck, time to parse and fill the cache ourselves 222 | fields, err := parseFieldsLocked(v) 223 | if err != nil { 224 | return nil, err 225 | } 226 | fieldCacheLock.Lock() 227 | fieldCache[t] = fields 228 | fieldCacheLock.Unlock() 229 | return fields, nil 230 | } 231 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func parseTest(data interface{}) error { 10 | _, err := parseFields(reflect.ValueOf(data)) 11 | return err 12 | } 13 | 14 | type empty struct{} 15 | 16 | func TestEmptyStruc(t *testing.T) { 17 | if err := parseTest(&empty{}); err == nil { 18 | t.Fatal("failed to error on empty struct") 19 | } 20 | } 21 | 22 | type chanStruct struct { 23 | Test chan int 24 | } 25 | 26 | func TestChanError(t *testing.T) { 27 | if err := parseTest(&chanStruct{}); err == nil { 28 | // TODO: should probably ignore channel fields 29 | t.Fatal("failed to error on struct containing channel") 30 | } 31 | } 32 | 33 | type badSizeof struct { 34 | Size int `struc:"sizeof=Bad"` 35 | } 36 | 37 | func TestBadSizeof(t *testing.T) { 38 | if err := parseTest(&badSizeof{}); err == nil { 39 | t.Fatal("failed to error on missing Sizeof target") 40 | } 41 | } 42 | 43 | type missingSize struct { 44 | Test []byte 45 | } 46 | 47 | func TestMissingSize(t *testing.T) { 48 | if err := parseTest(&missingSize{}); err == nil { 49 | t.Fatal("failed to error on missing field size") 50 | } 51 | } 52 | 53 | type badNested struct { 54 | Empty empty 55 | } 56 | 57 | func TestNestedParseError(t *testing.T) { 58 | var buf bytes.Buffer 59 | if err := Pack(&buf, &badNested{}); err == nil { 60 | t.Fatal("failed to error on bad nested struct") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /struc.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | ) 9 | 10 | type Options struct { 11 | ByteAlign int 12 | PtrSize int 13 | Order binary.ByteOrder 14 | } 15 | 16 | func (o *Options) Validate() error { 17 | if o.PtrSize == 0 { 18 | o.PtrSize = 32 19 | } else { 20 | switch o.PtrSize { 21 | case 8, 16, 32, 64: 22 | default: 23 | return fmt.Errorf("Invalid Options.PtrSize: %d. Must be in (8, 16, 32, 64)", o.PtrSize) 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | var emptyOptions = &Options{} 30 | 31 | func init() { 32 | // fill default values to avoid data race to be reported by race detector. 33 | emptyOptions.Validate() 34 | } 35 | 36 | func prep(data interface{}) (reflect.Value, Packer, error) { 37 | value := reflect.ValueOf(data) 38 | for value.Kind() == reflect.Ptr { 39 | next := value.Elem().Kind() 40 | if next == reflect.Struct || next == reflect.Ptr { 41 | value = value.Elem() 42 | } else { 43 | break 44 | } 45 | } 46 | switch value.Kind() { 47 | case reflect.Struct: 48 | fields, err := parseFields(value) 49 | return value, fields, err 50 | default: 51 | if !value.IsValid() { 52 | return reflect.Value{}, nil, fmt.Errorf("Invalid reflect.Value for %+v", data) 53 | } 54 | if c, ok := data.(Custom); ok { 55 | return value, customFallback{c}, nil 56 | } 57 | return value, binaryFallback(value), nil 58 | } 59 | } 60 | 61 | func Pack(w io.Writer, data interface{}) error { 62 | return PackWithOptions(w, data, nil) 63 | } 64 | 65 | func PackWithOptions(w io.Writer, data interface{}, options *Options) error { 66 | if options == nil { 67 | options = emptyOptions 68 | } 69 | if err := options.Validate(); err != nil { 70 | return err 71 | } 72 | val, packer, err := prep(data) 73 | if err != nil { 74 | return err 75 | } 76 | if val.Type().Kind() == reflect.String { 77 | val = val.Convert(reflect.TypeOf([]byte{})) 78 | } 79 | size := packer.Sizeof(val, options) 80 | buf := make([]byte, size) 81 | if _, err := packer.Pack(buf, val, options); err != nil { 82 | return err 83 | } 84 | _, err = w.Write(buf) 85 | return err 86 | } 87 | 88 | func Unpack(r io.Reader, data interface{}) error { 89 | return UnpackWithOptions(r, data, nil) 90 | } 91 | 92 | func UnpackWithOptions(r io.Reader, data interface{}, options *Options) error { 93 | if options == nil { 94 | options = emptyOptions 95 | } 96 | if err := options.Validate(); err != nil { 97 | return err 98 | } 99 | val, packer, err := prep(data) 100 | if err != nil { 101 | return err 102 | } 103 | return packer.Unpack(r, val, options) 104 | } 105 | 106 | func Sizeof(data interface{}) (int, error) { 107 | return SizeofWithOptions(data, nil) 108 | } 109 | 110 | func SizeofWithOptions(data interface{}, options *Options) (int, error) { 111 | if options == nil { 112 | options = emptyOptions 113 | } 114 | if err := options.Validate(); err != nil { 115 | return 0, err 116 | } 117 | val, packer, err := prep(data) 118 | if err != nil { 119 | return 0, err 120 | } 121 | return packer.Sizeof(val, options), nil 122 | } 123 | -------------------------------------------------------------------------------- /struc_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | type Nested struct { 12 | Test2 int `struc:"int8"` 13 | } 14 | 15 | type Example struct { 16 | Pad []byte `struc:"[5]pad"` // 00 00 00 00 00 17 | I8f int `struc:"int8"` // 01 18 | I16f int `struc:"int16"` // 00 02 19 | I32f int `struc:"int32"` // 00 00 00 03 20 | I64f int `struc:"int64"` // 00 00 00 00 00 00 00 04 21 | U8f int `struc:"uint8,little"` // 05 22 | U16f int `struc:"uint16,little"` // 06 00 23 | U32f int `struc:"uint32,little"` // 07 00 00 00 24 | U64f int `struc:"uint64,little"` // 08 00 00 00 00 00 00 00 25 | Boolf int `struc:"bool"` // 01 26 | Byte4f []byte `struc:"[4]byte"` // "abcd" 27 | 28 | I8 int8 // 09 29 | I16 int16 // 00 0a 30 | I32 int32 // 00 00 00 0b 31 | I64 int64 // 00 00 00 00 00 00 00 0c 32 | U8 uint8 `struc:"little"` // 0d 33 | U16 uint16 `struc:"little"` // 0e 00 34 | U32 uint32 `struc:"little"` // 0f 00 00 00 35 | U64 uint64 `struc:"little"` // 10 00 00 00 00 00 00 00 36 | BoolT bool // 01 37 | BoolF bool // 00 38 | Byte4 [4]byte // "efgh" 39 | Float1 float32 // 41 a0 00 00 40 | Float2 float64 // 41 35 00 00 00 00 00 00 41 | 42 | I32f2 int64 `struc:"int32"` // ff ff ff ff 43 | U32f2 int64 `struc:"uint32"` // ff ff ff ff 44 | 45 | I32f3 int32 `struc:"int64"` // ff ff ff ff ff ff ff ff 46 | 47 | Size int `struc:"sizeof=Str,little"` // 0a 00 00 00 48 | Str string `struc:"[]byte"` // "ijklmnopqr" 49 | Strb string `struc:"[4]byte"` // "stuv" 50 | 51 | Size2 int `struc:"uint8,sizeof=Str2"` // 04 52 | Str2 string // "1234" 53 | 54 | Size3 int `struc:"uint8,sizeof=Bstr"` // 04 55 | Bstr []byte // "5678" 56 | 57 | Size4 int `struc:"little"` // 07 00 00 00 58 | Str4a string `struc:"[]byte,sizefrom=Size4"` // "ijklmno" 59 | Str4b string `struc:"[]byte,sizefrom=Size4"` // "pqrstuv" 60 | 61 | Size5 int `struc:"uint8"` // 04 62 | Bstr2 []byte `struc:"sizefrom=Size5"` // "5678" 63 | 64 | Nested Nested // 00 00 00 01 65 | NestedP *Nested // 00 00 00 02 66 | TestP64 *int `struc:"int64"` // 00 00 00 05 67 | 68 | NestedSize int `struc:"sizeof=NestedA"` // 00 00 00 02 69 | NestedA []Nested // [00 00 00 03, 00 00 00 04] 70 | 71 | Skip int `struc:"skip"` 72 | 73 | CustomTypeSize Int3 `struc:"sizeof=CustomTypeSizeArr"` // 00 00 00 04 74 | CustomTypeSizeArr []byte // "ABCD" 75 | } 76 | 77 | var five = 5 78 | 79 | type ExampleStructWithin struct { 80 | a uint8 81 | } 82 | 83 | type ExampleSlice struct { 84 | PropsLen uint8 `struc:"sizeof=Props"` 85 | Props []ExampleStructWithin 86 | } 87 | 88 | type ExampleArray struct { 89 | PropsLen uint8 90 | Props [16]ExampleStructWithin `struc:"[16]ExampleStructWithin"` 91 | } 92 | 93 | var arraySliceReferenceBytes = []byte{ 94 | 16, 95 | 0, 0, 0, 1, 96 | 0, 0, 0, 1, 97 | 0, 0, 0, 2, 98 | 0, 0, 0, 3, 99 | 0, 0, 0, 4, 100 | 0, 0, 0, 5, 101 | 0, 0, 0, 6, 102 | 0, 0, 0, 7, 103 | 0, 0, 0, 8, 104 | 0, 0, 0, 9, 105 | 0, 0, 0, 10, 106 | 0, 0, 0, 11, 107 | 0, 0, 0, 12, 108 | 0, 0, 0, 13, 109 | 0, 0, 0, 14, 110 | 0, 0, 0, 15, 111 | 0, 0, 0, 16, 112 | } 113 | 114 | var arrayReference = &ExampleArray{ 115 | 16, 116 | [16]ExampleStructWithin{ 117 | ExampleStructWithin{1}, 118 | ExampleStructWithin{2}, 119 | ExampleStructWithin{3}, 120 | ExampleStructWithin{4}, 121 | ExampleStructWithin{5}, 122 | ExampleStructWithin{6}, 123 | ExampleStructWithin{7}, 124 | ExampleStructWithin{8}, 125 | ExampleStructWithin{9}, 126 | ExampleStructWithin{10}, 127 | ExampleStructWithin{11}, 128 | ExampleStructWithin{12}, 129 | ExampleStructWithin{13}, 130 | ExampleStructWithin{14}, 131 | ExampleStructWithin{15}, 132 | ExampleStructWithin{16}, 133 | }, 134 | } 135 | 136 | var sliceReference = &ExampleSlice{ 137 | 16, 138 | []ExampleStructWithin{ 139 | ExampleStructWithin{1}, 140 | ExampleStructWithin{2}, 141 | ExampleStructWithin{3}, 142 | ExampleStructWithin{4}, 143 | ExampleStructWithin{5}, 144 | ExampleStructWithin{6}, 145 | ExampleStructWithin{7}, 146 | ExampleStructWithin{8}, 147 | ExampleStructWithin{9}, 148 | ExampleStructWithin{10}, 149 | ExampleStructWithin{11}, 150 | ExampleStructWithin{12}, 151 | ExampleStructWithin{13}, 152 | ExampleStructWithin{14}, 153 | ExampleStructWithin{15}, 154 | ExampleStructWithin{16}, 155 | }, 156 | } 157 | 158 | var reference = &Example{ 159 | nil, 160 | 1, 2, 3, 4, 5, 6, 7, 8, 0, []byte{'a', 'b', 'c', 'd'}, 161 | 9, 10, 11, 12, 13, 14, 15, 16, true, false, [4]byte{'e', 'f', 'g', 'h'}, 162 | 20, 21, 163 | -1, 164 | 4294967295, 165 | -1, 166 | 10, "ijklmnopqr", "stuv", 167 | 4, "1234", 168 | 4, []byte("5678"), 169 | 7, "ijklmno", "pqrstuv", 170 | 4, []byte("5678"), 171 | Nested{1}, &Nested{2}, &five, 172 | 6, []Nested{{3}, {4}, {5}, {6}, {7}, {8}}, 173 | 0, 174 | Int3(4), []byte("ABCD"), 175 | } 176 | 177 | var referenceBytes = []byte{ 178 | 0, 0, 0, 0, 0, // pad(5) 179 | 1, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, // fake int8-int64(1-4) 180 | 5, 6, 0, 7, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, // fake little-endian uint8-uint64(5-8) 181 | 0, // fake bool(0) 182 | 'a', 'b', 'c', 'd', // fake [4]byte 183 | 184 | 9, 0, 10, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 12, // real int8-int64(9-12) 185 | 13, 14, 0, 15, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, // real little-endian uint8-uint64(13-16) 186 | 1, 0, // real bool(1), bool(0) 187 | 'e', 'f', 'g', 'h', // real [4]byte 188 | 65, 160, 0, 0, // real float32(20) 189 | 64, 53, 0, 0, 0, 0, 0, 0, // real float64(21) 190 | 191 | 255, 255, 255, 255, // fake int32(-1) 192 | 255, 255, 255, 255, // fake uint32(4294967295) 193 | 194 | 255, 255, 255, 255, 255, 255, 255, 255, // fake int64(-1) 195 | 196 | 10, 0, 0, 0, // little-endian int32(10) sizeof=Str 197 | 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', // Str 198 | 's', 't', 'u', 'v', // fake string([4]byte) 199 | 04, '1', '2', '3', '4', // real string 200 | 04, '5', '6', '7', '8', // fake []byte(string) 201 | 202 | 7, 0, 0, 0, // little-endian int32(7) 203 | 'i', 'j', 'k', 'l', 'm', 'n', 'o', // Str4a sizefrom=Size4 204 | 'p', 'q', 'r', 's', 't', 'u', 'v', // Str4b sizefrom=Size4 205 | 04, '5', '6', '7', '8', // fake []byte(string) 206 | 207 | 1, 2, // Nested{1}, Nested{2} 208 | 0, 0, 0, 0, 0, 0, 0, 5, // &five 209 | 210 | 0, 0, 0, 6, // int32(6) 211 | 3, 4, 5, 6, 7, 8, // [Nested{3}, ...Nested{8}] 212 | 213 | 0, 0, 4, 'A', 'B', 'C', 'D', // Int3(4), []byte("ABCD") 214 | } 215 | 216 | func TestCodec(t *testing.T) { 217 | var buf bytes.Buffer 218 | if err := Pack(&buf, reference); err != nil { 219 | t.Fatal(err) 220 | } 221 | out := &Example{} 222 | if err := Unpack(&buf, out); err != nil { 223 | t.Fatal(err) 224 | } 225 | if !reflect.DeepEqual(reference, out) { 226 | fmt.Printf("got: %#v\nwant: %#v\n", out, reference) 227 | t.Fatal("encode/decode failed") 228 | } 229 | } 230 | 231 | func TestEncode(t *testing.T) { 232 | var buf bytes.Buffer 233 | if err := Pack(&buf, reference); err != nil { 234 | t.Fatal(err) 235 | } 236 | if !bytes.Equal(buf.Bytes(), referenceBytes) { 237 | fmt.Printf("got: %#v\nwant: %#v\n", buf.Bytes(), referenceBytes) 238 | t.Fatal("encode failed") 239 | } 240 | } 241 | 242 | func TestDecode(t *testing.T) { 243 | buf := bytes.NewReader(referenceBytes) 244 | out := &Example{} 245 | if err := Unpack(buf, out); err != nil { 246 | t.Fatal(err) 247 | } 248 | if !reflect.DeepEqual(reference, out) { 249 | fmt.Printf("got: %#v\nwant: %#v\n", out, reference) 250 | t.Fatal("decode failed") 251 | } 252 | } 253 | 254 | func TestSizeof(t *testing.T) { 255 | size, err := Sizeof(reference) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | if size != len(referenceBytes) { 260 | t.Fatalf("sizeof failed; expected %d, got %d", len(referenceBytes), size) 261 | } 262 | } 263 | 264 | type ExampleEndian struct { 265 | T int `struc:"int16,big"` 266 | } 267 | 268 | func TestEndianSwap(t *testing.T) { 269 | var buf bytes.Buffer 270 | big := &ExampleEndian{1} 271 | if err := PackWithOrder(&buf, big, binary.BigEndian); err != nil { 272 | t.Fatal(err) 273 | } 274 | little := &ExampleEndian{} 275 | if err := UnpackWithOrder(&buf, little, binary.LittleEndian); err != nil { 276 | t.Fatal(err) 277 | } 278 | if little.T != 256 { 279 | t.Fatal("big -> little conversion failed") 280 | } 281 | } 282 | 283 | func TestNilValue(t *testing.T) { 284 | var buf bytes.Buffer 285 | if err := Pack(&buf, nil); err == nil { 286 | t.Fatal("failed throw error for bad struct value") 287 | } 288 | if err := Unpack(&buf, nil); err == nil { 289 | t.Fatal("failed throw error for bad struct value") 290 | } 291 | if _, err := Sizeof(nil); err == nil { 292 | t.Fatal("failed to throw error for bad struct value") 293 | } 294 | } 295 | 296 | type sliceUnderrun struct { 297 | Str string `struc:"[10]byte"` 298 | Arr []uint16 `struc:"[10]uint16"` 299 | } 300 | 301 | func TestSliceUnderrun(t *testing.T) { 302 | var buf bytes.Buffer 303 | v := sliceUnderrun{ 304 | Str: "foo", 305 | Arr: []uint16{1, 2, 3}, 306 | } 307 | if err := Pack(&buf, &v); err != nil { 308 | t.Fatal(err) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /test_pack_init/doc.go: -------------------------------------------------------------------------------- 1 | package test_pack_init 2 | 3 | // This is a placeholder package for a test on specific race detector report on 4 | // default Options initialization. 5 | -------------------------------------------------------------------------------- /test_pack_init/pack_init_test.go: -------------------------------------------------------------------------------- 1 | package test_pack_init 2 | 3 | import ( 4 | "bytes" 5 | "github.com/lunixbochs/struc" 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | type Example struct { 11 | I int `struc:int` 12 | } 13 | 14 | // TestParallelPack checks whether Pack is goroutine-safe. Run it with -race flag. 15 | // Keep it as a single test in package since it is likely to be triggered on initialization 16 | // of global objects reported as a data race by race detector. 17 | func TestParallelPack(t *testing.T) { 18 | var wg sync.WaitGroup 19 | val := Example{} 20 | for i := 0; i < 2; i++ { 21 | wg.Add(1) 22 | go func() { 23 | defer wg.Done() 24 | var buf bytes.Buffer 25 | _ = struc.Pack(&buf, &val) 26 | }() 27 | } 28 | wg.Wait() 29 | } 30 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type Type int 9 | 10 | const ( 11 | Invalid Type = iota 12 | Pad 13 | Bool 14 | Int 15 | Int8 16 | Uint8 17 | Int16 18 | Uint16 19 | Int32 20 | Uint32 21 | Int64 22 | Uint64 23 | Float32 24 | Float64 25 | String 26 | Struct 27 | Ptr 28 | 29 | SizeType 30 | OffType 31 | CustomType 32 | ) 33 | 34 | func (t Type) Resolve(options *Options) Type { 35 | switch t { 36 | case OffType: 37 | switch options.PtrSize { 38 | case 8: 39 | return Int8 40 | case 16: 41 | return Int16 42 | case 32: 43 | return Int32 44 | case 64: 45 | return Int64 46 | default: 47 | panic(fmt.Sprintf("unsupported ptr bits: %d", options.PtrSize)) 48 | } 49 | case SizeType: 50 | switch options.PtrSize { 51 | case 8: 52 | return Uint8 53 | case 16: 54 | return Uint16 55 | case 32: 56 | return Uint32 57 | case 64: 58 | return Uint64 59 | default: 60 | panic(fmt.Sprintf("unsupported ptr bits: %d", options.PtrSize)) 61 | } 62 | } 63 | return t 64 | } 65 | 66 | func (t Type) String() string { 67 | return typeNames[t] 68 | } 69 | 70 | func (t Type) Size() int { 71 | switch t { 72 | case SizeType, OffType: 73 | panic("Size_t/Off_t types must be converted to another type using options.PtrSize") 74 | case Pad, String, Int8, Uint8, Bool: 75 | return 1 76 | case Int16, Uint16: 77 | return 2 78 | case Int32, Uint32, Float32: 79 | return 4 80 | case Int64, Uint64, Float64: 81 | return 8 82 | default: 83 | panic("Cannot resolve size of type:" + t.String()) 84 | } 85 | } 86 | 87 | var typeLookup = map[string]Type{ 88 | "pad": Pad, 89 | "bool": Bool, 90 | "byte": Uint8, 91 | "int8": Int8, 92 | "uint8": Uint8, 93 | "int16": Int16, 94 | "uint16": Uint16, 95 | "int32": Int32, 96 | "uint32": Uint32, 97 | "int64": Int64, 98 | "uint64": Uint64, 99 | "float32": Float32, 100 | "float64": Float64, 101 | 102 | "size_t": SizeType, 103 | "off_t": OffType, 104 | } 105 | 106 | var typeNames = map[Type]string{ 107 | CustomType: "Custom", 108 | } 109 | 110 | func init() { 111 | for name, enum := range typeLookup { 112 | typeNames[enum] = name 113 | } 114 | } 115 | 116 | type Size_t uint64 117 | type Off_t int64 118 | 119 | var reflectTypeMap = map[reflect.Kind]Type{ 120 | reflect.Bool: Bool, 121 | reflect.Int8: Int8, 122 | reflect.Int16: Int16, 123 | reflect.Int: Int32, 124 | reflect.Int32: Int32, 125 | reflect.Int64: Int64, 126 | reflect.Uint8: Uint8, 127 | reflect.Uint16: Uint16, 128 | reflect.Uint: Uint32, 129 | reflect.Uint32: Uint32, 130 | reflect.Uint64: Uint64, 131 | reflect.Float32: Float32, 132 | reflect.Float64: Float64, 133 | reflect.String: String, 134 | reflect.Struct: Struct, 135 | reflect.Ptr: Ptr, 136 | } 137 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package struc 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestBadType(t *testing.T) { 9 | defer func() { recover() }() 10 | Type(-1).Size() 11 | t.Fatal("failed to panic for invalid Type.Size()") 12 | } 13 | 14 | func TestTypeString(t *testing.T) { 15 | if Pad.String() != "pad" { 16 | t.Fatal("type string representation failed") 17 | } 18 | } 19 | 20 | type sizeOffTest struct { 21 | Size Size_t 22 | Off Off_t 23 | } 24 | 25 | func TestSizeOffTypes(t *testing.T) { 26 | bits := []int{8, 16, 32, 64} 27 | var buf bytes.Buffer 28 | test := &sizeOffTest{1, 2} 29 | for _, b := range bits { 30 | if err := PackWithOptions(&buf, test, &Options{PtrSize: b}); err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | reference := []byte{ 35 | 1, 2, 36 | 0, 1, 0, 2, 37 | 0, 0, 0, 1, 0, 0, 0, 2, 38 | 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 39 | } 40 | if !bytes.Equal(reference, buf.Bytes()) { 41 | t.Errorf("reference: %v != bytes: %v", reference, buf.Bytes()) 42 | } 43 | reader := bytes.NewReader(buf.Bytes()) 44 | for _, b := range bits { 45 | out := &sizeOffTest{} 46 | if err := UnpackWithOptions(reader, out, &Options{PtrSize: b}); err != nil { 47 | t.Fatal(err) 48 | } 49 | if out.Size != 1 || out.Off != 2 { 50 | t.Errorf("Size_t/Off_t mismatch: {%d, %d}\n%v", out.Size, out.Off, buf.Bytes()) 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------