Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jattento committed Aug 17, 2020
0 parents commit e475308
Show file tree
Hide file tree
Showing 32 changed files with 4,199 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea/
*.DS_Store
examples/
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: go

go:
- 1.14.x

before_install:
- go get -t -v ./...
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0

script:
- go test ./... -race -coverprofile=coverage.txt -covermode=atomic
- make

after_success:
- bash <(curl -s https://codecov.io/bash)
33 changes: 33 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### Required tools
GOTOOLS_CHECK = golangci-lint

all: fmt ensure-deps linter test

### Testing
test:
go test ./... -covermode=atomic -coverpkg=./... -count=1 -race

test-cover:
go test ./... -covermode=atomic -coverprofile=/tmp/coverage.out -coverpkg=./... -count=1
go tool cover -html=/tmp/coverage.out

test-integration:
go test -tags integration ./... -covermode=atomic -coverpkg=./... -count=1 -race

### Formatting, linting, and deps
fmt:
go fmt ./...

ensure-deps:
@echo "==> Running go mod tidy"
go mod download
go mod tidy

linter:
@echo "==> Running linter"
golangci-lint run ./...

# To avoid unintended conflicts with file names, always add to .PHONY
# unless there is a reason not to.
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html
.PHONY: check_tools test test-cover fmt linter ensure-deps
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
[![codecov](https://codecov.io/gh/jattento/go-iso8583/branch/master/graph/badge.svg)](https://codecov.io/gh/jattento/go-iso8583)
[![Build Status](https://travis-ci.com/jattento/go-iso8583.svg?branch=master)](https://travis-ci.com/jattento/go-iso8583)

# go-iso8583

An easy to use, yet flexible marshaler for ISO-8583.

API [godoc](https://godoc.org/github.com/jattento/go-iso8583/pkg/iso8583) documentation.

This library provides:
- Marshal and Unmarshal functions with his respective interfaces
including MTI, VAR, LLVAR, LLLVAR and bitmaps fields ready for use
but with the possibility to easily add new field types.
- Inbuid Support for ASCII and EBCDIC but not limited to them


## Installation

To install go-iso8583 package, you need to install Go and set your Go workspace first.

1. First you need [Go](https://golang.org/) installed, then you can use the below Go command to install go-iso8583.
```sh
$ go get -u github.com/jattento/go-iso8583/iso8583
```

2. Import it in your code:
```go
import "github.com/jattento/go-iso8583/pkg/iso8583"
```

## Quick start

```go
import "github.com/go-iso8583/pkg/iso8583"

type PurchaseRequest struct {
MTI iso8583.MTI `iso8583:"mti"`
FirstBitmap iso8583.BITMAP `iso8583:"bitmap,length:64"` // length is the maximum amount of represented elements.
SecondBitmap iso8583.BITMAP `iso8583:"1,length:64"` // length is the maximum amount of represented elements.
PAN iso8583.LLVAR `iso8583:"2"`
ProcessingCode iso8583.VAR `iso8583:"3"`
Amount iso8583.VAR `iso8583:"4,encoding:ebcdic"` // By default ASCII is assumed but dont limit yourself!
DateTime iso8583.VAR `iso8583:"7"`
SystemTraceAuditNumber iso8583.VAR `iso8583:"11,omitempty"` // omitempty is supported!
LocalTransactionTime iso8583.VAR `iso8583:"12"`
LocalTransactionDate iso8583.VAR `iso8583:"-"` // You can explicitly ignore a field.
ExpirationDate iso8583.VAR `iso8583:"14"`
MerchantType iso8583.VAR `iso8583:"18"`
ICC iso8583.LLLVAR `iso8583:"55"`
SettlementCode iso8583.VAR `iso8583:"66"`
MessageNumber iso8583.VAR `iso8583:"71"`
TransactionDescriptor iso8583.VAR `iso8583:"104"`
}

func GenerateStaticReqBytes() ([]byte, error) {
req := PurchaseRequest{
MTI: "0100",
// FirstBitmap is generated by library
// SecondBitmap is generated by library
PAN: "54000000000000111", // LL part is added by library!
ProcessingCode: "1000",
Amount: "000000000100",
MessageNumber: "1",
}

byt, err := iso8583.Marshal(req)
if err != nil {
return nil, err
}

return byt, nil
}
```

```go
import "github.com/go-iso8583/pkg/iso8583"

type PurchaseResponse struct {
MTI iso8583.MTI `iso8583:"mti,length:4"`
FirstBitmap iso8583.BITMAP `iso8583:"bitmap,length:64"` // length is the maximum amount of represented elements.
SecondBitmap iso8583.BITMAP `iso8583:"1,length:64"` // length is the maximum amount of represented elements.
PAN iso8583.LLVAR `iso8583:"2,length:2"` // length is the amount of bytes of the LL part.
ProcessingCode iso8583.VAR `iso8583:"3,length:6"`
Amount iso8583.VAR `iso8583:"4,length:12"`
DateTime iso8583.VAR `iso8583:"7,length:10"`
SystemTraceAuditNumber iso8583.VAR `iso8583:"11,length:6"`
LocalTransactionTime iso8583.VAR `iso8583:"12,length:6"`
LocalTransactionDate iso8583.VAR `iso8583:"13,length:4"`
ExpirationDate iso8583.VAR `iso8583:"14,length:4"`
MerchantType iso8583.VAR `iso8583:"18,length:4"`
ResponseCode iso8583.VAR `iso8583:"39,length:2"`
ICC iso8583.LLLVAR `iso8583:"55,length:3,encoding:ebcdic/ascii"` // LLL and VAR part use different encoding? Use a / to indicate both
SettlementCode iso8583.VAR `iso8583:"66,length:1"`
MessageNumber iso8583.VAR `iso8583:"71,length:4"`
TransactionDescriptor iso8583.VAR `iso8583:"104,length:100"`
}

func ReadResp(byt []byte) (PurchaseResponse,error){
var resp PurchaseResponse

_, err :=iso8583.Unmarshal(byt,&resp)
if err != nil{
return PurchaseResponse{}, err
}

return resp,nil
}
```
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/jattento/go-iso8583

go 1.14

require github.com/stretchr/testify v1.6.1
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
153 changes: 153 additions & 0 deletions pkg/bitmap/bitmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package bitmap

import (
"errors"
"fmt"
"math"
)

type Bitmap = map[int]bool

const (
_bitmapLength = 8
_bitsInByte = 8

_firstByteIndex = 0
_firstBitOffset = 7
_lastBitOffset = 0
)

var (
// Errors...
ErrBitmapISOWrongLength = errors.New("wrong bitmap length input")
ErrBitmapISOBadBitmapPosition = errors.New("bad bitmap position input")
ErrBitmapISOImpossibleBitmap = errors.New("impossible generate bitmap, lowest and highest limits too far")
ErrBitmapISOFirstBitProhibited = errors.New("first bit can be setted manually in input")
)

// ISO8583FromBytes indicates which elements of a ISO8583 message are present.
// It receives a 8 byte long ISO8583 bitmap with a position (to indicate if its the first or second).
// Returns a map[int]bool to allow searching by element.
func ISO8583FromBytes(b []byte, bitmapPosition int) (presentElements Bitmap, nextBitmapPresent bool, returnErr error) {
const nextBitmapIndicator = 1

// Validate input
if len(b) != _bitmapLength {
return nil, false,
fmt.Errorf("%w: should be %v, but its %v", ErrBitmapISOWrongLength, _bitmapLength, len(b))
}

if bitmapPosition < 1 {
return nil, false,
fmt.Errorf("%w: should not be lower than 1, but its %v", ErrBitmapISOBadBitmapPosition, bitmapPosition)
}

rawBitmap := FromBytes(b)
isoBitmap := make(Bitmap)

for k, v := range rawBitmap {
// Next bitmap indicator is returned separately in the second return value
if k != nextBitmapIndicator {
isoBitmap[_bitsInByte*_bitmapLength*(bitmapPosition-1)+k] = v
}
}

return isoBitmap, rawBitmap[nextBitmapIndicator], nil
}

// ISO8583ToBytes creates a bitmap in byte format.
// Map key 1 must not be present.
func ISO8583ToBytes(b Bitmap, nextBitmapPresent bool) ([]byte, error) {
// Find the highest and lowest element in map
lowestElement, highestElement := Extremities(b)

inferiorLimit := 1
for checkedLimit := 1; checkedLimit < lowestElement; checkedLimit += 64 {
inferiorLimit = checkedLimit
}

superiorLimit := inferiorLimit + 63

if superiorLimit < highestElement {
return nil, fmt.Errorf("%w: lowest limit %v (element %v), highest limit %v (element %v)",
ErrBitmapISOImpossibleBitmap, inferiorLimit, lowestElement, superiorLimit, highestElement)
}

if _, exist := b[inferiorLimit]; exist {
return nil, fmt.Errorf("%w: position %v", ErrBitmapISOFirstBitProhibited, inferiorLimit)
}

bmap := b
bmap[inferiorLimit] = nextBitmapPresent

if _, exist := bmap[superiorLimit]; !exist {
bmap[superiorLimit] = false
}

byt := ToBytes(bmap)

return byt[len(byt)-_bitmapLength:], nil
}

// FromBytes given bytes it returns a map[int]bool indicating which biy is on or off. Most left is 1.
func FromBytes(b []byte) Bitmap {
availableElements := make(Bitmap)

// Iterate over each bit of the input from most left to most right
for bytePosition := _firstByteIndex; bytePosition < len(b); bytePosition++ {
for bitOffset := _firstBitOffset; bitOffset >= _lastBitOffset; bitOffset-- {
// Calculate element position and save in map
previousBytesSummary := _bitsInByte * bytePosition
bitPosition := _bitsInByte - bitOffset

availableElements[bitPosition+previousBytesSummary] = hasBitSet(b[bytePosition], uint(bitOffset))
}
}

return availableElements
}

// ToBytes creates a bitmap in []byte format. Most left is 1.
func ToBytes(b Bitmap) []byte {
getPosition := func(byt, bit int) int { return bit + byt*_bitsInByte }

_, highestElement := Extremities(b)

bmap := make([]byte, int(math.Ceil(float64(highestElement)/_bitsInByte)))
for bytePosition := _firstByteIndex; bytePosition < len(bmap); bytePosition++ {
for bitOffset := _firstBitOffset; bitOffset >= _lastBitOffset; bitOffset-- {
if isOn, exist := b[getPosition(bytePosition, _bitsInByte-bitOffset)]; exist && isOn {
bmap[bytePosition] = setBit(bmap[bytePosition], uint(bitOffset))
}
}
}

return bmap
}

// Returns if the indicated bit is on.
func hasBitSet(n byte, pos uint) bool {
val := n & (1 << pos)
return val > 0
}

// Sets the bit at pos in the integer n.
func setBit(n byte, pos uint) byte {
n |= 1 << pos
return n
}

func Extremities(b Bitmap) (low, high int) {
firstIteration := true
for k := range b {
if k > high {
high = k
}
if k < low || firstIteration {
low = k
}
firstIteration = false
}

return low, high
}
Loading

0 comments on commit e475308

Please sign in to comment.