From 1197d54849bb52c4c9a3f802131a1cc77b4cc78d Mon Sep 17 00:00:00 2001 From: Kevin Parsons Date: Mon, 13 May 2019 14:22:42 -0700 Subject: [PATCH 1/5] pkg/guid: Add variant and version support Signed-off-by: Kevin Parsons --- pkg/guid/guid.go | 40 ++++++++++++++++++++++++++++++++++++++++ pkg/guid/guid_test.go | 13 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index 3c112d2..8f58b46 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -12,6 +12,26 @@ import ( "golang.org/x/sys/windows" ) +// Variant specifies which GUID variant (or "type") of the GUID. It determines +// how the entirety of the rest of the GUID is interpreted. +type Variant uint8 + +// The variants specified by RFC 4122. +const ( + // VariantUnknown specifies a GUID variant which does not conform to one of + // the variant encodings specified in RFC 4122. + VariantUnknown Variant = iota + VariantNCS + VariantRFC4122 + VariantMicrosoft + VariantFuture +) + +// Version specifies how the bits in the GUID were generated. For instance, a +// version 4 GUID is randomly generated, and a version 5 is generated from the +// hash of an input string. +type Version uint8 + var _ = (json.Marshaler)(GUID{}) var _ = (json.Unmarshaler)(&GUID{}) @@ -92,6 +112,26 @@ func FromString(s string) (GUID, error) { return g, nil } +// Variant returns the GUID variant, as defined in RFC 4122. +func (g GUID) Variant() Variant { + b := g.Data4[0] + if b&0x80 == 0 { + return VariantNCS + } else if b&0xc0 == 0x80 { + return VariantRFC4122 + } else if b&0xe0 == 0xc0 { + return VariantMicrosoft + } else if b&0xe0 == 0xe0 { + return VariantFuture + } + return VariantUnknown +} + +// Version returns the GUID version, as defined in RFC 4122. +func (g GUID) Version() Version { + return Version((g.Data3 & 0xF000) >> 12) +} + // MarshalJSON marshals the GUID to JSON representation and returns it as a // slice of bytes. func (g GUID) MarshalJSON() ([]byte, error) { diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go index 9bf37e6..c1e0261 100644 --- a/pkg/guid/guid_test.go +++ b/pkg/guid/guid_test.go @@ -20,6 +20,19 @@ func Test_New(t *testing.T) { } } +func Test_V4HasCorrectVersionAndVariant(t *testing.T) { + g, err := NewV4() + if err != nil { + t.Fatal(err) + } + if g.Version() != 4 { + t.Fatalf("Version is not 4: %s", g) + } + if g.Variant() != VariantRFC4122 { + t.Fatalf("Variant is not RFC4122: %s", g) + } +} + func Test_FromString(t *testing.T) { orig := "8e35239e-2084-490e-a3db-ab18ee0744cb" g, err := FromString(orig) From 6dd84a2574b3164f2c00bf25bdad4f9fe5f6031f Mon Sep 17 00:00:00 2001 From: Kevin Parsons Date: Mon, 13 May 2019 14:25:53 -0700 Subject: [PATCH 2/5] pkg/guid: Add helper functions to tests Signed-off-by: Kevin Parsons --- pkg/guid/guid_test.go | 44 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go index c1e0261..77c06b8 100644 --- a/pkg/guid/guid_test.go +++ b/pkg/guid/guid_test.go @@ -6,25 +6,32 @@ import ( "testing" ) -func Test_New(t *testing.T) { +func mustNewV4(t *testing.T) GUID { g, err := NewV4() if err != nil { t.Fatal(err) } - g2, err := NewV4() + return g +} + +func mustFromString(t *testing.T, s string) GUID { + g, err := FromString(s) if err != nil { t.Fatal(err) } + return g +} + +func Test_NewV4IsUnique(t *testing.T) { + g := mustNewV4(t) + g2 := mustNewV4(t) if g == g2 { t.Fatalf("GUIDs are equal: %s, %s", g, g2) } } func Test_V4HasCorrectVersionAndVariant(t *testing.T) { - g, err := NewV4() - if err != nil { - t.Fatal(err) - } + g := mustNewV4(t) if g.Version() != 4 { t.Fatalf("Version is not 4: %s", g) } @@ -35,10 +42,7 @@ func Test_V4HasCorrectVersionAndVariant(t *testing.T) { func Test_FromString(t *testing.T) { orig := "8e35239e-2084-490e-a3db-ab18ee0744cb" - g, err := FromString(orig) - if err != nil { - t.Fatal(err) - } + g := mustFromString(t, orig) s := g.String() if orig != s { t.Fatalf("GUIDs not equal: %s, %s", orig, s) @@ -46,10 +50,7 @@ func Test_FromString(t *testing.T) { } func Test_MarshalJSON(t *testing.T) { - g, err := NewV4() - if err != nil { - t.Fatal(err) - } + g := mustNewV4(t) j, err := json.Marshal(g) if err != nil { t.Fatal(err) @@ -64,10 +65,7 @@ func Test_MarshalJSON_Nested(t *testing.T) { type test struct { G GUID } - g, err := NewV4() - if err != nil { - t.Fatal(err) - } + g := mustNewV4(t) t1 := test{g} j, err := json.Marshal(t1) if err != nil { @@ -80,10 +78,7 @@ func Test_MarshalJSON_Nested(t *testing.T) { } func Test_UnmarshalJSON(t *testing.T) { - g, err := NewV4() - if err != nil { - t.Fatal(err) - } + g := mustNewV4(t) j, err := json.Marshal(g) if err != nil { t.Fatal(err) @@ -101,10 +96,7 @@ func Test_UnmarshalJSON_Nested(t *testing.T) { type test struct { G GUID } - g, err := NewV4() - if err != nil { - t.Fatal(err) - } + g := mustNewV4(t) t1 := test{g} j, err := json.Marshal(t1) if err != nil { From 2dc6637e2c50c4537f0947928a85d5cfe4cbdc91 Mon Sep 17 00:00:00 2001 From: Kevin Parsons Date: Mon, 13 May 2019 14:28:49 -0700 Subject: [PATCH 3/5] pkg/guid: Support big-endian and Windows encodings Signed-off-by: Kevin Parsons --- pkg/guid/guid.go | 56 ++++++++++++++++++++++++++++++++++++------- pkg/guid/guid_test.go | 34 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index 8f58b46..f1add40 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -1,3 +1,8 @@ +// Package guid provides a GUID type. The backing structure for a GUID is +// identical to that used by the golang.org/x/sys/windows GUID type. +// There are two main binary encodings used for a GUID, the big-endian encoding, +// and the Windows (mixed-endian) encoding. See here for details: +// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding package guid import ( @@ -49,15 +54,50 @@ func NewV4() (GUID, error) { return GUID{}, err } - var g GUID - g.Data1 = binary.LittleEndian.Uint32(b[0:4]) - g.Data2 = binary.LittleEndian.Uint16(b[4:6]) - g.Data3 = binary.LittleEndian.Uint16(b[6:8]) - copy(g.Data4[:], b[8:16]) + b[6] = (b[6] & 0x0f) | 0x40 // Version 4 (randomly generated) + b[8] = (b[8] & 0x3f) | 0x80 // RFC4122 variant - g.Data3 = (g.Data3 & 0x0fff) | 0x4000 // Version 4 (randomly generated) - g.Data4[0] = (g.Data4[0] & 0x3f) | 0x80 // RFC4122 variant - return g, nil + return FromArray(b), nil +} + +func fromArray(b [16]byte, order binary.ByteOrder) GUID { + var g GUID + g.Data1 = order.Uint32(b[0:4]) + g.Data2 = order.Uint16(b[4:6]) + g.Data3 = order.Uint16(b[6:8]) + copy(g.Data4[:], b[8:16]) + return g +} + +func (g GUID) toArray(order binary.ByteOrder) [16]byte { + b := [16]byte{} + order.PutUint32(b[0:4], g.Data1) + order.PutUint16(b[4:6], g.Data2) + order.PutUint16(b[6:8], g.Data3) + copy(b[8:16], g.Data4[:]) + return b +} + +// FromArray constructs a GUID from a big-endian encoding array of 16 bytes. +func FromArray(b [16]byte) GUID { + return fromArray(b, binary.BigEndian) +} + +// ToArray returns an array of 16 bytes representing the GUID in big-endian +// encoding. +func (g GUID) ToArray() [16]byte { + return g.toArray(binary.BigEndian) +} + +// FromWindowsArray constructs a GUID from a Windows encoding array of bytes. +func FromWindowsArray(b [16]byte) GUID { + return fromArray(b, binary.LittleEndian) +} + +// ToWindowsArray returns an array of 16 bytes representing the GUID in Windows +// encoding. +func (g GUID) ToWindowsArray() [16]byte { + return g.toArray(binary.LittleEndian) } func (g GUID) String() string { diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go index 77c06b8..b229cc6 100644 --- a/pkg/guid/guid_test.go +++ b/pkg/guid/guid_test.go @@ -40,6 +40,40 @@ func Test_V4HasCorrectVersionAndVariant(t *testing.T) { } } +func Test_ToArray(t *testing.T) { + g := mustFromString(t, "73c39589-192e-4c64-9acf-6c5d0aa18528") + b := g.ToArray() + expected := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} + if b != expected { + t.Fatalf("GUID does not match array form: %x, %x", expected, b) + } +} + +func Test_FromArrayAndBack(t *testing.T) { + b := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} + b2 := FromArray(b).ToArray() + if b != b2 { + t.Fatalf("Arrays do not match: %x, %x", b, b2) + } +} + +func Test_ToWindowsArray(t *testing.T) { + g := mustFromString(t, "73c39589-192e-4c64-9acf-6c5d0aa18528") + b := g.ToWindowsArray() + expected := [16]byte{0x89, 0x95, 0xc3, 0x73, 0x2e, 0x19, 0x64, 0x4c, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} + if b != expected { + t.Fatalf("GUID does not match array form: %x, %x", expected, b) + } +} + +func Test_FromWindowsArrayAndBack(t *testing.T) { + b := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} + b2 := FromWindowsArray(b).ToWindowsArray() + if b != b2 { + t.Fatalf("Arrays do not match: %x, %x", b, b2) + } +} + func Test_FromString(t *testing.T) { orig := "8e35239e-2084-490e-a3db-ab18ee0744cb" g := mustFromString(t, orig) From b940a6f7aee39d956b8eb7e1d9a2da4232c1e79b Mon Sep 17 00:00:00 2001 From: Kevin Parsons Date: Mon, 13 May 2019 14:30:09 -0700 Subject: [PATCH 4/5] pkg/guid: Improve error messages Signed-off-by: Kevin Parsons --- pkg/guid/guid.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index f1add40..58895dd 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - "github.com/pkg/errors" "golang.org/x/sys/windows" ) @@ -115,36 +114,36 @@ func (g GUID) String() string { // format. func FromString(s string) (GUID, error) { if len(s) != 36 { - return GUID{}, errors.New("invalid GUID format (length)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { - return GUID{}, errors.New("invalid GUID format (dashes)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } var g GUID data1, err := strconv.ParseUint(s[0:8], 16, 32) if err != nil { - return GUID{}, errors.Wrap(err, "invalid GUID format (Data1)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data1 = uint32(data1) data2, err := strconv.ParseUint(s[9:13], 16, 16) if err != nil { - return GUID{}, errors.Wrap(err, "invalid GUID format (Data2)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data2 = uint16(data2) data3, err := strconv.ParseUint(s[14:18], 16, 16) if err != nil { - return GUID{}, errors.Wrap(err, "invalid GUID format (Data3)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data3 = uint16(data3) for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} { v, err := strconv.ParseUint(s[x:x+2], 16, 8) if err != nil { - return GUID{}, errors.Wrap(err, "invalid GUID format (Data4)") + return GUID{}, fmt.Errorf("invalid GUID %q", s) } g.Data4[i] = uint8(v) } From 3dac69f6eaf77472d472dd9347db16a624ce7c4b Mon Sep 17 00:00:00 2001 From: Kevin Parsons Date: Mon, 13 May 2019 14:31:21 -0700 Subject: [PATCH 5/5] pkg/guid: Switch from json.Marshaler to encoding.TextMarshaler Signed-off-by: Kevin Parsons --- pkg/guid/guid.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index 58895dd..d0595f6 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -7,11 +7,10 @@ package guid import ( "crypto/rand" + "encoding" "encoding/binary" - "encoding/json" "fmt" "strconv" - "strings" "golang.org/x/sys/windows" ) @@ -36,8 +35,8 @@ const ( // hash of an input string. type Version uint8 -var _ = (json.Marshaler)(GUID{}) -var _ = (json.Unmarshaler)(&GUID{}) +var _ = (encoding.TextMarshaler)(GUID{}) +var _ = (encoding.TextUnmarshaler)(&GUID{}) // GUID represents a GUID/UUID. It has the same structure as // golang.org/x/sys/windows.GUID so that it can be used with functions expecting @@ -171,16 +170,15 @@ func (g GUID) Version() Version { return Version((g.Data3 & 0xF000) >> 12) } -// MarshalJSON marshals the GUID to JSON representation and returns it as a -// slice of bytes. -func (g GUID) MarshalJSON() ([]byte, error) { - return json.Marshal(g.String()) +// MarshalText returns the textual representation of the GUID. +func (g GUID) MarshalText() ([]byte, error) { + return []byte(g.String()), nil } -// UnmarshalJSON unmarshals a GUID from JSON representation and sets itself to -// the unmarshaled GUID. -func (g *GUID) UnmarshalJSON(data []byte) error { - g2, err := FromString(strings.Trim(string(data), "\"")) +// UnmarshalText takes the textual representation of a GUID, and unmarhals it +// into this GUID. +func (g *GUID) UnmarshalText(text []byte) error { + g2, err := FromString(string(text)) if err != nil { return err }