From fa4832c57f2c24198bf00e36d1f9d2aa5dbe9f2b Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Sat, 22 Apr 2023 09:47:32 +0200 Subject: [PATCH 1/6] use length of the field value for prefixer, not length of encoded data --- field/binary.go | 2 +- field/composite_test.go | 52 ++++++------ field/hex.go | 171 ++++++++++++++++++++++++++++++++++++++++ field/numeric.go | 2 +- field/spec.go | 8 +- field/string.go | 2 +- field/track1.go | 2 +- field/track2.go | 2 +- field/track3.go | 2 +- message_test.go | 64 +++++++++++++++ 10 files changed, 273 insertions(+), 34 deletions(-) create mode 100644 field/hex.go diff --git a/field/binary.go b/field/binary.go index c7f12a66..64d00a8c 100644 --- a/field/binary.go +++ b/field/binary.go @@ -84,7 +84,7 @@ func (f *Binary) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/field/composite_test.go b/field/composite_test.go index f410f3ad..c18390f3 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -233,12 +233,12 @@ var ( Sort: sort.StringsByHex, }, Subfields: map[string]Field{ - "9A": NewString(&Spec{ + "9A": NewHex(&Spec{ Description: "Transaction Date", Enc: encoding.ASCIIHexToBytes, Pref: prefix.BerTLV, }), - "9F02": NewString(&Spec{ + "9F02": NewHex(&Spec{ Description: "Amount, Authorized (Numeric)", Enc: encoding.ASCIIHexToBytes, Pref: prefix.BerTLV, @@ -255,12 +255,12 @@ var ( Sort: sort.StringsByHex, }, Subfields: map[string]Field{ - "82": NewString(&Spec{ + "82": NewHex(&Spec{ Description: "Application Interchange Profile", Enc: encoding.ASCIIHexToBytes, Pref: prefix.BerTLV, }), - "9F36": NewString(&Spec{ + "9F36": NewHex(&Spec{ Description: "Currency Code, Application Reference", Enc: encoding.ASCIIHexToBytes, Pref: prefix.BerTLV, @@ -273,7 +273,7 @@ var ( Sort: sort.StringsByHex, }, Subfields: map[string]Field{ - "9F45": NewString(&Spec{ + "9F45": NewHex(&Spec{ Description: "Data Authentication Code", Enc: encoding.ASCIIHexToBytes, Pref: prefix.BerTLV, @@ -306,18 +306,18 @@ type CompositeTestDataWithoutTagPaddingWithIndexTag struct { } type TLVTestData struct { - F9A *String - F9F02 *String + F9A *Hex + F9F02 *Hex } type ConstructedTLVTestData struct { - F82 *String - F9F36 *String + F82 *Hex + F9F36 *Hex F9F3B *SubConstructedTLVTestData } type SubConstructedTLVTestData struct { - F9F45 *String + F9F45 *Hex } func TestComposite_SetData(t *testing.T) { @@ -334,8 +334,8 @@ func TestCompositeFieldUnmarshal(t *testing.T) { // we will do it by packing the field composite := NewComposite(tlvTestSpec) err := composite.SetData(&TLVTestData{ - F9A: NewStringValue("210720"), - F9F02: NewStringValue("000000000501"), + F9A: NewHexValue("210720"), + F9F02: NewHexValue("000000000501"), }) require.NoError(t, err) @@ -353,10 +353,10 @@ func TestCompositeFieldUnmarshal(t *testing.T) { t.Run("Unmarshal gets data for composite field (constructed)", func(t *testing.T) { composite := NewComposite(constructedBERTLVTestSpec) err := composite.SetData(&ConstructedTLVTestData{ - F82: NewStringValue("017F"), - F9F36: NewStringValue("027F"), + F82: NewHexValue("017F"), + F9F36: NewHexValue("027F"), F9F3B: &SubConstructedTLVTestData{ - F9F45: NewStringValue("047F"), + F9F45: NewHexValue("047F"), }, }) require.NoError(t, err) @@ -375,15 +375,15 @@ func TestCompositeFieldUnmarshal(t *testing.T) { t.Run("Unmarshal gets data for composite field using field tag `index`", func(t *testing.T) { type tlvTestData struct { - Date *String `index:"9A"` - TransactionID *String `index:"9F02"` + Date *Hex `index:"9A"` + TransactionID *Hex `index:"9F02"` } // first, we need to populate fields of composite field // we will do it by packing the field composite := NewComposite(tlvTestSpec) err := composite.SetData(&TLVTestData{ - F9A: NewStringValue("210720"), - F9F02: NewStringValue("000000000501"), + F9A: NewHexValue("210720"), + F9F02: NewHexValue("000000000501"), }) require.NoError(t, err) @@ -402,8 +402,8 @@ func TestCompositeFieldUnmarshal(t *testing.T) { func TestTLVPacking(t *testing.T) { t.Run("Pack correctly serializes data to bytes (general tlv)", func(t *testing.T) { data := &TLVTestData{ - F9A: NewStringValue("210720"), - F9F02: NewStringValue("000000000501"), + F9A: NewHexValue("210720"), + F9F02: NewHexValue("000000000501"), } composite := NewComposite(tlvTestSpec) @@ -465,10 +465,10 @@ func TestTLVPacking(t *testing.T) { t.Run("Pack correctly serializes data to bytes (constructed ber-tlv)", func(t *testing.T) { data := &ConstructedTLVTestData{ - F82: NewStringValue("017f"), - F9F36: NewStringValue("027f"), + F82: NewHexValue("017f"), + F9F36: NewHexValue("027f"), F9F3B: &SubConstructedTLVTestData{ - F9F45: NewStringValue("047f"), + F9F45: NewHexValue("047f"), }, } @@ -1671,8 +1671,8 @@ func TestTLVJSONConversion(t *testing.T) { t.Run("MarshalJSON TLV Data Ok", func(t *testing.T) { data := &TLVTestData{ - F9A: NewStringValue("210720"), - F9F02: NewStringValue("000000000501"), + F9A: NewHexValue("210720"), + F9F02: NewHexValue("000000000501"), } composite := NewComposite(tlvTestSpec) diff --git a/field/hex.go b/field/hex.go new file mode 100644 index 00000000..ee2646f8 --- /dev/null +++ b/field/hex.go @@ -0,0 +1,171 @@ +package field + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/moov-io/iso8583/utils" +) + +var _ Field = (*Hex)(nil) +var _ json.Marshaler = (*Hex)(nil) +var _ json.Unmarshaler = (*Hex)(nil) + +// Hex is a field that contains a hex string. +type Hex struct { + value string + spec *Spec + data *Hex +} + +func NewHex(spec *Spec) *Hex { + return &Hex{ + spec: spec, + } +} + +func NewHexValue(val string) *Hex { + return &Hex{ + value: val, + } +} + +func (f *Hex) Spec() *Spec { + return f.spec +} + +func (f *Hex) SetSpec(spec *Spec) { + f.spec = spec +} + +func (f *Hex) SetBytes(b []byte) error { + f.value = string(b) + if f.data != nil { + *(f.data) = *f + } + return nil +} + +func (f *Hex) Bytes() ([]byte, error) { + if f == nil { + return nil, nil + } + return []byte(f.value), nil +} + +func (f *Hex) String() (string, error) { + if f == nil { + return "", nil + } + return f.value, nil +} + +func (f *Hex) Value() string { + if f == nil { + return "" + } + return f.value +} + +func (f *Hex) SetValue(v string) { + f.value = v +} + +func (f *Hex) Pack() ([]byte, error) { + data := []byte(f.value) + + 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) + } + + dataLen := hex.DecodedLen(len(data)) + + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, dataLen) + if err != nil { + return nil, fmt.Errorf("failed to encode length: %w", err) + } + + return append(packedLength, packed...), nil +} + +func (f *Hex) Unpack(data []byte) (int, error) { + 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 +} + +func (f *Hex) Unmarshal(v interface{}) error { + if v == nil { + return nil + } + + str, ok := v.(*Hex) + if !ok { + return errors.New("data does not match required *Hex type") + } + + str.value = f.value + + return nil +} + +func (f *Hex) SetData(data interface{}) error { + if data == nil { + return nil + } + + str, ok := data.(*Hex) + if !ok { + return fmt.Errorf("data does not match required *Hex type") + } + + f.data = str + if str.value != "" { + f.value = str.value + } + return nil +} + +func (f *Hex) Marshal(data interface{}) error { + return f.SetData(data) +} + +func (f *Hex) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(f.value) + if err != nil { + return nil, utils.NewSafeError(err, "failed to JSON marshal string to bytes") + } + return bytes, nil +} + +func (f *Hex) 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") + } + return f.SetBytes([]byte(v)) +} diff --git a/field/numeric.go b/field/numeric.go index 3cc533d9..18e41936 100644 --- a/field/numeric.go +++ b/field/numeric.go @@ -97,7 +97,7 @@ func (f *Numeric) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/field/spec.go b/field/spec.go index 74828d2e..eb01992b 100644 --- a/field/spec.go +++ b/field/spec.go @@ -40,8 +40,12 @@ type TagSpec struct { // Spec defines the structure of a field. type Spec struct { - // Length defines the maximum length of field (bytes, characters or - // digits), for both fixed and variable lengths. + // Length defines the maximum length of field (bytes, characters, + // digits or hex digits), for both fixed and variable lengths. + // You should use appropriate field types corresponding to the + // length of the field you're defining, e.g. Numeric, String, Binary + // etc. For Hex fields, the length is defined in terms of the number + // of bytes, while the value of the field is hex string. Length int // Tag sets the tag specification. Only applicable to composite field // types. diff --git a/field/string.go b/field/string.go index 05a9bcd3..94a1a8a4 100644 --- a/field/string.go +++ b/field/string.go @@ -83,7 +83,7 @@ func (f *String) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/field/track1.go b/field/track1.go index 5f82b542..f96aae9c 100644 --- a/field/track1.go +++ b/field/track1.go @@ -77,7 +77,7 @@ func (f *Track1) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/field/track2.go b/field/track2.go index 5ce87ca8..8d05383a 100644 --- a/field/track2.go +++ b/field/track2.go @@ -76,7 +76,7 @@ func (f *Track2) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/field/track3.go b/field/track3.go index 83c1f10b..9e180b4a 100644 --- a/field/track3.go +++ b/field/track3.go @@ -74,7 +74,7 @@ func (f *Track3) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(packed)) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } diff --git a/message_test.go b/message_test.go index 66bb575b..63687d12 100644 --- a/message_test.go +++ b/message_test.go @@ -1,6 +1,7 @@ package iso8583 import ( + "encoding/hex" "encoding/json" "reflect" "testing" @@ -551,6 +552,69 @@ func TestPackUnpack(t *testing.T) { require.Error(t, err) }) + + // this test should check that BCD fields are packed and + // unpacked correctly it's a confirmation that issue + // https://github.com/moov-io/iso8583/issues/220 is fixed + t.Run("Pack and Unpack BCD fields", func(t *testing.T) { + var spec = &MessageSpec{ + Fields: map[int]field.Field{ + 0: field.NewNumeric(&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.NewNumeric(&field.Spec{ + Length: 4, + Description: "SomeFixedField", + Enc: encoding.BCD, + Pref: prefix.BCD.Fixed, + }), + 3: field.NewNumeric(&field.Spec{ + Length: 3, + Description: "SomeVarField", + Enc: encoding.BCD, + Pref: prefix.BCD.LLLL, + }), + }, + } + + msg := NewMessage(spec) + + msg.MTI("1234") + msg.Field(2, "4567") + msg.Field(3, "890") + + out, err := msg.Pack() + require.NoError(t, err) + + got := hex.EncodeToString(out) + + expected := "1234" + // MTI + "6000000000000000" + // Bitmap + "4567" + // SomeFixedField + "0003" + // LLLL in BCD + "0890" // SomeVarField in BCD 0x08 0x90 + + require.Equal(t, expected, got) + + in := NewMessage(spec) + + err = in.Unpack(out) + require.NoError(t, err) + + result, _ := in.GetField(2).String() + require.Equal(t, "4567", result) + + result, _ = in.GetField(3).String() + require.Equal(t, "890", result) + }) } func TestMessageJSON(t *testing.T) { From 4d9d86c85ebeff2b22312941500fdd450920b442 Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Fri, 5 May 2023 16:15:31 +0200 Subject: [PATCH 2/6] add test for composite with BCD fields --- encoding/encoder.go | 7 ++++- field/composite.go | 3 +++ field/composite_test.go | 60 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/encoding/encoder.go b/encoding/encoder.go index 6a879ea6..1f174eb2 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -1,7 +1,12 @@ package encoding type Encoder interface { + // Encode encodes source data (ASCII, characters, digits, etc.) into + // destination bytes. It returns encoded bytes and any error Encode([]byte) ([]byte, error) - // Returns data decoded into ASCII (or bytes), how many bytes were read, error + + // Decode decodes data into into bytes (ASCII, characters, digits, + // etc.). It returns the bytes representing the decoded data, the + // number of bytes read from the input, and any error Decode([]byte, int) (data []byte, read int, err error) } diff --git a/field/composite.go b/field/composite.go index 325eee44..f75dbd83 100644 --- a/field/composite.go +++ b/field/composite.go @@ -26,6 +26,9 @@ var _ json.Unmarshaler = (*Composite)(nil) // documentation and error messages. These subfields are defined using the // 'Subfields' field on the field.Spec struct. // +// Because composite subfields may be encoded with different encodings, the +// Length field on the field.Spec struct is in bytes. +// // Composite handles aggregate fields of the following format: // - Length (if variable) // - []Subfield diff --git a/field/composite_test.go b/field/composite_test.go index c18390f3..101dcb1d 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -513,7 +513,7 @@ func TestTLVPacking(t *testing.T) { require.Equal(t, "047F", data.F9F3B.F9F45.Value()) }) - t.Run("Unpack correctly deserialises bytes to the data struct (constructed ber-tlv, unordered value)", func(t *testing.T) { + t.Run("123Unpack correctly deserialises bytes to the data struct (constructed ber-tlv, unordered value)", func(t *testing.T) { composite := NewComposite(constructedBERTLVTestSpec) read, err := composite.Unpack([]byte{0x30, 0x31, 0x37, 0x9f, 0x36, 0x2, 0x2, 0x7f, 0x9f, 0x3b, 0x5, 0x9f, 0x45, 0x2, 0x4, 0x7f, 0x82, 0x2, 0x1, 0x7f}) @@ -629,6 +629,64 @@ func TestCompositePacking(t *testing.T) { require.Equal(t, "ABCD12", string(packed)) }) + t.Run("Pack and unpack data with BCD encoding", func(t *testing.T) { + var compositeSpecWithBCD = &Spec{ + Length: 2, // always in bytes + Description: "Point of Service Entry Mode", + Pref: prefix.BCD.Fixed, + Tag: &TagSpec{ + Sort: sort.StringsByInt, + }, + Subfields: map[string]Field{ + "1": NewString(&Spec{ + Length: 2, + Description: "PAN/Date Entry Mode", + Enc: encoding.BCD, + Pref: prefix.BCD.Fixed, + }), + "2": NewString(&Spec{ + Length: 2, + Description: "PIN Entry Capability", + Enc: encoding.BCD, + Pref: prefix.BCD.Fixed, + }), + }, + } + + type data struct { + PANEntryMode *String `index:"1"` + PINEntryMode *String `index:"2"` + } + + f := NewComposite(compositeSpecWithBCD) + + d := &data{ + PANEntryMode: NewStringValue("01"), + PINEntryMode: NewStringValue("02"), + } + + err := f.Marshal(d) + require.NoError(t, err) + + packed, err := f.Pack() + require.NoError(t, err) + require.Equal(t, []byte{0x01, 0x02}, packed) + + // unpacking + + f = NewComposite(compositeSpecWithBCD) + read, err := f.Unpack(packed) + require.NoError(t, err) + require.Equal(t, 2, read) // two bytes read + + d = &data{} + err = f.Unmarshal(d) + require.NoError(t, err) + + require.Equal(t, "01", d.PANEntryMode.Value()) + require.Equal(t, "02", d.PINEntryMode.Value()) + }) + t.Run("Unpack returns an error on mismatch of subfield types", func(t *testing.T) { type TestDataIncorrectType struct { F1 *Numeric From f39e77dd61a55622a7b6e584d9c9a046a1ab8efb Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Tue, 9 May 2023 15:55:21 +0200 Subject: [PATCH 3/6] update composite test and hex field --- field/composite_test.go | 10 +++++----- field/hex.go | 31 ++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/field/composite_test.go b/field/composite_test.go index 101dcb1d..84c5c8a5 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -235,12 +235,12 @@ var ( Subfields: map[string]Field{ "9A": NewHex(&Spec{ Description: "Transaction Date", - Enc: encoding.ASCIIHexToBytes, + Enc: encoding.Binary, Pref: prefix.BerTLV, }), "9F02": NewHex(&Spec{ Description: "Amount, Authorized (Numeric)", - Enc: encoding.ASCIIHexToBytes, + Enc: encoding.Binary, Pref: prefix.BerTLV, }), }, @@ -257,12 +257,12 @@ var ( Subfields: map[string]Field{ "82": NewHex(&Spec{ Description: "Application Interchange Profile", - Enc: encoding.ASCIIHexToBytes, + Enc: encoding.Binary, Pref: prefix.BerTLV, }), "9F36": NewHex(&Spec{ Description: "Currency Code, Application Reference", - Enc: encoding.ASCIIHexToBytes, + Enc: encoding.Binary, Pref: prefix.BerTLV, }), "9F3B": NewComposite(&Spec{ @@ -275,7 +275,7 @@ var ( Subfields: map[string]Field{ "9F45": NewHex(&Spec{ Description: "Data Authentication Code", - Enc: encoding.ASCIIHexToBytes, + Enc: encoding.Binary, Pref: prefix.BerTLV, }), }, diff --git a/field/hex.go b/field/hex.go index ee2646f8..71b4a1d5 100644 --- a/field/hex.go +++ b/field/hex.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/moov-io/iso8583/utils" ) @@ -13,7 +14,7 @@ var _ Field = (*Hex)(nil) var _ json.Marshaler = (*Hex)(nil) var _ json.Unmarshaler = (*Hex)(nil) -// Hex is a field that contains a hex string. +// Hex is a field that contains a hex string value, but is encoded as binary type Hex struct { value string spec *Spec @@ -26,6 +27,9 @@ func NewHex(spec *Spec) *Hex { } } +// NewHexValue creates a new Hex 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) *Hex { return &Hex{ value: val, @@ -41,7 +45,7 @@ func (f *Hex) SetSpec(spec *Spec) { } func (f *Hex) SetBytes(b []byte) error { - f.value = string(b) + f.value = strings.ToUpper(hex.EncodeToString(b)) if f.data != nil { *(f.data) = *f } @@ -52,7 +56,7 @@ func (f *Hex) Bytes() ([]byte, error) { if f == nil { return nil, nil } - return []byte(f.value), nil + return hex.DecodeString(f.value) } func (f *Hex) String() (string, error) { @@ -74,7 +78,10 @@ func (f *Hex) SetValue(v string) { } func (f *Hex) Pack() ([]byte, error) { - data := []byte(f.value) + 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) @@ -85,9 +92,7 @@ func (f *Hex) Pack() ([]byte, error) { return nil, fmt.Errorf("failed to encode content: %w", err) } - dataLen := hex.DecodedLen(len(data)) - - packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, dataLen) + packedLength, err := f.spec.Pref.EncodeLength(f.spec.Length, len(data)) if err != nil { return nil, fmt.Errorf("failed to encode length: %w", err) } @@ -154,7 +159,12 @@ func (f *Hex) Marshal(data interface{}) error { } func (f *Hex) MarshalJSON() ([]byte, error) { - bytes, err := json.Marshal(f.value) + 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") } @@ -167,5 +177,8 @@ func (f *Hex) UnmarshalJSON(b []byte) error { if err != nil { return utils.NewSafeError(err, "failed to JSON unmarshal bytes to string") } - return f.SetBytes([]byte(v)) + + f.value = v + + return nil } From a4bd20af7d68b775392146b851312c860dde7a2a Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Wed, 10 May 2023 12:26:02 +0200 Subject: [PATCH 4/6] add tests for Hex field --- field/hex_test.go | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 field/hex_test.go diff --git a/field/hex_test.go b/field/hex_test.go new file mode 100644 index 00000000..25c4a96b --- /dev/null +++ b/field/hex_test.go @@ -0,0 +1,144 @@ +package field + +import ( + "errors" + "testing" + + "github.com/moov-io/iso8583/encoding" + "github.com/moov-io/iso8583/prefix" + "github.com/moov-io/iso8583/utils" + "github.com/stretchr/testify/require" +) + +func TestHexField(t *testing.T) { + spec := &Spec{ + Length: 5, // 5 bytes, 10 hex chars + Description: "Field", + Enc: encoding.Binary, + Pref: prefix.ASCII.Fixed, + } + + t.Run("packing", func(t *testing.T) { + f := NewHexValue("AABBCCDDEE") + f.SetSpec(spec) + + packed, err := f.Pack() + + require.NoError(t, err) + require.Equal(t, []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}, packed) + }) + + t.Run("unpacking", func(t *testing.T) { + f := NewHex(spec) + read, err := f.Unpack([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}) + + require.NoError(t, err) + require.Equal(t, 5, read) + require.Equal(t, "AABBCCDDEE", f.Value()) + }) + + t.Run("marshaling", func(t *testing.T) { + f := NewHexValue("AABBCCDDEE") + f2 := &Hex{} + + f2.Marshal(f) + + require.Equal(t, f.Value(), f2.Value()) + }) + + t.Run("unmarshaling", func(t *testing.T) { + f := NewHexValue("AABBCCDDEE") + f2 := &Hex{} + + f.Unmarshal(f2) + + require.Equal(t, f.Value(), f2.Value()) + }) + + t.Run("JSON marshaling/unmarshaling", func(t *testing.T) { + // when marshaling, we should get the hex string, not base64 + f := NewHexValue("AABBCCDDEE") + f.SetSpec(spec) + + b, err := f.MarshalJSON() + require.NoError(t, err) + require.Equal(t, "\"AABBCCDDEE\"", string(b)) + + var f2 Hex + err = f2.UnmarshalJSON(b) + require.NoError(t, err) + require.Equal(t, f.Value(), f2.Value()) + }) + + t.Run("methods", func(t *testing.T) { + f := NewHex(spec) + f.SetBytes([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}) + + require.Equal(t, "AABBCCDDEE", f.Value()) + + str, err := f.String() + require.NoError(t, err) + require.Equal(t, "AABBCCDDEE", str) + + b, err := f.Bytes() + require.NoError(t, err) + require.Equal(t, []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}, b) + + // SetValue + f.SetValue("EEBBCCDDEE") + require.Equal(t, "EEBBCCDDEE", f.Value()) + }) + + t.Run("errors", func(t *testing.T) { + f := NewHex(spec) + f.SetBytes([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}) + + // invalid length + f.SetValue("AABBCCDDE") + + _, err := f.Bytes() + require.EqualError(t, err, "encoding/hex: odd length hex string") + + // invalid hex + f.SetValue("AABBCCDDEG") + _, err = f.Bytes() + require.EqualError(t, err, "encoding/hex: invalid byte: U+0047 'G'") + + _, err = f.Pack() + require.EqualError(t, err, "converting hex field into bytes") + + var e *utils.SafeError + require.True(t, errors.As(err, &e)) + require.Equal(t, "converting hex field into bytes: encoding/hex: invalid byte: U+0047 'G'", e.UnsafeError()) + }) +} + +func TestHexNil(t *testing.T) { + var f *Hex = nil + + bs, err := f.Bytes() + require.NoError(t, err) + require.Nil(t, bs) + + value, err := f.String() + require.NoError(t, err) + require.Equal(t, "", value) + + value = f.Value() + require.Equal(t, "", value) +} + +func TestHexPack(t *testing.T) { + t.Run("returns error for zero value when fixed length and no padding specified", func(t *testing.T) { + spec := &Spec{ + Length: 10, + Description: "Field", + Enc: encoding.ASCII, + Pref: prefix.ASCII.Fixed, + } + str := NewHex(spec) + _, err := str.Pack() + + require.EqualError(t, err, "failed to encode length: field length: 0 should be fixed: 10") + }) +} From ddcc82dd268eabd78321773490f84e0ae0bb55f3 Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Fri, 26 May 2023 13:23:22 +0200 Subject: [PATCH 5/6] fix typo --- field/composite_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/field/composite_test.go b/field/composite_test.go index 84c5c8a5..7b867eb3 100644 --- a/field/composite_test.go +++ b/field/composite_test.go @@ -513,7 +513,7 @@ func TestTLVPacking(t *testing.T) { require.Equal(t, "047F", data.F9F3B.F9F45.Value()) }) - t.Run("123Unpack correctly deserialises bytes to the data struct (constructed ber-tlv, unordered value)", func(t *testing.T) { + t.Run("Unpack correctly deserialises bytes to the data struct (constructed ber-tlv, unordered value)", func(t *testing.T) { composite := NewComposite(constructedBERTLVTestSpec) read, err := composite.Unpack([]byte{0x30, 0x31, 0x37, 0x9f, 0x36, 0x2, 0x2, 0x7f, 0x9f, 0x3b, 0x5, 0x9f, 0x45, 0x2, 0x4, 0x7f, 0x82, 0x2, 0x1, 0x7f}) From 30d24b1906a52231d6f0c829c70b5f162994c94f Mon Sep 17 00:00:00 2001 From: Pavel Gabriel Date: Fri, 26 May 2023 13:26:18 +0200 Subject: [PATCH 6/6] update description of the Hex field --- field/hex.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/field/hex.go b/field/hex.go index 71b4a1d5..d11f38e3 100644 --- a/field/hex.go +++ b/field/hex.go @@ -14,7 +14,11 @@ var _ Field = (*Hex)(nil) var _ json.Marshaler = (*Hex)(nil) var _ json.Unmarshaler = (*Hex)(nil) -// Hex is a field that contains a hex string value, but is encoded as binary +// Hex 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 Hex struct { value string spec *Spec