Skip to content

Commit 58cc049

Browse files
authored
util/cstruct: add package for decoding padded C structures (tailscale#5429)
I was working on my "dump iptables rules using only syscalls" branch and had a bunch of C structure decoding to do. Rather than manually calculating the padding or using unsafe trickery to actually cast variable-length structures to Go types, I'd rather use a helper package that deals with padding for me. Padding rules were taken from the following article: http://www.catb.org/esr/structure-packing/ Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
1 parent 9b77ac1 commit 58cc049

File tree

3 files changed

+405
-0
lines changed

3 files changed

+405
-0
lines changed

util/cstruct/cstruct.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package cstruct provides a helper for decoding binary data that is in the
6+
// form of a padded C structure.
7+
package cstruct
8+
9+
import (
10+
"errors"
11+
"io"
12+
13+
"tailscale.com/util/endian"
14+
)
15+
16+
// Size of a pointer-typed value, in bits
17+
const pointerSize = 32 << (^uintptr(0) >> 63)
18+
19+
// We assume that non-64-bit platforms are 32-bit; we don't expect Go to run on
20+
// a 16- or 8-bit architecture any time soon.
21+
const is64Bit = pointerSize == 64
22+
23+
// Decoder reads and decodes padded fields from a slice of bytes. All fields
24+
// are decoded with native endianness.
25+
//
26+
// Methods of a Decoder do not return errors, but rather store any error within
27+
// the Decoder. The first error can be obtained via the Err method; after the
28+
// first error, methods will return the zero value for their type.
29+
type Decoder struct {
30+
b []byte
31+
off int
32+
err error
33+
dbuf [8]byte // for decoding
34+
}
35+
36+
// NewDecoder creates a Decoder from a byte slice.
37+
func NewDecoder(b []byte) *Decoder {
38+
return &Decoder{b: b}
39+
}
40+
41+
var errUnsupportedSize = errors.New("unsupported size")
42+
43+
func padBytes(offset, size int) int {
44+
if offset == 0 || size == 1 {
45+
return 0
46+
}
47+
remainder := offset % size
48+
return size - remainder
49+
}
50+
51+
func (d *Decoder) getField(b []byte) error {
52+
size := len(b)
53+
54+
// We only support fields that are multiples of 2 (or 1-sized)
55+
if size != 1 && size&1 == 1 {
56+
return errUnsupportedSize
57+
}
58+
59+
// Fields are aligned to their size
60+
padBytes := padBytes(d.off, size)
61+
if d.off+size+padBytes > len(d.b) {
62+
return io.EOF
63+
}
64+
d.off += padBytes
65+
66+
copy(b, d.b[d.off:d.off+size])
67+
d.off += size
68+
return nil
69+
}
70+
71+
// Err returns the first error that was encountered by this Decoder.
72+
func (d *Decoder) Err() error {
73+
return d.err
74+
}
75+
76+
// Offset returns the current read offset for data in the buffer.
77+
func (d *Decoder) Offset() int {
78+
return d.off
79+
}
80+
81+
// Byte returns a single byte from the buffer.
82+
func (d *Decoder) Byte() byte {
83+
if d.err != nil {
84+
return 0
85+
}
86+
87+
if err := d.getField(d.dbuf[0:1]); err != nil {
88+
d.err = err
89+
return 0
90+
}
91+
return d.dbuf[0]
92+
}
93+
94+
// Byte returns a number of bytes from the buffer based on the size of the
95+
// input slice. No padding is applied.
96+
//
97+
// If an error is encountered or this Decoder has previously encountered an
98+
// error, no changes are made to the provided buffer.
99+
func (d *Decoder) Bytes(b []byte) {
100+
if d.err != nil {
101+
return
102+
}
103+
104+
// No padding for byte slices
105+
size := len(b)
106+
if d.off+size >= len(d.b) {
107+
d.err = io.EOF
108+
return
109+
}
110+
copy(b, d.b[d.off:d.off+size])
111+
d.off += size
112+
}
113+
114+
// Uint16 returns a uint16 decoded from the buffer.
115+
func (d *Decoder) Uint16() uint16 {
116+
if d.err != nil {
117+
return 0
118+
}
119+
120+
if err := d.getField(d.dbuf[0:2]); err != nil {
121+
d.err = err
122+
return 0
123+
}
124+
return endian.Native.Uint16(d.dbuf[0:2])
125+
}
126+
127+
// Uint32 returns a uint32 decoded from the buffer.
128+
func (d *Decoder) Uint32() uint32 {
129+
if d.err != nil {
130+
return 0
131+
}
132+
133+
if err := d.getField(d.dbuf[0:4]); err != nil {
134+
d.err = err
135+
return 0
136+
}
137+
return endian.Native.Uint32(d.dbuf[0:4])
138+
}
139+
140+
// Uint64 returns a uint64 decoded from the buffer.
141+
func (d *Decoder) Uint64() uint64 {
142+
if d.err != nil {
143+
return 0
144+
}
145+
146+
if err := d.getField(d.dbuf[0:8]); err != nil {
147+
d.err = err
148+
return 0
149+
}
150+
return endian.Native.Uint64(d.dbuf[0:8])
151+
}
152+
153+
// Uintptr returns a uintptr decoded from the buffer.
154+
func (d *Decoder) Uintptr() uintptr {
155+
if d.err != nil {
156+
return 0
157+
}
158+
159+
if is64Bit {
160+
return uintptr(d.Uint64())
161+
} else {
162+
return uintptr(d.Uint32())
163+
}
164+
}
165+
166+
// Int16 returns a int16 decoded from the buffer.
167+
func (d *Decoder) Int16() int16 {
168+
return int16(d.Uint16())
169+
}
170+
171+
// Int32 returns a int32 decoded from the buffer.
172+
func (d *Decoder) Int32() int32 {
173+
return int32(d.Uint32())
174+
}
175+
176+
// Int64 returns a int64 decoded from the buffer.
177+
func (d *Decoder) Int64() int64 {
178+
return int64(d.Uint64())
179+
}

util/cstruct/cstruct_example_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Only built on 64-bit platforms to avoid complexity
6+
7+
//go:build amd64 || arm64 || mips64le || ppc64le || riscv64
8+
// +build amd64 arm64 mips64le ppc64le riscv64
9+
10+
package cstruct
11+
12+
import "fmt"
13+
14+
// This test provides a semi-realistic example of how you can
15+
// use this package to decode a C structure.
16+
func ExampleDecoder() {
17+
// Our example C structure:
18+
// struct mystruct {
19+
// char *p;
20+
// char c;
21+
// /* implicit: char _pad[3]; */
22+
// int x;
23+
// };
24+
//
25+
// The Go structure definition:
26+
type myStruct struct {
27+
Ptr uintptr
28+
Ch byte
29+
Intval uint32
30+
}
31+
32+
// Our "in-memory" version of the above structure
33+
buf := []byte{
34+
1, 2, 3, 4, 0, 0, 0, 0, // ptr
35+
5, // ch
36+
99, 99, 99, // padding
37+
78, 6, 0, 0, // x
38+
}
39+
d := NewDecoder(buf)
40+
41+
// Decode the structure; if one of these function returns an error,
42+
// then subsequent decoder functions will return the zero value.
43+
var x myStruct
44+
x.Ptr = d.Uintptr()
45+
x.Ch = d.Byte()
46+
x.Intval = d.Uint32()
47+
48+
// Note that per the Go language spec:
49+
// [...] when evaluating the operands of an expression, assignment,
50+
// or return statement, all function calls, method calls, and
51+
// (channel) communication operations are evaluated in lexical
52+
// left-to-right order
53+
//
54+
// Since each field is assigned via a function call, one could use the
55+
// following snippet to decode the struct.
56+
// x := myStruct{
57+
// Ptr: d.Uintptr(),
58+
// Ch: d.Byte(),
59+
// Intval: d.Uint32(),
60+
// }
61+
//
62+
// However, this means that reordering the fields in the initialization
63+
// statement–normally a semantically identical operation–would change
64+
// the way the structure is parsed. Thus we do it as above with
65+
// explicit ordering.
66+
67+
// After finishing with the decoder, check errors
68+
if err := d.Err(); err != nil {
69+
panic(err)
70+
}
71+
72+
// Print the decoder offset and structure
73+
fmt.Printf("off=%d struct=%#v\n", d.Offset(), x)
74+
// Output: off=16 struct=cstruct.myStruct{Ptr:0x4030201, Ch:0x5, Intval:0x64e}
75+
}

0 commit comments

Comments
 (0)