Skip to content

Commit a9037ef

Browse files
authored
Implement encoding interfaces for MAC address (#341)
* Add basic tests and benchmarks covering the mac.go file * Implement encoding interfaces for MAC address Implements the encoding TextMarshaler, BinaryMarshaler, TextAppender and BinaryAppender interfaces for MAC for efficient encoding of MAC addresses. The String method is written in terms of the new methods for consistency and improved performance. This avoids unnecessary string allocations and conversions during the formatting process. This also allows MAC type to be used with other encoding packages like encoding/json, encoding/xml. * Refactored while writing tests. * Add unsafe string cast that matches the spirit of what uuid is doing. The uuid package is using the string builder package. That package works in bytes and self manages length. Because mac address string length is always know this commit uses the same method as string builder when casting the bytes to string. * Add tests that cover MarshalText, AppendText, UnmarshalBinary and AppendBinary
1 parent 5befb38 commit a9037ef

File tree

2 files changed

+327
-31
lines changed

2 files changed

+327
-31
lines changed

mac.go

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package bluetooth
22

3-
import "errors"
3+
import (
4+
"errors"
5+
"unsafe"
6+
)
47

58
// MAC represents a MAC address, in little endian format.
69
type MAC [6]byte
710

8-
var errInvalidMAC = errors.New("bluetooth: failed to parse MAC address")
9-
10-
// ParseMAC parses the given MAC address, which must be in 11:22:33:AA:BB:CC
11-
// format. If it cannot be parsed, an error is returned.
12-
func ParseMAC(s string) (mac MAC, err error) {
11+
// UnmarshalText unmarshals the text into itself.
12+
// The given MAC address byte array must be of the format 11:22:33:AA:BB:CC.
13+
// If it cannot be unmarshaled, an error is returned.
14+
func (mac *MAC) UnmarshalText(s []byte) error {
1315
macIndex := 11
1416
for i := 0; i < len(s); i++ {
1517
c := s[i]
@@ -22,12 +24,10 @@ func ParseMAC(s string) (mac MAC, err error) {
2224
} else if c >= 'A' && c <= 'F' {
2325
nibble = c - 'A' + 0xA
2426
} else {
25-
err = errInvalidMAC
26-
return
27+
return ErrInvalidMAC
2728
}
2829
if macIndex < 0 {
29-
err = errInvalidMAC
30-
return
30+
return ErrInvalidMAC
3131
}
3232
if macIndex%2 == 0 {
3333
mac[macIndex/2] |= nibble
@@ -37,39 +37,68 @@ func ParseMAC(s string) (mac MAC, err error) {
3737
macIndex--
3838
}
3939
if macIndex != -1 {
40-
err = errInvalidMAC
40+
return ErrInvalidMAC
4141
}
42+
return nil
43+
}
44+
45+
// ParseMAC parses the given MAC address, which must be in 11:22:33:AA:BB:CC
46+
// format. If it cannot be parsed, an error is returned.
47+
func ParseMAC(s string) (mac MAC, err error) {
48+
err = (&mac).UnmarshalText([]byte(s))
4249
return
4350
}
4451

4552
// String returns a human-readable version of this MAC address, such as
4653
// 11:22:33:AA:BB:CC.
4754
func (mac MAC) String() string {
48-
// TODO: make this more efficient.
49-
s := ""
55+
buf, _ := mac.MarshalText()
56+
return unsafe.String(unsafe.SliceData(buf), 17)
57+
}
58+
59+
const hexDigit = "0123456789ABCDEF"
60+
61+
// AppendText appends the textual representation of itself to the end of b
62+
// (allocating a larger slice if necessary) and returns the updated slice.
63+
func (mac MAC) AppendText(buf []byte) ([]byte, error) {
5064
for i := 5; i >= 0; i-- {
51-
c := mac[i]
52-
// Insert a hyphen at the correct locations.
5365
if i != 5 {
54-
s += ":"
66+
buf = append(buf, ':')
5567
}
68+
buf = append(buf, hexDigit[mac[i]>>4])
69+
buf = append(buf, hexDigit[mac[i]&0xF])
70+
}
71+
return buf, nil
72+
}
5673

57-
// First nibble.
58-
nibble := c >> 4
59-
if nibble <= 9 {
60-
s += string(nibble + '0')
61-
} else {
62-
s += string(nibble + 'A' - 10)
63-
}
74+
// MarshalText marshals itself into a string of format 11:22:33:AA:BB:CC.
75+
// It is a simple wrapper of the AppentText method.
76+
func (mac MAC) MarshalText() (text []byte, err error) {
77+
return mac.AppendText(make([]byte, 0, 17))
78+
}
6479

65-
// Second nibble.
66-
nibble = c & 0x0f
67-
if nibble <= 9 {
68-
s += string(nibble + '0')
69-
} else {
70-
s += string(nibble + 'A' - 10)
71-
}
80+
var ErrInvalidMAC = errors.New("bluetooth: failed to parse MAC address")
81+
82+
// MarshalBinary marshals itself into a binary format.
83+
// This is a simple wrapper of the AppendBinary method
84+
func (mac MAC) MarshalBinary() (data []byte, err error) {
85+
return mac.AppendBinary(make([]byte, 0, 6))
86+
}
87+
88+
var ErrInvalidBinaryMac = errors.New("bluetooth: failed to unmarshal the binary MAC address")
89+
90+
// UnmarshalBinary unmarshals the mac byte slice into itself.
91+
// It will return the ErrInvalidBinaryMac error if the given slice is not exactually 6 in length.
92+
func (mac *MAC) UnmarshalBinary(data []byte) error {
93+
if len(data) != 6 {
94+
return ErrInvalidBinaryMac
7295
}
96+
copy(mac[:], data)
97+
return nil
98+
}
7399

74-
return s
100+
// AppendBinary appends the binary representation of itself to the end of b
101+
// (allocating a larger slice if necessary) and returns the updated slice.
102+
func (mac MAC) AppendBinary(b []byte) ([]byte, error) {
103+
return append(b, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]), nil
75104
}

mac_test.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package bluetooth
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func TestParseMAC(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
strMac string
12+
wantMac MAC
13+
wantErr bool
14+
}{
15+
{
16+
name: "incrementing",
17+
strMac: "12:34:56:78:9A:BC",
18+
wantMac: [6]byte{188, 154, 120, 86, 52, 18},
19+
wantErr: false,
20+
},
21+
{
22+
name: "decrementing",
23+
strMac: "CB:A9:87:65:43:21",
24+
wantMac: [6]byte{33, 67, 101, 135, 169, 203},
25+
wantErr: false,
26+
},
27+
{
28+
name: "normal",
29+
strMac: "11:22:33:AA:BB:CC",
30+
wantMac: [6]byte{204, 187, 170, 51, 34, 17},
31+
wantErr: false,
32+
},
33+
{
34+
name: "lower",
35+
strMac: "11:22:33:aa:bb:cc",
36+
wantMac: [6]byte{},
37+
wantErr: true,
38+
},
39+
{
40+
name: "longer",
41+
strMac: "11:22:33:AA:BB:CC:22",
42+
wantMac: [6]byte{},
43+
wantErr: true,
44+
},
45+
{
46+
name: "extra2",
47+
strMac: "11:222:33:AA:BB:CC",
48+
wantMac: [6]byte{},
49+
wantErr: true,
50+
},
51+
{
52+
name: "empty",
53+
strMac: "",
54+
wantMac: [6]byte{},
55+
wantErr: true,
56+
},
57+
}
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
gotMac, err := ParseMAC(tt.strMac)
61+
if (err != nil) != tt.wantErr {
62+
t.Errorf("ParseMAC() error = %v, wantErr %v", err, tt.wantErr)
63+
return
64+
}
65+
66+
if !tt.wantErr && !bytes.Equal(gotMac[:], tt.wantMac[:]) {
67+
t.Errorf("ParseMAC() = %v, want %v", [6]byte(gotMac), [6]byte(tt.wantMac))
68+
}
69+
})
70+
}
71+
}
72+
73+
func TestMAC_String(t *testing.T) {
74+
tests := []struct {
75+
name string
76+
mac MAC
77+
want string
78+
}{
79+
{
80+
name: "incrementing",
81+
want: "12:34:56:78:9A:BC",
82+
mac: [6]byte{188, 154, 120, 86, 52, 18},
83+
},
84+
{
85+
name: "decrementing",
86+
want: "CB:A9:87:65:43:21",
87+
mac: [6]byte{33, 67, 101, 135, 169, 203},
88+
},
89+
{
90+
name: "normal",
91+
want: "11:22:33:AA:BB:CC",
92+
mac: [6]byte{204, 187, 170, 51, 34, 17},
93+
},
94+
{
95+
name: "nil",
96+
want: "00:00:00:00:00:00",
97+
mac: [6]byte{},
98+
},
99+
}
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
if got := tt.mac.String(); got != tt.want {
103+
t.Errorf("MAC.String() = %v, want %v", got, tt.want)
104+
}
105+
})
106+
}
107+
}
108+
109+
func BenchmarkParseMAC(b *testing.B) {
110+
mac := "CB:A9:87:65:43:21"
111+
var err error
112+
for i := 0; i < b.N; i++ {
113+
_, err = ParseMAC(mac)
114+
if err != nil {
115+
b.Errorf("expected nil but got %v", err)
116+
}
117+
}
118+
}
119+
120+
func BenchmarkMacToString(b *testing.B) {
121+
mac, err := ParseMAC("CB:A9:87:65:43:21")
122+
if err != nil {
123+
b.Errorf("expected nil but got %v", err)
124+
}
125+
for i := 0; i < b.N; i++ {
126+
_ = mac.String()
127+
}
128+
}
129+
130+
func TestMAC_UnmarshalBinary(t *testing.T) {
131+
tests := []struct {
132+
name string
133+
mac []byte
134+
wantMac MAC
135+
wantErr bool
136+
}{
137+
{
138+
name: "incrementing",
139+
mac: []byte{188, 154, 120, 86, 52, 18},
140+
wantMac: [6]byte{188, 154, 120, 86, 52, 18},
141+
wantErr: false,
142+
},
143+
{
144+
name: "decrementing",
145+
mac: []byte{33, 67, 101, 135, 169, 203},
146+
wantMac: [6]byte{33, 67, 101, 135, 169, 203},
147+
wantErr: false,
148+
},
149+
{
150+
name: "normal",
151+
mac: []byte{204, 187, 170, 51, 34, 17},
152+
wantMac: [6]byte{204, 187, 170, 51, 34, 17},
153+
wantErr: false,
154+
},
155+
{
156+
name: "empty",
157+
mac: []byte{},
158+
wantMac: [6]byte{},
159+
wantErr: true,
160+
},
161+
{
162+
name: "extra",
163+
mac: []byte{33, 67, 101, 135, 169, 203, 255},
164+
wantMac: [6]byte{},
165+
wantErr: true,
166+
},
167+
}
168+
for _, tt := range tests {
169+
t.Run(tt.name, func(t *testing.T) {
170+
gotMac := MAC{}
171+
gotMac.UnmarshalBinary(tt.mac)
172+
173+
if !tt.wantErr && !bytes.Equal(gotMac[:], tt.wantMac[:]) {
174+
t.Errorf("ParseMAC() = %v, want %v", [6]byte(gotMac), [6]byte(tt.wantMac))
175+
}
176+
})
177+
}
178+
}
179+
180+
func TestMAC_MarshalBinary(t *testing.T) {
181+
tests := []struct {
182+
name string
183+
mac MAC
184+
wantData []byte
185+
wantErr bool
186+
}{
187+
{
188+
name: "incrementing",
189+
mac: [6]byte{188, 154, 120, 86, 52, 18},
190+
wantData: []byte{188, 154, 120, 86, 52, 18},
191+
wantErr: false,
192+
},
193+
{
194+
name: "decrementing",
195+
mac: [6]byte{33, 67, 101, 135, 169, 203},
196+
wantData: []byte{33, 67, 101, 135, 169, 203},
197+
wantErr: false,
198+
},
199+
{
200+
name: "normal",
201+
mac: [6]byte{204, 187, 170, 51, 34, 17},
202+
wantData: []byte{204, 187, 170, 51, 34, 17},
203+
wantErr: false,
204+
},
205+
}
206+
for _, tt := range tests {
207+
t.Run(tt.name, func(t *testing.T) {
208+
gotData, err := tt.mac.MarshalBinary()
209+
if (err != nil) != tt.wantErr {
210+
t.Errorf("MAC.MarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
211+
return
212+
}
213+
if !tt.wantErr && !bytes.Equal(gotData, tt.wantData) {
214+
t.Errorf("MAC.MarshalBinary() = %v, want %v", gotData, tt.wantData)
215+
}
216+
})
217+
}
218+
}
219+
220+
func TestMAC_MarshalUnmarshalBinary(t *testing.T) {
221+
tests := []struct {
222+
name string
223+
mac MAC
224+
wantMac MAC
225+
wantErr bool
226+
}{
227+
{
228+
name: "incrementing",
229+
mac: [6]byte{188, 154, 120, 86, 52, 18},
230+
wantMac: [6]byte{188, 154, 120, 86, 52, 18},
231+
wantErr: false,
232+
},
233+
{
234+
name: "decrementing",
235+
mac: [6]byte{33, 67, 101, 135, 169, 203},
236+
wantMac: [6]byte{33, 67, 101, 135, 169, 203},
237+
wantErr: false,
238+
},
239+
{
240+
name: "normal",
241+
mac: [6]byte{204, 187, 170, 51, 34, 17},
242+
wantMac: [6]byte{204, 187, 170, 51, 34, 17},
243+
wantErr: false,
244+
},
245+
}
246+
for _, tt := range tests {
247+
t.Run(tt.name, func(t *testing.T) {
248+
b, err := tt.mac.MarshalBinary()
249+
if (err != nil) != tt.wantErr {
250+
t.Errorf("MAC.MarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
251+
return
252+
}
253+
254+
gotMac := &MAC{}
255+
256+
err = gotMac.UnmarshalBinary(b)
257+
if (err != nil) != tt.wantErr {
258+
t.Errorf("MAC.UnmarshalBinary() error = %v, wantErr %v", err, tt.wantErr)
259+
return
260+
}
261+
262+
if !tt.wantErr && !bytes.Equal(gotMac[:], tt.wantMac[:]) {
263+
t.Errorf("MAC.MarshalBinary() = %v, want %v", gotMac, tt.wantMac)
264+
}
265+
})
266+
}
267+
}

0 commit comments

Comments
 (0)