@ -7,6 +7,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/url"
"os"
@ -14,13 +15,14 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode/utf8"
"golang.org/x/crypto/sha3"
"golang.org/x/text/language"
urn "github.com/leodido/go-urn"
"github.com/leodido/go-urn"
)
// Func accepts a FieldLevel interface for all validation needs. The return
@ -86,7 +88,9 @@ var (
"min" : hasMinOf ,
"max" : hasMaxOf ,
"eq" : isEq ,
"eq_ignore_case" : isEqIgnoreCase ,
"ne" : isNe ,
"ne_ignore_case" : isNeIgnoreCase ,
"lt" : isLt ,
"lte" : isLte ,
"gt" : isGt ,
@ -121,11 +125,14 @@ var (
"e164" : isE164 ,
"email" : isEmail ,
"url" : isURL ,
"http_url" : isHttpURL ,
"uri" : isURI ,
"urn_rfc2141" : isUrnRFC2141 , // RFC 2141
"file" : isFile ,
"filepath" : isFilePath ,
"base64" : isBase64 ,
"base64url" : isBase64URL ,
"base64rawurl" : isBase64RawURL ,
"contains" : contains ,
"containsany" : containsAny ,
"containsrune" : containsRune ,
@ -140,6 +147,7 @@ var (
"isbn10" : isISBN10 ,
"isbn13" : isISBN13 ,
"eth_addr" : isEthereumAddress ,
"eth_addr_checksum" : isEthereumAddressChecksum ,
"btc_addr" : isBitcoinAddress ,
"btc_addr_bech32" : isBitcoinBech32Address ,
"uuid" : isUUID ,
@ -194,6 +202,7 @@ var (
"html_encoded" : isHTMLEncoded ,
"url_encoded" : isURLEncoded ,
"dir" : isDir ,
"dirpath" : isDirPath ,
"json" : isJSON ,
"jwt" : isJWT ,
"hostname_port" : isHostnamePort ,
@ -214,6 +223,10 @@ var (
"semver" : isSemverFormat ,
"dns_rfc1035_label" : isDnsRFC1035LabelFormat ,
"credit_card" : isCreditCard ,
"cve" : isCveFormat ,
"luhn_checksum" : hasLuhnChecksum ,
"mongodb" : isMongoDB ,
"cron" : isCron ,
}
)
@ -307,18 +320,42 @@ func isUnique(fl FieldLevel) bool {
}
m := reflect . MakeMap ( reflect . MapOf ( sfTyp , v . Type ( ) ) )
var fieldlen int
for i := 0 ; i < field . Len ( ) ; i ++ {
m . SetMapIndex ( reflect . Indirect ( reflect . Indirect ( field . Index ( i ) ) . FieldByName ( param ) ) , v )
key := reflect . Indirect ( reflect . Indirect ( field . Index ( i ) ) . FieldByName ( param ) )
if key . IsValid ( ) {
fieldlen ++
m . SetMapIndex ( key , v )
}
}
return field . Len ( ) == m . Len ( )
return field len == m . Len ( )
case reflect . Map :
m := reflect . MakeMap ( reflect . MapOf ( field . Type ( ) . Elem ( ) , v . Type ( ) ) )
var m reflect . Value
if field . Type ( ) . Elem ( ) . Kind ( ) == reflect . Ptr {
m = reflect . MakeMap ( reflect . MapOf ( field . Type ( ) . Elem ( ) . Elem ( ) , v . Type ( ) ) )
} else {
m = reflect . MakeMap ( reflect . MapOf ( field . Type ( ) . Elem ( ) , v . Type ( ) ) )
}
for _ , k := range field . MapKeys ( ) {
m . SetMapIndex ( field . MapIndex ( k ) , v )
m . SetMapIndex ( reflect. Indirect ( field. MapIndex ( k ) ) , v )
}
return field . Len ( ) == m . Len ( )
default :
if parent := fl . Parent ( ) ; parent . Kind ( ) == reflect . Struct {
uniqueField := parent . FieldByName ( param )
if uniqueField == reflect . ValueOf ( nil ) {
panic ( fmt . Sprintf ( "Bad field name provided %s" , param ) )
}
if uniqueField . Kind ( ) != field . Kind ( ) {
panic ( fmt . Sprintf ( "Bad field type %T:%T" , field . Interface ( ) , uniqueField . Interface ( ) ) )
}
return field . Interface ( ) != uniqueField . Interface ( )
}
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
}
@ -613,14 +650,16 @@ func isISBN10(fl FieldLevel) bool {
func isEthereumAddress ( fl FieldLevel ) bool {
address := fl . Field ( ) . String ( )
return ethAddressRegex . MatchString ( address )
}
// isEthereumAddressChecksum is the validation function for validating if the field's value is a valid checksumed Ethereum address.
func isEthereumAddressChecksum ( fl FieldLevel ) bool {
address := fl . Field ( ) . String ( )
if ! ethAddressRegex . MatchString ( address ) {
return false
}
if ethAddressRegexUpper . MatchString ( address ) || ethAddressRegexLower . MatchString ( address ) {
return true
}
// Checksum validation. Reference: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
address = address [ 2 : ] // Skip "0x" prefix.
h := sha3 . NewLegacyKeccak256 ( )
@ -889,6 +928,12 @@ func isNe(fl FieldLevel) bool {
return ! isEq ( fl )
}
// isNe is the validation function for validating that the field's string value does not equal the
// provided param value. The comparison is case-insensitive
func isNeIgnoreCase ( fl FieldLevel ) bool {
return ! isEqIgnoreCase ( fl )
}
// isLteCrossStructField is the validation function for validating if the current field's value is less than or equal to the field, within a separate struct, specified by the param's value.
func isLteCrossStructField ( fl FieldLevel ) bool {
field := fl . Field ( )
@ -1260,6 +1305,22 @@ func isEq(fl FieldLevel) bool {
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isEqIgnoreCase is the validation function for validating if the current field's string value is
// equal to the param's value.
// The comparison is case-insensitive.
func isEqIgnoreCase ( fl FieldLevel ) bool {
field := fl . Field ( )
param := fl . Param ( )
switch field . Kind ( ) {
case reflect . String :
return strings . EqualFold ( field . String ( ) , param )
}
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isPostcodeByIso3166Alpha2 validates by value which is country code in iso 3166 alpha 2
// example: `postcode_iso3166_alpha2=US`
func isPostcodeByIso3166Alpha2 ( fl FieldLevel ) bool {
@ -1311,6 +1372,11 @@ func isBase64URL(fl FieldLevel) bool {
return base64URLRegex . MatchString ( fl . Field ( ) . String ( ) )
}
// isBase64RawURL is the validation function for validating if the current field's value is a valid base64 URL safe string without '=' padding.
func isBase64RawURL ( fl FieldLevel ) bool {
return base64RawURLRegex . 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 {
field := fl . Field ( )
@ -1370,6 +1436,23 @@ func isURL(fl FieldLevel) bool {
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isHttpURL is the validation function for validating if the current field's value is a valid HTTP(s) URL.
func isHttpURL ( fl FieldLevel ) bool {
if ! isURL ( fl ) {
return false
}
field := fl . Field ( )
switch field . Kind ( ) {
case reflect . String :
s := strings . ToLower ( field . String ( ) )
return strings . HasPrefix ( s , "http://" ) || strings . HasPrefix ( s , "https://" )
}
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isUrnRFC2141 is the validation function for validating if the current field's value is a valid URN as per RFC 2141.
func isUrnRFC2141 ( fl FieldLevel ) bool {
field := fl . Field ( )
@ -1387,7 +1470,7 @@ func isUrnRFC2141(fl FieldLevel) bool {
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isFile is the validation function for validating if the current field's value is a valid file path.
// isFile is the validation function for validating if the current field's value is a valid existing file path.
func isFile ( fl FieldLevel ) bool {
field := fl . Field ( )
@ -1404,6 +1487,57 @@ func isFile(fl FieldLevel) bool {
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isFilePath is the validation function for validating if the current field's value is a valid file path.
func isFilePath ( fl FieldLevel ) bool {
var exists bool
var err error
field := fl . Field ( )
// If it exists, it obviously is valid.
// This is done first to avoid code duplication and unnecessary additional logic.
if exists = isFile ( fl ) ; exists {
return true
}
// It does not exist but may still be a valid filepath.
switch field . Kind ( ) {
case reflect . String :
// Every OS allows for whitespace, but none
// let you use a file with no filename (to my knowledge).
// Unless you're dealing with raw inodes, but I digress.
if strings . TrimSpace ( field . String ( ) ) == "" {
return false
}
// We make sure it isn't a directory.
if strings . HasSuffix ( field . String ( ) , string ( os . PathSeparator ) ) {
return false
}
if _ , err = os . Stat ( field . String ( ) ) ; err != nil {
switch t := err . ( type ) {
case * fs . PathError :
if t . Err == syscall . EINVAL {
// It's definitely an invalid character in the filepath.
return false
}
// It could be a permission error, a does-not-exist error, etc.
// Out-of-scope for this validation, though.
return true
default :
// Something went *seriously* wrong.
/ *
Per https : //pkg.go.dev/os#Stat:
"If there is an error, it will be of type *PathError."
* /
panic ( err )
}
}
}
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
func isE164 ( fl FieldLevel ) bool {
return e164Regex . MatchString ( fl . Field ( ) . String ( ) )
@ -1539,7 +1673,9 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
}
// requireCheckFieldValue is a func for check field value
func requireCheckFieldValue ( fl FieldLevel , param string , value string , defaultNotFoundValue bool ) bool {
func requireCheckFieldValue (
fl FieldLevel , param string , value string , defaultNotFoundValue bool ,
) bool {
field , kind , _ , found := fl . GetStructFieldOKAdvanced2 ( fl . Parent ( ) , param )
if ! found {
return defaultNotFoundValue
@ -1623,10 +1759,10 @@ func excludedUnless(fl FieldLevel) bool {
}
for i := 0 ; i < len ( params ) ; i += 2 {
if ! requireCheckFieldValue ( fl , params [ i ] , params [ i + 1 ] , false ) {
return true
return ! hasValue ( fl )
}
}
return ! hasValue ( fl )
return true
}
// excludedWith is the validation function
@ -2275,7 +2411,7 @@ func isFQDN(fl FieldLevel) bool {
return fqdnRegexRFC1123 . MatchString ( val )
}
// isDir is the validation function for validating if the current field's value is a valid directory.
// isDir is the validation function for validating if the current field's value is a valid existing directory.
func isDir ( fl FieldLevel ) bool {
field := fl . Field ( )
@ -2291,6 +2427,64 @@ func isDir(fl FieldLevel) bool {
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isDirPath is the validation function for validating if the current field's value is a valid directory.
func isDirPath ( fl FieldLevel ) bool {
var exists bool
var err error
field := fl . Field ( )
// If it exists, it obviously is valid.
// This is done first to avoid code duplication and unnecessary additional logic.
if exists = isDir ( fl ) ; exists {
return true
}
// It does not exist but may still be a valid path.
switch field . Kind ( ) {
case reflect . String :
// Every OS allows for whitespace, but none
// let you use a dir with no name (to my knowledge).
// Unless you're dealing with raw inodes, but I digress.
if strings . TrimSpace ( field . String ( ) ) == "" {
return false
}
if _ , err = os . Stat ( field . String ( ) ) ; err != nil {
switch t := err . ( type ) {
case * fs . PathError :
if t . Err == syscall . EINVAL {
// It's definitely an invalid character in the path.
return false
}
// It could be a permission error, a does-not-exist error, etc.
// Out-of-scope for this validation, though.
// Lastly, we make sure it is a directory.
if strings . HasSuffix ( field . String ( ) , string ( os . PathSeparator ) ) {
return true
} else {
return false
}
default :
// Something went *seriously* wrong.
/ *
Per https : //pkg.go.dev/os#Stat:
"If there is an error, it will be of type *PathError."
* /
panic ( err )
}
}
// We repeat the check here to make sure it is an explicit directory in case the above os.Stat didn't trigger an error.
if strings . HasSuffix ( field . String ( ) , string ( os . PathSeparator ) ) {
return true
} else {
return false
}
}
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
// isJSON is the validation function for validating if the current field's value is a valid json string.
func isJSON ( fl FieldLevel ) bool {
field := fl . Field ( )
@ -2316,7 +2510,9 @@ func isHostnamePort(fl FieldLevel) bool {
return false
}
// Port must be a iny <= 65535.
if portNum , err := strconv . ParseInt ( port , 10 , 32 ) ; err != nil || portNum > 65535 || portNum < 1 {
if portNum , err := strconv . ParseInt (
port , 10 , 32 ,
) ; err != nil || portNum > 65535 || portNum < 1 {
return false
}
@ -2479,6 +2675,13 @@ func isSemverFormat(fl FieldLevel) bool {
return semverRegex . MatchString ( semverString )
}
// isCveFormat is the validation function for validating if the current field's value is a valid cve id, defined in CVE mitre org
func isCveFormat ( fl FieldLevel ) bool {
cveString := fl . Field ( ) . String ( )
return cveRegex . MatchString ( cveString )
}
// isDnsRFC1035LabelFormat is the validation function
// for validating if the current field's value is
// a valid dns RFC 1035 label, defined in RFC 1035.
@ -2487,6 +2690,35 @@ func isDnsRFC1035LabelFormat(fl FieldLevel) bool {
return dnsRegexRFC1035Label . MatchString ( val )
}
// digitsHaveLuhnChecksum returns true if and only if the last element of the given digits slice is the Luhn checksum of the previous elements
func digitsHaveLuhnChecksum ( digits [ ] string ) bool {
size := len ( digits )
sum := 0
for i , digit := range digits {
value , err := strconv . Atoi ( digit )
if err != nil {
return false
}
if size % 2 == 0 && i % 2 == 0 || size % 2 == 1 && i % 2 == 1 {
v := value * 2
if v >= 10 {
sum += 1 + ( v % 10 )
} else {
sum += v
}
} else {
sum += value
}
}
return ( sum % 10 ) == 0
}
// isMongoDB is the validation function for validating if the current field's value is valid mongoDB objectID
func isMongoDB ( fl FieldLevel ) bool {
val := fl . Field ( ) . String ( )
return mongodbRegex . MatchString ( val )
}
// isCreditCard is the validation function for validating if the current field's value is a valid credit card number
func isCreditCard ( fl FieldLevel ) bool {
val := fl . Field ( ) . String ( )
@ -2505,22 +2737,33 @@ func isCreditCard(fl FieldLevel) bool {
return false
}
sum := 0
for i , digit := range ccDigits {
value , err := strconv . Atoi ( digit )
if err != nil {
return false
}
if size % 2 == 0 && i % 2 == 0 || size % 2 == 1 && i % 2 == 1 {
v := value * 2
if v >= 10 {
sum += 1 + ( v % 10 )
} else {
sum += v
}
} else {
sum += value
}
return digitsHaveLuhnChecksum ( ccDigits )
}
// hasLuhnChecksum is the validation for validating if the current field's value has a valid Luhn checksum
func hasLuhnChecksum ( fl FieldLevel ) bool {
field := fl . Field ( )
var str string // convert to a string which will then be split into single digits; easier and more readable than shifting/extracting single digits from a number
switch field . Kind ( ) {
case reflect . String :
str = field . String ( )
case reflect . Int , reflect . Int8 , reflect . Int16 , reflect . Int32 , reflect . Int64 :
str = strconv . FormatInt ( field . Int ( ) , 10 )
case reflect . Uint , reflect . Uint8 , reflect . Uint16 , reflect . Uint32 , reflect . Uint64 :
str = strconv . FormatUint ( field . Uint ( ) , 10 )
default :
panic ( fmt . Sprintf ( "Bad field type %T" , field . Interface ( ) ) )
}
return ( sum % 10 ) == 0
size := len ( str )
if size < 2 { // there has to be at least one digit that carries a meaning + the checksum
return false
}
digits := strings . Split ( str , "" )
return digitsHaveLuhnChecksum ( digits )
}
// isCron is the validation function for validating if the current field's value is a valid cron expression
func isCron ( fl FieldLevel ) bool {
cronString := fl . Field ( ) . String ( )
return cronRegex . MatchString ( cronString )
}