Skip to content

Commit

Permalink
UUIDs: implement encoding methods, enhance testing and improve perfor…
Browse files Browse the repository at this point in the history
…mance

This changeset implements these interfaces found in the encoding package:
 - MarshalBinary() (data []byte, err error)
 - UnmarshalBinary(data []byte) error
 - AppendBinary(b []byte) ([]byte, error)
 - MarshalText() (text []byte, err error)
 - UnmarshalText(text []byte) error
 - AppendText(b []byte) ([]byte, error)

ParseUUID and uuid.String are now implemented in terms of these new
methods.  UnmarshalText and AppendText use algorithms inspired by
the hex package. Also the UUID.String method has been optimized
to avoid allocations by using unsafe.String.
Finally the new UnmarshalText supports 128, 32 and 16 bit uuids.

Additional tests were added to verify the new methods and some existing methods
missing coverage.

Benchmark results (using benchstat, 10 runs each):
 - `ParseUUID`:  ~71.04% faster (p=0.000, n=10)
 - `UUID.String()`: ~81.26% faster (p=0.000, n=10)
 - `UUID.String()`: Memory allocations reduced by 100% (48 B/op to 0 B/op)
  • Loading branch information
onshorechet committed Feb 23, 2025
1 parent 680584e commit dd26313
Show file tree
Hide file tree
Showing 2 changed files with 509 additions and 54 deletions.
340 changes: 286 additions & 54 deletions uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package bluetooth

import (
"errors"
"strings"
"unsafe"
)

// UUID is a single UUID as used in the Bluetooth stack. It is represented as a
Expand Down Expand Up @@ -112,75 +112,307 @@ func (uuid UUID) Bytes() [16]byte {
return buf
}

// ParseUUID parses the given UUID, which must be in
// 00001234-0000-1000-8000-00805f9b34fb format. This means that it cannot (yet)
// parse 16-bit UUIDs unless they are serialized as a 128-bit UUID. If the UUID
// cannot be parsed, an error is returned. It will always successfully parse
// UUIDs generated by UUID.String().
// AppendBinary appends the bytes of the uuid to the given byte slice b.
func (uuid UUID) AppendBinary(b []byte) ([]byte, error) {
return append(b,
byte(uuid[0]),
byte(uuid[0]>>8),
byte(uuid[0]>>16),
byte(uuid[0]>>24),
byte(uuid[1]),
byte(uuid[1]>>8),
byte(uuid[1]>>16),
byte(uuid[1]>>24),
byte(uuid[2]),
byte(uuid[2]>>8),
byte(uuid[2]>>16),
byte(uuid[2]>>24),
byte(uuid[3]),
byte(uuid[3]>>8),
byte(uuid[3]>>16),
byte(uuid[3]>>24),
), nil
}

// MarshalBinary marshals the uuid into and byte slice and returns the slice. It will not return an error
func (uuid UUID) MarshalBinary() (data []byte, err error) {
return uuid.AppendBinary(make([]byte, 0, 16))
}

// ParseUUID parses the given UUID
//
// Expected formats:
//
// 00001234-0000-1000-8000-00805f9b34fb
// 00001234
// 1234
//
// If the UUID cannot be parsed, an error is returned.
// It will always successfully parse UUIDs generated by UUID.String().
func ParseUUID(s string) (uuid UUID, err error) {
uuidIndex := 0
for i := 0; i < len(s); i++ {
c := s[i]
if c == '-' {
continue
err = (&uuid).UnmarshalText([]byte(s))
return
}

// UnmarshalText unmarshals a text representation of a UUID.
//
// Expected formats:
//
// 00001234-0000-1000-8000-00805f9b34fb
// 00001234
// 1234
//
// If the UUID cannot be parsed, an error is returned.
// It will always successfully parse UUIDs generated by UUID.String().
// This method is an adaptation of hex.Decode idea of using a reverse hex table.
func (u *UUID) UnmarshalText(s []byte) error {
switch len(s) {
case 36:
return u.unmarshalText128(s)
case 8:
return u.unmarshalText32(s)
case 4:
return u.unmarshalText16(s)
default:
return errInvalidUUID
}
}

// Using the reverseHexTable rebuild the UUID from the string s represented in bytes
// This implementation is the inverse of MarshalText and reaches performance pairity
func (u *UUID) unmarshalText128(s []byte) error {
var j uint8
for i := 3; i >= 0; i-- {
// Skip hyphens
if s[j] == '-' {
j++
}

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[i] |= uint32(reverseHexTable[s[j]]) << 28
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[i] |= uint32(reverseHexTable[s[j]]) << 24
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[i] |= uint32(reverseHexTable[s[j]]) << 20
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
var nibble byte
if c >= '0' && c <= '9' {
nibble = c - '0' + 0x0
} else if c >= 'a' && c <= 'f' {
nibble = c - 'a' + 0xa
} else if c >= 'A' && c <= 'F' {
nibble = c - 'A' + 0xa
} else {
err = errInvalidUUID
return
u[i] |= uint32(reverseHexTable[s[j]]) << 16
j++

// skip hypens
if s[j] == '-' {
j++
}

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
if uuidIndex > 31 {
err = errInvalidUUID
return
u[i] |= uint32(reverseHexTable[s[j]]) << 12
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[i] |= uint32(reverseHexTable[s[j]]) << 8
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[i] |= uint32(reverseHexTable[s[j]]) << 4
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
uuid[3-uuidIndex/8] |= uint32(nibble) << (4 * (7 - uuidIndex%8))
uuidIndex++
u[i] |= uint32(reverseHexTable[s[j]])
j++
}
if uuidIndex != 32 {
// The UUID doesn't have exactly 32 nibbles. Perhaps a 16-bit or 32-bit
// UUID?
err = errInvalidUUID

return nil
}

// Using the reverseHexTable rebuild the UUID from the string s represented in bytes
// This implementation is the inverse of MarshalText and reaches performance pairity
func (u *UUID) unmarshalText32(s []byte) error {
u[0] = 0x5F9B34FB
u[1] = 0x80000080
u[2] = 0x00001000

var j uint8 = 0

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
return
u[3] |= uint32(reverseHexTable[s[j]]) << 28
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 24
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 20
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 16
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 12
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 8
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 4
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]])
j++

return nil
}

// Using the reverseHexTable rebuild the UUID from the string s represented in bytes
// This implementation is the inverse of MarshalText and reaches performance pairity
func (u *UUID) unmarshalText16(s []byte) error {
u[0] = 0x5F9B34FB
u[1] = 0x80000080
u[2] = 0x00001000

var j uint8 = 0
if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 12
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 8
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]]) << 4
j++

if reverseHexTable[s[j]] == 255 {
return errInvalidUUID
}
u[3] |= uint32(reverseHexTable[s[j]])
j++

return nil
}

var reverseHexTable = [256]uint8{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}

// String returns a human-readable version of this UUID, such as
// 00001234-0000-1000-8000-00805f9b34fb.
func (uuid UUID) String() string {
var s strings.Builder
s.Grow(36)
raw := uuid.Bytes()
for i := range raw {
func (u UUID) String() string {
buf, _ := u.AppendText(make([]byte, 0, 36))

// pulled from the guts of string builder
return unsafe.String(unsafe.SliceData(buf), 36)
}

const hexDigitLower = "0123456789abcdef"

// AppendText converts and appends the uuid onto the given byte slice
// representing a human-readable version of this UUID, such as
// 00001234-0000-1000-8000-00805f9b34fb.
func (u UUID) AppendText(buf []byte) ([]byte, error) {
for i := 3; i >= 0; i-- {
// Insert a hyphen at the correct locations.
if i == 4 || i == 6 || i == 8 || i == 10 {
s.WriteRune('-')
// position 4 and 8
if i != 3 && i != 0 {
buf = append(buf, '-')
}

// The character to convert to hex.
c := raw[15-i]
buf = append(buf, hexDigitLower[byte(u[i]>>24)>>4])
buf = append(buf, hexDigitLower[byte(u[i]>>24)&0xF])

// First nibble.
nibble := c >> 4
if nibble <= 9 {
s.WriteByte(nibble + '0')
} else {
s.WriteByte(nibble + 'a' - 10)
}
buf = append(buf, hexDigitLower[byte(u[i]>>16)>>4])
buf = append(buf, hexDigitLower[byte(u[i]>>16)&0xF])

// Second nibble.
nibble = c & 0x0f
if nibble <= 9 {
s.WriteByte(nibble + '0')
} else {
s.WriteByte(nibble + 'a' - 10)
// Insert a hyphen at the correct locations.
// position 6 and 10
if i == 2 || i == 1 {
buf = append(buf, '-')
}

buf = append(buf, hexDigitLower[byte(u[i]>>8)>>4])
buf = append(buf, hexDigitLower[byte(u[i]>>8)&0xF])

buf = append(buf, hexDigitLower[byte(u[i])>>4])
buf = append(buf, hexDigitLower[byte(u[i])&0xF])
}

return buf, nil
}

// MarshalText returns the converted uuid as a bytle slice
// representing a human-readable version, such as
// 00001234-0000-1000-8000-00805f9b34fb.
func (u UUID) MarshalText() ([]byte, error) {
return u.AppendText(make([]byte, 0, 36))
}

var ErrInvalidBinaryUUID = errors.New("bluetooth: failed to unmarshal the given binary UUID")

// UnmarshalBinary copies the given uuid bytes onto itself
func (u *UUID) UnmarshalBinary(uuid []byte) error {
if len(uuid) != 16 {
return ErrInvalidBinaryUUID
}

return s.String()
u[0] = uint32(uuid[0]) | uint32(uuid[1])<<8 | uint32(uuid[2])<<16 | uint32(uuid[3])<<24
u[1] = uint32(uuid[4]) | uint32(uuid[5])<<8 | uint32(uuid[6])<<16 | uint32(uuid[7])<<24
u[2] = uint32(uuid[8]) | uint32(uuid[9])<<8 | uint32(uuid[10])<<16 | uint32(uuid[11])<<24
u[3] = uint32(uuid[12]) | uint32(uuid[13])<<8 | uint32(uuid[14])<<16 | uint32(uuid[15])<<24
return nil
}
Loading

0 comments on commit dd26313

Please sign in to comment.