├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md └── binary-pack ├── binary_pack.go └── binary_pack_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # External packages folder 30 | vendor/ 31 | 32 | # Created by .ignore support plugin (hsz.mobi) 33 | # IntelliJ project files 34 | .idea 35 | *.iml 36 | out 37 | gen 38 | 39 | # Sensitive or high-churn files: 40 | .idea/dataSources/ 41 | .idea/dataSources.ids 42 | .idea/dataSources.xml 43 | .idea/dataSources.local.xml 44 | .idea/sqlDataSources.xml 45 | .idea/dynamic.xml 46 | .idea/uiDesigner.xml 47 | 48 | # Gradle: 49 | .idea/gradle.xml 50 | .idea/libraries 51 | 52 | # Mongo Explorer plugin: 53 | .idea/mongoSettings.xml 54 | 55 | ## File-based project format: 56 | *.iws 57 | 58 | ## Plugin-specific files: 59 | 60 | # IntelliJ 61 | /out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: golang 2 | golang: 1.7.1 3 | script: 4 | - "go test -v -race ./..." 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Roman Kachanovsky 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of nor the names of its contributors may be used to 13 | endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go BinaryPack 2 | 3 | [![Build Status](https://travis-ci.org/roman-kachanovsky/go-binary-pack.svg?branch=master)](https://travis-ci.org/roman-kachanovsky/go-binary-pack) 4 | [![GoDoc](https://godoc.org/github.com/roman-kachanovsky/go-binary-pack/binary-pack?status.svg)](http://godoc.org/github.com/roman-kachanovsky/go-binary-pack/binary-pack) 5 | 6 | BinaryPack is a simple Golang library which implements some functionality of Python's [struct](https://docs.python.org/2/library/struct.html) package. 7 | 8 | **Install** 9 | 10 | `go get github.com/roman-kachanovsky/go-binary-pack/binary-pack` 11 | 12 | **How to use** 13 | 14 | ```go 15 | // Prepare format (slice of strings) 16 | format := []string{"I", "?", "d", "6s"} 17 | 18 | // Prepare values to pack 19 | values := []interface{}{4, true, 3.14, "Golang"} 20 | 21 | // Create BinaryPack object 22 | bp := new(BinaryPack) 23 | 24 | // Pack values to []byte 25 | data, err := bp.Pack(format, values) 26 | 27 | // Unpack binary data to []interface{} 28 | unpacked_values, err := bp.UnPack(format, data) 29 | 30 | // You can calculate size of expected binary data by format 31 | size, err := bp.CalcSize(format) 32 | 33 | ``` 34 | -------------------------------------------------------------------------------- /binary-pack/binary_pack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Roman Kachanovsky. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package binary_pack performs conversions between some Go values represented as byte slices. 7 | This can be used in handling binary data stored in files or from network connections, 8 | among other sources. It uses format slices of strings as compact descriptions of the layout 9 | of the Go structs. 10 | 11 | Format characters (some characters like H have been reserved for future implementation of unsigned numbers): 12 | ? - bool, packed size 1 byte 13 | h, H - int, packed size 2 bytes (in future it will support pack/unpack of int8, uint8 values) 14 | i, I, l, L - int, packed size 4 bytes (in future it will support pack/unpack of int16, uint16, int32, uint32 values) 15 | q, Q - int, packed size 8 bytes (in future it will support pack/unpack of int64, uint64 values) 16 | f - float32, packed size 4 bytes 17 | d - float64, packed size 8 bytes 18 | Ns - string, packed size N bytes, N is a number of runes to pack/unpack 19 | 20 | */ 21 | package binary_pack 22 | 23 | import ( 24 | "strings" 25 | "strconv" 26 | "errors" 27 | "encoding/binary" 28 | "bytes" 29 | "fmt" 30 | ) 31 | 32 | type BinaryPack struct {} 33 | 34 | // Return a byte slice containing the values of msg slice packed according to the given format. 35 | // The items of msg slice must match the values required by the format exactly. 36 | func (bp *BinaryPack) Pack(format []string, msg []interface{}) ([]byte, error) { 37 | if len(format) > len(msg) { 38 | return nil, errors.New("Format is longer than values to pack") 39 | } 40 | 41 | res := []byte{} 42 | 43 | for i, f := range format { 44 | switch f { 45 | case "?": 46 | casted_value, ok := msg[i].(bool) 47 | if !ok { 48 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (bool)") 49 | } 50 | res = append(res, boolToBytes(casted_value)...) 51 | case "h", "H": 52 | casted_value, ok := msg[i].(int) 53 | if !ok { 54 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (int, 2 bytes)") 55 | } 56 | res = append(res, intToBytes(casted_value, 2)...) 57 | case "i", "I", "l", "L": 58 | casted_value, ok := msg[i].(int) 59 | if !ok { 60 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (int, 4 bytes)") 61 | } 62 | res = append(res, intToBytes(casted_value, 4)...) 63 | case "q", "Q": 64 | casted_value, ok := msg[i].(int) 65 | if !ok { 66 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (int, 8 bytes)") 67 | } 68 | res = append(res, intToBytes(casted_value, 8)...) 69 | case "f": 70 | casted_value, ok := msg[i].(float32) 71 | if !ok { 72 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (float32)") 73 | } 74 | res = append(res, float32ToBytes(casted_value, 4)...) 75 | case "d": 76 | casted_value, ok := msg[i].(float64) 77 | if !ok { 78 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (float64)") 79 | } 80 | res = append(res, float64ToBytes(casted_value, 8)...) 81 | default: 82 | if strings.Contains(f, "s") { 83 | casted_value, ok := msg[i].(string) 84 | if !ok { 85 | return nil, errors.New("Type of passed value doesn't match to expected '" + f + "' (string)") 86 | } 87 | n, _ := strconv.Atoi(strings.TrimRight(f, "s")) 88 | res = append(res, []byte(fmt.Sprintf("%s%s", 89 | casted_value, strings.Repeat("\x00", n - len(casted_value))))...) 90 | } else { 91 | return nil, errors.New("Unexpected format token: '" + f + "'") 92 | } 93 | } 94 | } 95 | 96 | return res, nil 97 | } 98 | 99 | // Unpack the byte slice (presumably packed by Pack(format, msg)) according to the given format. 100 | // The result is a []interface{} slice even if it contains exactly one item. 101 | // The byte slice must contain not less the amount of data required by the format 102 | // (len(msg) must more or equal CalcSize(format)). 103 | func (bp *BinaryPack) UnPack(format []string, msg []byte) ([]interface{}, error) { 104 | expected_size, err := bp.CalcSize(format) 105 | 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | if expected_size > len(msg) { 111 | return nil, errors.New("Expected size is bigger than actual size of message") 112 | } 113 | 114 | res := []interface{}{} 115 | 116 | for _, f := range format { 117 | switch f { 118 | case "?": 119 | res = append(res, bytesToBool(msg[:1])) 120 | msg = msg[1:] 121 | case "h", "H": 122 | res = append(res, bytesToInt(msg[:2])) 123 | msg = msg[2:] 124 | case "i", "I", "l", "L": 125 | res = append(res, bytesToInt(msg[:4])) 126 | msg = msg[4:] 127 | case "q", "Q": 128 | res = append(res, bytesToInt(msg[:8])) 129 | msg = msg[8:] 130 | case "f": 131 | res = append(res, bytesToFloat32(msg[:4])) 132 | msg = msg[4:] 133 | case "d": 134 | res = append(res, bytesToFloat64(msg[:8])) 135 | msg = msg[8:] 136 | default: 137 | if strings.Contains(f, "s") { 138 | n, _ := strconv.Atoi(strings.TrimRight(f, "s")) 139 | res = append(res, string(msg[:n])) 140 | msg = msg[n:] 141 | } else { 142 | return nil, errors.New("Unexpected format token: '" + f + "'") 143 | } 144 | } 145 | } 146 | 147 | return res, nil 148 | } 149 | 150 | // Return the size of the struct (and hence of the byte slice) corresponding to the given format. 151 | func (bp *BinaryPack) CalcSize(format []string) (int, error) { 152 | var size int 153 | 154 | for _, f := range format { 155 | switch f { 156 | case "?": 157 | size = size + 1 158 | case "h", "H": 159 | size = size + 2 160 | case "i", "I", "l", "L", "f": 161 | size = size + 4 162 | case "q", "Q", "d": 163 | size = size + 8 164 | default: 165 | if strings.Contains(f, "s") { 166 | n, _ := strconv.Atoi(strings.TrimRight(f, "s")) 167 | size = size + n 168 | } else { 169 | return 0, errors.New("Unexpected format token: '" + f + "'") 170 | } 171 | } 172 | } 173 | 174 | return size, nil 175 | } 176 | 177 | func boolToBytes(x bool) []byte { 178 | if x { 179 | return intToBytes(1, 1) 180 | } 181 | return intToBytes(0, 1) 182 | } 183 | 184 | func bytesToBool(b []byte) bool { 185 | return bytesToInt(b) > 0 186 | } 187 | 188 | func intToBytes(n int, size int) []byte { 189 | buf := bytes.NewBuffer([]byte{}) 190 | binary.Write(buf, binary.LittleEndian, int64(n)) 191 | return buf.Bytes()[0:size] 192 | } 193 | 194 | func bytesToInt(b []byte) int { 195 | buf := bytes.NewBuffer(b) 196 | 197 | switch len(b) { 198 | case 1: 199 | var x int8 200 | binary.Read(buf, binary.LittleEndian, &x) 201 | return int(x) 202 | case 2: 203 | var x int16 204 | binary.Read(buf, binary.LittleEndian, &x) 205 | return int(x) 206 | case 4: 207 | var x int32 208 | binary.Read(buf, binary.LittleEndian, &x) 209 | return int(x) 210 | default: 211 | var x int64 212 | binary.Read(buf, binary.LittleEndian, &x) 213 | return int(x) 214 | } 215 | } 216 | 217 | func float32ToBytes(n float32, size int) []byte { 218 | buf := bytes.NewBuffer([]byte{}) 219 | binary.Write(buf, binary.LittleEndian, n) 220 | return buf.Bytes()[0:size] 221 | } 222 | 223 | func bytesToFloat32(b []byte) float32 { 224 | var x float32 225 | buf := bytes.NewBuffer(b) 226 | binary.Read(buf, binary.LittleEndian, &x) 227 | return x 228 | } 229 | 230 | func float64ToBytes(n float64, size int) []byte { 231 | buf := bytes.NewBuffer([]byte{}) 232 | binary.Write(buf, binary.LittleEndian, n) 233 | return buf.Bytes()[0:size] 234 | } 235 | 236 | func bytesToFloat64(b []byte) float64 { 237 | var x float64 238 | buf := bytes.NewBuffer(b) 239 | binary.Read(buf, binary.LittleEndian, &x) 240 | return x 241 | } 242 | -------------------------------------------------------------------------------- /binary-pack/binary_pack_test.go: -------------------------------------------------------------------------------- 1 | package binary_pack 2 | 3 | import ( 4 | "testing" 5 | "reflect" 6 | ) 7 | 8 | func TestBinaryPack_CalcSize(t *testing.T) { 9 | cases := []struct { 10 | in []string 11 | want int 12 | e bool 13 | }{ 14 | {[]string{}, 0, false}, 15 | {[]string{"I", "I", "I", "4s"}, 16, false}, 16 | {[]string{"H", "H", "I", "H", "8s", "H"}, 20, false}, 17 | {[]string{"i", "?", "H", "f", "d", "h", "I", "5s"}, 30, false}, 18 | {[]string{"?", "h", "H", "i", "I", "l", "L", "q", "Q", "f", "d", "1s"}, 50, false}, 19 | // Unknown tokens 20 | {[]string{"a", "b", "c"}, 0, true}, 21 | } 22 | 23 | for _, c := range cases { 24 | got, err := new(BinaryPack).CalcSize(c.in) 25 | 26 | if err != nil && !c.e { 27 | t.Errorf("CalcSize(%v) raised %v", c.in, err) 28 | } 29 | 30 | if err == nil && got != c.want { 31 | t.Errorf("CalcSize(%v) == %d want %d", c.in, got, c.want) 32 | } 33 | } 34 | } 35 | 36 | func TestBinaryPack_Pack(t *testing.T) { 37 | cases := []struct { 38 | f []string 39 | a []interface{} 40 | want []byte 41 | e bool 42 | }{ 43 | {[]string{"?", "?"}, []interface{}{true, false}, []byte{1, 0}, false}, 44 | {[]string{"h", "h", "h"}, []interface{}{0, 5, -5}, 45 | []byte{0, 0, 5, 0, 251, 255}, false}, 46 | {[]string{"H", "H", "H"}, []interface{}{0, 5, 2300}, 47 | []byte{0, 0, 5, 0, 252, 8}, false}, 48 | {[]string{"i", "i", "i"}, []interface{}{0, 5, -5}, 49 | []byte{0, 0, 0, 0, 5, 0, 0, 0, 251, 255, 255, 255}, false}, 50 | {[]string{"I", "I", "I"}, []interface{}{0, 5, 2300}, 51 | []byte{0, 0, 0, 0, 5, 0, 0, 0, 252, 8, 0, 0}, false}, 52 | {[]string{"f", "f", "f"}, []interface{}{float32(0.0), float32(5.3), float32(-5.3)}, 53 | []byte{0, 0, 0, 0, 154, 153, 169, 64, 154, 153, 169, 192}, false}, 54 | {[]string{"d", "d", "d"}, []interface{}{0.0, 5.3, -5.3}, 55 | []byte{0, 0, 0, 0, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 21, 64, 51, 51, 51, 51, 51, 51, 21, 192}, false}, 56 | {[]string{"1s", "2s", "10s"}, []interface{}{"a", "bb", "1234567890"}, 57 | []byte{97, 98, 98, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48}, false}, 58 | {[]string{"I", "I", "I", "4s"}, []interface{}{1, 2, 4, "DUMP"}, 59 | []byte{1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 68, 85, 77, 80}, false}, 60 | // Wrong format length 61 | {[]string{"I", "I", "I", "4s"}, []interface{}{1, 4, "DUMP"}, nil, true}, 62 | // Wrong format token 63 | {[]string{"I", "a", "I", "4s"}, []interface{}{1, 2, 4, "DUMP"}, nil, true}, 64 | // Wrong types 65 | {[]string{"?"}, []interface{}{1.0}, nil, true}, 66 | {[]string{"H"}, []interface{}{int8(1)}, nil, true}, 67 | {[]string{"I"}, []interface{}{int32(2)}, nil, true}, 68 | {[]string{"Q"}, []interface{}{int64(3)}, nil, true}, 69 | {[]string{"f"}, []interface{}{float64(2.5)}, nil, true}, 70 | {[]string{"d"}, []interface{}{float32(2.5)}, nil, true}, 71 | {[]string{"1s"}, []interface{}{'a'}, nil, true}, 72 | } 73 | 74 | for _, c := range cases { 75 | got, err := new(BinaryPack).Pack(c.f, c.a) 76 | 77 | if err != nil && !c.e { 78 | t.Errorf("Pack(%v, %v) raised %v", c.f, c.a, err) 79 | } 80 | 81 | if err == nil && !reflect.DeepEqual(got, c.want) { 82 | t.Errorf("Pack(%v, %v) == %v want %v", c.f, c.a, got, c.want) 83 | } 84 | } 85 | } 86 | 87 | func TestBinaryPack_UnPack(t *testing.T) { 88 | cases := []struct { 89 | f []string 90 | a []byte 91 | want []interface{} 92 | e bool 93 | }{ 94 | {[]string{"?", "?"}, []byte{1, 0}, []interface{}{true, false}, false}, 95 | {[]string{"h", "h", "h"}, []byte{0, 0, 5, 0, 251, 255}, 96 | []interface{}{0, 5, -5}, false}, 97 | {[]string{"H", "H", "H"}, []byte{0, 0, 5, 0, 252, 8}, 98 | []interface{}{0, 5, 2300}, false}, 99 | {[]string{"i", "i", "i"}, []byte{0, 0, 0, 0, 5, 0, 0, 0, 251, 255, 255, 255}, 100 | []interface{}{0, 5, -5}, false}, 101 | {[]string{"I", "I", "I"}, []byte{0, 0, 0, 0, 5, 0, 0, 0, 252, 8, 0, 0}, 102 | []interface{}{0, 5, 2300}, false}, 103 | {[]string{"f", "f", "f"}, 104 | []byte{0, 0, 0, 0, 154, 153, 169, 64, 154, 153, 169, 192}, 105 | []interface{}{float32(0.0), float32(5.3), float32(-5.3)}, false}, 106 | {[]string{"d", "d", "d"}, 107 | []byte{0, 0, 0, 0, 0, 0, 0, 0, 51, 51, 51, 51, 51, 51, 21, 64, 51, 51, 51, 51, 51, 51, 21, 192}, 108 | []interface{}{0.0, 5.3, -5.3}, false}, 109 | {[]string{"1s", "2s", "10s"}, 110 | []byte{97, 98, 98, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48}, 111 | []interface{}{"a", "bb", "1234567890"}, false}, 112 | {[]string{"I", "I", "I", "4s"}, 113 | []byte{1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 68, 85, 77, 80}, 114 | []interface{}{1, 2, 4, "DUMP"}, false}, 115 | // Wrong format length 116 | {[]string{"I", "I", "I", "4s", "H"}, []byte{1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 68, 85, 77, 80}, 117 | nil, true}, 118 | // Wrong format token 119 | {[]string{"I", "a", "I", "4s"}, []byte{1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 68, 85, 77, 80}, 120 | nil, true}, 121 | } 122 | 123 | for _, c := range cases { 124 | got, err := new(BinaryPack).UnPack(c.f, c.a) 125 | 126 | if err != nil && !c.e { 127 | t.Errorf("UnPack(%v, %v) raised %v", c.f, c.a, err) 128 | } 129 | 130 | if err == nil && !reflect.DeepEqual(got, c.want) { 131 | t.Errorf("UnPack(%v, %v) == %v want %v", c.f, c.a, got, c.want) 132 | } 133 | } 134 | } 135 | 136 | func TestBinaryPackPartialRead(t *testing.T) { 137 | cases := []struct { 138 | f []string 139 | a []byte 140 | i int // Position of expected value 141 | want interface{} 142 | e bool 143 | }{ 144 | {[]string{"I", "I", "I"}, // []interface{}{1, 2, 4, "DUMP"} <- encoded collection has 4 values 145 | []byte{1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 68, 85, 77, 80}, 2, 4, false}, 146 | } 147 | 148 | for _, c := range cases { 149 | got, err := new(BinaryPack).UnPack(c.f, c.a) 150 | 151 | if err != nil && !c.e { 152 | t.Errorf("UnPack(%v, %v) raised %v", c.f, c.a, err) 153 | } 154 | 155 | if err == nil && got[c.i] != c.want { 156 | t.Errorf("UnPack(%v, %v) == %v want %v", c.f, c.a, got[c.i], c.want) 157 | } 158 | } 159 | } 160 | 161 | func TestBinaryPackUsageExample(t *testing.T) { 162 | // Prepare format (slice of strings) 163 | format := []string{"I", "?", "d", "6s"} 164 | 165 | // Prepare values to pack 166 | values := []interface{}{4, true, 3.14, "Golang"} 167 | 168 | // Create BinaryPack object 169 | bp := new(BinaryPack) 170 | 171 | // Pack values to struct 172 | data, _ := bp.Pack(format, values) 173 | 174 | // Unpack binary data to []interface{} 175 | unpacked_values, _ := bp.UnPack(format, data) 176 | 177 | if !reflect.DeepEqual(unpacked_values, values) { 178 | t.Errorf("Unpacked %v != original %v", unpacked_values, values) 179 | } 180 | 181 | // You can calculate size of expected binary data by format 182 | size, _ := bp.CalcSize(format) 183 | 184 | if size != len(data) { 185 | t.Errorf("Size(%v) != %v", size, len(data)) 186 | } 187 | } 188 | --------------------------------------------------------------------------------