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

odd value parsing issue in ISOMessage #327

Open
anilgorgec opened this issue Sep 29, 2024 · 9 comments
Open

odd value parsing issue in ISOMessage #327

anilgorgec opened this issue Sep 29, 2024 · 9 comments

Comments

@anilgorgec
Copy link

Hello,

I have a question regarding odd numbers. for example the Track 2 Data field has a length of 37 characters, which is an odd number. Since ISO 8583 messages are often processed in hexadecimal or BCD format, they need to be byte-aligned.

I tried to parse the iso message in the example below.but I could not get the output as expected output.

Can you help me with this issue?

ISO Message:
020030200580208000000000000000000015001079890071002100375111111211111111d11110000000000000000f5445535430303031

output

MTI..........: 0200
Bitmap HEX...: 3020058020800000
Bitmap bits..:
    [1-8]00110000    [9-16]00100000   [17-24]00000101   [25-32]10000000
  [33-40]00100000   [41-48]10000000   [49-56]00000000   [57-64]00000000
F0   Message Type Indicator.................: 0200
F3   Processing Code........................: 0
F4   Transaction Amount.....................: 1500
F11  Systems Trace Audit Number (STAN)......: 107989
F22  Point of Sale (POS) Entry Mode.........: 007
F24  Function Code..........................: 100
F25  Point of Service Condition Code........: 21
F35  Track 2 Data...........................: =^^
F41  Card Acceptor Terminal Identification..: 37511111

expected output

MTI..........: 0200
Bitmap HEX...: 3020058020800000
Bitmap bits..:
    [1-8]00110000    [9-16]00100000   [17-24]00000101   [25-32]10000000
  [33-40]00100000   [41-48]10000000   [49-56]00000000   [57-64]00000000
F0   Message Type Indicator.................: 0200
F3   Processing Code........................: 0
F4   Transaction Amount.....................: 1500
F11  Systems Trace Audit Number (STAN)......: 107989
F22  Point of Sale (POS) Entry Mode.........: 0071
F24  Function Code..........................: 0021
F25  Point of Service Condition Code........: 00
F35  Track 2 Data...........................: 5111111211111111d11110000000000000000
F41  Card Acceptor Terminal Identification..: TEST0001 ```





@alovak
Copy link
Contributor

alovak commented Oct 4, 2024

Could you please share your (Go) spec here?

@carlosmgc2003
Copy link

carlosmgc2003 commented Oct 4, 2024

I've had exactly this problem and solved it by creating a custom Hex field and a custom BCD prefix:

		35: custom.NewTrack2(&field.Spec{
			Length:      37,
			Description: "Track 2 Data",
			Enc:         encoding.Binary,
			Pref:        custom.BCD.LL,
		}),

I could'nt manage to use moov/iso8583 built in types (perhaps it is possible to do so). I can share you this implementation here if you want and moderators allow.

@anilgorgec
Copy link
Author

Could you please share your (Go) spec here?

The spec is as follows.

var Spec87 = &iso8583.MessageSpec{
	Name: "ISO",
	Fields: map[int]field.Field{
		0: field.NewString(&field.Spec{
			Length:      4,
			Description: "Message Type Indicator",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		1: field.NewBitmap(&field.Spec{
			Length:      8,
			Description: "Bitmap",
			Enc:         encoding.BytesToASCIIHex,
			Pref:        prefix.Hex.Fixed,
		}),
		3: field.NewNumeric(&field.Spec{
			Length:      6,
			Description: "Processing Code",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		4: field.NewString(&field.Spec{
			Length:      12,
			Description: "Transaction Amount",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
			Pad:         padding.Left('0'),
		}),

		11: field.NewString(&field.Spec{
			Length:      6,
			Description: "Systems Trace Audit Number (STAN)",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		12: field.NewString(&field.Spec{
			Length:      6,
			Description: "Local Transaction Time",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		13: field.NewString(&field.Spec{
			Length:      4,
			Description: "Local Transaction Date",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		22: field.NewString(&field.Spec{
			Length:      3,
			Description: "Point of Sale (POS) Entry Mode",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),

		24: field.NewString(&field.Spec{
			Length:      3,
			Description: "Function Code",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		25: field.NewString(&field.Spec{
			Length:      2,
			Description: "Point of Service Condition Code",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
		35: field.NewTrack2(&field.Spec{
			Length:      37,
			Description: "Track 2 Data",
			Enc:         encoding.ASCIIHexToBytes,
			Pref:        prefix.ASCII.LL,
		}),
		41: field.NewString(&field.Spec{
			Length:      8,
			Description: "Card Acceptor Terminal Identification",
			Enc:         encoding.ASCII,
			Pref:        prefix.ASCII.Fixed,
		}),
	},
}

@anilgorgec
Copy link
Author

I've had exactly this problem and solved it by creating a custom Hex field and a custom BCD prefix:

		35: custom.NewTrack2(&field.Spec{
			Length:      37,
			Description: "Track 2 Data",
			Enc:         encoding.Binary,
			Pref:        custom.BCD.LL,
		}),

I could'nt manage to use moov/iso8583 built in types (perhaps it is possible to do so). I can share you this implementation here if you want and moderators allow.

if they allows, I wanna see it. It will give idea at least.

@alovak
Copy link
Contributor

alovak commented Oct 7, 2024

Hey! @anilgorgec , I'm not sure what this means: "Since ISO 8583 messages are often processed in hexadecimal or BCD format, they need to be byte-aligned."

Do you mean some fields of the message are in the BCD format? Is your whole message encoded in HEX? Because BCD is used only for decimal numbers, I'm not sure how it can be used to encode whole message.

In provided message:

ISO Message:
020030200580208000000000000000000015001079890071002100375111111211111111d11110000000000000000f5445535430303031

I see that fields are in ASCII and your spec also uses ASCII. The length prefix for the field 35 is ASCII.LL. The content of the field, as far as I see, is in ASCII. What is the reason for using encoding.ASCIIHexToBytes in the spec?

Also, please, check the length of the fields 22 and 24. In expected output you show 4 digits, but in the field spec, you set the length to 3.

My guess is that you might use BCD encoding for all numeric fields, with BCD prefix and Binary for the rest. You have to check the ISO 8583 specification of your provider.

@alovak
Copy link
Contributor

alovak commented Oct 7, 2024

@carlosmgc2003 sure, please share your implementation. I want to understand the case and to find out why it can't be done with existing encoders.

@carlosmgc2003
Copy link

Sure!
Our acquirer required us to encode track 2 in BCD, with a max lenght of 37. They would not allow a length of 38.
Another problem I had is to encode the middle D character inside the field.

For example:

4379960000125625D26012217630000000000

package custom

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"github.com/moov-io/iso8583/utils"
	"reflect"
	"strings"

	"github.com/moov-io/iso8583/field"
)

var _ field.Field = (*Track2)(nil)
var _ json.Marshaler = (*Track2)(nil)
var _ json.Unmarshaler = (*Track2)(nil)

// Track2 is a customization of github.com/moov-io/iso8583 Hex Field.
// this field allows working with hex strings but under the hood it's a binary
// field. It's convenient to use when you need to work with hex strings, but
// don't want to deal with converting them to bytes manually.
// If provided value is not a valid hex string, it will return an error during
// packing.
type Track2 struct {
	value string
	spec  *field.Spec
}

func NewTrack2(spec *field.Spec) *Track2 {
	return &Track2{
		spec: spec,
	}
}

// NewHexValue creates a new Track2 field with the given value The value is
// converted from hex to bytes before packing, so we don't validate that val is
// a valid hex string here.
func NewHexValue(val string) *Track2 {
	return &Track2{
		value: val,
	}
}

func (f *Track2) Spec() *field.Spec {
	return f.spec
}

func (f *Track2) SetSpec(spec *field.Spec) {
	f.spec = spec
}

func (f *Track2) SetBytes(b []byte) error {
	aux := strings.ToUpper(hex.EncodeToString(b))
	if len(aux) > f.spec.Length {
		aux = aux[:f.spec.Length]
	}
	f.value = aux
	return nil
}

func (f *Track2) Bytes() ([]byte, error) {
	if f == nil {
		return nil, nil
	}
	return hex.DecodeString(f.value)
}

func (f *Track2) String() (string, error) {
	if f == nil {
		return "", nil
	}
	return f.value, nil
}

func (f *Track2) Value() string {
	if f == nil {
		return ""
	}
	return f.value
}

func (f *Track2) SetValue(v string) {
	f.value = v
}

func (f *Track2) Pack() ([]byte, error) {
	var data []byte
	var err error
	// Customizacion: si el string entrante tiene longitud impar concateno un 0 para volverla par.
	if len(f.value)%2 != 0 {
		data, err = hex.DecodeString(f.value + "0")
	} else {
		data, err = f.Bytes()
	}
	if err != nil {
		return nil, utils.NewSafeErrorf(err, "converting hex field into bytes")
	}

	if f.spec.Pad != nil {
		data = f.spec.Pad.Pad(data, f.spec.Length)
	}

	packed, err := f.spec.Enc.Encode(data)
	if err != nil {
		return nil, fmt.Errorf("failed to encode content: %w", err)
	}
	// Customizacion: pasamos el valor de longitud del String en lugar del Array de Nibbles (que es la mitad)
	packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(f.value))
	if err != nil {
		return nil, fmt.Errorf("failed to encode length: %w", err)
	}

	return append(packedLength, packed...), nil
}

func (f *Track2) Unpack(data []byte) (int, error) {
	// Requiere usar la implementacion de bcd prefix custom de ini para funcionar (bcd.go)
	dataLen, prefBytes, err := f.spec.Pref.DecodeLength(f.spec.Length, data)
	if err != nil {
		return 0, fmt.Errorf("failed to decode length: %w", err)
	}
	raw, read, err := f.spec.Enc.Decode(data[prefBytes:], dataLen)
	if err != nil {
		return 0, fmt.Errorf("failed to decode content: %w", err)
	}

	if f.spec.Pad != nil {
		raw = f.spec.Pad.Unpad(raw)
	}

	if err := f.SetBytes(raw); err != nil {
		return 0, fmt.Errorf("failed to set bytes: %w", err)
	}

	return read + prefBytes, nil
}

// Deprecated. Use Marshal instead
func (f *Track2) SetData(data interface{}) error {
	return f.Marshal(data)
}

func (f *Track2) Unmarshal(v interface{}) error {
	switch val := v.(type) {
	case reflect.Value:
		if !val.CanSet() {
			return fmt.Errorf("cannot set reflect.Value of type %s", val.Kind())
		}

		switch val.Kind() { //nolint:exhaustive
		case reflect.String:
			str, _ := f.String()
			val.SetString(str)
		case reflect.Slice:
			buf, _ := f.Bytes()
			val.SetBytes(buf)
		default:
			return fmt.Errorf("unsupported reflect.Value type: %s", val.Kind())
		}
	case *string:
		*val, _ = f.String()
	case *[]byte:
		*val, _ = f.Bytes()
	case *Track2:
		val.value = f.value
	default:
		return fmt.Errorf("unsupported type: expected *Track2, *string, *[]byte, or reflect.Value, got %T", v)
	}

	return nil
}

func (f *Track2) Marshal(v interface{}) error {
	if v == nil || reflect.ValueOf(v).IsZero() {
		f.value = ""
		return nil
	}

	switch v := v.(type) {
	case *Track2:
		f.value = v.value
	case string:
		f.value = v
	case *string:
		f.value = *v
	case []byte:
		f.SetBytes(v)
	case *[]byte:
		f.SetBytes(*v)
	default:
		return fmt.Errorf("data does not match required *Track2 or (string, *string, []byte, *[]byte) type")
	}

	return nil
}

func (f *Track2) MarshalJSON() ([]byte, error) {
	data, err := f.String()
	if err != nil {
		return nil, utils.NewSafeError(err, "convert hex field into bytes")
	}

	bytes, err := json.Marshal(data)
	if err != nil {
		return nil, utils.NewSafeError(err, "failed to JSON marshal string to bytes")
	}
	return bytes, nil
}

func (f *Track2) UnmarshalJSON(b []byte) error {
	var v string
	err := json.Unmarshal(b, &v)
	if err != nil {
		return utils.NewSafeError(err, "failed to JSON unmarshal bytes to string")
	}

	f.value = v

	return nil
}
package custom

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/moov-io/iso8583/encoding"
	"github.com/moov-io/iso8583/prefix"
	"github.com/yerden/go-util/bcd"
)

type bcdVarPrefixer struct {
	Digits int
}

// BCD is a customized implementation for Prisma of the BCD prefix of github.com/moov-io/iso8583
// it is modified to support nibble hex data arrays. If the length is not even, it decodes to a valid
// array length (dataLen + 1) / 2
var BCD = prefix.Prefixers{
	Fixed:  &bcdFixedPrefixer{},
	L:      &bcdVarPrefixer{1},
	LL:     &bcdVarPrefixer{2},
	LLL:    &bcdVarPrefixer{3},
	LLLL:   &bcdVarPrefixer{4},
	LLLLL:  &bcdVarPrefixer{5},
	LLLLLL: &bcdVarPrefixer{6},
}

func (p *bcdVarPrefixer) EncodeLength(maxLen, dataLen int) ([]byte, error) {
	if dataLen > maxLen {
		return nil, fmt.Errorf("field length: %d is larger than maximum: %d", dataLen, maxLen)
	}

	if len(strconv.Itoa(dataLen)) > p.Digits {
		return nil, fmt.Errorf("number of digits in length: %d exceeds: %d", dataLen, p.Digits)
	}

	strLen := fmt.Sprintf("%0*d", p.Digits, dataLen)
	res, err := encoding.BCD.Encode([]byte(strLen))
	if err != nil {
		return nil, err
	}

	return res, nil
}

func (p *bcdVarPrefixer) DecodeLength(maxLen int, data []byte) (int, int, error) {
	length := bcd.EncodedLen(p.Digits)
	if len(data) < length {
		return 0, 0, fmt.Errorf("length mismatch: want to read %d bytes, get only %d", length, len(data))
	}

	bDigits, _, err := encoding.BCD.Decode(data[:length], p.Digits)
	if err != nil {
		return 0, 0, err
	}

	dataLen, err := strconv.Atoi(string(bDigits))
	if err != nil {
		return 0, 0, err
	}

	if dataLen > maxLen {
		return 0, 0, fmt.Errorf("data length %d is larger than maximum %d", dataLen, maxLen)
	}
	// Customizacion: Dividimos la longitud por dos para pasar de BCD (nibbles) a BYTES
	if dataLen%2 != 0 {
		dataLen += 1
	}
	// Luego retornamos la longitud del array necesario para decodificar el campo (dataLen / 2)
	return dataLen / 2, length, nil
}

func (p *bcdVarPrefixer) Inspect() string {
	return fmt.Sprintf("BCD.%s", strings.Repeat("L", p.Digits))
}

type bcdFixedPrefixer struct {
}

func (p *bcdFixedPrefixer) EncodeLength(fixLen, dataLen int) ([]byte, error) {
	if dataLen > fixLen {
		return nil, fmt.Errorf("field length: %d should be fixed: %d", dataLen, fixLen)
	}

	return []byte{}, nil
}

// Returns number of characters that should be decoded
func (p *bcdFixedPrefixer) DecodeLength(fixLen int, data []byte) (int, int, error) {
	return fixLen, 0, nil
}

func (p *bcdFixedPrefixer) Inspect() string {
	return "BCD.Fixed"
}

@anilgorgec
Copy link
Author

Hey! @anilgorgec , I'm not sure what this means: "Since ISO 8583 messages are often processed in hexadecimal or BCD format, they need to be byte-aligned."

Do you mean some fields of the message are in the BCD format? Is your whole message encoded in HEX? Because BCD is used only for decimal numbers, I'm not sure how it can be used to encode whole message.

In provided message:

ISO Message:
020030200580208000000000000000000015001079890071002100375111111211111111d11110000000000000000f5445535430303031

I see that fields are in ASCII and your spec also uses ASCII. The length prefix for the field 35 is ASCII.LL. The content of the field, as far as I see, is in ASCII. What is the reason for using encoding.ASCIIHexToBytes in the spec?

Also, please, check the length of the fields 22 and 24. In expected output you show 4 digits, but in the field spec, you set the length to 3.

My guess is that you might use BCD encoding for all numeric fields, with BCD prefix and Binary for the rest. You have to check the ISO 8583 specification of your provider.

@alovak , 22 and 24 field are presented in the document as 3 digit. it looks 4 digit because of one byte can't be presented 1 digit.

@bpross
Copy link
Contributor

bpross commented Nov 11, 2024

@anilgorgec you might try to leverage what I have implemented in this PR: #336 to see if it fixes your issue. Specifically the packer/unpacker implementation that handles even/odd length track 2 data.

@carlosmgc2003 you might try a spec something like this:

			35: field.NewTrack2(&field.Spec{
				Length:      37,
				Description: "Track 2 Data",
				Enc:         encoding.ASCIIHexToBytes,
				Pref:        prefix.Binary.L,
				Pad:         padding.Left('0'), // using the custom packer this will pad only if length is odd
				Packer:      field.Track2Packer{},
				Unpacker:    field.Track2Unpacker{},
			}),

to see if it fixes your issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants