diff --git a/.githooks/pre-commit/01-docgen b/.githooks/pre-commit/01-docgen new file mode 100755 index 0000000..427d9a6 --- /dev/null +++ b/.githooks/pre-commit/01-docgen @@ -0,0 +1,19 @@ +#!/bin/bash + +docsdir="${PWD}" + +if ! command -v asciidoctor &> /dev/null; +then + exit 0 +fi + +mkdir -p "${docsdir}" + +for f in $(find . -maxdepth 1 -type f -iname "*.adoc"); do + filename=$(basename -- "${f}") + nosuffix="${filename%.*}" + + asciidoctor -o "${docsdir}/${nosuffix}.html" "${f}" + git add "${docsdir}/${nosuffix}.html" +done +echo "Regenerated docs" diff --git a/.gitignore b/.gitignore index adf8f72..5f32697 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,24 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# +*.7z +*.bak +*.deb +*.jar +*.rar +*.run +*.sig +*.tar +*.tar.bz2 +*.tar.gz +*.tar.xz +*.tbz +*.tbz2 +*.tgz +*.txz +*.zip +.*.swp +.editix +.idea/ + +# https://github.com/github/gitignore/blob/master/Go.gitignore # Binaries for programs and plugins *.exe *.exe~ @@ -17,7 +34,3 @@ # Dependency directories (remove the comment below to include it) # vendor/ - -# Go workspace file -go.work - diff --git a/LICENSE b/LICENSE index c3f599f..fe78d1d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2024 r00t2. +Copyright (c) 2024 Brent Saner (r00t^2). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..da5392d --- /dev/null +++ b/README.adoc @@ -0,0 +1,59 @@ +//// + Go WireProto API Documentation © 2024 by Brent Saner is licensed under Creative Commons Attribution-ShareAlike 4.0 International. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/ +//// + += Go WireProto API Documentation +Brent Saner +Last rendered {localdatetime} +:doctype: book +:docinfo: shared +:data-uri: +:imagesdir: images +:sectlinks: +:sectnums: +:sectnumlevels: 7 +:toc: preamble +:toc2: left +:idprefix: +:toclevels: 7 +:source-highlighter: rouge +:docinfo: shared +:this_protover: 1 +:this_protover_hex: 0x00000001 + +[id="ref"] +== Reference +In addition to the documentation found in this document and https://wireproto.io/[the specification^], library usage documentation can be found at https://pkg.go.dev/go.pkg.dev/r00t2.io/WireProto[the Golang module documentation page^]: + +++++ + + Go Reference + +++++ + +[id="lic"] +== License +This library is licensed for use, inclusion, and distribution under the https://opensource.org/license/bsd-3-clause["3-Clause BSD" license^]. + +.Full License +[%collapsible] +==== +[source,plain] +---- +include::LICENSE[] +---- +==== + +[id="todo"] +== TODO +The following are a wishlist or things planned that may come in later versions. + +* More clear errors +** Currently during e.g. `UnmarshalBinary` calls, just an `io.EOF` will be returned if the buffer is exhausted early. This may be able to be a little more context-helpful by using the `Err*` errors. +* Confirmation of read/write sizes in buffers +** We know the sizes they *should* be, there's no reason to not confirm it. +* Goroutines +** This of course won't work for serializing and keeping *order* of children (e.g. RG => Record); that'd still need to be ordered, but it will allow for parallel parsing *of* those children. Should benchmark, though; it may not be worth it. +* `context.Context` support for `Read*` and `Write*` funcs +** This is a relatively low priority as the passed `net.Conn` will likely return an error if its own context is canceled. This can be handled in the caller downstream. diff --git a/README.html b/README.html new file mode 100644 index 0000000..29a0542 --- /dev/null +++ b/README.html @@ -0,0 +1,669 @@ + + + + + + + + +Go WireProto API Documentation + + + + + + +
+
+

1. Reference

+
+
+

In addition to the documentation found in this document and the specification, library usage documentation can be found at the Golang module documentation page:

+
+ + Go Reference + +
+
+
+

2. License

+
+
+

This library is licensed for use, inclusion, and distribution under the "3-Clause BSD" license.

+
+
+Full License +
+
+
+
Copyright (c) 2024 Brent Saner (r00t^2). 
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+
+
+
+
+

3. TODO

+
+
+

The following are a wishlist or things planned that may come in later versions.

+
+
+
    +
  • +

    More clear errors

    +
    +
      +
    • +

      Currently during e.g. UnmarshalBinary calls, just an io.EOF will be returned if the buffer is exhausted early. This may be able to be a little more context-helpful by using the Err* errors.

      +
    • +
    +
    +
  • +
  • +

    Confirmation of read/write sizes in buffers

    +
    +
      +
    • +

      We know the sizes they should be, there’s no reason to not confirm it.

      +
    • +
    +
    +
  • +
  • +

    Goroutines

    +
    +
      +
    • +

      This of course won’t work for serializing and keeping order of children (e.g. RG ⇒ Record); that’d still need to be ordered, but it will allow for parallel parsing of those children. Should benchmark, though; it may not be worth it.

      +
    • +
    +
    +
  • +
  • +

    context.Context support for Read* and Write* funcs

    +
    +
      +
    • +

      This is a relatively low priority as the passed net.Conn will likely return an error if its own context is canceled. This can be handled in the caller downstream.

      +
    • +
    +
    +
  • +
+
+
+
+
+ + + \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 0260310..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# go_wireproto_sha1 - -A very efficient and flexible message protocol designed for close-to-the-wire transactions. \ No newline at end of file diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..262ed15 --- /dev/null +++ b/consts.go @@ -0,0 +1,99 @@ +package wireproto + +import ( + `encoding/binary` +) + +var ( + // IndentChars is used when rendering a Model; it indicates the leading indent. + IndentChars string = IndentDefault + // SeparatorChars is used when rendering a Model; it indicates the separation between the value and the comment. + SeparatorChars string = SeparatorDefault +) + +const ( + IndentDefault string = "\t" + SeparatorDefault string = " " + maxByteLine int = 12 // split if a value is more than this number of bytes.. +) + +const ( + indentR uint = iota + indentRG + indentRec + indentKvp +) +const indentOrigRec uint = 2 + +const ( + // ProtoVersion specifies the protocol version for the specification of a Message. + ProtoVersion uint32 = 1 +) + +const ( + // PackedNumSize is the size (length of bytes) of a packed unsigned integer. + PackedNumSize int = 4 // They're all uint32's. + // CksumPackedSize is the size (length of bytes) of the checksum algorithm used. + CksumPackedSize int = 4 // CRC32 is a big-endian uint32, but if we use a different algo we need to change this. +) + +// See https://square-r00t.net/ascii.html for further details. +const ( + AsciiNUL uint8 = iota // 0x00 + AsciiSOH // 0x01 + AsciiSTX // 0x02 + AsciiETX // 0x03 + AsciiEOT // 0x04 + AsciiENQ // 0x05 + AsciiACK // 0x06 + AsciiBEL // 0x07 + AsciiBS // 0x08 + AsciiHT // 0x09 + AsciiLF // 0x0a + AsciiVT // 0x0b + AsciiFF // 0x0c + AsciiCR // 0x0d + AsciiSO // 0x0e + AsciiSI // 0x0f + AsciiDLE // 0x10 + AsciiDC1 // 0x11 + AsciiDC2 // 0x12 + AsciiDC3 // 0x13 + AsciiDC4 // 0x14 + AsciiNAK // 0x15 + AsciiSYN // 0x16 + AsciiETB // 0x17 + AsciiCAN // 0x18 + AsciiEM // 0x19 + AsciiSUB // 0x1a + AsciiESC // 0x1b + AsciiFS // 0x1c + AsciiGS // 0x1d + AsciiRS // 0x1e + AsciiUS // 0x1f +) + +const ( + RespStatusByteOK uint8 = AsciiACK + RespStatusByteErr = AsciiNAK +) + +var ( + byteOrder binary.ByteOrder = binary.BigEndian +) + +var ( + respStatusOK []byte = []byte{RespStatusByteOK} + respStatusErr []byte = []byte{RespStatusByteErr} + // hdrCKSUM *must* be *exactly* as long as hdrMSGSTART and *must not* match hdrMSGSTART. + hdrCKSUM []byte = []byte{AsciiESC} + hdrMSGSTART []byte = []byte{AsciiSOH} + hdrBODYSTART []byte = []byte{AsciiSTX} + hdrBODYEND []byte = []byte{AsciiETX} + hdrMSGEND []byte = []byte{AsciiEOT} + endSeq []byte = []byte{AsciiETX, AsciiEOT} +) + +const ( + WriteChunkSize int = 1024 +) diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..119fe8d --- /dev/null +++ b/doc.go @@ -0,0 +1,11 @@ +/* + +# WireProto - A Fast, Efficient, and Safe Bytes-on-the-Wire Message Format + +For more details and the actual specification, visit [WireProto]. + +[WireProto]: https://wireproto.io/ + + +*/ +package wireproto diff --git a/errs.go b/errs.go new file mode 100644 index 0000000..7878c6e --- /dev/null +++ b/errs.go @@ -0,0 +1,14 @@ +package wireproto + +import ( + "errors" +) + +var ( + ErrBadCksum error = errors.New("checksums do not match") + ErrBadHdr error = errors.New("a header mismatch occurred") + ErrBadNumRecords error = errors.New("the number of child objects does not match the count") + ErrCustomUnmarshal error = errors.New("an error occurred during custom unmarshaling") + ErrInvalidNums error = errors.New("invalid number of results") + ErrNotEnoughFields error = errors.New("not enough fields/parameters in request") +) diff --git a/funcs.go b/funcs.go new file mode 100644 index 0000000..a0018a7 --- /dev/null +++ b/funcs.go @@ -0,0 +1,604 @@ +package wireproto + +import ( + `bytes` + `encoding/binary` + `fmt` + `io` + `net` + `slices` +) + +// GetByteOrder returns the byte order ("endianness") used by the library. +func GetByteOrder() (order binary.ByteOrder) { + + order = byteOrder + + return +} + +// GetHdrs returns a map of a header name/status indicator and its byte sequence. +func GetHdrs() (hdrs map[string][]byte) { + + hdrs = map[string][]byte{ + "OK": respStatusOK, + "ERR": respStatusErr, + "CKSUM": hdrCKSUM, + "MSGSTART": hdrMSGSTART, + "BODYSTART": hdrBODYSTART, + "BODYEND": hdrBODYEND, + "MSGEND": hdrMSGEND, + } + + return +} + +// ReadConnRequest returns a Request from a net.Conn. +func ReadConnRequest(conn net.Conn) (req *Request, err error) { + + var b []byte + var size int + var buf *bytes.Buffer = new(bytes.Buffer) + + // First check for a checksum. + b = make([]byte, len(hdrMSGSTART)) + if _, err = conn.Read(b); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + if bytes.Equal(b, hdrCKSUM) { + // A checksum was found. Continue reading in the checksum and MSGSTART. + if _, err = io.CopyN(buf, conn, int64(CksumPackedSize+len(hdrMSGSTART))); err != nil { + return + } + } + // Otherwise no checksum was found. + + // Protocol version, BODYSTART, RG count. + if _, err = io.CopyN(buf, conn, int64(PackedNumSize+len(hdrBODYSTART)+PackedNumSize)); err != nil { + return + } + + // RG size. + b = make([]byte, PackedNumSize) + if _, err = conn.Read(b); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + size = UnpackInt(b) + + // Now copy in the RG, BODYEND, MSGEND. + if _, err = io.CopyN(buf, conn, int64(size+len(hdrBODYEND)+len(hdrMSGEND))); err != nil { + return + } + + // Unmarshal. + req = new(Request) + if err = req.UnmarshalBinary(buf.Bytes()); err != nil { + return + } + + return +} + +// ReadConnResponse returns a Response from a net.Conn. +func ReadConnResponse(conn net.Conn) (resp *Response, err error) { + + var b []byte + var size int + var buf *bytes.Buffer = new(bytes.Buffer) + + // First get the checksum. + b = make([]byte, len(hdrCKSUM)) + if _, err = conn.Read(b); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + if !bytes.Equal(b, hdrCKSUM) { + err = ErrBadHdr + return + } + // A checksum was found. Continue reading in the checksum, MSGSTART, protocol version, BODYSTART, and RG count. + if _, err = io.CopyN( + buf, conn, int64(CksumPackedSize+len(hdrMSGSTART)+PackedNumSize+len(hdrBODYSTART)+PackedNumSize), + ); err != nil { + return + } + + // RG size. + b = make([]byte, PackedNumSize) + if _, err = conn.Read(b); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + size = UnpackInt(b) + + // Now copy in the RG, BODYEND, MSGEND. + if _, err = io.CopyN(buf, conn, int64(size+len(hdrBODYEND)+len(hdrMSGEND))); err != nil { + return + } + + // Unmarshal. + resp = new(Response) + if err = resp.UnmarshalBinary(buf.Bytes()); err != nil { + return + } + + return +} + +// PackInt is a more generic form of PackUint32 as a convenience wrapper. +func PackInt(i int) (b []byte) { + + b = make([]byte, PackedNumSize) + + byteOrder.PutUint32(b, uint32(i)) + + return +} + +// PackUint32 uses the package-wide byteOrder to pack a uint32 into a series of bytes. +func PackUint32(u uint32) (b []byte) { + + b = make([]byte, PackedNumSize) + + byteOrder.PutUint32(b, u) + + return +} + +// UnpackInt is a more generic form of UnpackUint32 as a convenience wrapper. The same caveat applies for UnpackUint32. +func UnpackInt(b []byte) (i int) { + + var u uint32 + + u = UnpackUint32(b) + i = int(u) + + return +} + +// UnpackUint32 returns a uint32 from byteslice b. The byteslice *MUST* be PackedNumSize (4) bytes long, or u will always be 0. +func UnpackUint32(b []byte) (u uint32) { + + if b == nil || len(b) != PackedNumSize { + return + } + u = byteOrder.Uint32(b) + + return +} + +/* + WriteConnRequest writes a Request to a net.Conn. + + If chunkSize == 0, no chunking will be performed. This may be unreliable on some connections. + + If chunkSize > 0, chunking will be performed based on chunkSize. + + If chunkSize < 0, chunking will be done based on WriteChunkSize number of bytes, which should be sensible for most connections. +*/ +func WriteConnRequest(req *Request, conn net.Conn, chunkSize int) (err error) { + + if err = WriteRequest(req, conn, chunkSize); err != nil { + return + } + + return +} + +/* + WriteConnRequestSegmented writes a Request to a net.Conn, chunking it by each logical section. + + This is slower than a continuous stream/fixed-chunk stream to the end (see WriteConnRequest), + but will be more manageable over high-latency connections and will prevent clients from being + overwhelmed on a large Request. +*/ +func WriteConnRequestSegmented(req *Request, conn net.Conn) (err error) { + + if err = WriteRequestSegmented(req, conn); err != nil { + return + } + + return +} + +/* + WriteConnResponse writes a Response to a net.Conn. + + If chunkSize == 0, no chunking will be performed. This may be unreliable on some connections. + + If chunkSize > 0, chunking will be performed based on chunkSize. + + If chunkSize < 0, chunking will be done based on WriteChunkSize number of bytes, which should be sensible for most connections. +*/ +func WriteConnResponse(resp *Response, conn net.Conn, chunkSize int) (err error) { + + if err = WriteResponse(resp, conn, chunkSize); err != nil { + return + } + + return +} + +/* + WriteConnResponseSegmented writes a Response to a net.Conn, chunking it by each logical section. + + This is slower than a continuous stream/fixed-chunk stream to the end (see WriteConnResponse), + but will be more manageable over high-latency connections and will prevent clients from being + overwhelmed on a large Response. +*/ +func WriteConnResponseSegmented(resp *Response, conn net.Conn) (err error) { + + if err = WriteResponseSegmented(resp, conn); err != nil { + return + } + + return +} + +/* + WriteRequest writes a Request to an io.Writer. + + If chunkSize == 0, no chunking will be performed. This may be unreliable for some io.Writers. + + If chunkSize > 0, chunking will be performed based on chunkSize. + + If chunkSize < 0, chunking will be done based on WriteChunkSize number of bytes, which should be sensible for most io.Writers. +*/ +func WriteRequest(req *Request, w io.Writer, chunkSize int) (err error) { + + var b []byte + var buf *bytes.Buffer + + if req == nil { + return + } + + if b, err = req.MarshalBinary(); err != nil { + return + } + + if chunkSize == 0 { + if _, err = w.Write(b); err != nil { + return + } + return + } + + if chunkSize < 0 { + chunkSize = WriteChunkSize + } + + buf = bytes.NewBuffer(b) + + for buf.Len() != 0 { + if buf.Len() < chunkSize { + chunkSize = buf.Len() + } + if _, err = io.CopyN(w, buf, int64(chunkSize)); err != nil { + return + } + } + + return +} + +/* + WriteRequestSegmented writes a Request to an io.Writer, chunking it by each logical section. + + This is slower than a continuous stream/fixed-chunk stream to the end (see WriteRequest), + but will be more manageable for slower io.Writers. +*/ +func WriteRequestSegmented(req *Request, w io.Writer) (err error) { + + var size int + + if req == nil { + return + } + + for _, rg := range req.RecordGroups { + size += rg.Size() + } + + if req.Checksum != nil { + if _, err = w.Write(hdrCKSUM); err != nil { + return + } + if _, err = w.Write(PackUint32(*req.Checksum)); err != nil { + return + } + } + if _, err = w.Write(hdrMSGSTART); err != nil { + return + } + + if _, err = w.Write(PackUint32(req.ProtocolVersion)); err != nil { + return + } + + if _, err = w.Write(PackInt(len(req.RecordGroups))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + + for _, rg := range req.RecordGroups { + size = 0 + for _, rec := range rg.Records { + size += rec.Size() + } + if _, err = w.Write(PackInt(len(rg.Records))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + for _, rec := range rg.Records { + size = 0 + for _, kvp := range rec.Pairs { + size += kvp.Size() + } + if _, err = w.Write(PackInt(len(rec.Pairs))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + for _, kvp := range rec.Pairs { + if _, err = w.Write(PackInt(kvp.Name.Size())); err != nil { + return + } + if _, err = w.Write(PackInt(kvp.Value.Size())); err != nil { + return + } + if _, err = w.Write(kvp.Name); err != nil { + return + } + if _, err = w.Write(kvp.Value); err != nil { + return + } + } + } + } + + if _, err = w.Write(hdrBODYEND); err != nil { + return + } + if _, err = w.Write(hdrMSGEND); err != nil { + return + } + + return +} + +/* + WriteResponse writes a Response to an io.Writer. + + If chunkSize == 0, no chunking will be performed. This may be unreliable for some io.Writers. + + If chunkSize > 0, chunking will be performed based on chunkSize. + + If chunkSize < 0, chunking will be done based on WriteChunkSize number of bytes, which should be sensible for most io.Writers. +*/ +func WriteResponse(resp *Response, w io.Writer, chunkSize int) (err error) { + + var b []byte + var buf *bytes.Buffer + + if resp == nil { + return + } + + if b, err = resp.MarshalBinary(); err != nil { + return + } + + if chunkSize == 0 { + if _, err = w.Write(b); err != nil { + return + } + return + } + + if chunkSize < 0 { + chunkSize = WriteChunkSize + } + + buf = bytes.NewBuffer(b) + + for buf.Len() != 0 { + if buf.Len() < chunkSize { + chunkSize = buf.Len() + } + if _, err = io.CopyN(w, buf, int64(chunkSize)); err != nil { + return + } + } + + return +} + +/* + WriteResponseSegmented writes a Response to an io.Writer, chunking it by each logical section. + + This is slower than a continuous stream/fixed-chunk stream to the end (see WriteResponse), + but will be more manageable for slower io.Writers. +*/ +func WriteResponseSegmented(resp *Response, w io.Writer) (err error) { + + var size int + + if resp == nil { + return + } + + for _, rg := range resp.RecordGroups { + size += rg.Size() + } + + if _, err = w.Write([]byte{resp.Status}); err != nil { + return + } + + if _, err = w.Write(hdrCKSUM); err != nil { + return + } + if _, err = w.Write(PackUint32(resp.Checksum)); err != nil { + return + } + + if _, err = w.Write(hdrMSGSTART); err != nil { + return + } + + if _, err = w.Write(PackUint32(resp.ProtocolVersion)); err != nil { + return + } + + if _, err = w.Write(PackInt(len(resp.RecordGroups))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + + for _, rg := range resp.RecordGroups { + size = 0 + for _, rec := range rg.Records { + size += rec.Size() + } + if _, err = w.Write(PackInt(len(rg.Records))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + for _, rec := range rg.Records { + size = 0 + for _, kvp := range rec.Pairs { + size += kvp.Size() + } + if _, err = w.Write(PackInt(len(rec.Pairs))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + if _, err = w.Write(PackInt(rec.OriginalRecord.Size())); err != nil { + return + } + for _, kvp := range rec.Pairs { + if _, err = w.Write(PackInt(kvp.Name.Size())); err != nil { + return + } + if _, err = w.Write(PackInt(kvp.Value.Size())); err != nil { + return + } + if _, err = w.Write(kvp.Name); err != nil { + return + } + if _, err = w.Write(kvp.Value); err != nil { + return + } + } + size = 0 + for _, kvp := range rec.OriginalRecord.Pairs { + size += kvp.Size() + } + if _, err = w.Write(PackInt(len(rec.OriginalRecord.Pairs))); err != nil { + return + } + if _, err = w.Write(PackInt(size)); err != nil { + return + } + for _, kvp := range rec.OriginalRecord.Pairs { + if _, err = w.Write(PackInt(kvp.Name.Size())); err != nil { + return + } + if _, err = w.Write(PackInt(kvp.Value.Size())); err != nil { + return + } + if _, err = w.Write(kvp.Name); err != nil { + return + } + if _, err = w.Write(kvp.Value); err != nil { + return + } + } + } + } + + if _, err = w.Write(hdrBODYEND); err != nil { + return + } + if _, err = w.Write(hdrMSGEND); err != nil { + return + } + + return +} + +// chunkByteLine splits b into a chunked slice of no more than maxByteLine per 1st-level element. +func chunkByteLine(b []byte) (chunked [][]byte) { + + chunked = make([][]byte, 0, (len(b)+maxByteLine-1)/maxByteLine) + + // slices.Chunk requires Golang 1.23+ + for chunk := range slices.Chunk(b, maxByteLine) { + chunked = append(chunked, chunk) + } + + return +} + +// cksumBytes returns a byte-packed representation of the checksum. +func cksumBytes(cksum uint32) (packed []byte) { + + packed = make([]byte, CksumPackedSize) + byteOrder.PutUint32(packed, cksum) + + return +} + +/* + padBytesRight is used when rendering Model objects. + + val is assumed to *not* be in hex format already; it should be the raw []byte representation. +*/ +func padBytesRight(val []byte, length int) (out string) { + + out = fmt.Sprintf("%-*x", length, val) + + return +} + +/* + padIntRight is used when rendering Model objects. +*/ +func padIntRight(val int, length int) (out string) { + + out = padBytesRight(PackUint32(uint32(val)), length) + + return +} + +/* + padStrRight is used when rendering Model objects. +*/ +func padStrRight(val string, length int) (out string) { + + out = padBytesRight([]byte(val), length) + + return +} diff --git a/funcs_fieldname.go b/funcs_fieldname.go new file mode 100644 index 0000000..049bf10 --- /dev/null +++ b/funcs_fieldname.go @@ -0,0 +1,75 @@ +package wireproto + +// MarshalBinary renders a FieldName into a byte-packed format. +func (f *FieldName) MarshalBinary() (data []byte, err error) { + + if f == nil { + data = []byte{} + return + } + + data = []byte(*f) + + return +} + +// MarshalText renders a FieldName into plaintext. +func (f *FieldName) MarshalText() (data []byte, err error) { + + if f == nil { + data = []byte{} + return + } + + data = []byte(*f) + + return +} + +// UnmarshalBinary populates a FieldName from packed bytes. +func (f *FieldName) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + *f = FieldName(data) + + return +} + +// UnmarshalText populates a FieldName from plaintext. +func (f *FieldName) UnmarshalText(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + *f = FieldName(data) + + return +} + +// Size returns the FieldName's calculated size (in bytes). +func (f *FieldName) Size() (size int) { + + if f == nil { + return + } + + size = len(*f) + + return +} + +// String returns a string representation of a FieldName. +func (f *FieldName) String() (s string) { + + if f == nil { + return + } + + s = string([]byte(*f)) + + return +} diff --git a/funcs_fieldvalue.go b/funcs_fieldvalue.go new file mode 100644 index 0000000..7f399b8 --- /dev/null +++ b/funcs_fieldvalue.go @@ -0,0 +1,80 @@ +package wireproto + +/* + MarshalBinary renders a FieldValue into a byte-packed format. + + Unlike other types (aside from FieldName), it does *not* include its allocator! +*/ +func (f *FieldValue) MarshalBinary() (data []byte, err error) { + + if f == nil { + data = []byte{} + return + } + + data = []byte(*f) + + return +} + +// MarshalText renders a FieldValue into plaintext. +func (f *FieldValue) MarshalText() (data []byte, err error) { + + if f == nil { + data = []byte{} + return + } + + data = []byte(*f) + + return +} + +// UnmarshalBinary populates a FieldValue from packed bytes. +func (f *FieldValue) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + // Strip the header; remainder should be value. + *f = FieldValue(data) + + return +} + +// UnmarshalText populates a FieldValue from plaintext. +func (f *FieldValue) UnmarshalText(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + *f = FieldValue(data) + + return +} + +// Size returns the FieldValue's calculated size (in bytes). +func (f *FieldValue) Size() (size int) { + + if f == nil { + return + } + + size = len(*f) + + return +} + +// String returns a string representation of a FieldValue. +func (f *FieldValue) String() (s string) { + + if f == nil { + return + } + + s = string([]byte(*f)) + + return +} diff --git a/funcs_fieldvaluepair.go b/funcs_fieldvaluepair.go new file mode 100644 index 0000000..c29ae7e --- /dev/null +++ b/funcs_fieldvaluepair.go @@ -0,0 +1,268 @@ +package wireproto + +import ( + `bytes` + `fmt` + `strings` + + `github.com/google/uuid` +) + +// ConnId returns a copy of the connection ID. +func (f *FieldValuePair) ConnId() (conn uuid.UUID) { + + if f.connId == nil { + return + } + + conn = *f.connId + + return +} + +// GetParent returns the parent Record, if set. +func (f *FieldValuePair) GetParent() (rec Record) { + + rec = f.parent + + return +} + +// MarshalBinary renders a FieldValuePair into a byte-packed format. +func (f *FieldValuePair) MarshalBinary() (data []byte, err error) { + + var b []byte + var buf *bytes.Buffer = new(bytes.Buffer) + + _ = f.Size() + + if _, err = buf.Write(PackInt(f.Name.Size())); err != nil { + return + } + if _, err = buf.Write(PackInt(f.Value.Size())); err != nil { + return + } + + if b, err = f.Name.MarshalBinary(); err != nil { + return + } + buf.Write(b) + if b, err = f.Value.MarshalBinary(); err != nil { + return + } + buf.Write(b) + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (f *FieldValuePair) Model() (out string) { + + out = f.ModelCustom(IndentChars, SeparatorChars, indentKvp) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (f *FieldValuePair) ModelCustom(indent, sep string, level uint) (out string) { + + var fnSize int + var fvSize int + var splitFn [][]byte // for multi-chunk hex + var splitFv [][]byte // """ + var sb strings.Builder + var maxLen int = 8 // len of size uint32 hex + + // This one is the biggest pain because it has to handle arbitrary lengths. + + fnSize = f.Name.Size() * 2 // 2x because Hex string + fvSize = f.Value.Size() * 2 // """ + if fnSize > maxLen { + if f.Name.Size() <= maxByteLine { + maxLen = fnSize + } else { + maxLen = maxByteLine * 2 + } + } + if fvSize > maxLen { + if f.Value.Size() <= maxByteLine { + maxLen = fvSize + } else { + maxLen = maxByteLine * 2 + } + } + + splitFn = chunkByteLine(f.Name) + splitFv = chunkByteLine(f.Value) + + // SIZES + // Field Name + // e.g. "\t\t\t00000003 // Field Name Size (3)" + sb.WriteString(strings.Repeat(indent, int(level))) // \t\t\t + sb.WriteString(padIntRight(f.Name.Size(), maxLen)) // "00000003 " + sb.WriteString(sep) // " " + sb.WriteString(fmt.Sprintf("// Field Name Size (%d)\n", f.Name.Size())) // "// Field Name Size (3)". The extra space is intentional. + // Field Value + // e.g. "\t\t\t00000018 // Field Value Size (24)" + sb.WriteString(strings.Repeat(indent, int(level))) // \t\t\t + sb.WriteString(padIntRight(f.Value.Size(), maxLen)) // "000000018 " + sb.WriteString(sep) // " " + sb.WriteString(fmt.Sprintf("// Field Value Size (%d)\n", f.Value.Size())) // "// Field Value Size (24)". + + // VALUES + // Name + // e.g. `\t\t\t666f6f // "foo"` + for idx, chunk := range splitFn { + sb.WriteString(strings.Repeat(indent, int(level))) // "\t\t\t" + if idx == 0 { + sb.WriteString(padBytesRight(chunk, maxLen)) // "666f6f " + sb.WriteString(sep) // " " + sb.WriteString(fmt.Sprintf("// \"%s\"", f.Name)) // `// "foo"` + } else { + sb.WriteString(fmt.Sprintf("%x", chunk)) // "666f6f" + } + sb.WriteString("\n") + } + // Value + /* + e.g.: + \t\t\t736f6d65206c6f6e6765722076616c75 // "some longer value string" + \t\t\t6520737472696e67 + */ + for idx, chunk := range splitFv { + sb.WriteString(strings.Repeat(indent, int(level))) // "\t\t\t" + if idx == 0 { + sb.WriteString(padBytesRight(chunk, maxLen)) // "736f6d65206c6f6e6765722076616c75" + sb.WriteString(sep) // " " + sb.WriteString(fmt.Sprintf("// \"%s\"", f.Value)) // `// "some longer value string"` + } else { + sb.WriteString(fmt.Sprintf("%x", chunk)) // "6520737472696e67" + } + sb.WriteString("\n") + } + + out = sb.String() + + return +} + +/* + ToMap returns a map of map[name]value. + While an interface{} in the map, the value is actually a FieldValue. +*/ +func (f *FieldValuePair) ToMap() (m map[string]interface{}) { + + m = map[string]interface{}{ + f.Name.String(): f.Value, + } + + return +} + +// UnmarshalBinary populates a FieldValuePair from packed bytes. +func (f *FieldValuePair) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var nmSize, valSize int + var buf *bytes.Reader = bytes.NewReader(data) + + if f == nil { + *f = FieldValuePair{} + } + f.common = &common{} + f.size = 0 + + // Get the name/value sizes. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + nmSize = UnpackInt(b) + + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + valSize = UnpackInt(b) + + // Get the name. + if nmSize != 0 { + b = make([]byte, nmSize) + if _, err = buf.Read(b); err != nil { + return + } + if err = f.Name.UnmarshalBinary(b); err != nil { + return + } + } else { + f.Name = nil + } + + // Get the value. + if valSize != 0 { + b = make([]byte, valSize) + if _, err = buf.Read(b); err != nil { + return + } + if err = f.Value.UnmarshalBinary(b); err != nil { + return + } + } else { + f.Value = nil + } + + _ = f.Size() + + return +} + +// Size returns the FieldValuePair's calculated size (in bytes) and updates the size field. +func (f *FieldValuePair) Size() (size int) { + + if f == nil { + return + } + + // Count and Size uint32s + size += PackedNumSize * 2 + + size += f.Name.Size() + size += f.Value.Size() + + if f.common == nil { + f.common = new(common) + } + + f.common.size = uint32(size) + + return +} + +// getIdx returns the FVP index in the parent Record. +func (f *FieldValuePair) getIdx() (idx int) { + + var fvps []FVP + + idx = -1 + + if f == nil || f.parent == nil { + return + } + + fvps = f.parent.getFvps() + + for i, fv := range fvps { + if fv == f { + idx = i + return + } + } + + return +} diff --git a/funcs_request.go b/funcs_request.go new file mode 100644 index 0000000..0693916 --- /dev/null +++ b/funcs_request.go @@ -0,0 +1,421 @@ +package wireproto + +import ( + `bytes` + `cmp` + `fmt` + `hash/crc32` + `io` + `strings` + + `github.com/google/uuid` +) + +// ConnId returns a copy of the connection ID. +func (r *Request) ConnId() (conn uuid.UUID) { + + conn = r.connId + + return +} + +// MarshalBinary renders a Request into a byte-packed format. +func (r *Request) MarshalBinary() (data []byte, err error) { + + var b []byte + var msgSize int + var cksum uint32 + var buf *bytes.Buffer = new(bytes.Buffer) + var msgBuf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + for _, i := range r.RecordGroups { + msgSize += i.Size() + } + + // The message "body" - we do this first so we can checksum (if not nil). + if _, err = msgBuf.Write(hdrBODYSTART); err != nil { + return + } + // Record group count + if _, err = msgBuf.Write(PackInt(len(r.RecordGroups))); err != nil { + return + } + // And size. + if _, err = msgBuf.Write(PackInt(msgSize)); err != nil { + return + } + for _, i := range r.RecordGroups { + if b, err = i.MarshalBinary(); err != nil { + return + } + if _, err = msgBuf.Write(b); err != nil { + return + } + } + if _, err = msgBuf.Write(hdrBODYEND); err != nil { + return + } + + // Now the surrounding request. + + // Checksum - update and serialize if not null. + if r.Checksum != nil { + cksum = crc32.ChecksumIEEE(msgBuf.Bytes()) + *r.Checksum = cksum + if _, err = buf.Write(hdrCKSUM); err != nil { + return + } + if _, err = buf.Write(cksumBytes(*r.Checksum)); err != nil { + return + } + } + + // Message start + if _, err = buf.Write(hdrMSGSTART); err != nil { + return + } + + // Protocol version + if _, err = buf.Write(PackUint32(r.ProtocolVersion)); err != nil { + return + } + + // Then copy the msgBuf in. + if _, err = msgBuf.WriteTo(buf); err != nil { + return + } + // And then the message end. + if _, err = buf.Write(hdrMSGEND); err != nil { + return + } + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (r *Request) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentR) + + return +} + +func (r *Request) ModelCustom(indent, sep string, level uint) (out string) { + + var maxFtr int + var size int + var sb strings.Builder + + for _, rg := range r.RecordGroups { + size += rg.Size() + } + + // Checksum (optional for Request) + sb.WriteString(strings.Repeat(indent, int(level))) + if r.Checksum == nil { + sb.WriteString(strings.Repeat("-", 8)) + sb.WriteString(sep) + sb.WriteString("// (No Checksum Present)\n") + } else { + // HDR: CKSUM + sb.WriteString(padBytesRight(hdrCKSUM, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:CKSUM\n") + // Checksum + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(cksumBytes(*r.Checksum), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Checksum Value (%d)\n", *r.Checksum)) + } + + // Header: MSGSTART + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrMSGSTART, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:MSGSTART\n") + + // Protocol Version + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(int(r.ProtocolVersion), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Protocol Version (%d)\n", r.ProtocolVersion)) + + // Header: BODYSTART + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrBODYSTART, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:BODYSTART\n") + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(len(r.RecordGroups), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Group Count (%d)\n", len(r.RecordGroups))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(size, 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Groups Size (%d)\n", size)) + + // VALUES + for idx, rg := range r.RecordGroups { + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("// Record Group %d (%d bytes)\n", idx+1, rg.Size())) + sb.WriteString(rg.ModelCustom(indent, sep, level+1)) + } + + // Make the footers a little more nicely aligned. + switch cmp.Compare(len(hdrBODYEND), len(hdrMSGEND)) { + case -1: + maxFtr = len(hdrMSGEND) + case 1, 0: + maxFtr = len(hdrBODYEND) + } + + // Footer: BODYEND + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrBODYEND, maxFtr)) + sb.WriteString(sep) + sb.WriteString("// HDR:BODYEND\n") + + // Footer: MSGEND + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrMSGEND, maxFtr)) + sb.WriteString(sep) + sb.WriteString("// HDR:MSGEND\n") + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *Request) Resolve() { + for idx, i := range r.RecordGroups { + i.parent = r + i.rgIdx = idx + i.Resolve() + } +} + +// SetConnID allows for setting a connection ID. This is largely just used for debugging purposes. +func (r *Request) SetConnID(connId uuid.UUID) { + + r.connId = connId + + for _, rg := range r.RecordGroups { + rg.connId = connId + for _, rec := range rg.Records { + rec.connId = connId + for _, kvp := range rec.Pairs { + kvp.connId = &connId + } + } + } + +} + +// Size returns the Request's calculated size (in bytes) and updates the size field if 0. +func (r *Request) Size() (size int) { + + if r == nil { + return + } + + // Checksum + if r.Checksum != nil { + size += len(hdrCKSUM) + size += CksumPackedSize + } + + // Message header + size += len(hdrMSGSTART) + + // Protocol version + size += PackedNumSize + + // Count and Size uint32s + size += PackedNumSize * 2 + + // Message begin + size += len(hdrBODYSTART) + + for _, p := range r.RecordGroups { + size += p.Size() + } + + // Message end + size += len(hdrBODYEND) + + // And closing sequence. + size += len(hdrMSGEND) + + if r.common == nil { + r.common = new(common) + } + + r.size = uint32(size) + + return +} + +// ToMap returns a slice of slice of slice of FVP maps for this Message. +func (r *Request) ToMap() (m [][][]map[string]interface{}) { + + m = make([][][]map[string]interface{}, len(r.RecordGroups)) + for idx, rg := range r.RecordGroups { + m[idx] = rg.ToMap() + } + + return +} + +// UnmarshalBinary populates a Request from packed bytes. +func (r *Request) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var rgCnt, bodySize int + var rgSize int + var rgBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + var msgBuf *bytes.Buffer = new(bytes.Buffer) + + if r == nil { + *r = Request{} + } + r.common = &common{} + r.size = 0 + + // Check for a checksum. + b = make([]byte, len(hdrMSGSTART)) + if _, err = buf.Read(b); err != nil { + return + } + if bytes.Equal(b, hdrCKSUM) { + // A checksum header was found. + b = make([]byte, CksumPackedSize) + if _, err = buf.Read(b); err != nil { + return + } + r.Checksum = new(uint32) + *r.Checksum = byteOrder.Uint32(b) + // Since we've only read the checksum, we now also have to read in the MSGSTART... + b = make([]byte, len(hdrMSGSTART)) + if _, err = buf.Read(b); err != nil { + return + } + // But we don't need to do anything with it. + } else { + // We've already read MSGSTART as part of the checksum check. + r.Checksum = nil + } + + // Get the protocol version. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + r.ProtocolVersion = UnpackUint32(b) + + // Skip over the BODYSTART (but write it to msgBuf). + if _, err = io.CopyN(msgBuf, buf, int64(len(hdrBODYSTART))); err != nil { + return + } + + // Get the count of record groups + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + rgCnt = UnpackInt(b) + if _, err = msgBuf.Write(b); err != nil { + return + } + // And their size + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + bodySize = UnpackInt(b) + if _, err = msgBuf.Write(b); err != nil { + return + } + + // And the record groups themselves. + if _, err = io.CopyN(msgBuf, buf, int64(bodySize+len(hdrBODYEND))); err != nil { + return + } + + // Validate the checksum. + if r.Checksum != nil { + if crc32.ChecksumIEEE(msgBuf.Bytes()) != *r.Checksum { + err = ErrBadCksum + return + } + } + + // Now that we've validated the checksum (if provided), we trim the msgBuf to only RGs. + // Skip over the BODYSTART, record group count, and record group size. + if _, err = msgBuf.Read(make([]byte, len(hdrBODYSTART)+(PackedNumSize*2))); err != nil { + return + } + // Then truncate. + msgBuf.Truncate(bodySize) + + r.RecordGroups = make([]*RequestRecordGroup, rgCnt) + + for idx := 0; idx < rgCnt; idx++ { + rgBuf = new(bytes.Buffer) + + // The RG unmarshaler handles the record count, but we need to read it to discard it in msgBuf. + if _, err = io.CopyN(rgBuf, msgBuf, int64(PackedNumSize)); err != nil { + return + } + + b = make([]byte, PackedNumSize) + if _, err = msgBuf.Read(b); err != nil { + return + } + if _, err = rgBuf.Write(b); err != nil { + return + } + rgSize = UnpackInt(b) + + if _, err = io.CopyN(rgBuf, msgBuf, int64(rgSize)); err != nil { + return + } + + r.RecordGroups[idx] = new(RequestRecordGroup) + if err = r.RecordGroups[idx].UnmarshalBinary(rgBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + + return +} + +// getIdx is a NOOP for Messages, but is used for Model conformance. +func (r *Request) getIdx() (idx int) { + return +} + +// getRecordGroups returns the RecordGroups in this Message. +func (r *Request) getRecordGroups() (recordGroups []RecordGroup) { + + recordGroups = make([]RecordGroup, len(r.RecordGroups)) + for idx, rg := range r.RecordGroups { + recordGroups[idx] = rg + } + + return +} diff --git a/funcs_requestrecord.go b/funcs_requestrecord.go new file mode 100644 index 0000000..551c968 --- /dev/null +++ b/funcs_requestrecord.go @@ -0,0 +1,254 @@ +package wireproto + +import ( + `bytes` + `fmt` + `io` + `strings` + + `github.com/google/uuid` +) + +// ConnId returns a copy of the connection ID. +func (r *RequestRecord) ConnId() (conn uuid.UUID) { + + conn = r.connId + + return +} + +// GetParent returns the parent RecordGroup. +func (r *RequestRecord) GetParent() (rg RecordGroup) { + + rg = r.parent + + return +} + +// MarshalBinary renders a RequestRecord into a byte-packed format. +func (r *RequestRecord) MarshalBinary() (data []byte, err error) { + + var b []byte + var recSize int + var buf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + if _, err = buf.Write(PackInt(len(r.Pairs))); err != nil { + return + } + for _, i := range r.Pairs { + recSize += i.Size() + } + if _, err = buf.Write(PackInt(recSize)); err != nil { + return + } + + for _, i := range r.Pairs { + if b, err = i.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (r *RequestRecord) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentRec) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (r *RequestRecord) ModelCustom(indent, sep string, level uint) (out string) { + + var size int + var sb strings.Builder + + for _, p := range r.Pairs { + size += p.Size() + } + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(len(r.Pairs))))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Field/Value Count (%d)\n", len(r.Pairs))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(size)))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Size (%d)\n", size)) + + // VALUES + for idx, p := range r.Pairs { + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString( + fmt.Sprintf( + "// Record Group %d, Record %d, Field/Value %d (%d bytes)\n", + r.rgIdx+1, r.rIdx+1, idx+1, p.Size(), + ), + ) + sb.WriteString(p.ModelCustom(indent, sep, level+1)) + } + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *RequestRecord) Resolve() { + for idx, i := range r.Pairs { + i.parent = r + i.rgIdx = r.rgIdx + i.rIdx = idx + i.fvpidx = idx + // KVP have no Resolve() method. + } +} + +// Size returns the RequestRecord's calculated size (in bytes) and updates the size field if 0. +func (r *RequestRecord) Size() (size int) { + + if r == nil { + return + } + + // Count and Size uint32s + size += PackedNumSize * 2 + + for _, i := range r.Pairs { + size += i.Size() + } + + if r.common == nil || r.size == 0 { + r.common = &common{ + size: uint32(size), + } + } + + r.size = uint32(size) + + return +} + +// ToMap returns a slice of FVP maps for this Record. +func (r *RequestRecord) ToMap() (m []map[string]interface{}) { + + m = make([]map[string]interface{}, len(r.Pairs)) + for idx, p := range r.Pairs { + m[idx] = p.ToMap() + } + + return +} + +// UnmarshalBinary populates a RequestRecord from packed bytes. +func (r *RequestRecord) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var cnt, size int + var fvpNmSize, fvpValSize int + var fvpBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + + if r == nil { + *r = RequestRecord{} + } + if r.common == nil { + r.common = new(common) + } + r.size = 0 + + // FVP count. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + cnt = UnpackInt(b) + + // Size of record. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + size = UnpackInt(b) + + // And now we handle the FVPs themselves. + b = make([]byte, size) + if _, err = buf.Read(b); err != nil { + return + } + buf = bytes.NewReader(b) + + r.Pairs = make([]*FieldValuePair, cnt) + + for idx := 0; idx < cnt; idx++ { + fvpBuf = new(bytes.Buffer) + + // Unlike parents, the FVP needs both allocators because they're both size allocators. + // FVP Name + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpNmSize = UnpackInt(b) + // FVP Value + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpValSize = UnpackInt(b) + + if _, err = io.CopyN(fvpBuf, buf, int64(fvpNmSize+fvpValSize)); err != nil { + return + } + + r.Pairs[idx] = new(FieldValuePair) + if err = r.Pairs[idx].UnmarshalBinary(fvpBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + _ = size + + return +} + +// getFvps returns this Record's FVP. +func (r *RequestRecord) getFvps() (fvp []FVP) { + + fvp = make([]FVP, len(r.Pairs)) + for idx, p := range r.Pairs { + fvp[idx] = p + } + + return +} + +// getIdx returns the Record index in the parent RecordGroup. +func (r *RequestRecord) getIdx() (idx int) { + + idx = r.rIdx + + return +} diff --git a/funcs_requestrecordgroup.go b/funcs_requestrecordgroup.go new file mode 100644 index 0000000..4105e9b --- /dev/null +++ b/funcs_requestrecordgroup.go @@ -0,0 +1,250 @@ +package wireproto + +import ( + `bytes` + `fmt` + `io` + `strings` + + `github.com/google/uuid` +) + +// ConnId returns a copy of the connection ID. +func (r *RequestRecordGroup) ConnId() (conn uuid.UUID) { + + conn = r.connId + + return +} + +// GetParent returns this RecordGroup's Message. +func (r *RequestRecordGroup) GetParent() (msg Message) { + + msg = r.parent + + return +} + +// MarshalBinary renders a RequestRecordGroup into a byte-packed format. +func (r *RequestRecordGroup) MarshalBinary() (data []byte, err error) { + + var b []byte + var rgSize int + var buf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + for _, i := range r.Records { + rgSize += i.Size() + } + + // Count + if _, err = buf.Write(PackInt(len(r.Records))); err != nil { + return + } + // Size + if _, err = buf.Write(PackInt(rgSize)); err != nil { + return + } + + for _, i := range r.Records { + if b, err = i.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (r *RequestRecordGroup) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentRG) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (r *RequestRecordGroup) ModelCustom(indent, sep string, level uint) (out string) { + + var size int + var sb strings.Builder + + for _, rec := range r.Records { + size += rec.Size() + } + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(len(r.Records))))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Count (%d)\n", len(r.Records))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(size)))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Group Size (%d)\n", size)) + + // VALUES + for idx, rec := range r.Records { + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString( + fmt.Sprintf( + "// Record Group %d, Record %d (%d bytes)\n", + r.rgIdx+1, idx+1, rec.Size(), + ), + ) + sb.WriteString(rec.ModelCustom(indent, sep, level+1)) + } + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *RequestRecordGroup) Resolve() { + for idx, i := range r.Records { + i.parent = r + i.rgIdx = r.rgIdx + i.rIdx = idx + i.Resolve() + } +} + +// Size returns the RequestRecordGroup's calculated size (in bytes) and updates the size field if 0. +func (r *RequestRecordGroup) Size() (size int) { + + if r == nil { + return + } + + // Count and Size uint32s + size += PackedNumSize * 2 + + for _, p := range r.Records { + size += p.Size() + } + + if r.common == nil || r.size == 0 { + r.common = new(common) + } + + r.common.size = uint32(size) + + return +} + +// ToMap returns a slice of slice of FVP maps for this RecordGroup. +func (r *RequestRecordGroup) ToMap() (m [][]map[string]interface{}) { + + m = make([][]map[string]interface{}, len(r.Records)) + for idx, rec := range r.Records { + m[idx] = rec.ToMap() + } + + return +} + +// UnmarshalBinary populates a RequestRecordGroup from packed bytes. +func (r *RequestRecordGroup) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var cnt, size int + var recSize int + var recBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + + if r == nil { + *r = RequestRecordGroup{} + } + if r.common == nil { + r.common = new(common) + } + r.size = 0 + + // The record count. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + cnt = UnpackInt(b) + + // The record group size. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + size = UnpackInt(b) + + b = make([]byte, size) + if _, err = buf.Read(b); err != nil { + return + } + + // Get a new buf for the actual records. + buf = bytes.NewReader(b) + + r.Records = make([]*RequestRecord, cnt) + + for idx := 0; idx < cnt; idx++ { + recBuf = new(bytes.Buffer) + + // We skip over the KVP count; that's handled in the record Unmarshaler. + // We *do*, however, need to save it to the recBuf. + if _, err = io.CopyN(recBuf, buf, int64(PackedNumSize)); err != nil { + return + } + + // Size of the actual record + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = recBuf.Write(b); err != nil { + return + } + recSize = UnpackInt(b) + + if _, err = io.CopyN(recBuf, buf, int64(recSize)); err != nil { + return + } + + r.Records[idx] = new(RequestRecord) + if err = r.Records[idx].UnmarshalBinary(recBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + + return +} + +// getIdx returns the RecordGroup index in the parent Message. +func (r *RequestRecordGroup) getIdx() (idx int) { + + idx = r.rgIdx + + return +} + +// getRecords returns the Records in this RecordGroup. +func (r *RequestRecordGroup) getRecords() (records []Record) { + + records = make([]Record, len(r.Records)) + for idx, rec := range r.Records { + records[idx] = rec + } + + return +} diff --git a/funcs_response.go b/funcs_response.go new file mode 100644 index 0000000..4e7f806 --- /dev/null +++ b/funcs_response.go @@ -0,0 +1,485 @@ +package wireproto + +import ( + `bytes` + `cmp` + `fmt` + `hash/crc32` + `io` + `strings` + + `r00t2.io/goutils/multierr` +) + +// GenChecksum (re-)generates and returns the checksum. The body that is checksummed is returned in buf. +func (r *Response) GenChecksum() (cksum uint32, buf *bytes.Buffer, err error) { + + var b []byte + var size int + + buf = new(bytes.Buffer) + + _ = r.Size() + + for _, p := range r.RecordGroups { + size += p.Size() + } + + if _, err = buf.Write(hdrBODYSTART); err != nil { + return + } + + if _, err = buf.Write(PackInt(len(r.RecordGroups))); err != nil { + return + } + if _, err = buf.Write(PackInt(size)); err != nil { + return + } + + for _, rg := range r.RecordGroups { + if b, err = rg.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + if _, err = buf.Write(hdrBODYEND); err != nil { + return + } + + cksum = crc32.ChecksumIEEE(buf.Bytes()) + + r.Checksum = cksum + + return +} + +// RespStat trawls the KVP for a field name with "error" and updates the status to reflect an error was found. +func (r *Response) RespStat() { + + r.Status = RespStatusByteOK + + for _, rg := range r.RecordGroups { + for _, rec := range rg.Records { + for _, kvp := range rec.Pairs { + if strings.ToLower(kvp.Name.String()) == "error" { + r.Status = RespStatusByteErr + return + } + } + } + } + +} + +// MarshalBinary renders a Response into a byte-packed format. +func (r *Response) MarshalBinary() (data []byte, err error) { + + var b []byte + var msgSize int + var hasErr bool + var mErr *multierr.MultiError = multierr.NewMultiError(nil) + var buf *bytes.Buffer = new(bytes.Buffer) + var msgBuf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + for _, i := range r.RecordGroups { + msgSize += i.Size() + } + + // The message "body" - we do this first so we can checksum . + if _, err = msgBuf.Write(hdrBODYSTART); err != nil { + return + } + // Record group count + if _, err = msgBuf.Write(PackInt(len(r.RecordGroups))); err != nil { + return + } + // And size. + if _, err = msgBuf.Write(PackInt(msgSize)); err != nil { + return + } + for _, i := range r.RecordGroups { + if b, err = i.MarshalBinary(); err != nil { + mErr.AddError(err) + err = nil + hasErr = true + } + if _, err = msgBuf.Write(b); err != nil { + mErr.AddError(err) + err = nil + hasErr = true + } + } + if _, err = msgBuf.Write(hdrBODYEND); err != nil { + mErr.AddError(err) + err = nil + hasErr = true + } + + // Now we write the response as a whole. + + // Status + if r.Status == RespStatusByteOK && hasErr { + r.Status = RespStatusByteErr + } + if _, err = buf.Write([]byte{r.Status}); err != nil { + return + } + + // Checksum -- ALWAYS present for responses! + if _, _, err = r.GenChecksum(); err != nil { + return + } + if _, err = buf.Write(hdrCKSUM); err != nil { + return + } + if _, err = buf.Write(cksumBytes(r.Checksum)); err != nil { + return + } + + // Message start + if _, err = buf.Write(hdrMSGSTART); err != nil { + return + } + + // Protocol version + if _, err = buf.Write(PackUint32(r.ProtocolVersion)); err != nil { + return + } + + // Then copy the msgBuf in. + if _, err = msgBuf.WriteTo(buf); err != nil { + return + } + // And then the message end. + if _, err = buf.Write(hdrMSGEND); err != nil { + return + } + + data = buf.Bytes() + + if !mErr.IsEmpty() { + err = mErr + return + } + + return +} + +// Model returns an indented string representation of the model. +func (r *Response) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentR) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (r *Response) ModelCustom(indent, sep string, level uint) (out string) { + + var maxFtr int + var size int + var sb strings.Builder + + for _, rg := range r.RecordGroups { + size += rg.Size() + } + + _, _, _ = r.GenChecksum() + + // HDR: RESPSTART/RESPERR (RESPSTATUS) + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight([]byte{byte(r.Status)}, 8)) + sb.WriteString(sep) + switch r.Status { + case RespStatusByteOK: + sb.WriteString("// HDR:RESPSTART (Status: OK)\n") + case RespStatusByteErr: + sb.WriteString("// HDR:RESPERR (Status: Error)\n") + } + + // HDR: CKSUM + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrCKSUM, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:CKSUM\n") + // Checksum + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(cksumBytes(r.Checksum), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Checksum Value (%d)\n", r.Checksum)) + + // Header: MSGSTART + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrMSGSTART, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:MSGSTART\n") + + // Protocol Version + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(int(r.ProtocolVersion), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Protocol Version (%d)\n", r.ProtocolVersion)) + + // Header: BODYSTART + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrBODYSTART, 8)) + sb.WriteString(sep) + sb.WriteString("// HDR:BODYSTART\n") + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(len(r.RecordGroups), 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Group Count (%d)\n", len(r.RecordGroups))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padIntRight(size, 8)) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Groups Size (%d)\n", size)) + + // VALUES + for idx, rg := range r.RecordGroups { + sb.WriteString(fmt.Sprintf("// Record Group %d (%d)\n", idx+1, rg.Size())) + sb.WriteString(rg.ModelCustom(indent, sep, level+1)) + } + + // Make the footers a little more nicely aligned. + switch cmp.Compare(len(hdrBODYEND), len(hdrMSGEND)) { + case -1: + maxFtr = len(hdrMSGEND) + case 1, 0: + maxFtr = len(hdrBODYEND) + } + + // Footer: BODYEND + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrBODYEND, maxFtr)) + sb.WriteString(sep) + sb.WriteString("// HDR:BODYEND\n") + + // Footer: MSGEND + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(padBytesRight(hdrMSGEND, maxFtr)) + sb.WriteString(sep) + sb.WriteString("// HDR:MSGEND\n") + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *Response) Resolve() { + for idx, i := range r.RecordGroups { + i.parent = r + i.rgIdx = idx + i.Resolve() + } +} + +// Size returns the Response's calculated size (in bytes) and updates the size field if 0. +func (r *Response) Size() (size int) { + + if r == nil { + return + } + + // Response Status + size += 1 + + // Checksum + size += len(hdrCKSUM) + size += CksumPackedSize + + // Message header + size += len(hdrMSGSTART) + + // Protocol version + size += PackedNumSize + + // Count and Size uint32s + size += PackedNumSize * 2 + + // Message begin + size += len(hdrBODYSTART) + + for _, p := range r.RecordGroups { + size += p.Size() + } + + // Message end + size += len(hdrBODYEND) + + // And closing sequence. + size += len(hdrMSGEND) + + if r.common == nil || r.size == 0 { + r.common = new(common) + } + + r.size = uint32(size) + + return +} + +// ToMap returns a slice of slice of slice of FVP maps for this Message. +func (r *Response) ToMap() (m [][][]map[string]interface{}) { + + m = make([][][]map[string]interface{}, len(r.RecordGroups)) + for idx, rg := range r.RecordGroups { + m[idx] = rg.ToMap() + } + + return +} + +// UnmarshalBinary populates a Response from packed bytes. +func (r *Response) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var rgCnt, bodySize int + var rgSize int + var rgBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + var msgBuf *bytes.Buffer = new(bytes.Buffer) + + if r == nil { + *r = Response{} + } + r.common = new(common) + r.size = 0 + + // Get the status. + if r.Status, err = buf.ReadByte(); err != nil { + return + } + + // And the checksum -- responses *always* have checksums per spec! + // Toss the checksum header (after confirming). + b = make([]byte, len(hdrCKSUM)) + if _, err = buf.Read(b); err != nil { + return + } + if !bytes.Equal(b, hdrCKSUM) { + err = ErrBadHdr + return + } + // And get the checksum. + b = make([]byte, CksumPackedSize) + if _, err = buf.Read(b); err != nil { + return + } + r.Checksum = UnpackUint32(b) + + // Read (and toss) the message start header. + if _, err = buf.Read(make([]byte, len(hdrMSGSTART))); err != nil { + return + } + + // Get the protocol version. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + r.ProtocolVersion = UnpackUint32(b) + + // Skip over the BODYSTART (but write it to msgBuf). + if _, err = io.CopyN(msgBuf, buf, int64(len(hdrBODYSTART))); err != nil { + return + } + // Get the count of record groups + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + rgCnt = UnpackInt(b) + if _, err = msgBuf.Write(b); err != nil { + return + } + // Get the size of record groups + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + bodySize = UnpackInt(b) + if _, err = msgBuf.Write(b); err != nil { + return + } + + // And the record groups themselves. + if _, err = io.CopyN(msgBuf, buf, int64(bodySize+len(hdrBODYEND))); err != nil { + return + } + + // Now validate the checksum before continuing. + if crc32.ChecksumIEEE(msgBuf.Bytes()) != r.Checksum { + err = ErrBadCksum + return + } + + // Now that we've validated the checksum, we trim the msgBuf to only RGs. + // Skip over the BODYSTART, record group count, and record group size. + if _, err = msgBuf.Read(make([]byte, len(hdrBODYSTART)+(PackedNumSize*2))); err != nil { + return + } + // Then truncate. + msgBuf.Truncate(bodySize) + + r.RecordGroups = make([]*ResponseRecordGroup, rgCnt) + + for idx := 0; idx < rgCnt; idx++ { + rgBuf = new(bytes.Buffer) + + // The RG unmarshaler handles the record count, but we need to read it into msgBuf. + if _, err = io.CopyN(rgBuf, msgBuf, int64(PackedNumSize)); err != nil { + return + } + + b = make([]byte, PackedNumSize) + if _, err = msgBuf.Read(b); err != nil { + return + } + if _, err = rgBuf.Write(b); err != nil { + return + } + rgSize = UnpackInt(b) + + if _, err = io.CopyN(rgBuf, msgBuf, int64(rgSize)); err != nil { + return + } + + r.RecordGroups[idx] = new(ResponseRecordGroup) + if err = r.RecordGroups[idx].UnmarshalBinary(rgBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + + return +} + +// getIdx is a NOOP for Messages, but is used for Model conformance. +func (r *Response) getIdx() (idx int) { + return +} + +// getRecordGroups returns the RecordGroups in this Message. +func (r *Response) getRecordGroups() (recordGroups []RecordGroup) { + + recordGroups = make([]RecordGroup, len(r.RecordGroups)) + for idx, rg := range r.RecordGroups { + recordGroups[idx] = rg + } + + return +} diff --git a/funcs_responserecord.go b/funcs_responserecord.go new file mode 100644 index 0000000..d71b7eb --- /dev/null +++ b/funcs_responserecord.go @@ -0,0 +1,393 @@ +package wireproto + +import ( + `bytes` + `fmt` + `io` + `strings` +) + +// GetParent returns the parent RecordGroup. +func (r *ResponseRecord) GetParent() (rg RecordGroup) { + + rg = r.parent + + return +} + +// MarshalBinary renders a ResponseRecord into a byte-packed format. +func (r *ResponseRecord) MarshalBinary() (data []byte, err error) { + + var b []byte + var recSize int + var buf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + // KVP Count + if _, err = buf.Write(PackInt(len(r.Pairs))); err != nil { + return + } + for _, i := range r.Pairs { + recSize += i.Size() + } + // KVP Size + if _, err = buf.Write(PackInt(recSize)); err != nil { + return + } + // Original/Request Size + if _, err = buf.Write(PackInt(r.OriginalRecord.Size())); err != nil { + return + } + + for _, i := range r.Pairs { + if b, err = i.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + if r.OriginalRecord != nil { + if b, err = r.OriginalRecord.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (r *ResponseRecord) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentRec) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (r *ResponseRecord) ModelCustom(indent, sep string, level uint) (out string) { + + var size int + var sb strings.Builder + + for _, p := range r.Pairs { + size += p.Size() + } + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(len(r.Pairs))))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Field/Value Count (%d)\n", len(r.Pairs))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(size)))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Size (%d)\n", size)) + // Request Record Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(r.OriginalRecord.Size())))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Request Record Size (%d)\n", r.OriginalRecord.Size())) + + // VALUES + for idx, p := range r.Pairs { + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString( + fmt.Sprintf( + "// Record Group %d, Record %d, Field/Value %d (%d bytes)\n", + r.rgIdx+1, r.rIdx+1, idx+1, p.Size(), + ), + ) + sb.WriteString(p.ModelCustom(indent, sep, level+1)) + } + + // REQUEST + sb.WriteString(strings.Repeat(indent, int(level))) + if r.OriginalRecord == nil { + sb.WriteString("// (No Original Request Record Attached)\n") + } else { + sb.WriteString( + fmt.Sprintf("// Record Group %d, Record %d (REQUEST RECORD) (%d bytes)\n", r.rgIdx+1, r.rIdx+1, r.OriginalRecord.Size()), + ) + sb.WriteString(r.OriginalRecord.ModelCustom(indent, sep, level+1)) + } + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *ResponseRecord) Resolve() { + for idx, i := range r.Pairs { + i.parent = r + i.rgIdx = r.rgIdx + i.rIdx = idx + i.fvpidx = idx + // KVP have no Resolve() method. + } +} + +/* + Size returns the ResponseRecord's calculated size (in bytes) and updates the size field if 0. + + Note that it *includes* the size of ResponseRecord.OriginalRecord and its allocator. +*/ +func (r *ResponseRecord) Size() (size int) { + + if r == nil { + return + } + + // Count and Size uint32s, plus Size for response record. + size += PackedNumSize * 3 + + for _, p := range r.Pairs { + size += p.Size() + } + + if r.OriginalRecord != nil { + size += r.OriginalRecord.Size() + } + + if r.common == nil { + r.common = new(common) + } + + r.size = uint32(size) + + return +} + +/* + SizeNoResp is like Size but does not include the size of the ResponseRecord.OriginalRecord nor its allocator. + + It does *not* update the internal size field. +*/ +func (r *ResponseRecord) SizeNoResp() (size int) { + + if r == nil { + return + } + + // Count and Size uint32s + size += PackedNumSize * 2 + + for _, p := range r.Pairs { + size += p.Size() + } + + return +} + +// ToMap returns a slice of FVP maps for this Record. +func (r *ResponseRecord) ToMap() (m []map[string]interface{}) { + + m = make([]map[string]interface{}, len(r.Pairs)) + for idx, p := range r.Pairs { + m[idx] = p.ToMap() + } + + return +} + +// UnmarshalBinary populates a ResponseRecord from packed bytes. +func (r *ResponseRecord) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var cnt, size, reqSize int + var fvpNmSize, fvpValSize int + var fvpBuf *bytes.Buffer + var recBuf *bytes.Buffer = new(bytes.Buffer) + var origBuf *bytes.Buffer = new(bytes.Buffer) + var buf *bytes.Reader = bytes.NewReader(data) + + if r == nil { + *r = ResponseRecord{} + } + if r.common == nil { + r.common = new(common) + } + r.size = 0 + + // FVP count. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + cnt = UnpackInt(b) + + // Size of record. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + size = UnpackInt(b) + + // Size of original record. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + reqSize = UnpackInt(b) + + // And split out the FVPs and original record. + if _, err = io.CopyN(recBuf, buf, int64(size)); err != nil { + return + } + if _, err = io.CopyN(origBuf, buf, int64(reqSize)); err != nil { + return + } + + r.Pairs = make([]*FieldValuePair, cnt) + + for idx := 0; idx < cnt; idx++ { + fvpBuf = new(bytes.Buffer) + + // Field name size + b = make([]byte, PackedNumSize) + if _, err = recBuf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpNmSize = UnpackInt(b) + + // Field value size + b = make([]byte, PackedNumSize) + if _, err = recBuf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpValSize = UnpackInt(b) + + if _, err = io.CopyN(fvpBuf, recBuf, int64(fvpNmSize+fvpValSize)); err != nil { + return + } + + r.Pairs[idx] = new(FieldValuePair) + if err = r.Pairs[idx].UnmarshalBinary(fvpBuf.Bytes()); err != nil { + return + } + } + + if reqSize != 0 { + r.OriginalRecord = new(RequestRecord) + if err = r.OriginalRecord.UnmarshalBinary(origBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + + return +} + +// getFvps returns this Record's FVP. +func (r *ResponseRecord) getFvps() (fvp []FVP) { + + fvp = make([]FVP, len(r.Pairs)) + for idx, p := range r.Pairs { + fvp[idx] = p + } + + return +} + +// getIdx returns the Record index in the parent RecordGroup. +func (r *ResponseRecord) getIdx() (idx int) { + + idx = r.rIdx + + return +} + +/* + recToFVPs returns a slice of FieldValuePair from a Record. Mostly used for ResponseFieldValuePair.OriginalRecord. + + data should be a Record data structure. +*/ +func (r *ResponseRecord) recToFVPs(data []byte) (fvps []*FieldValuePair, err error) { + + var b []byte + var fvpCnt, recSize int + var fvpNmSize, fvpValSize int + var fvpBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + + // Number of FVPs + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + fvpCnt = UnpackInt(b) + fvps = make([]*FieldValuePair, fvpCnt) + + // Size of record + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + recSize = UnpackInt(b) + + // And create a new buffer from the remainder. + b = make([]byte, recSize) + if _, err = buf.Read(b); err != nil { + return + } + buf = bytes.NewReader(b) + + for idx := 0; idx < fvpCnt; idx++ { + fvpBuf = new(bytes.Buffer) + + // Get the fvp name size + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpNmSize = UnpackInt(b) + + // Get the fvp value size + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + fvpValSize = UnpackInt(b) + + b = make([]byte, fvpNmSize+fvpValSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = fvpBuf.Write(b); err != nil { + return + } + + fvps[idx] = new(FieldValuePair) + if err = fvps[idx].UnmarshalBinary(fvpBuf.Bytes()); err != nil { + return + } + } + + return +} diff --git a/funcs_responserecordgroup.go b/funcs_responserecordgroup.go new file mode 100644 index 0000000..2e0a03f --- /dev/null +++ b/funcs_responserecordgroup.go @@ -0,0 +1,251 @@ +package wireproto + +import ( + `bytes` + `fmt` + `io` + `strings` +) + +// GetParent returns this RecordGroup's Message. +func (r *ResponseRecordGroup) GetParent() (msg Message) { + + msg = r.parent + + return +} + +// MarshalBinary renders a ResponseRecordGroup into a byte-packed format. +func (r *ResponseRecordGroup) MarshalBinary() (data []byte, err error) { + + var b []byte + var rgSize int + var buf *bytes.Buffer = new(bytes.Buffer) + + _ = r.Size() + + for _, i := range r.Records { + rgSize += i.Size() + } + + // Count + if _, err = buf.Write(PackInt(len(r.Records))); err != nil { + return + } + // Size + if _, err = buf.Write(PackInt(rgSize)); err != nil { + return + } + + for _, i := range r.Records { + if b, err = i.MarshalBinary(); err != nil { + return + } + if _, err = buf.Write(b); err != nil { + return + } + } + + data = buf.Bytes() + + return +} + +// Model returns an indented string representation of the model. +func (r *ResponseRecordGroup) Model() (out string) { + + out = r.ModelCustom(IndentChars, SeparatorChars, indentRG) + + return +} + +// ModelCustom is like Model with user-defined formatting. +func (r *ResponseRecordGroup) ModelCustom(indent, sep string, level uint) (out string) { + + var sb strings.Builder + var size int + + for _, rec := range r.Records { + size += rec.Size() + } + + // Count + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(len(r.Records))))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Count (%d)\n", len(r.Records))) + // Size + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString(fmt.Sprintf("%x", PackUint32(uint32(size)))) + sb.WriteString(sep) + sb.WriteString(fmt.Sprintf("// Record Group Size (%d)\n", size)) + + // VALUES + for idx, rec := range r.Records { + sb.WriteString(strings.Repeat(indent, int(level))) + sb.WriteString( + fmt.Sprintf( + "// Record Group %d, Record %d (%d bytes)\n", + r.rgIdx+1, idx+1, rec.Size(), + ), + ) + sb.WriteString(rec.ModelCustom(indent, sep, level+1)) + } + + out = sb.String() + + return +} + +// Resolve associates children with parents. +func (r *ResponseRecordGroup) Resolve() { + for idx, i := range r.Records { + i.parent = r + i.rgIdx = r.rgIdx + i.rIdx = idx + i.Resolve() + } +} + +// Size returns the ResponseRecordGroup's calculated size (in bytes) and updates the size field if 0. +func (r *ResponseRecordGroup) Size() (size int) { + + if r == nil { + return + } + + // Count and Size uint32s + size += PackedNumSize * 2 + + for _, p := range r.Records { + size += p.Size() + } + + if r.common == nil { + r.common = new(common) + } + + r.common.size = uint32(size) + + return +} + +// ToMap returns a slice of slice of FVP maps for this RecordGroup. +func (r *ResponseRecordGroup) ToMap() (m [][]map[string]interface{}) { + + m = make([][]map[string]interface{}, len(r.Records)) + for idx, rec := range r.Records { + m[idx] = rec.ToMap() + } + + return +} + +// UnmarshalBinary populates a ResponseRecordGroup from packed bytes. +func (r *ResponseRecordGroup) UnmarshalBinary(data []byte) (err error) { + + if data == nil || len(data) == 0 { + return + } + + var b []byte + var cnt, size int + var recSize int + var recBuf *bytes.Buffer + var buf *bytes.Reader = bytes.NewReader(data) + + if r == nil { + *r = ResponseRecordGroup{} + } + if r.common == nil { + r.common = new(common) + } + r.size = 0 + + // The record count. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + cnt = UnpackInt(b) + + // The record group size. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + size = UnpackInt(b) + + b = make([]byte, size) + if _, err = buf.Read(b); err != nil { + return + } + + // Get a new buf for the actual records. + buf = bytes.NewReader(b) + + r.Records = make([]*ResponseRecord, cnt) + + for idx := 0; idx < cnt; idx++ { + recBuf = new(bytes.Buffer) + + // We skip over the KVP count; that's handled in the record Unmarshaler. + // We *do*, however, need to save it to the recBuf. + if _, err = io.CopyN(recBuf, buf, int64(PackedNumSize)); err != nil { + return + } + + // Size of the actual record + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = recBuf.Write(b); err != nil { + return + } + recSize = UnpackInt(b) + + // And add the size of the response record. + b = make([]byte, PackedNumSize) + if _, err = buf.Read(b); err != nil { + return + } + if _, err = recBuf.Write(b); err != nil { + return + } + recSize += UnpackInt(b) + + // Then the record (and original record). + if _, err = io.CopyN(recBuf, buf, int64(recSize)); err != nil { + return + } + + r.Records[idx] = new(ResponseRecord) + if err = r.Records[idx].UnmarshalBinary(recBuf.Bytes()); err != nil { + return + } + } + + _ = r.Size() + + return +} + +// getIdx returns the RecordGroup index in the parent Message. +func (r *ResponseRecordGroup) getIdx() (idx int) { + + idx = r.rgIdx + + return +} + +// getRecords returns the Records in this RecordGroup. +func (r *ResponseRecordGroup) getRecords() (records []Record) { + + records = make([]Record, len(r.Records)) + for idx, rec := range r.Records { + records[idx] = rec + } + + return +} diff --git a/funcs_test.go b/funcs_test.go new file mode 100644 index 0000000..58addc8 --- /dev/null +++ b/funcs_test.go @@ -0,0 +1,328 @@ +package wireproto + +import ( + `bytes` + `encoding/hex` + `encoding/json` + `fmt` + "testing" + + // `github.com/davecgh/go-spew/spew` +) + +var ( + testSimpleReqEncoded []byte = []byte{ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x31, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x03, 0x04, + } + testSimpleRespEncoded []byte = []byte{ + 0x06, 0x1b, 0xce, 0xfd, 0x07, 0x20, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x10, + 0x64, 0x61, 0x74, 0x61, 0x31, 0x3c, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, + 0x64, 0x61, 0x74, 0x61, 0x3e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x31, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x32, 0x03, 0x04, + } + testMultiReqEncoded []byte = []byte{ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x41, 0x31, 0x41, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x41, 0x31, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x41, 0x31, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x31, 0x42, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x41, 0x32, 0x41, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x32, 0x41, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x41, 0x32, 0x42, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x41, 0x32, 0x42, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x42, 0x31, 0x41, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x31, 0x41, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x31, 0x42, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x31, 0x42, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x32, 0x41, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x32, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x42, 0x32, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x32, 0x42, 0x03, 0x04, + } + testMultiRespEncoded []byte = []byte{ + 0x06, 0x1b, 0xae, 0x88, 0xbe, 0xd2, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x01, 0x98, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, + 0x64, 0x61, 0x74, 0x61, 0x41, 0x31, 0x3c, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, + 0x20, 0x64, 0x61, 0x74, 0x61, 0x3e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x41, 0x31, 0x41, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x41, 0x31, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x41, 0x31, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x31, 0x42, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x00, 0x10, 0x64, 0x61, 0x74, 0x61, 0x41, 0x32, 0x3c, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, + 0x72, 0x79, 0x20, 0x64, 0x61, 0x74, 0x61, 0x3e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x41, 0x32, 0x41, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x32, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x41, 0x32, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x32, 0x42, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x64, 0x61, 0x74, 0x61, + 0x42, 0x31, 0x3c, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x64, 0x61, 0x74, + 0x61, 0x3e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x31, 0x41, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, + 0x31, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, + 0x31, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x31, 0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x1e, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x64, 0x61, + 0x74, 0x61, 0x42, 0x32, 0x3c, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x20, 0x64, + 0x61, 0x74, 0x61, 0x3e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x32, 0x41, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x42, 0x32, 0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x42, 0x32, 0x42, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x32, 0x42, 0x03, 0x04, + } +) + +const ( + testPrintModel bool = true +) + +func TestSimpleRequestMarshal(t *testing.T) { + + var err error + var b []byte + + testSimpleReq.Resolve() + + if b, err = testSimpleReq.MarshalBinary(); err != nil { + t.Fatalf("Failed marshaling request: %v", err) + } + + t.Logf("Target:\n%v", hex.EncodeToString(testSimpleReqEncoded)) + t.Logf("Marshaled:\n%v", hex.EncodeToString(b)) + if !bytes.Equal(b, testSimpleReqEncoded) { + t.Fatal("Marshaled request binary does not match") + } + + _ = b +} + +func TestSimpleRequestUnmarshal(t *testing.T) { + + var err error + var b []byte + var b2 []byte + var req *Request = new(Request) + + testSimpleReq.Resolve() + + if err = req.UnmarshalBinary(testSimpleReqEncoded); err != nil { + t.Fatalf("Error when unmarshaling: %v", err) + } + + // if !reflect.DeepEqual(req, testSimpleReq) { + // t.Fatalf("Unmarshaled request and canonical request do not match!") + // } + + // t.Log(spew.Sdump(req)) + if b, err = json.MarshalIndent(req, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + if b2, err = json.MarshalIndent(testSimpleReq, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + if !bytes.Equal(b, b2) { + // t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", hex.EncodeToString(b), hex.EncodeToString(b2)) + t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", string(b), string(b2)) + t.Fatalf("Unmarshaled request and canonical request do not match!") + } + + t.Logf(string(b)) + + _, _ = b, b2 +} + +func TestSimpleResponseMarshal(t *testing.T) { + + var err error + var b []byte + + testSimpleResp.Resolve() + + if b, err = testSimpleResp.MarshalBinary(); err != nil { + t.Fatalf("Failed marshaling request: %v", err) + } + + t.Logf("Target:\n%v", hex.EncodeToString(testSimpleRespEncoded)) + t.Logf("Marshaled:\n%v", hex.EncodeToString(b)) + if !bytes.Equal(b, testSimpleRespEncoded) { + t.Fatal("Marshaled request binary does not match") + } + + _ = b +} + +func TestSimpleResponseUnmarshal(t *testing.T) { + + var err error + var b []byte + var b2 []byte + var resp *Response = new(Response) + + testSimpleResp.Resolve() + + if err = resp.UnmarshalBinary(testSimpleRespEncoded); err != nil { + t.Fatalf("Error when unmarshaling: %v", err) + } + + // if !reflect.DeepEqual(resp, testSimpleResp) { + // t.Fatalf("Unmarshaled response and canonical response do not match!") + // } + + // t.Log(spew.Sdump(resp)) + if b, err = json.MarshalIndent(resp, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + if b2, err = json.MarshalIndent(testSimpleResp, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + if !bytes.Equal(b, b2) { + // t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", hex.EncodeToString(b), hex.EncodeToString(b2)) + t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", string(b), string(b2)) + t.Fatalf("Unmarshaled response and canonical response do not match!") + } + + t.Logf(string(b)) + + _, _ = b, b2 +} + +func TestMultiRequestMarshal(t *testing.T) { + + var err error + var b []byte + + testMultiReq.Resolve() + + if b, err = testMultiReq.MarshalBinary(); err != nil { + t.Fatalf("Failed marshaling request: %v", err) + } + + t.Logf("Target:\n%v", hex.EncodeToString(testMultiReqEncoded)) + t.Logf("Marshaled:\n%v", hex.EncodeToString(b)) + if !bytes.Equal(b, testMultiReqEncoded) { + t.Fatal("Marshaled request binary does not match") + } + + _ = b +} + +func TestMultiRequestUnmarshal(t *testing.T) { + + var err error + var b []byte + var b2 []byte + var req *Request = new(Request) + + testMultiReq.Resolve() + + if err = req.UnmarshalBinary(testMultiReqEncoded); err != nil { + t.Fatalf("Error when unmarshaling: %v", err) + } + + // if !reflect.DeepEqual(req, testMultiReq) { + // t.Fatalf("Unmarshaled request and canonical request do not match!") + // } + + // t.Log(spew.Sdump(req)) + if b, err = json.MarshalIndent(req, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + if b2, err = json.MarshalIndent(testMultiReq, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + if !bytes.Equal(b, b2) { + // t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", hex.EncodeToString(b), hex.EncodeToString(b2)) + t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", string(b), string(b2)) + t.Fatalf("Unmarshaled request and canonical request do not match!") + } + + t.Logf(string(b)) + + _, _ = b, b2 +} + +func TestMultiResponseMarshal(t *testing.T) { + + var err error + var b []byte + + testMultiResp.Resolve() + + if b, err = testMultiResp.MarshalBinary(); err != nil { + t.Fatalf("Failed marshaling request: %v", err) + } + + t.Logf("Target:\n%v", hex.EncodeToString(testMultiRespEncoded)) + t.Logf("Marshaled:\n%v", hex.EncodeToString(b)) + if !bytes.Equal(b, testMultiRespEncoded) { + t.Fatal("Marshaled request binary does not match") + } + + _ = b +} + +func TestMultiResponseUnmarshal(t *testing.T) { + + var err error + var b []byte + var b2 []byte + var resp *Response = new(Response) + + testMultiResp.Resolve() + + if err = resp.UnmarshalBinary(testMultiRespEncoded); err != nil { + t.Fatalf("Error when unmarshaling: %v", err) + } + + // if !reflect.DeepEqual(resp, testMultiResp) { + // t.Fatalf("Unmarshaled response and canonical response do not match!") + // } + + // t.Log(spew.Sdump(resp)) + if b, err = json.MarshalIndent(resp, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + if b2, err = json.MarshalIndent(testMultiResp, "", " "); err != nil { + t.Fatalf("Failed to marshal to JSON: %v", err) + } + + if !bytes.Equal(b, b2) { + // t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", hex.EncodeToString(b), hex.EncodeToString(b2)) + t.Logf("Unmarshaled JSON conversion:\n%v\nCanonical:\n%v", string(b), string(b2)) + t.Fatalf("Unmarshaled response and canonical response do not match!") + } + + t.Logf(string(b)) + + _, _ = b, b2 +} + +func TestModels(t *testing.T) { + + testSimpleReq.Resolve() + testSimpleResp.Resolve() + testMultiReq.Resolve() + testMultiResp.Resolve() + + if testPrintModel { + fmt.Println("// REQUEST (Simple)") + fmt.Println(testSimpleReq.Model()) + + fmt.Println("// RESPONSE (Simple)") + fmt.Println(testSimpleResp.Model()) + + fmt.Println("// REQUEST (Complex)") + fmt.Println(testMultiReq.Model()) + + fmt.Println("// RESPONSE (Complex)") + fmt.Println(testMultiResp.Model()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..35782b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module r00t2.io/wireproto + +go 1.23 + +require ( + github.com/google/uuid v1.6.0 + r00t2.io/goutils v1.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..05a4759 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +r00t2.io/goutils v1.7.0 h1:iQluWlkOyBwOKaK94D5QSnSMYpGKtMb/5WjefmdfHgI= +r00t2.io/goutils v1.7.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= +r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= diff --git a/test_obj_multi_req.go b/test_obj_multi_req.go new file mode 100644 index 0000000..08d404e --- /dev/null +++ b/test_obj_multi_req.go @@ -0,0 +1,66 @@ +package wireproto + +var ( + // REQUEST (Complex) + testMultiReq *Request = &Request{ + ProtocolVersion: ProtoVersion, + RecordGroups: []*RequestRecordGroup{ + &RequestRecordGroup{ + Records: []*RequestRecord{ + &RequestRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("fieldA1A"), + Value: []byte("valueA1A"), + }, + &FieldValuePair{ + Name: []byte("fieldA1B"), + Value: []byte("valueA1B"), + }, + }, + }, + &RequestRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("fieldA2A"), + Value: []byte("valueA2A"), + }, + &FieldValuePair{ + Name: []byte("fieldA2B"), + Value: []byte("valueA2B"), + }, + }, + }, + }, + }, + &RequestRecordGroup{ + Records: []*RequestRecord{ + &RequestRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("fieldB1A"), + Value: []byte("valueB1A"), + }, + &FieldValuePair{ + Name: []byte("fieldB1B"), + Value: []byte("valueB1B"), + }, + }, + }, + &RequestRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("fieldB2A"), + Value: []byte("valueB2A"), + }, + &FieldValuePair{ + Name: []byte("fieldB2B"), + Value: []byte("valueB2B"), + }, + }, + }, + }, + }, + }, + } +) diff --git a/test_obj_multi_resp.go b/test_obj_multi_resp.go new file mode 100644 index 0000000..62717a7 --- /dev/null +++ b/test_obj_multi_resp.go @@ -0,0 +1,56 @@ +package wireproto + +var ( + // RESPONSE (Complex) + testMultiResp *Response = &Response{ + Status: AsciiACK, + Checksum: 2928197330, // 0xae88bed2 + ProtocolVersion: ProtoVersion, + RecordGroups: []*ResponseRecordGroup{ + &ResponseRecordGroup{ + Records: []*ResponseRecord{ + &ResponseRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("dataA1"), + Value: []byte(""), + }, + }, + OriginalRecord: testMultiReq.RecordGroups[0].Records[0], + }, + &ResponseRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("dataA2"), + Value: []byte(""), + }, + }, + OriginalRecord: testMultiReq.RecordGroups[0].Records[1], + }, + }, + }, + &ResponseRecordGroup{ + Records: []*ResponseRecord{ + &ResponseRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("dataB1"), + Value: []byte(""), + }, + }, + OriginalRecord: testMultiReq.RecordGroups[1].Records[0], + }, + &ResponseRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("dataB2"), + Value: []byte(""), + }, + }, + OriginalRecord: testMultiReq.RecordGroups[1].Records[1], + }, + }, + }, + }, + } +) diff --git a/test_obj_simple_req.go b/test_obj_simple_req.go new file mode 100644 index 0000000..daaf632 --- /dev/null +++ b/test_obj_simple_req.go @@ -0,0 +1,26 @@ +package wireproto + +var ( + // REQUEST (Simple) + testSimpleReq *Request = &Request{ + ProtocolVersion: ProtoVersion, + RecordGroups: []*RequestRecordGroup{ + &RequestRecordGroup{ + Records: []*RequestRecord{ + &RequestRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("field1"), + Value: []byte("value1"), + }, + &FieldValuePair{ + Name: []byte("field2"), + Value: []byte("value2"), + }, + }, + }, + }, + }, + }, + } +) diff --git a/test_obj_simple_resp.go b/test_obj_simple_resp.go new file mode 100644 index 0000000..9b276db --- /dev/null +++ b/test_obj_simple_resp.go @@ -0,0 +1,25 @@ +package wireproto + +var ( + // RESPONSE (Simple) + testSimpleResp *Response = &Response{ + Status: AsciiACK, + Checksum: 3472688928, // 0xcefd0720 + ProtocolVersion: ProtoVersion, + RecordGroups: []*ResponseRecordGroup{ + &ResponseRecordGroup{ + Records: []*ResponseRecord{ + &ResponseRecord{ + Pairs: []*FieldValuePair{ + &FieldValuePair{ + Name: []byte("data1"), + Value: []byte(""), + }, + }, + OriginalRecord: testSimpleReq.RecordGroups[0].Records[0], + }, + }, + }, + }, + } +) diff --git a/types.go b/types.go new file mode 100644 index 0000000..4c899c6 --- /dev/null +++ b/types.go @@ -0,0 +1,182 @@ +package wireproto + +import ( + `encoding/xml` + + `github.com/google/uuid` +) + +/* + Fields, commonly referred to in this library as "fv", "fvpair", etc. consist of both a name and a value. + + This not intended to be a map conceptually so much as a "type" of data and the data itself, but it certainly can be represented as map-like. +*/ +type ( + // FieldName represents the name/identifier of a field. + FieldName []byte + // FieldValue is the value (data) of a specified field + FieldValue []byte +) + +type common struct { + size uint32 +} + +type ( + FVP interface { + Model + GetParent() (rec Record) + ToMap() (m map[string]interface{}) + } + Record interface { + Model + GetParent() (rg RecordGroup) + ToMap() (m []map[string]interface{}) + getFvps() (fvp []FVP) + } + RecordGroup interface { + Model + GetParent() (msg Message) + ToMap() (m [][]map[string]interface{}) + getRecords() (records []Record) + } + Message interface { + Model + ToMap() (m [][][]map[string]interface{}) + getRecordGroups() (recordGroups []RecordGroup) + } + Model interface { + Model() (out string) + ModelCustom(indent, sep string, level uint) (out string) + getIdx() (idx int) + } +) + +/* + Request contains a Request message as sent to a remote target. It conforms to Message. + + Either end ("client" or "server") may send a request, but it should represent the beginning of a complete/new transaction (a Message). +*/ +type Request struct { + XMLName xml.Name `xml:"request" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + /* + Checksum is a CRC32 (see specification) checksum of the message. + + It is optional (but recommended) for requests. + */ + Checksum *uint32 `json:"cksum,omitempty" xml:"cksum,attr,omitempty" yaml:"Checksum,omitempty" toml:"Checksum,omitempty" validate:"omitempty"` + // ProtocolVersion specifies the specification version of this message. + ProtocolVersion uint32 `json:"proto_ver" xml:"protoVer,attr" yaml:"Protocol Version" toml:"ProtocolVersion" validate:"required"` + /* + RecordGroups contains grouped sets (RequestRecordGroup) of RequestRecord. + At least one is required. + + Most implementations are likely to use only a single RequestRecordGroup in RecordGroups, + but multiple may be included. They are assumed to be seperate contexts/queries/commands/etc. if + multiple groups are included. + */ + RecordGroups []*RequestRecordGroup `json:"rg" xml:"recordGroups>recordGroup" yaml:"Record Groups" toml:"RecordGroups" validate:"required,dive"` + resp *ResponseRecord // only if part of a Response. + connId uuid.UUID +} + +// RequestRecordGroup contains one or more related RequestRecord. It conforms to RecordGroup. +type RequestRecordGroup struct { + XMLName xml.Name `xml:"recordGroup" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + // Records contains the sets of one or more related RequestRecord. + Records []*RequestRecord `json:"rec" xml:"records>record" yaml:"Records" toml:"Records" validate:"required,dive"` + connId uuid.UUID + parent *Request + rgIdx int +} + +/* + RequestRecord contains a record of one or more related FieldValuePair. It conforms to Record. + + It is designed such that a single RequestRecord is responded with a single ResponseRecord. +*/ +type RequestRecord struct { + XMLName xml.Name `xml:"record" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + // Pairs contains one or more related FieldValuePair. + Pairs []*FieldValuePair `json:"fvp" xml:"fvPairs>fvPair" yaml:"Field/Value Pairs" toml:"FieldValuePairs" validate:"required,dive"` + connId uuid.UUID + parent *RequestRecordGroup + rgIdx int + rIdx int +} + +// Response contains an entire response message to a Request. It conforms to Message. +type Response struct { + XMLName xml.Name `xml:"response" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + /* + Status is a short identifier of the status for the Request as executed. + It is the very first byte on the wire to allow the remote end to easily check whether an error has occurred. + (Detailed error messages should be contained in the RecordGroups.) + + TODO: custom type, marshal/unmarshalers? + */ + Status uint8 `json:"status" xml:"status,attr" yaml:"Status" toml:"Status" validate:"required"` + /* + Checksum is a CRC32 (see specification) checksum of the message. + + It is required for responses. + */ + Checksum uint32 `json:"cksum" xml:"cksum,attr" yaml:"Checksum" toml:"Checksum" validate:"required"` + // ProtocolVersion specifies the specification version of this message. + ProtocolVersion uint32 `json:"proto_ver" xml:"protoVer,attr" yaml:"Protocol Version" toml:"ProtocolVersion" validate:"required"` + /* + RecordGroups contains grouped sets (ResponseRecordGroup) of ResponseRecord. + At least one is required. + + Most implementations are likely to use only a single ResponseRecordGroup in RecordGroups, + but multiple may be included. They are assumed to be seperate contexts/queries/commands/etc. if + multiple groups are included. + */ + RecordGroups []*ResponseRecordGroup `json:"rg" xml:"recordGroups>recordGroup" yaml:"Record Groups"` +} + +// ResponseRecordGroup contains related ResponseRecord objects. It conforms to RecordGroup. +type ResponseRecordGroup struct { + XMLName xml.Name `xml:"recordGroup" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + // Records contains one or more related ResponseRecord. + Records []*ResponseRecord `json:"rec" xml:"records>record" yaml:"Records" toml:"Records" validate:"required,dive"` + parent *Response + rgIdx int +} + +// ResponseRecord contains the response to a single RequestRecord. It conforms to Record. +type ResponseRecord struct { + XMLName xml.Name `xml:"record" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + Pairs []*FieldValuePair `json:"fvp" xml:"fvPairs>fvPair" yaml:"Field/Value Pairs" toml:"FieldValuePairs" validate:"required,dive"` + /* + OriginalRecord contains the associcated RequestRecord. + */ + OriginalRecord *RequestRecord `json:"orig_req" xml:"originalReq" yaml:"Original Request" toml:"OriginalRequest"` + parent *ResponseRecordGroup + rgIdx int + rIdx int +} + +// FieldValuePair contains a single "key" (identifier) and value. It is found in both a RequestRecord and a ResponseRecord. It conforms to FVP. +type FieldValuePair struct { + XMLName xml.Name `xml:"fvPair" json:"-" yaml:"-" toml:"-" validate:"-"` + *common + // Name should be treated as a "type" or "identifier" for the data in Value. + Name FieldName `json:"name,omitempty" xml:"name,attr,omitempty" yaml:"Name,omitempty" toml:"Name,omitempty" validate:"omitempty"` + /* + Value containts arbitrary data. + Content, validation/verification, parsing, etc. of the data is left to downstream implementations and is out of spec for this protocol. + */ + Value FieldValue `json:"value,omitempty" xml:"value,chardata,omitempty" yaml:"Value,omitempty" toml:"Value,omitempty" validate:"omitempty"` + connId *uuid.UUID + parent Record + rgIdx int + rIdx int + fvpidx int +}