Repository: moov-io/ach
Pull request #1449 (2024-07-17 22:09:37)

fix: expand which latin characters are accepted
Fixes: https://github.com/moov-io/ach/issues/1448


Patch:
diff --git a/fileHeader.go b/fileHeader.go
index f8d5e596..f3a0b180 100644
--- a/fileHeader.go
+++ b/fileHeader.go
@@ -211,7 +211,7 @@ func (fh *FileHeader) ValidateWith(opts *ValidateOpts) error {
 	if err := fh.fieldInclusion(); err != nil {
 		return err
 	}
-	if err := fh.isUpperAlphanumeric(fh.FileIDModifier); err != nil {
+	if err := fh.isUpperASCII(fh.FileIDModifier); err != nil {
 		return fieldError("FileIDModifier", err, fh.FileIDModifier)
 	}
 	if len(fh.FileIDModifier) != 1 {
diff --git a/validators.go b/validators.go
index df4df001..cd6d6e6a 100644
--- a/validators.go
+++ b/validators.go
@@ -28,29 +28,6 @@ import (
 	"unicode/utf8"
 )
 
-var (
-	lowerAlphaCharacters  = "abcdefghijklmnopqrstuvwxyz"
-	numericCharacters     = "0123456789"
-	asciiCharacters       = ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`"
-	ebcdicExtraCharacters = `¢¬¦±`
-	realWorldEncountered  = `Ø`
-
-	validAlphaNumericCharacters          map[rune]bool
-	validUppercaseAlphaNumericCharacters map[rune]bool
-)
-
-func init() {
-	validAlphaNumericCharacters = setupCharacterMap(
-		lowerAlphaCharacters, strings.ToUpper(lowerAlphaCharacters), numericCharacters, asciiCharacters,
-		ebcdicExtraCharacters, realWorldEncountered,
-	)
-
-	validUppercaseAlphaNumericCharacters = setupCharacterMap(
-		strings.ToUpper(lowerAlphaCharacters), numericCharacters, asciiCharacters,
-		ebcdicExtraCharacters, realWorldEncountered,
-	)
-}
-
 // validator is common validation and formatting of golang types to ach type strings
 type validator struct{}
 
@@ -438,40 +415,43 @@ func (v *validator) isTransactionTypeCode(s string) error {
 	return ErrTransactionTypeCode
 }
 
-func setupCharacterMap(inputs ...string) map[rune]bool {
-	out := make(map[rune]bool)
-	for _, input := range inputs {
-		for _, r := range input {
-			out[r] = true
-		}
-	}
-	return out
-}
-
-func (v *validator) includesValidCharacters(input string, charset map[rune]bool) error {
-	for _, i := range input {
-		_, found := charset[i]
-		if !found {
-			return fmt.Errorf("invalid character: %v", i)
+// isUpperASCII checks if string only contains ASCII alphanumeric upper case characters
+func (v *validator) isUpperASCII(s string) error {
+	for _, r := range s {
+		if (r == 0x20) || (0x30 <= r && r <= 0x39) || 0x41 <= r && r <= 0x5A { // Space, 0 to 9, A to Z
+			continue
 		}
+		return fmt.Errorf("%w: %c", ErrUpperAlpha, r)
 	}
 	return nil
 }
 
-// isUpperAlphanumeric checks if string only contains ASCII alphanumeric upper case characters
-func (v *validator) isUpperAlphanumeric(s string) error {
-	err := v.includesValidCharacters(s, validUppercaseAlphaNumericCharacters)
-	if err != nil {
-		return fmt.Errorf("%w: %v", ErrUpperAlpha, err)
-	}
-	return nil
-}
+var (
+	slashZero = []rune(`Ø`)[0]
+)
 
 // isAlphanumeric checks if a string only contains ASCII alphanumeric characters
 func (v *validator) isAlphanumeric(s string) error {
-	err := v.includesValidCharacters(s, validAlphaNumericCharacters)
-	if err != nil {
-		return fmt.Errorf("%w: %v", ErrNonAlphanumeric, err)
+	for _, r := range s {
+		if 0x20 <= r && r <= 0x7E { // Space to ~ (Typical ASCII)
+			continue
+		}
+		if 0xC0 <= r && r <= 0xFF { // À to ÿ (Extended Latin Alphabet)
+			continue
+		}
+		// Specific characters that are accepted
+		switch r {
+		case
+			0xA2, // ¢ - Cent Sign
+			0xAC, // ¬ - Negation
+			0xA6, // ¦ - Pipe
+			0xB1, // ± - Plus or Minus Sign
+			slashZero:
+			continue
+
+			// case `¢`, `¬`, `¦`, `±`, `Ø`:
+		}
+		return fmt.Errorf("%w: %c", ErrNonAlphanumeric, r)
 	}
 	return nil
 }


Test patch:
diff --git a/validators_test.go b/validators_test.go
index bd1cb203..cc38d6ca 100644
--- a/validators_test.go
+++ b/validators_test.go
@@ -146,15 +146,15 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 			name:      "is alphanumeric",
 			checkFunc: v.isAlphanumeric,
 			shouldErr: func(i int) bool {
-				return i <= 0x1F || i > 0x7E
+				return (i <= 0x1F || i > 0x7E) && (i < 0xC0 || i > 0xFF)
 			},
 		},
-		// Ensure that ASCII characters from 0x20 to 0x60 and 0x7B to 0x7E are considered upper case alphanumeric.
+		// Ensure that ASCII characters from 0x41 to 0x5A are considered upper case ASCII.
 		{
-			name:      "is upper alphanumeric",
-			checkFunc: v.isUpperAlphanumeric,
+			name:      "is upper ascii",
+			checkFunc: v.isUpperASCII,
 			shouldErr: func(i int) bool {
-				return i <= 0x1F || i > 0x7E || (i > 0x60 && i < 0x7B)
+				return i != 0x20 && (i < 0x30 || i > 0x39) && (i < 0x41 || i > 0x5A)
 			},
 		},
 	}
@@ -187,13 +187,13 @@ func TestValidators__isAlphanumeric(t *testing.T) {
 func TestValidators__isAlphanumericExamples(t *testing.T) {
 	v := validator{}
 
-	validCases := []string{"Acme Corp!", `|`, `¦`, `¢`, `¬`, `±`}
+	validCases := []string{"Acme Corp!", `|`, `¦`, `¢`, `¬`, `±`, `ã`, `è`, `ñ`}
 	for i := range validCases {
 		err := v.isAlphanumeric(validCases[i])
 		require.NoError(t, err, fmt.Sprintf("input: %q", validCases[i]))
 	}
 
-	invalidCases := []string{`©`, `®`, `§101.1`, `ã`, `è`}
+	invalidCases := []string{`©`, `®`, `§101.1`}
 	for i := range invalidCases {
 		err := v.isAlphanumeric(invalidCases[i])
 		require.ErrorIs(t, err, ErrNonAlphanumeric, fmt.Sprintf("input: %q", invalidCases[i]))