Skip to content

Commit

Permalink
Change FromHex() to more powerful Parse() (#4)
Browse files Browse the repository at this point in the history
* Migrate code and adopt new package name

* Change FromHex() to more powerful Parse()

* Adopt changes in CHANGELOG
  • Loading branch information
themue authored Aug 30, 2021
1 parent 1057d92 commit 536762e
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 42 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

## v0.1.0
## v0.1.1 (2021-08-30)

* Migrated UUID code from former DSA Identifier package.
* Renamed FromHex() to Parse() and improved it

## v0.1.0 (2021-08-28)

* Migrated UUID code from former DSA Identifier package
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module tideland.dev/go/uuid

go 1.16
go 1.17

require tideland.dev/go/audit v0.4.0
120 changes: 91 additions & 29 deletions uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,33 @@ import (
"encoding/hex"
"fmt"
"net"
"strings"
"time"
)

//--------------------
// UUID
//--------------------

// Version represents a UUID's version.
type Version byte

// UUID versions and variants.
const (
V1 byte = 1
V3 byte = 3
V4 byte = 4
V5 byte = 5

VariantNCS byte = 0
VariantRFC4122 byte = 4
VariantMicrosoft byte = 6
VariantFuture byte = 7
V1 Version = 1
V3 Version = 3
V4 Version = 4
V5 Version = 5
)

// Variant represents a UUID's variant.
type Variant byte

const (
VariantNCS Variant = 0 // Reserved, NCS backward compatibility.
VariantRFC4122 Variant = 4 // The variant specified in RFC4122.
VariantMicrosoft Variant = 6 // Reserved, Microsoft Corporation backward compatibility.
VariantFuture Variant = 7 // Reserved for future definition.
)

// UUID represents a universal identifier with 16 bytes.
Expand Down Expand Up @@ -131,29 +140,51 @@ func NewV5(ns UUID, name []byte) (UUID, error) {
return uuid, nil
}

// FromHex creates a UUID based on the passed hex string which has to
// have the length of 32 bytes.
func FromHex(source string) (UUID, error) {
uuid := UUID{}
if len([]byte(source)) != 32 {
return uuid, fmt.Errorf("source length is not 32")
// Parse creates a UUID based on the given hex string which has to have
// one of the following formats:
//
// - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
// - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
//
// The net data always has to have the length of 32 bytes.
func Parse(source string) (UUID, error) {
var uuid UUID
var hexSource string
var err error
switch len(source) {
case 36:
hexSource, err = parseSource(source, "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
case 36 + 9:
hexSource, err = parseSource(source, "urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
case 36 + 2:
hexSource, err = parseSource(source, "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}")
case 32:
hexSource, err = parseSource(source, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
default:
return uuid, fmt.Errorf("invalid source format: %q", source)
}
if err != nil {
return uuid, err
}
raw, err := hex.DecodeString(source)
hexData, err := hex.DecodeString(hexSource)
if err != nil {
return uuid, fmt.Errorf("source is no hex value: %w", err)
}
copy(uuid[:], raw)
copy(uuid[:], hexData)
// TODO: Validate UUID (version, variant).
return uuid, nil
}

// Version returns the version number of the UUID algorithm.
func (uuid UUID) Version() byte {
return uuid[6] & 0xf0 >> 4
func (uuid UUID) Version() Version {
return Version(uuid[6] & 0xf0 >> 4)
}

// Variant returns the variant of the UUID.
func (uuid UUID) Variant() byte {
return uuid[8] & 0xe0 >> 5
func (uuid UUID) Variant() Variant {
return Variant(uuid[8] & 0xe0 >> 5)
}

// Copy returns a copy of the UUID.
Expand Down Expand Up @@ -190,43 +221,74 @@ func (uuid UUID) String() string {
}

// setVersion sets the version part of the UUID.
func (uuid *UUID) setVersion(v byte) {
uuid[6] = (uuid[6] & 0x0f) | (v << 4)
func (uuid *UUID) setVersion(v Version) {
uuid[6] = (uuid[6] & 0x0f) | (byte(v) << 4)
}

// setVariant sets the variant part of the UUID.
func (uuid *UUID) setVariant(v byte) {
uuid[8] = (uuid[8] & 0x1f) | (v << 5)
func (uuid *UUID) setVariant(v Variant) {
uuid[8] = (uuid[8] & 0x1f) | (byte(v) << 5)
}

// NamespaceDNS returns the DNS namespace UUID for a v3 or a v5.
func NamespaceDNS() UUID {
uuid, _ := FromHex("6ba7b8109dad11d180b400c04fd430c8")
uuid, _ := Parse("6ba7b8109dad11d180b400c04fd430c8")
return uuid
}

// NamespaceURL returns the URL namespace UUID for a v3 or a v5.
func NamespaceURL() UUID {
uuid, _ := FromHex("6ba7b8119dad11d180b400c04fd430c8")
uuid, _ := Parse("6ba7b8119dad11d180b400c04fd430c8")
return uuid
}

// NamespaceOID returns the OID namespace UUID for a v3 or a v5.
func NamespaceOID() UUID {
uuid, _ := FromHex("6ba7b8129dad11d180b400c04fd430c8")
uuid, _ := Parse("6ba7b8129dad11d180b400c04fd430c8")
return uuid
}

// NamespaceX500 returns the X.500 namespace UUID for a v3 or a v5.
func NamespaceX500() UUID {
uuid, _ := FromHex("6ba7b8149dad11d180b400c04fd430c8")
uuid, _ := Parse("6ba7b8149dad11d180b400c04fd430c8")
return uuid
}

//--------------------
// PRIVATE HELPERS
//--------------------

// parseSource parses a source based on the given pattern. Only the
// char x of the pattern is interpreted as hex char. If the result is
// longer than 32 bytes it's an error.
func parseSource(source, pattern string) (string, error) {
lower := []byte(strings.ToLower(source))
raw := make([]byte, 32)
rawPos := 0
patternPos := 0
patternLen := len(pattern)
for i, b := range lower {
if patternPos == patternLen {
return "", fmt.Errorf("source %q too long for pattern %q", source, pattern)
}
switch pattern[patternPos] {
case 'x':
if (b < '0' || b > '9') && (b < 'a' || b > 'f') {
return "", fmt.Errorf("source char %d is no hex char: %c", i, b)
}
raw[rawPos] = b
rawPos++
patternPos++
default:
if b != pattern[patternPos] {
return "", fmt.Errorf("source char %d does not match pattern: %x is not %c", i, b, pattern[patternPos])
}
patternPos++
}
}
return string(raw), nil
}

// macAddress retrieves the MAC address of the computer.
func macAddress() []byte {
address := [6]byte{}
Expand Down
60 changes: 50 additions & 10 deletions uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ func TestStandard(t *testing.T) {
func TestVersions(t *testing.T) {
assert := asserts.NewTesting(t, asserts.FailStop)
ns := uuid.NamespaceOID()
name := []byte{1, 3, 3, 7}
// Asserts.
uuidV1, err := uuid.NewV1()
assert.Nil(err)
assert.Equal(uuidV1.Version(), uuid.V1)
assert.Equal(uuidV1.Variant(), uuid.VariantRFC4122)
assert.Logf("UUID V1: %v", uuidV1)
uuidV3, err := uuid.NewV3(ns, []byte{4, 7, 1, 1})
uuidV3, err := uuid.NewV3(ns, name)
assert.Nil(err)
assert.Equal(uuidV3.Version(), uuid.V3)
assert.Equal(uuidV3.Variant(), uuid.VariantRFC4122)
Expand All @@ -62,23 +63,62 @@ func TestVersions(t *testing.T) {
assert.Equal(uuidV4.Version(), uuid.V4)
assert.Equal(uuidV4.Variant(), uuid.VariantRFC4122)
assert.Logf("UUID V4: %v", uuidV4)
uuidV5, err := uuid.NewV5(ns, []byte{4, 7, 1, 1})
uuidV5, err := uuid.NewV5(ns, name)
assert.Nil(err)
assert.Equal(uuidV5.Version(), uuid.V5)
assert.Equal(uuidV5.Variant(), uuid.VariantRFC4122)
assert.Logf("UUID V5: %v", uuidV5)
}

// TestFromHex tests creating UUIDs from hex strings.
func TestFromHex(t *testing.T) {
// TestParse tests creating UUIDs from different string representations.
func TestParse(t *testing.T) {
assert := asserts.NewTesting(t, asserts.FailStop)
ns := uuid.NamespaceOID()
name := []byte{1, 3, 3, 7}
// Asserts.
_, err := uuid.FromHex("ffff")
assert.ErrorMatch(err, `source length is not 32`)
_, err = uuid.FromHex("012345678901234567890123456789zz")
assert.ErrorMatch(err, `source is no hex value: .*`)
_, err = uuid.FromHex("012345678901234567890123456789ab")
assert.Nil(err)
tests := []struct {
source func() string
version uuid.Version
variant uuid.Variant
err string
}{
{func() string { u, _ := uuid.NewV1(); return u.String() }, uuid.V1, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV3(ns, name); return u.String() }, uuid.V3, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV4(); return u.String() }, uuid.V4, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV5(ns, name); return u.String() }, uuid.V5, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV1(); return "urn:uuid:" + u.String() }, uuid.V1, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV3(ns, name); return "urn:uuid:" + u.String() }, uuid.V3, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV4(); return "urn:uuid:" + u.String() }, uuid.V4, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV5(ns, name); return "urn:uuid:" + u.String() }, uuid.V5, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV1(); return "{" + u.String() + "}" }, uuid.V1, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV3(ns, name); return "{" + u.String() + "}" }, uuid.V3, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV4(); return "{" + u.String() + "}" }, uuid.V4, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV5(ns, name); return "{" + u.String() + "}" }, uuid.V5, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV1(); return u.ShortString() }, uuid.V1, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV3(ns, name); return u.ShortString() }, uuid.V3, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV4(); return u.ShortString() }, uuid.V4, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV5(ns, name); return u.ShortString() }, uuid.V5, uuid.VariantRFC4122, ""},
{func() string { u, _ := uuid.NewV4(); return u.String() + "-ffaabb" }, 0, 0, "invalid source format"},
{func() string { u, _ := uuid.NewV4(); return u.String() + "-ffxxyy" }, 0, 0, "invalid source format"},
{func() string { u, _ := uuid.NewV4(); return "uuid:" + u.String() }, 0, 0, "invalid source format"},
{func() string { u, _ := uuid.NewV4(); return "{" + u.ShortString() + "}" }, 0, 0, "invalid source format"},
{func() string { return "ababababababababab" }, 0, 0, "invalid source format"},
{func() string { return "abcdefabcdefZZZZefabcdefabcdefab" }, 0, 0, "source char 12 is no hex char"},
{func() string { return "[abcdefabcdefabcdefabcdefabcdefab]" }, 0, 0, "invalid source format"},
{func() string { return "abcdefab=cdef=abcd=efab=cdefabcdefab" }, 0, 0, "source char 8 does not match pattern"},
}
for i, test := range tests {
source := test.source()
assert.Logf("test #%d source %s", i, source)
uuidT, err := uuid.Parse(source)
if test.err == "" {
assert.NoError(err)
assert.Equal(uuidT.Version(), test.version)
assert.Equal(uuidT.Variant(), test.variant)
} else {
assert.ErrorContains(err, test.err)
}
}
}

// EOF

0 comments on commit 536762e

Please sign in to comment.