Categorygithub.com/renproject/surge
modulepackage
1.2.7
Repository: https://github.com/renproject/surge.git
Documentation: pkg.go.dev

# README

🔌 surge

GitHub Coverage Report

Documentation

A library for fast binary (un)marshaling. Designed to be used in Byzantine networks, 🔌 surge never explicitly panics, protects against malicious inputs, allocates minimally, and has very few dependencies (its only dependency is the ginkgo testing framework). It supports the (un)marshaling of:

  • scalars,
  • arrays,
  • slices,
  • maps,
  • structs, and
  • custom implementations (using the Marshaler and Unmarshaler interfaces).

Built-in Types

All built-in types that can be marshaled are supported by surge. And, for the vast majority of use cases, ToBinary and FromBinary are the only functions that you will need to use:

Scalars

// Marshal
x := uint64(42)
data, err := surge.ToBinary(x)
if err != nil {
    panic(err)
}

// Unmarshal
y := uint64(0)
if err := surge.FromBinary(&y, data); err != nil {
    panic(err)
}

Arrays

Arrays are collections of a known, fixed, length. Arrays are not length prefixed, because their length is part of their type. All arrays marshal their elements one-by-one, with the exception of byte arrays (which are marshaled in bulk using copy):

// Marshal
x := [4]uint64{42, 43, 44, 45}
data, err := surge.ToBinary(x)
if err != nil {
    panic(err)
}

// Unmarshal
y := [4]uint64{}
if err := surge.FromBinary(&y, data); err != nil {
    panic(err)
}

Slices

Slices are collections of variable length. Slices are length prefixed, because their length is not known at compile-time. All slices marshal their elements one-by-one, with the exception of byte slices (which are marshaled in bulk using copy):

// Marshal
x := []uint64{42, 43, 44, 45}
data, err := surge.ToBinary(x)
if err != nil {
    panic(err)
}

// Unmarshal
y := []uint64{}
if err := surge.FromBinary(&y, data); err != nil {
    panic(err)
}

Maps

Maps are effectively slices of key/value pairs. Maps are length prefixed, because their length is not known at compile-time. Maps are marshaled as a sorted slice of (key, value) tuples, sorted lexographically by keys (after the key has been marshaled, because not all key types are directly comparable). Sorting is done because it guarantees that the binary output is always the same when the key/value pairs are the same (this is particularly useful when hashing/signing maps for authenticity):

// Marshal
x := map[string]uint64{"foo": 42, "bar": 43, "baz": 44}
data, err := surge.ToBinary(x)
if err != nil {
    panic(err)
}

// Unmarshal
y := map[string]uint64{}
if err := surge.FromBinary(&y, data); err != nil {
    panic(err)
}

User-defined types

The same pattern that we have seen above works for custom structs too. You will not need to make any changes to your struct, as long as all of its fields are marshalable by surge:

type MyStruct struct {
    Foo int64
    Bar float64
    Baz MyInnerStruct
}

type MyInnerStruct struct {
    Inner1 []bool
    Inner2 []string
}

// Marshal
x := MyStruct{
    Foo: int64(43),
    Bar: float64(3.14),
    Baz: MyInnerStruct{
        Inner1: []bool{true, false},
        Inner2: []string{"hello", "world"},
    },
}
data, err := surge.ToBinary(x)
if err != nil {
    panic(err)
}

// Unmarshal
y := MyStruct{}
if err := surge.FromBinary(&y, data); err != nil {
    panic(err)
}

Specialisation

Using the default marshaler built into surge is great for prototyping, and will good enough for many applications. But, sometimes we need to specialise our marshaling. Providing our own implementation will not only be faster, but it will also give us the ability to customise the marshaler (which can be necessary when thinking about backward compatibility, etc.):

type MyStruct struct {
  Foo int64
  Bar float64
  Baz string
}

// SizeHint tells surge how many bytes our
// custom type needs when being represented
// in its binary form.
func (myStruct MyStruct) SizeHint() int {
    return surge.SizeHintI64 +
           surge.SizeHintF64 +
           surge.SizeHintString(myStruct.Baz)
}

// Marshal tells surge exactly how to marshal
// our custom type. As you can see, most implementations
// will be very straight forward, and mostly exist
// for performance reasons. In the future, surge might
// adopt some kind of generator to automatically
// generate these implementations.
func (myStruct MyStruct) Marshal(buf []byte, rem int) ([]byte, int, error) {
    var err error
    if buf, rem, err = surge.MarshalI64(myStruct.Foo, buf, rem); err != nil {
        return buf, rem, err
    }
    if buf, rem, err = surge.MarshalF64(myStruct.Bar, buf, rem); err != nil {
        return buf, rem, err
    }
    if buf, rem, err = surge.MarshalString(myStruct.Baz, buf, rem); err != nil {
        return buf, rem, err
    }
    return buf, rem, err
}

// Unmarshal is the opposite of Marshal, and requires
// a pointer receiver.
func (myStruct *MyStruct) Unmarshal(buf []byte, rem int) ([]byte, int, error) {
    var err error
    if buf, rem, err = surge.UnmarshalI64(&myStruct.Foo, buf, rem); err != nil {
        return buf, rem, err
    }
    if buf, rem, err = surge.UnmarshalF64(&myStruct.Bar, buf, rem); err != nil {
        return buf, rem, err
    }
    if buf, rem, err = surge.UnmarshalString(&myStruct.Baz, buf, rem); err != nil {
        return buf, rem, err
    }
    return buf, rem, err
}

Testing

Testing custom marshaling implementations is incredibly important, but it can also be very tedious, and so it is rarely done as extensively as it should be. Luckily, surge helps us get this done quickly. By using the surgeutil package, we can write comprehensive tests very quickly:

func TestMyStruct(t *testing.T) {
    // Reflect on our custom type
    t := reflect.TypeOf(MyStruct{})
    
    // Fuzz and expect that it does not panic.
    surgeutil.Fuzz(t)
    
    // Marshal, then unmarshal, then check for
    // equality, and expect there to be no
    // errors.
    if err := surgeutil.MarshalUnmarshalCheck(t); err != nil {
        t.Fatalf("bad marshal/unmarshal/check: %v", err)
    }
    
    // Marshal when the buffer is too small
    // and check that it does not work.
    if err := surgeutil.MarshalBufTooSmall(t); err != nil {
        t.Fatalf("bad marshal with insufficient buffer: %v", err)
    }
    
    // Marshal when the remaining memory quota
    // is too small and check that it does not
    // work.
    if err := surgeutil.MarshalRemTooSmall(t); err != nil {
        t.Fatalf("bad marshal with insufficient rem quota: %v", err)
    }
    
    // Unmarshal when the buffer is too small
    // and check that it does not work.
    if err := surgeutil.UnmarshalBufTooSmall(t); err != nil {
        t.Fatalf("bad marshal with insufficient buffer: %v", err)
    }
    
    // Unmarshal when the remaining memory quota
    // is too small and check that it does not
    // work.
    if err := surgeutil.UnmarshalRemTooSmall(t); err != nil {
        t.Fatalf("bad marshal with insufficient rem quota: %v", err)
    }
}

Internally, surgeutil makes use of the quick standard library. So, for surgeutil to work, your type needs to be compatible with quick. This is usually automatic, and most of the time you will not need to think about quick at all. For the more exotic types, that do need custom support, all you need to do is implement the quick.Generator interface. For more examples of surgeutil in use, checkout any of the *_test.go files. All of the testing in surge is done using the surgeutil package.

Benchmarks

When using specialised implementations, surge is about as fast as you can get; it does not really do much under-the-hood. When using the default implementations, the need to use reflect introduces some slow-down, but performance is still faster than most alternatives:

goos: darwin
goarch: amd64
pkg: github.com/renproject/surge
BenchmarkPointMarshalJSON-8              2064483               563 ns/op              80 B/op          1 allocs/op
BenchmarkTriangleMarshalJSON-8            583173              1752 ns/op             239 B/op          1 allocs/op
BenchmarkModelMarshalJSON-8                 7018            163255 ns/op           24588 B/op          1 allocs/op
BenchmarkPointMarshal-8                 11212546               109 ns/op               0 B/op          0 allocs/op
BenchmarkTriangleMarshal-8               3700579               294 ns/op               0 B/op          0 allocs/op
BenchmarkModelMarshal-8                    38652             28270 ns/op              32 B/op          1 allocs/op
BenchmarkFoo-8                          33130609                33 ns/op               0 B/op          0 allocs/op

Contributions

Built with ❤ by Ren.

# Packages

No description provided by the author

# Functions

FromBinary unmarshals a byte representation of a value to a pointer to that value.
Marshal a value into its binary representation, and store the value in a byte slice.
MarshalBool into a byte slice.
MarshalBytes into a byte slice.
MarshalF32 into a byte slice.
MarshalF64 into a byte slice.
MarshalI16 into a byte slice.
MarshalI32 into a byte slice.
MarshalI64 into a byte slice.
MarshalI8 into a byte slice.
MarshalLen marshals the given slice length.
MarshalString into a byte slice.
MarshalU16 into a byte slice.
MarshalU32 into a byte slice.
MarshalU64 into a byte slice.
MarshalU8 into a byte slice.
NewErrUnsupportedMarshalType constructs a new unsupported marshal type error for the given type.
NewErrUnsupportedUnmarshalType constructs a new unsupported unmarshal type error for the given type.
SizeHint returns the number of bytes required to store a value in its binary representation.
SizeHintBytes is the number of bytes required to represent the given byte slice in binary.
SizeHintString is the number of bytes required to represent the given string in binary.
ToBinary returns the byte representation of a value.
Unmarshal a value from its binary representation by reading from a byte slice.
UnmarshalBool from a byte slice.
UnmarshalBytes from a byte slice.
UnmarshalF32 from a byte slice.
UnmarshalF64 from a byte slice.
UnmarshalI16 from a byte slice.
UnmarshalI32 from a byte slice.
UnmarshalI64 from a byte slice.
UnmarshalI8 from a byte slice.
UnmarshalLen unmarshals a slice length, checking that the total space required for the slice will not exceed rem.
UnmarshalString from a byte slice.
UnmarshalU16 from a byte slice.
UnmarshalU32 from a byte slice.
UnmarshalU64 from a byte slice.
UnmarshalU8 from a byte slice.

# Constants

MaxBytes is set to 64 MB by default.
SizeHintBool is the number of bytes required to represent a boolean value in binary.
SizeHintF32 is the number of bytes required to represent a float32 value in binary.
SizeHintF64 is the number of bytes required to represent a float64 value in binary.
SizeHintI16 is the number of bytes required to represent a int16 value in binary.
SizeHintI32 is the number of bytes required to represent a int32 value in binary.
SizeHintI64 is the number of bytes required to represent a int64 value in binary.
SizeHintI8 is the number of bytes required to represent a int8 value in binary.
SizeHintU16 is the number of bytes required to represent a uint16 value in binary.
SizeHintU32 is the number of bytes required to represent a uint32 value in binary.
SizeHintU64 is the number of bytes required to represent a uint64 value in binary.
SizeHintU8 is the number of bytes required to represent a uint8 value in binary.

# Variables

ErrLengthOverflow is returned when the length of an array or slice has overflowed.
ErrUnexpectedEndOfBuffer is used when reading/writing from/to a buffer that has less space than expected.

# Structs

ErrUnsupportedMarshalType is returned when the an unsupported type is encountered during marshaling.
ErrUnsupportedUnmarshalType is returned when the an unsupported type is encountered during unmarshaling.

# Interfaces

A Marshaler can marshal itself into bytes.
A MarshalUnmarshaler is a marshaler and an unmarshaler.
A SizeHinter can hint at the number of bytes required to represented it in binary.
An Unmarshaler can unmarshal itself from bytes.