summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Chabowski <kevin@kch42.de>2014-03-21 21:49:34 +0100
committerKevin Chabowski <kevin@kch42.de>2014-03-21 21:49:34 +0100
commit931a17e36d5c972cbf17ee739b81529aa97883cb (patch)
treee6ecc9f5f0d1157a8df2a1da219dea949319791f
downloadbinproto-931a17e36d5c972cbf17ee739b81529aa97883cb.tar.gz
binproto-931a17e36d5c972cbf17ee739b81529aa97883cb.tar.bz2
binproto-931a17e36d5c972cbf17ee739b81529aa97883cb.zip
Initial commit
-rw-r--r--README.markdown54
-rw-r--r--binproto_test.go262
-rw-r--r--binprotodebug/main.go326
-rw-r--r--binstream.go114
-rw-r--r--common.go68
-rw-r--r--demux.go75
-rw-r--r--demux_test.go75
-rw-r--r--idkvmapscanner.go187
-rw-r--r--idkvmapscanner_test.go132
-rw-r--r--recv.go256
-rw-r--r--send.go97
11 files changed, 1646 insertions, 0 deletions
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..3ab3c57
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,54 @@
+# binproto
+
+binproto is a simple binary protocol written in Go. It was originally a part of a larger (now discontinued) project that I'll probably never publish.
+
+This was pretty much the first code that I've written in Go, so it contains some unidiomatic and ugly stuff. For example, it uses no reflection, which can make reading and writing data quite tedious.
+
+Another bad thing: Currently the sending code has no buffering and therefore sends a *lot* of small TCP packets where a single larger one would be better, adding quite a bit of overhead.
+
+Still, it's BinStream data type is quite nice: By sending a BinStream you open a data stream inside of the stream (yo, dawg...), allowing you to send arbitrary data without too much overhead.
+
+I'll publish this code, despite it's many quirks. Perhaps someone has a use for it?
+
+## Installation
+
+`go get github.com/kch42/binproto`
+
+## Documentation
+
+Either install the package and use a local godoc server or use [godoc.org](http://godoc.org/github.com/kch42/binproto)
+
+## Protocol definition
+
+The protocol assumes a server and a client. Clients send requests to the server, the server answers with an answer. The server can also send an event message.
+
+The protocol sends units over the connection, a unit is one byte that determines the unit type and a payload that is different for each unit type.
+
+Here are the unit types:
+
+ Number | Name | Payload
+ -------+-----------+--------------------------------------------------
+ 0 | Nil | no Payload
+ 1 | Request | 2 byte request code + another unit
+ 2 | Answer | 2 byte response code + another unit
+ 3 | Event | 2 byte event code + another unit
+ 4 | Bin | 4 byte length + binary data of that length
+ 5 | Number | 8 byte int64
+ 6 | List | more units terminated by the Term unit
+ 7 | TextKVMap | multiple pairs of Bin (with(!) type byte) + any
+ | | type. Terminated by the Term unit
+ 8 | IdKVMap | payload are multiple pairs of UKey + any type.
+ | | Terminated by the Term Unit
+ 9 | UKey | 1 byte
+ 10 | BinStream | multiple pairs of 4 byte(signed) length + binary
+ | | data of that length. Terminated with negative
+ | | length (MSB set)
+ 11 | Term | no Payload
+ 12 | Bool | a single byte interpreted as bool
+ | | (0 = false, true otherwise)
+ 13 | Byte | a single byte
+
+## binprotodebug
+
+binprotodebug is a debugging utility for a binproto-based protocol. It allows you to play the role of a client `-mode client` or can function as a proxy `-mode proxy`. It displays the data in a human readable form.
+ \ No newline at end of file
diff --git a/binproto_test.go b/binproto_test.go
new file mode 100644
index 0000000..1a44a3d
--- /dev/null
+++ b/binproto_test.go
@@ -0,0 +1,262 @@
+package binproto
+
+import (
+ "bytes"
+ "io/ioutil"
+ "testing"
+)
+
+var data = []byte{
+ 0x01, 0x2a, 0x00, // Request(42)
+ 0x08, // IdKVMap
+ 0x09, 0x10, // UKey (16)
+ 0x04, 0x02, 0x00, 0x00, 0x00, 'h', 'i', // Bin(hi)
+ 0x09, 0x01, // UKey(1)
+ 0x0a, // BinStream
+ 0x05, 0x00, 0x00, 0x00, 'h', 'e', 'l', 'l', 'o', // BinStream chunk (hello)
+ 0x02, 0x00, 0x00, 0x00, ',', ' ', // BinStream chunk (, )
+ 0x06, 0x00, 0x00, 0x00, 'w', 'o', 'r', 'l', 'd', '!', // BinStream chunk (world!)
+ 0xff, 0xff, 0xff, 0xff, // Terminating BinStream
+ 0x09, 0x02, // UKey(2)
+ 0x06, // List
+ 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(1)
+ 0x05, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(2)
+ 0x0b, // Term
+ 0x09, 0x03, // UKey(3)
+ 0x07, // TextKVMap
+ 0x04, 0x03, 0x00, 0x00, 0x00, 'f', 'o', 'o', // Bin(foo)
+ 0x04, 0x03, 0x00, 0x00, 0x00, 'b', 'a', 'r', // Bin(bar)
+ 0x0b, // Term
+ 0x09, 0x04, // UKey(4)
+ 0x0c, 0x00, // UBool(false)
+ 0x09, 0x05, // UKey(5)
+ 0x0c, 0x01, // UBool(true)
+ 0x09, 0x06, // UKey(6)
+ 0x0d, 0x0a, // UByte(10)
+ 0x0b} // Term
+
+func chkUnitType(t *testing.T, recv, expected UnitType) {
+ if recv != expected {
+ t.Fatalf("Unit has type %s, %s expected.", recv, expected)
+ }
+}
+
+func readExpect2(t *testing.T, ur UnitReader, expected UnitType) interface{} {
+ data, err := ReadExpect(ur, expected)
+ if err != nil {
+ t.Fatalf("Error from ReadExpect: %s", err)
+ }
+
+ return data
+}
+
+func TestReading(t *testing.T) {
+ r := bytes.NewReader(data)
+ ur := NewSimpleUnitReader(r)
+
+ data := readExpect2(t, ur, UTRequest)
+ if code := data.(uint16); code != 42 {
+ t.Errorf("Request had code %d, not 42.", code)
+ }
+
+ readExpect2(t, ur, UTIdKVMap)
+
+ idkvp, err := ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 16) || (idkvp.ValueType != UTBin) || (!bytes.Equal(idkvp.ValuePayload.([]byte), []byte("hi"))) {
+ t.Errorf("Wrong idkvp content: %v", idkvp)
+ }
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 1) || (idkvp.ValueType != UTBinStream) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+ d, err := ioutil.ReadAll(idkvp.ValuePayload.(*BinstreamReader))
+ if err != nil {
+ t.Fatalf("BinstreamReader failed: %s", err)
+ }
+ if !bytes.Equal(d, []byte("hello, world!")) {
+ t.Errorf("Wrong Binstream data: %v", d)
+ }
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 2) || (idkvp.ValueType != UTList) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+
+ if item := readExpect2(t, ur, UTNumber); item.(int64) != 1 {
+ t.Errorf("Wrong number in list. Want: 1. Got: %d", item.(int64))
+ }
+
+ if item := readExpect2(t, ur, UTNumber); item.(int64) != 2 {
+ t.Errorf("Wrong number in list. Want: 2. Got: %d", item.(int64))
+ }
+
+ readExpect2(t, ur, UTTerm)
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 3) || (idkvp.ValueType != UTTextKVMap) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+
+ textkvp, err := ReadTextKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read TextKVPair: %s", err)
+ }
+ if (textkvp.Key != "foo") || (textkvp.ValueType != UTBin) || (!bytes.Equal(textkvp.ValuePayload.([]byte), []byte("bar"))) {
+ t.Errorf("Wrong textkvp content: %v", textkvp)
+ }
+
+ if _, err = ReadTextKVPair(ur); err != Terminated {
+ t.Fatal("TextKVMap not terminated?")
+ }
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 4) || (idkvp.ValueType != UTBool) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+ if idkvp.ValuePayload.(bool) != false {
+ t.Error("Got true, want false")
+ }
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 5) || (idkvp.ValueType != UTBool) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+ if idkvp.ValuePayload.(bool) != true {
+ t.Error("Got false, want true")
+ }
+
+ idkvp, err = ReadIdKVPair(ur)
+ if err != nil {
+ t.Fatalf("Could not read IdKVPair: %s", err)
+ }
+ if (idkvp.Key != 6) || (idkvp.ValueType != UTByte) {
+ t.Fatalf("Wrong key or value type: %d, %s", idkvp.Key, idkvp.ValueType)
+ }
+ if val := idkvp.ValuePayload.(byte); val != 10 {
+ t.Errorf("Got byte %d, want 10.", val)
+ }
+
+ if _, err = ReadIdKVPair(ur); err != Terminated {
+ t.Fatal("IdKVMap not terminated?")
+ }
+}
+
+func chkerr(t *testing.T, err error, whatfailed string) {
+ if err != nil {
+ t.Fatalf("%s failed: %s", whatfailed, err)
+ }
+}
+
+func TestWriting(t *testing.T) {
+ w := new(bytes.Buffer)
+
+ chkerr(t, InitRequest(w, 42), "InitRequest")
+ chkerr(t, InitIdKVMap(w), "InitIdKVMap")
+
+ chkerr(t, SendUKey(w, 16), "SendUKey")
+ chkerr(t, SendBin(w, []byte("hi")), "SendBin")
+
+ chkerr(t, SendUKey(w, 1), "SendUKey")
+
+ bsw, err := InitBinStream(w)
+ if err != nil {
+ t.Fatalf("Could not init a BinstremWriter: %s", err)
+ }
+ if _, err := bsw.Write([]byte("hello")); err != nil {
+ t.Fatalf("Could not write chunk to bsw: %s", err)
+ }
+ if _, err := bsw.Write([]byte(", ")); err != nil {
+ t.Fatalf("Could not write chunk to bsw: %s", err)
+ }
+ if _, err := bsw.Write([]byte("world!")); err != nil {
+ t.Fatalf("Could not write chunk to bsw: %s", err)
+ }
+ if err := bsw.Close(); err != nil {
+ t.Fatalf("Could not close bsw: %s", err)
+ }
+
+ chkerr(t, SendUKey(w, 2), "SendUKey")
+ chkerr(t, InitList(w), "InitList")
+ chkerr(t, SendNumber(w, 1), "SendNumber")
+ chkerr(t, SendNumber(w, 2), "SendNumber")
+ chkerr(t, SendTerm(w), "SendTerm")
+
+ chkerr(t, SendUKey(w, 3), "SendUKey")
+ chkerr(t, InitTextKVMap(w), "InitTextKVMap")
+ chkerr(t, SendTextKey(w, "foo"), "SendTextKey")
+ chkerr(t, SendBin(w, []byte("bar")), "SendBin")
+
+ chkerr(t, SendTerm(w), "SendTerm")
+
+ chkerr(t, SendUKey(w, 4), "SendUKey")
+ chkerr(t, SendBool(w, false), "SendBool")
+
+ chkerr(t, SendUKey(w, 5), "SendUKey")
+ chkerr(t, SendBool(w, true), "SendBool")
+
+ chkerr(t, SendUKey(w, 6), "SendUKey")
+ chkerr(t, SendByte(w, 10), "SendByte")
+
+ chkerr(t, SendTerm(w), "SendTerm")
+
+ if !bytes.Equal(w.Bytes(), data) {
+ t.Errorf("Wrong data constructed, got: %v", w.Bytes())
+ }
+}
+
+func TestSkipping(t *testing.T) {
+ skipdata := []byte{
+ 0x08, // IdKVMap
+ 0x09, 0x01, // UKey(1)
+ 0x06, // List
+ 0x06, // List
+ 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(1)
+ 0x05, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(2)
+ 0x0b, // Term
+ 0x06, // List
+ 0x05, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(3)
+ 0x05, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Number(4)
+ 0x0b, // Term
+ 0x0b, // Term
+ 0x09, 0x02, // UKey(2)
+ 0x07, // TextKVMap
+ 0x04, 0x03, 0x00, 0x00, 0x00, 'f', 'o', 'o', // Bin(foo)
+ 0x0a, // BinStream
+ 0x05, 0x00, 0x00, 0x00, 'h', 'e', 'l', 'l', 'o', // BinStream chunk (hello)
+ 0x02, 0x00, 0x00, 0x00, ',', ' ', // BinStream chunk (, )
+ 0x06, 0x00, 0x00, 0x00, 'w', 'o', 'r', 'l', 'd', '!', // BinStream chunk (world!)
+ 0xff, 0xff, 0xff, 0xff, // Terminating BinStream
+ 0x0b, // Term
+ 0x0b, // Term
+ 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // Number(8)
+
+ r := bytes.NewReader(skipdata)
+ ur := NewSimpleUnitReader(r)
+
+ if err := SkipNext(ur); err != nil {
+ t.Fatalf("Skipping failed: %s", err)
+ }
+
+ if ut, data, err := ur.ReadUnit(); (err != nil) || (ut != UTNumber) || (data.(int64) != 8) {
+ t.Errorf("ReadUnit returned with unexpected data: %s, %v, %s", ut, data, err)
+ }
+}
diff --git a/binprotodebug/main.go b/binprotodebug/main.go
new file mode 100644
index 0000000..e474b07
--- /dev/null
+++ b/binprotodebug/main.go
@@ -0,0 +1,326 @@
+package main
+
+import (
+ "bufio"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "github.com/kch42/binproto"
+ "io"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+)
+
+var (
+ mode = flag.String("mode", "", "Mode. Either 'client' or 'proxy'")
+ raddr = flag.String("raddr", "", "Address to connect to.")
+ laddr = flag.String("laddr", "[::1]:31337", "Address to listen to (for proxy mode).")
+)
+
+func dedent(s string) string {
+ l := len(s)
+ if l > 0 {
+ s = s[1:]
+ }
+ return s
+}
+
+func displayIncoming(r io.Reader, prefix string) {
+ indent := ""
+ out := func(f string, args ...interface{}) {
+ fmt.Printf("%s%s"+f+"\n", append([]interface{}{prefix, indent}, args...)...)
+ }
+
+ ur := binproto.NewSimpleUnitReader(r)
+
+ for {
+ ut, data, err := ur.ReadUnit()
+ switch err {
+ case nil:
+ case io.EOF:
+ return
+ default:
+ fmt.Fprintf(os.Stderr, "could not read next unit: %s\n", err)
+ os.Exit(1)
+ }
+
+ switch ut {
+ case binproto.UTNil:
+ out("Nil")
+ case binproto.UTRequest:
+ out("Request %d", data.(uint16))
+ case binproto.UTAnswer:
+ out("Answer %d", data.(uint16))
+ case binproto.UTEvent:
+ out("Event %d", data.(uint16))
+ case binproto.UTBin:
+ out("Bin %s", strconv.Quote(string(data.([]byte))))
+ case binproto.UTNumber:
+ out("Num %d", data.(int64))
+ case binproto.UTList:
+ out("List")
+ indent += " "
+ case binproto.UTTextKVMap:
+ out("TextKVMap")
+ indent += " "
+ case binproto.UTIdKVMap:
+ out("IdKVMap")
+ indent += " "
+ case binproto.UTUKey:
+ out("UKey %d", data.(byte))
+ case binproto.UTBinStream:
+ out("Binstream")
+ dumper := hex.Dumper(os.Stdout)
+ if _, err := io.Copy(dumper, data.(*binproto.BinstreamReader)); err != nil {
+ dumper.Close()
+ fmt.Fprintf(os.Stderr, "error while dumping binstream: %s\n", err)
+ os.Exit(1)
+ }
+ if err := dumper.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "error while dumping binstream: %s\n", err)
+ os.Exit(1)
+ }
+ case binproto.UTTerm:
+ out("Term")
+ indent = dedent(indent)
+ }
+ }
+}
+
+func clientUsage() {
+ fmt.Fprint(os.Stderr, `One of:
+Nil
+Request <num>
+Answer <num>
+Event <num>
+Bin <go string>
+Number <num>
+List
+TextKVMap
+IdKVMap
+UKey <num>
+BinStream <file>
+Term
+`)
+}
+
+func getuint(parts []string, bits int) (uint64, bool) {
+ if len(parts) != 2 {
+ clientUsage()
+ return 0, false
+ }
+ n, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 0, 16)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not parse number: %s\n", err)
+ clientUsage()
+ return 0, false
+ }
+ return n, true
+}
+
+func client() int {
+ conn, err := net.Dial("tcp", *raddr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "could not connect to '%s': %s\n", *raddr, err)
+ os.Exit(1)
+ }
+ defer conn.Close()
+
+ go func() {
+ displayIncoming(conn, "")
+ fmt.Fprintln(os.Stderr, "--- Connection closed by remote host")
+ os.Exit(0)
+ }()
+
+ bufin := bufio.NewReader(os.Stdin)
+
+readloop:
+ for {
+ line, err := bufin.ReadString('\n')
+ switch err {
+ case nil:
+ case io.EOF:
+ return 0
+ default:
+ fmt.Fprintf(os.Stderr, "Could not read line: %s", err)
+ return 1
+ }
+ line = line[:len(line)-1]
+
+ parts := strings.SplitN(line, " ", 2)
+
+ switch strings.ToLower(parts[0]) {
+ case "nil":
+ if err := binproto.SendNil(conn); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "request":
+ if n, ok := getuint(parts, 16); ok {
+ if err := binproto.InitRequest(conn, uint16(n)); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ }
+ case "answer":
+ if n, ok := getuint(parts, 16); ok {
+ if err := binproto.InitAnswer(conn, uint16(n)); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ }
+ case "event":
+ if n, ok := getuint(parts, 16); ok {
+ if err := binproto.InitEvent(conn, uint16(n)); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ }
+ case "bin":
+ if len(parts) != 2 {
+ clientUsage()
+ continue readloop
+ }
+ s, err := strconv.Unquote(strings.TrimSpace(parts[1]))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not interpret string: %s\n", err)
+ clientUsage()
+ continue readloop
+ }
+ if err := binproto.SendBin(conn, []byte(s)); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "number":
+ if len(parts) != 2 {
+ clientUsage()
+ continue readloop
+ }
+ n, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 0, 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not parse number: %s\n", err)
+ clientUsage()
+ continue readloop
+ }
+ if err := binproto.SendNumber(conn, n); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "list":
+ if err := binproto.InitList(conn); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "textkvmap":
+ if err := binproto.InitTextKVMap(conn); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "idkvmap":
+ if err := binproto.InitIdKVMap(conn); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ case "ukey":
+ if n, ok := getuint(parts, 8); ok {
+ if err := binproto.SendUKey(conn, byte(n)); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ }
+ case "binstream":
+ if len(parts) != 2 {
+ clientUsage()
+ continue readloop
+ }
+
+ if func(fname string) bool {
+ f, err := os.Open(fname)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not open '%s': %s\n", fname, err)
+ return false
+ }
+ defer f.Close()
+
+ bsw, err := binproto.InitBinStream(conn)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not init binstream: %s\n", err)
+ return true
+ }
+ defer bsw.Close()
+
+ if _, err = io.Copy(bsw, f); err != nil {
+ fmt.Fprintf(os.Stderr, "Could not copy file to binstream: %s\n", err)
+ return true
+ }
+ return false
+ }(strings.TrimSpace(parts[1])) {
+ return 1
+ }
+ case "term":
+ if err := binproto.SendTerm(conn); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ default:
+ clientUsage()
+ }
+ }
+ return 0
+}
+
+func proxy() {
+ listener, err := net.Listen("tcp", *laddr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not listen on '%s': %s\n", *laddr, err)
+ }
+ defer listener.Close()
+
+ connL, err := listener.Accept()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Accept() failed: %s\n", err)
+ }
+ defer connL.Close()
+
+ connR, err := net.Dial("tcp", *raddr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not connect to '%s', %s\n", *raddr, err)
+ }
+ defer connR.Close()
+
+ l2r := io.TeeReader(connL, connR)
+ r2l := io.TeeReader(connR, connL)
+
+ exit := make(chan bool)
+
+ dispInWrap := func(r io.Reader, prefix string, exit chan<- bool, onexit bool) {
+ displayIncoming(r, prefix)
+ exit <- onexit
+ }
+
+ go dispInWrap(l2r, "[l -> r] ", exit, false)
+ go dispInWrap(r2l, "[r -> l] ", exit, true)
+
+ if <-exit {
+ fmt.Fprintln(os.Stderr, "--- Connection closed by remote host")
+ }
+}
+
+func main() {
+ flag.Parse()
+
+ switch *mode {
+ case "client":
+ os.Exit(client())
+ case "proxy":
+ proxy()
+ case "":
+ flag.Usage()
+ os.Exit(1)
+ default:
+ fmt.Fprint(os.Stderr, "Unknown mode: '%s'\n", *mode)
+ os.Exit(1)
+ }
+}
diff --git a/binstream.go b/binstream.go
new file mode 100644
index 0000000..4240517
--- /dev/null
+++ b/binstream.go
@@ -0,0 +1,114 @@
+package binproto
+
+import (
+ "encoding/binary"
+ "errors"
+ "github.com/kch42/kagus"
+ "io"
+ "sync"
+)
+
+// BinstreamReader reads a binary stream from a binproto stream.
+type BinstreamReader struct {
+ r io.Reader
+ err error
+ toread int
+ surMu *sync.Mutex // The mutex of the parent SimpleUnitReader
+}
+
+// Read implements io.Reader.
+func (bsr *BinstreamReader) Read(p []byte) (int, error) {
+ if bsr.err != nil {
+ return 0, bsr.err
+ }
+
+ if bsr.toread == 0 {
+ var _toread int32
+ if err := binary.Read(bsr.r, binary.LittleEndian, &_toread); err != nil {
+ bsr.err = err
+ return 0, err
+ }
+
+ if _toread < 0 {
+ bsr.toread = -1
+ bsr.err = io.EOF
+ bsr.surMu.Unlock() // TODO: Unlock on other conditions?
+ return 0, io.EOF
+ }
+
+ bsr.toread = int(_toread)
+ }
+
+ want := len(p)
+ if bsr.toread < want {
+ want = bsr.toread
+ }
+ n, err := bsr.r.Read(p[:want])
+
+ switch err {
+ case nil:
+ bsr.toread -= n
+ return n, nil
+ case io.EOF:
+ // NOTE: Perhaps we should log this? IDK...
+ bsr.err = errors.New("binstream terminated abnormally")
+ return n, bsr.err
+ }
+
+ bsr.err = err
+ return n, err
+}
+
+// FastForward skips to the end of the stream. Use this, if the data is useless.
+func (bsr *BinstreamReader) FastForward() error {
+ nirvana := kagus.NewNirvanaWriter()
+ _, err := io.Copy(nirvana, bsr)
+ return err
+}
+
+// BinstreamReader writes a binary stream to a binproto stream.
+type BinstreamWriter struct {
+ w io.Writer
+ err error
+}
+
+// Write implements io.Writer.
+func (bsw *BinstreamWriter) Write(p []byte) (int, error) {
+ if bsw.err != nil {
+ return 0, bsw.err
+ }
+
+ l := len(p)
+ if l == 0 {
+ return 0, nil
+ }
+
+ if err := binary.Write(bsw.w, binary.LittleEndian, int32(l)); err != nil {
+ bsw.err = err
+ return 0, err
+ }
+
+ n, err := bsw.w.Write(p)
+ if err != nil {
+ bsw.err = err
+ }
+
+ return n, err
+}
+
+// Close implements io.Closer. You MUST close a stream, so it is terminated properly.
+func (bsw *BinstreamWriter) Close() error {
+ switch bsw.err {
+ case nil:
+ case io.EOF:
+ return nil
+ default:
+ return bsw.err
+ }
+ if err := binary.Write(bsw.w, binary.LittleEndian, int32(-1)); err != nil {
+ return err
+ }
+
+ bsw.err = io.EOF
+ return nil
+}
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..97c8fdc
--- /dev/null
+++ b/common.go
@@ -0,0 +1,68 @@
+// Package binproto provides functions to handle a simple binary protocol.
+package binproto
+
+import (
+ "errors"
+)
+
+type UnitType byte
+
+// Possible UnitType values
+const (
+ UTNil = iota
+ UTRequest
+ UTAnswer
+ UTEvent
+ UTBin
+ UTNumber
+ UTList
+ UTTextKVMap
+ UTIdKVMap
+ UTUKey
+ UTBinStream
+ UTTerm
+ UTBool
+ UTByte
+)
+
+func (ut UnitType) String() string {
+ switch ut {
+ case UTNil:
+ return "UTNil"
+ case UTRequest:
+ return "UTRequest"
+ case UTAnswer:
+ return "UTAnswer"
+ case UTEvent:
+ return "UTEvent"
+ case UTBin:
+ return "UTBin"
+ case UTNumber:
+ return "UTNumber"
+ case UTList:
+ return "UTList"
+ case UTTextKVMap:
+ return "UTTextKVMap"
+ case UTIdKVMap:
+ return "UTIdKVMap"
+ case UTUKey:
+ return "UTUKey"
+ case UTBinStream:
+ return "UTBinStream"
+ case UTTerm:
+ return "UTTerm"
+ case UTBool:
+ return "UTBool"
+ case UTByte:
+ return "UTByte"
+ }
+ return "Unknown unit"
+}
+
+// Errors
+var (
+ UnknownUnit = errors.New("Unknown unit received")
+ UnexpectedUnit = errors.New("Unexpected unit received")
+ Terminated = errors.New("List or KVMap terminated")
+ TooDeeplyNested = errors.New("Received data is too deeply nested to skip")
+)
diff --git a/demux.go b/demux.go
new file mode 100644
index 0000000..632630d
--- /dev/null
+++ b/demux.go
@@ -0,0 +1,75 @@
+package binproto
+
+type urReturn struct {
+ ut UnitType
+ data interface{}
+}
+
+type Demux struct {
+ ur UnitReader
+ events, other chan urReturn
+ err error
+}
+
+func NewDemux(ur UnitReader) (d *Demux) {
+ d = &Demux{
+ ur: ur,
+ events: make(chan urReturn),
+ other: make(chan urReturn),
+ err: nil}
+ go d.demux()
+ return
+}
+
+func (d *Demux) demux() {
+ inEvent := false
+ nesting := 0
+
+ for {
+ ut, data, err := d.ur.ReadUnit()
+ if err != nil {
+ d.err = err
+ close(d.events)
+ close(d.other)
+ return
+ }
+
+ if inEvent {
+ switch ut {
+ case UTList, UTIdKVMap, UTTextKVMap:
+ nesting++
+ case UTTerm:
+ nesting--
+ }
+
+ d.events <- urReturn{ut, data}
+
+ if nesting <= 0 {
+ inEvent = false
+ }
+ } else if ut == UTEvent {
+ d.events <- urReturn{ut, data}
+ inEvent = true
+ nesting = 0
+ } else {
+ d.other <- urReturn{ut, data}
+ }
+ }
+}
+
+type PartUnitReader struct {
+ ch chan urReturn
+ d *Demux
+}
+
+func (d *Demux) Events() *PartUnitReader { return &PartUnitReader{d.events, d} }
+func (d *Demux) Other() *PartUnitReader { return &PartUnitReader{d.other, d} }
+
+func (pur *PartUnitReader) ReadUnit() (UnitType, interface{}, error) {
+ urr, ok := <-pur.ch
+ if !ok {
+ return 0, nil, pur.d.err
+ }
+
+ return urr.ut, urr.data, nil
+}
diff --git a/demux_test.go b/demux_test.go
new file mode 100644
index 0000000..f7cdfac
--- /dev/null
+++ b/demux_test.go
@@ -0,0 +1,75 @@
+package binproto
+
+import (
+ "bytes"
+ "io"
+ "testing"
+)
+
+func TestDemux(t *testing.T) {
+ r := bytes.NewReader([]byte{
+ 0x02, 0x01, 0x00, // Answer(1)
+ 0x06, // List
+ 0x06, // List
+ 0x0d, 0x01, // Byte(1)
+ 0x0d, 0x02, // Byte(2)
+ 0x0b, // Term
+ 0x06, // List
+ 0x0d, 0x03, // Byte(3)
+ 0x0d, 0x04, // Byte(4)
+ 0x0b, // Term
+ 0x0b, // Term
+ 0x03, 0x02, 0x00, // Event(2)
+ 0x0d, 0x2a, // Byte(42)
+ 0x02, 0x03, 0x00, // Event(3)
+ 0x00}) // Nil
+
+ ur := NewSimpleUnitReader(r)
+ demux := NewDemux(ur)
+
+ events := demux.Events()
+ other := demux.Other()
+
+ _code, err := ReadExpect(other, UTAnswer)
+ if err != nil {
+ t.Fatalf("Could not read an Answer from other: %s", err)
+ }
+ if code := _code.(uint16); code != 1 {
+ t.Errorf("Expected code 1, got %d.", code)
+ }
+
+ if err := SkipNext(other); err != nil {
+ t.Fatalf("Error while skipping data: %s", err)
+ }
+
+ _code, err = ReadExpect(events, UTEvent)
+ if err != nil {
+ t.Fatalf("Could not read an event: %s", err)
+ }
+ if code := _code.(uint16); code != 2 {
+ t.Errorf("Expected code 2, got %d.", code)
+ }
+ _b, err := ReadExpect(events, UTByte)
+ if err != nil {
+ t.Fatalf("Could not read event data: %s", err)
+ }
+ if b := _b.(byte); b != 42 {
+ t.Errorf("Unexpected event data. Want 42, got: %d", b)
+ }
+
+ _code, err = ReadExpect(other, UTAnswer)
+ if err != nil {
+ t.Fatalf("Could not read an Answer from other (2): %s", err)
+ }
+ if code := _code.(uint16); code != 3 {
+ t.Errorf("Expected code 3, got %d.", code)
+ }
+
+ if err := SkipNext(other); err != nil {
+ t.Fatalf("Error while skipping data (2): %s", err)
+ }
+
+ if _, _, err := other.ReadUnit(); err != io.EOF {
+ t.Errorf("Expected io.EOF, got: %s", err)
+ }
+}
diff --git a/idkvmapscanner.go b/idkvmapscanner.go
new file mode 100644
index 0000000..c51f041
--- /dev/null
+++ b/idkvmapscanner.go
@@ -0,0 +1,187 @@
+package binproto
+
+import (
+ "errors"
+ "fmt"
+ "io"
+)
+
+// UKeyGetter defines what to do on a UKey. Used by ScanIdKVMap.
+type UKeyGetter struct {
+ Type UnitType // Which type must the unit have?
+ Optional bool // Is this key optional?
+ Action GetterAction // Performing this action
+ Captured *bool // Pointer to variable that should be set to true, if key was found. Can be nil, if this information is not needed.
+}
+
+// Errors of ScanIdKVMap.
+var (
+ KeyMissing = errors.New("Mandatory key is missing")
+ UnknownKey = errors.New("Unknown key found")
+ UnexpectedTypeForKey = errors.New("Unexpected unit type for key")
+)
+
+// GetterAction specifies how an Action in UKeyGetter has to look like. Any error will be passed to the caller.
+//
+// If fatal is true, ScanIdKVMap will not try to cleanly skip the IdKVMap.
+// If fatal is false, the action MUST handle all Units in a clear manner, so ScanIdKVMap can continue operating on a valid stream.
+//
+// fatal will only be checked, if err != nil.
+//
+// Actions for keys that are present will be executed asap, so you can e.g. NOT rely that all non-optional keys were captured.
+type GetterAction func(interface{}, UnitReader) (err error, fatal bool)
+
+// ScanIdKVMap scans a IdKVMap for keys, tests some properties of the values and will trigger a user given action.
+// It also takes care of optional/mandatory keys and will generally make reading the often used IdKVMap less painful.
+//
+// The input stream must be positioned after the opening UTIdKVMap.
+//
+// If a key is missing KeyMissing is returned.
+// If a key had a value with the wrong type, UnexpectedTypeForKey is returned.
+// If a key is unknown AND failOnUnknown is true, UnknownKey is returned.
+//
+// Other errors either indicate an error in the stream OR an action returned that error.
+// Since actions should only return errors when something is really wrong, you should not process the stream any further.
+func ScanIdKVMap(ur UnitReader, getters map[byte]UKeyGetter, failOnUnknown bool) (outerr error) {
+ seen := make(map[byte]bool)
+
+ skipAll := false
+ for {
+ ut, data, err := ur.ReadUnit()
+ if err != nil {
+ return err
+ }
+
+ if ut == UTTerm {
+ break
+ }
+
+ if skipAll {
+ if err := SkipUnit(ur, ut, data); err != nil {
+ return fmt.Errorf("Error while skipping: %s. Previous outerr was: %s", err, outerr)
+ }
+ continue
+ }
+
+ if ut != UTUKey {
+ return fmt.Errorf("Found Unit of type %s (%d) in IdKVMap, expected UTUKey.", ut, ut)
+ }
+
+ key := data.(byte)
+
+ ut, data, err = ur.ReadUnit()
+ if err != nil {
+ return err
+ }
+
+ getter, ok := getters[key]
+ if !ok {
+ if failOnUnknown {
+ outerr = UnknownKey
+ if err := SkipUnit(ur, ut, data); err != nil {
+ return err
+ }
+ skipAll = true
+ } else {
+ if err := SkipUnit(ur, ut, data); err != nil {
+ return err
+ }
+ }
+ continue
+ }
+
+ if ut != getter.Type {
+ if err := SkipUnit(ur, ut, data); err != nil {
+ return err
+ }
+ outerr = UnexpectedTypeForKey
+ skipAll = true
+ continue
+ }
+
+ err, fatal := getter.Action(data, ur)
+ if err != nil {
+ if fatal {
+ return err
+ }
+
+ outerr = err
+ skipAll = true
+ continue
+ }
+
+ seen[key] = true
+ if getter.Captured != nil {
+ *(getter.Captured) = true
+ }
+ }
+
+ if skipAll {
+ return
+ }
+
+ for key, getter := range getters {
+ if !getter.Optional {
+ if !seen[key] {
+ return KeyMissing
+ }
+ }
+ }
+
+ return nil
+}
+
+// ActionSkip builds an action to skip this unit (useful, if you only want to know that a key exists, or for debugging).
+func ActionSkip(ut UnitType) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ return SkipUnit(ur, ut, data), true // Since second return value will only be inspected, if first is != nil, this is okay.
+ }
+}
+
+// ActionStoreNumber builds an action for storing a number.
+func ActionStoreNumber(n *int64) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ *n = data.(int64)
+ return nil, false
+ }
+}
+
+// ActionStoreBin builds an action for storing binary data.
+func ActionStoreBin(b *[]byte) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ *b = data.([]byte)
+ return nil, false
+ }
+}
+
+// ActionStoreBool builds an action for storing a boolean value.
+func ActionStoreBool(b *bool) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ *b = data.(bool)
+ return nil, false
+ }
+}
+
+// ActionStoreByte builds an action for storing a byte.
+func ActionStoreByte(b *byte) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ *b = data.(byte)
+ return nil, false
+ }
+}
+
+// ActionCopyBinStream builds an action that will copy the content of a BinStream to a writer.
+func ActionCopyBinStream(w io.Writer) GetterAction {
+ return func(data interface{}, ur UnitReader) (error, bool) {
+ bsr := data.(*BinstreamReader)
+ _, err := io.Copy(w, bsr)
+ if err != nil {
+ // Try to skip the rest of the binstream in case the error resulted from w.
+ if err := bsr.FastForward(); err != nil {
+ return err, true // This is a fatal error, since the stream is now corrupted.
+ }
+ return err, false
+ }
+ return nil, false
+ }
+}
diff --git a/idkvmapscanner_test.go b/idkvmapscanner_test.go
new file mode 100644
index 0000000..41bcb80
--- /dev/null
+++ b/idkvmapscanner_test.go
@@ -0,0 +1,132 @@
+package binproto
+
+import (
+ "bytes"
+ "io"
+ "testing"
+)
+
+func TestRegularIdKVMap(t *testing.T) {
+ // We assume that we are already in the map
+ testdata := []byte{
+ 0x09, 0x01, // UKey(1)
+ 0x05, 0x2a, 0, 0, 0, 0, 0, 0, 0, // Number(42)
+ 0x09, 0x02, // UKey(2)
+ 0x0c, 0x00, // Bool(false)
+ 0x09, 0x03, // UKey(3)
+ 0x06, // List, for testing skipping unknown fields
+ 0x05, 0x01, 0, 0, 0, 0, 0, 0, 0, // Number(1)
+ 0x05, 0x02, 0, 0, 0, 0, 0, 0, 0, // Number(2)
+ 0x05, 0x03, 0, 0, 0, 0, 0, 0, 0, // Number(3)
+ 0x0b, // Term
+ 0x09, 0x04, // UKey(4)
+ 0x04, 0x02, 0, 0, 0, 'h', 'i', // Bin("hi")
+ 0x0b} // Term
+
+ r := bytes.NewReader(testdata)
+ ur := NewSimpleUnitReader(r)
+
+ var n1 int64
+ var b2 bool
+ var bs4 []byte
+
+ var seen1, seen2, seen4, seen5 bool
+
+ err := ScanIdKVMap(ur, map[byte]UKeyGetter{
+ 1: {UTNumber, false, ActionStoreNumber(&n1), &seen1},
+ 2: {UTBool, false, ActionStoreBool(&b2), &seen2},
+ 4: {UTBin, false, ActionStoreBin(&bs4), &seen4},
+ 5: {UTNil, true, ActionSkip(UTNil), &seen5}}, false)
+
+ if err != nil {
+ t.Errorf("Did not expect error, got: %s", err)
+ }
+
+ if n1 != 42 {
+ t.Errorf("n1 wrong, got %d, want 42", n1)
+ }
+
+ if b2 != false {
+ t.Error("b2 wrong, want false")
+ }
+
+ if !bytes.Equal(bs4, []byte("hi")) {
+ t.Errorf("bs4 wrong: %v", bs4)
+ }
+
+ if !(seen1 && seen2 && seen4 && (!seen5)) {
+ t.Errorf("Unexpected values for seen* vars: %v %v %v %v", seen1, seen2, seen4, seen5)
+ }
+
+ if b, err := r.ReadByte(); err != io.EOF {
+ t.Errorf("Expected EOF for reader r, got %x, %v", b, err)
+ }
+}
+
+func TestFailOnUnknown(t *testing.T) {
+ // We assume that we are already in the map
+ testdata := []byte{
+ 0x09, 0x01, // UKey(1)
+ 0x05, 0x2a, 0, 0, 0, 0, 0, 0, 0, // Number(42)
+ 0x09, 0x02, // UKey(2)
+ 0x0c, 0x00, // Bool(false)
+ 0x09, 0x03, // UKey(3)
+ 0x06, // List, for testing skipping unknown fields
+ 0x05, 0x01, 0, 0, 0, 0, 0, 0, 0, // Number(1)
+ 0x05, 0x02, 0, 0, 0, 0, 0, 0, 0, // Number(2)
+ 0x05, 0x03, 0, 0, 0, 0, 0, 0, 0, // Number(3)
+ 0x0b, // Term
+ 0x09, 0x04, // UKey(4)
+ 0x04, 0x02, 0, 0, 0, 'h', 'i', // Bin("hi")
+ 0x0b} // Term
+
+ r := bytes.NewReader(testdata)
+ ur := NewSimpleUnitReader(r)
+
+ err := ScanIdKVMap(ur, map[byte]UKeyGetter{
+ 1: {UTNumber, false, ActionSkip(UTNumber), nil},
+ 2: {UTBool, false, ActionSkip(UTBool), nil},
+ 4: {UTBin, false, ActionSkip(UTBin), nil},
+ 5: {UTNil, true, ActionSkip(UTNil), nil}}, true)
+
+ if err != UnknownKey {
+ t.Errorf("Got wrong error: %s", err)
+ }
+
+ if b, err := r.ReadByte(); err != io.EOF {
+ t.Errorf("Expected EOF for reader r, got %x, %v", b, err)
+ }
+}
+
+func TestMissingMandatory(t *testing.T) {
+ // We assume that we are already in the map
+ testdata := []byte{
+ 0x09, 0x01, // UKey(1)
+ 0x05, 0x2a, 0, 0, 0, 0, 0, 0, 0, // Number(42)
+ 0x09, 0x03, // UKey(3)
+ 0x06, // List, for testing skipping unknown fields
+ 0x05, 0x01, 0, 0, 0, 0, 0, 0, 0, // Number(1)
+ 0x05, 0x02, 0, 0, 0, 0, 0, 0, 0, // Number(2)
+ 0x05, 0x03, 0, 0, 0, 0, 0, 0, 0, // Number(3)
+ 0x0b, // Term
+ 0x09, 0x04, // UKey(4)
+ 0x04, 0x02, 0, 0, 0, 'h', 'i', // Bin("hi")
+ 0x0b} // Term
+
+ r := bytes.NewReader(testdata)
+ ur := NewSimpleUnitReader(r)
+
+ err := ScanIdKVMap(ur, map[byte]UKeyGetter{
+ 1: {UTNumber, false, ActionSkip(UTNumber), nil},
+ 2: {UTBool, false, ActionSkip(UTBool), nil},
+ 4: {UTBin, false, ActionSkip(UTBin), nil},
+ 5: {UTNil, true, ActionSkip(UTNil), nil}}, false)
+
+ if err != KeyMissing {
+ t.Errorf("Got wrong error: %s", err)
+ }
+
+ if b, err := r.ReadByte(); err != io.EOF {
+ t.Errorf("Expected EOF for reader r, got %x, %v", b, err)
+ }
+}
diff --git a/recv.go b/recv.go
new file mode 100644
index 0000000..1e374f9
--- /dev/null
+++ b/recv.go
@@ -0,0 +1,256 @@
+package binproto
+
+import (
+ "encoding/binary"
+ "github.com/kch42/kagus"
+ "io"
+ "sync"
+)
+
+// UnitReader is an interface with the ReadUnit function, which ist the basic reading function of the binproto.
+// ReadUnit reads the next binproto unit from the reader. The second output has a different meaning for each unit type:
+//
+// UTRequest, UTAnswer, UTEvent - uint16
+// UTBin - []byte
+// UTNumber - int64
+// UTUKey, UTByte - byte
+// UTBinStream - *BinStreamReader
+// UTBool - bool
+//
+// A UnitReader implementation should usually wrap the SimpleUnitReader implementation.
+type UnitReader interface {
+ ReadUnit() (UnitType, interface{}, error)
+}
+
+// SimpleUnitReader is a UnitReader implementation that gets its data from an io.Reader.
+type SimpleUnitReader struct {
+ r io.Reader
+ mu *sync.Mutex
+}
+
+func NewSimpleUnitReader(r io.Reader) *SimpleUnitReader {
+ return &SimpleUnitReader{
+ r: r,
+ mu: new(sync.Mutex)}
+}
+
+func (sur *SimpleUnitReader) ReadUnit() (UnitType, interface{}, error) {
+ r := sur.r
+
+ sur.mu.Lock()
+ doUnlock := true
+ defer func() {
+ if doUnlock {
+ sur.mu.Unlock()
+ }
+ }()
+
+ _ut, err := kagus.ReadByte(r)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ ut := UnitType(_ut)
+ switch ut {
+ case UTNil:
+ return ut, nil, nil
+ case UTRequest, UTAnswer, UTEvent:
+ var code uint16
+ if err := binary.Read(r, binary.LittleEndian, &code); err != nil {
+ return ut, nil, err
+ }
+ return ut, code, nil
+ case UTBin:
+ var l uint32
+ if err := binary.Read(r, binary.LittleEndian, &l); err != nil {
+ return ut, nil, err
+ }
+ buf := make([]byte, l)
+ _, err := io.ReadFull(r, buf)
+ if err != nil {
+ return ut, nil, err
+ }
+ return ut, buf, nil
+ case UTNumber:
+ var n int64
+ if err := binary.Read(r, binary.LittleEndian, &n); err != nil {
+ return ut, nil, err
+ }
+ return ut, n, nil
+ case UTList, UTTextKVMap, UTIdKVMap:
+ return ut, nil, nil
+ case UTUKey, UTByte:
+ k, err := kagus.ReadByte(r)
+ return ut, k, err
+ case UTBinStream:
+ doUnlock = false
+ return ut, &BinstreamReader{r: r, surMu: sur.mu}, nil
+ case UTTerm:
+ return ut, nil, nil
+ case UTBool:
+ _b, err := kagus.ReadByte(r)
+ b := true
+ if _b == 0 {
+ b = false
+ }
+ return ut, b, err
+ }
+
+ return ut, nil, UnknownUnit
+}
+
+// IdKVPair will be returned from ReadIdKVPair. ValueType and ValuePayload are the first two outputs of ReadUnit for the value.
+type IdKVPair struct {
+ Key uint8
+ ValueType UnitType
+ ValuePayload interface{}
+}
+
+// ReadIdKVPair reads a UKey + any unit pair. err will be Terminated, if this was the last KVPair.
+func ReadIdKVPair(ur UnitReader) (kvp IdKVPair, err error) {
+ var ut UnitType
+ var data interface{}
+ ut, data, err = ur.ReadUnit()
+ if err != nil {
+ return
+ }
+
+ switch ut {
+ case UTUKey:
+ kvp.Key = data.(byte)
+ case UTTerm:
+ err = Terminated
+ return
+ default:
+ err = UnexpectedUnit
+ return
+ }
+
+ kvp.ValueType, kvp.ValuePayload, err = ur.ReadUnit()
+ return
+}
+
+// TextKVPair will be returned from ReadTextKVPair. ValueType and ValuePayload are the first two outputs of ReadUnit for the value.
+type TextKVPair struct {
+ Key string
+ ValueType UnitType
+ ValuePayload interface{}
+}
+
+// ReadTextKVPair reads a Bin(as string) + any unit pair. err will be Terminated, if this was the last KVPair.
+func ReadTextKVPair(ur UnitReader) (kvp TextKVPair, err error) {
+ var ut UnitType
+ var data interface{}
+ ut, data, err = ur.ReadUnit()
+ if err != nil {
+ return
+ }
+
+ switch ut {
+ case UTBin:
+ kvp.Key = string(data.([]byte))
+ case UTTerm:
+ err = Terminated
+ return
+ default:
+ err = UnexpectedUnit
+ return
+ }
+
+ kvp.ValueType, kvp.ValuePayload, err = ur.ReadUnit()
+ return
+}
+
+const maxSkipDepth = 16
+
+func skipUnit(ur UnitReader, ut UnitType, data interface{}, revDepth int) error {
+ if revDepth == 0 {
+ return TooDeeplyNested
+ }
+
+ switch ut {
+ case UTNil, UTRequest, UTAnswer, UTEvent, UTBin, UTNumber, UTUKey, UTTerm, UTBool, UTByte:
+ return nil
+ case UTList:
+ for {
+ nUt, nData, nErr := ur.ReadUnit()
+ if nErr != nil {
+ return nErr
+ }
+
+ if nUt == UTTerm {
+ return nil
+ }
+
+ if err := skipUnit(ur, nUt, nData, revDepth-1); err != nil {
+ return err
+ }
+ }
+ case UTTextKVMap:
+ for {
+ switch kvp, err := ReadTextKVPair(ur); err {
+ case nil:
+ if err := skipUnit(ur, kvp.ValueType, kvp.ValuePayload, revDepth-1); err != nil {
+ return err
+ }
+ case Terminated:
+ return nil
+ default:
+ return err
+ }
+ }
+ case UTIdKVMap:
+ for {
+ switch kvp, err := ReadIdKVPair(ur); err {
+ case nil:
+ if err := skipUnit(ur, kvp.ValueType, kvp.ValuePayload, revDepth-1); err != nil {
+ return err
+ }
+ case Terminated:
+ return nil
+ default:
+ return err
+ }
+ }
+ case UTBinStream:
+ bsr := data.(*BinstreamReader)
+ return bsr.FastForward()
+ }
+
+ return UnexpectedUnit
+}
+
+// SkipUnit skips the current unit recursively. ut and data are the first two outputs of ReadUnit.
+// If the structure is nested too deeply, this function will abort with TooDeeplyNested.
+func SkipUnit(ur UnitReader, ut UnitType, data interface{}) error {
+ return skipUnit(ur, ut, data, maxSkipDepth)
+}
+
+// SkipNext is ReadNext + SkipUnit.
+func SkipNext(ur UnitReader) error {
+ ut, data, err := ur.ReadUnit()
+ if err != nil {
+ return err
+ }
+ return SkipUnit(ur, ut, data)
+}
+
+// ReadExpect reads a unit and tests, if the type is te expected type.
+// If not, UnexpectedUnit is returned and the read unit will be skipped
+// (if this fails, the reason for that is returned instead of UnexpectedError).
+func ReadExpect(ur UnitReader, expected UnitType) (interface{}, error) {
+ ut, data, err := ur.ReadUnit()
+ if err != nil {
+ return nil, err
+ }
+
+ if ut == expected {
+ return data, nil
+ }
+
+ if err := SkipUnit(ur, ut, data); err != nil {
+ return nil, err
+ }
+
+ return nil, UnexpectedUnit
+}
diff --git a/send.go b/send.go
new file mode 100644
index 0000000..bcd956e
--- /dev/null
+++ b/send.go
@@ -0,0 +1,97 @@
+package binproto
+
+import (
+ "encoding/binary"
+ "io"
+)
+
+func SendNil(w io.Writer) error {
+ _, err := w.Write([]byte{UTNil})
+ return err
+}
+
+func sendRAE(w io.Writer, what UnitType, code uint16) error {
+ if _, err := w.Write([]byte{byte(what)}); err != nil {
+ return err
+ }
+
+ return binary.Write(w, binary.LittleEndian, code)
+}
+
+func InitRequest(w io.Writer, code uint16) error { return sendRAE(w, UTRequest, code) }
+func InitAnswer(w io.Writer, code uint16) error { return sendRAE(w, UTAnswer, code) }
+func InitEvent(w io.Writer, code uint16) error { return sendRAE(w, UTEvent, code) }
+
+func SendBin(w io.Writer, bindata []byte) error {
+ if _, err := w.Write([]byte{UTBin}); err != nil {
+ return err
+ }
+
+ if err := binary.Write(w, binary.LittleEndian, uint32(len(bindata))); err != nil {
+ return err
+ }
+
+ _, err := w.Write(bindata)
+ return err
+}
+
+func SendNumber(w io.Writer, n int64) error {
+ if _, err := w.Write([]byte{UTNumber}); err != nil {
+ return err
+ }
+
+ return binary.Write(w, binary.LittleEndian, n)
+}
+
+func InitList(w io.Writer) error {
+ _, err := w.Write([]byte{UTList})
+ return err
+}
+
+func InitTextKVMap(w io.Writer) error {
+ _, err := w.Write([]byte{UTTextKVMap})
+ return err
+}
+
+func InitIdKVMap(w io.Writer) error {
+ _, err := w.Write([]byte{UTIdKVMap})
+ return err
+}
+
+func sendTypedByte(w io.Writer, t UnitType, b byte) error {
+ _, err := w.Write([]byte{byte(t), b})
+ return err
+}
+
+func SendUKey(w io.Writer, key byte) error {
+ return sendTypedByte(w, UTUKey, key)
+}
+
+func SendBool(w io.Writer, b bool) error {
+ if b {
+ return sendTypedByte(w, UTBool, 1)
+ }
+ return sendTypedByte(w, UTBool, 0)
+}
+
+func SendByte(w io.Writer, b byte) error {
+ return sendTypedByte(w, UTByte, b)
+}
+
+func SendTextKey(w io.Writer, key string) error {
+ return SendBin(w, []byte(key))
+}
+
+func SendTerm(w io.Writer) error {
+ _, err := w.Write([]byte{UTTerm})
+ return err
+}
+
+func InitBinStream(w io.Writer) (*BinstreamWriter, error) {
+ _, err := w.Write([]byte{UTBinStream})
+ if err != nil {
+ return nil, err
+ }
+
+ return &BinstreamWriter{w: w}, nil
+}