diff --git a/pkg/guid/guid.go b/pkg/guid/guid.go index 3c112d2..d0595f6 100644 --- a/pkg/guid/guid.go +++ b/pkg/guid/guid.go @@ -1,19 +1,42 @@ +// 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 ( "crypto/rand" + "encoding" "encoding/binary" - "encoding/json" "fmt" "strconv" - "strings" - "github.com/pkg/errors" "golang.org/x/sys/windows" ) -var _ = (json.Marshaler)(GUID{}) -var _ = (json.Unmarshaler)(&GUID{}) +// 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 _ = (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 @@ -29,15 +52,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 { @@ -55,36 +113,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) } @@ -92,16 +150,35 @@ func FromString(s string) (GUID, error) { return g, nil } -// 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()) +// 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 } -// 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), "\"")) +// Version returns the GUID version, as defined in RFC 4122. +func (g GUID) Version() Version { + return Version((g.Data3 & 0xF000) >> 12) +} + +// MarshalText returns the textual representation of the GUID. +func (g GUID) MarshalText() ([]byte, error) { + return []byte(g.String()), nil +} + +// 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 } diff --git a/pkg/guid/guid_test.go b/pkg/guid/guid_test.go index 9bf37e6..b229cc6 100644 --- a/pkg/guid/guid_test.go +++ b/pkg/guid/guid_test.go @@ -6,26 +6,77 @@ 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 := mustNewV4(t) + 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_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, 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) @@ -33,10 +84,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) @@ -51,10 +99,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 { @@ -67,10 +112,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) @@ -88,10 +130,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 {