moov-io / iso8583

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

Unable to pack a f55 with subfield ASCIIHexToBytes with length 1 byte if we build the message from the bytes #315

Open Achuthen opened 1 month ago

Achuthen commented 1 month ago

Having trouble to pack the f55 if we the field was build by unpacking from bytes. ( thru marshal it works well ).

  1. unpack from bytes
  2. pack/bytes to get bytes throws error.

may be i am doing something wrong here. please advice

// Copyright 2020 The Moov Authors
// Use of this source code is governed by an Apache License
// license that can be found in the LICENSE file.

package main

import (
    "fmt"

    "github.com/moov-io/iso8583/encoding"
    "github.com/moov-io/iso8583/field"
    "github.com/moov-io/iso8583/padding"
    "github.com/moov-io/iso8583/prefix"
    "github.com/moov-io/iso8583/sort"
)

//  The sample is for VSDC chip data usage

//  This field 55 VSDC chip data usage contains three subfields after the length subfield.
//  Positions:
//      1 2 3 4 ... 255
//  Fields
//   - Subfield 1: length Byte, a one-byte binary subfield  that contains the number of bytes in this field after the length subfield
//   - Subfield 2: dataset ID, a one-byte binary identifier
//   - Subfield 3: dataset length, 2-byte binary subfield that contains the total length of all TLV elements that follow.
//   - Subfield 4:
//      Chip Card TLV data elements
//      Tag Length Value Tag Length Value

type Dataset struct {
    F9A   *field.String
    F9F02 *field.String
}

type TestISOF55Data struct {
    F1 *field.String
    F2 *field.String
    F3 *field.String
    F4 *Dataset
}

func main() {

    datasetSpec := &field.Spec{
        Description: "Chip Card TLV data elements",
        Pref:        prefix.None.Fixed,
        Tag: &field.TagSpec{
            Sort: sort.StringsByHex,
            Enc:  encoding.BerTLVTag,
        },
        Subfields: map[string]field.Field{
            "9A": field.NewString(&field.Spec{
                Description: "Transaction Date",
                Enc:         encoding.Binary,
                Pref:        prefix.BerTLV,
            }),
            "9F02": field.NewString(&field.Spec{
                Description: "Amount, Authorized (Numeric)",
                Enc:         encoding.Binary,
                Pref:        prefix.BerTLV,
            }),
        },
    }

    f55Spec := &field.Spec{
        Length:      999,
        Description: "ICC Data – EMV Having Multiple Tags",
        Pref:        prefix.ASCII.LLL,
        Pad:         padding.None,
        Tag: &field.TagSpec{
            Sort: sort.StringsByInt,
        },
        Subfields: map[string]field.Field{
            "1": field.NewString(&field.Spec{
                Description: "Length Subfield",
                Enc:         encoding.ASCIIHexToBytes,
                Length:      1,
                Pref:        prefix.ASCII.Fixed,
                Pad:         padding.Left('0'),
            }),
            "2": field.NewString(&field.Spec{
                Description: "Dataset ID Subfield",
                Enc:         encoding.ASCIIHexToBytes,
                Length:      1,
                Pref:        prefix.ASCII.Fixed,
                Pad:         padding.Left('0'),
            }),
            "3": field.NewString(&field.Spec{
                Description: "Dataset Length Subfield",
                Enc:         encoding.ASCIIHexToBytes,
                Length:      2,
                Pref:        prefix.ASCII.Fixed,
                Pad:         padding.Left('0'),
            }),
            "4": field.NewComposite(datasetSpec),
        },
    }

    dataSet := Dataset{
        F9A:   field.NewStringValue("210720"),
        F9F02: field.NewStringValue("000000000501"),
    }

    // Getting size
    datasetField := field.NewComposite(datasetSpec)
    err := datasetField.Marshal(&dataSet)
    if err != nil {
        fmt.Println(err)
        return
    }

    buf, err := datasetField.Pack()
    if err != nil {
        fmt.Println(err)
        return
    }

    // Making F55 field
    datasetSize := len(buf)

    f55Data := TestISOF55Data{
        F1: field.NewStringValue(fmt.Sprintf("%02x", datasetSize+3)),
        F2: field.NewStringValue("01"),
        F3: field.NewStringValue(fmt.Sprintf("%04x", datasetSize)),
        F4: &dataSet,
    }

    // creating field
    f55 := field.NewComposite(f55Spec)

    // Setting value
    err = f55.Marshal(&f55Data)

    if err != nil {
        fmt.Println(err)
        return
    }

    // get binary value of the field
    rawValue, err := f55.Pack()
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(fmt.Sprintf("0x%X", rawValue))

    fmt.Println("\n EMV Having Multiple Tags (Bit 55)")
    fmt.Println("ICC Data length: ", fmt.Sprintf("0x%X", rawValue[0:3]))
    fmt.Println(".........length: ", fmt.Sprintf("0x%X", rawValue[3:4]))
    fmt.Println(".............id: ", fmt.Sprintf("0x%X", rawValue[4:5]))
    fmt.Println(".dataset length: ", fmt.Sprintf("0x%X", rawValue[5:7]))
    fmt.Println("..dataset(tlvs): ", fmt.Sprintf("0x%X", rawValue[7:]))

    // this is how to reprodice
    // form field from bytes
    newF55 := field.NewComposite(f55Spec)
    _, err = newF55.Unpack(rawValue)
    if err != nil {
        fmt.Println(err)
        return
    }
    // pack field again, there is a bug here
    _, err = newF55.Pack()
    if err != nil {
        fmt.Println(err)
        //return
    }

    // pack field again, there is a bug here
    _, err = newF55.Bytes()
    if err != nil {
        fmt.Println(err)
        //return
    }

}
alovak commented 3 weeks ago

Hey, @Achuthen.

out VSDC example is misleading, sorry. I'll remove it. Here is how it should look like. In rare cases we should have length fields. If you see it in the spec, most probably something is wrong. Having the length field prevents the spec from unpacking the message (in most cases).:

package examples

import (
    "fmt"
    "testing"

    "github.com/moov-io/iso8583/encoding"
    "github.com/moov-io/iso8583/field"
    "github.com/moov-io/iso8583/prefix"
    "github.com/moov-io/iso8583/sort"
    "github.com/stretchr/testify/require"
)

func TestICCField55(t *testing.T) {
    field55Spec := &field.Spec{
        Length:      255,
        Description: "Integrated Circuit Card (ICC) Data",
        Pref:        prefix.Binary.L,
        Tag: &field.TagSpec{
            Length: 1,
            Enc:    encoding.ASCIIHexToBytes,
            Sort:   sort.StringsByHex,
        },
        Subfields: map[string]field.Field{
            "01": field.NewComposite(&field.Spec{
                Length:      252,
                Description: "VSDC Data",
                Pref:        prefix.Binary.LL,
                Tag: &field.TagSpec{
                    Enc:  encoding.BerTLVTag,
                    Sort: sort.StringsByHex,
                },
                Subfields: map[string]field.Field{
                    "9A": field.NewString(&field.Spec{
                        Description: "Transaction Date",
                        Enc:         encoding.Binary,
                        Pref:        prefix.BerTLV,
                    }),
                    "9F02": field.NewString(&field.Spec{
                        Description: "Amount, Authorized (Numeric)",
                        Enc:         encoding.Binary,
                        Pref:        prefix.BerTLV,
                    }),
                },
            }),
        },
    }

    type VSDCData struct {
        TransactionDate string `iso8583:"9A"`
        Amount          string `iso8583:"9F02"`
    }

    type ICCData struct {
        VSDCData *VSDCData `iso8583:"01"`
    }

    filed55 := field.NewComposite(field55Spec)
    err := filed55.Marshal(&ICCData{
        VSDCData: &VSDCData{
            TransactionDate: "210720",
            Amount:          "000000000501",
        },
    })
    require.NoError(t, err)

    packed, err := filed55.Pack()
    require.NoError(t, err)

    require.Equal(t, "1A0100179A063231303732309F020C303030303030303030353031", fmt.Sprintf("%X", packed))
}

Also, you should remember that most probably for real data / spec, you will have to use SkipUnknownTLVTags (for both TagSpec in the example above) and PrefUnknownTLV only for the first TagSpec as we should tell parser what is the length of the length prefix so it can read the length of unknown tag.

Please, let me know if that worked for you.

alovak commented 3 weeks ago

I added the test into the examples directory: https://github.com/moov-io/iso8583/blob/master/examples/icc_test.go