File: marshal.go

package info (click to toggle)
golang-github-cilium-ebpf 0.17.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 4,684 kB
  • sloc: ansic: 1,259; makefile: 127; python: 113; awk: 29; sh: 24
file content (177 lines) | stat: -rw-r--r-- 4,652 bytes parent folder | download
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
package sysenc

import (
	"bytes"
	"encoding"
	"encoding/binary"
	"errors"
	"fmt"
	"reflect"
	"slices"
	"sync"
	"unsafe"

	"github.com/cilium/ebpf/internal"
)

// Marshal turns data into a byte slice using the system's native endianness.
//
// If possible, avoids allocations by directly using the backing memory
// of data. This means that the variable must not be modified for the lifetime
// of the returned [Buffer].
//
// Returns an error if the data can't be turned into a byte slice according to
// the behaviour of [binary.Write].
func Marshal(data any, size int) (Buffer, error) {
	if data == nil {
		return Buffer{}, errors.New("can't marshal a nil value")
	}

	var buf []byte
	var err error
	switch value := data.(type) {
	case encoding.BinaryMarshaler:
		buf, err = value.MarshalBinary()
	case string:
		buf = unsafe.Slice(unsafe.StringData(value), len(value))
	case []byte:
		buf = value
	case int16:
		buf = internal.NativeEndian.AppendUint16(make([]byte, 0, 2), uint16(value))
	case uint16:
		buf = internal.NativeEndian.AppendUint16(make([]byte, 0, 2), value)
	case int32:
		buf = internal.NativeEndian.AppendUint32(make([]byte, 0, 4), uint32(value))
	case uint32:
		buf = internal.NativeEndian.AppendUint32(make([]byte, 0, 4), value)
	case int64:
		buf = internal.NativeEndian.AppendUint64(make([]byte, 0, 8), uint64(value))
	case uint64:
		buf = internal.NativeEndian.AppendUint64(make([]byte, 0, 8), value)
	default:
		if buf := unsafeBackingMemory(data); len(buf) == size {
			return newBuffer(buf), nil
		}

		wr := internal.NewBuffer(make([]byte, 0, size))
		defer internal.PutBuffer(wr)

		err = binary.Write(wr, internal.NativeEndian, value)
		buf = wr.Bytes()
	}
	if err != nil {
		return Buffer{}, err
	}

	if len(buf) != size {
		return Buffer{}, fmt.Errorf("%T doesn't marshal to %d bytes", data, size)
	}

	return newBuffer(buf), nil
}

var bytesReaderPool = sync.Pool{
	New: func() interface{} {
		return new(bytes.Reader)
	},
}

// Unmarshal a byte slice in the system's native endianness into data.
//
// Returns an error if buf can't be unmarshalled according to the behaviour
// of [binary.Read].
func Unmarshal(data interface{}, buf []byte) error {
	switch value := data.(type) {
	case encoding.BinaryUnmarshaler:
		return value.UnmarshalBinary(buf)

	case *string:
		*value = string(buf)
		return nil

	case *[]byte:
		// Backwards compat: unmarshaling into a slice replaces the whole slice.
		*value = slices.Clone(buf)
		return nil

	default:
		if dataBuf := unsafeBackingMemory(data); len(dataBuf) == len(buf) {
			copy(dataBuf, buf)
			return nil
		}

		rd := bytesReaderPool.Get().(*bytes.Reader)
		defer bytesReaderPool.Put(rd)

		rd.Reset(buf)

		if err := binary.Read(rd, internal.NativeEndian, value); err != nil {
			return err
		}

		if rd.Len() != 0 {
			return fmt.Errorf("unmarshaling %T doesn't consume all data", data)
		}

		return nil
	}
}

// unsafeBackingMemory returns the backing memory of data if it can be used
// instead of calling into package binary.
//
// Returns nil if the value is not a pointer or a slice, or if it contains
// padding or unexported fields.
func unsafeBackingMemory(data any) []byte {
	if data == nil {
		return nil
	}

	value := reflect.ValueOf(data)
	var valueSize int
	switch value.Kind() {
	case reflect.Pointer:
		if value.IsNil() {
			return nil
		}

		if elemType := value.Type().Elem(); elemType.Kind() != reflect.Slice {
			valueSize = int(elemType.Size())
			break
		}

		// We're dealing with a pointer to a slice. Dereference and
		// handle it like a regular slice.
		value = value.Elem()
		fallthrough

	case reflect.Slice:
		valueSize = int(value.Type().Elem().Size()) * value.Len()

	default:
		// Prevent Value.UnsafePointer from panicking.
		return nil
	}

	// Some nil pointer types currently crash binary.Size. Call it after our own
	// code so that the panic isn't reachable.
	// See https://github.com/golang/go/issues/60892
	if size := binary.Size(data); size == -1 || size != valueSize {
		// The type contains padding or unsupported types.
		return nil
	}

	if hasUnexportedFields(reflect.TypeOf(data)) {
		return nil
	}

	// Reinterpret the pointer as a byte slice. This violates the unsafe.Pointer
	// rules because it's very unlikely that the source data has "an equivalent
	// memory layout". However, we can make it safe-ish because of the
	// following reasons:
	//  - There is no alignment mismatch since we cast to a type with an
	//    alignment of 1.
	//  - There are no pointers in the source type so we don't upset the GC.
	//  - The length is verified at runtime.
	return unsafe.Slice((*byte)(value.UnsafePointer()), valueSize)
}