Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Luhn Checksum Validation #1009

Merged
merged 6 commits into from Mar 19, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -176,6 +176,7 @@ Baked-in Validations
| jwt | JSON Web Token (JWT) |
| latitude | Latitude |
| longitude | Longitude |
| luhn_checksum | Luhn Algorithm Checksum (for strings, and (u)int) |
hf-kklein marked this conversation as resolved.
Show resolved Hide resolved
| postcode_iso3166_alpha2 | Postcode |
| postcode_iso3166_alpha2_field | Postcode |
| rgb | RGB String |
Expand Down
63 changes: 46 additions & 17 deletions baked_in.go
Expand Up @@ -214,6 +214,7 @@ var (
"semver": isSemverFormat,
"dns_rfc1035_label": isDnsRFC1035LabelFormat,
"credit_card": isCreditCard,
"luhn_checksum": hasLuhnChecksum,
}
)

Expand Down Expand Up @@ -2487,6 +2488,29 @@ 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
}

// 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()
Expand All @@ -2505,22 +2529,27 @@ 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)
}
7 changes: 6 additions & 1 deletion doc.go
Expand Up @@ -1323,8 +1323,13 @@ This validates that a string value contains a valid credit card number using Luh

Usage: credit_card

Alias Validators and Tags
Luhn Checksum

This validates that a string or (u)int value contains a valid checksum using the Luhn algorithm.

Usage: luhn_checksum

Alias Validators and Tags
NOTE: When returning an error, the tag returned in "FieldError" will be
the alias tag unless the dive tag is part of the alias. Everything after the
dive tag is not reported as the alias tag. Also, the "ActualTag" in the before
Expand Down
45 changes: 45 additions & 0 deletions validator_test.go
Expand Up @@ -12300,6 +12300,51 @@ func TestCreditCardFormatValidation(t *testing.T) {
}
}

func TestLuhnChecksumValidation(t *testing.T) {
testsUint := []struct {
value interface{} `validate:"luhn_checksum"` // the type is interface{} because the luhn_checksum works on both strings and numbers
tag string
expected bool
}{
{uint64(586824160825533338), "luhn_checksum", true}, // credit card numbers are just special cases of numbers with luhn checksum
{586824160825533338, "luhn_checksum", true},
{"586824160825533338", "luhn_checksum", true},
{uint64(586824160825533328), "luhn_checksum", false},
{586824160825533328, "luhn_checksum", false},
{"586824160825533328", "luhn_checksum", false},
{10000000116, "luhn_checksum", true}, // but there may be shorter numbers (11 digits)
{"10000000116", "luhn_checksum", true},
{10000000117, "luhn_checksum", false},
{"10000000117", "luhn_checksum", false},
{uint64(12345678123456789011), "luhn_checksum", true}, // or longer numbers (19 digits)
{"12345678123456789011", "luhn_checksum", true},
{1, "luhn_checksum", false}, // single digits (checksum only) are not allowed
{"1", "luhn_checksum", false},
{-10, "luhn_checksum", false}, // negative ints are not allowed
{"abcdefghijklmnop", "luhn_checksum", false},
}

validate := New()

for i, test := range testsUint {
errs := validate.Var(test.value, test.tag)
if test.expected {
if !IsEqual(errs, nil) {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
}
} else {
if IsEqual(errs, nil) {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
} else {
val := getError(errs, "", "")
if val.Tag() != "luhn_checksum" {
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
}
}
}
}
}

func TestMultiOrOperatorGroup(t *testing.T) {
tests := []struct {
Value int `validate:"eq=1|gte=5,eq=1|lt=7"`
Expand Down