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 Unmarshal to convert TLVs to Go struct(s) #7

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## Features

- Encode and decode BER-TLV data structures.
- Unmarshal BER-TLV data into Go structs
- Support for both simple and composite TLV tags.
- Easy pretty-printing of decoded TLV structures for debugging and analysis.

Expand Down Expand Up @@ -75,6 +76,7 @@ func TestEncodeDecode(t *testing.T) {
- **Decode**: The `bertlv.Decode` decodes a binary value back into a TLV objects.
- **FindTag**: The `bertlv.FindTag` returns the first TLV object matching the specified path (e.g., "6F.A5.BF0C.61.50").
- **PrettyPrint**: The `bertlv.PrettyPrint` visaulizes the TLV structure in a readable format.
- **Unmarshal**: The `bertlv.Unmarshal` converts TLV objects into a Go struct using struct tags.

### TLV Creation
You can create TLV objects using the following helper functions (preferred way):
Expand All @@ -101,6 +103,24 @@ Also, you can create TLV objects directly using the `bertlv.TLV` struct (less pr
}
```

### Unmarshaling to structs

The `bertlv.Unmarshal` function allows you to unmarshal TLV data directly into Go structs using struct tags. Fields can be mapped to TLV tags using the `bertlv` struct tag:

```go
type EMVData struct {
DedicatedFileName []byte `bertlv:"84"`
ApplicationTemplate struct {
ApplicationID string `bertlv:"4F"` // Will be converted to HEX string
ApplicationLabel string `bertlv:"50,ascii"` // Will be converted to ASCII string
ApplicationPriorityIndicator []byte `bertlv:"87"`
adamdecaf marked this conversation as resolved.
Show resolved Hide resolved
} `bertlv:"61"`
}

data := []bertlv.TLV{...} // Your TLV data
var emvData EMVData
err := bertlv.Unmarshal(data, &emvData)

## Contribution

Feel free to contribute by opening issues or creating pull requests. Any contributions, such as adding new features or improving the documentation, are welcome.
Expand Down
80 changes: 80 additions & 0 deletions tlv.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/hex"
"errors"
"fmt"
"reflect"
"slices"
"strings"
)

Expand Down Expand Up @@ -282,3 +284,81 @@ func FindFirstTag(tlvs []TLV, tag string) (TLV, bool) {

return TLV{}, false
}

type fieldTag struct {
name string
options []string
}

func (v fieldTag) HasOption(option string) bool {
return slices.Contains(v.options, option)
}

func newFieldTag(s string) fieldTag {
splits := strings.Split(s, ",")

return fieldTag{
name: splits[0],
options: splits[0:],
}
}

func Unmarshal(tlvs []TLV, s any) error {
// let's create map for lookup
tagToValue := make(map[string]TLV)
for _, tlv := range tlvs {
tagToValue[tlv.Tag] = tlv
}

v := reflect.ValueOf(s)
if v.Kind() != reflect.Pointer || v.IsNil() {
return fmt.Errorf("%T is not a pointer or nil", s)
}

v = v.Elem()

if v.Kind() != reflect.Struct {
return fmt.Errorf("%T is not a struct, but", v.Kind())
}

t := v.Type()

for i := 0; i < t.NumField(); i++ {
typeField := t.Field(i)

tag := newFieldTag(typeField.Tag.Get("bertlv"))
if tag.name == "" {
continue
}

tlv, ok := tagToValue[tag.name]
if !ok {
continue
}

valField := v.Field(i)

if typeField.Type.Kind() == reflect.Struct {
if err := Unmarshal(tlv.TLVs, valField.Addr().Interface()); err != nil {
return fmt.Errorf("unmarshalling nested field %s: %w", typeField.Name, err)
}

continue
}

switch {
case typeField.Type.Kind() == reflect.Slice && typeField.Type.Elem().Kind() == reflect.Uint8:
valField.Set(reflect.ValueOf(tlv.Value))
case typeField.Type.Kind() == reflect.String:
var str string
if tag.HasOption("ascii") {
str = string(tlv.Value)
} else {
str = strings.ToUpper(hex.EncodeToString(tlv.Value))
}
valField.SetString(str)
}
}

return nil
}
69 changes: 69 additions & 0 deletions tlv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,72 @@ func TestFindTag(t *testing.T) {
require.True(t, found)
require.Equal(t, []byte{0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10}, tag.Value)
}

func TestUnmarshalSuccess(t *testing.T) {
data := []bertlv.TLV{
bertlv.NewTag("84", []byte{0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31}),
bertlv.NewComposite("61", // Application Template
bertlv.NewTag("4F", []byte{0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10}),
bertlv.NewTag("50", []byte{0x4D, 0x61, 0x73, 0x74, 0x65, 0x72, 0x63, 0x61, 0x72, 0x64}),
bertlv.NewTag("87", []byte{0x01}), // Application Priority Indicator
),
}

type EMVData struct {
DedicatedFileName []byte `bertlv:"84"`
ApplicationTemplate struct {
ApplicationID string `bertlv:"4F"`
ApplicationLabel string `bertlv:"50,ascii"`
ApplicationPriorityIndicator []byte `bertlv:"87"`
} `bertlv:"61"`
}

emvData := &EMVData{}

err := bertlv.Unmarshal(data, emvData)
require.NoError(t, err)

require.Equal(t, []byte{0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31}, emvData.DedicatedFileName)
require.Equal(t, "A0000000041010", emvData.ApplicationTemplate.ApplicationID)
require.Equal(t, "Mastercard", emvData.ApplicationTemplate.ApplicationLabel)
require.Equal(t, []byte{0x01}, emvData.ApplicationTemplate.ApplicationPriorityIndicator)
}

func TestUnmarshalEdgeCases(t *testing.T) {
data := []bertlv.TLV{
bertlv.NewComposite("61"), // empty composite
}

type Nested struct {
Template struct {
Field []byte `bertlv:"4F"`
} `bertlv:"61"`
}

var n Nested
err := bertlv.Unmarshal(data, &n)
require.NoError(t, err)

// Missing tag in data
data = []bertlv.TLV{
bertlv.NewTag("84", []byte{1}),
bertlv.NewComposite("61",
bertlv.NewTag("4F", []byte{2}),
),
}

type MissingTag struct {
Field []byte `bertlv:"99"` // non-existent tag
Field2 []byte // no mapping to tag
}

err = bertlv.Unmarshal(data, &MissingTag{})
require.NoError(t, err) // should skip missing tags

// Nil pointer
var nilPtr *struct {
Field []byte `bertlv:"84"`
}
err = bertlv.Unmarshal(data, nilPtr)
require.Error(t, err)
}
Loading