moov-io / iso8583

A golang implementation to marshal and unmarshal iso8583 message.
https://moov.io
Apache License 2.0
304 stars 100 forks source link

Numeric field of all zeros not encoded correctly #238

Closed fresanov closed 11 months ago

fresanov commented 1 year ago

I am trying to construct a message which has processing code of all zeros. This is a common situation, combination of MTI 0100 and processing code 000000 represents sale. However I get an error when trying to decode such a message: failed to unpack field 3 (Processing Code): failed to decode content: not enough data to decode. expected len 3, got 1

Here is my code for setting fields:

request := iso8583.NewMessage(iso.Spec)
    request.MTI("0200")
    request.Field(2, "4242424242424242")
    request.Field(3, "000000")

My spec for these fields:

               0: field.NewString(&field.Spec{
            Length:      4,
            Description: "Message Type Indicator",
            Enc:         encoding.BCD,
            Pref:        prefix.BCD.Fixed,
        }),
        1: field.NewBitmap(&field.Spec{
            Description: "Bitmap",
            Enc:         encoding.Binary,
            Pref:        prefix.Binary.Fixed,
        }),2: field.NewString(&field.Spec{
            Length:      19,
            Description: "Primary Account Number",
            Enc:         encoding.BCD,
            Pref:        prefix.BCD.LL,
        }),
        3: field.NewNumeric(&field.Spec{
            Length:      6,
            Description: "Processing Code",
            Enc:         encoding.BCD,
            Pref:        prefix.BCD.Fixed,
        }),

I captured the message with wireshark, I think the issue is that the processing code in this case in encoded as one 0x00 byte. Instead it should be encoded as three zero bytes: 0x00 0x00 0x00

alovak commented 1 year ago

@fresanov if it's still relevant, here are some questions to clarify the issue:

alovak commented 1 year ago

@fresanov reviewing some code I found why you saw one byte instead of 3.

Let's review the process step by step.

  1. We have a spec, where the processing code is field.Numeric
  2. We set it's value request.Field(3, "000000") by using string representation of the number
  3. Here is what request.Field method does:
func (m *Message) Field(id int, val string) error {
    if f, ok := m.fields[id]; ok { // <--- we find the field
        //...
        return f.SetBytes([]byte(val)) // <--- we call `SetBytes` method on the field
    }
    // ...
}
  1. Let's look into SetBytes of the Numeric field. In our case it will be the else branch:
func (f *Numeric) SetBytes(b []byte) error {
    if len(b) == 0 {
        // ...
    } else {
        // otherwise parse the raw to an int
        val, err := strconv.Atoi(string(b)) // <--- if we convert "000000" into `int` it will be 0
        if err != nil {
            return utils.NewSafeError(err, "failed to convert into number")
        }
        f.value = val
    }

    // ...
}

as you can see we convert processing code "000000" into int and the underlying value of the field is 0.

When you pack it, it results in a single byte (0x00). That explains why you see a single byte and not three bytes. Receiving side will fail to unpack it as it expects to get 3 bytes, not 1.

You may ask why it does not fail as the length of the packed data does not match the specified length 6. As far as I remember, there were some issues with both BCD encoding and returning errors when the packed length didn't match the specified one.

Possible solutions to the issue

  1. Keep it as Numeric but set padding for the field like this:

        3: field.NewNumeric(&field.Spec{
            Length:      6,
            Description: "Processing Code",
            Enc:         encoding.BCD,
            Pref:        prefix.BCD.Fixed,
            Pad:         padding.Left('0'), 
        }),

    when the field is packed, the value will be padded with 0 on the left side. When the data for the field is unpacked, 0 will be removed from the left side, resulting in a single 0 (int).

  2. Use field.String instead of field.Numeric and then your 000000 will stay as is during packing and unpacking.

I apologize for the delayed resolution of the issue.

fresanov commented 1 year ago

@alovak Thank you for the response. I determined that the root cause of this behavior is this method in numeric.go file in "field" package:

func (f *Numeric) SetBytes(b []byte) error {
    if len(b) == 0 {
        // for a length 0 raw, string(raw) would become "" which makes Atoi return an error
        // however for example "0000" (value 0 left-padded with '0') should have 0 as output, not an error
        // so if the length of raw is 0, set f.value to 0 instead of parsing the raw
        f.value = 0
    } else {
        // otherwise parse the raw to an int
        val, err := strconv.Atoi(string(b))
        if err != nil {
            return utils.NewSafeError(err, "failed to convert into number")
        }
        f.value = val
    }

    if f.data != nil {
        *(f.data) = *f
    }
    return nil
}

On line 50 ( val, err := strconv.Atoi(string(b)) ) casting a byte slice of zeros of any length to string will result in single '0' character string and subsequently when converting it to int this will result in a single 0 digit.

I will try your workaround. Thank you.

alovak commented 11 months ago

@fresanov please, feel free to close the issue if the solution worked for you