diff --git a/internals/is/regex.go b/internals/is/regex.go new file mode 100644 index 0000000..0ebd640 --- /dev/null +++ b/internals/is/regex.go @@ -0,0 +1,21 @@ +package is + +import "regexp" + +var ( + // Email Regex used in: https://github.com/AfterShip/email-verifier/tree/aa8f77c0586ed2ecf9c20cb221de09282ce75355 + // emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$") + // Email regex used in: https://github.com/go-playground/validator/blob/0e3e2f997385102062275f226e825b4a109f4833/regexes.go#L21 + emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$") + + // Zod UUID regex + uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$`) +) + +func Email(val string) bool { + return emailRegex.MatchString(val) +} + +func UUIDv4(val string) bool { + return uuidRegex.MatchString(val) +} diff --git a/internals/is/regex_test.go b/internals/is/regex_test.go new file mode 100644 index 0000000..ed801cd --- /dev/null +++ b/internals/is/regex_test.go @@ -0,0 +1,159 @@ +package is + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmail(t *testing.T) { + + validEmails := []string{ + `email@domain.com`, + `firstname.lastname@domain.com`, + `email@subdomain.domain.com`, + `firstname+lastname@domain.com`, + `1234567890@domain.com`, + `email@domain-one.com`, + `_______@domain.com`, + `email@domain.name`, + `email@domain.co.jp`, + `firstname-lastname@domain.com`, + `very.common@example.com`, + `disposable.style.email.with+symbol@example.com`, + `other.email-with-hyphen@example.com`, + `fully-qualified-domain@example.com`, + `user.name+tag+sorting@example.com`, + `x@example.com`, + `mojojojo@asdf.example.com`, + `example-indeed@strange-example.com`, + `example@s.example`, + `user-@example.org`, + `user@my-example.com`, + `a@b.cd`, + `work+user@mail.com`, + `tom@test.te-st.com`, + `something@subdomain.domain-with-hyphens.tld`, + `common'name@domain.com`, + `francois@etu.inp-n7.fr`, + } + invalidEmails := []string{ + // no "printable characters" + // `user%example.com@example.org`, + // `mailhost!username@example.org`, + // `test/test@test.com`, + + // double @ + `francois@@etu.inp-n7.fr`, + // do not support quotes + // do not support comma + `a,b@domain.com`, + + // do not support IPv4 + `email@123.123.123.123`, + `email@[123.123.123.123]`, + `postmaster@123.123.123.123`, + `user@[68.185.127.196]`, + `ipv4@[85.129.96.247]`, + `valid@[79.208.229.53]`, + `valid@[255.255.255.255]`, + `valid@[255.0.55.2]`, + `valid@[255.0.55.2]`, + + // do not support ipv6 + `hgrebert0@[IPv6:4dc8:ac7:ce79:8878:1290:6098:5c50:1f25]`, + `bshapiro4@[IPv6:3669:c709:e981:4884:59a3:75d1:166b:9ae]`, + `jsmith@[IPv6:2001:db8::1]`, + `postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]`, + `postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:192.168.1.1]`, + + // microsoft test cases + `plainaddress`, + `#@%^%#$@#$@#.com`, + `@domain.com`, + `Joe Smith <email@domain.com>`, + `email.domain.com`, + `email@domain@domain.com`, + `.email@domain.com`, + `email.@domain.com`, + `email..email@domain.com`, + `email@domain.com (Joe Smith)`, + `email@domain`, + `email@-domain.com`, + `email@111.222.333.44444`, + `email@domain..com`, + `Abc.example.com`, + `A@b@c@example.com`, + `colin..hacks@domain.com`, + `a"b(c)d,e:f;gi[j\k]l@example.com`, + `just"not"right@example.com`, + `this is"not\allowed@example.com`, + `this\ still\"not\\allowed@example.com`, + + // random + "email@email", + `i_like_underscore@but_its_not_allowed_in_this_part.example.com`, + `QA[icon]CHOCOLATE[icon]@test.com`, + `invalid@-start.com`, + `invalid@end.com-`, + `invalid@[1.1.1.-1]`, + `invalid@[68.185.127.196.55]`, + `temp@[192.168.1]`, + `temp@[9.18.122.]`, + `double..point@test.com`, + `asdad@test..com`, + `asdad@hghg...sd...au`, + `asdad@hghg........au`, + `invalid@[256.2.2.48]`, + `invalid@[256.2.2.48]`, + `invalid@[999.465.265.1]`, + `jkibbey4@[IPv6:82c4:19a8::70a9:2aac:557::ea69:d985:28d]`, + `mlivesay3@[9952:143f:b4df:2179:49a1:5e82:b92e:6b6]`, + `gbacher0@[IPv6:bc37:4d3f:5048:2e26:37cc:248e:df8e:2f7f:af]`, + `invalid@[IPv6:5348:4ed3:5d38:67fb:e9b:acd2:c13:192.168.256.1]`, + `test@.com`, + + // Should be false but don't work with the current regex + // `"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com`, + // `aaaaaaaaaaaaaaalongemailthatcausesregexDoSvulnerability@test.c`, + // `a.b@c.d`, + // `あいうえお@domain.com`, + // `"john..doe"@example.org`, + // `" "@example.org`, + // `"email"@domain.com`, + // `"e asdf sadf ?<>ail"@domain.com`, + } + + for _, email := range validEmails { + assert.True(t, Email(email), "should be valid email: %s", email) + } + + for _, email := range invalidEmails { + assert.False(t, Email(email), "should be invalid email: %s", email) + } +} + +func TestUUIDv4(t *testing.T) { + validUUIDs := []string{ + "9491d710-3185-4e06-bea0-6a2f275345e0", + "d89e7b01-7598-ed11-9d7a-0022489382fd", + "00000000-0000-0000-0000-000000000000", + "b3ce60f8-e8b9-40f5-1150-172ede56ff74", + "92e76bf9-28b3-4730-cd7f-cb6bc51f8c09", + } + + invalidUUIDs := []string{ + "9491d710-3185-4e06-bea0-6a2f275345e0X", + // random + "not a uuid", + } + + for _, uuid := range validUUIDs { + assert.True(t, UUIDv4(uuid), "should be valid uuid: %s", uuid) + } + + for _, uuid := range invalidUUIDs { + assert.False(t, UUIDv4(uuid), "should be invalid uuid: %s", uuid) + } + +} diff --git a/string.go b/string.go index f086915..19bed23 100644 --- a/string.go +++ b/string.go @@ -7,15 +7,13 @@ import ( "github.com/Oudwins/zog/conf" p "github.com/Oudwins/zog/internals" + "github.com/Oudwins/zog/internals/is" "github.com/Oudwins/zog/zconst" ) var ( _ PrimitiveZogSchema[string] = (*StringSchema[string])(nil) _ NotStringSchema[string] = (*StringSchema[string])(nil) - - emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$`) ) type likeString interface { @@ -214,7 +212,7 @@ func (v *StringSchema[T]) Len(n int, options ...TestOption) *StringSchema[T] { func (v *StringSchema[T]) Email(options ...TestOption) *StringSchema[T] { t := p.Test[*T]{IssueCode: zconst.IssueCodeEmail} fn := func(v *T, ctx Ctx) bool { - return emailRegex.MatchString(string(*v)) + return is.Email(string(*v)) } return v.addTest(t, fn, options...) } @@ -317,7 +315,7 @@ func (v *StringSchema[T]) ContainsSpecial(options ...TestOption) *StringSchema[T func (v *StringSchema[T]) UUID(options ...TestOption) *StringSchema[T] { t := p.Test[*T]{IssueCode: zconst.IssueCodeUUID} fn := func(v *T, ctx Ctx) bool { - return uuidRegex.MatchString(string(*v)) + return is.UUIDv4(string(*v)) } return v.addTest(t, fn, options...)