From 825a3c18fbc8e0497eafea9254baadb2951f23c6 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Tue, 16 Mar 2021 02:14:56 +0000 Subject: [PATCH] feat(strings): add UUID function to generate random RFC 4122 UUID v4 strings --- strings.go | 27 +++++++++++++++++++++++++++ strings_example_test.go | 5 +++++ strings_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/strings.go b/strings.go index ddea864..f3152d3 100644 --- a/strings.go +++ b/strings.go @@ -18,6 +18,7 @@ const ( alphabeticChars = upperChars + lowerChars alphanumericChars = alphabeticChars + numericChars dnsLabelChars = lowerNumericChars + "-" + uuidHyphen = byte('-') ) var ( @@ -233,6 +234,32 @@ func DNSLabel(n int) (string, error) { } } +// UUID returns a random UUID v4 in string format as defined by RFC 4122, +// section 4.4. +func UUID() (string, error) { + b, err := Bytes(16) + if err != nil { + return "", err + } + + b[6] = (b[6] & 0x0f) | 0x40 // Version: 4 (random) + b[8] = (b[8] & 0x3f) | 0x80 // Variant: RFC 4122 + + // Construct a UUID v4 string according to RFC 4122 specifications. + dst := make([]byte, 36) + hex.Encode(dst[0:8], b[0:4]) // time-low + dst[8] = uuidHyphen + hex.Encode(dst[9:13], b[4:6]) // time-mid + dst[13] = uuidHyphen + hex.Encode(dst[14:18], b[6:8]) // time-high-and-version + dst[18] = uuidHyphen + hex.Encode(dst[19:23], b[8:10]) // clock-seq-and-reserved, clock-seq-low + dst[23] = uuidHyphen + hex.Encode(dst[24:], b[10:]) // node + + return string(dst), nil +} + func isASCII(s string) bool { for _, c := range s { if c > unicode.MaxASCII { diff --git a/strings_example_test.go b/strings_example_test.go index 9ca4380..e59a97a 100644 --- a/strings_example_test.go +++ b/strings_example_test.go @@ -70,3 +70,8 @@ func ExampleDNSLabel() { s, _ := rands.DNSLabel(16) fmt.Println(s) // => z0ij9o8qkbs0ru-h } + +func ExampleUUID() { + s, _ := rands.UUID() + fmt.Println(s) // => a62b8712-f238-43ba-a47e-333f5fffe785 +} diff --git a/strings_test.go b/strings_test.go index 03b5c31..1df898e 100644 --- a/strings_test.go +++ b/strings_test.go @@ -2,6 +2,7 @@ package rands import ( "encoding/base64" + "encoding/hex" "errors" "regexp" "strings" @@ -579,6 +580,34 @@ func BenchmarkDNSLabel(b *testing.B) { } } +func TestUUID(t *testing.T) { + m := regexp.MustCompile( + `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`, + ) + + for i := 0; i < 10000; i++ { + got, err := UUID() + require.NoError(t, err) + require.Regexp(t, m, got) + + raw := strings.ReplaceAll(got, "-", "") + b := make([]byte, 16) + _, err = hex.Decode(b, []byte(raw)) + require.NoError(t, err) + + require.Equal(t, 4, int(b[6]>>4), "version is not 4") + require.Equal(t, byte(0x80), b[8]&0xc0, + "variant is not RFC 4122", + ) + } +} + +func BenchmarkUUID(b *testing.B) { + for n := 0; n < b.N; n++ { + _, _ = UUID() + } +} + // // Helpers //