Skip to content

Commit 6da2ad9

Browse files
add some extra coverage to the IBM037 encoding to extend ASCII compatibility when in FRB_COMPATIBILITY_MODE (#386)
1 parent 95b483b commit 6da2ad9

14 files changed

+139
-58
lines changed

checkDetail.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func (cd *CheckDetail) Validate() error {
258258
if cd.DocumentationTypeIndicator != "" {
259259
// Z is valid for CashLetter DocumentationTypeIndicator only
260260
if cd.DocumentationTypeIndicator == "Z" {
261-
msg := fmt.Sprint(msgDocumentationTypeIndicator)
261+
msg := msgDocumentationTypeIndicator
262262
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: cd.DocumentationTypeIndicator, Msg: msg}
263263
}
264264
if err := cd.isDocumentationTypeIndicator(cd.DocumentationTypeIndicator); err != nil {

checkDetailAddendumA_test.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func TestCDAddendumATruncationIndicatorFRB(t *testing.T) {
187187
var e *FieldError
188188
require.ErrorAs(t, err, &e)
189189
require.Equal(t, "TruncationIndicator", e.FieldName)
190-
t.Setenv(FRBCompatibilityMode, "")
190+
t.Setenv(FRBCompatibilityMode, "true")
191191
require.NoError(t, cdAddendumA.Validate())
192192
}
193193

@@ -308,3 +308,57 @@ func TestStringFieldTrim(t *testing.T) {
308308
cdAddendumA.ReturnLocationRoutingNumber = "12345678912345"
309309
require.Len(t, cdAddendumA.ReturnLocationRoutingNumberField(), 9)
310310
}
311+
312+
func TestParseCheckDetailAddendumA_BOFDAccountEBCDIC(t *testing.T) {
313+
t.Setenv("FRB_COMPATIBILITY_MODE", "true")
314+
line := "\xf2\xf6" + // Record Type 26
315+
strings.Repeat("\xf1", 33) + // Fill with '1's
316+
"@@@@@@@@@@@" + // Spaces
317+
"\xad\x85\x94\x97\xa3\xa8\xbd" + // [empty] in IBM1047
318+
strings.Repeat("@", 20) + // More spaces
319+
"\xe8\xf2\xf0@@@@" // End padding
320+
r := NewReader(strings.NewReader(line), ReadEbcdicEncodingOption())
321+
r.line = line
322+
323+
clh := mockCashLetterHeader()
324+
r.addCurrentCashLetter(NewCashLetter(clh))
325+
bh := mockBundleHeader()
326+
b := NewBundle(bh)
327+
r.currentCashLetter.AddBundle(b)
328+
r.addCurrentBundle(b)
329+
cd := mockCheckDetail()
330+
r.currentCashLetter.currentBundle.AddCheckDetail(cd)
331+
332+
err := r.parseCheckDetailAddendumA()
333+
require.NoError(t, err)
334+
335+
record := r.currentCashLetter.currentBundle.GetChecks()[0].CheckDetailAddendumA[0]
336+
require.Equal(t, "[empty]", record.BOFDAccountNumber)
337+
t.Setenv("FRB_COMPATIBILITY_MODE", "")
338+
}
339+
340+
func TestParseCheckDetailAddendumA_BOFDAccountEBCDIC_NoFlag(t *testing.T) {
341+
t.Setenv("FRB_COMPATIBILITY_MODE", "false")
342+
line := "\xf2\xf6" +
343+
strings.Repeat("\xf1", 33) +
344+
"@@@@@@@@@@@" +
345+
"\xad\x85\x94\x97\xa3\xa8\xbd" +
346+
strings.Repeat("@", 20) +
347+
"\xe8\xf2\xf0@@@@"
348+
349+
r := NewReader(strings.NewReader(line), ReadEbcdicEncodingOption())
350+
r.line = line
351+
352+
clh := mockCashLetterHeader()
353+
r.addCurrentCashLetter(NewCashLetter(clh))
354+
bh := mockBundleHeader()
355+
b := NewBundle(bh)
356+
r.currentCashLetter.AddBundle(b)
357+
r.addCurrentBundle(b)
358+
cd := mockCheckDetail()
359+
r.currentCashLetter.currentBundle.AddCheckDetail(cd)
360+
361+
err := r.parseCheckDetailAddendumA()
362+
require.Error(t, err, "Expected an error when FRB_COMPATIBILITY_MODE is false")
363+
t.Setenv("FRB_COMPATIBILITY_MODE", "")
364+
}

config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ package imagecashletter
66

77
import (
88
"os"
9+
"strings"
910
)
1011

1112
const FRBCompatibilityMode = "FRB_COMPATIBILITY_MODE"
1213

1314
// Determine if FRB (Federal Reserve Bank) compatibility mode is enabled
1415
func IsFRBCompatibilityModeEnabled() bool {
15-
_, ok := os.LookupEnv(FRBCompatibilityMode)
16-
return ok
16+
return strings.ToLower(os.Getenv("FRB_COMPATIBILITY_MODE")) == "true"
1717
}

config_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212

1313
// TestMockBundleChecks creates a Bundle of checks
1414
func TestFRBCompatibilityMode(t *testing.T) {
15-
assert.Equal(t, IsFRBCompatibilityModeEnabled(), false)
16-
t.Setenv(FRBCompatibilityMode, "")
17-
assert.Equal(t, IsFRBCompatibilityModeEnabled(), true)
15+
t.Setenv(FRBCompatibilityMode, "false")
16+
assert.False(t, IsFRBCompatibilityModeEnabled())
17+
t.Setenv(FRBCompatibilityMode, "true")
18+
assert.True(t, IsFRBCompatibilityModeEnabled())
1819
}

creditItem.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,12 @@ func (ci *CreditItem) Validate() error {
211211
if ci.DocumentationTypeIndicator != "" {
212212
// Z is valid for CashLetter DocumentationTypeIndicator only
213213
if ci.DocumentationTypeIndicator == "Z" {
214-
msg := fmt.Sprint(msgDocumentationTypeIndicator)
214+
msg := msgDocumentationTypeIndicator
215215
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: ci.DocumentationTypeIndicator, Msg: msg}
216216
}
217217
// M is not valid for CreditItem DocumentationTypeIndicator
218218
if ci.DocumentationTypeIndicator == "M" {
219-
msg := fmt.Sprint(msgDocumentationTypeIndicator)
219+
msg := msgDocumentationTypeIndicator
220220
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: ci.DocumentationTypeIndicator, Msg: msg}
221221
}
222222
if err := ci.isDocumentationTypeIndicator(ci.DocumentationTypeIndicator); err != nil {

imageViewDetail_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ func TestIVDetailDigitalSignatureMethodFRB(t *testing.T) {
220220
require.ErrorAs(t, err, &e)
221221
require.Equal(t, "DigitalSignatureMethod", e.FieldName)
222222
// "0" should be accepted in FRB compatibility mode
223-
t.Setenv(FRBCompatibilityMode, "")
223+
t.Setenv(FRBCompatibilityMode, "true")
224224
require.NoError(t, ivDetail.Validate())
225225
}
226226

@@ -284,7 +284,7 @@ func TestIVDetailFIImageCreatorRoutingNumberFRB(t *testing.T) {
284284
var e *FieldError
285285
require.ErrorAs(t, err, &e)
286286
require.Equal(t, "ImageCreatorRoutingNumber", e.FieldName)
287-
t.Setenv(FRBCompatibilityMode, "")
287+
t.Setenv(FRBCompatibilityMode, "true")
288288
require.NoError(t, ivDetail.Validate())
289289
}
290290

issue125_test.go

+18-18
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,22 @@ func TestIssue125(t *testing.T) {
3737
}
3838

3939
// check each record count
40-
assert.Equal(t, counts["01"], 1)
41-
assert.Equal(t, counts["10"], 2)
42-
assert.Equal(t, counts["20"], 4)
43-
assert.Equal(t, counts["25"], 4)
44-
assert.Equal(t, counts["26"], 4)
45-
assert.Equal(t, counts["27"], 4)
46-
assert.Equal(t, counts["28"], 4)
47-
assert.Equal(t, counts["31"], 4)
48-
assert.Equal(t, counts["32"], 4)
49-
assert.Equal(t, counts["33"], 4)
50-
assert.Equal(t, counts["34"], 4)
51-
assert.Equal(t, counts["35"], 4)
52-
assert.Equal(t, counts["50"], 8)
53-
assert.Equal(t, counts["52"], 8)
54-
assert.Equal(t, counts["54"], 8)
55-
assert.Equal(t, counts["70"], 4)
56-
assert.Equal(t, counts["90"], 2)
57-
assert.Equal(t, counts["99"], 1)
40+
assert.Equal(t, 1, counts["01"])
41+
assert.Equal(t, 2, counts["10"])
42+
assert.Equal(t, 4, counts["20"])
43+
assert.Equal(t, 4, counts["25"])
44+
assert.Equal(t, 4, counts["26"])
45+
assert.Equal(t, 4, counts["27"])
46+
assert.Equal(t, 4, counts["28"])
47+
assert.Equal(t, 4, counts["31"])
48+
assert.Equal(t, 4, counts["32"])
49+
assert.Equal(t, 4, counts["33"])
50+
assert.Equal(t, 4, counts["34"])
51+
assert.Equal(t, 4, counts["35"])
52+
assert.Equal(t, 8, counts["50"])
53+
assert.Equal(t, 8, counts["52"])
54+
assert.Equal(t, 8, counts["54"])
55+
assert.Equal(t, 4, counts["70"])
56+
assert.Equal(t, 2, counts["90"])
57+
assert.Equal(t, 1, counts["99"])
5858
}

reader.go

+37-11
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,16 @@ func (r *Reader) parseCheckDetail() error {
418418
func (r *Reader) parseCheckDetailAddendumA() error {
419419
r.recordName = "CheckDetailAddendumA"
420420
if r.currentCashLetter.currentBundle.GetChecks() == nil {
421-
msg := fmt.Sprint(msgFileBundleOutside)
421+
msg := msgFileBundleOutside
422422
return r.error(&FileError{FieldName: "CheckDetailAddendumA", Msg: msg})
423423
}
424-
lineOut, err := r.decodeLine(r.line)
424+
inputBytes := []byte(r.line)
425+
adjustedBytes := handleIBM1047Compatibility(inputBytes)
426+
lineOut, err := r.decodeLine(string(adjustedBytes))
425427
if err != nil {
426428
return err
427429
}
430+
428431
cdAddendumA := NewCheckDetailAddendumA()
429432
cdAddendumA.Parse(lineOut)
430433
if err := cdAddendumA.Validate(); err != nil {
@@ -436,11 +439,34 @@ func (r *Reader) parseCheckDetailAddendumA() error {
436439
return nil
437440
}
438441

442+
func handleIBM1047Compatibility(input []byte) []byte {
443+
if !IsFRBCompatibilityModeEnabled() {
444+
return input
445+
}
446+
447+
output := make([]byte, len(input))
448+
copy(output, input)
449+
450+
// Replace bytes that map differently between IBM037 and IBM1047
451+
// but only for the ascii subset see https://en.wikibooks.org/wiki/Character_Encodings/Code_Tables/EBCDIC/EBCDIC_1047
452+
for i, b := range output {
453+
switch b {
454+
case 0xAD: // Ý -> [
455+
output[i] = 0xBA
456+
case 0xBD: // ¨ -> ]
457+
output[i] = 0xBB
458+
case 0x5F: // ¬ -> ^
459+
output[i] = 0xB0
460+
}
461+
}
462+
return output
463+
}
464+
439465
// parseCheckDetailAddendumB takes the input record string and parses the CheckDetailAddendumB values
440466
func (r *Reader) parseCheckDetailAddendumB() error {
441467
r.recordName = "CheckDetailAddendumB"
442468
if r.currentCashLetter.currentBundle.GetChecks() == nil {
443-
msg := fmt.Sprint(msgFileBundleOutside)
469+
msg := msgFileBundleOutside
444470
return r.error(&FileError{FieldName: "CheckDetailAddendumB", Msg: msg})
445471
}
446472
lineOut, err := r.decodeLine(r.line)
@@ -461,7 +487,7 @@ func (r *Reader) parseCheckDetailAddendumB() error {
461487
func (r *Reader) parseCheckDetailAddendumC() error {
462488
r.recordName = "CheckDetailAddendumC"
463489
if r.currentCashLetter.currentBundle.GetChecks() == nil {
464-
msg := fmt.Sprint(msgFileBundleOutside)
490+
msg := msgFileBundleOutside
465491
return r.error(&FileError{FieldName: "CheckDetailAddendumC", Msg: msg})
466492
}
467493
lineOut, err := r.decodeLine(r.line)
@@ -503,7 +529,7 @@ func (r *Reader) parseReturnDetail() error {
503529
func (r *Reader) parseReturnDetailAddendumA() error {
504530
r.recordName = "ReturnDetailAddendumA"
505531
if r.currentCashLetter.currentBundle.GetReturns() == nil {
506-
msg := fmt.Sprint(msgFileBundleOutside)
532+
msg := msgFileBundleOutside
507533
return r.error(&FileError{FieldName: "ReturnDetailAddendumA", Msg: msg})
508534
}
509535
lineOut, err := r.decodeLine(r.line)
@@ -525,7 +551,7 @@ func (r *Reader) parseReturnDetailAddendumA() error {
525551
func (r *Reader) parseReturnDetailAddendumB() error {
526552
r.recordName = "ReturnDetailAddendumB"
527553
if r.currentCashLetter.currentBundle.GetReturns() == nil {
528-
msg := fmt.Sprint(msgFileBundleOutside)
554+
msg := msgFileBundleOutside
529555
return r.error(&FileError{FieldName: "ReturnDetailAddendumB", Msg: msg})
530556
}
531557
lineOut, err := r.decodeLine(r.line)
@@ -546,7 +572,7 @@ func (r *Reader) parseReturnDetailAddendumB() error {
546572
func (r *Reader) parseReturnDetailAddendumC() error {
547573
r.recordName = "ReturnDetailAddendumC"
548574
if r.currentCashLetter.currentBundle.GetReturns() == nil {
549-
msg := fmt.Sprint(msgFileBundleOutside)
575+
msg := msgFileBundleOutside
550576
return r.error(&FileError{FieldName: "ReturnDetailAddendumC", Msg: msg})
551577
}
552578
lineOut, err := r.decodeLine(r.line)
@@ -568,7 +594,7 @@ func (r *Reader) parseReturnDetailAddendumD() error {
568594
r.recordName = "ReturnDetailAddendumD"
569595

570596
if r.currentCashLetter.currentBundle.GetReturns() == nil {
571-
msg := fmt.Sprint(msgFileBundleOutside)
597+
msg := msgFileBundleOutside
572598
return r.error(&FileError{FieldName: "ReturnDetailAddendumD", Msg: msg})
573599
}
574600
lineOut, err := r.decodeLine(r.line)
@@ -622,7 +648,7 @@ func (r *Reader) ImageViewDetail() error {
622648
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
623649
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewDetail(ivDetail)
624650
} else {
625-
msg := fmt.Sprint(msgFileBundleOutside)
651+
msg := msgFileBundleOutside
626652
return r.error(&FileError{FieldName: "ImageViewDetail", Msg: msg})
627653
}
628654

@@ -658,7 +684,7 @@ func (r *Reader) ImageViewData() error {
658684
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
659685
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewData(ivData)
660686
} else {
661-
msg := fmt.Sprint(msgFileBundleOutside)
687+
msg := msgFileBundleOutside
662688
return r.error(&FileError{FieldName: "ImageViewData", Msg: msg})
663689
}
664690

@@ -702,7 +728,7 @@ func (r *Reader) ImageViewAnalysis() error {
702728
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
703729
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewAnalysis(ivAnalysis)
704730
} else {
705-
msg := fmt.Sprint(msgFileBundleOutside)
731+
msg := msgFileBundleOutside
706732
return r.error(&FileError{FieldName: "ImageViewAnalysis", Msg: msg})
707733
}
708734

reader_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestICL_ReadVariableLineLengthOption(t *testing.T) {
7171
expected, err := os.ReadFile(filepath.Join("test", "testdata", "valid-x937.json"))
7272
require.NoError(t, err)
7373

74-
require.Equal(t, string(actual), string(expected))
74+
require.Equal(t, string(expected), string(actual))
7575
}
7676

7777
func TestICL_EBCDICEncodingOption(t *testing.T) {
@@ -96,7 +96,7 @@ func TestICL_EBCDICEncodingOption(t *testing.T) {
9696
expected, err := os.ReadFile(filepath.Join("test", "testdata", "valid-x937.json"))
9797
require.NoError(t, err)
9898

99-
require.Equal(t, string(actual), string(expected))
99+
require.Equal(t, string(expected), string(actual))
100100
}
101101

102102
func getFileError(t *testing.T, err error) *FileError {
@@ -801,8 +801,8 @@ func TestICLCreditRecord61File(t *testing.T) {
801801

802802
// ensure we have a validated file structure
803803
require.NoError(t, iclFile.Validate())
804-
require.Equal(t, 2, len(iclFile.CashLetters))
805-
require.Equal(t, 1, len(iclFile.CashLetters[0].Credits))
804+
require.Len(t, iclFile.CashLetters, 2)
805+
require.Len(t, iclFile.CashLetters[0].Credits, 1)
806806
}
807807

808808
func TestICLBase64ImageData(t *testing.T) {
@@ -874,5 +874,5 @@ func Test_DecodeEBCDIC(t *testing.T) {
874874
require.NoError(t, err)
875875
r, n := utf8.DecodeRuneInString(decoded)
876876
require.Equal(t, 3, n)
877-
require.Equal(t, r, utf8.RuneError)
877+
require.Equal(t, utf8.RuneError, r)
878878
}

returnDetail.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (rd *ReturnDetail) Validate() error {
280280
if rd.DocumentationTypeIndicator != "" {
281281
// Z is valid for CashLetter DocumentationTypeIndicator only
282282
if rd.DocumentationTypeIndicator == "Z" {
283-
msg := fmt.Sprint(msgDocumentationTypeIndicator)
283+
msg := msgDocumentationTypeIndicator
284284
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: rd.DocumentationTypeIndicator, Msg: msg}
285285
}
286286
if err := rd.isDocumentationTypeIndicator(rd.DocumentationTypeIndicator); err != nil {
@@ -308,7 +308,7 @@ func (rd *ReturnDetail) Validate() error {
308308
_, arc := AdministrativeReturnCodeDict[rd.ReturnReason]
309309
if !crc && !arc {
310310
// Return msgReturnCode
311-
msg := fmt.Sprint(msgReturnCode)
311+
msg := msgReturnCode
312312
return &FieldError{FieldName: "ReturnReason", Value: rd.ReturnReason, Msg: msg}
313313
}
314314
return nil

returnDetailAddendumA_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func TestRDAddendumAFIBOFDEndorsementDate(t *testing.T) {
330330
func TestRDAddendumAFIBOFDEndorsementDateFRB(t *testing.T) {
331331
rdAddendumA := mockReturnDetailAddendumA()
332332
rdAddendumA.BOFDEndorsementDate = time.Time{}
333-
t.Setenv(FRBCompatibilityMode, "")
333+
t.Setenv(FRBCompatibilityMode, "true")
334334
require.NoError(t, rdAddendumA.Validate())
335335
}
336336

0 commit comments

Comments
 (0)