diff --git a/baked_in.go b/baked_in.go index 231b78e..66ecc61 100644 --- a/baked_in.go +++ b/baked_in.go @@ -96,6 +96,7 @@ var ( "url": isURL, "uri": isURI, "base64": isBase64, + "base64url": isBase64URL, "contains": contains, "containsany": containsAny, "containsrune": containsRune, @@ -845,6 +846,11 @@ func isBase64(fl FieldLevel) bool { return base64Regex.MatchString(fl.Field().String()) } +// IsBase64URL is the validation function for validating if the current field's value is a valid base64 URL safe string. +func isBase64URL(fl FieldLevel) bool { + return base64URLRegex.MatchString(fl.Field().String()) +} + // IsURI is the validation function for validating if the current field's value is a valid URI. func isURI(fl FieldLevel) bool { diff --git a/doc.go b/doc.go index f7efe23..5dfc518 100644 --- a/doc.go +++ b/doc.go @@ -609,6 +609,15 @@ this with the omitempty tag. Usage: base64 +Base64URL String + +This validates that a string value contains a valid base64 URL safe value. +Although an empty string is a valid base64 URL safe value, this will report +an empty string as an error, if you wish to accept an empty string as valid +you can use this with the omitempty tag. + + Usage: base64url + Contains This validates that a string value contains the substring value. diff --git a/regexes.go b/regexes.go index 78f3ea0..5dee75f 100644 --- a/regexes.go +++ b/regexes.go @@ -17,6 +17,7 @@ const ( hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" emailRegexString = "^(?:(?:(?:(?:[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}])))\\.?$" base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" + base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$" iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" @@ -49,6 +50,7 @@ var ( hslaRegex = regexp.MustCompile(hslaRegexString) emailRegex = regexp.MustCompile(emailRegexString) base64Regex = regexp.MustCompile(base64RegexString) + base64URLRegex = regexp.MustCompile(base64URLRegexString) iSBN10Regex = regexp.MustCompile(iSBN10RegexString) iSBN13Regex = regexp.MustCompile(iSBN13RegexString) uUID3Regex = regexp.MustCompile(uUID3RegexString) diff --git a/validator_test.go b/validator_test.go index 9600d0f..c6467db 100644 --- a/validator_test.go +++ b/validator_test.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "database/sql/driver" + "encoding/base64" "encoding/json" "fmt" "reflect" @@ -4398,6 +4399,53 @@ func TestBase64Validation(t *testing.T) { AssertError(t, errs, "", "", "", "", "base64") } +func TestBase64URLValidation(t *testing.T) { + validate := New() + + testCases := []struct { + decoded, encoded string + success bool + }{ + // empty string, although a valid base64 string, should fail + {"", "", false}, + // invalid length + {"", "a", false}, + // base64 with padding + {"f", "Zg==", true}, + {"fo", "Zm8=", true}, + // base64 without padding + {"foo", "Zm9v", true}, + {"", "Zg", false}, + {"", "Zm8", false}, + // base64 URL safe encoding with invalid, special characters '+' and '/' + {"\x14\xfb\x9c\x03\xd9\x7e", "FPucA9l+", false}, + {"\x14\xfb\x9c\x03\xf9\x73", "FPucA/lz", false}, + // base64 URL safe encoding with valid, special characters '-' and '_' + {"\x14\xfb\x9c\x03\xd9\x7e", "FPucA9l-", true}, + {"\x14\xfb\x9c\x03\xf9\x73", "FPucA_lz", true}, + // non base64 characters + {"", "@mc=", false}, + {"", "Zm 9", false}, + } + for _, tc := range testCases { + err := validate.Var(tc.encoded, "base64url") + if tc.success { + Equal(t, err, nil) + // make sure encoded value is decoded back to the expected value + d, err := base64.URLEncoding.DecodeString(tc.encoded) + Equal(t, err, nil) + Equal(t, tc.decoded, string(d)) + } else { + NotEqual(t, err, nil) + if len(tc.encoded) > 0 { + // make sure that indeed the encoded value was faulty + _, err := base64.URLEncoding.DecodeString(tc.encoded) + NotEqual(t, err, nil) + } + } + } +} + func TestNoStructLevelValidation(t *testing.T) { type Inner struct {