From d0bd127ab34c1ee7cd9cf413cdfaf26366e9e18e Mon Sep 17 00:00:00 2001 From: Brad Moylan Date: Wed, 19 Dec 2018 01:38:17 -0500 Subject: [PATCH] Add bearertoken, datetime, rid, safelong, and uuid packages (#132) --- bearertoken/bearertoken.go | 9 ++ datetime/datetime.go | 49 +++++++ datetime/datetime_test.go | 121 ++++++++++++++++ godel/config/license-plugin.yml | 4 + rid/resource_identifier.go | 97 +++++++++++++ rid/resource_identifier_test.go | 69 +++++++++ safelong/safelong.go | 63 ++++++++ safelong/safelong_test.go | 95 +++++++++++++ uuid/internal/uuid/LICENSE | 27 ++++ uuid/internal/uuid/README.md | 25 ++++ uuid/internal/uuid/doc.go | 12 ++ uuid/internal/uuid/marshal.go | 39 +++++ uuid/internal/uuid/util.go | 32 +++++ uuid/internal/uuid/uuid.go | 245 ++++++++++++++++++++++++++++++++ uuid/internal/uuid/version4.go | 40 ++++++ uuid/uuid.go | 48 +++++++ uuid/uuid_test.go | 56 ++++++++ 17 files changed, 1031 insertions(+) create mode 100644 bearertoken/bearertoken.go create mode 100644 datetime/datetime.go create mode 100644 datetime/datetime_test.go create mode 100644 rid/resource_identifier.go create mode 100644 rid/resource_identifier_test.go create mode 100644 safelong/safelong.go create mode 100644 safelong/safelong_test.go create mode 100644 uuid/internal/uuid/LICENSE create mode 100644 uuid/internal/uuid/README.md create mode 100644 uuid/internal/uuid/doc.go create mode 100644 uuid/internal/uuid/marshal.go create mode 100644 uuid/internal/uuid/util.go create mode 100644 uuid/internal/uuid/uuid.go create mode 100644 uuid/internal/uuid/version4.go create mode 100644 uuid/uuid.go create mode 100644 uuid/uuid_test.go diff --git a/bearertoken/bearertoken.go b/bearertoken/bearertoken.go new file mode 100644 index 00000000..20de8b1f --- /dev/null +++ b/bearertoken/bearertoken.go @@ -0,0 +1,9 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bearertoken + +// Token represents a bearer token, generally sent by a REST client in a +// Authorization or Cookie header for authentication purposes. +type Token string diff --git a/datetime/datetime.go b/datetime/datetime.go new file mode 100644 index 00000000..1d25474b --- /dev/null +++ b/datetime/datetime.go @@ -0,0 +1,49 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package datetime + +import ( + "strings" + "time" +) + +// DateTime is an alias for time.Time which implements serialization matching the +// conjure wire specification at https://github.com/palantir/conjure/blob/master/docs/spec/wire.md +type DateTime time.Time + +func (d DateTime) String() string { + return time.Time(d).Format(time.RFC3339Nano) +} + +// MarshalText implements encoding.TextMarshaler (used by encoding/json and others). +func (d DateTime) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others). +func (d *DateTime) UnmarshalText(b []byte) error { + t, err := ParseDateTime(string(b)) + if err != nil { + return err + } + *d = t + return nil +} + +// ParseDateTime parses a DateTime from a string. Conjure supports DateTime inputs that end with an optional +// zone identifier enclosed in square brackets (for example, "2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]"). +func ParseDateTime(s string) (DateTime, error) { + // If the input string ends in a ']' and contains a '[', parse the string up to '['. + if strings.HasSuffix(s, "]") { + if openBracketIdx := strings.LastIndex(s, "["); openBracketIdx != -1 { + s = s[:openBracketIdx] + } + } + timeVal, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + return DateTime(time.Time{}), err + } + return DateTime(timeVal), nil +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go new file mode 100644 index 00000000..a1cbff63 --- /dev/null +++ b/datetime/datetime_test.go @@ -0,0 +1,121 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package datetime_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/palantir/pkg/datetime" +) + +var dateTimeJSONs = []struct { + sec int64 + zoneOffset int + str string + json string +}{ + { + sec: 1483326245, + str: `2017-01-02T03:04:05Z`, + json: `"2017-01-02T03:04:05Z"`, + }, + { + sec: 1483326245, + str: `2017-01-02T03:04:05Z`, + json: `"2017-01-02T03:04:05.000Z"`, + }, + { + sec: 1483326245, + str: `2017-01-02T03:04:05Z`, + json: `"2017-01-02T03:04:05.000000000Z"`, + }, + { + sec: 1483326245, + zoneOffset: 3600, + str: `2017-01-02T04:04:05+01:00`, + json: `"2017-01-02T04:04:05.000000000+01:00"`, + }, + { + sec: 1483326245, + zoneOffset: 7200, + str: `2017-01-02T05:04:05+02:00`, + json: `"2017-01-02T05:04:05.000000000+02:00"`, + }, + { + sec: 1483326245, + zoneOffset: 3600, + str: `2017-01-02T04:04:05+01:00`, + json: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]"`, + }, +} + +func TestDateTimeString(t *testing.T) { + for i, currCase := range dateTimeJSONs { + currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset))) + assert.Equal(t, currCase.str, currDateTime.String(), "Case %d", i) + } +} + +func TestDateTimeMarshal(t *testing.T) { + for i, currCase := range dateTimeJSONs { + currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset))) + bytes, err := json.Marshal(currDateTime) + require.NoError(t, err, "Case %d: marshal %q", i, currDateTime.String()) + + var unmarshaledFromMarshal datetime.DateTime + err = json.Unmarshal(bytes, &unmarshaledFromMarshal) + require.NoError(t, err, "Case %d: unmarshal %q", i, string(bytes)) + + var unmarshaledFromCase datetime.DateTime + err = json.Unmarshal([]byte(currCase.json), &unmarshaledFromCase) + require.NoError(t, err, "Case %d: unmarshal %q", i, currCase.json) + + assert.Equal(t, unmarshaledFromCase, unmarshaledFromMarshal, "Case %d", i) + } +} + +func TestDateTimeUnmarshal(t *testing.T) { + for i, currCase := range dateTimeJSONs { + wantDateTime := time.Unix(currCase.sec, 0).UTC() + if currCase.zoneOffset != 0 { + wantDateTime = wantDateTime.In(time.FixedZone("", currCase.zoneOffset)) + } + + var gotDateTime datetime.DateTime + err := json.Unmarshal([]byte(currCase.json), &gotDateTime) + require.NoError(t, err, "Case %d", i) + + assert.Equal(t, wantDateTime, time.Time(gotDateTime), "Case %d", i) + } +} + +func TestDateTimeUnmarshalInvalid(t *testing.T) { + for i, currCase := range []struct { + input string + wantErr string + }{ + { + input: `"foo"`, + wantErr: "parsing time \"foo\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"foo\" as \"2006\"", + }, + { + input: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin"`, + wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin\": extra text: [Europe/Berlin", + }, + { + input: `"2017-01-02T04:04:05.000000000+01:00[[Europe/Berlin]]"`, + wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[\": extra text: [", + }, + } { + var gotDateTime *datetime.DateTime + err := json.Unmarshal([]byte(currCase.input), &gotDateTime) + assert.EqualError(t, err, currCase.wantErr, "Case %d", i) + } +} diff --git a/godel/config/license-plugin.yml b/godel/config/license-plugin.yml index 621982a2..98aa725b 100644 --- a/godel/config/license-plugin.yml +++ b/godel/config/license-plugin.yml @@ -2,3 +2,7 @@ header: | // Copyright (c) {{YEAR}} Palantir Technologies. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + +exclude: + paths: + - uuid/internal/uuid diff --git a/rid/resource_identifier.go b/rid/resource_identifier.go new file mode 100644 index 00000000..7e36c747 --- /dev/null +++ b/rid/resource_identifier.go @@ -0,0 +1,97 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rid + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// A ResourceIdentifier is a four-part identifier string for a resource +// whose format is specified at https://github.com/palantir/resource-identifier. +// +// Resource Identifiers offer a common encoding for wrapping existing unique +// identifiers with some additional context that can be useful when storing +// those identifiers in other applications. Additionally, the context can be +// used to disambiguate application-unique, but not globally-unique, +// identifiers when used in a common space. +type ResourceIdentifier struct { + // Service is a string that represents the service (or application) that namespaces the rest of the identifier. + // Must conform with regex pattern [a-z][a-z0-9\-]*. + Service string + // Instance is an optionally empty string that represents a specific service cluster, to allow disambiguation of artifacts from different service clusters. + // Must conform to regex pattern ([a-z0-9][a-z0-9\-]*)?. + Instance string + // Type is a service-specific resource type to namespace a group of locators. + // Must conform to regex pattern [a-z][a-z0-9\-]*. + Type string + // Locator is a string used to uniquely locate the specific resource. + // Must conform to regex pattern [a-zA-Z0-9\-\._]+. + Locator string +} + +func (rid ResourceIdentifier) String() string { + return rid.Service + "." + rid.Instance + "." + rid.Type + "." + rid.Locator +} + +// MarshalText implements encoding.TextMarshaler (used by encoding/json and others). +func (rid ResourceIdentifier) MarshalText() (text []byte, err error) { + return []byte(rid.String()), rid.validate() +} + +// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others). +func (rid *ResourceIdentifier) UnmarshalText(text []byte) error { + var err error + parsed, err := ParseRID(string(text)) + if err != nil { + return err + } + *rid = parsed + return nil +} + +// ParseRID parses a string into a 4-part resource identifier. +func ParseRID(s string) (ResourceIdentifier, error) { + segments := strings.SplitN(s, ".", 4) + if len(segments) != 4 { + return ResourceIdentifier{}, errors.New("invalid resource identifier") + } + rid := ResourceIdentifier{ + Service: segments[0], + Instance: segments[1], + Type: segments[2], + Locator: segments[3], + } + return rid, rid.validate() +} + +var ( + servicePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`) + instancePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`) + typePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`) + locatorPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\._]+$`) +) + +func (rid ResourceIdentifier) validate() error { + var msgs []string + if !servicePattern.MatchString(rid.Service) { + msgs = append(msgs, fmt.Sprintf("rid first segment (service) does not match %s pattern", servicePattern)) + } + if !instancePattern.MatchString(rid.Instance) { + msgs = append(msgs, fmt.Sprintf("rid second segment (instance) does not match %s pattern", instancePattern)) + } + if !typePattern.MatchString(rid.Type) { + msgs = append(msgs, fmt.Sprintf("rid third segment (type) does not match %s pattern", typePattern)) + } + if !locatorPattern.MatchString(rid.Locator) { + msgs = append(msgs, fmt.Sprintf("rid fourth segment (locator) does not match %s pattern", locatorPattern)) + } + if len(msgs) != 0 { + return errors.New(strings.Join(msgs, ": ")) + } + return nil +} diff --git a/rid/resource_identifier_test.go b/rid/resource_identifier_test.go new file mode 100644 index 00000000..9d61e66f --- /dev/null +++ b/rid/resource_identifier_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rid_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/palantir/pkg/rid" +) + +func TestResourceIdentifier(t *testing.T) { + for _, test := range []struct { + Name string + Input rid.ResourceIdentifier + Expected string + ExpectedErr string + }{ + { + Name: "basic RID", + Input: rid.ResourceIdentifier{ + Service: "my-service", + Instance: "my-instance", + Type: "my-type", + Locator: "my.locator.with.dots", + }, + Expected: "my-service.my-instance.my-type.my.locator.with.dots", + }, + { + Name: "invalid casing", + Input: rid.ResourceIdentifier{ + Service: "myService", + Instance: "myInstance", + Type: "myType", + Locator: "my.locator.with.dots", + }, + ExpectedErr: `rid first segment (service) does not match ^[a-z][a-z0-9\-]*$ pattern: rid second segment (instance) does not match ^[a-z0-9][a-z0-9\-]*$ pattern: rid third segment (type) does not match ^[a-z][a-z0-9\-]*$ pattern`, + }, + } { + t.Run(test.Name, func(t *testing.T) { + type ridContainer struct { + RID rid.ResourceIdentifier `json:"rid"` + } + + // Test Marshal + jsonBytes, err := json.Marshal(ridContainer{RID: test.Input}) + if test.ExpectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), test.ExpectedErr) + return + } + require.NoError(t, err) + require.Equal(t, fmt.Sprintf(`{"rid":%q}`, test.Expected), string(jsonBytes)) + + // Test Unmarshal + var unmarshaled ridContainer + err = json.Unmarshal(jsonBytes, &unmarshaled) + require.NoError(t, err, "failed to unmarshal json: %s", string(jsonBytes)) + assert.Equal(t, test.Expected, unmarshaled.RID.String()) + assert.Equal(t, test.Input, unmarshaled.RID) + }) + } +} diff --git a/safelong/safelong.go b/safelong/safelong.go new file mode 100644 index 00000000..5e66a250 --- /dev/null +++ b/safelong/safelong.go @@ -0,0 +1,63 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package safelong + +import ( + "encoding/json" + "fmt" + "strconv" +) + +const ( + safeIntVal = int64(1) << 53 + minVal = -safeIntVal + 1 + maxVal = safeIntVal - 1 +) + +type SafeLong int64 + +func NewSafeLong(val int64) (SafeLong, error) { + if err := validate(val); err != nil { + return 0, err + } + return SafeLong(val), nil +} + +func ParseSafeLong(s string) (SafeLong, error) { + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, err + } + return NewSafeLong(i) +} + +func (s *SafeLong) UnmarshalJSON(b []byte) error { + var val int64 + if err := json.Unmarshal(b, &val); err != nil { + return err + } + + newVal, err := NewSafeLong(val) + if err != nil { + return err + } + *s = newVal + + return nil +} + +func (s SafeLong) MarshalJSON() ([]byte, error) { + if err := validate(int64(s)); err != nil { + return nil, err + } + return json.Marshal(int64(s)) +} + +func validate(val int64) error { + if val < minVal || val > maxVal { + return fmt.Errorf("%d is not a valid value for a SafeLong as it is not safely representable in Javascript: must be between %d and %d", val, minVal, maxVal) + } + return nil +} diff --git a/safelong/safelong_test.go b/safelong/safelong_test.go new file mode 100644 index 00000000..d5b18b8b --- /dev/null +++ b/safelong/safelong_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package safelong_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/palantir/pkg/safelong" +) + +var safeLongJSONs = []struct { + val int64 + json string +}{ + { + val: 13, + json: `13`, + }, + { + val: -42, + json: `-42`, + }, + { + val: 0, + json: `0`, + }, +} + +func TestSafeLongMarshal(t *testing.T) { + for i, currCase := range safeLongJSONs { + currSafeLong, err := safelong.NewSafeLong(currCase.val) + require.NoError(t, err, "Case %d", i) + bytes, err := json.Marshal(currSafeLong) + require.NoError(t, err, "Case %d", i) + assert.Equal(t, currCase.json, string(bytes), "Case %d", i) + } +} + +func TestSafeLongUnmarshal(t *testing.T) { + for i, currCase := range safeLongJSONs { + wantSafeLong, err := safelong.NewSafeLong(currCase.val) + require.NoError(t, err, "Case %d", i) + + var gotSafeLong safelong.SafeLong + err = json.Unmarshal([]byte(currCase.json), &gotSafeLong) + require.NoError(t, err, "Case %d", i) + + assert.Equal(t, wantSafeLong, gotSafeLong, "Case %d", i) + } +} + +func TestSafeLongBoundsEnforcedByMarshal(t *testing.T) { + wantErrFmt := "json: error calling MarshalJSON for type safelong.SafeLong: %d is not a valid value for a SafeLong as it is not safely representable in Javascript: must be between -9007199254740991 and 9007199254740991" + + for i, currVal := range []int64{ + int64(1) << 53, + -(int64(1) << 53), + } { + currSafeLong := safelong.SafeLong(currVal) + _, err := json.Marshal(currSafeLong) + assert.EqualError(t, err, fmt.Sprintf(wantErrFmt, currVal), "Case %d", i) + } +} + +func TestSafeLongBoundsEnforcedByUnmarshal(t *testing.T) { + wantErrFmt := "%d is not a valid value for a SafeLong as it is not safely representable in Javascript: must be between -9007199254740991 and 9007199254740991" + + for i, currVal := range []int64{ + int64(1) << 53, + -(int64(1) << 53), + } { + var gotSafeLong *safelong.SafeLong + err := json.Unmarshal([]byte(fmt.Sprintf("%d", currVal)), &gotSafeLong) + assert.EqualError(t, err, fmt.Sprintf(wantErrFmt, currVal), "Case %d", i) + } +} + +func TestBoundsEnforcedByNewSafeLong(t *testing.T) { + wantErrFmt := "%d is not a valid value for a SafeLong as it is not safely representable in Javascript: must be between -9007199254740991 and 9007199254740991" + + for i, currVal := range []int64{ + int64(1) << 53, + -(int64(1) << 53), + } { + _, err := safelong.NewSafeLong(currVal) + assert.EqualError(t, err, fmt.Sprintf(wantErrFmt, currVal), "Case %d", i) + } +} diff --git a/uuid/internal/uuid/LICENSE b/uuid/internal/uuid/LICENSE new file mode 100644 index 00000000..5dc68268 --- /dev/null +++ b/uuid/internal/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/uuid/internal/uuid/README.md b/uuid/internal/uuid/README.md new file mode 100644 index 00000000..f52bab59 --- /dev/null +++ b/uuid/internal/uuid/README.md @@ -0,0 +1,25 @@ +**Provenance**: This package was adapted from https://github.com/google/uuid/tree/v1.1.0 +to minimize external dependencies of generated code. We use a subset of the API and have +removed all references to non-V4 UUIDs. + +--- + +# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on +[RFC 4122](http://tools.ietf.org/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +`go get github.com/google/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/google/uuid diff --git a/uuid/internal/uuid/doc.go b/uuid/internal/uuid/doc.go new file mode 100644 index 00000000..5b8a4b9a --- /dev/null +++ b/uuid/internal/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/uuid/internal/uuid/marshal.go b/uuid/internal/uuid/marshal.go new file mode 100644 index 00000000..4f17761b --- /dev/null +++ b/uuid/internal/uuid/marshal.go @@ -0,0 +1,39 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "fmt" +) + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err == nil { + *uuid = id + } + return err +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/uuid/internal/uuid/util.go b/uuid/internal/uuid/util.go new file mode 100644 index 00000000..65f21a6a --- /dev/null +++ b/uuid/internal/uuid/util.go @@ -0,0 +1,32 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 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, + 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, + 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, + 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, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/uuid/internal/uuid/uuid.go b/uuid/internal/uuid/uuid.go new file mode 100644 index 00000000..524404cc --- /dev/null +++ b/uuid/internal/uuid/uuid.go @@ -0,0 +1,245 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +var rander = rand.Reader // random function + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if strings.ToLower(s[:9]) != "urn:uuid:" { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} diff --git a/uuid/internal/uuid/version4.go b/uuid/internal/uuid/version4.go new file mode 100644 index 00000000..ed591034 --- /dev/null +++ b/uuid/internal/uuid/version4.go @@ -0,0 +1,40 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + var uuid UUID + _, err := io.ReadFull(rander, uuid[:]) + if err != nil { + return UUID{}, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/uuid/uuid.go b/uuid/uuid.go new file mode 100644 index 00000000..6ea56aec --- /dev/null +++ b/uuid/uuid.go @@ -0,0 +1,48 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding" + "fmt" + + "github.com/palantir/pkg/uuid/internal/uuid" +) + +func NewUUID() UUID { + return [16]byte(uuid.New()) +} + +var ( + _ fmt.Stringer = UUID{} + _ encoding.TextMarshaler = UUID{} + _ encoding.TextUnmarshaler = &UUID{} +) + +// UUID (universally unique identifier) is a 128-bit number used to +// identify information in computer systems as defined in RFC 4122. +type UUID [16]byte + +func ParseUUID(s string) (UUID, error) { + var u UUID + err := (&u).UnmarshalText([]byte(s)) + return u, err +} + +// String returns uuid string representation "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +// or "" if uuid is invalid. +func (u UUID) String() string { + return uuid.UUID(u).String() +} + +// MarshalText implements encoding.TextMarshaler. +func (u UUID) MarshalText() ([]byte, error) { + return uuid.UUID(u).MarshalText() +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (u *UUID) UnmarshalText(data []byte) error { + return (*uuid.UUID)(u).UnmarshalText(data) +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go new file mode 100644 index 00000000..c3bbe4f0 --- /dev/null +++ b/uuid/uuid_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2018 Palantir Technologies. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/palantir/pkg/uuid" +) + +var testUUID = uuid.UUID{ + 0x0, 0x1, 0x2, 0x3, + 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, 0xA, 0xB, + 0xC, 0xD, 0xE, 0xF, +} + +func TestUUID_MarshalJSON(t *testing.T) { + marshalledUUID, err := json.Marshal(testUUID) + assert.NoError(t, err) + assert.Equal(t, `"00010203-0405-0607-0809-0a0b0c0d0e0f"`, string(marshalledUUID)) +} + +func TestUUID_UnmarshalJSON(t *testing.T) { + t.Run("correct lower case", func(t *testing.T) { + var actual uuid.UUID + err := json.Unmarshal([]byte(`"00010203-0405-0607-0809-0a0b0c0d0e0f"`), &actual) + assert.NoError(t, err) + assert.Equal(t, testUUID, actual) + }) + + t.Run("correct upper case", func(t *testing.T) { + var actual uuid.UUID + err := json.Unmarshal([]byte(`"00010203-0405-0607-0809-0A0B0C0D0E0F"`), &actual) + assert.NoError(t, err) + assert.Equal(t, testUUID, actual) + }) + + t.Run("incorrect group", func(t *testing.T) { + var actual uuid.UUID + err := json.Unmarshal([]byte(`"00010203-04Z5-0607-0809-0A0B0C0D0E0F"`), &actual) + assert.EqualError(t, err, "invalid UUID format") + }) +} + +func TestNewUUID(t *testing.T) { + u1 := uuid.NewUUID() + u2 := uuid.NewUUID() + require.NotEqual(t, u1.String(), u2.String(), "Two UUIDs should not be equal.") +}