Repository: moov-io/ach
Pull request #1262 (2023-07-17 14:51:19)

fix: allow full range of ASCII and EBCDIC characters accepted by Nacha
The 2022 Nacha rules state that Valid Characters are:

> 0-9, A-Z, a-z, space, EBCDIC values greater than hexadecimal "3F",
> ASCII values greater than hexadecimal "1F".

- [ASCII Characters](https://en.wikipedia.org/wiki/ASCII#Character_set)
- [EBCDIC Characters](https://en.wikipedia.org/wiki/EBCDIC)


Patch:
diff --git a/validators.go b/validators.go
index aa37573e..a20ccd2c 100644
--- a/validators.go
+++ b/validators.go
@@ -29,10 +29,13 @@ import (
 )
 
 var (
-	upperAlphanumericRegex = regexp.MustCompile(fmt.Sprintf(
-		`[^ A-Z0-9!"#$%%&'()*+,-.\\/:;<>=?@\[\]^_{}|~%s]+`, "`"))
-	alphanumericRegex = regexp.MustCompile(fmt.Sprintf(
-		`[^ \w!"#$%%&'()*+,-.\\/:;<>=?@\[\]^_{}|~%s]+`, "`"))
+	lowerAlphaCharacters  = "abcdefghijklmnopqrstuvwxyz"
+	numericCharacters     = "0123456789"
+	asciiCharacters       = ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`"
+	ebcdicExtraCharacters = `¢¬¦±`
+
+	validAlphaNumericCharacters          = lowerAlphaCharacters + strings.ToUpper(lowerAlphaCharacters) + numericCharacters + asciiCharacters + ebcdicExtraCharacters
+	validUppercaseAlphaNumericCharacters = strings.ToUpper(lowerAlphaCharacters) + numericCharacters + asciiCharacters + ebcdicExtraCharacters
 )
 
 // validator is common validation and formatting of golang types to ach type strings
@@ -422,19 +425,36 @@ func (v *validator) isTransactionTypeCode(s string) error {
 	return ErrTransactionTypeCode
 }
 
+func (v *validator) includesValidCharacters(input string, charset string) error {
+	for _, i := range input {
+		var found bool
+		for _, c := range charset {
+			if i == c {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return fmt.Errorf("invalid character: %v", i)
+		}
+	}
+	return nil
+}
+
 // isUpperAlphanumeric checks if string only contains ASCII alphanumeric upper case characters
 func (v *validator) isUpperAlphanumeric(s string) error {
-	if upperAlphanumericRegex.MatchString(s) {
-		return ErrUpperAlpha
+	err := v.includesValidCharacters(s, validUppercaseAlphaNumericCharacters)
+	if err != nil {
+		return fmt.Errorf("%w: %v", ErrUpperAlpha, err)
 	}
 	return nil
 }
 
 // isAlphanumeric checks if a string only contains ASCII alphanumeric characters
 func (v *validator) isAlphanumeric(s string) error {
-	if alphanumericRegex.MatchString(s) {
-		// ^[ A-Za-z0-9_@./#&+-]*$/
-		return ErrNonAlphanumeric
+	err := v.includesValidCharacters(s, validAlphaNumericCharacters)
+	if err != nil {
+		return fmt.Errorf("%w: %v", ErrNonAlphanumeric, err)
 	}
 	return nil
 }


Test patch:
diff --git a/validators_test.go b/validators_test.go
index 03e47d8a..ef118a8a 100644
--- a/validators_test.go
+++ b/validators_test.go
@@ -145,7 +145,7 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 			name:      "is alphanumeric",
 			checkFunc: v.isAlphanumeric,
 			shouldErr: func(i int) bool {
-				return i < 0x20 || i > 0x7E
+				return i <= 0x1F || i > 0x7E
 			},
 		},
 		// Ensure that ASCII characters from 0x20 to 0x60 and 0x7B to 0x7E are considered upper case alphanumeric.
@@ -153,7 +153,7 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 			name:      "is upper alphanumeric",
 			checkFunc: v.isUpperAlphanumeric,
 			shouldErr: func(i int) bool {
-				return i < 0x20 || i > 0x7E || (i > 0x60 && i < 0x7B)
+				return i <= 0x1F || i > 0x7E || (i > 0x60 && i < 0x7B)
 			},
 		},
 	}
@@ -165,6 +165,11 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 				err := tt.checkFunc(chr)
 				shouldError := tt.shouldErr(i)
 
+				switch chr {
+				case `¢`, `¦`, `¬`, `±`: // skip valid ASCII characters
+					continue
+				}
+
 				if shouldError && err == nil {
 					t.Errorf("expected rune %x (%s) to be non-alphanumeric", i, chr)
 				} else if !shouldError && err != nil {
@@ -175,6 +180,22 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 	}
 }
 
+func TestValidators__isAlphanumericExamples(t *testing.T) {
+	v := validator{}
+
+	validCases := []string{`|`, `¦`, `¢`, `¬`, `±`}
+	for i := range validCases {
+		err := v.isAlphanumeric(validCases[i])
+		require.NoError(t, err, fmt.Sprintf("input: %q", validCases[i]))
+	}
+
+	invalidCases := []string{`©`, `®`, `§101.1`, `ã`, `è`}
+	for i := range invalidCases {
+		err := v.isAlphanumeric(invalidCases[i])
+		require.ErrorIs(t, err, ErrNonAlphanumeric, fmt.Sprintf("input: %q", invalidCases[i]))
+	}
+}
+
 func TestValidators__validateJulianDay(t *testing.T) {
 	empty := "   "
 	cases := map[string]string{