Skip to content

Commit 740a7c6

Browse files
committed
fix(uuid): UUID regexes to support all-or-none '-' separator
This PR changes the UUID validation to support either UUIDs with the expected number of separators or no separator at all. Under the hood, UUID validation no longer relies on regular expressions (exported regexp patterns are marked as deprecated) but on github.com/google/uuid. This brings a significant performance improvement on validation (~ 15-20 times faster). Notice that some non-standard UUID schemes as well as UUID v6 and v7 now pass "IsUUID". * contributes go-swagger/go-swagger#2878 Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 03a91f9 commit 740a7c6

File tree

4 files changed

+163
-25
lines changed

4 files changed

+163
-25
lines changed

default.go

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strings"
2626

2727
"github.com/asaskevich/govalidator"
28+
"github.com/google/uuid"
2829
"go.mongodb.org/mongo-driver/bson"
2930
)
3031

@@ -57,24 +58,35 @@ const (
5758
// - long top-level domain names (e.g. example.london) are permitted
5859
// - symbol unicode points are permitted (e.g. emoji) (not for top-level domain)
5960
HostnamePattern = `^([a-zA-Z0-9\p{S}\p{L}]((-?[a-zA-Z0-9\p{S}\p{L}]{0,62})?)|([a-zA-Z0-9\p{S}\p{L}](([a-zA-Z0-9-\p{S}\p{L}]{0,61}[a-zA-Z0-9\p{S}\p{L}])?)(\.)){1,}([a-zA-Z\p{L}]){2,63})$`
61+
62+
// json null type
63+
jsonNull = "null"
64+
)
65+
66+
const (
6067
// UUIDPattern Regex for UUID that allows uppercase
61-
UUIDPattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$`
68+
//
69+
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
70+
UUIDPattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{32}$)`
71+
6272
// UUID3Pattern Regex for UUID3 that allows uppercase
63-
UUID3Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$`
73+
//
74+
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
75+
UUID3Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$)|(^[0-9a-f]{12}3[0-9a-f]{3}?[0-9a-f]{16}$)`
76+
6477
// UUID4Pattern Regex for UUID4 that allows uppercase
65-
UUID4Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$`
78+
//
79+
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
80+
UUID4Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}$)`
81+
6682
// UUID5Pattern Regex for UUID5 that allows uppercase
67-
UUID5Pattern = `(?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$`
68-
// json null type
69-
jsonNull = "null"
83+
//
84+
// Deprecated: strfmt no longer uses regular expressions to validate UUIDs.
85+
UUID5Pattern = `(?i)(^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)|(^[0-9a-f]{12}5[0-9a-f]{3}[89ab][0-9a-f]{15}$)`
7086
)
7187

7288
var (
7389
rxHostname = regexp.MustCompile(HostnamePattern)
74-
rxUUID = regexp.MustCompile(UUIDPattern)
75-
rxUUID3 = regexp.MustCompile(UUID3Pattern)
76-
rxUUID4 = regexp.MustCompile(UUID4Pattern)
77-
rxUUID5 = regexp.MustCompile(UUID5Pattern)
7890
)
7991

8092
// IsHostname returns true when the string is a valid hostname
@@ -99,24 +111,28 @@ func IsHostname(str string) bool {
99111
return valid
100112
}
101113

102-
// IsUUID returns true is the string matches a UUID, upper case is allowed
114+
// IsUUID returns true is the string matches a UUID (in any version, including v6 and v7), upper case is allowed
103115
func IsUUID(str string) bool {
104-
return rxUUID.MatchString(str)
116+
_, err := uuid.Parse(str)
117+
return err == nil
105118
}
106119

107-
// IsUUID3 returns true is the string matches a UUID, upper case is allowed
120+
// IsUUID3 returns true is the string matches a UUID v3, upper case is allowed
108121
func IsUUID3(str string) bool {
109-
return rxUUID3.MatchString(str)
122+
id, err := uuid.Parse(str)
123+
return err == nil && id.Version() == uuid.Version(3)
110124
}
111125

112-
// IsUUID4 returns true is the string matches a UUID, upper case is allowed
126+
// IsUUID4 returns true is the string matches a UUID v4, upper case is allowed
113127
func IsUUID4(str string) bool {
114-
return rxUUID4.MatchString(str)
128+
id, err := uuid.Parse(str)
129+
return err == nil && id.Version() == uuid.Version(4)
115130
}
116131

117-
// IsUUID5 returns true is the string matches a UUID, upper case is allowed
132+
// IsUUID5 returns true is the string matches a UUID v5, upper case is allowed
118133
func IsUUID5(str string) bool {
119-
return rxUUID5.MatchString(str)
134+
id, err := uuid.Parse(str)
135+
return err == nil && id.Version() == uuid.Version(5)
120136
}
121137

122138
// IsEmail validates an email address.

default_test.go

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"encoding/base64"
2222
"encoding/json"
2323
"fmt"
24+
"io"
2425
"reflect"
26+
"regexp"
2527
"strings"
2628
"testing"
2729

@@ -175,9 +177,26 @@ func TestFormatMAC(t *testing.T) {
175177
func TestFormatUUID3(t *testing.T) {
176178
first3 := uuid.NewMD5(uuid.NameSpaceURL, []byte("somewhere.com"))
177179
other3 := uuid.NewMD5(uuid.NameSpaceURL, []byte("somewhereelse.com"))
180+
other4 := uuid.Must(uuid.NewRandom())
181+
other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))
178182
uuid3 := UUID3(first3.String())
179183
str := other3.String()
180-
testStringFormat(t, &uuid3, "uuid3", str, []string{}, []string{"not-a-uuid"})
184+
testStringFormat(t, &uuid3, "uuid3", str,
185+
[]string{
186+
other3.String(),
187+
strings.ReplaceAll(other3.String(), "-", ""),
188+
},
189+
[]string{
190+
"not-a-uuid",
191+
other4.String(),
192+
other5.String(),
193+
strings.ReplaceAll(other4.String(), "-", ""),
194+
strings.ReplaceAll(other5.String(), "-", ""),
195+
strings.Replace(other3.String(), "-", "", 2),
196+
strings.Replace(other4.String(), "-", "", 2),
197+
strings.Replace(other5.String(), "-", "", 2),
198+
},
199+
)
181200

182201
// special case for zero UUID
183202
var uuidZero UUID3
@@ -188,10 +207,27 @@ func TestFormatUUID3(t *testing.T) {
188207

189208
func TestFormatUUID4(t *testing.T) {
190209
first4 := uuid.Must(uuid.NewRandom())
210+
other3 := uuid.NewMD5(uuid.NameSpaceURL, []byte("somewhere.com"))
191211
other4 := uuid.Must(uuid.NewRandom())
212+
other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))
192213
uuid4 := UUID4(first4.String())
193214
str := other4.String()
194-
testStringFormat(t, &uuid4, "uuid4", str, []string{}, []string{"not-a-uuid"})
215+
testStringFormat(t, &uuid4, "uuid4", str,
216+
[]string{
217+
other4.String(),
218+
strings.ReplaceAll(other4.String(), "-", ""),
219+
},
220+
[]string{
221+
"not-a-uuid",
222+
other3.String(),
223+
other5.String(),
224+
strings.ReplaceAll(other3.String(), "-", ""),
225+
strings.ReplaceAll(other5.String(), "-", ""),
226+
strings.Replace(other3.String(), "-", "", 2),
227+
strings.Replace(other4.String(), "-", "", 2),
228+
strings.Replace(other5.String(), "-", "", 2),
229+
},
230+
)
195231

196232
// special case for zero UUID
197233
var uuidZero UUID4
@@ -202,10 +238,27 @@ func TestFormatUUID4(t *testing.T) {
202238

203239
func TestFormatUUID5(t *testing.T) {
204240
first5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhere.com"))
241+
other3 := uuid.NewMD5(uuid.NameSpaceURL, []byte("somewhere.com"))
242+
other4 := uuid.Must(uuid.NewRandom())
205243
other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))
206244
uuid5 := UUID5(first5.String())
207245
str := other5.String()
208-
testStringFormat(t, &uuid5, "uuid5", str, []string{}, []string{"not-a-uuid"})
246+
testStringFormat(t, &uuid5, "uuid5", str,
247+
[]string{
248+
other5.String(),
249+
strings.ReplaceAll(other5.String(), "-", ""),
250+
},
251+
[]string{
252+
"not-a-uuid",
253+
other3.String(),
254+
other4.String(),
255+
strings.ReplaceAll(other3.String(), "-", ""),
256+
strings.ReplaceAll(other4.String(), "-", ""),
257+
strings.Replace(other3.String(), "-", "", 2),
258+
strings.Replace(other4.String(), "-", "", 2),
259+
strings.Replace(other5.String(), "-", "", 2),
260+
},
261+
)
209262

210263
// special case for zero UUID
211264
var uuidZero UUID5
@@ -216,10 +269,34 @@ func TestFormatUUID5(t *testing.T) {
216269

217270
func TestFormatUUID(t *testing.T) {
218271
first5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhere.com"))
272+
other3 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))
273+
other4 := uuid.Must(uuid.NewRandom())
219274
other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))
275+
other6 := uuid.Must(uuid.NewV6())
276+
other7 := uuid.Must(uuid.NewV7())
277+
microsoft := "0" + other4.String() + "f"
278+
220279
uuid := UUID(first5.String())
221280
str := other5.String()
222-
testStringFormat(t, &uuid, "uuid", str, []string{}, []string{"not-a-uuid"})
281+
testStringFormat(t, &uuid, "uuid", str,
282+
[]string{
283+
other3.String(),
284+
other4.String(),
285+
other5.String(),
286+
strings.ReplaceAll(other3.String(), "-", ""),
287+
strings.ReplaceAll(other4.String(), "-", ""),
288+
strings.ReplaceAll(other5.String(), "-", ""),
289+
other6.String(),
290+
other7.String(),
291+
microsoft,
292+
},
293+
[]string{
294+
"not-a-uuid",
295+
strings.Replace(other3.String(), "-", "", 2),
296+
strings.Replace(other4.String(), "-", "", 2),
297+
strings.Replace(other5.String(), "-", "", 2),
298+
},
299+
)
223300

224301
// special case for zero UUID
225302
var uuidZero UUID
@@ -775,3 +852,48 @@ func TestDeepCopyPassword(t *testing.T) {
775852
out3 := inNil.DeepCopy()
776853
assert.Nil(t, out3)
777854
}
855+
856+
func BenchmarkIsUUID(b *testing.B) {
857+
const sampleSize = 100
858+
rxUUID := regexp.MustCompile(UUIDPattern)
859+
rxUUID3 := regexp.MustCompile(UUID3Pattern)
860+
rxUUID4 := regexp.MustCompile(UUID4Pattern)
861+
rxUUID5 := regexp.MustCompile(UUID5Pattern)
862+
863+
uuids := make([]string, 0, sampleSize)
864+
uuid3s := make([]string, 0, sampleSize)
865+
uuid4s := make([]string, 0, sampleSize)
866+
uuid5s := make([]string, 0, sampleSize)
867+
868+
for i := 0; i < sampleSize; i++ {
869+
seed := []byte(uuid.Must(uuid.NewRandom()).String())
870+
uuids = append(uuids, uuid.Must(uuid.NewRandom()).String())
871+
uuid3s = append(uuid3s, uuid.NewMD5(uuid.NameSpaceURL, seed).String())
872+
uuid4s = append(uuid4s, uuid.Must(uuid.NewRandom()).String())
873+
uuid5s = append(uuid5s, uuid.NewSHA1(uuid.NameSpaceURL, seed).String())
874+
}
875+
876+
b.Run("IsUUID - google.uuid", benchmarkIs(uuids, IsUUID))
877+
b.Run("IsUUID - regexp", benchmarkIs(uuids, func(id string) bool { return rxUUID.MatchString(id) }))
878+
879+
b.Run("IsUUIDv3 - google.uuid", benchmarkIs(uuid3s, IsUUID3))
880+
b.Run("IsUUIDv3 - regexp", benchmarkIs(uuid3s, func(id string) bool { return rxUUID3.MatchString(id) }))
881+
882+
b.Run("IsUUIDv4 - google.uuid", benchmarkIs(uuid4s, IsUUID4))
883+
b.Run("IsUUIDv4 - regexp", benchmarkIs(uuid4s, func(id string) bool { return rxUUID4.MatchString(id) }))
884+
885+
b.Run("IsUUIDv5 - google.uuid", benchmarkIs(uuid5s, IsUUID5))
886+
b.Run("IsUUIDv5 - regexp", benchmarkIs(uuid5s, func(id string) bool { return rxUUID5.MatchString(id) }))
887+
}
888+
889+
func benchmarkIs(input []string, fn func(string) bool) func(*testing.B) {
890+
return func(b *testing.B) {
891+
var isTrue bool
892+
b.ReportAllocs()
893+
b.ResetTimer()
894+
for i := 0; i < b.N; i++ {
895+
isTrue = fn(input[i%len(input)])
896+
}
897+
fmt.Fprintln(io.Discard, isTrue)
898+
}
899+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/go-openapi/strfmt
33
require (
44
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
55
github.com/go-openapi/errors v0.21.0
6-
github.com/google/uuid v1.4.0
6+
github.com/google/uuid v1.5.0
77
github.com/mitchellh/mapstructure v1.5.0
88
github.com/oklog/ulid v1.3.1
99
github.com/stretchr/testify v1.8.4

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuW
77
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
88
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
99
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
10-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
11-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
10+
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
11+
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
1212
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
1313
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1414
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=

0 commit comments

Comments
 (0)