Skip to content

g4s8/go-bundle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CI GitHub release

Hits-of-Code Lines of code

Simple binary encoding and decoding tool, a type-safe wrapper for encoding/binary package.

Implements BinaryMarshaler and BinaryUnmarshaler from encoding, supports different encoding.BiteOrders. Errors accumulator to avoid each line error checks. It could be useful to implement custom encoding binary protocols.

The problem

Go has a package encoding/binary for binary serialization, it provides good and low-level API for encoding. The problem is that it's not really handy and requires a lot of boilerplate code to read and write data to stream, such as: lack of type safety (accepts interface{} params) error-checks for each line, custom array/slice encoding/decoding protocols, custom nested encoding/decoding.

This library aims to solve these issues by providing type-safe and extended wrapper over encoding/binary package which reduce the amount of bolierplate codes and ensures type-safety.

Since it uses encoding/binary under the hood, keep in mind that:

This package favors simplicity over efficiency. Clients that require high-performance serialization, especially for large data structures, should look at more advanced solutions such as the encoding/gob package or protocol buffers.

Install

Add to imports: "github.com/g4s8/go-bundle"

Usage

The bundle package provides two main types: Out and Inp. Out (output) is used to put data into bundle and produce byte array from it, Inp (input) is used to read data from bytes array.

Output

Create new bundle output

There are different factory functions to create bundle outputs:

out := NewOut(order) // creates bundle and specify byte order
outBE := NewBEOut() // creates bundle with big-endian byte order
outLE := NewLEOut() // creates bundle with little-endian byte order

Put data into bundle

Each out bundle method has type-name suffix and accepts only fixed-length and strongly typed parameters:

out := NewBEOut()
out.PutInt32(int32(42))
out.PutUIint64(uint64(1111111111111))
out.PutFloat32(float32(1.4))
out.PutBytes([]byte{0x00, 0x01, 0x02})
out.PutString("hello bundle!")

Also, output accepts encoding.BinaryMarshaler types to put: out.PutBinary(binary).

Nested bundles

Bundle can contain nested bundles, to put one bundle into another use PutBundle method:

parent := NewLEOut()
nested := NewLEOut()
parent.PutBundle(nested)

Errors

Output Put* methods doesn't return errors, but in case of error it will be accumulated. It can be checked with out.Error() method. If error happens, put method won't try to put next values to internal bytes buffer.

Marshaling

Output implements encoding.BinaryMarshaler protocol. It can be used to get byte array of the bundle:

out := NewBEOut()
out.PutInt32(int32(42))
bytes, err := out.MarshalBinary()

It returns buffer bytes or first error.

Flip

Flip methods creates input from output: func (o *Out) Flip() *Inp. It can be useful for testing.

Input

Creating new input

These factory functions can be used to wrap byte array with bundle input type:

i1 := NewInput(order) // creates input with specified order
i1 := NewBEInput() // creates big-endian input
i3 := NewLEInput() // creates little-endian input
i4 := InpWrapBytes(order, bts) // creates input from bytes with order

Filling input with data

Bundle input implements encoding.BinaryUnmarshaler protocol, it means it can be read from byte array:

inp := NewInput(binary.BigEndian)
err := inp.UnmarshalBinary(data)

Alternatively, it can be constructed directly from bytes using wrap factory method:

inp := InpWrapBytes(order, bytes)

Input can't be read twise, the call to UnmarshalBinary returns an error if input was already read.

Reading data from input

Input bundle methods has type suffixes and accepts output reference parameters to read the data:

// Reads int32
var i32 int32
inp.ReadInt32(&i32)

// Reads byte-array
var bts []byte
inp.ReadBytes(&bts)

// Read binary unmarshaler
// subInput - implements `encoding.BinaryUnmarshaler`
inp.ReadBinary(subInput)

Reading nested bundles

Inp can contain nested bundles, to read it create new empty Inp with new(Inp) and read it from parent bundle:

// var parent Inp
nested := new(Inp)
parent.GetBundle(nested)

Errors

Errors are accumulated and can be accessed with Error() method. In case of error, next read methods won't be performed.

Example

Assuming you need to implement encoding.BinaryMarshaler and encoding.BinaryUnmarshaler for some struct. You can use bundle to do that:

import (
  "encoding/binary"
  "github.com/g4s8/go-bundle"
  ma "github.com/multiformats/go-multiaddr"
)

type Foo struct {
  ID int32
  Description string
  internal []byte
  Addr ma.Multiaddr
}

var byteOrder = binary.BigEndian

func (f *Foo) MarshalBinary() ([]byte, error) {
  out := bundle.NewOut(byteOrder)
  out.PutInt32(f.ID)
  out.PutString(f.Description)
  out.PutBytes(f.internal)
  out.PutBinary(f.Addr)
  return out.MarshalBinary()
}

func (f *Foo) UnmarshalBinary(data []byte) error {
  inp := bundle.NewInput(byteOrder)
  if err := inp.UnmarshalBinary(data); err != nil {
         return err
  }
  inp.GetInt32(&f.ID)
  inp.GetString(&f.Description)
  inp.GetBytes(&f.internal)
  var rawAddr []byte
  inp.GetBytes(&rawAddr)
  addr, err := ma.NewMultiaddrBytes(rawAddr)
  if err != nil {
    return err
  }
  f.Addr = addr
  return nil
}