From 9c764ff908d1b8d6502fce5296d8252a2069e5ed Mon Sep 17 00:00:00 2001 From: David Norris Date: Wed, 4 Dec 2024 14:47:29 -0500 Subject: [PATCH] feat: add IBM1047 ASCII compatibility when in FRB_COMPATIBILITY_MODE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for IBM1047 EBCDIC encoding compatibility with IBM037 for the three ASCII characters that differ between the encodings: [ ] ^ When FRB_COMPATIBILITY_MODE is enabled, converts IBM037 byte values to their IBM1047 equivalents before decoding: - 0xAD (Ý) -> 0xBA ([) - 0xBD (¨) -> 0xBB (]) - 0x5F (¬) -> 0xB0 (^) --- checkDetailAddendumA_test.go | 56 +++++++++++++++++++++++++++++++++++- reader.go | 28 +++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/checkDetailAddendumA_test.go b/checkDetailAddendumA_test.go index a35ee057..03b2ef95 100644 --- a/checkDetailAddendumA_test.go +++ b/checkDetailAddendumA_test.go @@ -187,7 +187,7 @@ func TestCDAddendumATruncationIndicatorFRB(t *testing.T) { var e *FieldError require.ErrorAs(t, err, &e) require.Equal(t, "TruncationIndicator", e.FieldName) - t.Setenv(FRBCompatibilityMode, "") + t.Setenv(FRBCompatibilityMode, "true") require.NoError(t, cdAddendumA.Validate()) } @@ -308,3 +308,57 @@ func TestStringFieldTrim(t *testing.T) { cdAddendumA.ReturnLocationRoutingNumber = "12345678912345" require.Len(t, cdAddendumA.ReturnLocationRoutingNumberField(), 9) } + +func TestParseCheckDetailAddendumA_BOFDAccountEBCDIC(t *testing.T) { + t.Setenv("FRB_COMPATIBILITY_MODE", "true") + line := "\xf2\xf6" + // Record Type 26 + strings.Repeat("\xf1", 33) + // Fill with '1's + "@@@@@@@@@@@" + // Spaces + "\xad\x85\x94\x97\xa3\xa8\xbd" + // [empty] in IBM1047 + strings.Repeat("@", 20) + // More spaces + "\xe8\xf2\xf0@@@@" // End padding + r := NewReader(strings.NewReader(line), ReadEbcdicEncodingOption()) + r.line = line + + clh := mockCashLetterHeader() + r.addCurrentCashLetter(NewCashLetter(clh)) + bh := mockBundleHeader() + b := NewBundle(bh) + r.currentCashLetter.AddBundle(b) + r.addCurrentBundle(b) + cd := mockCheckDetail() + r.currentCashLetter.currentBundle.AddCheckDetail(cd) + + err := r.parseCheckDetailAddendumA() + require.NoError(t, err) + + record := r.currentCashLetter.currentBundle.GetChecks()[0].CheckDetailAddendumA[0] + require.Equal(t, "[empty]", record.BOFDAccountNumber) + t.Setenv("FRB_COMPATIBILITY_MODE", "") +} + +func TestParseCheckDetailAddendumA_BOFDAccountEBCDIC_NoFlag(t *testing.T) { + t.Setenv("FRB_COMPATIBILITY_MODE", "false") + line := "\xf2\xf6" + + strings.Repeat("\xf1", 33) + + "@@@@@@@@@@@" + + "\xad\x85\x94\x97\xa3\xa8\xbd" + + strings.Repeat("@", 20) + + "\xe8\xf2\xf0@@@@" + + r := NewReader(strings.NewReader(line), ReadEbcdicEncodingOption()) + r.line = line + + clh := mockCashLetterHeader() + r.addCurrentCashLetter(NewCashLetter(clh)) + bh := mockBundleHeader() + b := NewBundle(bh) + r.currentCashLetter.AddBundle(b) + r.addCurrentBundle(b) + cd := mockCheckDetail() + r.currentCashLetter.currentBundle.AddCheckDetail(cd) + + err := r.parseCheckDetailAddendumA() + require.Error(t, err, "Expected an error when FRB_COMPATIBILITY_MODE is false") + t.Setenv("FRB_COMPATIBILITY_MODE", "") +} diff --git a/reader.go b/reader.go index 144725eb..3c4ef9ff 100644 --- a/reader.go +++ b/reader.go @@ -421,10 +421,13 @@ func (r *Reader) parseCheckDetailAddendumA() error { msg := fmt.Sprint(msgFileBundleOutside) return r.error(&FileError{FieldName: "CheckDetailAddendumA", Msg: msg}) } - lineOut, err := r.decodeLine(r.line) + inputBytes := []byte(r.line) + adjustedBytes := handleIBM1047Compatibility(inputBytes) + lineOut, err := r.decodeLine(string(adjustedBytes)) if err != nil { return err } + cdAddendumA := NewCheckDetailAddendumA() cdAddendumA.Parse(lineOut) if err := cdAddendumA.Validate(); err != nil { @@ -436,6 +439,29 @@ func (r *Reader) parseCheckDetailAddendumA() error { return nil } +func handleIBM1047Compatibility(input []byte) []byte { + if !IsFRBCompatibilityModeEnabled() { + return input + } + + output := make([]byte, len(input)) + copy(output, input) + + // Replace bytes that map differently between IBM037 and IBM1047 + // but only for the ascii subset see https://en.wikibooks.org/wiki/Character_Encodings/Code_Tables/EBCDIC/EBCDIC_1047 + for i, b := range output { + switch b { + case 0xAD: // Ý -> [ + output[i] = 0xBA + case 0xBD: // ¨ -> ] + output[i] = 0xBB + case 0x5F: // ¬ -> ^ + output[i] = 0xB0 + } + } + return output +} + // parseCheckDetailAddendumB takes the input record string and parses the CheckDetailAddendumB values func (r *Reader) parseCheckDetailAddendumB() error { r.recordName = "CheckDetailAddendumB"