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

add some extra coverage to the IBM037 encoding to extend ASCII compatibility when in FRB_COMPATIBILITY_MODE #386

Merged
merged 3 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion checkDetail.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func (cd *CheckDetail) Validate() error {
if cd.DocumentationTypeIndicator != "" {
// Z is valid for CashLetter DocumentationTypeIndicator only
if cd.DocumentationTypeIndicator == "Z" {
msg := fmt.Sprint(msgDocumentationTypeIndicator)
msg := msgDocumentationTypeIndicator
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: cd.DocumentationTypeIndicator, Msg: msg}
}
if err := cd.isDocumentationTypeIndicator(cd.DocumentationTypeIndicator); err != nil {
Expand Down
56 changes: 55 additions & 1 deletion checkDetailAddendumA_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down Expand Up @@ -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", "")
}
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ package imagecashletter

import (
"os"
"strings"
)

const FRBCompatibilityMode = "FRB_COMPATIBILITY_MODE"

// Determine if FRB (Federal Reserve Bank) compatibility mode is enabled
func IsFRBCompatibilityModeEnabled() bool {
_, ok := os.LookupEnv(FRBCompatibilityMode)
return ok
return strings.ToLower(os.Getenv("FRB_COMPATIBILITY_MODE")) == "true"
}
7 changes: 4 additions & 3 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (

// TestMockBundleChecks creates a Bundle of checks
func TestFRBCompatibilityMode(t *testing.T) {
assert.Equal(t, IsFRBCompatibilityModeEnabled(), false)
t.Setenv(FRBCompatibilityMode, "")
assert.Equal(t, IsFRBCompatibilityModeEnabled(), true)
t.Setenv(FRBCompatibilityMode, "false")
assert.False(t, IsFRBCompatibilityModeEnabled())
t.Setenv(FRBCompatibilityMode, "true")
assert.True(t, IsFRBCompatibilityModeEnabled())
}
4 changes: 2 additions & 2 deletions creditItem.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ func (ci *CreditItem) Validate() error {
if ci.DocumentationTypeIndicator != "" {
// Z is valid for CashLetter DocumentationTypeIndicator only
if ci.DocumentationTypeIndicator == "Z" {
msg := fmt.Sprint(msgDocumentationTypeIndicator)
msg := msgDocumentationTypeIndicator
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: ci.DocumentationTypeIndicator, Msg: msg}
}
// M is not valid for CreditItem DocumentationTypeIndicator
if ci.DocumentationTypeIndicator == "M" {
msg := fmt.Sprint(msgDocumentationTypeIndicator)
msg := msgDocumentationTypeIndicator
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: ci.DocumentationTypeIndicator, Msg: msg}
}
if err := ci.isDocumentationTypeIndicator(ci.DocumentationTypeIndicator); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions imageViewDetail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func TestIVDetailDigitalSignatureMethodFRB(t *testing.T) {
require.ErrorAs(t, err, &e)
require.Equal(t, "DigitalSignatureMethod", e.FieldName)
// "0" should be accepted in FRB compatibility mode
t.Setenv(FRBCompatibilityMode, "")
t.Setenv(FRBCompatibilityMode, "true")
require.NoError(t, ivDetail.Validate())
}

Expand Down Expand Up @@ -284,7 +284,7 @@ func TestIVDetailFIImageCreatorRoutingNumberFRB(t *testing.T) {
var e *FieldError
require.ErrorAs(t, err, &e)
require.Equal(t, "ImageCreatorRoutingNumber", e.FieldName)
t.Setenv(FRBCompatibilityMode, "")
t.Setenv(FRBCompatibilityMode, "true")
require.NoError(t, ivDetail.Validate())
}

Expand Down
36 changes: 18 additions & 18 deletions issue125_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@ func TestIssue125(t *testing.T) {
}

// check each record count
assert.Equal(t, counts["01"], 1)
assert.Equal(t, counts["10"], 2)
assert.Equal(t, counts["20"], 4)
assert.Equal(t, counts["25"], 4)
assert.Equal(t, counts["26"], 4)
assert.Equal(t, counts["27"], 4)
assert.Equal(t, counts["28"], 4)
assert.Equal(t, counts["31"], 4)
assert.Equal(t, counts["32"], 4)
assert.Equal(t, counts["33"], 4)
assert.Equal(t, counts["34"], 4)
assert.Equal(t, counts["35"], 4)
assert.Equal(t, counts["50"], 8)
assert.Equal(t, counts["52"], 8)
assert.Equal(t, counts["54"], 8)
assert.Equal(t, counts["70"], 4)
assert.Equal(t, counts["90"], 2)
assert.Equal(t, counts["99"], 1)
assert.Equal(t, 1, counts["01"])
assert.Equal(t, 2, counts["10"])
assert.Equal(t, 4, counts["20"])
assert.Equal(t, 4, counts["25"])
assert.Equal(t, 4, counts["26"])
assert.Equal(t, 4, counts["27"])
assert.Equal(t, 4, counts["28"])
assert.Equal(t, 4, counts["31"])
assert.Equal(t, 4, counts["32"])
assert.Equal(t, 4, counts["33"])
assert.Equal(t, 4, counts["34"])
assert.Equal(t, 4, counts["35"])
assert.Equal(t, 8, counts["50"])
assert.Equal(t, 8, counts["52"])
assert.Equal(t, 8, counts["54"])
assert.Equal(t, 4, counts["70"])
assert.Equal(t, 2, counts["90"])
assert.Equal(t, 1, counts["99"])
}
48 changes: 37 additions & 11 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,13 +418,16 @@ func (r *Reader) parseCheckDetail() error {
func (r *Reader) parseCheckDetailAddendumA() error {
r.recordName = "CheckDetailAddendumA"
if r.currentCashLetter.currentBundle.GetChecks() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := 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 {
Expand All @@ -436,11 +439,34 @@ 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"
if r.currentCashLetter.currentBundle.GetChecks() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "CheckDetailAddendumB", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand All @@ -461,7 +487,7 @@ func (r *Reader) parseCheckDetailAddendumB() error {
func (r *Reader) parseCheckDetailAddendumC() error {
r.recordName = "CheckDetailAddendumC"
if r.currentCashLetter.currentBundle.GetChecks() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "CheckDetailAddendumC", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand Down Expand Up @@ -503,7 +529,7 @@ func (r *Reader) parseReturnDetail() error {
func (r *Reader) parseReturnDetailAddendumA() error {
r.recordName = "ReturnDetailAddendumA"
if r.currentCashLetter.currentBundle.GetReturns() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ReturnDetailAddendumA", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand All @@ -525,7 +551,7 @@ func (r *Reader) parseReturnDetailAddendumA() error {
func (r *Reader) parseReturnDetailAddendumB() error {
r.recordName = "ReturnDetailAddendumB"
if r.currentCashLetter.currentBundle.GetReturns() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ReturnDetailAddendumB", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand All @@ -546,7 +572,7 @@ func (r *Reader) parseReturnDetailAddendumB() error {
func (r *Reader) parseReturnDetailAddendumC() error {
r.recordName = "ReturnDetailAddendumC"
if r.currentCashLetter.currentBundle.GetReturns() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ReturnDetailAddendumC", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand All @@ -568,7 +594,7 @@ func (r *Reader) parseReturnDetailAddendumD() error {
r.recordName = "ReturnDetailAddendumD"

if r.currentCashLetter.currentBundle.GetReturns() == nil {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ReturnDetailAddendumD", Msg: msg})
}
lineOut, err := r.decodeLine(r.line)
Expand Down Expand Up @@ -622,7 +648,7 @@ func (r *Reader) ImageViewDetail() error {
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewDetail(ivDetail)
} else {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ImageViewDetail", Msg: msg})
}

Expand Down Expand Up @@ -658,7 +684,7 @@ func (r *Reader) ImageViewData() error {
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewData(ivData)
} else {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ImageViewData", Msg: msg})
}

Expand Down Expand Up @@ -702,7 +728,7 @@ func (r *Reader) ImageViewAnalysis() error {
entryIndex := len(r.currentCashLetter.currentBundle.GetReturns()) - 1
r.currentCashLetter.currentBundle.Returns[entryIndex].AddImageViewAnalysis(ivAnalysis)
} else {
msg := fmt.Sprint(msgFileBundleOutside)
msg := msgFileBundleOutside
return r.error(&FileError{FieldName: "ImageViewAnalysis", Msg: msg})
}

Expand Down
10 changes: 5 additions & 5 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestICL_ReadVariableLineLengthOption(t *testing.T) {
expected, err := os.ReadFile(filepath.Join("test", "testdata", "valid-x937.json"))
require.NoError(t, err)

require.Equal(t, string(actual), string(expected))
require.Equal(t, string(expected), string(actual))
}

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

require.Equal(t, string(actual), string(expected))
require.Equal(t, string(expected), string(actual))
}

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

// ensure we have a validated file structure
require.NoError(t, iclFile.Validate())
require.Equal(t, 2, len(iclFile.CashLetters))
require.Equal(t, 1, len(iclFile.CashLetters[0].Credits))
require.Len(t, iclFile.CashLetters, 2)
require.Len(t, iclFile.CashLetters[0].Credits, 1)
}

func TestICLBase64ImageData(t *testing.T) {
Expand Down Expand Up @@ -874,5 +874,5 @@ func Test_DecodeEBCDIC(t *testing.T) {
require.NoError(t, err)
r, n := utf8.DecodeRuneInString(decoded)
require.Equal(t, 3, n)
require.Equal(t, r, utf8.RuneError)
require.Equal(t, utf8.RuneError, r)
}
4 changes: 2 additions & 2 deletions returnDetail.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func (rd *ReturnDetail) Validate() error {
if rd.DocumentationTypeIndicator != "" {
// Z is valid for CashLetter DocumentationTypeIndicator only
if rd.DocumentationTypeIndicator == "Z" {
msg := fmt.Sprint(msgDocumentationTypeIndicator)
msg := msgDocumentationTypeIndicator
return &FieldError{FieldName: "DocumentationTypeIndicator", Value: rd.DocumentationTypeIndicator, Msg: msg}
}
if err := rd.isDocumentationTypeIndicator(rd.DocumentationTypeIndicator); err != nil {
Expand Down Expand Up @@ -308,7 +308,7 @@ func (rd *ReturnDetail) Validate() error {
_, arc := AdministrativeReturnCodeDict[rd.ReturnReason]
if !crc && !arc {
// Return msgReturnCode
msg := fmt.Sprint(msgReturnCode)
msg := msgReturnCode
return &FieldError{FieldName: "ReturnReason", Value: rd.ReturnReason, Msg: msg}
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion returnDetailAddendumA_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func TestRDAddendumAFIBOFDEndorsementDate(t *testing.T) {
func TestRDAddendumAFIBOFDEndorsementDateFRB(t *testing.T) {
rdAddendumA := mockReturnDetailAddendumA()
rdAddendumA.BOFDEndorsementDate = time.Time{}
t.Setenv(FRBCompatibilityMode, "")
t.Setenv(FRBCompatibilityMode, "true")
require.NoError(t, rdAddendumA.Validate())
}

Expand Down
Loading
Loading