From 174634a330b9e337ad1b028497cc13fa3edb1d27 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Apr 2025 15:42:15 -0400 Subject: [PATCH 001/132] METADAPT-B can now send very large messages --- metadapta_test.go | 1 - metadaptb.go | 12 +++++------- metadaptb_test.go | 15 +++------------ 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/metadapta_test.go b/metadapta_test.go index 1b2bc44..0ecebe2 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -78,7 +78,6 @@ func TestConnA(test *testing.T) { test.Fatal("CLIENT wrong error:", err) } test.Log("CLIENT done") - // TODO test error from trans/connection closed by other side } func TestEncodeMessageA(test *testing.T) { diff --git a/metadaptb.go b/metadaptb.go index f8bed04..52e98fc 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -85,23 +85,21 @@ type Stream interface { } func encodeMessageB(writer io.Writer, method uint16, data []byte) error { - buffer := make([]byte, 4 + len(data)) + buffer := make([]byte, 10 + len(data)) tape.EncodeI16(buffer[:2], method) - length, ok := tape.U16CastSafe(len(data)) - if !ok { return ErrPayloadTooLarge } - tape.EncodeI16(buffer[2:4], length) - copy(buffer[4:], data) + tape.EncodeI64(buffer[2:10], uint64(len(data))) + copy(buffer[10:], data) _, err := writer.Write(buffer) return err } func decodeMessageB(reader io.Reader) (uint16, []byte, error) { - headerBuffer := [4]byte { } + headerBuffer := [10]byte { } _, err := io.ReadFull(reader, headerBuffer[:]) if err != nil { return 0, nil, err } method, err := tape.DecodeI16[uint16](headerBuffer[:2]) if err != nil { return 0, nil, err } - length, err := tape.DecodeI16[uint16](headerBuffer[2:4]) + length, err := tape.DecodeI64[uint64](headerBuffer[2:10]) if err != nil { return 0, nil, err } payloadBuffer := make([]byte, int(length)) _, err = io.ReadFull(reader, payloadBuffer) diff --git a/metadaptb_test.go b/metadaptb_test.go index 416f5fd..dc0e8d4 100644 --- a/metadaptb_test.go +++ b/metadaptb_test.go @@ -11,7 +11,7 @@ func TestEncodeMessageB(test *testing.T) { payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } err := encodeMessageB(buffer, 0x6B12, payload) correct := []byte { - 0x6B, 0x12, + 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, } @@ -23,18 +23,9 @@ func TestEncodeMessageB(test *testing.T) { } } -func TestEncodeMessageBErr(test *testing.T) { - buffer := new(bytes.Buffer) - payload := make([]byte, 0x10000) - err := encodeMessageB(buffer, 0x6B12, payload) - if !errors.Is(err, ErrPayloadTooLarge) { - test.Fatalf("wrong error: %v", err) - } -} - func TestDecodeMessageB(test *testing.T) { method, payload, err := decodeMessageB(bytes.NewReader([]byte { - 0x6B, 0x12, + 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, })) @@ -52,7 +43,7 @@ func TestDecodeMessageB(test *testing.T) { func TestDecodeMessageBErr(test *testing.T) { _, _, err := decodeMessageB(bytes.NewReader([]byte { - 0x6B, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6B, 0x12, 0x01, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, })) -- 2.46.1 From 1ac0ed51c7769e166ab7c70794ccf6ff2273002a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Apr 2025 16:07:20 -0400 Subject: [PATCH 002/132] METADAPT-B supports setting a message length limit --- connection.go | 6 ++++++ metadaptb.go | 32 ++++++++++++++++++++++++++------ metadaptb_test.go | 17 +++++++++++++---- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/connection.go b/connection.go index adefbdf..11f6fe9 100644 --- a/connection.go +++ b/connection.go @@ -3,6 +3,8 @@ package hopp import "net" // import "time" +const defaultSizeLimit = 1024 * 1024 // 1 megabyte + // Conn is a HOPP connection. type Conn interface { // Close closes the connection. Any blocked operations on the connection @@ -19,6 +21,10 @@ type Conn interface { // AcceptTrans accepts a transaction from the other party. This must // be called in a loop to avoid the connection locking up. AcceptTrans() (Trans, error) + + // SetSizeLimit sets a limit (in bytes) for how large messages can be. + // By default, this limit is 1 megabyte. + SetSizeLimit(limit int) } // Trans is a HOPP transaction. diff --git a/metadaptb.go b/metadaptb.go index 52e98fc..1c6eb9f 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -8,6 +8,7 @@ import "git.tebibyte.media/sashakoshka/hopp/tape" // B implements METADAPT-B over a multiplexed stream-oriented transport such as // QUIC. type b struct { + sizeLimit int underlying MultiConn } @@ -15,6 +16,7 @@ type b struct { // oriented transport such as TCP or UNIX domain stream sockets. func AdaptB(underlying MultiConn) Conn { return &b { + sizeLimit: defaultSizeLimit, underlying: underlying, } } @@ -34,16 +36,28 @@ func (this *b) RemoteAddr() net.Addr { func (this *b) OpenTrans() (Trans, error) { stream, err := this.underlying.OpenStream() if err != nil { return nil, err } - return transB { underlying: stream }, nil + return this.newTrans(stream), nil } func (this *b) AcceptTrans() (Trans, error) { stream, err := this.underlying.AcceptStream(context.Background()) if err != nil { return nil, err } - return transB { underlying: stream }, nil + return this.newTrans(stream), nil +} + +func (this *b) SetSizeLimit(limit int) { + this.sizeLimit = limit +} + +func (this *b) newTrans(underlying Stream) transB { + return transB { + sizeLimit: this.sizeLimit, + underlying: underlying, + } } type transB struct { + sizeLimit int underlying Stream } @@ -56,11 +70,11 @@ func (trans transB) ID() int64 { } func (trans transB) Send(method uint16, data []byte) error { - return encodeMessageB(trans.underlying, method, data) + return encodeMessageB(trans.underlying, trans.sizeLimit, method, data) } func (trans transB) Receive() (uint16, []byte, error) { - return decodeMessageB(trans.underlying) + return decodeMessageB(trans.underlying, trans.sizeLimit) } // MultiConn represens a multiplexed stream-oriented transport for use in @@ -84,7 +98,10 @@ type Stream interface { ID() int64 } -func encodeMessageB(writer io.Writer, method uint16, data []byte) error { +func encodeMessageB(writer io.Writer, sizeLimit int, method uint16, data []byte) error { + if len(data) > sizeLimit { + return ErrPayloadTooLarge + } buffer := make([]byte, 10 + len(data)) tape.EncodeI16(buffer[:2], method) tape.EncodeI64(buffer[2:10], uint64(len(data))) @@ -93,7 +110,7 @@ func encodeMessageB(writer io.Writer, method uint16, data []byte) error { return err } -func decodeMessageB(reader io.Reader) (uint16, []byte, error) { +func decodeMessageB(reader io.Reader, sizeLimit int) (uint16, []byte, error) { headerBuffer := [10]byte { } _, err := io.ReadFull(reader, headerBuffer[:]) if err != nil { return 0, nil, err } @@ -101,6 +118,9 @@ func decodeMessageB(reader io.Reader) (uint16, []byte, error) { if err != nil { return 0, nil, err } length, err := tape.DecodeI64[uint64](headerBuffer[2:10]) if err != nil { return 0, nil, err } + if length > uint64(sizeLimit) { + return 0, nil, ErrPayloadTooLarge + } payloadBuffer := make([]byte, int(length)) _, err = io.ReadFull(reader, payloadBuffer) if err != nil { return 0, nil, err } diff --git a/metadaptb_test.go b/metadaptb_test.go index dc0e8d4..add8043 100644 --- a/metadaptb_test.go +++ b/metadaptb_test.go @@ -9,7 +9,7 @@ import "testing" func TestEncodeMessageB(test *testing.T) { buffer := new(bytes.Buffer) payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } - err := encodeMessageB(buffer, 0x6B12, payload) + err := encodeMessageB(buffer, defaultSizeLimit, 0x6B12, payload) correct := []byte { 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, @@ -23,12 +23,21 @@ func TestEncodeMessageB(test *testing.T) { } } +func TestEncodeMessageBErr(test *testing.T) { + buffer := new(bytes.Buffer) + payload := make([]byte, 0x10000) + err := encodeMessageB(buffer, 255, 0x6B12, payload) + if !errors.Is(err, ErrPayloadTooLarge) { + test.Fatalf("wrong error: %v", err) + } +} + func TestDecodeMessageB(test *testing.T) { method, payload, err := decodeMessageB(bytes.NewReader([]byte { 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - })) + }), defaultSizeLimit) if err != nil { test.Fatal(err) } @@ -43,10 +52,10 @@ func TestDecodeMessageB(test *testing.T) { func TestDecodeMessageBErr(test *testing.T) { _, _, err := decodeMessageB(bytes.NewReader([]byte { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6B, 0x12, + 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - })) + }), defaultSizeLimit) if !errors.Is(err, io.ErrUnexpectedEOF) { test.Fatalf("wrong error: %v", err) } -- 2.46.1 From 5c28510342558c68952dba25eea5a48e1df4a559 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Apr 2025 21:04:45 -0400 Subject: [PATCH 003/132] Add new METADAPT protocol specifications from #2 --- design/protocol.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index ca37998..48e4c9b 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -129,8 +129,9 @@ on the same machine. Both protocols are supported through METADAPT. ## Message and Transaction Demarcation Protocol (METADAPT) The Message and Transaction Demarcation Protocol is used to break one or more reliable data streams into transactions, which are broken down further into -messages. A message, as well as its associated metadata (length, transaction, -method, etc.) together is referred to as METADAPT Message Block (MMB). +messages. The representation of a message (or a part thereof) on the protocol, +including its associated metadata (length, transaction, method, etc.) is +referred to as METADAPT Message Block (MMB). For transports that offer multiple multiplexed data streams that can be created and destroyed on-demand (such as QUIC) each stream is used as a transaction. If @@ -145,8 +146,12 @@ METADAPT-A requires a transport which offers a single full-duplex data stream that persists for the duration of the connection. All transactions are multiplexed onto this single stream. Each MMB contains a 12-octet long header, with the transaction ID, then the method, and then the payload size (in octets). -The transaction ID is encoded as an I64, and the method and payload size are -both encoded as U16s. The remainder of the message is the payload. Since each +The transaction ID is encoded as an I64, the method is encoded as a U16 and the +and payload size is encoded as a U64. Only the 63 least significant bits of the +payload size describe the actual size, the most significant bit controlling +chunking. See the section on chunking for more information. + +The remainder of the message is the payload. Since each MMB is self-describing, they are sent sequentially with no gaps in-between them. Transactions "open" when the first message with a given transaction ID is sent. @@ -162,13 +167,25 @@ used up, the connection must fail. Don't worry about this though, because the sun will have expanded to swallow earth by then. Your connection will not last that long. +#### Message Chunking + +The most significant bit of the payload size field of an MMB is called the Chunk +Control Bit (CCB). If the CCB of a given MMB is zero, the represented message is +interpreted as being self-contained and the data is processed immediately. If +the CCB is one, the message is interpreted as being chunked, with the data of +the current MMB being the first chunk. The data of further MMBs sent along the +transaction will be appended to the message until an MMB is read with a zero +CCB, in which case the MMB will be the last chunk and any more MMBs will be +interpreted as normal. + ### METADAPT-B METADAPT-B requires a transport which offers multiple multiplexed full-duplex data streams per connection that can be created and destroyed on-demand. Each data stream is used as an individual transaction. Each MMB contains a 4-octet -long header with the method and then the payload size (in octets) both encoded -as U16s. The remainder of the message is the payload. Since each MMB is -self-describing, they are sent sequentially with no gaps in-between them. +long header with the method and then the payload size (in octets) encoded as a +U16 and U64 respectively. The remainder of the message is the payload. Since +each MMB is self-describing, they are sent sequentially with no gaps in-between +them. The ID of any transaction will reflect the ID of its corresponding stream. The lifetime of the transaction is tied to the lifetime of the stream, that is to -- 2.46.1 From 4eae69dc94fe38c975733c8007834aaa2daf3664 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Apr 2025 21:08:41 -0400 Subject: [PATCH 004/132] Add ReceiveReader to Transaction interface --- connection.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/connection.go b/connection.go index 11f6fe9..005d734 100644 --- a/connection.go +++ b/connection.go @@ -1,5 +1,6 @@ package hopp +import "io" import "net" // import "time" @@ -43,4 +44,7 @@ type Trans interface { Send(method uint16, data []byte) error // Receive receives a message. Receive() (method uint16, data []byte, err error) + // ReceiveReader receives a message as an [io.Reader]. Any reader + // previously opened through this function will be discarded. + ReceiveReader() (method uint16, size int64, data io.Reader, err error) } -- 2.46.1 From b07cdf088a6538625fc13828ca0f65b54595d1c7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 6 Apr 2025 11:25:12 -0400 Subject: [PATCH 005/132] design: State support for TCP/TLS --- design/protocol.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index 48e4c9b..550cc86 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -123,8 +123,9 @@ be of the same size. ## Transports A transport is a protocol that HOPP connections can run on top of. HOPP currently supports the QUIC transport protocol for communicating between -machines, and UNIX domain sockets for quicker communication among applications -on the same machine. Both protocols are supported through METADAPT. +machines, TCP/TLS for legacy systems that do not support QUIC, and UNIX domain +sockets for faster communication among applications on the same machine. Both +protocols are supported through METADAPT. ## Message and Transaction Demarcation Protocol (METADAPT) The Message and Transaction Demarcation Protocol is used to break one or more -- 2.46.1 From fe8f2fc3ea18c8c29037b8205a185a841d050b69 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 6 Apr 2025 11:25:28 -0400 Subject: [PATCH 006/132] Do not require METADAPT to return a message length when getting a reader --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 005d734..56154a7 100644 --- a/connection.go +++ b/connection.go @@ -46,5 +46,5 @@ type Trans interface { Receive() (method uint16, data []byte, err error) // ReceiveReader receives a message as an [io.Reader]. Any reader // previously opened through this function will be discarded. - ReceiveReader() (method uint16, size int64, data io.Reader, err error) + ReceiveReader() (method uint16, data io.Reader, err error) } -- 2.46.1 From f4f8039fa025c43255e32655fdc344c9b7ce090d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 6 Apr 2025 14:17:39 -0400 Subject: [PATCH 007/132] Support getting a reader for a message in METADAPT-B --- metadaptb.go | 83 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/metadaptb.go b/metadaptb.go index 1c6eb9f..f78bb26 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -5,6 +5,8 @@ import "net" import "context" import "git.tebibyte.media/sashakoshka/hopp/tape" +// TODO: change size limit to be int64 + // B implements METADAPT-B over a multiplexed stream-oriented transport such as // QUIC. type b struct { @@ -12,8 +14,8 @@ type b struct { underlying MultiConn } -// AdaptB returns a connection implementing METADAPT-B over a singular stream- -// oriented transport such as TCP or UNIX domain stream sockets. +// AdaptB returns a connection implementing METADAPT-B over a multiplexed +// stream-oriented transport such as QUIC. func AdaptB(underlying MultiConn) Conn { return &b { sizeLimit: defaultSizeLimit, @@ -49,8 +51,8 @@ func (this *b) SetSizeLimit(limit int) { this.sizeLimit = limit } -func (this *b) newTrans(underlying Stream) transB { - return transB { +func (this *b) newTrans(underlying Stream) *transB { + return &transB { sizeLimit: this.sizeLimit, underlying: underlying, } @@ -59,22 +61,49 @@ func (this *b) newTrans(underlying Stream) transB { type transB struct { sizeLimit int underlying Stream + currentData io.Reader } -func (trans transB) Close() error { - return trans.underlying.Close() +func (this *transB) Close() error { + return this.underlying.Close() } -func (trans transB) ID() int64 { - return trans.underlying.ID() +func (this *transB) ID() int64 { + return this.underlying.ID() } -func (trans transB) Send(method uint16, data []byte) error { - return encodeMessageB(trans.underlying, trans.sizeLimit, method, data) +func (this *transB) Send(method uint16, data []byte) error { + return encodeMessageB(this.underlying, this.sizeLimit, method, data) } -func (trans transB) Receive() (uint16, []byte, error) { - return decodeMessageB(trans.underlying, trans.sizeLimit) +func (this *transB) Receive() (uint16, []byte, error) { + // get a reader for the next message + method, size, data, err := this.receiveReader() + if err != nil { return 0, nil, err } + // read the entire thing + payloadBuffer := make([]byte, int(size)) + _, err = io.ReadFull(data, payloadBuffer) + if err != nil { return 0, nil, err } + // we have used up the reader by now so we can forget it exists + this.currentData = nil + return method, payloadBuffer, nil +} + +func (this *transB) ReceiveReader() (uint16, io.Reader, error) { + method, _, data, err := this.receiveReader() + return method, data, err +} + +func (this *transB) receiveReader() (uint16, int64, io.Reader, error) { + // decode the message + method, size, data, err := decodeMessageB(this.underlying, this.sizeLimit) + if err != nil { return 0, 0, nil, err } + // discard current reader if there is one + if this.currentData == nil { + io.Copy(io.Discard, this.currentData) + } + this.currentData = data + return method, size, data, nil } // MultiConn represens a multiplexed stream-oriented transport for use in @@ -110,19 +139,27 @@ func encodeMessageB(writer io.Writer, sizeLimit int, method uint16, data []byte) return err } -func decodeMessageB(reader io.Reader, sizeLimit int) (uint16, []byte, error) { +func decodeMessageB( + reader io.Reader, + sizeLimit int, +) ( + method uint16, + size int64, + data io.Reader, + err error, +) { headerBuffer := [10]byte { } - _, err := io.ReadFull(reader, headerBuffer[:]) - if err != nil { return 0, nil, err } - method, err := tape.DecodeI16[uint16](headerBuffer[:2]) - if err != nil { return 0, nil, err } + _, err = io.ReadFull(reader, headerBuffer[:]) + if err != nil { return 0, 0, nil, err } + method, err = tape.DecodeI16[uint16](headerBuffer[:2]) + if err != nil { return 0, 0, nil, err } length, err := tape.DecodeI64[uint64](headerBuffer[2:10]) - if err != nil { return 0, nil, err } + if err != nil { return 0, 0, nil, err } if length > uint64(sizeLimit) { - return 0, nil, ErrPayloadTooLarge + return 0, 0, nil, ErrPayloadTooLarge } - payloadBuffer := make([]byte, int(length)) - _, err = io.ReadFull(reader, payloadBuffer) - if err != nil { return 0, nil, err } - return method, payloadBuffer, nil + return method, int64(length), &io.LimitedReader { + R: reader, + N: int64(length), + }, nil } -- 2.46.1 From db10355c84ed3cefdd8427b445b4e6df9263a3d2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 6 Apr 2025 14:19:39 -0400 Subject: [PATCH 008/132] Change the size limit type to an int64 --- connection.go | 4 ++-- metadaptb.go | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/connection.go b/connection.go index 56154a7..a2f2080 100644 --- a/connection.go +++ b/connection.go @@ -4,7 +4,7 @@ import "io" import "net" // import "time" -const defaultSizeLimit = 1024 * 1024 // 1 megabyte +const defaultSizeLimit int64 = 1024 * 1024 // 1 megabyte // Conn is a HOPP connection. type Conn interface { @@ -25,7 +25,7 @@ type Conn interface { // SetSizeLimit sets a limit (in bytes) for how large messages can be. // By default, this limit is 1 megabyte. - SetSizeLimit(limit int) + SetSizeLimit(limit int64) } // Trans is a HOPP transaction. diff --git a/metadaptb.go b/metadaptb.go index f78bb26..18776da 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -5,12 +5,10 @@ import "net" import "context" import "git.tebibyte.media/sashakoshka/hopp/tape" -// TODO: change size limit to be int64 - // B implements METADAPT-B over a multiplexed stream-oriented transport such as // QUIC. type b struct { - sizeLimit int + sizeLimit int64 underlying MultiConn } @@ -47,7 +45,7 @@ func (this *b) AcceptTrans() (Trans, error) { return this.newTrans(stream), nil } -func (this *b) SetSizeLimit(limit int) { +func (this *b) SetSizeLimit(limit int64) { this.sizeLimit = limit } @@ -59,7 +57,7 @@ func (this *b) newTrans(underlying Stream) *transB { } type transB struct { - sizeLimit int + sizeLimit int64 underlying Stream currentData io.Reader } @@ -127,8 +125,8 @@ type Stream interface { ID() int64 } -func encodeMessageB(writer io.Writer, sizeLimit int, method uint16, data []byte) error { - if len(data) > sizeLimit { +func encodeMessageB(writer io.Writer, sizeLimit int64, method uint16, data []byte) error { + if len(data) > int(sizeLimit) { return ErrPayloadTooLarge } buffer := make([]byte, 10 + len(data)) @@ -141,7 +139,7 @@ func encodeMessageB(writer io.Writer, sizeLimit int, method uint16, data []byte) func decodeMessageB( reader io.Reader, - sizeLimit int, + sizeLimit int64, ) ( method uint16, size int64, -- 2.46.1 From e4f13a41429a695d4c2fd9b50d29c2cdbb4f80b9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 6 Apr 2025 17:01:00 -0400 Subject: [PATCH 009/132] WIP METADAPT-A changes --- metadapta.go | 108 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/metadapta.go b/metadapta.go index 1879b15..c23d579 100644 --- a/metadapta.go +++ b/metadapta.go @@ -17,6 +17,7 @@ type Party bool; const ( ) type a struct { + sizeLimit int underlying net.Conn party Party transID int64 @@ -87,6 +88,10 @@ func (this *a) AcceptTrans() (Trans, error) { } } +func (this *a) SetSizeLimit(limit int) { + this.sizeLimit = limit +} + func (this *a) unlistTransactionSafe(id int64) { this.transLock.Lock() defer this.transLock.Unlock() @@ -110,13 +115,13 @@ func (this *a) receive() { clear(this.transMap) }() for { - transID, method, payload, err := decodeMessageA(this.underlying) + transID, method, chunked, payload, err := decodeMessageA(this.underlying) if err != nil { this.err = fmt.Errorf("could not receive message: %w", err) return } - err = this.receiveMultiplex(transID, method, payload) + err = this.receiveMultiplex(transID, method, chunked, payload) if err != nil { this.err = fmt.Errorf("could not receive message: %w", err) return @@ -124,7 +129,7 @@ func (this *a) receive() { } } -func (this *a) receiveMultiplex(transID int64, method uint16, payload []byte) error { +func (this *a) receiveMultiplex(transID int64, method uint16, chunked bool, payload []byte) error { if transID == 0 { return ErrMessageMalformed } trans, err := func() (*transA, error) { @@ -152,15 +157,17 @@ func (this *a) receiveMultiplex(transID int64, method uint16, payload []byte) er trans.incoming.Send(incomingMessage { method: method, + chunked: chunked, payload: payload, }) return nil } type transA struct { - parent *a - id int64 - incoming usync.Gate[incomingMessage] + parent *a + id int64 + incoming usync.Gate[incomingMessage] + currentReader io.Reader } func (this *transA) Close() error { @@ -183,26 +190,78 @@ func (this *transA) Send(method uint16, data []byte) error { } func (this *transA) Receive() (method uint16, data []byte, err error) { - receive := this.incoming.Receive() + method, reader, err := this.ReceiveReader() + if err != nil { return 0, nil, err } + data, err = io.ReadAll(reader) + if err != nil { return 0, nil, err } + return method, data, nil +} + +func (this *transA) ReceiveReader() (uint16, io.Reader, error) { + // drain previous reader if necessary + if this.currentReader != nil { + io.Copy(io.Discard, this.currentReader) + } + + // create new reader + reader := &readerA { + parent: this, + } + method, err := reader.pull() + if err != nil { return 0, nil, err} + this.currentReader = reader + return method, reader, nil +} + +type readerA struct { + parent *transA + leftover []byte + eof bool +} + +func (this *readerA) pull() (uint16, error) { + // if the previous message ended the chain, return an io.EOF + if this.eof { + return 0, io.EOF + } + + // get a message from the transaction we are a part of + receive := this.parent.incoming.Receive() if receive != nil { if message, ok := <- receive; ok { if message.method != closeMethod { - return message.method, message.payload, nil + this.leftover = append(this.leftover, message.payload...) + if !message.chunked { + this.eof = true + } } } } // close and return error on failure - this.Close() - if this.parent.err == nil { - return 0, nil, fmt.Errorf("could not receive message: %w", io.EOF) + this.eof = true + this.parent.Close() + if this.parent.parent.err == nil { + return 0, fmt.Errorf("could not receive message: %w", io.EOF) } else { - return 0, nil, this.parent.err + return 0, this.parent.parent.err } } +func (this *readerA) Read(buffer []byte) (int, error) { + if len(this.leftover) == 0 { + if this.eof { return 0, io.EOF } + this.pull() + } + + copied := copy(buffer, this.leftover) + this.leftover = this.leftover[copied:] + return copied, nil +} + type incomingMessage struct { method uint16 + chunked bool payload []byte } @@ -218,22 +277,27 @@ func encodeMessageA(writer io.Writer, trans int64, method uint16, data []byte) e return err } -func decodeMessageA(reader io.Reader) (int64, uint16, []byte, error) { - headerBuffer := [12]byte { } +func decodeMessageA(reader io.Reader) (int64, uint16, bool, []byte, error) { + headerBuffer := [18]byte { } _, err := io.ReadFull(reader, headerBuffer[:]) - if err != nil { return 0, 0, nil, err } + if err != nil { return 0, 0, false, nil, err } transID, err := tape.DecodeI64[int64](headerBuffer[:8]) - if err != nil { return 0, 0, nil, err } + if err != nil { return 0, 0, false, nil, err } method, err := tape.DecodeI16[uint16](headerBuffer[8:10]) - if err != nil { return 0, 0, nil, err } - length, err := tape.DecodeI16[uint16](headerBuffer[10:12]) - if err != nil { return 0, 0, nil, err } - payloadBuffer := make([]byte, int(length)) + if err != nil { return 0, 0, false, nil, err } + size, err := tape.DecodeI64[uint64](headerBuffer[10:18]) + if err != nil { return 0, 0, false, nil, err } + chunked, size := splitCCBSize(size) + payloadBuffer := make([]byte, int(size)) _, err = io.ReadFull(reader, payloadBuffer) - if err != nil { return 0, 0, nil, err } - return transID, method, payloadBuffer, nil + if err != nil { return 0, 0, false, nil, err } + return transID, method, chunked, payloadBuffer, nil } func partyFromTransID(id int64) Party { return id > 0 } + +func splitCCBSize(size uint64) (bool, uint64) { + return size >> 63 > 1, size & 0x7FFFFFFFFFFFFFFF +} -- 2.46.1 From 6de3cbbc489bf77b45dfc475cba0689ecf8fbe94 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 21 Apr 2025 19:10:45 -0400 Subject: [PATCH 010/132] Fix method signature of SetSizeLimit --- metadapta.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadapta.go b/metadapta.go index c23d579..aeea21f 100644 --- a/metadapta.go +++ b/metadapta.go @@ -17,7 +17,7 @@ type Party bool; const ( ) type a struct { - sizeLimit int + sizeLimit int64 underlying net.Conn party Party transID int64 @@ -88,7 +88,7 @@ func (this *a) AcceptTrans() (Trans, error) { } } -func (this *a) SetSizeLimit(limit int) { +func (this *a) SetSizeLimit(limit int64) { this.sizeLimit = limit } -- 2.46.1 From 7a766b74d887a9d3165f153d10cecf20ac1b8d3c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 21 Apr 2025 20:49:58 -0400 Subject: [PATCH 011/132] Name return values of decodeMessageA --- metadapta.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/metadapta.go b/metadapta.go index aeea21f..ca6a629 100644 --- a/metadapta.go +++ b/metadapta.go @@ -277,18 +277,18 @@ func encodeMessageA(writer io.Writer, trans int64, method uint16, data []byte) e return err } -func decodeMessageA(reader io.Reader) (int64, uint16, bool, []byte, error) { +func decodeMessageA(reader io.Reader) (transID int64, method uint16, chunked bool, payloadBuffer []byte, err error) { headerBuffer := [18]byte { } - _, err := io.ReadFull(reader, headerBuffer[:]) + _, err = io.ReadFull(reader, headerBuffer[:]) if err != nil { return 0, 0, false, nil, err } - transID, err := tape.DecodeI64[int64](headerBuffer[:8]) + transID, err = tape.DecodeI64[int64](headerBuffer[:8]) if err != nil { return 0, 0, false, nil, err } - method, err := tape.DecodeI16[uint16](headerBuffer[8:10]) + method, err = tape.DecodeI16[uint16](headerBuffer[8:10]) if err != nil { return 0, 0, false, nil, err } size, err := tape.DecodeI64[uint64](headerBuffer[10:18]) if err != nil { return 0, 0, false, nil, err } - chunked, size := splitCCBSize(size) - payloadBuffer := make([]byte, int(size)) + chunked, size = splitCCBSize(size) + payloadBuffer = make([]byte, int(size)) _, err = io.ReadFull(reader, payloadBuffer) if err != nil { return 0, 0, false, nil, err } return transID, method, chunked, payloadBuffer, nil -- 2.46.1 From f34620c4345cc3fbcd6355798e61123598ab24f3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 21 Apr 2025 20:50:33 -0400 Subject: [PATCH 012/132] METADAPT-A tests run --- metadapta_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadapta_test.go b/metadapta_test.go index 0ecebe2..93ed006 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -108,7 +108,7 @@ func TestEncodeMessageAErr(test *testing.T) { } func TestDecodeMessageA(test *testing.T) { - transID, method, payload, err := decodeMessageA(bytes.NewReader([]byte { + transID, method, _, payload, err := decodeMessageA(bytes.NewReader([]byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, 0x00, 0x06, @@ -130,7 +130,7 @@ func TestDecodeMessageA(test *testing.T) { } func TestDecodeMessageAErr(test *testing.T) { - _, _, _, err := decodeMessageA(bytes.NewReader([]byte { + _, _, _, _, err := decodeMessageA(bytes.NewReader([]byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, 0x01, 0x06, -- 2.46.1 From 945d81c5058f0ff2e1c7bb5cc378159685890688 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 21 Apr 2025 20:51:02 -0400 Subject: [PATCH 013/132] METADAPT-B tests run --- metadaptb_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metadaptb_test.go b/metadaptb_test.go index add8043..f40f25a 100644 --- a/metadaptb_test.go +++ b/metadaptb_test.go @@ -33,7 +33,7 @@ func TestEncodeMessageBErr(test *testing.T) { } func TestDecodeMessageB(test *testing.T) { - method, payload, err := decodeMessageB(bytes.NewReader([]byte { + method, _, data, err := decodeMessageB(bytes.NewReader([]byte { 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, @@ -44,6 +44,7 @@ func TestDecodeMessageB(test *testing.T) { if got, correct := method, uint16(0x6B12); got != correct { test.Fatalf("not equal: %v %v", got, correct) } + payload, _ := io.ReadAll(data) correctPayload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } if got, correct := payload, correctPayload; !slices.Equal(got, correct) { test.Fatalf("not equal: %v %v", got, correct) @@ -51,7 +52,7 @@ func TestDecodeMessageB(test *testing.T) { } func TestDecodeMessageBErr(test *testing.T) { - _, _, err := decodeMessageB(bytes.NewReader([]byte { + _, _, _, err := decodeMessageB(bytes.NewReader([]byte { 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, -- 2.46.1 From fac0c4e31da8f606e2c9dcdfcf63d29015e7a313 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 22 Apr 2025 19:49:24 -0400 Subject: [PATCH 014/132] Actually use defaultSizeLimit --- metadapta.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/metadapta.go b/metadapta.go index ca6a629..e9b8e56 100644 --- a/metadapta.go +++ b/metadapta.go @@ -10,6 +10,7 @@ import "git.tebibyte.media/sashakoshka/go-util/sync" const closeMethod = 0xFFFF const int64Max = int64((^uint64(0)) >> 1) + // Party represents a side of a connection. type Party bool; const ( ServerSide Party = false @@ -33,6 +34,7 @@ type a struct { // oriented transport such as TCP or UNIX domain stream sockets. func AdaptA(underlying net.Conn, party Party) Conn { conn := &a { + sizeLimit: defaultSizeLimit, underlying: underlying, party: party, transMap: make(map[int64] *transA), @@ -115,7 +117,7 @@ func (this *a) receive() { clear(this.transMap) }() for { - transID, method, chunked, payload, err := decodeMessageA(this.underlying) + transID, method, chunked, payload, err := decodeMessageA(this.underlying, this.sizeLimit) if err != nil { this.err = fmt.Errorf("could not receive message: %w", err) return @@ -277,7 +279,16 @@ func encodeMessageA(writer io.Writer, trans int64, method uint16, data []byte) e return err } -func decodeMessageA(reader io.Reader) (transID int64, method uint16, chunked bool, payloadBuffer []byte, err error) { +func decodeMessageA( + reader io.Reader, + sizeLimit int64, +) ( + transID int64, + method uint16, + chunked bool, + payloadBuffer []byte, + err error, +) { headerBuffer := [18]byte { } _, err = io.ReadFull(reader, headerBuffer[:]) if err != nil { return 0, 0, false, nil, err } @@ -288,6 +299,9 @@ func decodeMessageA(reader io.Reader) (transID int64, method uint16, chunked boo size, err := tape.DecodeI64[uint64](headerBuffer[10:18]) if err != nil { return 0, 0, false, nil, err } chunked, size = splitCCBSize(size) + if size > uint64(sizeLimit) { + return 0, 0, false, nil, ErrPayloadTooLarge + } payloadBuffer = make([]byte, int(size)) _, err = io.ReadFull(reader, payloadBuffer) if err != nil { return 0, 0, false, nil, err } -- 2.46.1 From 46c6361602de96578c53b98aab8ac7062e831390 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 22 Apr 2025 20:10:57 -0400 Subject: [PATCH 015/132] Encode METADAPT-A MMBs properly lmao --- metadapta.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/metadapta.go b/metadapta.go index e9b8e56..265b027 100644 --- a/metadapta.go +++ b/metadapta.go @@ -268,13 +268,11 @@ type incomingMessage struct { } func encodeMessageA(writer io.Writer, trans int64, method uint16, data []byte) error { - buffer := make([]byte, 12 + len(data)) + buffer := make([]byte, 18 + len(data)) tape.EncodeI64(buffer[:8], trans) tape.EncodeI16(buffer[8:10], method) - length, ok := tape.U16CastSafe(len(data)) - if !ok { return ErrPayloadTooLarge } - tape.EncodeI16(buffer[10:12], length) - copy(buffer[12:], data) + tape.EncodeI64(buffer[10:18], uint64(len(data))) + copy(buffer[18:], data) _, err := writer.Write(buffer) return err } -- 2.46.1 From cbaff8b593fb4c6db83f4c1f9cb086e0a66be574 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 14:15:53 -0400 Subject: [PATCH 016/132] Allow readerA.pull to return an actual result --- metadapta.go | 1 + 1 file changed, 1 insertion(+) diff --git a/metadapta.go b/metadapta.go index 265b027..aa941e3 100644 --- a/metadapta.go +++ b/metadapta.go @@ -236,6 +236,7 @@ func (this *readerA) pull() (uint16, error) { if !message.chunked { this.eof = true } + return message.method, nil } } } -- 2.46.1 From 8fe3ba8d4f05e92d31264314b725a93c7e5019a9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:07:47 -0400 Subject: [PATCH 017/132] Close METADAPT-A transaction channel --- metadapta.go | 1 + 1 file changed, 1 insertion(+) diff --git a/metadapta.go b/metadapta.go index aa941e3..20074b3 100644 --- a/metadapta.go +++ b/metadapta.go @@ -109,6 +109,7 @@ func (this *a) sendMessageSafe(trans int64, method uint16, data []byte) error { func (this *a) receive() { defer func() { this.underlying.Close() + close(this.transChan) this.transLock.Lock() defer this.transLock.Unlock() for _, trans := range this.transMap { -- 2.46.1 From 86cf3ee89dc72af6e9125f88acc9dff1f93faa2a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:08:31 -0400 Subject: [PATCH 018/132] Make the TestConnA pass --- metadapta_test.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/metadapta_test.go b/metadapta_test.go index 93ed006..d3741ca 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -31,21 +31,23 @@ func TestConnA(test *testing.T) { // server listener, err := net.Listen(network, addr) if err != nil { test.Fatal(err) } - defer listener.Close() + test.Cleanup(func() { listener.Close() }) go func() { test.Log("SERVER listening") conn, err := listener.Accept() if err != nil { test.Error("SERVER", err); return } defer conn.Close() + test.Cleanup(func() { conn.Close() }) a := AdaptA(conn, ServerSide) trans, err := a.OpenTrans() if err != nil { test.Error("SERVER", err); return } - defer trans.Close() + test.Cleanup(func() { trans.Close() }) for method, payload := range payloads { - test.Log("SERVER", method, payload) + test.Log("SERVER m:", method, "p:", payload) err := trans.Send(uint16(method), []byte(payload)) if err != nil { test.Error("SERVER", err); return } } + test.Log("SERVER closing connection") }() // client @@ -54,18 +56,18 @@ func TestConnA(test *testing.T) { if err != nil { test.Fatal("CLIENT", err) } test.Log("CLIENT dialed") a := AdaptA(conn, ClientSide) - defer a.Close() + test.Cleanup(func() { a.Close() }) test.Log("CLIENT accepting transaction") trans, err := a.AcceptTrans() if err != nil { test.Fatal("CLIENT", err) } test.Log("CLIENT accepted transaction") - defer trans.Close() + test.Cleanup(func() { trans.Close() }) for method, payload := range payloads { test.Log("CLIENT waiting...") gotMethod, gotPayloadBytes, err := trans.Receive() if err != nil { test.Fatal("CLIENT", err) } gotPayload := string(gotPayloadBytes) - test.Log("CLIENT", gotMethod, gotPayload) + test.Log("CLIENT m:", gotMethod, "p:", gotPayload) if int(gotMethod) != method { test.Errorf("CLIENT method not equal") } @@ -73,11 +75,13 @@ func TestConnA(test *testing.T) { test.Errorf("CLIENT payload not equal") } } + test.Log("CLIENT waiting for connection close...") _, _, err = trans.Receive() if !errors.Is(err, io.EOF) { test.Fatal("CLIENT wrong error:", err) } test.Log("CLIENT done") + conn.Close() } func TestEncodeMessageA(test *testing.T) { @@ -87,7 +91,7 @@ func TestEncodeMessageA(test *testing.T) { correct := []byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, - 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, } if err != nil { @@ -111,9 +115,9 @@ func TestDecodeMessageA(test *testing.T) { transID, method, _, payload, err := decodeMessageA(bytes.NewReader([]byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, - 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - })) + }), defaultSizeLimit) if err != nil { test.Fatal(err) } @@ -133,9 +137,9 @@ func TestDecodeMessageAErr(test *testing.T) { _, _, _, _, err := decodeMessageA(bytes.NewReader([]byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, - 0x01, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - })) + }), defaultSizeLimit) if !errors.Is(err, io.ErrUnexpectedEOF) { test.Fatalf("wrong error: %v", err) } -- 2.46.1 From 9bf0c596ba79f1790ecbaaee79092212175e2e2d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:12:01 -0400 Subject: [PATCH 019/132] Make TestEncodeMessageAErr pass --- metadapta.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/metadapta.go b/metadapta.go index 20074b3..503e8af 100644 --- a/metadapta.go +++ b/metadapta.go @@ -103,7 +103,7 @@ func (this *a) unlistTransactionSafe(id int64) { func (this *a) sendMessageSafe(trans int64, method uint16, data []byte) error { this.sendLock.Lock() defer this.sendLock.Unlock() - return encodeMessageA(this.underlying, trans, method, data) + return encodeMessageA(this.underlying, this.sizeLimit, trans, method, data) } func (this *a) receive() { @@ -269,7 +269,16 @@ type incomingMessage struct { payload []byte } -func encodeMessageA(writer io.Writer, trans int64, method uint16, data []byte) error { +func encodeMessageA( + writer io.Writer, + sizeLimit int64, + trans int64, + method uint16, + data []byte, +) error { + if int64(len(data)) > sizeLimit { + return ErrPayloadTooLarge + } buffer := make([]byte, 18 + len(data)) tape.EncodeI64(buffer[:8], trans) tape.EncodeI16(buffer[8:10], method) -- 2.46.1 From f6fe9c307d931fba212a068d48ecf7777607fcdc Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:17:32 -0400 Subject: [PATCH 020/132] This should have been in the last commit --- metadapta_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadapta_test.go b/metadapta_test.go index d3741ca..4cb8fdb 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -87,7 +87,7 @@ func TestConnA(test *testing.T) { func TestEncodeMessageA(test *testing.T) { buffer := new(bytes.Buffer) payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } - err := encodeMessageA(buffer, 0x5800FEABC3104F04, 0x6B12, payload) + err := encodeMessageA(buffer, defaultSizeLimit, 0x5800FEABC3104F04, 0x6B12, payload) correct := []byte { 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x6B, 0x12, @@ -105,7 +105,7 @@ func TestEncodeMessageA(test *testing.T) { func TestEncodeMessageAErr(test *testing.T) { buffer := new(bytes.Buffer) payload := make([]byte, 0x10000) - err := encodeMessageA(buffer, 0x5800FEABC3104F04, 0x6B12, payload) + err := encodeMessageA(buffer, 0x20, 0x5800FEABC3104F04, 0x6B12, payload) if !errors.Is(err, ErrPayloadTooLarge) { test.Fatalf("wrong error: %v", err) } -- 2.46.1 From 87c4ac8efba8f5ce40d8f7347ee37b0d370f0629 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:21:52 -0400 Subject: [PATCH 021/132] More robust integer comparison --- metadaptb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadaptb.go b/metadaptb.go index 18776da..fdf2ba2 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -126,7 +126,7 @@ type Stream interface { } func encodeMessageB(writer io.Writer, sizeLimit int64, method uint16, data []byte) error { - if len(data) > int(sizeLimit) { + if int64(len(data)) > sizeLimit { return ErrPayloadTooLarge } buffer := make([]byte, 10 + len(data)) -- 2.46.1 From 47645a8fce7a65b7c67a349f4640f2b27a94abb0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 15:26:12 -0400 Subject: [PATCH 022/132] Pass TestDecodeMessageBErr --- metadaptb.go | 6 +++++- metadaptb_test.go | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/metadaptb.go b/metadaptb.go index fdf2ba2..867dedb 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -2,6 +2,7 @@ package hopp import "io" import "net" +import "errors" import "context" import "git.tebibyte.media/sashakoshka/hopp/tape" @@ -148,7 +149,10 @@ func decodeMessageB( ) { headerBuffer := [10]byte { } _, err = io.ReadFull(reader, headerBuffer[:]) - if err != nil { return 0, 0, nil, err } + if err != nil { + if errors.Is(err, io.EOF) { return 0, 0, nil, io.ErrUnexpectedEOF } + return 0, 0, nil, err + } method, err = tape.DecodeI16[uint16](headerBuffer[:2]) if err != nil { return 0, 0, nil, err } length, err := tape.DecodeI64[uint64](headerBuffer[2:10]) diff --git a/metadaptb_test.go b/metadaptb_test.go index f40f25a..cad341f 100644 --- a/metadaptb_test.go +++ b/metadaptb_test.go @@ -53,9 +53,7 @@ func TestDecodeMessageB(test *testing.T) { func TestDecodeMessageBErr(test *testing.T) { _, _, _, err := decodeMessageB(bytes.NewReader([]byte { - 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x06, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x6B, 0x12, 0x00, 0x00, 0x00, 0x00, }), defaultSizeLimit) if !errors.Is(err, io.ErrUnexpectedEOF) { test.Fatalf("wrong error: %v", err) -- 2.46.1 From c51a81bc134c88e665c54a1790543d5db00b98dd Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 16:02:09 -0400 Subject: [PATCH 023/132] Add a SendWriter method to Trans --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index a2f2080..e1b07c4 100644 --- a/connection.go +++ b/connection.go @@ -42,6 +42,9 @@ type Trans interface { // Send sends a message. Send(method uint16, data []byte) error + // SendWriter sends data written to an [io.Writer]. Any writer + // previously opened through this function will be discarded. + SendWriter(method uint16) (io.Writer, error) // Receive receives a message. Receive() (method uint16, data []byte, err error) // ReceiveReader receives a message as an [io.Reader]. Any reader -- 2.46.1 From 8a3df95491f47b1f85d5e38b5f78f31e24ccec65 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 16:06:17 -0400 Subject: [PATCH 024/132] Clarify concurrency in Trans methods --- connection.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/connection.go b/connection.go index e1b07c4..05dec8a 100644 --- a/connection.go +++ b/connection.go @@ -28,26 +28,31 @@ type Conn interface { SetSizeLimit(limit int64) } -// Trans is a HOPP transaction. +// Trans is a HOPP transaction. Methods of this interface are not safe for +// concurrent use with the exception of the Close and ID methods. The +// recommended use case is one goroutine per transaction. type Trans interface { // Close closes the transaction. Any blocked operations will be - // unblocked and return errors. + // unblocked and return errors. This method is safe for concurrent use. Close() error // ID returns the transaction ID. This must not change, and it must be - // unique within the connection. + // unique within the connection. This method is safe for concurrent use. ID() int64 // TODO: add methods for setting send and receive deadlines - // Send sends a message. + // Send sends a message. This method is not safe for concurrent use. Send(method uint16, data []byte) error // SendWriter sends data written to an [io.Writer]. Any writer - // previously opened through this function will be discarded. + // previously opened through this function will be discarded. This + // method is not safe for concurrent use, and neither is its result. SendWriter(method uint16) (io.Writer, error) - // Receive receives a message. + // Receive receives a message. This method is not safe for concurrent + // use. Receive() (method uint16, data []byte, err error) // ReceiveReader receives a message as an [io.Reader]. Any reader - // previously opened through this function will be discarded. + // previously opened through this function will be discarded. This + // method is not safe for concurrent use, and neither is its result. ReceiveReader() (method uint16, data io.Reader, err error) } -- 2.46.1 From 41f5cfefabd828496147c10c6224eed0eb406227 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 17:53:12 -0400 Subject: [PATCH 025/132] Implement SendWriter for METADAPT-A --- metadapta.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/metadapta.go b/metadapta.go index 503e8af..03bef8e 100644 --- a/metadapta.go +++ b/metadapta.go @@ -9,7 +9,7 @@ import "git.tebibyte.media/sashakoshka/go-util/sync" const closeMethod = 0xFFFF const int64Max = int64((^uint64(0)) >> 1) - +const defaultChunkSize = 0x1000 // Party represents a side of a connection. type Party bool; const ( @@ -171,6 +171,8 @@ type transA struct { id int64 incoming usync.Gate[incomingMessage] currentReader io.Reader + currentWriter io.Closer + writeBuffer []byte } func (this *transA) Close() error { @@ -192,6 +194,27 @@ func (this *transA) Send(method uint16, data []byte) error { return this.parent.sendMessageSafe(this.id, method, data) } +func (this *transA) SendWriter(method uint16) (io.Writer, error) { + // close previous writer if necessary + if this.currentWriter != nil { + this.currentWriter.Close() + this.currentWriter = nil + } + + // create new writer + writer := &writerA { + parent: this, + // there is only ever one writer at a time, so they can all + // share a buffer + buffer: this.writeBuffer[:0], + method: method, + chunkSize: defaultChunkSize, + open: true, + } + this.currentWriter = writer + return writer, nil +} + func (this *transA) Receive() (method uint16, data []byte, err error) { method, reader, err := this.ReceiveReader() if err != nil { return 0, nil, err } @@ -263,6 +286,57 @@ func (this *readerA) Read(buffer []byte) (int, error) { return copied, nil } +type writerA struct { + parent *transA + buffer []byte + method uint16 + chunkSize int64 + open bool +} + +func (this *writerA) Write(data []byte) (n int, err error) { + if !this.open { return 0, io.EOF } + toSend := data + for len(toSend) > 0 { + nn, err := this.writeOne(toSend) + n += nn + toSend = toSend[nn:] + if err != nil { return n, err } + } + return n, nil +} + +func (this *writerA) Close() error { + this.open = false + return nil +} + +func (this *writerA) writeOne(data []byte) (n int, err error) { + data = data[:min(len(data), int(this.chunkSize))] + + // if there is more room, append to the buffer and exit + if int64(len(this.buffer) + len(data)) <= this.chunkSize { + this.buffer = append(this.buffer, data...) + n = len(data) + // if have a full chunk, flush + if int64(len(this.buffer)) == this.chunkSize { + err = this.flush() + if err != nil { return n, err } + } + return n, nil + } + + // if not, flush and store as much as we can in the buffer + err = this.flush() + if err != nil { return n, err } + this.buffer = append(this.buffer, data...) + return n, nil +} + +func (this *writerA) flush() error { + return this.parent.parent.sendMessageSafe(this.parent.id, this.method, this.buffer) +} + type incomingMessage struct { method uint16 chunked bool -- 2.46.1 From dd89245c34da151ae17e45af4a785a4a8ea39632 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 18:06:00 -0400 Subject: [PATCH 026/132] Change the result of Trans.SendWriter to a WriteCloser --- connection.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index 05dec8a..7c82c8f 100644 --- a/connection.go +++ b/connection.go @@ -44,10 +44,12 @@ type Trans interface { // Send sends a message. This method is not safe for concurrent use. Send(method uint16, data []byte) error - // SendWriter sends data written to an [io.Writer]. Any writer - // previously opened through this function will be discarded. This - // method is not safe for concurrent use, and neither is its result. - SendWriter(method uint16) (io.Writer, error) + // SendWriter sends data written to an [io.Writer]. The writer must be + // closed after use. Closing the writer flushes any data that hasn't + // been written yet. Any writer previously opened through this function + // will be discarded. This method is not safe for concurrent use, and + // neither is its result. + SendWriter(method uint16) (io.WriteCloser, error) // Receive receives a message. This method is not safe for concurrent // use. Receive() (method uint16, data []byte, err error) -- 2.46.1 From 9d2bbec7f9cb6c0db221499e5b3f06cd72f91c31 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 18:14:47 -0400 Subject: [PATCH 027/132] Update METADAPT-A implementation --- metadapta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadapta.go b/metadapta.go index 03bef8e..55f1c0c 100644 --- a/metadapta.go +++ b/metadapta.go @@ -194,7 +194,7 @@ func (this *transA) Send(method uint16, data []byte) error { return this.parent.sendMessageSafe(this.id, method, data) } -func (this *transA) SendWriter(method uint16) (io.Writer, error) { +func (this *transA) SendWriter(method uint16) (io.WriteCloser, error) { // close previous writer if necessary if this.currentWriter != nil { this.currentWriter.Close() -- 2.46.1 From 7a0bf64c17a4dd86e8ad518addda487805b83fe7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 18:15:38 -0400 Subject: [PATCH 028/132] Implement SendWriter for METADAPT-B --- metadaptb.go | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/metadaptb.go b/metadaptb.go index 867dedb..c91db72 100644 --- a/metadaptb.go +++ b/metadaptb.go @@ -2,6 +2,7 @@ package hopp import "io" import "net" +import "bytes" import "errors" import "context" import "git.tebibyte.media/sashakoshka/hopp/tape" @@ -58,9 +59,10 @@ func (this *b) newTrans(underlying Stream) *transB { } type transB struct { - sizeLimit int64 - underlying Stream - currentData io.Reader + sizeLimit int64 + underlying Stream + currentData io.Reader + currentWriter *writerB } func (this *transB) Close() error { @@ -75,6 +77,24 @@ func (this *transB) Send(method uint16, data []byte) error { return encodeMessageB(this.underlying, this.sizeLimit, method, data) } +func (this *transB) SendWriter(method uint16) (io.WriteCloser, error) { + if this.currentWriter != nil { + this.currentWriter.Close() + } + // TODO: come up with a fix that allows us to pipe data through the + // writer. as of now, it just reads whatever is written into a buffer + // and sends the message on close. we should probably introduce chunked + // encoding to METADAPT-B to fix this. the implementation would be + // simpler than on METADAPT-A, but most of the code could just be + // copied over. + writer := &writerB { + parent: this, + method: method, + } + this.currentWriter = writer + return writer, nil +} + func (this *transB) Receive() (uint16, []byte, error) { // get a reader for the next message method, size, data, err := this.receiveReader() @@ -105,6 +125,20 @@ func (this *transB) receiveReader() (uint16, int64, io.Reader, error) { return method, size, data, nil } +type writerB struct { + parent *transB + buffer bytes.Buffer + method uint16 +} + +func (this *writerB) Write(data []byte) (int, error) { + return this.buffer.Write(data) +} + +func (this *writerB) Close() error { + return this.parent.Send(this.method, this.buffer.Bytes()) +} + // MultiConn represens a multiplexed stream-oriented transport for use in // [AdaptB]. type MultiConn interface { -- 2.46.1 From c0bfcc02f7762e8609562126a1c911d0cc78ae95 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 18:19:43 -0400 Subject: [PATCH 029/132] Send a close message when METADAPT-A transactions close --- metadapta.go | 1 + 1 file changed, 1 insertion(+) diff --git a/metadapta.go b/metadapta.go index 55f1c0c..41ce59d 100644 --- a/metadapta.go +++ b/metadapta.go @@ -183,6 +183,7 @@ func (this *transA) Close() error { func (this *transA) closeDontUnlist() error { this.Send(closeMethod, nil) + this.parent.sendMessageSafe(this.id, 0xFFFF, nil) return this.incoming.Close() } -- 2.46.1 From a83aedc1287b066774ebd7e31f85678de6c167c6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 18:38:01 -0400 Subject: [PATCH 030/132] Break METADAPT-A client/server environment from TestConnA --- metadapta_test.go | 115 +++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 47 deletions(-) diff --git a/metadapta_test.go b/metadapta_test.go index 4cb8fdb..c85d6d9 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -24,21 +24,34 @@ func TestConnA(test *testing.T) { "world", "When the impostor is sus!", } - - network := "tcp" - addr := "localhost:7959" - // server - listener, err := net.Listen(network, addr) - if err != nil { test.Fatal(err) } - test.Cleanup(func() { listener.Close() }) - go func() { - test.Log("SERVER listening") - conn, err := listener.Accept() - if err != nil { test.Error("SERVER", err); return } - defer conn.Close() - test.Cleanup(func() { conn.Close() }) - a := AdaptA(conn, ServerSide) + clientFunc := func(a Conn) { + test.Log("CLIENT accepting transaction") + trans, err := a.AcceptTrans() + if err != nil { test.Fatal("CLIENT", err) } + test.Log("CLIENT accepted transaction") + test.Cleanup(func() { trans.Close() }) + for method, payload := range payloads { + test.Log("CLIENT waiting...") + gotMethod, gotPayloadBytes, err := trans.Receive() + if err != nil { test.Fatal("CLIENT", err) } + gotPayload := string(gotPayloadBytes) + test.Log("CLIENT m:", gotMethod, "p:", gotPayload) + if int(gotMethod) != method { + test.Errorf("CLIENT method not equal") + } + if gotPayload != payload { + test.Errorf("CLIENT payload not equal") + } + } + test.Log("CLIENT waiting for transaction close...") + _, _, err = trans.Receive() + if !errors.Is(err, io.EOF) { + test.Fatal("CLIENT wrong error:", err) + } + } + + serverFunc := func(a Conn) { trans, err := a.OpenTrans() if err != nil { test.Error("SERVER", err); return } test.Cleanup(func() { trans.Close() }) @@ -48,40 +61,9 @@ func TestConnA(test *testing.T) { if err != nil { test.Error("SERVER", err); return } } test.Log("SERVER closing connection") - }() + } - // client - test.Log("CLIENT dialing") - conn, err := net.Dial(network, addr) - if err != nil { test.Fatal("CLIENT", err) } - test.Log("CLIENT dialed") - a := AdaptA(conn, ClientSide) - test.Cleanup(func() { a.Close() }) - test.Log("CLIENT accepting transaction") - trans, err := a.AcceptTrans() - if err != nil { test.Fatal("CLIENT", err) } - test.Log("CLIENT accepted transaction") - test.Cleanup(func() { trans.Close() }) - for method, payload := range payloads { - test.Log("CLIENT waiting...") - gotMethod, gotPayloadBytes, err := trans.Receive() - if err != nil { test.Fatal("CLIENT", err) } - gotPayload := string(gotPayloadBytes) - test.Log("CLIENT m:", gotMethod, "p:", gotPayload) - if int(gotMethod) != method { - test.Errorf("CLIENT method not equal") - } - if gotPayload != payload { - test.Errorf("CLIENT payload not equal") - } - } - test.Log("CLIENT waiting for connection close...") - _, _, err = trans.Receive() - if !errors.Is(err, io.EOF) { - test.Fatal("CLIENT wrong error:", err) - } - test.Log("CLIENT done") - conn.Close() + clientServerEnvironment(test, clientFunc, serverFunc) } func TestEncodeMessageA(test *testing.T) { @@ -144,3 +126,42 @@ func TestDecodeMessageAErr(test *testing.T) { test.Fatalf("wrong error: %v", err) } } + +func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), serverFunc func(conn Conn)) { + network := "tcp" + addr := "localhost:7959" + + // server + listener, err := net.Listen(network, addr) + if err != nil { test.Fatal(err) } + test.Cleanup(func() { listener.Close() }) + go func() { + test.Log("SERVER listening") + conn, err := listener.Accept() + if err != nil { test.Error("SERVER", err); return } + defer conn.Close() + test.Cleanup(func() { conn.Close() }) + a := AdaptA(conn, ServerSide) + test.Cleanup(func() { a.Close() }) + + serverFunc(a) + }() + + // client + test.Log("CLIENT dialing") + conn, err := net.Dial(network, addr) + if err != nil { test.Fatal("CLIENT", err) } + test.Log("CLIENT dialed") + a := AdaptA(conn, ClientSide) + test.Cleanup(func() { a.Close() }) + + clientFunc(a) + + test.Log("CLIENT waiting for connection close...") + _, err = a.AcceptTrans() + if !errors.Is(err, io.EOF) { + test.Fatal("CLIENT wrong error:", err) + } + test.Log("CLIENT DONE") + conn.Close() +} -- 2.46.1 From 23c37c3d1f50bbdf2503f26586f504c2a6c93cdb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 25 Apr 2025 19:57:33 -0400 Subject: [PATCH 031/132] Fix transaction ID counting --- metadapta.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/metadapta.go b/metadapta.go index 41ce59d..8250478 100644 --- a/metadapta.go +++ b/metadapta.go @@ -66,18 +66,21 @@ func (this *a) RemoteAddr() net.Addr { func (this *a) OpenTrans() (Trans, error) { this.transLock.Lock() defer this.transLock.Unlock() + if this.transID == int64Max { + return nil, fmt.Errorf("could not open transaction: %w", ErrIntegerOverflow) + } id := this.transID - this.transID ++ trans := &transA { parent: this, id: id, incoming: usync.NewGate[incomingMessage](), } this.transMap[id] = trans - if this.transID == int64Max { - return nil, fmt.Errorf("could not open transaction: %w", ErrIntegerOverflow) + if this.party == ClientSide { + this.transID ++ + } else { + this.transID -- } - this.transID ++ return trans, nil } -- 2.46.1 From d60beccbcd53d86ee9373d35472be8a3e2155e1a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 14 May 2025 13:44:06 -0400 Subject: [PATCH 032/132] Finally fix A... this took too long --- metadapta.go | 79 +++++++++++++++++++++++++++------ metadapta_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 18 deletions(-) diff --git a/metadapta.go b/metadapta.go index 8250478..9739500 100644 --- a/metadapta.go +++ b/metadapta.go @@ -4,9 +4,13 @@ import "io" import "fmt" import "net" import "sync" +import "sync/atomic" import "git.tebibyte.media/sashakoshka/hopp/tape" import "git.tebibyte.media/sashakoshka/go-util/sync" +// TODO investigate why 30 never reaches the server, causing it to wait for ever +// and never close the connection, causing the client to also wait forever + const closeMethod = 0xFFFF const int64Max = int64((^uint64(0)) >> 1) const defaultChunkSize = 0x1000 @@ -17,6 +21,14 @@ type Party bool; const ( ClientSide Party = true ) +func (party Party) String() string { + if party == ServerSide { + return "server" + } else { + return "client" + } +} + type a struct { sizeLimit int64 underlying net.Conn @@ -52,7 +64,7 @@ func AdaptA(underlying net.Conn, party Party) Conn { func (this *a) Close() error { close(this.done) - return this.underlying.Close() + return nil } func (this *a) LocalAddr() net.Addr { @@ -85,11 +97,15 @@ func (this *a) OpenTrans() (Trans, error) { } func (this *a) AcceptTrans() (Trans, error) { + eof := fmt.Errorf("could not accept transaction: %w", io.EOF) select { case trans := <- this.transChan: + if trans == nil { + return nil, eof + } return trans, nil case <- this.done: - return nil, fmt.Errorf("could not accept transaction: %w", io.EOF) + return nil, eof } } @@ -119,7 +135,11 @@ func (this *a) receive() { trans.closeDontUnlist() } clear(this.transMap) + this.underlying.Close() }() + + // receive MMBs in a loop and forward them to transactions until shit + // starts closing for { transID, method, chunked, payload, err := decodeMessageA(this.underlying, this.sizeLimit) if err != nil { @@ -127,7 +147,7 @@ func (this *a) receive() { return } - err = this.receiveMultiplex(transID, method, chunked, payload) + err = this.multiplexMMB(transID, method, chunked, payload) if err != nil { this.err = fmt.Errorf("could not receive message: %w", err) return @@ -135,7 +155,7 @@ func (this *a) receive() { } } -func (this *a) receiveMultiplex(transID int64, method uint16, chunked bool, payload []byte) error { +func (this *a) multiplexMMB(transID int64, method uint16, chunked bool, payload []byte) error { if transID == 0 { return ErrMessageMalformed } trans, err := func() (*transA, error) { @@ -144,6 +164,12 @@ func (this *a) receiveMultiplex(transID int64, method uint16, chunked bool, payl trans, ok := this.transMap[transID] if !ok { + // check if this is a superfluous close message and just + // do nothing if so + if method == closeMethod { + return nil, nil + } + // it is forbidden for the other party to initiate a transaction // with an ID from this party if this.party == partyFromTransID(transID) { @@ -161,14 +187,24 @@ func (this *a) receiveMultiplex(transID int64, method uint16, chunked bool, payl }() if err != nil { return err } - trans.incoming.Send(incomingMessage { - method: method, - chunked: chunked, - payload: payload, - }) + if trans == nil { + return nil + } + + if method == closeMethod { + return trans.Close() + } else { + trans.incoming.Send(incomingMessage { + method: method, + chunked: chunked, + payload: payload, + }) + } return nil } +// most methods in transA don't need to be goroutine safe except those marked +// as such type transA struct { parent *a id int64 @@ -176,18 +212,24 @@ type transA struct { currentReader io.Reader currentWriter io.Closer writeBuffer []byte + closed atomic.Bool } func (this *transA) Close() error { + // MUST be goroutine safe err := this.closeDontUnlist() this.parent.unlistTransactionSafe(this.ID()) return err } -func (this *transA) closeDontUnlist() error { - this.Send(closeMethod, nil) - this.parent.sendMessageSafe(this.id, 0xFFFF, nil) - return this.incoming.Close() +func (this *transA) closeDontUnlist() (err error) { + // MUST be goroutine safe + this.incoming.Close() + if !this.closed.Load() { + err = this.Send(closeMethod, nil) + } + this.closed.Store(true) + return err } func (this *transA) ID() int64 { @@ -228,6 +270,11 @@ func (this *transA) Receive() (method uint16, data []byte, err error) { } func (this *transA) ReceiveReader() (uint16, io.Reader, error) { + // if the transaction has been closed, return an io.EOF + if this.closed.Load() { + return 0, nil, io.EOF + } + // drain previous reader if necessary if this.currentReader != nil { io.Copy(io.Discard, this.currentReader) @@ -249,13 +296,14 @@ type readerA struct { eof bool } +// pull pulls the next MMB in this message from the transaction. func (this *readerA) pull() (uint16, error) { // if the previous message ended the chain, return an io.EOF if this.eof { return 0, io.EOF } - // get a message from the transaction we are a part of + // get an MMB from the transaction we are a part of receive := this.parent.incoming.Receive() if receive != nil { if message, ok := <- receive; ok { @@ -265,6 +313,9 @@ func (this *readerA) pull() (uint16, error) { this.eof = true } return message.method, nil + } else { + // signal parent transaction of closure + this.parent.closed.Store(true) } } } diff --git a/metadapta_test.go b/metadapta_test.go index c85d6d9..62dfdd9 100644 --- a/metadapta_test.go +++ b/metadapta_test.go @@ -45,9 +45,12 @@ func TestConnA(test *testing.T) { } } test.Log("CLIENT waiting for transaction close...") - _, _, err = trans.Receive() + gotMethod, gotPayload, err := trans.Receive() if !errors.Is(err, io.EOF) { - test.Fatal("CLIENT wrong error:", err) + test.Error("CLIENT wrong error:", err) + test.Error("CLIENT method:", gotMethod) + test.Error("CLIENT payload:", gotPayload) + test.Fatal("CLIENT ok byeeeeeeeeeeeee") } } @@ -66,6 +69,79 @@ func TestConnA(test *testing.T) { clientServerEnvironment(test, clientFunc, serverFunc) } +func TestTransOpenCloseA(test *testing.T) { + // currently: + // + // | data sent | data recvd | close sent | close recvd + // 10 | X | X | X | server hangs + // 20 | X | X | X | client hangs + // 30 | X | | X | + // + // when a close message is recvd, it tries to push to the trans and + // hangs on trans.incoming.Send, which hangs on sending the value to the + // underlying channel. why is this? + // + // check if we are really getting values from the channel when pulling + // from the trans channel when we are expecting a close. + + clientFunc := func(conn Conn) { + // 10 + trans, err := conn.OpenTrans() + if err != nil { test.Error("CLIENT", err); return } + test.Log("CLIENT sending 10") + trans.Send(10, []byte("hi")) + trans.Close() + + // 20 + test.Log("CLIENT awaiting 20") + trans, err = conn.AcceptTrans() + if err != nil { test.Error("CLIENT", err); return } + test.Cleanup(func() { trans.Close() }) + gotMethod, gotPayload, err := trans.Receive() + if err != nil { test.Error("CLIENT", err); return } + test.Logf("CLIENT m: %d p: %s", gotMethod, gotPayload) + if gotMethod != 20 { test.Error("CLIENT wrong method")} + + // 30 + trans, err = conn.OpenTrans() + if err != nil { test.Error("CLIENT", err); return } + test.Log("CLIENT sending 30") + trans.Send(30, []byte("good")) + trans.Close() + } + + serverFunc := func(conn Conn) { + // 10 + test.Log("SERVER awaiting 10") + trans, err := conn.AcceptTrans() + if err != nil { test.Error("SERVER", err); return } + test.Cleanup(func() { trans.Close() }) + gotMethod, gotPayload, err := trans.Receive() + if err != nil { test.Error("SERVER", err); return } + test.Logf("SERVER m: %d p: %s", gotMethod, gotPayload) + if gotMethod != 10 { test.Error("SERVER wrong method")} + + // 20 + trans, err = conn.OpenTrans() + if err != nil { test.Error("SERVER", err); return } + test.Log("SERVER sending 20") + trans.Send(20, []byte("hi how r u")) + trans.Close() + + // 30 + test.Log("SERVER awaiting 30") + trans, err = conn.AcceptTrans() + if err != nil { test.Error("SERVER", err); return } + test.Cleanup(func() { trans.Close() }) + gotMethod, gotPayload, err = trans.Receive() + if err != nil { test.Error("SERVER", err); return } + test.Logf("SERVER m: %d p: %s", gotMethod, gotPayload) + if gotMethod != 30 { test.Error("SERVER wrong method")} + } + + clientServerEnvironment(test, clientFunc, serverFunc) +} + func TestEncodeMessageA(test *testing.T) { buffer := new(bytes.Buffer) payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } @@ -127,6 +203,28 @@ func TestDecodeMessageAErr(test *testing.T) { } } +func TestEncodeDecodeMessageA(test *testing.T) { + correctTransID := int64(2) + correctMethod := uint16(30) + correctPayload := []byte("good") + buffer := bytes.Buffer { } + err := encodeMessageA(&buffer, defaultSizeLimit, correctTransID, correctMethod, correctPayload) + if err != nil { test.Fatal(err) } + transID, method, chunked, payload, err := decodeMessageA(&buffer, defaultSizeLimit) + if got, correct := transID, int64(2); got != correct { + test.Fatalf("not equal: %v %v", got, correct) + } + if got, correct := method, uint16(30); got != correct { + test.Fatalf("not equal: %v %v", got, correct) + } + if chunked { + test.Fatalf("message should not be chunked") + } + if got, correct := payload, correctPayload; !slices.Equal(got, correct) { + test.Fatalf("not equal: %v %v", got, correct) + } +} + func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), serverFunc func(conn Conn)) { network := "tcp" addr := "localhost:7959" @@ -145,6 +243,7 @@ func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), server test.Cleanup(func() { a.Close() }) serverFunc(a) + test.Log("SERVER closing") }() // client @@ -158,9 +257,10 @@ func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), server clientFunc(a) test.Log("CLIENT waiting for connection close...") - _, err = a.AcceptTrans() + trans, err := a.AcceptTrans() if !errors.Is(err, io.EOF) { - test.Fatal("CLIENT wrong error:", err) + test.Error("CLIENT wrong error:", err) + test.Fatal("CLIENT trans:", trans) } test.Log("CLIENT DONE") conn.Close() -- 2.46.1 From 2fdf7d490d7fc192912f71e9d2b51e5207a8fdc0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 14 May 2025 13:52:03 -0400 Subject: [PATCH 033/132] Remove unneeded code --- metadapta.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/metadapta.go b/metadapta.go index 9739500..dfbe9a0 100644 --- a/metadapta.go +++ b/metadapta.go @@ -313,9 +313,6 @@ func (this *readerA) pull() (uint16, error) { this.eof = true } return message.method, nil - } else { - // signal parent transaction of closure - this.parent.closed.Store(true) } } } -- 2.46.1 From 218949bd46159207b101a75228e9a1c0865ad46f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 14 May 2025 14:39:19 -0400 Subject: [PATCH 034/132] Remove quic It's clear it won't survive this change because I can't even test it, so who knows if its good enough to have in main. --- dial.go | 18 ++--------------- go.mod | 14 -------------- go.sum | 56 ----------------------------------------------------- listen.go | 36 +++------------------------------- quicwrap.go | 54 --------------------------------------------------- 5 files changed, 5 insertions(+), 173 deletions(-) delete mode 100644 quicwrap.go diff --git a/dial.go b/dial.go index 95a24c9..a5e062f 100644 --- a/dial.go +++ b/dial.go @@ -1,9 +1,9 @@ package hopp import "net" +import "errors" import "context" import "crypto/tls" -import "github.com/quic-go/quic-go" // Dial opens a connection to a server. The network must be one of "quic", // "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". For now, "quic4" and @@ -31,12 +31,7 @@ func (diale Dialer) Dial(ctx context.Context, network, address string) (Conn, er } func (diale Dialer) dialQUIC(ctx context.Context, network, address string) (Conn, error) { - // sorry i fucking lied to you about the network parameter. for all - // quic-go's bullshit bloat, it doesnt even support that. not even when - // instantiating a transport. go figure :/ - conn, err := quic.DialAddr(ctx, address, tlsConfig(diale.TLSConfig), quicConfig()) - if err != nil { return nil, err } - return AdaptB(quicMultiConn { underlying: conn }), nil + return nil, errors.New("quic is not yet implemented") } func (diale Dialer) dialUnix(ctx context.Context, network, address string) (Conn, error) { @@ -60,15 +55,6 @@ func tlsConfig(conf *tls.Config) *tls.Config { return conf } -func quicConfig() *quic.Config { - return &quic.Config { - // TODO: perhaps we might want to put something here - // the quic config shouldn't be exported, just set up - // automatically. we can't have that strangely built quic-go - // package be part of the API, or any third-party packages for - // that matter. it must all be abstracted away. - } -} func quicNetworkToUDPNetwork(network string) (string, error) { switch network { diff --git a/go.mod b/go.mod index 1acc120..0b443b6 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,4 @@ go 1.23.0 require ( git.tebibyte.media/sashakoshka/go-util v0.9.1 github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 - github.com/quic-go/quic-go v0.48.2 -) - -require ( - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect - go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect ) diff --git a/go.sum b/go.sum index 2f2e05a..bb15dd1 100644 --- a/go.sum +++ b/go.sum @@ -1,60 +1,4 @@ git.tebibyte.media/sashakoshka/go-util v0.9.1 h1:eGAbLwYhOlh4aq/0w+YnJcxT83yPhXtxnYMzz6K7xGo= git.tebibyte.media/sashakoshka/go-util v0.9.1/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g= github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= -github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/listen.go b/listen.go index 09f1a03..723dea5 100644 --- a/listen.go +++ b/listen.go @@ -1,9 +1,8 @@ package hopp import "net" -import "context" +import "errors" import "crypto/tls" -import "github.com/quic-go/quic-go" // Listener is an object which listens for incoming HOPP connections. type Listener interface { @@ -30,19 +29,8 @@ func Listen(network, address string) (Listener, error) { // The network must be one of "quic", "quic4", (IPv4-only) or "quic6" // (IPv6-only). func ListenQUIC(network, address string, tlsConf *tls.Config) (Listener, error) { - tlsConf = tlsConfig(tlsConf) - quicConf := quicConfig() - udpNetwork, err := quicNetworkToUDPNetwork(network) - if err != nil { return nil, err } - addr, err := net.ResolveUDPAddr(udpNetwork, address) - if err != nil { return nil, err } - udpListener, err := net.ListenUDP(udpNetwork, addr) - if err != nil { return nil, err } - quicListener, err := quic.Listen(udpListener, tlsConf, quicConf) - if err != nil { return nil, err } - return &listenerQUIC { - underlying: quicListener, - }, nil + // tlsConf = tlsConfig(tlsConf) + return nil, errors.New("quic is not yet implemented") } // ListenUnix listens for incoming HOPP connections using a Unix domain socket @@ -58,24 +46,6 @@ func ListenUnix(network, address string) (Listener, error) { }, nil } -type listenerQUIC struct { - underlying *quic.Listener -} - -func (this *listenerQUIC) Accept() (Conn, error) { - conn, err := this.underlying.Accept(context.Background()) - if err != nil { return nil, err } - return AdaptB(quicMultiConn { underlying: conn }), nil -} - -func (this *listenerQUIC) Close() error { - return this.underlying.Close() -} - -func (this *listenerQUIC) Addr() net.Addr { - return this.underlying.Addr() -} - type listenerUnix struct { underlying *net.UnixListener } diff --git a/quicwrap.go b/quicwrap.go deleted file mode 100644 index 45b00b3..0000000 --- a/quicwrap.go +++ /dev/null @@ -1,54 +0,0 @@ -package hopp - -import "net" -import "context" -import "github.com/quic-go/quic-go" - -var _ MultiConn = quicMultiConn { } -type quicMultiConn struct { - underlying quic.Connection -} - -func (conn quicMultiConn) Close() error { - return conn.underlying.CloseWithError(0, "good bye") -} - -func (conn quicMultiConn) LocalAddr() net.Addr { - return conn.underlying.LocalAddr() -} - -func (conn quicMultiConn) RemoteAddr() net.Addr { - return conn.underlying.RemoteAddr() -} - -func (conn quicMultiConn) AcceptStream(ctx context.Context) (Stream, error) { - strea, err := conn.underlying.AcceptStream(ctx) - if err != nil { return nil, err } - return quicStream { underlying: strea }, nil -} - -func (conn quicMultiConn) OpenStream() (Stream, error) { - strea, err := conn.underlying.OpenStream() - if err != nil { return nil, err } - return quicStream { underlying: strea }, nil -} - -type quicStream struct { - underlying quic.Stream -} - -func (strea quicStream) Read(buffer []byte) (n int, err error) { - return strea.underlying.Read(buffer) -} - -func (strea quicStream) Write(buffer []byte) (n int, err error) { - return strea.underlying.Read(buffer) -} - -func (strea quicStream) Close() error { - return strea.underlying.Close() -} - -func (strea quicStream) ID() int64 { - return int64(strea.underlying.StreamID()) -} -- 2.46.1 From 0b98c768b37bb42fa0e864a9d1db28ab540abc4c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 14 May 2025 14:44:27 -0400 Subject: [PATCH 035/132] Fix some outdated doc comments --- dial.go | 5 ++--- listen.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dial.go b/dial.go index a5e062f..9b9d3d8 100644 --- a/dial.go +++ b/dial.go @@ -19,9 +19,8 @@ type Dialer struct { } // Dial opens a connection to a server. The network must be one of "quic", -// "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". For now, "quic4" and -// "quic6" don't do anything as the quic-go package doesn't seem to support this -// behavior. +// "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". For now, quic is not +// supported. func (diale Dialer) Dial(ctx context.Context, network, address string) (Conn, error) { switch network { case "quic", "quic4", "quic6": return diale.dialQUIC(ctx, network, address) diff --git a/listen.go b/listen.go index 723dea5..4c0681c 100644 --- a/listen.go +++ b/listen.go @@ -16,7 +16,8 @@ type Listener interface { } // Listen listens for incoming HOPP connections. The network must be one of -// "quic", "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". +// "quic", "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". For now, quic is +// not supported. func Listen(network, address string) (Listener, error) { switch network { case "quic", "quic4", "quic6": return ListenQUIC(network, address, nil) -- 2.46.1 From 83443b8c8870c3baed1078146eda3c71aeecd18d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 14 May 2025 15:15:03 -0400 Subject: [PATCH 036/132] design: Fix documentation on message payload length --- design/protocol.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index 550cc86..33daca2 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -18,12 +18,10 @@ dependant on which transport is being used. A message refers to a block of octets sent within a transaction, paired with an unsigned 16-bit method code. The order of messages within a given transaction is preserved, but the order of messages accross the entire connection is not -guaranteed. - -The message payload must be 65,535 (unsigned 16-bit integer limit) octets or -smaller in length. This does not include the method code. Applications are free -to send whatever data they wish as the payload, but TAPE is recommended for -encoding it. +guaranteed. There is no functional limit on the size of a message payload, but +there may be one depending on which +[METADAPT sub-protocol](#message-and-transaction-demarcation-protocol-metadapt) +is in use. Method codes should be written in upper-case base 16 with the prefix "M" in logs, error messages, documentation, etc. For example, the method code 62,206 in -- 2.46.1 From 835d6230873c1d89a986ff6d0d9b03d43a379baf Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 15 May 2025 17:49:29 -0400 Subject: [PATCH 037/132] Change the protocol definition for tape to conform to #2 --- design/protocol.md | 68 +++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index 33daca2..ab21173 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -35,25 +35,17 @@ fucking with you. ## Table Pair Encoding (TAPE) The Table Pair Encoding (TAPE) scheme is a method for encoding structured data within HOPP messages. It defines standard binary encoding methods for common -data types, as well as a corruption-resistant table structure that maps numeric -IDs to values. It is designed to allow applications to be presented with data -they are not equipped to handle while continuing to function normally. This -enables backwards compatibile application protocol changes. +data types, as well as aggregate data types such as tables and arrays. It is +designed to allow applications to be presented with data they are not equipped +to handle while continuing to function normally. This enables backwards +compatibile application protocol changes. -### Table Structure -A table is divided into two sections: the header, and the values. The header -begins with the number (U16) of pairs in the table, which is then followed by -that many tag-offset pairs. A tag-offset pair consists of a numerical (U16) tag, -followed the position (U16) of the value relative to the start of the values -section. The values section contains the value data for each pair, where the -start of each value is determined by its offset, and the end is determined by -the offset of the next value, or the end of the message if there is no value -after it. - -Both sections must be in the same order, and because of this, each value offset -must be greater than or equal to the last. If a message has erratic structure -(such as unordered or out-of-bounds offsets), implementations may opt to discard -only the erratic pairs, as well as the pairs directly before those. +The length of a TAPE structure is assumed to be given by the surrounding +protocol, which is usually METADAPT-A or B. The root of a TAPE structure can be +any data value, but is usually a table, which can contain several values that +each have a numeric key. Values can also be nested. Both sides of the connection +must agree on what data type should be the root value, the data type of each +known table value, etc. ### Data Value Types The table below lists all data value types supported by TAPE. @@ -68,9 +60,10 @@ The table below lists all data value types supported by TAPE. | U16 | 2 | An unsigned 16-bit integer | BEU | U32 | 4 | An unsigned 32-bit integer | BEU | U64 | 8 | An unsigned 64-bit integer | BEU -| Array[^1] | SOP[^2] | An array of any above type | PASTA -| String | N/A | A UTF-8 string | UTF-8 -| StringArray | n * 2 + SOP[^2] | An array the String type | VILA +| Array[^1] | | An array of any above type | PASTA +| String | | A UTF-8 string | UTF-8 +| StringArray | | An array the String type | VILA +| Table | | A table of any type | TTLV [^1]: Array types are written as Array, where is the element type. For example, an array of I32 would be written as I32Array. StringArray still follows @@ -95,6 +88,15 @@ Big-Endian, Unsigned integer. The size is defined as the least amount of whole octets which can fit all bits in the integer, regardless if the bits are on or off. Therefore, the size cannot change at runtime. +#### GBEU +Growing Big-Endian, Unsigned integer. The integer is broken up into 8-bit +chunks, where the first bit of each chunk is a CCB. The chunk with its CCB set +to zero instead of one is the last chunk in the integer. Chunks are ordered from +most significant to least significant (big endian). The size is defined as the +least amount of whole octets which can fit all chunks of the integer. The size +of this type is not fixed and may change at runtime, so this needs to be +accounted for during use. + #### PASTA Packed Single-Type Array. The size is defined as the size of an individual item times the number of items. Items are placed one after the other with no gaps @@ -110,13 +112,23 @@ for during use. #### VILA Variable Item Length Array. The size is defined as the least amount of whole -octets which can fit each item plus one U16 per item. The size of this type is -not fixed and may change at runtime, so this needs to be accounted for during -use. The amount of items must be greater than zero. Items are each prefixed by -their size (in octets) encoded as a U16, and they are placed one after the other -with no gaps in-between them, except as required to align the start of each item -to the nearest whole octet. Items should be of the same type but do not need to -be of the same size. +octets which can fit each item plus one GBEU per item describing that item's +size. The size of this type is not fixed and may change at runtime, so this +needs to be accounted for during use. The amount of items must be greater than +zero. Items are each prefixed by their size (in octets) encoded as a GBEU, and +they are placed one after the other with no gaps in-between them, except as +required to align the start of each item to the nearest whole octet. Items +should be of the same type but do not need to be of the same size. + +#### TTLV +TAPE Tag Length Value. The size is defined as the least amount of whole octets +which can fit each item plus one U16 and one GBEU per item, where the latter of +which describes that item's size. The size of this type is not fixed and may +change at runtime, so this needs to be accounted for during use. Items are each +prefixed by their numerical tag encoded as a U16, and their size (in octets) +encoded as a GBEU. Items are placed one after the other with no gaps in-between +them, except as required to align the start of each item to the nearest whole +octet. Items need not be of the same type nor the same size. ## Transports A transport is a protocol that HOPP connections can run on top of. HOPP -- 2.46.1 From dd5e7e96d5262c529e0f3c81e589befca56cd614 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 15 May 2025 17:56:41 -0400 Subject: [PATCH 038/132] design: Remove note about this limitation --- design/protocol.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index ab21173..d5e41e2 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -67,10 +67,7 @@ The table below lists all data value types supported by TAPE. [^1]: Array types are written as Array, where is the element type. For example, an array of I32 would be written as I32Array. StringArray still follows -this rule, even though it is encoded differently from other arrays. Nesting -arrays inside of arrays is prohibited. This problem can be avoided in most cases -by effectively utilizing the table structure, or by improving the design of -your protocol. +this rule, even though it is encoded differently from other arrays. [^2]: SOP (sum of parts) refers to the sum of the size of every item in a data structure. -- 2.46.1 From 1b25e306a67976cc52fd68820092b38a38621e5d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 16 May 2025 21:27:11 -0400 Subject: [PATCH 039/132] tape: Add GBEU encoding/decoding support --- tape/types.go | 48 ++++++++++++++++++++ tape/types_test.go | 110 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/tape/types.go b/tape/types.go index 32a2e59..55a19ec 100644 --- a/tape/types.go +++ b/tape/types.go @@ -10,6 +10,7 @@ const uint16Max = 0xFFFF type Error string; const ( ErrWrongBufferLength Error = "wrong buffer length" ErrDataTooLarge Error = "data too large" + ErrGBEUNotTerminated Error = "GBEU not terminated" ) // Error implements the error interface. @@ -25,6 +26,8 @@ type Int16 interface { ~uint16 | ~int16 } type Int32 interface { ~uint32 | ~int32 } // Int64 is any 64-bit integer. type Int64 interface { ~uint64 | ~int64 } +// UInt is any unsigned integer. +type UInt interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } // String is any string. type String interface { ~string } @@ -101,6 +104,51 @@ func EncodeI64[T Int64](buffer []byte, value T) error { return nil } +// DecodeGBEU decodes an 8 to 64 bit growing integer into the given buffer. +func DecodeGBEU[T UInt](data []byte) (T, error) { + var value uint64 + for _, chunk := range data { + value *= 0x80 + value += uint64(chunk & 0x7F) + ccb := chunk >> 7 + if ccb == 0 { + return T(value), nil + } + } + return 0, fmt.Errorf("decoding GBEU: %w", ErrGBEUNotTerminated) +} + +// EncodeGBEU encodes an 8 to 64 bit growing integer into a given buffer. +func EncodeGBEU[T UInt] (buffer []byte, value T) (error) { + window := (GBEUSize(value) - 1) * 7 + + index := 0 + for window >= 0 { + if index >= len(buffer) { return fmt.Errorf("encoding GBEU: %w", ErrWrongBufferLength) } + + chunk := uint8(value >> window) & 0x7F + if window > 0 { + chunk |= 0x80 + } else { + } + buffer[index] = chunk + + index += 1 + window -= 7 + } + return nil +} + +// GBEUSize returns the size (in octets) of a GBEU integer. +func GBEUSize[T UInt] (value T) int { + length := 0 + for { + value >>= 7 + length ++ + if value == 0 { return length } + } +} + // DecodeString decodes a string from the given data. func DecodeString[T String](data []byte) (T, error) { return T(data), nil diff --git a/tape/types_test.go b/tape/types_test.go index 994a586..e80b23b 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -1,5 +1,6 @@ package tape +import "fmt" import "slices" import "errors" import "testing" @@ -99,6 +100,115 @@ func TestI64(test *testing.T) { } } +func TestGBEU(test *testing.T) { + var buffer = [16]byte { + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + } + err := EncodeGBEU[uint64](buffer[:0], 5) + if err == nil { test.Fatal("no error") } + if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } + err = EncodeGBEU[uint64](buffer[:2], 5555555) + if err == nil { test.Fatal("no error") } + if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } + _, err = DecodeGBEU[uint64](buffer[:0]) + if err == nil { test.Fatal("no error") } + if err.Error() != "decoding GBEU: GBEU not terminated" { test.Fatal(err) } + + err = EncodeGBEU[uint64](buffer[:], 0x97) + if err != nil { test.Fatal(err) } + if correct, got := []byte { 0x81, 0x17 }, buffer[:2]; !slices.Equal(correct, got) { + message := "not equal:" + for _, item := range got { + message = fmt.Sprintf("%s %x", message, item) + } + test.Fatal(message) + } + decoded, err := DecodeGBEU[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + if correct, got := uint64(0x97), decoded; correct != got { + test.Fatalf("not equal: %x", got) + } + + err = EncodeGBEU[uint64](buffer[:], 0x123456) + if err != nil { test.Fatal(err) } + if correct, got := []byte { 0xc8, 0xe8, 0x56 }, buffer[:3]; !slices.Equal(correct, got) { + message := "not equal:" + for _, item := range got { + message = fmt.Sprintf("%s %x", message, item) + } + test.Fatal(message) + } + decoded, err = DecodeGBEU[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + if correct, got := uint64(0x123456), decoded; correct != got { + test.Fatalf("not equal: %x", got) + } + + maxGBEU64 := []byte { 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } + err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) + if err != nil { test.Fatal(err) } + if correct, got := maxGBEU64, buffer[:10]; !slices.Equal(correct, got) { + message := "not equal:" + for _, item := range got { + message = fmt.Sprintf("%s %x", message, item) + } + test.Fatal(message) + } + decoded, err = DecodeGBEU[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + if correct, got := uint64(0xFFFFFFFFFFFFFFFF), decoded; correct != got { + test.Fatalf("not equal: %x", got) + } + + err = EncodeGBEU[uint64](buffer[:], 11) + if err != nil { test.Fatal(err) } + if correct, got := byte(0xb), buffer[0]; correct != got { + test.Fatal("not equal:", got) + } + decoded, err = DecodeGBEU[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + if correct, got := uint64(0xb), decoded; correct != got { + test.Fatalf("not equal: %x", got) + } + + + for _ = range largeNumberNTestRounds { + buffer = [16]byte { } + number := uint64(rand.Int()) + err := EncodeGBEU[uint64](buffer[:], number) + if err != nil { test.Fatal(err) } + decoded, err := DecodeGBEU[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + if decoded != number { + test.Error("not equal:") + test.Errorf("%d != %d", decoded, number) + test.Errorf("%x != %x", decoded, number) + test.Fatal(buffer) + } + } +} + +func TestGBEUSize(test *testing.T) { + if correct, got := 3, GBEUSize(uint(0x100000)); correct != got { + test.Fatal("not equal:", got) + } + if correct, got := 3, GBEUSize(uint(0x123456)); correct != got { + test.Fatal("not equal:", got) + } + if correct, got := 4, GBEUSize(uint(0x223456)); correct != got { + test.Fatal("not equal:", got) + } + if correct, got := 1, GBEUSize(uint(0)); correct != got { + test.Fatal("not equal:", got) + } + if correct, got := 1, GBEUSize(uint(127)); correct != got { + test.Fatal("not equal:", got) + } +} + func TestString(test *testing.T) { var buffer [16]byte err := EncodeString[string](buffer[:], "hello") -- 2.46.1 From a05c0343133f49393087c7502379fea118985ee4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 16 May 2025 21:42:40 -0400 Subject: [PATCH 040/132] tape: Clean up --- tape/types.go | 1 - tape/types_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/tape/types.go b/tape/types.go index 55a19ec..82a0a15 100644 --- a/tape/types.go +++ b/tape/types.go @@ -129,7 +129,6 @@ func EncodeGBEU[T UInt] (buffer []byte, value T) (error) { chunk := uint8(value >> window) & 0x7F if window > 0 { chunk |= 0x80 - } else { } buffer[index] = chunk diff --git a/tape/types_test.go b/tape/types_test.go index e80b23b..5c684d9 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -174,7 +174,6 @@ func TestGBEU(test *testing.T) { test.Fatalf("not equal: %x", got) } - for _ = range largeNumberNTestRounds { buffer = [16]byte { } number := uint64(rand.Int()) -- 2.46.1 From 8d5ba2fa395d4caa5d46591055938245ce6fedfd Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 17 May 2025 10:38:50 -0400 Subject: [PATCH 041/132] tape: EncodeGBEU returns the amount of bytes written --- tape/types.go | 8 +++++--- tape/types_test.go | 14 +++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tape/types.go b/tape/types.go index 82a0a15..3d56c88 100644 --- a/tape/types.go +++ b/tape/types.go @@ -3,6 +3,8 @@ package tape import "fmt" +// encoding and decoding functions must not make any allocations + const dataMaxSize = 0xFFFF const uint16Max = 0xFFFF @@ -119,12 +121,12 @@ func DecodeGBEU[T UInt](data []byte) (T, error) { } // EncodeGBEU encodes an 8 to 64 bit growing integer into a given buffer. -func EncodeGBEU[T UInt] (buffer []byte, value T) (error) { +func EncodeGBEU[T UInt] (buffer []byte, value T) (n int, err error) { window := (GBEUSize(value) - 1) * 7 index := 0 for window >= 0 { - if index >= len(buffer) { return fmt.Errorf("encoding GBEU: %w", ErrWrongBufferLength) } + if index >= len(buffer) { return index, fmt.Errorf("encoding GBEU: %w", ErrWrongBufferLength) } chunk := uint8(value >> window) & 0x7F if window > 0 { @@ -135,7 +137,7 @@ func EncodeGBEU[T UInt] (buffer []byte, value T) (error) { index += 1 window -= 7 } - return nil + return index, nil } // GBEUSize returns the size (in octets) of a GBEU integer. diff --git a/tape/types_test.go b/tape/types_test.go index 5c684d9..13a1078 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -107,17 +107,17 @@ func TestGBEU(test *testing.T) { 255, 255, 255, 255, 255, 255, 255, 255, } - err := EncodeGBEU[uint64](buffer[:0], 5) + _, err := EncodeGBEU[uint64](buffer[:0], 5) if err == nil { test.Fatal("no error") } if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } - err = EncodeGBEU[uint64](buffer[:2], 5555555) + _, err = EncodeGBEU[uint64](buffer[:2], 5555555) if err == nil { test.Fatal("no error") } if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } _, err = DecodeGBEU[uint64](buffer[:0]) if err == nil { test.Fatal("no error") } if err.Error() != "decoding GBEU: GBEU not terminated" { test.Fatal(err) } - err = EncodeGBEU[uint64](buffer[:], 0x97) + _, err = EncodeGBEU[uint64](buffer[:], 0x97) if err != nil { test.Fatal(err) } if correct, got := []byte { 0x81, 0x17 }, buffer[:2]; !slices.Equal(correct, got) { message := "not equal:" @@ -132,7 +132,7 @@ func TestGBEU(test *testing.T) { test.Fatalf("not equal: %x", got) } - err = EncodeGBEU[uint64](buffer[:], 0x123456) + _, err = EncodeGBEU[uint64](buffer[:], 0x123456) if err != nil { test.Fatal(err) } if correct, got := []byte { 0xc8, 0xe8, 0x56 }, buffer[:3]; !slices.Equal(correct, got) { message := "not equal:" @@ -148,7 +148,7 @@ func TestGBEU(test *testing.T) { } maxGBEU64 := []byte { 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } - err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) + _, err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) if err != nil { test.Fatal(err) } if correct, got := maxGBEU64, buffer[:10]; !slices.Equal(correct, got) { message := "not equal:" @@ -163,7 +163,7 @@ func TestGBEU(test *testing.T) { test.Fatalf("not equal: %x", got) } - err = EncodeGBEU[uint64](buffer[:], 11) + _, err = EncodeGBEU[uint64](buffer[:], 11) if err != nil { test.Fatal(err) } if correct, got := byte(0xb), buffer[0]; correct != got { test.Fatal("not equal:", got) @@ -177,7 +177,7 @@ func TestGBEU(test *testing.T) { for _ = range largeNumberNTestRounds { buffer = [16]byte { } number := uint64(rand.Int()) - err := EncodeGBEU[uint64](buffer[:], number) + _, err := EncodeGBEU[uint64](buffer[:], number) if err != nil { test.Fatal(err) } decoded, err := DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } -- 2.46.1 From 44382109635385a1261404d1aaf3c0afdd2dac84 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 17 May 2025 23:55:56 -0400 Subject: [PATCH 042/132] tape: Add N length support to all types --- tape/pairs.go | 8 +-- tape/types.go | 146 +++++++++++++++++++++++---------------------- tape/types_test.go | 130 ++++++++++++++++++++-------------------- 3 files changed, 143 insertions(+), 141 deletions(-) diff --git a/tape/pairs.go b/tape/pairs.go index d51e593..7a5bd24 100644 --- a/tape/pairs.go +++ b/tape/pairs.go @@ -9,7 +9,7 @@ import "iter" func DecodePairs(data []byte) (iter.Seq2[uint16, []byte], error) { // determine section bounds if len(data) < 2 { return nil, ErrDataTooLarge } - length16, _ := DecodeI16[uint16](data[0:2]) + length16, _, _ := DecodeI16[uint16](data[0:2]) data = data[2:] length := int(length16) headerSize := length * 4 @@ -20,7 +20,7 @@ func DecodePairs(data []byte) (iter.Seq2[uint16, []byte], error) { var valuesSize int for index := range length { offset := index * 4 - end, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) + end, _, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) valuesSize = int(end) } if valuesSize > len(valuesData) { @@ -32,8 +32,8 @@ func DecodePairs(data []byte) (iter.Seq2[uint16, []byte], error) { start := uint16(0) for index := range length { offset := index * 4 - key , _ := DecodeI16[uint16](data[offset + 0:offset + 2]) - end, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) + key, _, _ := DecodeI16[uint16](data[offset + 0:offset + 2]) + end, _, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) // if nextValuesOffset < len(valuesData) { if !yield(key, valuesData[start:end]) { return diff --git a/tape/types.go b/tape/types.go index 3d56c88..527f214 100644 --- a/tape/types.go +++ b/tape/types.go @@ -34,54 +34,54 @@ type UInt interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } type String interface { ~string } // DecodeI8 decodes an 8 bit integer from the given data. -func DecodeI8[T Int8](data []byte) (T, error) { - if len(data) != 1 { return 0, fmt.Errorf("decoding int8: %w", ErrWrongBufferLength) } - return T(data[0]), nil +func DecodeI8[T Int8](data []byte) (value T, n int, err error) { + if len(data) != 1 { return 0, 0, fmt.Errorf("decoding int8: %w", ErrWrongBufferLength) } + return T(data[0]), 1, nil } // EncodeI8 encodes an 8 bit integer into the given buffer. -func EncodeI8[T Int8](buffer []byte, value T) error { - if len(buffer) != 1 { return fmt.Errorf("encoding int8: %w", ErrWrongBufferLength) } +func EncodeI8[T Int8](buffer []byte, value T) (n int, err error) { + if len(buffer) != 1 { return 0, fmt.Errorf("encoding int8: %w", ErrWrongBufferLength) } buffer[0] = byte(value) - return nil + return 1, nil } // DecodeI16 decodes a 16 bit integer from the given data. -func DecodeI16[T Int16](data []byte) (T, error) { - if len(data) != 2 { return 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) } - return T(data[0]) << 8 | T(data[1]), nil +func DecodeI16[T Int16](data []byte) (value T, n int, err error) { + if len(data) != 2 { return 0, 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) } + return T(data[0]) << 8 | T(data[1]), 2, nil } // EncodeI16 encodes a 16 bit integer into the given buffer. -func EncodeI16[T Int16](buffer []byte, value T) error { - if len(buffer) != 2 { return fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) } +func EncodeI16[T Int16](buffer []byte, value T) (n int, err error) { + if len(buffer) != 2 { return 0, fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 8) buffer[1] = byte(value) - return nil + return 2, nil } // DecodeI32 decodes a 32 bit integer from the given data. -func DecodeI32[T Int32](data []byte) (T, error) { - if len(data) != 4 { return 0, fmt.Errorf("decoding int32: %w", ErrWrongBufferLength) } +func DecodeI32[T Int32](data []byte) (value T, n int, err error) { + if len(data) != 4 { return 0, 0, fmt.Errorf("decoding int32: %w", ErrWrongBufferLength) } return T(data[0]) << 24 | T(data[1]) << 16 | T(data[2]) << 8 | - T(data[3]), nil + T(data[3]), 4, nil } // EncodeI32 encodes a 32 bit integer into the given buffer. -func EncodeI32[T Int32](buffer []byte, value T) error { - if len(buffer) != 4 { return fmt.Errorf("encoding int32: %w", ErrWrongBufferLength) } +func EncodeI32[T Int32](buffer []byte, value T) (n int, err error) { + if len(buffer) != 4 { return 0, fmt.Errorf("encoding int32: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 24) buffer[1] = byte(value >> 16) buffer[2] = byte(value >> 8) buffer[3] = byte(value) - return nil + return 4, nil } // DecodeI64 decodes a 64 bit integer from the given data. -func DecodeI64[T Int64](data []byte) (T, error) { - if len(data) != 8 { return 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) } +func DecodeI64[T Int64](data []byte) (value T, n int, err error) { + if len(data) != 8 { return 0, 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) } return T(data[0]) << 56 | T(data[1]) << 48 | T(data[2]) << 40 | @@ -89,12 +89,12 @@ func DecodeI64[T Int64](data []byte) (T, error) { T(data[4]) << 24 | T(data[5]) << 16 | T(data[6]) << 8 | - T(data[7]), nil + T(data[7]), 8, nil } // EncodeI64 encodes a 64 bit integer into the given buffer. -func EncodeI64[T Int64](buffer []byte, value T) error { - if len(buffer) != 8 { return fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) } +func EncodeI64[T Int64](buffer []byte, value T) (n int, err error) { + if len(buffer) != 8 { return 0, fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 56) buffer[1] = byte(value >> 48) buffer[2] = byte(value >> 40) @@ -103,21 +103,22 @@ func EncodeI64[T Int64](buffer []byte, value T) error { buffer[5] = byte(value >> 16) buffer[6] = byte(value >> 8) buffer[7] = byte(value) - return nil + return 8, nil } // DecodeGBEU decodes an 8 to 64 bit growing integer into the given buffer. -func DecodeGBEU[T UInt](data []byte) (T, error) { - var value uint64 +func DecodeGBEU[T UInt](data []byte) (value T, n int, err error) { + var fullValue uint64 for _, chunk := range data { - value *= 0x80 - value += uint64(chunk & 0x7F) + fullValue *= 0x80 + fullValue += uint64(chunk & 0x7F) ccb := chunk >> 7 if ccb == 0 { - return T(value), nil + return T(fullValue), n, nil } + n += 1 } - return 0, fmt.Errorf("decoding GBEU: %w", ErrGBEUNotTerminated) + return 0, n, fmt.Errorf("decoding GBEU: %w", ErrGBEUNotTerminated) } // EncodeGBEU encodes an 8 to 64 bit growing integer into a given buffer. @@ -151,15 +152,14 @@ func GBEUSize[T UInt] (value T) int { } // DecodeString decodes a string from the given data. -func DecodeString[T String](data []byte) (T, error) { - return T(data), nil +func DecodeString[T String](data []byte) (value T, n int, err error) { + return T(data), len(data), nil } // EncodeString encodes a string into the given buffer. -func EncodeString[T String](data []byte, value T) error { - if len(data) != len(value) { return fmt.Errorf("encoding string: %w", ErrWrongBufferLength) } - copy(data, value) - return nil +func EncodeString[T String](data []byte, value T) (n int, err error) { + if len(data) != len(value) { return 0, fmt.Errorf("encoding string: %w", ErrWrongBufferLength) } + return copy(data, value), nil } // StringSize returns the size of a string. Returns 0 and an error if the size @@ -170,33 +170,35 @@ func StringSize[T String](value T) (int, error) { } // DecodeStringArray decodes a packed string array from the given data. -func DecodeStringArray[T String](data []byte) ([]T, error) { - result := []T { } +func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { for len(data) > 0 { - if len(data) < 2 { return nil, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } - itemSize16, _ := DecodeI16[uint16](data[:2]) + if len(data) < 2 { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } + itemSize16, nn, _ := DecodeI16[uint16](data[:2]) itemSize := int(itemSize16) - data = data[2:] - if len(data) < itemSize { return nil, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } + n += nn + data = data[nn:] + if len(data) < itemSize { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } result = append(result, T(data[:itemSize])) data = data[itemSize:] + n += itemSize } - return result, nil + return result, n, nil } // EncodeStringArray encodes a packed string array into the given buffer. -func EncodeStringArray[T String](buffer []byte, value []T) error { +func EncodeStringArray[T String](buffer []byte, value []T) (n int, err error) { for _, item := range value { length, err := StringSize(item) - if err != nil { return err } - if len(buffer) < 2 + length { return fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } + if err != nil { return n, err } + if len(buffer) < 2 + length { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } EncodeI16(buffer[:2], uint16(length)) buffer = buffer[2:] copy(buffer, item) buffer = buffer[length:] + n += 2 + length } - if len(buffer) > 0 { return fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } - return nil + if len(buffer) > 0 { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } + return n, nil } // StringArraySize returns the size of a packed string array. Returns 0 and an @@ -211,21 +213,21 @@ func StringArraySize[T String](value []T) (int, error) { } // DecodeI8Array decodes a packed array of 8 bit integers from the given data. -func DecodeI8Array[T Int8](data []byte) ([]T, error) { - result := make([]T, len(data)) +func DecodeI8Array[T Int8](data []byte) (result []T, n int, err error) { + result = make([]T, len(data)) for index, item := range data { result[index] = T(item) } - return result, nil + return result, len(data), nil } // EncodeI8Array encodes a packed array of 8 bit integers into the given buffer. -func EncodeI8Array[T Int8](buffer []byte, value []T) error { - if len(buffer) != len(value) { return fmt.Errorf("encoding []int8: %w", ErrWrongBufferLength) } +func EncodeI8Array[T Int8](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) { return 0, fmt.Errorf("encoding []int8: %w", ErrWrongBufferLength) } for index, item := range value { buffer[index] = byte(item) } - return nil + return len(buffer), nil } // I8ArraySize returns the size of a packed 8 bit integer array. Returns 0 and @@ -237,26 +239,26 @@ func I8ArraySize[T Int8](value []T) (int, error) { } // DecodeI16Array decodes a packed array of 16 bit integers from the given data. -func DecodeI16Array[T Int16](data []byte) ([]T, error) { - if len(data) % 2 != 0 { return nil, fmt.Errorf("decoding []int16: %w", ErrWrongBufferLength) } +func DecodeI16Array[T Int16](data []byte) (value []T, n int, err error) { + if len(data) % 2 != 0 { return nil, 0, fmt.Errorf("decoding []int16: %w", ErrWrongBufferLength) } length := len(data) / 2 result := make([]T, length) for index := range length { offset := index * 2 result[index] = T(data[offset]) << 8 | T(data[offset + 1]) } - return result, nil + return result, len(data) / 2, nil } // EncodeI16Array encodes a packed array of 16 bit integers into the given buffer. -func EncodeI16Array[T Int16](buffer []byte, value []T) error { - if len(buffer) != len(value) * 2 { return fmt.Errorf("encoding []int16: %w", ErrWrongBufferLength) } +func EncodeI16Array[T Int16](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 2 { return 0, fmt.Errorf("encoding []int16: %w", ErrWrongBufferLength) } for _, item := range value { buffer[0] = byte(item >> 8) buffer[1] = byte(item) buffer = buffer[2:] } - return nil + return len(value) * 2, nil } // I16ArraySize returns the size of a packed 16 bit integer array. Returns 0 and @@ -268,8 +270,8 @@ func I16ArraySize[T Int16](value []T) (int, error) { } // DecodeI32Array decodes a packed array of 32 bit integers from the given data. -func DecodeI32Array[T Int32](data []byte) ([]T, error) { - if len(data) % 4 != 0 { return nil, fmt.Errorf("decoding []int32: %w", ErrWrongBufferLength) } +func DecodeI32Array[T Int32](data []byte) (value []T, n int, err error) { + if len(data) % 4 != 0 { return nil, 0, fmt.Errorf("decoding []int32: %w", ErrWrongBufferLength) } length := len(data) / 4 result := make([]T, length) for index := range length { @@ -280,12 +282,12 @@ func DecodeI32Array[T Int32](data []byte) ([]T, error) { T(data[offset + 2]) << 8 | T(data[offset + 3]) } - return result, nil + return result, len(data) / 4, nil } // EncodeI32Array encodes a packed array of 32 bit integers into the given buffer. -func EncodeI32Array[T Int32](buffer []byte, value []T) error { - if len(buffer) != len(value) * 4 { return fmt.Errorf("encoding []int32: %w", ErrWrongBufferLength) } +func EncodeI32Array[T Int32](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 4 { return 0, fmt.Errorf("encoding []int32: %w", ErrWrongBufferLength) } for _, item := range value { buffer[0] = byte(item >> 24) buffer[1] = byte(item >> 16) @@ -293,7 +295,7 @@ func EncodeI32Array[T Int32](buffer []byte, value []T) error { buffer[3] = byte(item) buffer = buffer[4:] } - return nil + return len(value) * 4, nil } // I32ArraySize returns the size of a packed 32 bit integer array. Returns 0 and @@ -305,8 +307,8 @@ func I32ArraySize[T Int32](value []T) (int, error) { } // DecodeI64Array decodes a packed array of 32 bit integers from the given data. -func DecodeI64Array[T Int64](data []byte) ([]T, error) { - if len(data) % 8 != 0 { return nil, fmt.Errorf("decoding []int64: %w", ErrWrongBufferLength) } +func DecodeI64Array[T Int64](data []byte) (value []T, n int, err error) { + if len(data) % 8 != 0 { return nil, 0, fmt.Errorf("decoding []int64: %w", ErrWrongBufferLength) } length := len(data) / 8 result := make([]T, length) for index := range length { @@ -321,12 +323,12 @@ func DecodeI64Array[T Int64](data []byte) ([]T, error) { T(data[offset + 6]) << 8 | T(data[offset + 7]) } - return result, nil + return result, len(data) / 8, nil } // EncodeI64Array encodes a packed array of 64 bit integers into the given buffer. -func EncodeI64Array[T Int64](buffer []byte, value []T) error { - if len(buffer) != len(value) * 8 { return fmt.Errorf("encoding []int64: %w", ErrWrongBufferLength) } +func EncodeI64Array[T Int64](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 8 { return 0, fmt.Errorf("encoding []int64: %w", ErrWrongBufferLength) } for _, item := range value { buffer[0] = byte(item >> 56) buffer[1] = byte(item >> 48) @@ -338,7 +340,7 @@ func EncodeI64Array[T Int64](buffer []byte, value []T) error { buffer[7] = byte(item) buffer = buffer[8:] } - return nil + return len(value) * 8, nil } // I64ArraySize returns the size of a packed 64 bit integer array. Returns 0 and diff --git a/tape/types_test.go b/tape/types_test.go index 13a1078..5daf5fd 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -11,19 +11,19 @@ const randStringBytes = "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU func TestI8(test *testing.T) { var buffer [16]byte - err := EncodeI8[uint8](buffer[:], 5) + _, err := EncodeI8[uint8](buffer[:], 5) if err.Error() != "encoding int8: wrong buffer length" { test.Fatal(err) } - err = EncodeI8[uint8](buffer[:0], 5) + _, err = EncodeI8[uint8](buffer[:0], 5) if err.Error() != "encoding int8: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI8[uint8](buffer[:]) + _, _, err = DecodeI8[uint8](buffer[:]) if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI8[uint8](buffer[:0]) + _, _, err = DecodeI8[uint8](buffer[:0]) if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } for number := range uint8(255) { - err := EncodeI8[uint8](buffer[:1], number) + _, err := EncodeI8[uint8](buffer[:1], number) if err != nil { test.Fatal(err) } - decoded, err := DecodeI8[uint8](buffer[:1]) + decoded, _, err := DecodeI8[uint8](buffer[:1]) if err != nil { test.Fatal(err) } if decoded != number { test.Fatalf("%d != %d", decoded, number) @@ -33,20 +33,20 @@ func TestI8(test *testing.T) { func TestI16(test *testing.T) { var buffer [16]byte - err := EncodeI16[uint16](buffer[:], 5) + _, err := EncodeI16[uint16](buffer[:], 5) if err.Error() != "encoding int16: wrong buffer length" { test.Fatal(err) } - err = EncodeI16[uint16](buffer[:0], 5) + _, err = EncodeI16[uint16](buffer[:0], 5) if err.Error() != "encoding int16: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI16[uint16](buffer[:]) + _, _, err = DecodeI16[uint16](buffer[:]) if err.Error() != "decoding int16: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI16[uint16](buffer[:0]) + _, _, err = DecodeI16[uint16](buffer[:0]) if err.Error() != "decoding int16: wrong buffer length" { test.Fatal(err) } for _ = range largeNumberNTestRounds { number := uint16(rand.Int()) - err := EncodeI16[uint16](buffer[:2], number) + _, err := EncodeI16[uint16](buffer[:2], number) if err != nil { test.Fatal(err) } - decoded, err := DecodeI16[uint16](buffer[:2]) + decoded, _, err := DecodeI16[uint16](buffer[:2]) if err != nil { test.Fatal(err) } if decoded != number { test.Fatalf("%d != %d", decoded, number) @@ -56,20 +56,20 @@ func TestI16(test *testing.T) { func TestI32(test *testing.T) { var buffer [16]byte - err := EncodeI32[uint32](buffer[:], 5) + _, err := EncodeI32[uint32](buffer[:], 5) if err.Error() != "encoding int32: wrong buffer length" { test.Fatal(err) } - err = EncodeI32[uint32](buffer[:0], 5) + _, err = EncodeI32[uint32](buffer[:0], 5) if err.Error() != "encoding int32: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI32[uint32](buffer[:]) + _, _, err = DecodeI32[uint32](buffer[:]) if err.Error() != "decoding int32: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI32[uint32](buffer[:0]) + _, _, err = DecodeI32[uint32](buffer[:0]) if err.Error() != "decoding int32: wrong buffer length" { test.Fatal(err) } for _ = range largeNumberNTestRounds { number := uint32(rand.Int()) - err := EncodeI32[uint32](buffer[:4], number) + _, err := EncodeI32[uint32](buffer[:4], number) if err != nil { test.Fatal(err) } - decoded, err := DecodeI32[uint32](buffer[:4]) + decoded, _, err := DecodeI32[uint32](buffer[:4]) if err != nil { test.Fatal(err) } if decoded != number { test.Fatalf("%d != %d", decoded, number) @@ -79,20 +79,20 @@ func TestI32(test *testing.T) { func TestI64(test *testing.T) { var buffer [16]byte - err := EncodeI64[uint64](buffer[:], 5) + _, err := EncodeI64[uint64](buffer[:], 5) if err.Error() != "encoding int64: wrong buffer length" { test.Fatal(err) } - err = EncodeI64[uint64](buffer[:0], 5) + _, err = EncodeI64[uint64](buffer[:0], 5) if err.Error() != "encoding int64: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI64[uint64](buffer[:]) + _, _, err = DecodeI64[uint64](buffer[:]) if err.Error() != "decoding int64: wrong buffer length" { test.Fatal(err) } - _, err = DecodeI64[uint64](buffer[:0]) + _, _, err = DecodeI64[uint64](buffer[:0]) if err.Error() != "decoding int64: wrong buffer length" { test.Fatal(err) } for _ = range largeNumberNTestRounds { number := uint64(rand.Int()) - err := EncodeI64[uint64](buffer[:8], number) + _, err := EncodeI64[uint64](buffer[:8], number) if err != nil { test.Fatal(err) } - decoded, err := DecodeI64[uint64](buffer[:8]) + decoded, _, err := DecodeI64[uint64](buffer[:8]) if err != nil { test.Fatal(err) } if decoded != number { test.Fatalf("%d != %d", decoded, number) @@ -113,7 +113,7 @@ func TestGBEU(test *testing.T) { _, err = EncodeGBEU[uint64](buffer[:2], 5555555) if err == nil { test.Fatal("no error") } if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } - _, err = DecodeGBEU[uint64](buffer[:0]) + _, _, err = DecodeGBEU[uint64](buffer[:0]) if err == nil { test.Fatal("no error") } if err.Error() != "decoding GBEU: GBEU not terminated" { test.Fatal(err) } @@ -126,7 +126,7 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, err := DecodeGBEU[uint64](buffer[:]) + decoded, _, err := DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := uint64(0x97), decoded; correct != got { test.Fatalf("not equal: %x", got) @@ -141,7 +141,7 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, err = DecodeGBEU[uint64](buffer[:]) + decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := uint64(0x123456), decoded; correct != got { test.Fatalf("not equal: %x", got) @@ -157,7 +157,7 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, err = DecodeGBEU[uint64](buffer[:]) + decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := uint64(0xFFFFFFFFFFFFFFFF), decoded; correct != got { test.Fatalf("not equal: %x", got) @@ -168,7 +168,7 @@ func TestGBEU(test *testing.T) { if correct, got := byte(0xb), buffer[0]; correct != got { test.Fatal("not equal:", got) } - decoded, err = DecodeGBEU[uint64](buffer[:]) + decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := uint64(0xb), decoded; correct != got { test.Fatalf("not equal: %x", got) @@ -179,7 +179,7 @@ func TestGBEU(test *testing.T) { number := uint64(rand.Int()) _, err := EncodeGBEU[uint64](buffer[:], number) if err != nil { test.Fatal(err) } - decoded, err := DecodeGBEU[uint64](buffer[:]) + decoded, _, err := DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if decoded != number { test.Error("not equal:") @@ -210,21 +210,21 @@ func TestGBEUSize(test *testing.T) { func TestString(test *testing.T) { var buffer [16]byte - err := EncodeString[string](buffer[:], "hello") + _, err := EncodeString[string](buffer[:], "hello") if !errIs(err, ErrWrongBufferLength, "encoding string: wrong buffer length") { test.Fatal(err) } - err = EncodeString[string](buffer[:0], "hello") + _, err = EncodeString[string](buffer[:0], "hello") if !errIs(err, ErrWrongBufferLength, "encoding string: wrong buffer length") { test.Fatal(err) } - _, err = DecodeString[string](buffer[:]) + _, _, err = DecodeString[string](buffer[:]) if err != nil { test.Fatal(err) } - _, err = DecodeString[string](buffer[:0]) + _, _, err = DecodeString[string](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { length := rand.Intn(16) str := randString(length) - err := EncodeString[string](buffer[:length], str) + _, err := EncodeString[string](buffer[:length], str) if err != nil { test.Fatal(err) } - decoded, err := DecodeString[string](buffer[:length]) + decoded, _, err := DecodeString[string](buffer[:length]) if err != nil { test.Fatal(err) } if decoded != str { test.Fatalf("%s != %s", decoded, str) @@ -234,22 +234,22 @@ func TestString(test *testing.T) { func TestI8Array(test *testing.T) { var buffer [64]byte - err := EncodeI8Array[uint8](buffer[:], []uint8 { 0, 4, 50, 19 }) + _, err := EncodeI8Array[uint8](buffer[:], []uint8 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - err = EncodeI8Array[uint8](buffer[:0], []uint8 { 0, 4, 50, 19 }) + _, err = EncodeI8Array[uint8](buffer[:0], []uint8 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - _, err = DecodeI8Array[uint8](buffer[:]) + _, _, err = DecodeI8Array[uint8](buffer[:]) if err != nil { test.Fatal(err) } - _, err = DecodeI8Array[uint8](buffer[:0]) + _, _, err = DecodeI8Array[uint8](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { array := randInts[uint8](rand.Intn(16)) length, _ := I8ArraySize(array) if length != len(array) { test.Fatalf("%d != %d", length, len(array)) } - err := EncodeI8Array[uint8](buffer[:length], array) + _, err := EncodeI8Array[uint8](buffer[:length], array) if err != nil { test.Fatal(err) } - decoded, err := DecodeI8Array[uint8](buffer[:length]) + decoded, _, err := DecodeI8Array[uint8](buffer[:length]) if err != nil { test.Fatal(err) } if !slices.Equal(decoded, array) { test.Fatalf("%v != %v", decoded, array) @@ -259,22 +259,22 @@ func TestI8Array(test *testing.T) { func TestI16Array(test *testing.T) { var buffer [128]byte - err := EncodeI16Array[uint16](buffer[:], []uint16 { 0, 4, 50, 19 }) + _, err := EncodeI16Array[uint16](buffer[:], []uint16 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - err = EncodeI16Array[uint16](buffer[:0], []uint16 { 0, 4, 50, 19 }) + _, err = EncodeI16Array[uint16](buffer[:0], []uint16 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - _, err = DecodeI16Array[uint16](buffer[:]) + _, _, err = DecodeI16Array[uint16](buffer[:]) if err != nil { test.Fatal(err) } - _, err = DecodeI16Array[uint16](buffer[:0]) + _, _, err = DecodeI16Array[uint16](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { array := randInts[uint16](rand.Intn(16)) length, _ := I16ArraySize(array) if length != 2 * len(array) { test.Fatalf("%d != %d", length, 2 * len(array)) } - err := EncodeI16Array[uint16](buffer[:length], array) + _, err := EncodeI16Array[uint16](buffer[:length], array) if err != nil { test.Fatal(err) } - decoded, err := DecodeI16Array[uint16](buffer[:length]) + decoded, _, err := DecodeI16Array[uint16](buffer[:length]) if err != nil { test.Fatal(err) } if !slices.Equal(decoded, array) { test.Fatalf("%v != %v", decoded, array) @@ -284,22 +284,22 @@ func TestI16Array(test *testing.T) { func TestI32Array(test *testing.T) { var buffer [256]byte - err := EncodeI32Array[uint32](buffer[:], []uint32 { 0, 4, 50, 19 }) + _, err := EncodeI32Array[uint32](buffer[:], []uint32 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - err = EncodeI32Array[uint32](buffer[:0], []uint32 { 0, 4, 50, 19 }) + _, err = EncodeI32Array[uint32](buffer[:0], []uint32 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - _, err = DecodeI32Array[uint32](buffer[:]) + _, _, err = DecodeI32Array[uint32](buffer[:]) if err != nil { test.Fatal(err) } - _, err = DecodeI32Array[uint32](buffer[:0]) + _, _, err = DecodeI32Array[uint32](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { array := randInts[uint32](rand.Intn(16)) length, _ := I32ArraySize(array) if length != 4 * len(array) { test.Fatalf("%d != %d", length, 4 * len(array)) } - err := EncodeI32Array[uint32](buffer[:length], array) + _, err := EncodeI32Array[uint32](buffer[:length], array) if err != nil { test.Fatal(err) } - decoded, err := DecodeI32Array[uint32](buffer[:length]) + decoded, _, err := DecodeI32Array[uint32](buffer[:length]) if err != nil { test.Fatal(err) } if !slices.Equal(decoded, array) { test.Fatalf("%v != %v", decoded, array) @@ -309,22 +309,22 @@ func TestI32Array(test *testing.T) { func TestI64Array(test *testing.T) { var buffer [512]byte - err := EncodeI64Array[uint64](buffer[:], []uint64 { 0, 4, 50, 19 }) + _, err := EncodeI64Array[uint64](buffer[:], []uint64 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - err = EncodeI64Array[uint64](buffer[:0], []uint64 { 0, 4, 50, 19 }) + _, err = EncodeI64Array[uint64](buffer[:0], []uint64 { 0, 4, 50, 19 }) if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - _, err = DecodeI64Array[uint64](buffer[:]) + _, _, err = DecodeI64Array[uint64](buffer[:]) if err != nil { test.Fatal(err) } - _, err = DecodeI64Array[uint64](buffer[:0]) + _, _, err = DecodeI64Array[uint64](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { array := randInts[uint64](rand.Intn(16)) length, _ := I64ArraySize(array) if length != 8 * len(array) { test.Fatalf("%d != %d", length, 8 * len(array)) } - err := EncodeI64Array[uint64](buffer[:length], array) + _, err := EncodeI64Array[uint64](buffer[:length], array) if err != nil { test.Fatal(err) } - decoded, err := DecodeI64Array[uint64](buffer[:length]) + decoded, _, err := DecodeI64Array[uint64](buffer[:length]) if err != nil { test.Fatal(err) } if !slices.Equal(decoded, array) { test.Fatalf("%v != %v", decoded, array) @@ -334,20 +334,20 @@ func TestI64Array(test *testing.T) { func TestStringArray(test *testing.T) { var buffer [8192]byte - err := EncodeStringArray[string](buffer[:], []string { "0", "4", "50", "19" }) + _, err := EncodeStringArray[string](buffer[:], []string { "0", "4", "50", "19" }) if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - err = EncodeStringArray[string](buffer[:0], []string { "0", "4", "50", "19" }) + _, err = EncodeStringArray[string](buffer[:0], []string { "0", "4", "50", "19" }) if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - _, err = DecodeStringArray[string](buffer[:0]) + _, _, err = DecodeStringArray[string](buffer[:0]) if err != nil { test.Fatal(err) } for _ = range largeNumberNTestRounds { array := randStrings[string](rand.Intn(16), 16) length, _ := StringArraySize(array) // TODO test length - err := EncodeStringArray[string](buffer[:length], array) + _, err := EncodeStringArray[string](buffer[:length], array) if err != nil { test.Fatal(err) } - decoded, err := DecodeStringArray[string](buffer[:length]) + decoded, _, err := DecodeStringArray[string](buffer[:length]) if err != nil { test.Fatal(err) } if !slices.Equal(decoded, array) { test.Fatalf("%v != %v", decoded, array) -- 2.46.1 From c3337641bcd7041352350165500828033e9adc6e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 17 May 2025 23:58:07 -0400 Subject: [PATCH 043/132] tape: Break out array code into separate file --- tape/array.go | 206 +++++++++++++++++++++++++++++++++++++++++++++ tape/array_test.go | 131 ++++++++++++++++++++++++++++ tape/types.go | 182 --------------------------------------- 3 files changed, 337 insertions(+), 182 deletions(-) create mode 100644 tape/array.go create mode 100644 tape/array_test.go diff --git a/tape/array.go b/tape/array.go new file mode 100644 index 0000000..1d933e3 --- /dev/null +++ b/tape/array.go @@ -0,0 +1,206 @@ +package tape + +import "fmt" +import "iter" +import "slices" + +// encoding and decoding functions must not make any allocations + +func DecodeArray(data []byte, itemLength int) iter.Seq[[]byte] { + return slices.Chunk(data, itemLength) +} + +func EncodeArray(data []byte, items ...[]byte) (n int, err error) { + for _, item := range items { + if n >= len(data) { return n, ErrWrongBufferLength } + copy(data[n:], item) + n += len(item) + } + return n, nil +} + +func ArraySize(length, itemLength int) int { + return length * itemLength +} + +// DecodeStringArray decodes a packed string array from the given data. +func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { + for len(data) > 0 { + if len(data) < 2 { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } + itemSize16, nn, _ := DecodeI16[uint16](data[:2]) + itemSize := int(itemSize16) + n += nn + data = data[nn:] + if len(data) < itemSize { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } + result = append(result, T(data[:itemSize])) + data = data[itemSize:] + n += itemSize + } + return result, n, nil +} + +// EncodeStringArray encodes a packed string array into the given buffer. +func EncodeStringArray[T String](buffer []byte, value []T) (n int, err error) { + for _, item := range value { + length, err := StringSize(item) + if err != nil { return n, err } + if len(buffer) < 2 + length { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } + EncodeI16(buffer[:2], uint16(length)) + buffer = buffer[2:] + copy(buffer, item) + buffer = buffer[length:] + n += 2 + length + } + if len(buffer) > 0 { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } + return n, nil +} + +// StringArraySize returns the size of a packed string array. Returns 0 and an +// error if the size is too large. +func StringArraySize[T String](value []T) (int, error) { + total := 0 + for _, item := range value { + total += 2 + len(item) + } + if total > dataMaxSize { return 0, ErrDataTooLarge } + return total, nil +} + +// DecodeI8Array decodes a packed array of 8 bit integers from the given data. +func DecodeI8Array[T Int8](data []byte) (result []T, n int, err error) { + result = make([]T, len(data)) + for index, item := range data { + result[index] = T(item) + } + return result, len(data), nil +} + +// EncodeI8Array encodes a packed array of 8 bit integers into the given buffer. +func EncodeI8Array[T Int8](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) { return 0, fmt.Errorf("encoding []int8: %w", ErrWrongBufferLength) } + for index, item := range value { + buffer[index] = byte(item) + } + return len(buffer), nil +} + +// I8ArraySize returns the size of a packed 8 bit integer array. Returns 0 and +// an error if the size is too large. +func I8ArraySize[T Int8](value []T) (int, error) { + total := len(value) + if total > dataMaxSize { return 0, ErrDataTooLarge } + return total, nil +} + +// DecodeI16Array decodes a packed array of 16 bit integers from the given data. +func DecodeI16Array[T Int16](data []byte) (value []T, n int, err error) { + if len(data) % 2 != 0 { return nil, 0, fmt.Errorf("decoding []int16: %w", ErrWrongBufferLength) } + length := len(data) / 2 + result := make([]T, length) + for index := range length { + offset := index * 2 + result[index] = T(data[offset]) << 8 | T(data[offset + 1]) + } + return result, len(data) / 2, nil +} + +// EncodeI16Array encodes a packed array of 16 bit integers into the given buffer. +func EncodeI16Array[T Int16](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 2 { return 0, fmt.Errorf("encoding []int16: %w", ErrWrongBufferLength) } + for _, item := range value { + buffer[0] = byte(item >> 8) + buffer[1] = byte(item) + buffer = buffer[2:] + } + return len(value) * 2, nil +} + +// I16ArraySize returns the size of a packed 16 bit integer array. Returns 0 and +// an error if the size is too large. +func I16ArraySize[T Int16](value []T) (int, error) { + total := len(value) * 2 + if total > dataMaxSize { return 0, ErrDataTooLarge } + return total, nil +} + +// DecodeI32Array decodes a packed array of 32 bit integers from the given data. +func DecodeI32Array[T Int32](data []byte) (value []T, n int, err error) { + if len(data) % 4 != 0 { return nil, 0, fmt.Errorf("decoding []int32: %w", ErrWrongBufferLength) } + length := len(data) / 4 + result := make([]T, length) + for index := range length { + offset := index * 4 + result[index] = + T(data[offset + 0]) << 24 | + T(data[offset + 1]) << 16 | + T(data[offset + 2]) << 8 | + T(data[offset + 3]) + } + return result, len(data) / 4, nil +} + +// EncodeI32Array encodes a packed array of 32 bit integers into the given buffer. +func EncodeI32Array[T Int32](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 4 { return 0, fmt.Errorf("encoding []int32: %w", ErrWrongBufferLength) } + for _, item := range value { + buffer[0] = byte(item >> 24) + buffer[1] = byte(item >> 16) + buffer[2] = byte(item >> 8) + buffer[3] = byte(item) + buffer = buffer[4:] + } + return len(value) * 4, nil +} + +// I32ArraySize returns the size of a packed 32 bit integer array. Returns 0 and +// an error if the size is too large. +func I32ArraySize[T Int32](value []T) (int, error) { + total := len(value) * 4 + if total > dataMaxSize { return 0, ErrDataTooLarge } + return total, nil +} + +// DecodeI64Array decodes a packed array of 32 bit integers from the given data. +func DecodeI64Array[T Int64](data []byte) (value []T, n int, err error) { + if len(data) % 8 != 0 { return nil, 0, fmt.Errorf("decoding []int64: %w", ErrWrongBufferLength) } + length := len(data) / 8 + result := make([]T, length) + for index := range length { + offset := index * 8 + result[index] = + T(data[offset + 0]) << 56 | + T(data[offset + 1]) << 48 | + T(data[offset + 2]) << 40 | + T(data[offset + 3]) << 32 | + T(data[offset + 4]) << 24 | + T(data[offset + 5]) << 16 | + T(data[offset + 6]) << 8 | + T(data[offset + 7]) + } + return result, len(data) / 8, nil +} + +// EncodeI64Array encodes a packed array of 64 bit integers into the given buffer. +func EncodeI64Array[T Int64](buffer []byte, value []T) (n int, err error) { + if len(buffer) != len(value) * 8 { return 0, fmt.Errorf("encoding []int64: %w", ErrWrongBufferLength) } + for _, item := range value { + buffer[0] = byte(item >> 56) + buffer[1] = byte(item >> 48) + buffer[2] = byte(item >> 40) + buffer[3] = byte(item >> 32) + buffer[4] = byte(item >> 24) + buffer[5] = byte(item >> 16) + buffer[6] = byte(item >> 8) + buffer[7] = byte(item) + buffer = buffer[8:] + } + return len(value) * 8, nil +} + +// I64ArraySize returns the size of a packed 64 bit integer array. Returns 0 and +// an error if the size is too large. +func I64ArraySize[T Int64](value []T) (int, error) { + total := len(value) * 8 + if total > dataMaxSize { return 0, ErrDataTooLarge } + return total, nil +} diff --git a/tape/array_test.go b/tape/array_test.go new file mode 100644 index 0000000..004cf85 --- /dev/null +++ b/tape/array_test.go @@ -0,0 +1,131 @@ +package tape + +// import "fmt" +import "slices" +// import "errors" +import "testing" +import "math/rand" + +func TestI8Array(test *testing.T) { + var buffer [64]byte + _, err := EncodeI8Array[uint8](buffer[:], []uint8 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } + _, err = EncodeI8Array[uint8](buffer[:0], []uint8 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } + _, _, err = DecodeI8Array[uint8](buffer[:]) + if err != nil { test.Fatal(err) } + _, _, err = DecodeI8Array[uint8](buffer[:0]) + if err != nil { test.Fatal(err) } + + for _ = range largeNumberNTestRounds { + array := randInts[uint8](rand.Intn(16)) + length, _ := I8ArraySize(array) + if length != len(array) { test.Fatalf("%d != %d", length, len(array)) } + _, err := EncodeI8Array[uint8](buffer[:length], array) + if err != nil { test.Fatal(err) } + decoded, _, err := DecodeI8Array[uint8](buffer[:length]) + if err != nil { test.Fatal(err) } + if !slices.Equal(decoded, array) { + test.Fatalf("%v != %v", decoded, array) + } + } +} + +func TestI16Array(test *testing.T) { + var buffer [128]byte + _, err := EncodeI16Array[uint16](buffer[:], []uint16 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } + _, err = EncodeI16Array[uint16](buffer[:0], []uint16 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } + _, _, err = DecodeI16Array[uint16](buffer[:]) + if err != nil { test.Fatal(err) } + _, _, err = DecodeI16Array[uint16](buffer[:0]) + if err != nil { test.Fatal(err) } + + for _ = range largeNumberNTestRounds { + array := randInts[uint16](rand.Intn(16)) + length, _ := I16ArraySize(array) + if length != 2 * len(array) { test.Fatalf("%d != %d", length, 2 * len(array)) } + _, err := EncodeI16Array[uint16](buffer[:length], array) + if err != nil { test.Fatal(err) } + decoded, _, err := DecodeI16Array[uint16](buffer[:length]) + if err != nil { test.Fatal(err) } + if !slices.Equal(decoded, array) { + test.Fatalf("%v != %v", decoded, array) + } + } +} + +func TestI32Array(test *testing.T) { + var buffer [256]byte + _, err := EncodeI32Array[uint32](buffer[:], []uint32 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } + _, err = EncodeI32Array[uint32](buffer[:0], []uint32 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } + _, _, err = DecodeI32Array[uint32](buffer[:]) + if err != nil { test.Fatal(err) } + _, _, err = DecodeI32Array[uint32](buffer[:0]) + if err != nil { test.Fatal(err) } + + for _ = range largeNumberNTestRounds { + array := randInts[uint32](rand.Intn(16)) + length, _ := I32ArraySize(array) + if length != 4 * len(array) { test.Fatalf("%d != %d", length, 4 * len(array)) } + _, err := EncodeI32Array[uint32](buffer[:length], array) + if err != nil { test.Fatal(err) } + decoded, _, err := DecodeI32Array[uint32](buffer[:length]) + if err != nil { test.Fatal(err) } + if !slices.Equal(decoded, array) { + test.Fatalf("%v != %v", decoded, array) + } + } +} + +func TestI64Array(test *testing.T) { + var buffer [512]byte + _, err := EncodeI64Array[uint64](buffer[:], []uint64 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } + _, err = EncodeI64Array[uint64](buffer[:0], []uint64 { 0, 4, 50, 19 }) + if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } + _, _, err = DecodeI64Array[uint64](buffer[:]) + if err != nil { test.Fatal(err) } + _, _, err = DecodeI64Array[uint64](buffer[:0]) + if err != nil { test.Fatal(err) } + + for _ = range largeNumberNTestRounds { + array := randInts[uint64](rand.Intn(16)) + length, _ := I64ArraySize(array) + if length != 8 * len(array) { test.Fatalf("%d != %d", length, 8 * len(array)) } + _, err := EncodeI64Array[uint64](buffer[:length], array) + if err != nil { test.Fatal(err) } + decoded, _, err := DecodeI64Array[uint64](buffer[:length]) + if err != nil { test.Fatal(err) } + if !slices.Equal(decoded, array) { + test.Fatalf("%v != %v", decoded, array) + } + } +} + +func TestStringArray(test *testing.T) { + var buffer [8192]byte + _, err := EncodeStringArray[string](buffer[:], []string { "0", "4", "50", "19" }) + if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } + _, err = EncodeStringArray[string](buffer[:0], []string { "0", "4", "50", "19" }) + if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } + _, _, err = DecodeStringArray[string](buffer[:0]) + if err != nil { test.Fatal(err) } + + for _ = range largeNumberNTestRounds { + array := randStrings[string](rand.Intn(16), 16) + length, _ := StringArraySize(array) + // TODO test length + _, err := EncodeStringArray[string](buffer[:length], array) + if err != nil { test.Fatal(err) } + decoded, _, err := DecodeStringArray[string](buffer[:length]) + if err != nil { test.Fatal(err) } + if !slices.Equal(decoded, array) { + test.Fatalf("%v != %v", decoded, array) + } + } +} + diff --git a/tape/types.go b/tape/types.go index 527f214..8a2c9d6 100644 --- a/tape/types.go +++ b/tape/types.go @@ -169,188 +169,6 @@ func StringSize[T String](value T) (int, error) { return len(value), nil } -// DecodeStringArray decodes a packed string array from the given data. -func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { - for len(data) > 0 { - if len(data) < 2 { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } - itemSize16, nn, _ := DecodeI16[uint16](data[:2]) - itemSize := int(itemSize16) - n += nn - data = data[nn:] - if len(data) < itemSize { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } - result = append(result, T(data[:itemSize])) - data = data[itemSize:] - n += itemSize - } - return result, n, nil -} - -// EncodeStringArray encodes a packed string array into the given buffer. -func EncodeStringArray[T String](buffer []byte, value []T) (n int, err error) { - for _, item := range value { - length, err := StringSize(item) - if err != nil { return n, err } - if len(buffer) < 2 + length { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } - EncodeI16(buffer[:2], uint16(length)) - buffer = buffer[2:] - copy(buffer, item) - buffer = buffer[length:] - n += 2 + length - } - if len(buffer) > 0 { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } - return n, nil -} - -// StringArraySize returns the size of a packed string array. Returns 0 and an -// error if the size is too large. -func StringArraySize[T String](value []T) (int, error) { - total := 0 - for _, item := range value { - total += 2 + len(item) - } - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI8Array decodes a packed array of 8 bit integers from the given data. -func DecodeI8Array[T Int8](data []byte) (result []T, n int, err error) { - result = make([]T, len(data)) - for index, item := range data { - result[index] = T(item) - } - return result, len(data), nil -} - -// EncodeI8Array encodes a packed array of 8 bit integers into the given buffer. -func EncodeI8Array[T Int8](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) { return 0, fmt.Errorf("encoding []int8: %w", ErrWrongBufferLength) } - for index, item := range value { - buffer[index] = byte(item) - } - return len(buffer), nil -} - -// I8ArraySize returns the size of a packed 8 bit integer array. Returns 0 and -// an error if the size is too large. -func I8ArraySize[T Int8](value []T) (int, error) { - total := len(value) - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI16Array decodes a packed array of 16 bit integers from the given data. -func DecodeI16Array[T Int16](data []byte) (value []T, n int, err error) { - if len(data) % 2 != 0 { return nil, 0, fmt.Errorf("decoding []int16: %w", ErrWrongBufferLength) } - length := len(data) / 2 - result := make([]T, length) - for index := range length { - offset := index * 2 - result[index] = T(data[offset]) << 8 | T(data[offset + 1]) - } - return result, len(data) / 2, nil -} - -// EncodeI16Array encodes a packed array of 16 bit integers into the given buffer. -func EncodeI16Array[T Int16](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 2 { return 0, fmt.Errorf("encoding []int16: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 8) - buffer[1] = byte(item) - buffer = buffer[2:] - } - return len(value) * 2, nil -} - -// I16ArraySize returns the size of a packed 16 bit integer array. Returns 0 and -// an error if the size is too large. -func I16ArraySize[T Int16](value []T) (int, error) { - total := len(value) * 2 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI32Array decodes a packed array of 32 bit integers from the given data. -func DecodeI32Array[T Int32](data []byte) (value []T, n int, err error) { - if len(data) % 4 != 0 { return nil, 0, fmt.Errorf("decoding []int32: %w", ErrWrongBufferLength) } - length := len(data) / 4 - result := make([]T, length) - for index := range length { - offset := index * 4 - result[index] = - T(data[offset + 0]) << 24 | - T(data[offset + 1]) << 16 | - T(data[offset + 2]) << 8 | - T(data[offset + 3]) - } - return result, len(data) / 4, nil -} - -// EncodeI32Array encodes a packed array of 32 bit integers into the given buffer. -func EncodeI32Array[T Int32](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 4 { return 0, fmt.Errorf("encoding []int32: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 24) - buffer[1] = byte(item >> 16) - buffer[2] = byte(item >> 8) - buffer[3] = byte(item) - buffer = buffer[4:] - } - return len(value) * 4, nil -} - -// I32ArraySize returns the size of a packed 32 bit integer array. Returns 0 and -// an error if the size is too large. -func I32ArraySize[T Int32](value []T) (int, error) { - total := len(value) * 4 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI64Array decodes a packed array of 32 bit integers from the given data. -func DecodeI64Array[T Int64](data []byte) (value []T, n int, err error) { - if len(data) % 8 != 0 { return nil, 0, fmt.Errorf("decoding []int64: %w", ErrWrongBufferLength) } - length := len(data) / 8 - result := make([]T, length) - for index := range length { - offset := index * 8 - result[index] = - T(data[offset + 0]) << 56 | - T(data[offset + 1]) << 48 | - T(data[offset + 2]) << 40 | - T(data[offset + 3]) << 32 | - T(data[offset + 4]) << 24 | - T(data[offset + 5]) << 16 | - T(data[offset + 6]) << 8 | - T(data[offset + 7]) - } - return result, len(data) / 8, nil -} - -// EncodeI64Array encodes a packed array of 64 bit integers into the given buffer. -func EncodeI64Array[T Int64](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 8 { return 0, fmt.Errorf("encoding []int64: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 56) - buffer[1] = byte(item >> 48) - buffer[2] = byte(item >> 40) - buffer[3] = byte(item >> 32) - buffer[4] = byte(item >> 24) - buffer[5] = byte(item >> 16) - buffer[6] = byte(item >> 8) - buffer[7] = byte(item) - buffer = buffer[8:] - } - return len(value) * 8, nil -} - -// I64ArraySize returns the size of a packed 64 bit integer array. Returns 0 and -// an error if the size is too large. -func I64ArraySize[T Int64](value []T) (int, error) { - total := len(value) * 8 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - // U16CastSafe safely casts an integer to a uint16. If an overflow or underflow // occurs, it will return (0, false). func U16CastSafe(n int) (uint16, bool) { -- 2.46.1 From 2080d607935b9c0c087ff34989685da6f1a720f0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 14:47:11 -0400 Subject: [PATCH 044/132] tape: Remove array tests from types_test.go --- tape/types_test.go | 123 --------------------------------------------- 1 file changed, 123 deletions(-) diff --git a/tape/types_test.go b/tape/types_test.go index 5daf5fd..75d14f1 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -232,129 +232,6 @@ func TestString(test *testing.T) { } } -func TestI8Array(test *testing.T) { - var buffer [64]byte - _, err := EncodeI8Array[uint8](buffer[:], []uint8 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI8Array[uint8](buffer[:0], []uint8 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI8Array[uint8](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI8Array[uint8](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint8](rand.Intn(16)) - length, _ := I8ArraySize(array) - if length != len(array) { test.Fatalf("%d != %d", length, len(array)) } - _, err := EncodeI8Array[uint8](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI8Array[uint8](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI16Array(test *testing.T) { - var buffer [128]byte - _, err := EncodeI16Array[uint16](buffer[:], []uint16 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI16Array[uint16](buffer[:0], []uint16 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI16Array[uint16](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI16Array[uint16](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint16](rand.Intn(16)) - length, _ := I16ArraySize(array) - if length != 2 * len(array) { test.Fatalf("%d != %d", length, 2 * len(array)) } - _, err := EncodeI16Array[uint16](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI16Array[uint16](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI32Array(test *testing.T) { - var buffer [256]byte - _, err := EncodeI32Array[uint32](buffer[:], []uint32 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI32Array[uint32](buffer[:0], []uint32 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI32Array[uint32](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI32Array[uint32](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint32](rand.Intn(16)) - length, _ := I32ArraySize(array) - if length != 4 * len(array) { test.Fatalf("%d != %d", length, 4 * len(array)) } - _, err := EncodeI32Array[uint32](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI32Array[uint32](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI64Array(test *testing.T) { - var buffer [512]byte - _, err := EncodeI64Array[uint64](buffer[:], []uint64 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI64Array[uint64](buffer[:0], []uint64 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI64Array[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI64Array[uint64](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint64](rand.Intn(16)) - length, _ := I64ArraySize(array) - if length != 8 * len(array) { test.Fatalf("%d != %d", length, 8 * len(array)) } - _, err := EncodeI64Array[uint64](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI64Array[uint64](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestStringArray(test *testing.T) { - var buffer [8192]byte - _, err := EncodeStringArray[string](buffer[:], []string { "0", "4", "50", "19" }) - if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - _, err = EncodeStringArray[string](buffer[:0], []string { "0", "4", "50", "19" }) - if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeStringArray[string](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randStrings[string](rand.Intn(16), 16) - length, _ := StringArraySize(array) - // TODO test length - _, err := EncodeStringArray[string](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeStringArray[string](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - func TestU16CastSafe(test *testing.T) { number, ok := U16CastSafe(90_000) if ok { test.Fatalf("false positive: %v, %v", number, ok) } -- 2.46.1 From 4f3b25682197813cb5c13d0a9a8bf0bd83c70acd Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 14:49:43 -0400 Subject: [PATCH 045/132] tape: Integer encoding accepts oversize buffers now --- tape/types.go | 16 ++++++++-------- tape/types_test.go | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tape/types.go b/tape/types.go index 8a2c9d6..ea5045d 100644 --- a/tape/types.go +++ b/tape/types.go @@ -35,26 +35,26 @@ type String interface { ~string } // DecodeI8 decodes an 8 bit integer from the given data. func DecodeI8[T Int8](data []byte) (value T, n int, err error) { - if len(data) != 1 { return 0, 0, fmt.Errorf("decoding int8: %w", ErrWrongBufferLength) } + if len(data) < 1 { return 0, 0, fmt.Errorf("decoding int8: %w", ErrWrongBufferLength) } return T(data[0]), 1, nil } // EncodeI8 encodes an 8 bit integer into the given buffer. func EncodeI8[T Int8](buffer []byte, value T) (n int, err error) { - if len(buffer) != 1 { return 0, fmt.Errorf("encoding int8: %w", ErrWrongBufferLength) } + if len(buffer) < 1 { return 0, fmt.Errorf("encoding int8: %w", ErrWrongBufferLength) } buffer[0] = byte(value) return 1, nil } // DecodeI16 decodes a 16 bit integer from the given data. func DecodeI16[T Int16](data []byte) (value T, n int, err error) { - if len(data) != 2 { return 0, 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) } + if len(data) < 2 { return 0, 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) } return T(data[0]) << 8 | T(data[1]), 2, nil } // EncodeI16 encodes a 16 bit integer into the given buffer. func EncodeI16[T Int16](buffer []byte, value T) (n int, err error) { - if len(buffer) != 2 { return 0, fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) } + if len(buffer) < 2 { return 0, fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 8) buffer[1] = byte(value) return 2, nil @@ -62,7 +62,7 @@ func EncodeI16[T Int16](buffer []byte, value T) (n int, err error) { // DecodeI32 decodes a 32 bit integer from the given data. func DecodeI32[T Int32](data []byte) (value T, n int, err error) { - if len(data) != 4 { return 0, 0, fmt.Errorf("decoding int32: %w", ErrWrongBufferLength) } + if len(data) < 4 { return 0, 0, fmt.Errorf("decoding int32: %w", ErrWrongBufferLength) } return T(data[0]) << 24 | T(data[1]) << 16 | T(data[2]) << 8 | @@ -71,7 +71,7 @@ func DecodeI32[T Int32](data []byte) (value T, n int, err error) { // EncodeI32 encodes a 32 bit integer into the given buffer. func EncodeI32[T Int32](buffer []byte, value T) (n int, err error) { - if len(buffer) != 4 { return 0, fmt.Errorf("encoding int32: %w", ErrWrongBufferLength) } + if len(buffer) < 4 { return 0, fmt.Errorf("encoding int32: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 24) buffer[1] = byte(value >> 16) buffer[2] = byte(value >> 8) @@ -81,7 +81,7 @@ func EncodeI32[T Int32](buffer []byte, value T) (n int, err error) { // DecodeI64 decodes a 64 bit integer from the given data. func DecodeI64[T Int64](data []byte) (value T, n int, err error) { - if len(data) != 8 { return 0, 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) } + if len(data) < 8 { return 0, 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) } return T(data[0]) << 56 | T(data[1]) << 48 | T(data[2]) << 40 | @@ -94,7 +94,7 @@ func DecodeI64[T Int64](data []byte) (value T, n int, err error) { // EncodeI64 encodes a 64 bit integer into the given buffer. func EncodeI64[T Int64](buffer []byte, value T) (n int, err error) { - if len(buffer) != 8 { return 0, fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) } + if len(buffer) < 8 { return 0, fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) } buffer[0] = byte(value >> 56) buffer[1] = byte(value >> 48) buffer[2] = byte(value >> 40) diff --git a/tape/types_test.go b/tape/types_test.go index 75d14f1..5cf16b6 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -12,11 +12,11 @@ const randStringBytes = "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU func TestI8(test *testing.T) { var buffer [16]byte _, err := EncodeI8[uint8](buffer[:], 5) - if err.Error() != "encoding int8: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, err = EncodeI8[uint8](buffer[:0], 5) if err.Error() != "encoding int8: wrong buffer length" { test.Fatal(err) } _, _, err = DecodeI8[uint8](buffer[:]) - if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, _, err = DecodeI8[uint8](buffer[:0]) if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } @@ -34,11 +34,11 @@ func TestI8(test *testing.T) { func TestI16(test *testing.T) { var buffer [16]byte _, err := EncodeI16[uint16](buffer[:], 5) - if err.Error() != "encoding int16: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, err = EncodeI16[uint16](buffer[:0], 5) if err.Error() != "encoding int16: wrong buffer length" { test.Fatal(err) } _, _, err = DecodeI16[uint16](buffer[:]) - if err.Error() != "decoding int16: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, _, err = DecodeI16[uint16](buffer[:0]) if err.Error() != "decoding int16: wrong buffer length" { test.Fatal(err) } @@ -57,11 +57,11 @@ func TestI16(test *testing.T) { func TestI32(test *testing.T) { var buffer [16]byte _, err := EncodeI32[uint32](buffer[:], 5) - if err.Error() != "encoding int32: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, err = EncodeI32[uint32](buffer[:0], 5) if err.Error() != "encoding int32: wrong buffer length" { test.Fatal(err) } _, _, err = DecodeI32[uint32](buffer[:]) - if err.Error() != "decoding int32: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, _, err = DecodeI32[uint32](buffer[:0]) if err.Error() != "decoding int32: wrong buffer length" { test.Fatal(err) } @@ -80,11 +80,11 @@ func TestI32(test *testing.T) { func TestI64(test *testing.T) { var buffer [16]byte _, err := EncodeI64[uint64](buffer[:], 5) - if err.Error() != "encoding int64: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, err = EncodeI64[uint64](buffer[:0], 5) if err.Error() != "encoding int64: wrong buffer length" { test.Fatal(err) } _, _, err = DecodeI64[uint64](buffer[:]) - if err.Error() != "decoding int64: wrong buffer length" { test.Fatal(err) } + if err != nil { test.Fatal(err) } _, _, err = DecodeI64[uint64](buffer[:0]) if err.Error() != "decoding int64: wrong buffer length" { test.Fatal(err) } -- 2.46.1 From 3d8a0124777b79cc9f39f4ba3ed7550a9b5aabed Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 15:50:24 -0400 Subject: [PATCH 046/132] tape: Add table encoding/decoding functions --- tape/table.go | 59 ++++++++++++++++++++++++++ tape/table_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 tape/table.go create mode 100644 tape/table_test.go diff --git a/tape/table.go b/tape/table.go new file mode 100644 index 0000000..2128dc2 --- /dev/null +++ b/tape/table.go @@ -0,0 +1,59 @@ +package tape + +import "iter" + +// encoding and decoding functions must not make any allocations + +type TablePushFunc func(tag uint16, value []byte) (n int, err error) + +func DecodeTable(data []byte) iter.Seq2[uint16, []byte] { + return func(yield func(tag uint16, value []byte) bool) { + n := 0 + for { + tag, nn, err := DecodeI16[uint16](data[n:]) + if err != nil { return } + n += nn + + length, nn, err := DecodeGBEU[uint64](data[n:]) + if err != nil { return } + n += nn + + value := data[n:n + int(length)] + yield(tag, value) + n += int(length) + } + } +} + +func EncodeTable(data []byte) TablePushFunc { + return func(tag uint16, value []byte) (n int, err error) { + if n >= len(data) { return n, ErrWrongBufferLength } + nn, err := EncodeI16(data[n:], uint16(tag)) + if err != nil { return n, err } + n += nn + + if n >= len(data) { return n, ErrWrongBufferLength } + nn, err = EncodeGBEU(data[n:], uint(len(value))) + if err != nil { return n, err } + n += nn + + if n >= len(data) { return n, ErrWrongBufferLength } + nn = copy(data[n:], value) + n += nn + if nn < len(value) { + return n, ErrWrongBufferLength + } + + if n >= len(data) { return n, ErrWrongBufferLength } + data = data[n:] + return n, nil + } +} + +func TableSize(itemLengths ...int) int { + sum := 0 + for _, length := range itemLengths { + sum += GBEUSize(uint(length)) + length + } + return sum +} diff --git a/tape/table_test.go b/tape/table_test.go new file mode 100644 index 0000000..6dc64dc --- /dev/null +++ b/tape/table_test.go @@ -0,0 +1,100 @@ +package tape + +import "fmt" +import "slices" +// import "errors" +import "testing" +// import "math/rand" + +var longText = +`Curious, I started off in that direction, only for Prism to stop me. "Wrong way, Void. It's right over here." He trotted over to a door to the left of us. It was marked with the number '4004'. He took a key out of his saddlebags, unlocked it, and pushed it open. "You know, some say this suite is haunted. They call the ghost that lives here the 'Spirit of 4004'. Ooooh!" He made paddling motions in the air with his hooves.` + +func TestTable(test *testing.T) { + item5 := []byte("hello") + item7 := []byte("world") + item0 := []byte(longText) + item3249 := []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 } + + buffer := [512]byte { } + push := EncodeTable(buffer[:]) + _, err := push(5, item5) + if err != nil { test.Fatal(err)} + _, err = push(7, item7) + if err != nil { test.Fatal(err)} + _, err = push(0, item0) + if err != nil { test.Fatal(err)} + _, err = push(3249, item3249) + if err != nil { test.Fatal(err)} + + test.Logf("len of longText: %d 0x%X", len(longText), len(longText)) + correct := []byte("\x00\x05\x05hello\x00\x07\x05world\x00\x00\x83\x28" + longText) + if got := buffer[:len(correct)]; !slices.Equal(got, correct) { + if !compareHexArray(test, correct, got) { + test.FailNow() + } + } +} + +func TestTableSmall(test *testing.T) { + item2 := []byte("hello") + item3249 := []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 } + + buffer := [64]byte { } + push := EncodeTable(buffer[:]) + _, err := push(2, item2) + if err != nil { test.Fatal(err) } + _, err = push(3249, item3249) + if err != nil { test.Fatal(err) } + + correct := []byte("\x00\x02\x05hello\x0C\xB1\x06\x00\x01\x02\x03\xA0\x05") + if got := buffer[:len(correct)]; !slices.Equal(got, correct) { + if !compareHexArray(test, correct, got) { + test.FailNow() + } + } +} + +func dumpHexArray(data []byte) (message string) { + for _, item := range data { + message = fmt.Sprintf("%s %02X", message, item) + } + return message +} + +func compareHexArray(test *testing.T, correct, got []byte) bool { + index := 0 + for { + if index >= len(correct) { + if index < len(got) { + test.Log("correct longer than got") + test.Log("got: ", dumpHexArray(got)) + test.Log("correct:", dumpHexArray(correct)) + return false + } + } + if index >= len(got) { + if index < len(correct) { + test.Log("got longer than correct") + test.Log("got: ", dumpHexArray(got)) + test.Log("correct:", dumpHexArray(correct)) + return false + } + } + if correct[index] != got[index] { + test.Log("not equal") + test.Log("got: ", dumpHexArray(got)) + test.Log("correct:", dumpHexArray(correct)) + partLow := index - 8 + partHigh := index + 8 + test.Log("got part ", dumpHexArray(safeSlice(got, partLow, partHigh))) + test.Log("correct part", dumpHexArray(safeSlice(correct, partLow, partHigh))) + return false + } + index ++ + } + return true +} + +func safeSlice[T any](slice []T, low, high int) []T { + return slice[max(low, 0):min(high, len(slice))] +} -- 2.46.1 From f50b2ca0cd396f500987b03bb71ab72ebc4ad5e3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 15:51:45 -0400 Subject: [PATCH 047/132] tape: Remove old "pairs" encoding --- tape/pairs.go | 83 ---------------------------------------------- tape/pairs_test.go | 62 ---------------------------------- 2 files changed, 145 deletions(-) delete mode 100644 tape/pairs.go delete mode 100644 tape/pairs_test.go diff --git a/tape/pairs.go b/tape/pairs.go deleted file mode 100644 index 7a5bd24..0000000 --- a/tape/pairs.go +++ /dev/null @@ -1,83 +0,0 @@ -package tape - -import "iter" - -// DecodePairs decodes message tag/value pairs from a byte slice. It returns an -// iterator over all pairs, where the first value is the tag and the second is -// the value. If data yielded by the iterator is retained, it must be copied -// first. -func DecodePairs(data []byte) (iter.Seq2[uint16, []byte], error) { - // determine section bounds - if len(data) < 2 { return nil, ErrDataTooLarge } - length16, _, _ := DecodeI16[uint16](data[0:2]) - data = data[2:] - length := int(length16) - headerSize := length * 4 - if len(data) < headerSize { return nil, ErrDataTooLarge } - valuesData := data[headerSize:] - - // ensure the value buffer is big enough - var valuesSize int - for index := range length { - offset := index * 4 - end, _, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) - valuesSize = int(end) - } - if valuesSize > len(valuesData) { - return nil, ErrDataTooLarge - } - - // return iterator - return func(yield func(uint16, []byte) bool) { - start := uint16(0) - for index := range length { - offset := index * 4 - key, _, _ := DecodeI16[uint16](data[offset + 0:offset + 2]) - end, _, _ := DecodeI16[uint16](data[offset + 2:offset + 4]) - // if nextValuesOffset < len(valuesData) { - if !yield(key, valuesData[start:end]) { - return - } - // } else { - // if !yield(key, nil) { - // return - // } - // } - start = end - } - }, nil -} - -// EncodePairs encodes message tag/value pairs into a byte slice. -func EncodePairs(pairs map[uint16] []byte) ([]byte, error) { - // determine section bounds - headerSize := 2 + len(pairs) * 4 - valuesSize := 0 - for _, value := range pairs { - valuesSize += len(value) - } - - // generate data - buffer := make([]byte, headerSize + valuesSize) - length16, ok := U16CastSafe(len(pairs)) - if !ok { return nil, ErrDataTooLarge } - EncodeI16[uint16](buffer[0:2], length16) - index := 0 - end := headerSize - for key, value := range pairs { - start := end - end += len(value) - tagOffset := 2 + index * 4 - end16, ok := U16CastSafe(end - headerSize) - if !ok { return nil, ErrDataTooLarge } - - // write tag and length - EncodeI16[uint16](buffer[tagOffset + 0:tagOffset + 2], key) - EncodeI16[uint16](buffer[tagOffset + 2:tagOffset + 4], end16) - - // write value - copy(buffer[start:end], value) - index ++ - } - return buffer, nil -} diff --git a/tape/pairs_test.go b/tape/pairs_test.go deleted file mode 100644 index 31bb7d6..0000000 --- a/tape/pairs_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package tape - -import "slices" -import "testing" - -func TestDecodePairs(test *testing.T) { - pairs := map[uint16] []byte { - 3894: []byte("foo"), - 7: []byte("br"), - } - got, err := DecodePairs([]byte { - 0, 2, - 0, 7, 0, 2, - 15, 54, 0, 5, - 98, 114, - 102, 111, 111}) - if err != nil { test.Fatal(err) } - length := 0 - for key, value := range got { - test.Log(key, value) - if !slices.Equal(pairs[key], value) { test.Fatal("not equal") } - length ++ - } - test.Log("length") - if length != len(pairs) { test.Fatal("wrong length") } -} - -func TestEncodePairs(test *testing.T) { - pairs := map[uint16] []byte { - 3894: []byte("foo"), - 7: []byte("br"), - } - got, err := EncodePairs(pairs) - if err != nil { test.Fatal(err) } - test.Log(got) - valid := slices.Equal(got, []byte { - 0, 2, - 15, 54, 0, 3, - 0, 7, 0, 5, - 102, 111, 111, - 98, 114}) || - slices.Equal(got, []byte { - 0, 2, - 0, 7, 0, 2, - 15, 54, 0, 5, - 98, 114, - 102, 111, 111}) - if !valid { test.Fatal("not equal") } -} - -func FuzzDecodePairs(fuzz *testing.F) { - fuzz.Add([]byte { - 0, 2, - 0, 7, 0, 2, - 15, 54, 0, 5, - 98, 114, - 102, 111, 111}) - fuzz.Fuzz(func(t *testing.T, buffer []byte) { - // ensure it does not panic :P - DecodePairs(buffer) - }) -} -- 2.46.1 From 568431f4c330b3a4f71e8295e3c05c6c4c308a0d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:08:47 -0400 Subject: [PATCH 048/132] tape: Improve table decoding --- tape/table.go | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tape/table.go b/tape/table.go index 2128dc2..8bd85dc 100644 --- a/tape/table.go +++ b/tape/table.go @@ -1,27 +1,24 @@ package tape -import "iter" - // encoding and decoding functions must not make any allocations type TablePushFunc func(tag uint16, value []byte) (n int, err error) -func DecodeTable(data []byte) iter.Seq2[uint16, []byte] { - return func(yield func(tag uint16, value []byte) bool) { - n := 0 - for { - tag, nn, err := DecodeI16[uint16](data[n:]) - if err != nil { return } - n += nn +type TablePullFunc func() (tag uint16, value []byte, n int, err error) - length, nn, err := DecodeGBEU[uint64](data[n:]) - if err != nil { return } - n += nn +func DecodeTable(data []byte) TablePullFunc { + return func() (tag uint16, value []byte, n int, err error) { + tag, nn, err := DecodeI16[uint16](data[n:]) + if err != nil { return tag, value, n, err } + n += nn - value := data[n:n + int(length)] - yield(tag, value) - n += int(length) - } + length, nn, err := DecodeGBEU[uint64](data[n:]) + if err != nil { return tag, value, n, err } + n += nn + + value = data[n:n + int(length)] + n += int(length) + return tag, value, n, err } } -- 2.46.1 From 3a88619f9b7f2b43bd5bf62c19b4250da4b3dcd2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:12:55 -0400 Subject: [PATCH 049/132] tape: Add back iter compatibility for table decoding --- tape/table.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tape/table.go b/tape/table.go index 8bd85dc..aa8f412 100644 --- a/tape/table.go +++ b/tape/table.go @@ -1,5 +1,7 @@ package tape +import "iter" + // encoding and decoding functions must not make any allocations type TablePushFunc func(tag uint16, value []byte) (n int, err error) @@ -22,6 +24,17 @@ func DecodeTable(data []byte) TablePullFunc { } } +func DecodeTableIter(data []byte) iter.Seq2[uint16, []byte] { + return func(yield func(uint16, []byte) bool) { + pull := DecodeTable(data) + for { + tag, value, _, err := pull() + if err != nil { return } + if !yield(tag, value) { return } + } + } +} + func EncodeTable(data []byte) TablePushFunc { return func(tag uint16, value []byte) (n int, err error) { if n >= len(data) { return n, ErrWrongBufferLength } -- 2.46.1 From f4fb5e80b98cc1bf1611b4d879157907aeb6dd01 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:22:52 -0400 Subject: [PATCH 050/132] tape: Test "n" sizes of integer types --- tape/types_test.go | 70 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/tape/types_test.go b/tape/types_test.go index 5cf16b6..39b623d 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -21,10 +21,16 @@ func TestI8(test *testing.T) { if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } for number := range uint8(255) { - _, err := EncodeI8[uint8](buffer[:1], number) + n, err := EncodeI8[uint8](buffer[:1], number) if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI8[uint8](buffer[:1]) + if correct, got := 1, n; correct != got { + test.Fatal("not equal:", got) + } + decoded, n, err := DecodeI8[uint8](buffer[:1]) if err != nil { test.Fatal(err) } + if correct, got := 1, n; correct != got { + test.Fatal("not equal:", got) + } if decoded != number { test.Fatalf("%d != %d", decoded, number) } @@ -44,10 +50,16 @@ func TestI16(test *testing.T) { for _ = range largeNumberNTestRounds { number := uint16(rand.Int()) - _, err := EncodeI16[uint16](buffer[:2], number) + n, err := EncodeI16[uint16](buffer[:2], number) if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI16[uint16](buffer[:2]) + if correct, got := 2, n; correct != got { + test.Fatal("not equal:", got) + } + decoded, n, err := DecodeI16[uint16](buffer[:2]) if err != nil { test.Fatal(err) } + if correct, got := 2, n; correct != got { + test.Fatal("not equal:", got) + } if decoded != number { test.Fatalf("%d != %d", decoded, number) } @@ -67,10 +79,16 @@ func TestI32(test *testing.T) { for _ = range largeNumberNTestRounds { number := uint32(rand.Int()) - _, err := EncodeI32[uint32](buffer[:4], number) + n, err := EncodeI32[uint32](buffer[:4], number) if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI32[uint32](buffer[:4]) + if correct, got := 4, n; correct != got { + test.Fatal("not equal:", got) + } + decoded, n, err := DecodeI32[uint32](buffer[:4]) if err != nil { test.Fatal(err) } + if correct, got := 4, n; correct != got { + test.Fatal("not equal:", got) + } if decoded != number { test.Fatalf("%d != %d", decoded, number) } @@ -90,10 +108,16 @@ func TestI64(test *testing.T) { for _ = range largeNumberNTestRounds { number := uint64(rand.Int()) - _, err := EncodeI64[uint64](buffer[:8], number) + n, err := EncodeI64[uint64](buffer[:8], number) if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI64[uint64](buffer[:8]) + if correct, got := 8, n; correct != got { + test.Fatal("not equal:", got) + } + decoded, n, err := DecodeI64[uint64](buffer[:8]) if err != nil { test.Fatal(err) } + if correct, got := 8, n; correct != got { + test.Fatal("not equal:", got) + } if decoded != number { test.Fatalf("%d != %d", decoded, number) } @@ -117,8 +141,11 @@ func TestGBEU(test *testing.T) { if err == nil { test.Fatal("no error") } if err.Error() != "decoding GBEU: GBEU not terminated" { test.Fatal(err) } - _, err = EncodeGBEU[uint64](buffer[:], 0x97) + n, err := EncodeGBEU[uint64](buffer[:], 0x97) if err != nil { test.Fatal(err) } + if correct, got := 2, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := []byte { 0x81, 0x17 }, buffer[:2]; !slices.Equal(correct, got) { message := "not equal:" for _, item := range got { @@ -126,14 +153,20 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, _, err := DecodeGBEU[uint64](buffer[:]) + decoded, n, err := DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } + if correct, got := 2, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := uint64(0x97), decoded; correct != got { test.Fatalf("not equal: %x", got) } - _, err = EncodeGBEU[uint64](buffer[:], 0x123456) + n, err = EncodeGBEU[uint64](buffer[:], 0x123456) if err != nil { test.Fatal(err) } + if correct, got := 3, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := []byte { 0xc8, 0xe8, 0x56 }, buffer[:3]; !slices.Equal(correct, got) { message := "not equal:" for _, item := range got { @@ -143,6 +176,9 @@ func TestGBEU(test *testing.T) { } decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } + if correct, got := 3, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := uint64(0x123456), decoded; correct != got { test.Fatalf("not equal: %x", got) } @@ -150,6 +186,9 @@ func TestGBEU(test *testing.T) { maxGBEU64 := []byte { 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } _, err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) if err != nil { test.Fatal(err) } + if correct, got := 10, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := maxGBEU64, buffer[:10]; !slices.Equal(correct, got) { message := "not equal:" for _, item := range got { @@ -159,17 +198,26 @@ func TestGBEU(test *testing.T) { } decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } + if correct, got := 10, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := uint64(0xFFFFFFFFFFFFFFFF), decoded; correct != got { test.Fatalf("not equal: %x", got) } _, err = EncodeGBEU[uint64](buffer[:], 11) if err != nil { test.Fatal(err) } + if correct, got := 1, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := byte(0xb), buffer[0]; correct != got { test.Fatal("not equal:", got) } decoded, _, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } + if correct, got := 1, n; correct != got { + test.Fatal("not equal:", got) + } if correct, got := uint64(0xb), decoded; correct != got { test.Fatalf("not equal: %x", got) } -- 2.46.1 From 6e5a7115d3d7e0f2e5553158fd94604749cfacf6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:25:48 -0400 Subject: [PATCH 051/132] tape: Fix "n" size returned by DecodeGBEU --- tape/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tape/types.go b/tape/types.go index ea5045d..448e0cf 100644 --- a/tape/types.go +++ b/tape/types.go @@ -113,10 +113,10 @@ func DecodeGBEU[T UInt](data []byte) (value T, n int, err error) { fullValue *= 0x80 fullValue += uint64(chunk & 0x7F) ccb := chunk >> 7 + n += 1 if ccb == 0 { return T(fullValue), n, nil } - n += 1 } return 0, n, fmt.Errorf("decoding GBEU: %w", ErrGBEUNotTerminated) } -- 2.46.1 From 37c3e49833f8f5609a6875809016378d6ccbd52f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:32:27 -0400 Subject: [PATCH 052/132] tape: Fix types_test.go not getting values for n in some cases --- tape/types_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tape/types_test.go b/tape/types_test.go index 39b623d..cd0fbd6 100644 --- a/tape/types_test.go +++ b/tape/types_test.go @@ -174,7 +174,7 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, _, err = DecodeGBEU[uint64](buffer[:]) + decoded, n, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := 3, n; correct != got { test.Fatal("not equal:", got) @@ -184,7 +184,7 @@ func TestGBEU(test *testing.T) { } maxGBEU64 := []byte { 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } - _, err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) + n, err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) if err != nil { test.Fatal(err) } if correct, got := 10, n; correct != got { test.Fatal("not equal:", got) @@ -196,7 +196,7 @@ func TestGBEU(test *testing.T) { } test.Fatal(message) } - decoded, _, err = DecodeGBEU[uint64](buffer[:]) + decoded, n, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := 10, n; correct != got { test.Fatal("not equal:", got) @@ -205,7 +205,7 @@ func TestGBEU(test *testing.T) { test.Fatalf("not equal: %x", got) } - _, err = EncodeGBEU[uint64](buffer[:], 11) + n, err = EncodeGBEU[uint64](buffer[:], 11) if err != nil { test.Fatal(err) } if correct, got := 1, n; correct != got { test.Fatal("not equal:", got) @@ -213,7 +213,7 @@ func TestGBEU(test *testing.T) { if correct, got := byte(0xb), buffer[0]; correct != got { test.Fatal("not equal:", got) } - decoded, _, err = DecodeGBEU[uint64](buffer[:]) + decoded, n, err = DecodeGBEU[uint64](buffer[:]) if err != nil { test.Fatal(err) } if correct, got := 1, n; correct != got { test.Fatal("not equal:", got) -- 2.46.1 From dd5325b3516c8a04e107356d0f20726e2dccf32e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 16:34:08 -0400 Subject: [PATCH 053/132] tape: Fix table decoding restarting after each pull --- tape/table.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tape/table.go b/tape/table.go index aa8f412..28f0ea2 100644 --- a/tape/table.go +++ b/tape/table.go @@ -9,7 +9,8 @@ type TablePushFunc func(tag uint16, value []byte) (n int, err error) type TablePullFunc func() (tag uint16, value []byte, n int, err error) func DecodeTable(data []byte) TablePullFunc { - return func() (tag uint16, value []byte, n int, err error) { + n := 0 + return func() (tag uint16, value []byte, n_ int, err error) { tag, nn, err := DecodeI16[uint16](data[n:]) if err != nil { return tag, value, n, err } n += nn -- 2.46.1 From 0e7e9353749e1245e063d79b2e7e20c3ad0a74bb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 17:59:05 -0400 Subject: [PATCH 054/132] tape: Make table decoding more robust --- tape/table.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tape/table.go b/tape/table.go index 28f0ea2..349aa79 100644 --- a/tape/table.go +++ b/tape/table.go @@ -11,15 +11,19 @@ type TablePullFunc func() (tag uint16, value []byte, n int, err error) func DecodeTable(data []byte) TablePullFunc { n := 0 return func() (tag uint16, value []byte, n_ int, err error) { + if n >= len(data) { return 0, nil, n, ErrWrongBufferLength } tag, nn, err := DecodeI16[uint16](data[n:]) - if err != nil { return tag, value, n, err } + if err != nil { return 0, nil, n, err } n += nn + if n >= len(data) { return 0, nil, n, ErrWrongBufferLength } length, nn, err := DecodeGBEU[uint64](data[n:]) - if err != nil { return tag, value, n, err } + if err != nil { return 0, nil, n, err } n += nn - value = data[n:n + int(length)] + end := n + int(length) + if end > len(data) { return 0, nil, n, ErrWrongBufferLength } + value = data[n:end] n += int(length) return tag, value, n, err } -- 2.46.1 From 23c3efa8454468921463762eb18b234b7a529e19 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 18 May 2025 21:32:08 -0400 Subject: [PATCH 055/132] tape: Improve table tests --- tape/table_test.go | 58 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/tape/table_test.go b/tape/table_test.go index 6dc64dc..361d30a 100644 --- a/tape/table_test.go +++ b/tape/table_test.go @@ -29,10 +29,38 @@ func TestTable(test *testing.T) { test.Logf("len of longText: %d 0x%X", len(longText), len(longText)) correct := []byte("\x00\x05\x05hello\x00\x07\x05world\x00\x00\x83\x28" + longText) if got := buffer[:len(correct)]; !slices.Equal(got, correct) { - if !compareHexArray(test, correct, got) { - test.FailNow() - } + if !compareHexArray(test, got, correct) { test.Fatal("failed") } } + + pull := DecodeTable(buffer[:len(correct)]) + + tag, value, _, err := pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(5); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte("hello")) { test.Fatal("failed") } + + tag, value, _, err = pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(7); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte("world")) { test.Fatal("failed") } + + tag, value, _, err = pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(0); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte(longText)) { test.Fatal("failed") } + + tag, value, _, err = pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(3249); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 }) { test.Fatal("failed") } } func TestTableSmall(test *testing.T) { @@ -48,10 +76,24 @@ func TestTableSmall(test *testing.T) { correct := []byte("\x00\x02\x05hello\x0C\xB1\x06\x00\x01\x02\x03\xA0\x05") if got := buffer[:len(correct)]; !slices.Equal(got, correct) { - if !compareHexArray(test, correct, got) { - test.FailNow() - } + if !compareHexArray(test, got, correct) { test.Fatal("failed") } } + + pull := DecodeTable(buffer[:len(correct)]) + + tag, value, _, err := pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(2); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte("hello")) { test.Fatal("failed") } + + tag, value, _, err = pull() + if err != nil { test.Fatal(err) } + if got, correct := tag, uint16(3249); got != correct { + test.Fatal("not equal:", got) + } + if !compareHexArray(test, value, []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 }) { test.Fatal("failed") } } func dumpHexArray(data []byte) (message string) { @@ -61,7 +103,7 @@ func dumpHexArray(data []byte) (message string) { return message } -func compareHexArray(test *testing.T, correct, got []byte) bool { +func compareHexArray(test *testing.T, got, correct []byte) bool { index := 0 for { if index >= len(correct) { @@ -71,6 +113,7 @@ func compareHexArray(test *testing.T, correct, got []byte) bool { test.Log("correct:", dumpHexArray(correct)) return false } + break } if index >= len(got) { if index < len(correct) { @@ -79,6 +122,7 @@ func compareHexArray(test *testing.T, correct, got []byte) bool { test.Log("correct:", dumpHexArray(correct)) return false } + break } if correct[index] != got[index] { test.Log("not equal") -- 2.46.1 From 2b3a53052fb070b95bbc5ddf5b4629511dc8bc30 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 22 May 2025 13:26:42 -0400 Subject: [PATCH 056/132] tape: Implement PASTA and VILA encoding/decoding --- tape/array.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/tape/array.go b/tape/array.go index 1d933e3..00cdffb 100644 --- a/tape/array.go +++ b/tape/array.go @@ -6,23 +6,90 @@ import "slices" // encoding and decoding functions must not make any allocations -func DecodeArray(data []byte, itemLength int) iter.Seq[[]byte] { +// ArrayPushFunc adds an item to an array that is being encoded. +type ArrayPushFunc func(value []byte) (n int, err error) + +// ArrayPullFunc gets the next item of an array that is being decoded. +type ArrayPullFunc func() (value []byte, n int, err error) + +// DecodePASTA decodes a packed single-type array. +func DecodePASTA(data []byte, itemLength int) ArrayPullFunc { + n := 0 + return func() (value []byte, n_ int, err error) { + if n > len(data) - itemLength { + return nil, 0, fmt.Errorf("decoding PASTA: %w", ErrWrongBufferLength) + } + value = data[n:n + itemLength] + n += itemLength + return value, itemLength, nil + } +} + +// DecodePASTAIter decodes a packed single-type array and returns it as an +// iterator. +func DecodePASTAIter(data []byte, itemLength int) iter.Seq[[]byte] { return slices.Chunk(data, itemLength) } -func EncodeArray(data []byte, items ...[]byte) (n int, err error) { - for _, item := range items { - if n >= len(data) { return n, ErrWrongBufferLength } - copy(data[n:], item) - n += len(item) +// EncodePASTA encodes a packed single-type array. +func EncodePASTA(data []byte, itemLength int) ArrayPushFunc { + n := 0 + return func(value []byte) (n_ int, err error) { + if n > len(data) - itemLength { + return 0, fmt.Errorf("encoding PASTA: %w", ErrWrongBufferLength) + } + copy(data[n:], value) + n += itemLength + return itemLength, nil } - return n, nil } -func ArraySize(length, itemLength int) int { +// PASTASize returns the size of a packed single-type array. +func PASTASize(length, itemLength int) int { return length * itemLength } +// DecodeVILA encodes a variable item length array. +func DecodeVILA(data []byte) ArrayPullFunc { + n := 0 + return func() (value []byte, n_ int, err error) { + if n >= len(data) { return nil, n_, fmt.Errorf("decoding VILA: %w", ErrWrongBufferLength) } + length, nn, err := DecodeGBEU[uint](data[n:]) + n += nn + n_ += nn + if err != nil { return nil, n_, err } + + if n > len(data) - int(length) { + return nil, n_, fmt.Errorf("decoding VILA: %w", ErrWrongBufferLength) + } + value = data[n:n + int(length)] + n += int(length) + n_ += int(length) + return value, int(length), nil + } +} + +// EncodeVILA encodes a variable item length array. +func EncodeVILA(data []byte, items ...[]byte) ArrayPushFunc { + n := 0 + return func(value []byte) (n_ int, err error) { + if n >= len(data) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } + nn, err := EncodeGBEU(data[n:], uint(len(value))) + n += nn + n_ += nn + if err != nil { return n, err } + + if n >= len(data) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } + nn = copy(data[n:], value) + n += nn + n_ += nn + if nn != len(value) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } + return n_, err + } +} + +// TODO: Decode, encode, size for string array needs to use VILA + // DecodeStringArray decodes a packed string array from the given data. func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { for len(data) > 0 { -- 2.46.1 From 32df336c3e5fa174725bf95c70f5ee611c2640a9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 22 May 2025 23:44:20 -0400 Subject: [PATCH 057/132] tape: Add DecodeVILAIter --- tape/array.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tape/array.go b/tape/array.go index 00cdffb..04265d2 100644 --- a/tape/array.go +++ b/tape/array.go @@ -69,6 +69,19 @@ func DecodeVILA(data []byte) ArrayPullFunc { } } +// DecodeVILAIter decodes a variable item length array and returns it as an +// iterator. +func DecodeVILAIter(data []byte) iter.Seq[[]byte] { + return func(yield func([]byte) bool) { + pull := DecodeVILA(data) + for { + value, _, err := pull() + if err != nil { return } + if !yield(value) { return } + } + } +} + // EncodeVILA encodes a variable item length array. func EncodeVILA(data []byte, items ...[]byte) ArrayPushFunc { n := 0 -- 2.46.1 From a927b9519e6569e81b64c209791a1ea0f9bbd6e6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 23 May 2025 00:03:39 -0400 Subject: [PATCH 058/132] tape: Update tape.String to include ~[]byte --- tape/types.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tape/types.go b/tape/types.go index 448e0cf..70ba3a2 100644 --- a/tape/types.go +++ b/tape/types.go @@ -1,6 +1,9 @@ // Package tape implements Table Pair Encoding. package tape +// TODO: in math/rand: uint is capitalized like Uint, we need to replicate that +// here + import "fmt" // encoding and decoding functions must not make any allocations @@ -31,7 +34,7 @@ type Int64 interface { ~uint64 | ~int64 } // UInt is any unsigned integer. type UInt interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } // String is any string. -type String interface { ~string } +type String interface { ~string | ~[]uint8 } // DecodeI8 decodes an 8 bit integer from the given data. func DecodeI8[T Int8](data []byte) (value T, n int, err error) { -- 2.46.1 From 134daacc038b24e818e4380c7037b03c6337e040 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 23 May 2025 00:04:57 -0400 Subject: [PATCH 059/132] tape: Switch StringArray over to using VILA --- tape/array.go | 54 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/tape/array.go b/tape/array.go index 04265d2..2b9273a 100644 --- a/tape/array.go +++ b/tape/array.go @@ -83,7 +83,7 @@ func DecodeVILAIter(data []byte) iter.Seq[[]byte] { } // EncodeVILA encodes a variable item length array. -func EncodeVILA(data []byte, items ...[]byte) ArrayPushFunc { +func EncodeVILA(data []byte) ArrayPushFunc { n := 0 return func(value []byte) (n_ int, err error) { if n >= len(data) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } @@ -101,49 +101,47 @@ func EncodeVILA(data []byte, items ...[]byte) ArrayPushFunc { } } -// TODO: Decode, encode, size for string array needs to use VILA +// VILASize returns the size of a variable item length array. +func VILASize(items ...[]byte) int { + size := 0 + for _, item := range items { + size += GBEUSize[uint](uint(len(item))) + size += len(item) + } + return size +} -// DecodeStringArray decodes a packed string array from the given data. +// DecodeStringArray decodes a VILA string array from the given data. func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { - for len(data) > 0 { - if len(data) < 2 { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } - itemSize16, nn, _ := DecodeI16[uint16](data[:2]) - itemSize := int(itemSize16) + pull := DecodeVILA(data) + for { + item, nn, err := pull() n += nn - data = data[nn:] - if len(data) < itemSize { return nil, n, fmt.Errorf("decoding []string: %w", ErrWrongBufferLength) } - result = append(result, T(data[:itemSize])) - data = data[itemSize:] - n += itemSize + if err != nil { return nil, n, err } + result = append(result, T(item)) } return result, n, nil } -// EncodeStringArray encodes a packed string array into the given buffer. +// EncodeStringArray encodes a VILA string array into the given buffer. func EncodeStringArray[T String](buffer []byte, value []T) (n int, err error) { + push := EncodeVILA(buffer) for _, item := range value { - length, err := StringSize(item) + nn, err := push([]byte(item)) + n += nn if err != nil { return n, err } - if len(buffer) < 2 + length { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } - EncodeI16(buffer[:2], uint16(length)) - buffer = buffer[2:] - copy(buffer, item) - buffer = buffer[length:] - n += 2 + length } - if len(buffer) > 0 { return n, fmt.Errorf("encoding []string: %w", ErrWrongBufferLength) } return n, nil } -// StringArraySize returns the size of a packed string array. Returns 0 and an -// error if the size is too large. -func StringArraySize[T String](value []T) (int, error) { - total := 0 +// StringArraySize returns the size of a VILA string array. +func StringArraySize[T String](value []T) int { + size := 0 for _, item := range value { - total += 2 + len(item) + size += GBEUSize[uint](uint(len(item))) + size += len(item) } - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil + return size } // DecodeI8Array decodes a packed array of 8 bit integers from the given data. -- 2.46.1 From 717754644c50b7facec73d39c08bec425ff0879a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 23 May 2025 00:15:56 -0400 Subject: [PATCH 060/132] tape: Fix capitalization of Uint --- tape/types.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tape/types.go b/tape/types.go index 70ba3a2..490655b 100644 --- a/tape/types.go +++ b/tape/types.go @@ -1,9 +1,6 @@ // Package tape implements Table Pair Encoding. package tape -// TODO: in math/rand: uint is capitalized like Uint, we need to replicate that -// here - import "fmt" // encoding and decoding functions must not make any allocations @@ -31,8 +28,8 @@ type Int16 interface { ~uint16 | ~int16 } type Int32 interface { ~uint32 | ~int32 } // Int64 is any 64-bit integer. type Int64 interface { ~uint64 | ~int64 } -// UInt is any unsigned integer. -type UInt interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } +// Uint is any unsigned integer. +type Uint interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } // String is any string. type String interface { ~string | ~[]uint8 } @@ -110,7 +107,7 @@ func EncodeI64[T Int64](buffer []byte, value T) (n int, err error) { } // DecodeGBEU decodes an 8 to 64 bit growing integer into the given buffer. -func DecodeGBEU[T UInt](data []byte) (value T, n int, err error) { +func DecodeGBEU[T Uint](data []byte) (value T, n int, err error) { var fullValue uint64 for _, chunk := range data { fullValue *= 0x80 @@ -125,7 +122,7 @@ func DecodeGBEU[T UInt](data []byte) (value T, n int, err error) { } // EncodeGBEU encodes an 8 to 64 bit growing integer into a given buffer. -func EncodeGBEU[T UInt] (buffer []byte, value T) (n int, err error) { +func EncodeGBEU[T Uint] (buffer []byte, value T) (n int, err error) { window := (GBEUSize(value) - 1) * 7 index := 0 @@ -145,7 +142,7 @@ func EncodeGBEU[T UInt] (buffer []byte, value T) (n int, err error) { } // GBEUSize returns the size (in octets) of a GBEU integer. -func GBEUSize[T UInt] (value T) int { +func GBEUSize[T Uint] (value T) int { length := 0 for { value >>= 7 -- 2.46.1 From 83fa77ba1363b4a1cb6c768fd87bcf0746964262 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 30 May 2025 07:05:55 -0400 Subject: [PATCH 061/132] codec: Add codec package to handle encoding and decoding ints, etc --- codec/decode.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++ codec/encode.go | 90 ++++++++++++++++++++++++++++++++++++++++++ codec/measure.go | 11 ++++++ 3 files changed, 201 insertions(+) create mode 100644 codec/decode.go create mode 100644 codec/encode.go create mode 100644 codec/measure.go diff --git a/codec/decode.go b/codec/decode.go new file mode 100644 index 0000000..e665e99 --- /dev/null +++ b/codec/decode.go @@ -0,0 +1,100 @@ +package codec + +import "io" + +// Decoder wraps an [io.Reader] and decodes data from it. +type Decoder struct { + io.Reader +} + +// ReadFull calls [io.ReadFull] on the reader. +func (this *Decoder) ReadFull(buffer []byte) (n int, err error) { + return io.ReadFull(this, buffer) +} + +// ReadByte decodes a single byte from the input reader. +func (this *Decoder) ReadByte() (value byte, n int, err error) { + uncasted, n, err := this.ReadUint8() + return byte(uncasted), n, err +} + +// ReadInt8 decodes an 8-bit signed integer from the input reader. +func (this *Decoder) ReadInt8() (value int8, n int, err error) { + uncasted, n, err := this.ReadUint8() + return int8(uncasted), n, err +} + +// ReadUint8 decodes an 8-bit unsigned integer from the input reader. +func (this *Decoder) ReadUint8() (value uint8, n int, err error) { + buffer := [1]byte { } + n, err = this.ReadFull(buffer[:]) + return uint8(buffer[0]), n, err +} + +// ReadInt16 decodes an 16-bit signed integer from the input reader. +func (this *Decoder) ReadInt16() (value int16, n int, err error) { + uncasted, n, err := this.ReadUint16() + return int16(uncasted), n, err +} + +// ReadUint16 decodes an 16-bit unsigned integer from the input reader. +func (this *Decoder) ReadUint16() (value uint16, n int, err error) { + buffer := [2]byte { } + n, err = this.ReadFull(buffer[:]) + return uint16(buffer[0]) << 8 | + uint16(buffer[1]), n, err +} + +// ReadInt32 decodes an 32-bit signed integer from the input reader. +func (this *Decoder) ReadInt32() (value int32, n int, err error) { + uncasted, n, err := this.ReadUint32() + return int32(uncasted), n, err +} + +// ReadUint32 decodes an 32-bit unsigned integer from the input reader. +func (this *Decoder) ReadUint32() (value uint32, n int, err error) { + buffer := [4]byte { } + n, err = this.ReadFull(buffer[:]) + return uint32(buffer[0]) << 24 | + uint32(buffer[1]) << 16 | + uint32(buffer[2]) << 8 | + uint32(buffer[3]), n, err +} + +// ReadInt64 decodes an 64-bit signed integer from the input reader. +func (this *Decoder) ReadInt64() (value int64, n int, err error) { + uncasted, n, err := this.ReadUint64() + return int64(uncasted), n, err +} + +// ReadUint64 decodes an 64-bit unsigned integer from the input reader. +func (this *Decoder) ReadUint64() (value uint64, n int, err error) { + buffer := [8]byte { } + n, err = this.ReadFull(buffer[:]) + return uint64(buffer[0]) << 56 | + uint64(buffer[1]) << 48 | + uint64(buffer[2]) << 48 | + uint64(buffer[3]) << 32 | + uint64(buffer[4]) << 24 | + uint64(buffer[5]) << 16 | + uint64(buffer[6]) << 8 | + uint64(buffer[7]), n, err +} + +// ReadGBEU decodes a growing unsigned integer of up to 64 bits from the input +// reader. +func (this *Decoder) ReadGBEU() (value uint64, n int, err error) { + var fullValue uint64 + for { + chunk, nn, err := this.ReadByte() + if err != nil { return 0, n, err } + n += nn + + fullValue *= 0x80 + fullValue += uint64(chunk & 0x7F) + ccb := chunk >> 7 + if ccb == 0 { + return fullValue, n, nil + } + } +} diff --git a/codec/encode.go b/codec/encode.go new file mode 100644 index 0000000..a18febd --- /dev/null +++ b/codec/encode.go @@ -0,0 +1,90 @@ +package codec + +import "io" + +// Encoder wraps an [io.Writer] and encodes data to it. +type Encoder struct { + io.Writer +} + +// WriteInt8 encodes an 8-bit signed integer to the output writer. +func (this *Encoder) WriteInt8(value int8) (n int, err error) { + return this.WriteUint8(uint8(value)) +} + +// WriteUint8 encodes an 8-bit unsigned integer to the output writer. +func (this *Encoder) WriteUint8(value uint8) (n int, err error) { + return this.Write([]byte { byte(value) }) +} + +// WriteInt16 encodes an 16-bit signed integer to the output writer. +func (this *Encoder) WriteInt16(value int16) (n int, err error) { + return this.WriteUint16(uint16(value)) +} + +// WriteUint16 encodes an 16-bit unsigned integer to the output writer. +func (this *Encoder) WriteUint16(value uint16) (n int, err error) { + return this.Write([]byte { + byte(value >> 8), + byte(value), + }) +} + +// WriteInt32 encodes an 32-bit signed integer to the output writer. +func (this *Encoder) WriteInt32(value int32) (n int, err error) { + return this.WriteUint32(uint32(value)) +} + +// WriteUint32 encodes an 32-bit unsigned integer to the output writer. +func (this *Encoder) WriteUint32(value uint32) (n int, err error) { + return this.Write([]byte { + byte(value >> 24), + byte(value >> 16), + byte(value >> 8), + byte(value), + }) +} + +// WriteInt64 encodes an 64-bit signed integer to the output writer. +func (this *Encoder) WriteInt64(value int64) (n int, err error) { + return this.WriteUint64(uint64(value)) +} + +// WriteUint64 encodes an 64-bit unsigned integer to the output writer. +func (this *Encoder) WriteUint64(value uint64) (n int, err error) { + return this.Write([]byte { + byte(value >> 56), + byte(value >> 48), + byte(value >> 40), + byte(value >> 32), + byte(value >> 24), + byte(value >> 16), + byte(value >> 8), + byte(value), + }) +} + +// EncodeGBEU encodes a growing unsigned integer of up to 64 bits to the output +// writer. +func (this *Encoder) EncodeGBEU(value uint64) (n int, err error) { + // increase if go somehow gets support for over 64 bit integers. we + // could also make an expanding int type in goutil to use here, or maybe + // there is one in the stdlib. keep this int64 version as well though + // because its ergonomic. + buffer := [16]byte { } + + window := (GBEUSize(value) - 1) * 7 + index := 0 + for window >= 0 { + chunk := uint8(value >> window) & 0x7F + if window > 0 { + chunk |= 0x80 + } + buffer[index] = chunk + + index += 1 + window -= 7 + } + + return this.Write(buffer[:]) +} diff --git a/codec/measure.go b/codec/measure.go new file mode 100644 index 0000000..777a982 --- /dev/null +++ b/codec/measure.go @@ -0,0 +1,11 @@ +package codec + +// GBEUSize returns the size (in octets) of a GBEU integer. +func GBEUSize(value uint64) int { + length := 0 + for { + value >>= 7 + length ++ + if value == 0 { return length } + } +} -- 2.46.1 From 58514f6afe3283fdd8b23d8d24b48554ce20c3fe Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 30 May 2025 07:08:43 -0400 Subject: [PATCH 062/132] codec: Add missing WriteByte function --- codec/encode.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/codec/encode.go b/codec/encode.go index a18febd..bbfbd6b 100644 --- a/codec/encode.go +++ b/codec/encode.go @@ -7,6 +7,11 @@ type Encoder struct { io.Writer } +// WriteByte encodes a single byte to the output writer. +func (this *Encoder) WriteByte(value byte) (n int, err error) { + return this.WriteByte(uint8(value)) +} + // WriteInt8 encodes an 8-bit signed integer to the output writer. func (this *Encoder) WriteInt8(value int8) (n int, err error) { return this.WriteUint8(uint8(value)) -- 2.46.1 From c4a985f622a750257100f96109610ea9fdf0bdba Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 30 May 2025 21:34:31 -0400 Subject: [PATCH 063/132] design: New TAPE design --- design/protocol.md | 132 +++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 76 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index d5e41e2..5967de4 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -40,92 +40,73 @@ designed to allow applications to be presented with data they are not equipped to handle while continuing to function normally. This enables backwards compatibile application protocol changes. -The length of a TAPE structure is assumed to be given by the surrounding -protocol, which is usually METADAPT-A or B. The root of a TAPE structure can be -any data value, but is usually a table, which can contain several values that -each have a numeric key. Values can also be nested. Both sides of the connection -must agree on what data type should be the root value, the data type of each -known table value, etc. +TAPE expresses types using tags. A tag is 8 bits in size, and is divided into +two parts: the Type Number (TN), and the Configuration Number (CN). The TN is 3 +bits, and the CN is 5 bits. Both are interpreted as unsigned integers. Both +sides of the connection must agree on the semantic meaning of the values and +their arrangement. + +TAPE is based on an encoding method previously developed by silt. ### Data Value Types -The table below lists all data value types supported by TAPE. +The table below lists all data value types supported by TAPE. They are discussed +in detail in the following sections. -| Name | Size | Description | Encoding Method -| ----------- | --------------: | --------------------------- | --------------- -| I8 | 1 | A signed 8-bit integer | BETC -| I16 | 2 | A signed 16-bit integer | BETC -| I32 | 4 | A signed 32-bit integer | BETC -| I64 | 8 | A signed 64-bit integer | BETC -| U8 | 1 | An unsigned 8-bit integer | BEU -| U16 | 2 | An unsigned 16-bit integer | BEU -| U32 | 4 | An unsigned 32-bit integer | BEU -| U64 | 8 | An unsigned 64-bit integer | BEU -| Array[^1] | | An array of any above type | PASTA -| String | | A UTF-8 string | UTF-8 -| StringArray | | An array the String type | VILA -| Table | | A table of any type | TTLV +| TN | Bits | Name | Description +| -: | ---: | ---- | ----------- +| 0 | 000 | SI | Small integer +| 1 | 001 | LI | Large integer +| 2 | 010 | FP | Floating point +| 3 | 011 | SBA | Small byte array +| 4 | 100 | LBA | Large byte array +| 5 | 101 | OTA | One-tag array +| 6 | 110 | KTV | Key-tag-value table +| 7 | 111 | N/A | Reserved -[^1]: Array types are written as Array, where is the element type. For -example, an array of I32 would be written as I32Array. StringArray still follows -this rule, even though it is encoded differently from other arrays. +#### No Value (NIL) +NIL is used to encode the absence of a value where there would otherwise be one. +The CN of a NIL is ignored. It has no payload. -[^2]: SOP (sum of parts) refers to the sum of the size of every item in a data -structure. +#### Small Integer (SI) +SI encodes an integer of up to 5 bits, which are stored in the CN. It has no +payload. Whether the bits are interpreted as unsigned or as signed two's +complement is semantic information and must be agreed upon by both sides of the +connection. Thus, the value may range from 0 to 31 if unsigned, and from -16 to +17 if signed. -### Encoding Methods -Below are all encoding methods supported by TAPE. +#### Large Integer (LI) +LI encodes an integer of up to 256 bits, which are stored in the payload. The CN +determine the length of the payload in bytes. The integer is big-endian. Whether +the payload is interpreted as unsigned or as signed two's complement is semantic +information and must be agreed upon by both sides of the connection. Thus, the +value may range from 0 to 31 if unsigned, and from -16 to 17 if signed. -#### BETC -Big-Endian, Two's Complement signed integer. The size is defined as the least -amount of whole octets which can fit all bits in the integer, regardless if the -bits are on or off. Therefore, the size cannot change at runtime. +#### Floating Point (FP) +FP encodes an IEEE 754 floating point number of up to 256 bits, which are stored +in the payload. The CN determines the length of the payload in bytes, and it may +only be one of these values: 16, 32, 64, 128, or 256. -#### BEU -Big-Endian, Unsigned integer. The size is defined as the least amount of whole -octets which can fit all bits in the integer, regardless if the bits are on or -off. Therefore, the size cannot change at runtime. +#### Small Byte Array (SBA) +SBA encodes an array of up to 32 bytes, which are stored in the paylod. The +CN determines the length of the payload in bytes. -#### GBEU -Growing Big-Endian, Unsigned integer. The integer is broken up into 8-bit -chunks, where the first bit of each chunk is a CCB. The chunk with its CCB set -to zero instead of one is the last chunk in the integer. Chunks are ordered from -most significant to least significant (big endian). The size is defined as the -least amount of whole octets which can fit all chunks of the integer. The size -of this type is not fixed and may change at runtime, so this needs to be -accounted for during use. +#### Large Byte Array (LBA) +LBA encodes an array of up to 2^256 bytes, which are stored in the second part +of the payload, directly after the length. The length of the data length field +in bytes is determined by the CN. -#### PASTA -Packed Single-Type Array. The size is defined as the size of an individual item -times the number of items. Items are placed one after the other with no gaps -in-between them, except as required to align the start of each item to the -nearest whole octet. Items should be of the same type and must be of the same -size. +#### One-Tag Array (OTA) +OTA encodes an array of up to 2^256 items, which are stored in the payload after +the length field and the item tag, where the length field comes first. Each item +must be the same length, as they all share the same tag. The length of the data +length field in bytes is determined by the CN. -#### UTF-8 -UTF-8 string. The size is defined as the least amount of whole octets which can -fit all bits in the string, regardless if the bits are on or off. The size of -this type is not fixed and may change at runtime, so this needs to be accounted -for during use. - -#### VILA -Variable Item Length Array. The size is defined as the least amount of whole -octets which can fit each item plus one GBEU per item describing that item's -size. The size of this type is not fixed and may change at runtime, so this -needs to be accounted for during use. The amount of items must be greater than -zero. Items are each prefixed by their size (in octets) encoded as a GBEU, and -they are placed one after the other with no gaps in-between them, except as -required to align the start of each item to the nearest whole octet. Items -should be of the same type but do not need to be of the same size. - -#### TTLV -TAPE Tag Length Value. The size is defined as the least amount of whole octets -which can fit each item plus one U16 and one GBEU per item, where the latter of -which describes that item's size. The size of this type is not fixed and may -change at runtime, so this needs to be accounted for during use. Items are each -prefixed by their numerical tag encoded as a U16, and their size (in octets) -encoded as a GBEU. Items are placed one after the other with no gaps in-between -them, except as required to align the start of each item to the nearest whole -octet. Items need not be of the same type nor the same size. +#### Key-Tag-Value Table (KTV) +KTV encodes a table of up to 2^256 key/value pairs, which are stored in the +payload after the length field. The pairs themselves consist of a 16-bit +unsigned big-endian key followed by a tag and then the payload. Pair values can +be of different types and sizes. The order of the pairs is not significant and +should never be treated as such. ## Transports A transport is a protocol that HOPP connections can run on top of. HOPP @@ -176,7 +157,6 @@ sun will have expanded to swallow earth by then. Your connection will not last that long. #### Message Chunking - The most significant bit of the payload size field of an MMB is called the Chunk Control Bit (CCB). If the CCB of a given MMB is zero, the represented message is interpreted as being self-contained and the data is processed immediately. If -- 2.46.1 From 99392d50fce7acabaa3228b8398982c6123d2433 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 31 May 2025 06:18:03 -0400 Subject: [PATCH 064/132] design: Remove description of NIL value --- design/protocol.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/design/protocol.md b/design/protocol.md index 5967de4..a9a4db8 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -63,10 +63,6 @@ in detail in the following sections. | 6 | 110 | KTV | Key-tag-value table | 7 | 111 | N/A | Reserved -#### No Value (NIL) -NIL is used to encode the absence of a value where there would otherwise be one. -The CN of a NIL is ignored. It has no payload. - #### Small Integer (SI) SI encodes an integer of up to 5 bits, which are stored in the CN. It has no payload. Whether the bits are interpreted as unsigned or as signed two's -- 2.46.1 From 38132dc58c901be883b8e0e02dbd44d1d505f247 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 1 Jun 2025 23:04:24 -0400 Subject: [PATCH 065/132] design: Add note about TAPE root values --- design/protocol.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design/protocol.md b/design/protocol.md index a9a4db8..f1a107e 100644 --- a/design/protocol.md +++ b/design/protocol.md @@ -46,6 +46,10 @@ bits, and the CN is 5 bits. Both are interpreted as unsigned integers. Both sides of the connection must agree on the semantic meaning of the values and their arrangement. +A TAPE structure begins with one root, which consists of a tag followed by a +payload. This is usually an aggregate data structure such as KTV to allow for +several different values. + TAPE is based on an encoding method previously developed by silt. ### Data Value Types -- 2.46.1 From b85f3e7866aec2a1ed72542b0984603c7711132f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 1 Jun 2025 23:06:28 -0400 Subject: [PATCH 066/132] tape: Clean slate --- tape/array.go | 284 --------------------------------------- tape/array_test.go | 131 ------------------ tape/table.go | 74 ---------- tape/table_test.go | 144 -------------------- tape/types.go | 180 ------------------------- tape/types_test.go | 326 --------------------------------------------- 6 files changed, 1139 deletions(-) delete mode 100644 tape/array.go delete mode 100644 tape/array_test.go delete mode 100644 tape/table.go delete mode 100644 tape/table_test.go delete mode 100644 tape/types.go delete mode 100644 tape/types_test.go diff --git a/tape/array.go b/tape/array.go deleted file mode 100644 index 2b9273a..0000000 --- a/tape/array.go +++ /dev/null @@ -1,284 +0,0 @@ -package tape - -import "fmt" -import "iter" -import "slices" - -// encoding and decoding functions must not make any allocations - -// ArrayPushFunc adds an item to an array that is being encoded. -type ArrayPushFunc func(value []byte) (n int, err error) - -// ArrayPullFunc gets the next item of an array that is being decoded. -type ArrayPullFunc func() (value []byte, n int, err error) - -// DecodePASTA decodes a packed single-type array. -func DecodePASTA(data []byte, itemLength int) ArrayPullFunc { - n := 0 - return func() (value []byte, n_ int, err error) { - if n > len(data) - itemLength { - return nil, 0, fmt.Errorf("decoding PASTA: %w", ErrWrongBufferLength) - } - value = data[n:n + itemLength] - n += itemLength - return value, itemLength, nil - } -} - -// DecodePASTAIter decodes a packed single-type array and returns it as an -// iterator. -func DecodePASTAIter(data []byte, itemLength int) iter.Seq[[]byte] { - return slices.Chunk(data, itemLength) -} - -// EncodePASTA encodes a packed single-type array. -func EncodePASTA(data []byte, itemLength int) ArrayPushFunc { - n := 0 - return func(value []byte) (n_ int, err error) { - if n > len(data) - itemLength { - return 0, fmt.Errorf("encoding PASTA: %w", ErrWrongBufferLength) - } - copy(data[n:], value) - n += itemLength - return itemLength, nil - } -} - -// PASTASize returns the size of a packed single-type array. -func PASTASize(length, itemLength int) int { - return length * itemLength -} - -// DecodeVILA encodes a variable item length array. -func DecodeVILA(data []byte) ArrayPullFunc { - n := 0 - return func() (value []byte, n_ int, err error) { - if n >= len(data) { return nil, n_, fmt.Errorf("decoding VILA: %w", ErrWrongBufferLength) } - length, nn, err := DecodeGBEU[uint](data[n:]) - n += nn - n_ += nn - if err != nil { return nil, n_, err } - - if n > len(data) - int(length) { - return nil, n_, fmt.Errorf("decoding VILA: %w", ErrWrongBufferLength) - } - value = data[n:n + int(length)] - n += int(length) - n_ += int(length) - return value, int(length), nil - } -} - -// DecodeVILAIter decodes a variable item length array and returns it as an -// iterator. -func DecodeVILAIter(data []byte) iter.Seq[[]byte] { - return func(yield func([]byte) bool) { - pull := DecodeVILA(data) - for { - value, _, err := pull() - if err != nil { return } - if !yield(value) { return } - } - } -} - -// EncodeVILA encodes a variable item length array. -func EncodeVILA(data []byte) ArrayPushFunc { - n := 0 - return func(value []byte) (n_ int, err error) { - if n >= len(data) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } - nn, err := EncodeGBEU(data[n:], uint(len(value))) - n += nn - n_ += nn - if err != nil { return n, err } - - if n >= len(data) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } - nn = copy(data[n:], value) - n += nn - n_ += nn - if nn != len(value) { return n_, fmt.Errorf("encoding VILA: %w", ErrWrongBufferLength) } - return n_, err - } -} - -// VILASize returns the size of a variable item length array. -func VILASize(items ...[]byte) int { - size := 0 - for _, item := range items { - size += GBEUSize[uint](uint(len(item))) - size += len(item) - } - return size -} - -// DecodeStringArray decodes a VILA string array from the given data. -func DecodeStringArray[T String](data []byte) (result []T, n int, err error) { - pull := DecodeVILA(data) - for { - item, nn, err := pull() - n += nn - if err != nil { return nil, n, err } - result = append(result, T(item)) - } - return result, n, nil -} - -// EncodeStringArray encodes a VILA string array into the given buffer. -func EncodeStringArray[T String](buffer []byte, value []T) (n int, err error) { - push := EncodeVILA(buffer) - for _, item := range value { - nn, err := push([]byte(item)) - n += nn - if err != nil { return n, err } - } - return n, nil -} - -// StringArraySize returns the size of a VILA string array. -func StringArraySize[T String](value []T) int { - size := 0 - for _, item := range value { - size += GBEUSize[uint](uint(len(item))) - size += len(item) - } - return size -} - -// DecodeI8Array decodes a packed array of 8 bit integers from the given data. -func DecodeI8Array[T Int8](data []byte) (result []T, n int, err error) { - result = make([]T, len(data)) - for index, item := range data { - result[index] = T(item) - } - return result, len(data), nil -} - -// EncodeI8Array encodes a packed array of 8 bit integers into the given buffer. -func EncodeI8Array[T Int8](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) { return 0, fmt.Errorf("encoding []int8: %w", ErrWrongBufferLength) } - for index, item := range value { - buffer[index] = byte(item) - } - return len(buffer), nil -} - -// I8ArraySize returns the size of a packed 8 bit integer array. Returns 0 and -// an error if the size is too large. -func I8ArraySize[T Int8](value []T) (int, error) { - total := len(value) - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI16Array decodes a packed array of 16 bit integers from the given data. -func DecodeI16Array[T Int16](data []byte) (value []T, n int, err error) { - if len(data) % 2 != 0 { return nil, 0, fmt.Errorf("decoding []int16: %w", ErrWrongBufferLength) } - length := len(data) / 2 - result := make([]T, length) - for index := range length { - offset := index * 2 - result[index] = T(data[offset]) << 8 | T(data[offset + 1]) - } - return result, len(data) / 2, nil -} - -// EncodeI16Array encodes a packed array of 16 bit integers into the given buffer. -func EncodeI16Array[T Int16](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 2 { return 0, fmt.Errorf("encoding []int16: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 8) - buffer[1] = byte(item) - buffer = buffer[2:] - } - return len(value) * 2, nil -} - -// I16ArraySize returns the size of a packed 16 bit integer array. Returns 0 and -// an error if the size is too large. -func I16ArraySize[T Int16](value []T) (int, error) { - total := len(value) * 2 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI32Array decodes a packed array of 32 bit integers from the given data. -func DecodeI32Array[T Int32](data []byte) (value []T, n int, err error) { - if len(data) % 4 != 0 { return nil, 0, fmt.Errorf("decoding []int32: %w", ErrWrongBufferLength) } - length := len(data) / 4 - result := make([]T, length) - for index := range length { - offset := index * 4 - result[index] = - T(data[offset + 0]) << 24 | - T(data[offset + 1]) << 16 | - T(data[offset + 2]) << 8 | - T(data[offset + 3]) - } - return result, len(data) / 4, nil -} - -// EncodeI32Array encodes a packed array of 32 bit integers into the given buffer. -func EncodeI32Array[T Int32](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 4 { return 0, fmt.Errorf("encoding []int32: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 24) - buffer[1] = byte(item >> 16) - buffer[2] = byte(item >> 8) - buffer[3] = byte(item) - buffer = buffer[4:] - } - return len(value) * 4, nil -} - -// I32ArraySize returns the size of a packed 32 bit integer array. Returns 0 and -// an error if the size is too large. -func I32ArraySize[T Int32](value []T) (int, error) { - total := len(value) * 4 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} - -// DecodeI64Array decodes a packed array of 32 bit integers from the given data. -func DecodeI64Array[T Int64](data []byte) (value []T, n int, err error) { - if len(data) % 8 != 0 { return nil, 0, fmt.Errorf("decoding []int64: %w", ErrWrongBufferLength) } - length := len(data) / 8 - result := make([]T, length) - for index := range length { - offset := index * 8 - result[index] = - T(data[offset + 0]) << 56 | - T(data[offset + 1]) << 48 | - T(data[offset + 2]) << 40 | - T(data[offset + 3]) << 32 | - T(data[offset + 4]) << 24 | - T(data[offset + 5]) << 16 | - T(data[offset + 6]) << 8 | - T(data[offset + 7]) - } - return result, len(data) / 8, nil -} - -// EncodeI64Array encodes a packed array of 64 bit integers into the given buffer. -func EncodeI64Array[T Int64](buffer []byte, value []T) (n int, err error) { - if len(buffer) != len(value) * 8 { return 0, fmt.Errorf("encoding []int64: %w", ErrWrongBufferLength) } - for _, item := range value { - buffer[0] = byte(item >> 56) - buffer[1] = byte(item >> 48) - buffer[2] = byte(item >> 40) - buffer[3] = byte(item >> 32) - buffer[4] = byte(item >> 24) - buffer[5] = byte(item >> 16) - buffer[6] = byte(item >> 8) - buffer[7] = byte(item) - buffer = buffer[8:] - } - return len(value) * 8, nil -} - -// I64ArraySize returns the size of a packed 64 bit integer array. Returns 0 and -// an error if the size is too large. -func I64ArraySize[T Int64](value []T) (int, error) { - total := len(value) * 8 - if total > dataMaxSize { return 0, ErrDataTooLarge } - return total, nil -} diff --git a/tape/array_test.go b/tape/array_test.go deleted file mode 100644 index 004cf85..0000000 --- a/tape/array_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package tape - -// import "fmt" -import "slices" -// import "errors" -import "testing" -import "math/rand" - -func TestI8Array(test *testing.T) { - var buffer [64]byte - _, err := EncodeI8Array[uint8](buffer[:], []uint8 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI8Array[uint8](buffer[:0], []uint8 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int8: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI8Array[uint8](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI8Array[uint8](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint8](rand.Intn(16)) - length, _ := I8ArraySize(array) - if length != len(array) { test.Fatalf("%d != %d", length, len(array)) } - _, err := EncodeI8Array[uint8](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI8Array[uint8](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI16Array(test *testing.T) { - var buffer [128]byte - _, err := EncodeI16Array[uint16](buffer[:], []uint16 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI16Array[uint16](buffer[:0], []uint16 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int16: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI16Array[uint16](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI16Array[uint16](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint16](rand.Intn(16)) - length, _ := I16ArraySize(array) - if length != 2 * len(array) { test.Fatalf("%d != %d", length, 2 * len(array)) } - _, err := EncodeI16Array[uint16](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI16Array[uint16](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI32Array(test *testing.T) { - var buffer [256]byte - _, err := EncodeI32Array[uint32](buffer[:], []uint32 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI32Array[uint32](buffer[:0], []uint32 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int32: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI32Array[uint32](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI32Array[uint32](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint32](rand.Intn(16)) - length, _ := I32ArraySize(array) - if length != 4 * len(array) { test.Fatalf("%d != %d", length, 4 * len(array)) } - _, err := EncodeI32Array[uint32](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI32Array[uint32](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestI64Array(test *testing.T) { - var buffer [512]byte - _, err := EncodeI64Array[uint64](buffer[:], []uint64 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - _, err = EncodeI64Array[uint64](buffer[:0], []uint64 { 0, 4, 50, 19 }) - if !errIs(err, ErrWrongBufferLength, "encoding []int64: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeI64Array[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI64Array[uint64](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randInts[uint64](rand.Intn(16)) - length, _ := I64ArraySize(array) - if length != 8 * len(array) { test.Fatalf("%d != %d", length, 8 * len(array)) } - _, err := EncodeI64Array[uint64](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeI64Array[uint64](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - -func TestStringArray(test *testing.T) { - var buffer [8192]byte - _, err := EncodeStringArray[string](buffer[:], []string { "0", "4", "50", "19" }) - if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - _, err = EncodeStringArray[string](buffer[:0], []string { "0", "4", "50", "19" }) - if !errIs(err, ErrWrongBufferLength, "encoding []string: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeStringArray[string](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - array := randStrings[string](rand.Intn(16), 16) - length, _ := StringArraySize(array) - // TODO test length - _, err := EncodeStringArray[string](buffer[:length], array) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeStringArray[string](buffer[:length]) - if err != nil { test.Fatal(err) } - if !slices.Equal(decoded, array) { - test.Fatalf("%v != %v", decoded, array) - } - } -} - diff --git a/tape/table.go b/tape/table.go deleted file mode 100644 index 349aa79..0000000 --- a/tape/table.go +++ /dev/null @@ -1,74 +0,0 @@ -package tape - -import "iter" - -// encoding and decoding functions must not make any allocations - -type TablePushFunc func(tag uint16, value []byte) (n int, err error) - -type TablePullFunc func() (tag uint16, value []byte, n int, err error) - -func DecodeTable(data []byte) TablePullFunc { - n := 0 - return func() (tag uint16, value []byte, n_ int, err error) { - if n >= len(data) { return 0, nil, n, ErrWrongBufferLength } - tag, nn, err := DecodeI16[uint16](data[n:]) - if err != nil { return 0, nil, n, err } - n += nn - - if n >= len(data) { return 0, nil, n, ErrWrongBufferLength } - length, nn, err := DecodeGBEU[uint64](data[n:]) - if err != nil { return 0, nil, n, err } - n += nn - - end := n + int(length) - if end > len(data) { return 0, nil, n, ErrWrongBufferLength } - value = data[n:end] - n += int(length) - return tag, value, n, err - } -} - -func DecodeTableIter(data []byte) iter.Seq2[uint16, []byte] { - return func(yield func(uint16, []byte) bool) { - pull := DecodeTable(data) - for { - tag, value, _, err := pull() - if err != nil { return } - if !yield(tag, value) { return } - } - } -} - -func EncodeTable(data []byte) TablePushFunc { - return func(tag uint16, value []byte) (n int, err error) { - if n >= len(data) { return n, ErrWrongBufferLength } - nn, err := EncodeI16(data[n:], uint16(tag)) - if err != nil { return n, err } - n += nn - - if n >= len(data) { return n, ErrWrongBufferLength } - nn, err = EncodeGBEU(data[n:], uint(len(value))) - if err != nil { return n, err } - n += nn - - if n >= len(data) { return n, ErrWrongBufferLength } - nn = copy(data[n:], value) - n += nn - if nn < len(value) { - return n, ErrWrongBufferLength - } - - if n >= len(data) { return n, ErrWrongBufferLength } - data = data[n:] - return n, nil - } -} - -func TableSize(itemLengths ...int) int { - sum := 0 - for _, length := range itemLengths { - sum += GBEUSize(uint(length)) + length - } - return sum -} diff --git a/tape/table_test.go b/tape/table_test.go deleted file mode 100644 index 361d30a..0000000 --- a/tape/table_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package tape - -import "fmt" -import "slices" -// import "errors" -import "testing" -// import "math/rand" - -var longText = -`Curious, I started off in that direction, only for Prism to stop me. "Wrong way, Void. It's right over here." He trotted over to a door to the left of us. It was marked with the number '4004'. He took a key out of his saddlebags, unlocked it, and pushed it open. "You know, some say this suite is haunted. They call the ghost that lives here the 'Spirit of 4004'. Ooooh!" He made paddling motions in the air with his hooves.` - -func TestTable(test *testing.T) { - item5 := []byte("hello") - item7 := []byte("world") - item0 := []byte(longText) - item3249 := []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 } - - buffer := [512]byte { } - push := EncodeTable(buffer[:]) - _, err := push(5, item5) - if err != nil { test.Fatal(err)} - _, err = push(7, item7) - if err != nil { test.Fatal(err)} - _, err = push(0, item0) - if err != nil { test.Fatal(err)} - _, err = push(3249, item3249) - if err != nil { test.Fatal(err)} - - test.Logf("len of longText: %d 0x%X", len(longText), len(longText)) - correct := []byte("\x00\x05\x05hello\x00\x07\x05world\x00\x00\x83\x28" + longText) - if got := buffer[:len(correct)]; !slices.Equal(got, correct) { - if !compareHexArray(test, got, correct) { test.Fatal("failed") } - } - - pull := DecodeTable(buffer[:len(correct)]) - - tag, value, _, err := pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(5); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte("hello")) { test.Fatal("failed") } - - tag, value, _, err = pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(7); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte("world")) { test.Fatal("failed") } - - tag, value, _, err = pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(0); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte(longText)) { test.Fatal("failed") } - - tag, value, _, err = pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(3249); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 }) { test.Fatal("failed") } -} - -func TestTableSmall(test *testing.T) { - item2 := []byte("hello") - item3249 := []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 } - - buffer := [64]byte { } - push := EncodeTable(buffer[:]) - _, err := push(2, item2) - if err != nil { test.Fatal(err) } - _, err = push(3249, item3249) - if err != nil { test.Fatal(err) } - - correct := []byte("\x00\x02\x05hello\x0C\xB1\x06\x00\x01\x02\x03\xA0\x05") - if got := buffer[:len(correct)]; !slices.Equal(got, correct) { - if !compareHexArray(test, got, correct) { test.Fatal("failed") } - } - - pull := DecodeTable(buffer[:len(correct)]) - - tag, value, _, err := pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(2); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte("hello")) { test.Fatal("failed") } - - tag, value, _, err = pull() - if err != nil { test.Fatal(err) } - if got, correct := tag, uint16(3249); got != correct { - test.Fatal("not equal:", got) - } - if !compareHexArray(test, value, []byte { 0x0, 0x1, 0x2, 0x3, 0xA0, 0x5 }) { test.Fatal("failed") } -} - -func dumpHexArray(data []byte) (message string) { - for _, item := range data { - message = fmt.Sprintf("%s %02X", message, item) - } - return message -} - -func compareHexArray(test *testing.T, got, correct []byte) bool { - index := 0 - for { - if index >= len(correct) { - if index < len(got) { - test.Log("correct longer than got") - test.Log("got: ", dumpHexArray(got)) - test.Log("correct:", dumpHexArray(correct)) - return false - } - break - } - if index >= len(got) { - if index < len(correct) { - test.Log("got longer than correct") - test.Log("got: ", dumpHexArray(got)) - test.Log("correct:", dumpHexArray(correct)) - return false - } - break - } - if correct[index] != got[index] { - test.Log("not equal") - test.Log("got: ", dumpHexArray(got)) - test.Log("correct:", dumpHexArray(correct)) - partLow := index - 8 - partHigh := index + 8 - test.Log("got part ", dumpHexArray(safeSlice(got, partLow, partHigh))) - test.Log("correct part", dumpHexArray(safeSlice(correct, partLow, partHigh))) - return false - } - index ++ - } - return true -} - -func safeSlice[T any](slice []T, low, high int) []T { - return slice[max(low, 0):min(high, len(slice))] -} diff --git a/tape/types.go b/tape/types.go deleted file mode 100644 index 490655b..0000000 --- a/tape/types.go +++ /dev/null @@ -1,180 +0,0 @@ -// Package tape implements Table Pair Encoding. -package tape - -import "fmt" - -// encoding and decoding functions must not make any allocations - -const dataMaxSize = 0xFFFF -const uint16Max = 0xFFFF - -// Error enumerates common errors in this package. -type Error string; const ( - ErrWrongBufferLength Error = "wrong buffer length" - ErrDataTooLarge Error = "data too large" - ErrGBEUNotTerminated Error = "GBEU not terminated" -) - -// Error implements the error interface. -func (err Error) Error() string { - return string(err) -} - -// Int8 is any 8-bit integer. -type Int8 interface { ~uint8 | ~int8 } -// Int16 is any 16-bit integer. -type Int16 interface { ~uint16 | ~int16 } -// Int32 is any 32-bit integer. -type Int32 interface { ~uint32 | ~int32 } -// Int64 is any 64-bit integer. -type Int64 interface { ~uint64 | ~int64 } -// Uint is any unsigned integer. -type Uint interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } -// String is any string. -type String interface { ~string | ~[]uint8 } - -// DecodeI8 decodes an 8 bit integer from the given data. -func DecodeI8[T Int8](data []byte) (value T, n int, err error) { - if len(data) < 1 { return 0, 0, fmt.Errorf("decoding int8: %w", ErrWrongBufferLength) } - return T(data[0]), 1, nil -} - -// EncodeI8 encodes an 8 bit integer into the given buffer. -func EncodeI8[T Int8](buffer []byte, value T) (n int, err error) { - if len(buffer) < 1 { return 0, fmt.Errorf("encoding int8: %w", ErrWrongBufferLength) } - buffer[0] = byte(value) - return 1, nil -} - -// DecodeI16 decodes a 16 bit integer from the given data. -func DecodeI16[T Int16](data []byte) (value T, n int, err error) { - if len(data) < 2 { return 0, 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) } - return T(data[0]) << 8 | T(data[1]), 2, nil -} - -// EncodeI16 encodes a 16 bit integer into the given buffer. -func EncodeI16[T Int16](buffer []byte, value T) (n int, err error) { - if len(buffer) < 2 { return 0, fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) } - buffer[0] = byte(value >> 8) - buffer[1] = byte(value) - return 2, nil -} - -// DecodeI32 decodes a 32 bit integer from the given data. -func DecodeI32[T Int32](data []byte) (value T, n int, err error) { - if len(data) < 4 { return 0, 0, fmt.Errorf("decoding int32: %w", ErrWrongBufferLength) } - return T(data[0]) << 24 | - T(data[1]) << 16 | - T(data[2]) << 8 | - T(data[3]), 4, nil -} - -// EncodeI32 encodes a 32 bit integer into the given buffer. -func EncodeI32[T Int32](buffer []byte, value T) (n int, err error) { - if len(buffer) < 4 { return 0, fmt.Errorf("encoding int32: %w", ErrWrongBufferLength) } - buffer[0] = byte(value >> 24) - buffer[1] = byte(value >> 16) - buffer[2] = byte(value >> 8) - buffer[3] = byte(value) - return 4, nil -} - -// DecodeI64 decodes a 64 bit integer from the given data. -func DecodeI64[T Int64](data []byte) (value T, n int, err error) { - if len(data) < 8 { return 0, 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) } - return T(data[0]) << 56 | - T(data[1]) << 48 | - T(data[2]) << 40 | - T(data[3]) << 32 | - T(data[4]) << 24 | - T(data[5]) << 16 | - T(data[6]) << 8 | - T(data[7]), 8, nil -} - -// EncodeI64 encodes a 64 bit integer into the given buffer. -func EncodeI64[T Int64](buffer []byte, value T) (n int, err error) { - if len(buffer) < 8 { return 0, fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) } - buffer[0] = byte(value >> 56) - buffer[1] = byte(value >> 48) - buffer[2] = byte(value >> 40) - buffer[3] = byte(value >> 32) - buffer[4] = byte(value >> 24) - buffer[5] = byte(value >> 16) - buffer[6] = byte(value >> 8) - buffer[7] = byte(value) - return 8, nil -} - -// DecodeGBEU decodes an 8 to 64 bit growing integer into the given buffer. -func DecodeGBEU[T Uint](data []byte) (value T, n int, err error) { - var fullValue uint64 - for _, chunk := range data { - fullValue *= 0x80 - fullValue += uint64(chunk & 0x7F) - ccb := chunk >> 7 - n += 1 - if ccb == 0 { - return T(fullValue), n, nil - } - } - return 0, n, fmt.Errorf("decoding GBEU: %w", ErrGBEUNotTerminated) -} - -// EncodeGBEU encodes an 8 to 64 bit growing integer into a given buffer. -func EncodeGBEU[T Uint] (buffer []byte, value T) (n int, err error) { - window := (GBEUSize(value) - 1) * 7 - - index := 0 - for window >= 0 { - if index >= len(buffer) { return index, fmt.Errorf("encoding GBEU: %w", ErrWrongBufferLength) } - - chunk := uint8(value >> window) & 0x7F - if window > 0 { - chunk |= 0x80 - } - buffer[index] = chunk - - index += 1 - window -= 7 - } - return index, nil -} - -// GBEUSize returns the size (in octets) of a GBEU integer. -func GBEUSize[T Uint] (value T) int { - length := 0 - for { - value >>= 7 - length ++ - if value == 0 { return length } - } -} - -// DecodeString decodes a string from the given data. -func DecodeString[T String](data []byte) (value T, n int, err error) { - return T(data), len(data), nil -} - -// EncodeString encodes a string into the given buffer. -func EncodeString[T String](data []byte, value T) (n int, err error) { - if len(data) != len(value) { return 0, fmt.Errorf("encoding string: %w", ErrWrongBufferLength) } - return copy(data, value), nil -} - -// StringSize returns the size of a string. Returns 0 and an error if the size -// is too large. -func StringSize[T String](value T) (int, error) { - if len(value) > dataMaxSize { return 0, ErrDataTooLarge } - return len(value), nil -} - -// U16CastSafe safely casts an integer to a uint16. If an overflow or underflow -// occurs, it will return (0, false). -func U16CastSafe(n int) (uint16, bool) { - if n < uint16Max && n >= 0 { - return uint16(n), true - } else { - return 0, false - } -} diff --git a/tape/types_test.go b/tape/types_test.go deleted file mode 100644 index cd0fbd6..0000000 --- a/tape/types_test.go +++ /dev/null @@ -1,326 +0,0 @@ -package tape - -import "fmt" -import "slices" -import "errors" -import "testing" -import "math/rand" - -const largeNumberNTestRounds = 2048 -const randStringBytes = "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - -func TestI8(test *testing.T) { - var buffer [16]byte - _, err := EncodeI8[uint8](buffer[:], 5) - if err != nil { test.Fatal(err) } - _, err = EncodeI8[uint8](buffer[:0], 5) - if err.Error() != "encoding int8: wrong buffer length" { test.Fatal(err) } - _, _, err = DecodeI8[uint8](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI8[uint8](buffer[:0]) - if err.Error() != "decoding int8: wrong buffer length" { test.Fatal(err) } - - for number := range uint8(255) { - n, err := EncodeI8[uint8](buffer[:1], number) - if err != nil { test.Fatal(err) } - if correct, got := 1, n; correct != got { - test.Fatal("not equal:", got) - } - decoded, n, err := DecodeI8[uint8](buffer[:1]) - if err != nil { test.Fatal(err) } - if correct, got := 1, n; correct != got { - test.Fatal("not equal:", got) - } - if decoded != number { - test.Fatalf("%d != %d", decoded, number) - } - } -} - -func TestI16(test *testing.T) { - var buffer [16]byte - _, err := EncodeI16[uint16](buffer[:], 5) - if err != nil { test.Fatal(err) } - _, err = EncodeI16[uint16](buffer[:0], 5) - if err.Error() != "encoding int16: wrong buffer length" { test.Fatal(err) } - _, _, err = DecodeI16[uint16](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI16[uint16](buffer[:0]) - if err.Error() != "decoding int16: wrong buffer length" { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - number := uint16(rand.Int()) - n, err := EncodeI16[uint16](buffer[:2], number) - if err != nil { test.Fatal(err) } - if correct, got := 2, n; correct != got { - test.Fatal("not equal:", got) - } - decoded, n, err := DecodeI16[uint16](buffer[:2]) - if err != nil { test.Fatal(err) } - if correct, got := 2, n; correct != got { - test.Fatal("not equal:", got) - } - if decoded != number { - test.Fatalf("%d != %d", decoded, number) - } - } -} - -func TestI32(test *testing.T) { - var buffer [16]byte - _, err := EncodeI32[uint32](buffer[:], 5) - if err != nil { test.Fatal(err) } - _, err = EncodeI32[uint32](buffer[:0], 5) - if err.Error() != "encoding int32: wrong buffer length" { test.Fatal(err) } - _, _, err = DecodeI32[uint32](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI32[uint32](buffer[:0]) - if err.Error() != "decoding int32: wrong buffer length" { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - number := uint32(rand.Int()) - n, err := EncodeI32[uint32](buffer[:4], number) - if err != nil { test.Fatal(err) } - if correct, got := 4, n; correct != got { - test.Fatal("not equal:", got) - } - decoded, n, err := DecodeI32[uint32](buffer[:4]) - if err != nil { test.Fatal(err) } - if correct, got := 4, n; correct != got { - test.Fatal("not equal:", got) - } - if decoded != number { - test.Fatalf("%d != %d", decoded, number) - } - } -} - -func TestI64(test *testing.T) { - var buffer [16]byte - _, err := EncodeI64[uint64](buffer[:], 5) - if err != nil { test.Fatal(err) } - _, err = EncodeI64[uint64](buffer[:0], 5) - if err.Error() != "encoding int64: wrong buffer length" { test.Fatal(err) } - _, _, err = DecodeI64[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeI64[uint64](buffer[:0]) - if err.Error() != "decoding int64: wrong buffer length" { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - number := uint64(rand.Int()) - n, err := EncodeI64[uint64](buffer[:8], number) - if err != nil { test.Fatal(err) } - if correct, got := 8, n; correct != got { - test.Fatal("not equal:", got) - } - decoded, n, err := DecodeI64[uint64](buffer[:8]) - if err != nil { test.Fatal(err) } - if correct, got := 8, n; correct != got { - test.Fatal("not equal:", got) - } - if decoded != number { - test.Fatalf("%d != %d", decoded, number) - } - } -} - -func TestGBEU(test *testing.T) { - var buffer = [16]byte { - 255, 255, 255, 255, - 255, 255, 255, 255, - 255, 255, 255, 255, - 255, 255, 255, 255, - } - _, err := EncodeGBEU[uint64](buffer[:0], 5) - if err == nil { test.Fatal("no error") } - if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } - _, err = EncodeGBEU[uint64](buffer[:2], 5555555) - if err == nil { test.Fatal("no error") } - if err.Error() != "encoding GBEU: wrong buffer length" { test.Fatal(err) } - _, _, err = DecodeGBEU[uint64](buffer[:0]) - if err == nil { test.Fatal("no error") } - if err.Error() != "decoding GBEU: GBEU not terminated" { test.Fatal(err) } - - n, err := EncodeGBEU[uint64](buffer[:], 0x97) - if err != nil { test.Fatal(err) } - if correct, got := 2, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := []byte { 0x81, 0x17 }, buffer[:2]; !slices.Equal(correct, got) { - message := "not equal:" - for _, item := range got { - message = fmt.Sprintf("%s %x", message, item) - } - test.Fatal(message) - } - decoded, n, err := DecodeGBEU[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - if correct, got := 2, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := uint64(0x97), decoded; correct != got { - test.Fatalf("not equal: %x", got) - } - - n, err = EncodeGBEU[uint64](buffer[:], 0x123456) - if err != nil { test.Fatal(err) } - if correct, got := 3, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := []byte { 0xc8, 0xe8, 0x56 }, buffer[:3]; !slices.Equal(correct, got) { - message := "not equal:" - for _, item := range got { - message = fmt.Sprintf("%s %x", message, item) - } - test.Fatal(message) - } - decoded, n, err = DecodeGBEU[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - if correct, got := 3, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := uint64(0x123456), decoded; correct != got { - test.Fatalf("not equal: %x", got) - } - - maxGBEU64 := []byte { 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F } - n, err = EncodeGBEU[uint64](buffer[:], 0xFFFFFFFFFFFFFFFF) - if err != nil { test.Fatal(err) } - if correct, got := 10, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := maxGBEU64, buffer[:10]; !slices.Equal(correct, got) { - message := "not equal:" - for _, item := range got { - message = fmt.Sprintf("%s %x", message, item) - } - test.Fatal(message) - } - decoded, n, err = DecodeGBEU[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - if correct, got := 10, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := uint64(0xFFFFFFFFFFFFFFFF), decoded; correct != got { - test.Fatalf("not equal: %x", got) - } - - n, err = EncodeGBEU[uint64](buffer[:], 11) - if err != nil { test.Fatal(err) } - if correct, got := 1, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := byte(0xb), buffer[0]; correct != got { - test.Fatal("not equal:", got) - } - decoded, n, err = DecodeGBEU[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - if correct, got := 1, n; correct != got { - test.Fatal("not equal:", got) - } - if correct, got := uint64(0xb), decoded; correct != got { - test.Fatalf("not equal: %x", got) - } - - for _ = range largeNumberNTestRounds { - buffer = [16]byte { } - number := uint64(rand.Int()) - _, err := EncodeGBEU[uint64](buffer[:], number) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeGBEU[uint64](buffer[:]) - if err != nil { test.Fatal(err) } - if decoded != number { - test.Error("not equal:") - test.Errorf("%d != %d", decoded, number) - test.Errorf("%x != %x", decoded, number) - test.Fatal(buffer) - } - } -} - -func TestGBEUSize(test *testing.T) { - if correct, got := 3, GBEUSize(uint(0x100000)); correct != got { - test.Fatal("not equal:", got) - } - if correct, got := 3, GBEUSize(uint(0x123456)); correct != got { - test.Fatal("not equal:", got) - } - if correct, got := 4, GBEUSize(uint(0x223456)); correct != got { - test.Fatal("not equal:", got) - } - if correct, got := 1, GBEUSize(uint(0)); correct != got { - test.Fatal("not equal:", got) - } - if correct, got := 1, GBEUSize(uint(127)); correct != got { - test.Fatal("not equal:", got) - } -} - -func TestString(test *testing.T) { - var buffer [16]byte - _, err := EncodeString[string](buffer[:], "hello") - if !errIs(err, ErrWrongBufferLength, "encoding string: wrong buffer length") { test.Fatal(err) } - _, err = EncodeString[string](buffer[:0], "hello") - if !errIs(err, ErrWrongBufferLength, "encoding string: wrong buffer length") { test.Fatal(err) } - _, _, err = DecodeString[string](buffer[:]) - if err != nil { test.Fatal(err) } - _, _, err = DecodeString[string](buffer[:0]) - if err != nil { test.Fatal(err) } - - for _ = range largeNumberNTestRounds { - length := rand.Intn(16) - str := randString(length) - _, err := EncodeString[string](buffer[:length], str) - if err != nil { test.Fatal(err) } - decoded, _, err := DecodeString[string](buffer[:length]) - if err != nil { test.Fatal(err) } - if decoded != str { - test.Fatalf("%s != %s", decoded, str) - } - } -} - -func TestU16CastSafe(test *testing.T) { - number, ok := U16CastSafe(90_000) - if ok { test.Fatalf("false positive: %v, %v", number, ok) } - number, ok = U16CastSafe(-478) - if ok { test.Fatalf("false positive: %v, %v", number, ok) } - number, ok = U16CastSafe(3870) - if !ok { test.Fatalf("false negative: %v, %v", number, ok) } - if got, correct := number, uint16(3870); got != correct { - test.Fatalf("not equal: %v %v", got, correct) - } - number, ok = U16CastSafe(0) - if !ok { test.Fatalf("false negative: %v, %v", number, ok) } - if got, correct := number, uint16(0); got != correct { - test.Fatalf("not equal: %v %v", got, correct) - } -} - -func randString(length int) string { - buffer := make([]byte, length) - for index := range buffer { - buffer[index] = randStringBytes[rand.Intn(len(randStringBytes))] - } - return string(buffer) -} - -func randInts[T interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 }] (length int) []T { - buffer := make([]T, length) - for index := range buffer { - buffer[index] = T(rand.Int()) - } - return buffer -} - -func randStrings[T interface { ~string }] (length, maxItemLength int) []T { - buffer := make([]T, length) - for index := range buffer { - buffer[index] = T(randString(rand.Intn(maxItemLength))) - } - return buffer -} - -func errIs(err error, wraps error, description string) bool { - return err != nil && (wraps == nil || errors.Is(err, wraps)) && err.Error() == description -} -- 2.46.1 From b261aa32f1479cde5d8bc0dabce89997cec8313f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 10:36:01 -0400 Subject: [PATCH 067/132] design: Initial PDL design --- design/pdl.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 design/pdl.md diff --git a/design/pdl.md b/design/pdl.md new file mode 100644 index 0000000..06075e6 --- /dev/null +++ b/design/pdl.md @@ -0,0 +1,83 @@ +# PDL Language Definition + +PDL allows defining a protocol using HOPP and TAPE. + +## Data Types + +| Syntax | TN | CN | Description +| -------- | ------- | -: | ----------- +| I5 | SI | | +| I8 | LI | 0 | +| I16 | LI | 1 | +| I32 | LI | 3 | +| I64 | LI | 7 | +| I128[^2] | LI | 15 | +| I256[^2] | LI | 31 | +| U5 | SI | | +| U8 | LI | 0 | +| U16 | LI | 1 | +| U32 | LI | 3 | +| U64 | LI | 7 | +| U128[^2] | LI | 15 | +| U256[^2] | LI | 31 | +| F16 | FP | 1 | +| F32 | FP | 3 | +| F64 | FP | 7 | +| F128[^2] | FP | 15 | +| F256[^2] | FP | 31 | +| String | SBA/LBA | * | UTF-8 string +| Buffer | SBA/LBA | * | Byte array +| [] | OTA | * | Array of any type[^1] +| Table | KTV | * | + +[^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and +Buffer are simply forced to use their "long" variant. + +[^2]: Some systems may lack support for this. + +## Tokens + +| Name | Syntax | +| -------- | --------------------- | +| Method | `M[0-9A-Fa-f]{4}` | +| Key | `[0-9A-Fa-f]{4}` | +| Ident | `[A-Za-z][A-Za-z0-9]` | +| Comma | `,` | +| LBrace | `{` | +| RBrace | `}` | +| LBracket | `[` | +| RBracket | `]` | + +## Syntax + +Types are expressed with an Ident. A table can be used by either writing the +name of the type (Table), or by defining a schema with curly braces. Arrays must +be expressed using two matching square brackets before their element type. + +A table schema contains comma-separated fields in-between its braces. Each field +has three parts: the key number (Key), the field name (Ident), and the field +type. Tables, Arrays, etc. can be nested. + +Files directly contain messages and types, which start with a Method token and +an Ident token respectively. A message consists of the method code (Method), the +message name (Ident), and the message's root type. This is usually a table, but +can be anything. + +Here is an example of all that: + +``` +M0000 Connect { + 0000 Name String, + 0001 Password String, +} + +M0001 UserList { + 0000 Users []User, +} + +User { + 0000 Name String, + 0001 Bio String, + 0002 Followers U32, +} +``` -- 2.46.1 From d67a4fb9f2acb2749452075b20b817053728c44b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 10:37:03 -0400 Subject: [PATCH 068/132] generate: Clean slate --- generate/generate.go | 302 ------------------------------------------- generate/protocol.go | 244 ---------------------------------- 2 files changed, 546 deletions(-) delete mode 100644 generate/generate.go delete mode 100644 generate/protocol.go diff --git a/generate/generate.go b/generate/generate.go deleted file mode 100644 index 65cbe15..0000000 --- a/generate/generate.go +++ /dev/null @@ -1,302 +0,0 @@ -package generate - -import "io" -import "fmt" -import "bufio" -import "strings" - -const send = -`// Send sends one message along a transaction. -func Send(trans hopp.Trans, message hopp.Message) error { - buffer, err := message.MarshalBinary() - if err != nil { return err } - return trans.Send(message.Method(), buffer) -} - -` - -// ResolveType resolves a HOPP type name to a Go type. For now, it supports all -// data types defined in TAPE. -func (this *Protocol) ResolveType(hopp string) (string, error) { - switch hopp { - case "I8": return "int8", nil - case "I16": return "int16", nil - case "I32": return "int32", nil - case "I64": return "int64", nil - case "U8": return "uint8", nil - case "U16": return "uint16", nil - case "U32": return "uint32", nil - case "U64": return "uint64", nil - case "I8Array": return "[]int8", nil - case "I16Array": return "[]int16", nil - case "I32Array": return "[]int32", nil - case "I64Array": return "[]int64", nil - case "U8Array": return "[]uint8", nil - case "U16Array": return "[]uint16", nil - case "U32Array": return "[]uint32", nil - case "U64Array": return "[]uint64", nil - case "String": return "string", nil - case "StringArray": return "[]string", nil - default: return "", fmt.Errorf("unknown type: %s", hopp) - } -} - -// Generate turns this protocol into code. The package name for the generated -// code must be specified. -func (this *Protocol) Generate(writer io.Writer, packag string) error { - out := bufio.NewWriter(writer) - defer out.Flush() - - fmt.Fprintf(out, "package %s\n\n", packag) - fmt.Fprintf(out, "import \"git.tebibyte.media/sashakoshka/hopp\"\n") - fmt.Fprintf(out, "import \"git.tebibyte.media/sashakoshka/hopp/tape\"\n\n") - - fmt.Fprintf(out, send) - this.receive(out) - - for _, message := range this.Messages { - err := this.defineMessage(out, message) - if err != nil { return err } - err = this.marshalMessage(out, message) - if err != nil { return err } - err = this.unmarshalMessage(out, message) - if err != nil { return err } - } - - return nil -} - -func (this *Protocol) receive(out io.Writer) error { - fmt.Fprintf(out, "// Receive receives one message from a transaction.\n") - fmt.Fprintf(out, "func Receive(trans hopp.Trans) (hopp.Message, error) {\n") - fmt.Fprintf(out, "\tmethod, data, err := trans.Receive()\n") - fmt.Fprintf(out, "\tif err != nil { return nil, err }\n") - fmt.Fprintf(out, "\tswitch method {\n") - for _, message := range this.Messages { - fmt.Fprintf(out, "\tcase 0x%04X:\n", message.Method) - fmt.Fprintf(out, "\t\tmessage := &Message%s { }\n", message.Name) - fmt.Fprintf(out, "\t\terr := message.UnmarshalBinary(data)\n") - fmt.Fprintf(out, "\t\tif err != nil { return nil, err }\n") - fmt.Fprintf(out, "\t\treturn message, nil\n") - } - fmt.Fprintf(out, "\tdefault: return nil, hopp.ErrUnknownMethod\n") - fmt.Fprintf(out, "\t}\n") - fmt.Fprintf(out, "}\n\n") - return nil -} - -func (this *Protocol) defineMessage(out io.Writer, message Message) error { - fmt.Fprintln(out, comment("//", fmt.Sprintf("(%d) %s\n", message.Method, message.Doc))) - fmt.Fprintf(out, "type Message%s struct {\n", message.Name) - for _, field := range message.Fields { - typ, err := this.ResolveType(field.Type) - if err != nil { return err } - if field.Doc != "" { - fmt.Fprintf(out, "\t%s\n", comment("\t//", field.Doc)) - } - if field.Optional { - typ = fmt.Sprintf("hopp.Option[%s]", typ) - } - fmt.Fprintf( - out, "\t/* %d */ %s %s\n", - field.Tag, field.Name, typ) - } - fmt.Fprintf(out, "}\n\n") - - fmt.Fprintf(out, "// Method returns the method number of the message.\n") - fmt.Fprintf(out, "func (msg Message%s) Method() uint16 {\n", message.Name) - fmt.Fprintf(out, "\treturn %d\n", message.Method) - fmt.Fprintf(out, "}\n\n") - return nil -} - -func (this *Protocol) marshalMessage(out io.Writer, message Message) error { - fmt.Fprintf(out, "// MarshalBinary encodes the data in this message into a buffer.\n") - fmt.Fprintf(out, "func (msg *Message%s) MarshalBinary() ([]byte, error) {\n", message.Name) - requiredCount := 0 - for _, field := range message.Fields { - if !field.Optional { requiredCount ++ } - } - fmt.Fprintf(out, "\tsize := 0\n") - fmt.Fprintf(out, "\tcount := %d\n", requiredCount) - for _, field := range message.Fields { - fmt.Fprintf(out, "\toffset%s := size\n", field.Name) - if field.Optional { - fmt.Fprintf(out, "\tif value, ok := msg.%s.Get(); ok {\n", field.Name) - fmt.Fprintf(out, "\t\tcount ++\n") - fmt.Fprintf(out, "\t\t") - err := this.marshalSizeOf(out, field) - if err != nil { return err } - fmt.Fprintf(out, " }\n") - } else { - fmt.Fprintf(out, "\t{") - fmt.Fprintf(out, "\tvalue := msg.%s\n", field.Name) - fmt.Fprintf(out, "\t\t") - err := this.marshalSizeOf(out, field) - if err != nil { return err } - fmt.Fprintf(out, " }\n") - } - } - fmt.Fprintf(out, "\tif size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}\n") - fmt.Fprintf(out, "\tif count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}\n") - fmt.Fprintf(out, "\tbuffer := make([]byte, 2 + 4 * count + size)\n") - fmt.Fprintf(out, "\ttape.EncodeI16(buffer[:2], uint16(count))\n") - for _, field := range message.Fields { - if field.Optional { - fmt.Fprintf(out, "\tif value, ok := msg.%s.Get(); ok {\n", field.Name) - fmt.Fprintf(out, "\t\t") - err := this.marshalField(out, field) - if err != nil { return err } - fmt.Fprintf(out, "}\n") - } else { - fmt.Fprintf(out, "\t{") - fmt.Fprintf(out, "\tvalue := msg.%s\n", field.Name) - fmt.Fprintf(out, "\t\t") - err := this.marshalField(out, field) - if err != nil { return err } - fmt.Fprintf(out, "}\n") - } - } - fmt.Fprintf(out, "\treturn buffer, nil\n") - fmt.Fprintf(out, "}\n\n") - return nil -} - -func (this *Protocol) marshalSizeOf(out io.Writer, field Field) error { - switch field.Type { - case "I8": fmt.Fprintf(out, "size += 1; _ = value") - case "I16": fmt.Fprintf(out, "size += 2; _ = value") - case "I32": fmt.Fprintf(out, "size += 4; _ = value") - case "I64": fmt.Fprintf(out, "size += 8; _ = value") - case "U8": fmt.Fprintf(out, "size += 1; _ = value") - case "U16": fmt.Fprintf(out, "size += 2; _ = value") - case "U32": fmt.Fprintf(out, "size += 4; _ = value") - case "U64": fmt.Fprintf(out, "size += 8; _ = value") - case "I8Array": fmt.Fprintf(out, "size += len(value)") - case "I16Array": fmt.Fprintf(out, "size += len(value) * 2") - case "I32Array": fmt.Fprintf(out, "size += len(value) * 4") - case "I64Array": fmt.Fprintf(out, "size += len(value) * 8") - case "U8Array": fmt.Fprintf(out, "size += len(value)") - case "U16Array": fmt.Fprintf(out, "size += len(value) * 2") - case "U32Array": fmt.Fprintf(out, "size += len(value) * 4") - case "U64Array": fmt.Fprintf(out, "size += len(value) * 8") - case "String": fmt.Fprintf(out, "size += len(value)") - case "StringArray": - fmt.Fprintf( - out, - "for _, el := range value { size += 2 + len(el) }") - default: - return fmt.Errorf("unknown type: %s", field.Type) - } - return nil -} - -func (this *Protocol) marshalField(out io.Writer, field Field) error { - switch field.Type { - case "I8": fmt.Fprintf(out, "tape.EncodeI8(buffer[offset%s:], value)", field.Name) - case "I16": fmt.Fprintf(out, "tape.EncodeI16(buffer[offset%s:], value)", field.Name) - case "I32": fmt.Fprintf(out, "tape.EncodeI32(buffer[offset%s:], value)", field.Name) - case "I64": fmt.Fprintf(out, "tape.EncodeI64(buffer[offset%s:], value)", field.Name) - case "U8": fmt.Fprintf(out, "tape.EncodeI8(buffer[offset%s:], value)", field.Name) - case "U16": fmt.Fprintf(out, "tape.EncodeI16(buffer[offset%s:], value)", field.Name) - case "U32": fmt.Fprintf(out, "tape.EncodeI32(buffer[offset%s:], value)", field.Name) - case "U64": fmt.Fprintf(out, "tape.EncodeI64(buffer[offset%s:], value)", field.Name) - case "I8Array": fmt.Fprintf(out, "tape.EncodeI8Array(buffer[offset%s:], value)", field.Name) - case "I16Array": fmt.Fprintf(out, "tape.EncodeI16Array(buffer[offset%s:], value)", field.Name) - case "I32Array": fmt.Fprintf(out, "tape.EncodeI32Array(buffer[offset%s:], value)", field.Name) - case "I64Array": fmt.Fprintf(out, "tape.EncodeI64Array(buffer[offset%s:], value)", field.Name) - case "U8Array": fmt.Fprintf(out, "tape.EncodeI8Array(buffer[offset%s:], value)", field.Name) - case "U16Array": fmt.Fprintf(out, "tape.EncodeI16Array(buffer[offset%s:], value)", field.Name) - case "U32Array": fmt.Fprintf(out, "tape.EncodeI32Array(buffer[offset%s:], value)", field.Name) - case "U64Array": fmt.Fprintf(out, "tape.EncodeI64Array(buffer[offset%s:], value)", field.Name) - case "String": fmt.Fprintf(out, "tape.EncodeString(buffer[offset%s:], value)", field.Name) - case "StringArray": fmt.Fprintf(out, "tape.EncodeStringArray(buffer[offset%s:], value)", field.Name) - default: - return fmt.Errorf("unknown type: %s", field.Type) - } - return nil -} - -func (this *Protocol) unmarshalMessage(out io.Writer, message Message) error { - fmt.Fprintf(out, "// UnmarshalBinary dencodes the data from a buffer int this message.\n") - fmt.Fprintf(out, - "func (msg *Message%s) UnmarshalBinary(buffer []byte) error {\n", - message.Name) - if len(message.Fields) < 1 { - fmt.Fprintf(out, "\t// no fields\n") - fmt.Fprintf(out, "\treturn nil\n") - fmt.Fprintf(out, "}\n\n") - return nil - } - fmt.Fprintf(out, "\tpairs, err := tape.DecodePairs(buffer)\n") - fmt.Fprintf(out, "\tif err != nil { return err }\n") - requiredTotal := 0 - for _, field := range message.Fields { - if field.Optional { - requiredTotal ++ - } - } - if requiredTotal > 0 { - fmt.Fprintf(out, "\tfoundRequired := 0\n") - } - fmt.Fprintf(out, "\tfor tag, data := range pairs {\n") - fmt.Fprintf(out, "\t\tswitch tag {\n") - for _, field := range message.Fields { - fmt.Fprintf(out, "\t\tcase %d:\n", field.Tag) - fmt.Fprintf(out, "\t\t\t") - err := this.unmarshalField(out, field) - if err != nil { return err } - fmt.Fprintf(out, "\n") - fmt.Fprintf(out, "\t\t\tif err != nil { return err }\n") - if field.Optional { - fmt.Fprintf(out, "\t\t\tmsg.%s = hopp.O(value)\n", field.Name) - } else { - fmt.Fprintf(out, "\t\t\tmsg.%s = value\n", field.Name) - if requiredTotal > 0 { - fmt.Fprintf(out, "\t\t\tfoundRequired ++\n") - } - } - } - fmt.Fprintf(out, "\t\t}\n") - fmt.Fprintf(out, "\t}\n") - if requiredTotal > 0 { - fmt.Fprintf(out, - "\tif foundRequired != %d { return hopp.ErrTablePairMissing }\n", - requiredTotal) - } - fmt.Fprintf(out, "\treturn nil\n") - fmt.Fprintf(out, "}\n\n") - return nil -} - -func (this *Protocol) unmarshalField(out io.Writer, field Field) error { - typ, err := this.ResolveType(field.Type) - if err != nil { return err } - switch field.Type { - case "I8": fmt.Fprintf(out, "value, err := tape.DecodeI8[%s](data)", typ) - case "I16": fmt.Fprintf(out, "value, err := tape.DecodeI16[%s](data)", typ) - case "I32": fmt.Fprintf(out, "value, err := tape.DecodeI32[%s](data)", typ) - case "I64": fmt.Fprintf(out, "value, err := tape.DecodeI64[%s](data)", typ) - case "U8": fmt.Fprintf(out, "value, err := tape.DecodeI8[%s](data)", typ) - case "U16": fmt.Fprintf(out, "value, err := tape.DecodeI16[%s](data)", typ) - case "U32": fmt.Fprintf(out, "value, err := tape.DecodeI32[%s](data)", typ) - case "U64": fmt.Fprintf(out, "value, err := tape.DecodeI64[%s](data)", typ) - case "I8Array": fmt.Fprintf(out, "value, err := tape.DecodeI8Array[%s](data)", typ) - case "I16Array": fmt.Fprintf(out, "value, err := tape.DecodeI16Array[%s](data)", typ) - case "I32Array": fmt.Fprintf(out, "value, err := tape.DecodeI32Array[%s](data)", typ) - case "I64Array": fmt.Fprintf(out, "value, err := tape.DecodeI64Array[%s](data)", typ) - case "U8Array": fmt.Fprintf(out, "value, err := tape.DecodeI8Array[%s](data)", typ) - case "U16Array": fmt.Fprintf(out, "value, err := tape.DecodeI16Array[%s](data)", typ) - case "U32Array": fmt.Fprintf(out, "value, err := tape.DecodeI32Array[%s](data)", typ) - case "U64Array": fmt.Fprintf(out, "value, err := tape.DecodeI64Array[%s](data)", typ) - case "String": fmt.Fprintf(out, "value, err := tape.DecodeString[%s](data)", typ) - case "StringArray": fmt.Fprintf(out, "value, err := tape.DecodeStringArray[%s](data)", typ) - default: - return fmt.Errorf("unknown type: %s", field.Type) - } - return nil -} - -func comment(prefix, text string) string { - return prefix + " " + strings.ReplaceAll(strings.TrimSpace(text), "\n", "\n" + prefix + " ") -} diff --git a/generate/protocol.go b/generate/protocol.go deleted file mode 100644 index 02fd456..0000000 --- a/generate/protocol.go +++ /dev/null @@ -1,244 +0,0 @@ -package generate - -import "io" -import "fmt" -import "errors" -import "strconv" -import "strings" -import "github.com/gomarkdown/markdown" -import "github.com/gomarkdown/markdown/ast" -import "github.com/gomarkdown/markdown/parser" - -// Protocol describes a protocol. -type Protocol struct { - Messages []Message -} - -// Message describes a protocol message. -type Message struct { - Doc string - Method uint16 - Name string - Fields []Field -} - -// Field describes a named value within a message. -type Field struct { - Doc string - Tag uint16 - Name string - Optional bool - Type string -} - -// ParseReader parses a protocol definition from a reader. -func ParseReader(reader io.Reader) (*Protocol, error) { - data, err := io.ReadAll(reader) - if err != nil { return nil, err } - protocol := new(Protocol) - err = protocol.UnmarshalText(data) - if err != nil { return nil, err } - return protocol, nil -} - -// UnmarshalText unmarshals markdown-formatted text data into the protocol. -func (this *Protocol) UnmarshalText(text []byte) error { - var state int; const ( - stateIdle = iota - stateMessage - stateMessageDoc - stateMessageField - ) - - var message *Message - addMessage := func(method uint16, name string) { - this.Messages = append(this.Messages, Message { - Method: method, - Name: name, - }) - message = &this.Messages[len(this.Messages) - 1] - } - - root := markdown.Parse(text, parser.New()) - for _, node := range root.GetChildren() { - if node, ok := node.(*ast.Heading); ok { - if node.Level == 2 { - if removeBreaks(flatten(node)) == "Messages" { - state = stateMessage - continue - } - } - - if node.Level > 3 { - state = stateIdle - continue - } - - if state != stateIdle && node.Level == 3 { - heading := removeBreaks(flatten(node)) - method, name, err := splitMessageHeading(heading) - if err != nil { return err } - addMessage(method, name) - state = stateMessageDoc - } - } - - if state == stateIdle { continue } - if message == nil { continue } - - // TODO when we are adding text content to the doc comment, it - // might be wise to do stuff like indent lists and quotes so - // that go doc renders them correctly - switch node := node.(type) { - case *ast.Paragraph: - if message.Doc != "" { message.Doc += "\n\n" } - message.Doc += removeBreaks(flatten(node)) - case *ast.BlockQuote: - if message.Doc != "" { message.Doc += "\n\n> " } - message.Doc += removeBreaks(flatten(node)) - case *ast.List: - // FIXME format the list - if message.Doc != "" { message.Doc += "\n\n" } - message.Doc += removeBreaks(flatten(node)) - case *ast.Table: - fields, err := processFieldTable(node) - if err != nil { return err} - message.Fields = append(message.Fields, fields...) - } - } - - return nil -} - -func processFieldTable(node *ast.Table) ([]Field, error) { - fields := []Field { } - children := node.GetChildren() - if len(children) != 2 { - return nil, errors.New("malformed field table") - } - - // get columns - columns := []string { } - if header, ok := children[0].(*ast.TableHeader); ok { - children := header.GetChildren() - if len(children) != 1 { - return nil, errors.New("malformed field table header") - } - if row, ok := header.Children[0].(*ast.TableRow); ok { - for _, cell := range row.GetChildren() { - if cell, ok := cell.(*ast.TableCell); ok { - columns = append(columns, flatten(cell)) - } - } - } else { - return nil, errors.New("malformed field table header") - } - for index, column := range columns { - columns[index] = strings.ToLower(column) - } - } else { - return nil, errors.New("malformed field table: no header") - } - - // get data - if body, ok := children[1].(*ast.TableBody); ok { - for _, node := range body.GetChildren() { - if row, ok := node.(*ast.TableRow); ok { - children := row.GetChildren() - if len(children) != len(columns) { - return nil, errors.New ( - "malformed field table row: wrong " + - "number of columns") - } - - field := Field { } - - for index, node := range children { - if cell, ok := node.(*ast.TableCell); ok { - text := flatten(cell) - switch columns[index] { - case "tag": - tag, err := parseTag(text) - if err != nil { return nil, err } - field.Tag = tag - case "name": - field.Name = text - case "required": - field.Optional = !parseBool(text) - case "optional": - field.Optional = parseBool(text) - case "type": - field.Type = text - case "comment", "purpose", "documentation": - field.Doc = text - } - }} - - fields = append(fields, field) - }} - } else { - return nil, errors.New("malformed field table: no body") - } - return fields, nil -} - -type nodeFlattener struct { - text string -} -func (this *nodeFlattener) String() string { return this.text } -func (this *nodeFlattener) Visit(node ast.Node, entering bool) ast.WalkStatus { - if entering { - if node := node.AsLeaf(); node != nil { - this.text += string(node.Literal) - } - } - return ast.GoToNext -} -func flatten(node ast.Node) string { - flattener := new(nodeFlattener) - ast.Walk(node, flattener) - return flattener.text -} - - -func removeBreaks(text string) string { - text = strings.ReplaceAll(text, "\n", " ") - text = strings.ReplaceAll(text, "\r", "") - return text -} - -func parseBool(text string) bool { - switch(strings.ToLower(text)) { - case "yes": return true - case "no": return false - case "true": return true - case "false": return false - } - return false -} - -func parseTag(text string) (uint16, error) { - tag, err := strconv.ParseUint(text, 10, 16) - if err != nil { - return 0, fmt.Errorf("malformed tag '%s': %w", text, err) - } - return uint16(tag), nil -} - -func splitMessageHeading(text string) (uint16, string, error) { - text = strings.TrimSpace(text) - methodText, name, ok := strings.Cut(text, " ") - if !ok { - return 0, "", fmt.Errorf( - "malformed message heading '%s': no message name", - text) - } - method, err := strconv.ParseUint(methodText, 16, 16) - if err != nil { - return 0, "", fmt.Errorf( - "malformed method number '%s': %w", - methodText, err) - } - name = strings.TrimSpace(name) - return uint16(method), name, nil -} -- 2.46.1 From 385c5a5972b35826c9a0fdaf7ee69e416a628428 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 10:38:42 -0400 Subject: [PATCH 069/132] design: Markdown fixes lol --- design/pdl.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/design/pdl.md b/design/pdl.md index 06075e6..416c72f 100644 --- a/design/pdl.md +++ b/design/pdl.md @@ -4,31 +4,31 @@ PDL allows defining a protocol using HOPP and TAPE. ## Data Types -| Syntax | TN | CN | Description -| -------- | ------- | -: | ----------- -| I5 | SI | | -| I8 | LI | 0 | -| I16 | LI | 1 | -| I32 | LI | 3 | -| I64 | LI | 7 | -| I128[^2] | LI | 15 | -| I256[^2] | LI | 31 | -| U5 | SI | | -| U8 | LI | 0 | -| U16 | LI | 1 | -| U32 | LI | 3 | -| U64 | LI | 7 | -| U128[^2] | LI | 15 | -| U256[^2] | LI | 31 | -| F16 | FP | 1 | -| F32 | FP | 3 | -| F64 | FP | 7 | -| F128[^2] | FP | 15 | -| F256[^2] | FP | 31 | -| String | SBA/LBA | * | UTF-8 string -| Buffer | SBA/LBA | * | Byte array -| [] | OTA | * | Array of any type[^1] -| Table | KTV | * | +| Syntax | TN | CN | Description +| ---------- | ------- | -: | ----------- +| I5 | SI | | +| I8 | LI | 0 | +| I16 | LI | 1 | +| I32 | LI | 3 | +| I64 | LI | 7 | +| I128[^2] | LI | 15 | +| I256[^2] | LI | 31 | +| U5 | SI | | +| U8 | LI | 0 | +| U16 | LI | 1 | +| U32 | LI | 3 | +| U64 | LI | 7 | +| U128[^2] | LI | 15 | +| U256[^2] | LI | 31 | +| F16 | FP | 1 | +| F32 | FP | 3 | +| F64 | FP | 7 | +| F128[^2] | FP | 15 | +| F256[^2] | FP | 31 | +| String | SBA/LBA | * | UTF-8 string +| Buffer | SBA/LBA | * | Byte array +| []\ | OTA | * | Array of any type[^1] +| Table | KTV | * | [^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and Buffer are simply forced to use their "long" variant. -- 2.46.1 From 1f62f6d973e656e53c6e5cf4e50532deb312b4eb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 10:40:14 -0400 Subject: [PATCH 070/132] design: PDL Idents should always start with capitals --- design/pdl.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/design/pdl.md b/design/pdl.md index 416c72f..6c5d625 100644 --- a/design/pdl.md +++ b/design/pdl.md @@ -37,16 +37,16 @@ Buffer are simply forced to use their "long" variant. ## Tokens -| Name | Syntax | -| -------- | --------------------- | -| Method | `M[0-9A-Fa-f]{4}` | -| Key | `[0-9A-Fa-f]{4}` | -| Ident | `[A-Za-z][A-Za-z0-9]` | -| Comma | `,` | -| LBrace | `{` | -| RBrace | `}` | -| LBracket | `[` | -| RBracket | `]` | +| Name | Syntax | +| -------- | ------------------ | +| Method | `M[0-9A-Fa-f]{4}` | +| Key | `[0-9A-Fa-f]{4}` | +| Ident | `[A-Z][A-Za-z0-9]` | +| Comma | `,` | +| LBrace | `{` | +| RBrace | `}` | +| LBracket | `[` | +| RBracket | `]` | ## Syntax -- 2.46.1 From ec965caa27478ba195149bb8886b8f27522d5bdd Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 12:20:04 -0400 Subject: [PATCH 071/132] codec: Add Encodable, Decodable interfaces --- codec/decode.go | 8 ++++++++ codec/encode.go | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/codec/decode.go b/codec/decode.go index e665e99..1c27312 100644 --- a/codec/decode.go +++ b/codec/decode.go @@ -2,6 +2,14 @@ package codec import "io" +// Decodable is any type that can decode itself from a decoder. +type Decodable interface { + // Decode reads data from decoder, replacing the data of the object. It + // returns the amount of bytes written, and an error if the write + // stopped early. + Decode(decoder *Decoder) (n int, err error) +} + // Decoder wraps an [io.Reader] and decodes data from it. type Decoder struct { io.Reader diff --git a/codec/encode.go b/codec/encode.go index bbfbd6b..2c96bba 100644 --- a/codec/encode.go +++ b/codec/encode.go @@ -2,6 +2,13 @@ package codec import "io" +// Encodable is any type that can write itself to an encoder. +type Encodable interface { + // Encode sends data to encoder. It returns the amount of bytes written, + // and an error if the write stopped early. + Encode(encoder *Encoder) (n int, err error) +} + // Encoder wraps an [io.Writer] and encodes data to it. type Encoder struct { io.Writer -- 2.46.1 From 44b174c161adc94c24eb4046a3f0a188db768541 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 12:46:20 -0400 Subject: [PATCH 072/132] design: Flesh out PDL document --- design/pdl.md | 126 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 11 deletions(-) diff --git a/design/pdl.md b/design/pdl.md index 6c5d625..b39da72 100644 --- a/design/pdl.md +++ b/design/pdl.md @@ -28,7 +28,8 @@ PDL allows defining a protocol using HOPP and TAPE. | String | SBA/LBA | * | UTF-8 string | Buffer | SBA/LBA | * | Byte array | []\ | OTA | * | Array of any type[^1] -| Table | KTV | * | +| Table | KTV | * | Table with undefined schema +| {...} | KTV | * | Table with defined schema [^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and Buffer are simply forced to use their "long" variant. @@ -37,19 +38,25 @@ Buffer are simply forced to use their "long" variant. ## Tokens -| Name | Syntax | -| -------- | ------------------ | -| Method | `M[0-9A-Fa-f]{4}` | -| Key | `[0-9A-Fa-f]{4}` | -| Ident | `[A-Z][A-Za-z0-9]` | -| Comma | `,` | -| LBrace | `{` | -| RBrace | `}` | -| LBracket | `[` | -| RBracket | `]` | +PDL files are divided into tokens, which assemble together into larger language +structures. They are separated by whitespace. + +| Name | Syntax | Description +| -------- | ------------------ | ----------- +| Magic | `PDL/0` | Must appear at the very start of the file. +| Method | `M[0-9A-Fa-f]{4}` | A 16-bit hexadecimal method code. +| Key | `[0-9A-Fa-f]{4}` | A 16-bit hexadecimal table key. +| Ident | `[A-Z][A-Za-z0-9]` | An identifier. +| Comma | `,` | A comma separator. +| LBrace | `{` | A left curly brace. +| RBrace | `}` | A right curly brace. +| LBracket | `[` | A left square bracket. +| RBracket | `]` | A right square bracket. ## Syntax +All files must begin with a Magic token. + Types are expressed with an Ident. A table can be used by either writing the name of the type (Table), or by defining a schema with curly braces. Arrays must be expressed using two matching square brackets before their element type. @@ -66,6 +73,8 @@ can be anything. Here is an example of all that: ``` +PDL/0 + M0000 Connect { 0000 Name String, 0001 Password String, @@ -81,3 +90,98 @@ User { 0002 Followers U32, } ``` + +## EBNF Description + +Below is an EBNF description of the language. + +``` + -> ( | -> "PDL/0" + -> /M[0-9A-Fa-f]{4}/ + -> /[0-9A-Fa-f]{4}/ + -> /[A-Z][A-Za-z0-9]/ + -> + -> + | "[" "]" + | "{" ( ",")* ? "}" + -> + -> +``` + +## Go Code Generation + +Given one or more PDL files representing a protocol, the compiler shall generate +a Go package named "protocol", which shall contain types for message and type +definitions, as well as encoding and decoding methods. + +### Static Section + +The compiler shall write a static section alongside the generated code. It +shall contain this text: + +```go +// Table is a KTV table with an undefined schema. +type Table map[uint16] any + +// Message is any message that can be sent along this protocol. +type Message interface { + codec.Encodable + codec.Decodable + + // Method returns the method code of the message. + Method() uint16 +} +``` + +### Preamble + +At the start of each file but after the package name, the compiler shall emit +this text: + +```go +/* # Do not edit this package by hand! + * + * This file was automatically generated by the Holanet PDL compiler. The + * source file is located at + * Please edit that file instead, and re-compile it to this location. + * + * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + */ +``` + +Where `` is the path of the protocol definition file relative to the +generated file. + +### Message Definitions + +For each defined message, the compiler shall generate a Go type named +`MessageName`, where `Name` is the name of the message as written in its +definition. The message shall be encodable, and shall have `Encode` and `Decode` +methods as described below. + +All messages shall satisfy a `Message` interface, which is defined in the +static section. + +### Type Definitions + +For each defined type, the compiler shall generate a Go type with the same name +as written in its definition. The Go type shall be encodable, and shall have +`Encode` and `Decode` methods as described below. + +### Encoding and Decoding Methods + +Each encodable type shall be given an `Encode` method and a `Decode` method, +which will take in a `codec.Encoder` and a `codec.Decoder` respectively. Both +return an `(int, error)` pair describing the amount of bytes written and an +error if the write stopped early. `Encode` will encode the data within the +message to the given encoder, and `Decode` will decode data from the given +decoder and place it in the type's value. The methods shall not retain or close +any encoders or decoders they are given. Both methods shall have pointer +receivers. In effect, these methods will satisfy `codec.Encodable` and +`codec.Decodable`. + +### Connection + +The compiler shall generate a `Conn` struct which embeds a `hopp.Conn`, which +is the real "porcelain" of the generated code. Any -- 2.46.1 From 64eea2b5faa92773fcf10fa0325cc367da92880b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 13:01:34 -0400 Subject: [PATCH 073/132] design: Separate PDL language and compiler documents --- design/pdl-compiler.md | 102 +++++++++++++++++++++++++++++++++++++++++ design/pdl.md | 77 ------------------------------- 2 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 design/pdl-compiler.md diff --git a/design/pdl-compiler.md b/design/pdl-compiler.md new file mode 100644 index 0000000..364961d --- /dev/null +++ b/design/pdl-compiler.md @@ -0,0 +1,102 @@ +# PDL Compiler Specification + +Given one or more PDL files representing a protocol, the compiler shall generate +a Go package named "protocol", which shall contain types for message and type +definitions, as well as encoding and decoding methods. + +## Static Section + +The compiler shall write a static section alongside the generated code. It +shall contain this text: + +```go +// Table is a KTV table with an undefined schema. +type Table map[uint16] any + +// Message is any message that can be sent along this protocol. +type Message interface { + codec.Encodable + codec.Decodable + + // Method returns the method code of the message. + Method() uint16 +} +``` + +## Preamble + +At the start of each file but after the package name, the compiler shall emit +this text: + +```go +/* # Do not edit this package by hand! + * + * This file was automatically generated by the Holanet PDL compiler. The + * source file is located at + * Please edit that file instead, and re-compile it to this location. + * + * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + */ +``` + +Where `` is the path of the protocol definition file relative to the +generated file. + +## Message Definitions + +For each defined message, the compiler shall generate a Go type named +`MessageName`, where `Name` is the name of the message as written in its +definition. The message shall be encodable, and shall have `Encode` and `Decode` +methods as described below. + +All messages shall satisfy a `Message` interface, which is defined in the +static section. + +## Type Definitions + +For each defined type, the compiler shall generate a Go type with the same name +as written in its definition. The Go type shall be encodable, and shall have +`Encode` and `Decode` methods as described below. + +## Encoding and Decoding Methods + +Each encodable type shall be given an `Encode` method and a `Decode` method, +which will take in a `codec.Encoder` and a `codec.Decoder` respectively. Both +return an `(int, error)` pair describing the amount of bytes written and an +error if the write stopped early. `Encode` will encode the data within the +message to the given encoder, and `Decode` will decode data from the given +decoder and place it in the type's value. The methods shall not retain or close +any encoders or decoders they are given. Both methods shall have pointer +receivers. In effect, these methods will satisfy `codec.Encodable` and +`codec.Decodable`. + +## Connection + +The compiler shall generate a `Conn` struct which embeds a `hopp.Conn`, which +is the real "porcelain" of the generated code. It shall provide methods to +create and accept transactions. Each transaction shall be a struct which embeds +a `hopp.Trans`, and shall have methods for sending and receiving messages. + +### Sending + +To send a message along a transaction, the program shall: + +1. Obtain the method code from the message +3. Obtain a writer from the connection using the method code +4. Wrap the writer in a `codec.Encoder` +5. Use the encoder to encode the message +6. Close the writer + +### Receiving + +To receiving a message from a transaction, the program shall: + +1. Obtain a method code and reader from the connection +2. Wrap the reader in a `codec.Decoder` +3. Switch on the method code, and decode the correct message using the decoder +4. Return the message to the caller as a value + +The recieve function must return the message as a value instead of a pointer in +order to avoid making an allocation. Because of this, the return value must be +`any` instead of `Message`. The caller must then use a type switch to figure out +what message was sent. diff --git a/design/pdl.md b/design/pdl.md index b39da72..dfbc6cf 100644 --- a/design/pdl.md +++ b/design/pdl.md @@ -108,80 +108,3 @@ Below is an EBNF description of the language. -> -> ``` - -## Go Code Generation - -Given one or more PDL files representing a protocol, the compiler shall generate -a Go package named "protocol", which shall contain types for message and type -definitions, as well as encoding and decoding methods. - -### Static Section - -The compiler shall write a static section alongside the generated code. It -shall contain this text: - -```go -// Table is a KTV table with an undefined schema. -type Table map[uint16] any - -// Message is any message that can be sent along this protocol. -type Message interface { - codec.Encodable - codec.Decodable - - // Method returns the method code of the message. - Method() uint16 -} -``` - -### Preamble - -At the start of each file but after the package name, the compiler shall emit -this text: - -```go -/* # Do not edit this package by hand! - * - * This file was automatically generated by the Holanet PDL compiler. The - * source file is located at - * Please edit that file instead, and re-compile it to this location. - * - * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz - */ -``` - -Where `` is the path of the protocol definition file relative to the -generated file. - -### Message Definitions - -For each defined message, the compiler shall generate a Go type named -`MessageName`, where `Name` is the name of the message as written in its -definition. The message shall be encodable, and shall have `Encode` and `Decode` -methods as described below. - -All messages shall satisfy a `Message` interface, which is defined in the -static section. - -### Type Definitions - -For each defined type, the compiler shall generate a Go type with the same name -as written in its definition. The Go type shall be encodable, and shall have -`Encode` and `Decode` methods as described below. - -### Encoding and Decoding Methods - -Each encodable type shall be given an `Encode` method and a `Decode` method, -which will take in a `codec.Encoder` and a `codec.Decoder` respectively. Both -return an `(int, error)` pair describing the amount of bytes written and an -error if the write stopped early. `Encode` will encode the data within the -message to the given encoder, and `Decode` will decode data from the given -decoder and place it in the type's value. The methods shall not retain or close -any encoders or decoders they are given. Both methods shall have pointer -receivers. In effect, these methods will satisfy `codec.Encodable` and -`codec.Decodable`. - -### Connection - -The compiler shall generate a `Conn` struct which embeds a `hopp.Conn`, which -is the real "porcelain" of the generated code. Any -- 2.46.1 From ee94e87a6a1822f722b2dafb7c3da6d8fffa39c3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 13:01:51 -0400 Subject: [PATCH 074/132] design: EBNF correction --- design/pdl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/pdl.md b/design/pdl.md index dfbc6cf..74745da 100644 --- a/design/pdl.md +++ b/design/pdl.md @@ -104,7 +104,7 @@ Below is an EBNF description of the language. -> -> | "[" "]" - | "{" ( ",")* ? "}" + | "{" ( ",")* [] "}" -> -> ``` -- 2.46.1 From 0ed55bcbc29a914864aaa53ebcbb4e20ed13c1e4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 4 Jun 2025 13:02:55 -0400 Subject: [PATCH 075/132] design: Rename pdl.md to pdl-language.md --- design/{pdl.md => pdl-language.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename design/{pdl.md => pdl-language.md} (100%) diff --git a/design/pdl.md b/design/pdl-language.md similarity index 100% rename from design/pdl.md rename to design/pdl-language.md -- 2.46.1 From bb5fc89cc5114263de851607b238ab2ac1e50c96 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 5 Jun 2025 20:28:23 -0400 Subject: [PATCH 076/132] design: Remove requirement for magic bytes in PDL file --- design/pdl-language.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/design/pdl-language.md b/design/pdl-language.md index 74745da..2eb9754 100644 --- a/design/pdl-language.md +++ b/design/pdl-language.md @@ -43,7 +43,6 @@ structures. They are separated by whitespace. | Name | Syntax | Description | -------- | ------------------ | ----------- -| Magic | `PDL/0` | Must appear at the very start of the file. | Method | `M[0-9A-Fa-f]{4}` | A 16-bit hexadecimal method code. | Key | `[0-9A-Fa-f]{4}` | A 16-bit hexadecimal table key. | Ident | `[A-Z][A-Za-z0-9]` | An identifier. @@ -55,8 +54,6 @@ structures. They are separated by whitespace. ## Syntax -All files must begin with a Magic token. - Types are expressed with an Ident. A table can be used by either writing the name of the type (Table), or by defining a schema with curly braces. Arrays must be expressed using two matching square brackets before their element type. @@ -73,8 +70,6 @@ can be anything. Here is an example of all that: ``` -PDL/0 - M0000 Connect { 0000 Name String, 0001 Password String, @@ -96,8 +91,7 @@ User { Below is an EBNF description of the language. ``` - -> ( | -> "PDL/0" + -> ( | -> /M[0-9A-Fa-f]{4}/ -> /[0-9A-Fa-f]{4}/ -> /[A-Z][A-Za-z0-9]/ -- 2.46.1 From 127aa23a61939f0e097711d5f780a7de4f66b942 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 5 Jun 2025 22:06:22 -0400 Subject: [PATCH 077/132] generate: Add PDL lexer --- generate/lex.go | 230 +++++++++++++++++++++++++++++++++++++++++++ generate/lex_test.go | 54 ++++++++++ 2 files changed, 284 insertions(+) create mode 100644 generate/lex.go create mode 100644 generate/lex_test.go diff --git a/generate/lex.go b/generate/lex.go new file mode 100644 index 0000000..0d9aaf8 --- /dev/null +++ b/generate/lex.go @@ -0,0 +1,230 @@ +package generate + +import "io" +import "bufio" +import "unicode" +import "unicode/utf8" +import "git.tebibyte.media/sashakoshka/goparse" + +const ( + TokenMethod parse.TokenKind = iota + TokenKey + TokenIdent + TokenComma + TokenLBrace + TokenRBrace + TokenLBracket + TokenRBracket +) + +var tokenNames = map[parse.TokenKind] string { + TokenMethod: "Method", + TokenKey: "Key", + TokenIdent: "Ident", + TokenComma: "Comma", + TokenLBrace: "LBrace", + TokenRBrace: "RBrace", + TokenLBracket: "LBracket", + TokenRBracket: "RBracket", +} + +func Lex(fileName string, reader io.Reader) (parse.Lexer, error) { + lex := &lexer { + fileName: fileName, + lineScanner: bufio.NewScanner(reader), + } + lex.nextRune() + return lex, nil +} + +type lexer struct { + fileName string + lineScanner *bufio.Scanner + rune rune + line string + lineFood string + + offset int + row int + column int + + eof bool + +} + +func (this *lexer) Next() (parse.Token, error) { + token, err := this.nextInternal() + if err == io.EOF { err = this.errUnexpectedEOF() } + return token, err +} + +func (this *lexer) nextInternal() (token parse.Token, err error) { + err = this.skipWhitespace() + token.Position = this.pos() + if this.eof { + token.Kind = parse.EOF + err = nil + return + } + if err != nil { return } + + appendRune := func () { + token.Value += string(this.rune) + err = this.nextRune() + } + + doNumber := func () { + for isDigit(this.rune) { + appendRune() + if this.eof { err = nil; return } + if err != nil { return } + } + } + + defer func () { + newPos := this.pos() + newPos.End -- // TODO figure out why tf we have to do this + token.Position = token.Position.Union(newPos) + } () + + switch { + // Method + case this.rune == 'M': + token.Kind = TokenMethod + err = this.nextRune() + if err != nil { return } + doNumber() + if this.eof { err = nil; return } + // Key + case isDigit(this.rune): + token.Kind = TokenKey + doNumber() + if this.eof { err = nil; return } + // Ident + case unicode.IsUpper(this.rune): + token.Kind = TokenIdent + for unicode.IsLetter(this.rune) || isDigit(this.rune) { + appendRune() + if this.eof { err = nil; return } + if err != nil { return } + } + // Comma + case this.rune == ',': + token.Kind = TokenComma + appendRune() + if this.eof { err = nil; return } + // LBrace + case this.rune == '{': + token.Kind = TokenLBrace + appendRune() + if this.eof { err = nil; return } + // RBrace + case this.rune == '}': + token.Kind = TokenRBrace + appendRune() + if this.eof { err = nil; return } + // LBracket + case this.rune == '[': + token.Kind = TokenLBracket + appendRune() + if this.eof { err = nil; return } + // RBracket + case this.rune == ']': + token.Kind = TokenRBracket + appendRune() + if this.eof { err = nil; return } + case unicode.IsPrint(this.rune): + err = parse.Errorf ( + this.pos(), "unexpected rune '%c'", + this.rune) + default: + err = parse.Errorf ( + this.pos(), "unexpected rune %U", + this.rune) + } + + return +} + +func (this *lexer) nextRune() error { + if this.lineFood == "" { + ok := this.lineScanner.Scan() + if ok { + this.line = this.lineScanner.Text() + this.lineFood = this.line + this.rune = '\n' + this.column = 0 + this.row ++ + } else { + err := this.lineScanner.Err() + if err == nil { + this.eof = true + return io.EOF + } else { + return err + } + } + } else { + var ch rune + var size int + for ch == 0 && this.lineFood != "" { + ch, size = utf8.DecodeRuneInString(this.lineFood) + this.lineFood = this.lineFood[size:] + } + this.rune = ch + this.column ++ + } + + return nil +} + +func (this *lexer) skipWhitespace() error { + err := this.skipComment() + if err != nil { return err } + for isWhitespace(this.rune) { + err := this.nextRune() + if err != nil { return err } + err = this.skipComment() + if err != nil { return err } + } + return nil +} + +func (this *lexer) skipComment() error { + if this.rune == ';' { + for this.rune != '\n' { + err := this.nextRune() + if err != nil { return err } + } + } + return nil +} + +func (this *lexer) pos() parse.Position { + return parse.Position { + File: this.fileName, + Line: this.lineScanner.Text(), + Row: this.row - 1, + Start: this.column - 1, + End: this.column, + } +} + +func (this *lexer) errUnexpectedEOF() error { + return parse.Errorf(this.pos(), "unexpected EOF") +} + +func isWhitespace(char rune) bool { + switch char { + case ' ', '\t', '\r', '\n': return true + default: return false + } +} + +func isDigit(char rune) bool { + return char >= '0' && char <= '9' +} + +func isHexDigit(char rune) bool { + return isDigit(char) || char >= 'a' && char <= 'f' || char >= 'A' && char <= 'F' +} diff --git a/generate/lex_test.go b/generate/lex_test.go new file mode 100644 index 0000000..fc4a967 --- /dev/null +++ b/generate/lex_test.go @@ -0,0 +1,54 @@ +package generate + +import "strings" +import "testing" +import "git.tebibyte.media/sashakoshka/goparse" + +func TestLex(test *testing.T) { + lexer, err := Lex("test.pdl", strings.NewReader(` + M0001 User { + 0000 Name String, + 0001 Users []User, + 0002 Followers U32, + }`)) + if err != nil { test.Fatal(parse.Format(err)) } + + correctTokens := []parse.Token { + tok(TokenMethod, "0001"), + tok(TokenIdent, "User"), + tok(TokenLBrace, "{"), + tok(TokenKey, "0000"), + tok(TokenIdent, "Name"), + tok(TokenIdent, "String"), + tok(TokenComma, ","), + tok(TokenKey, "0001"), + tok(TokenIdent, "Users"), + tok(TokenLBracket, "["), + tok(TokenRBracket, "]"), + tok(TokenIdent, "User"), + tok(TokenComma, ","), + tok(TokenKey, "0002"), + tok(TokenIdent, "Followers"), + tok(TokenIdent, "U32"), + tok(TokenComma, ","), + tok(TokenRBrace, "}"), + tok(parse.EOF, ""), + } + + for index, correct := range correctTokens { + got, err := lexer.Next() + if err != nil { test.Fatal(parse.Format(err)) } + if got.Kind != correct.Kind || got.Value != correct.Value { + test.Logf("token %d mismatch", index) + test.Log("GOT:", tokenNames[got.Kind], got.Value) + test.Fatal("CORRECT:", tokenNames[correct.Kind], correct.Value) + } + } +} + +func tok(kind parse.TokenKind, value string) parse.Token { + return parse.Token { + Kind: kind, + Value: value, + } +} -- 2.46.1 From 8ece6436b8e7f908e20f08aa5bfee934f2fe2e84 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 7 Jun 2025 22:38:02 -0400 Subject: [PATCH 078/132] generate: Add PDL parser --- generate/parse.go | 206 +++++++++++++++++++++++++++++++++++++++++ generate/parse_test.go | 69 ++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 generate/parse.go create mode 100644 generate/parse_test.go diff --git a/generate/parse.go b/generate/parse.go new file mode 100644 index 0000000..9d945e5 --- /dev/null +++ b/generate/parse.go @@ -0,0 +1,206 @@ +package generate + +import "io" +import "strconv" +import "git.tebibyte.media/sashakoshka/goparse" + +func Parse(lx parse.Lexer) (*Protocol, error) { + protocol := defaultProtocol() + par := parser { + Parser: parse.Parser { + Lexer: lx, + TokenNames: tokenNames, + }, + protocol: &protocol, + } + err := par.parse() + if err != nil { return nil, err } + return par.protocol, nil +} + +func defaultProtocol() Protocol { + return Protocol { + Messages: make(map[uint16] Message), + Types: map[string] Type { + "U8": TypeInt { Bits: 8 }, + "U16": TypeInt { Bits: 16 }, + "U32": TypeInt { Bits: 32 }, + "U64": TypeInt { Bits: 64 }, + "U128": TypeInt { Bits: 128 }, + "U256": TypeInt { Bits: 256 }, + "I8": TypeInt { Bits: 8, Signed: true }, + "I16": TypeInt { Bits: 16, Signed: true }, + "I32": TypeInt { Bits: 32, Signed: true }, + "I64": TypeInt { Bits: 64, Signed: true }, + "I128": TypeInt { Bits: 128, Signed: true }, + "I256": TypeInt { Bits: 256, Signed: true }, + "F16": TypeFloat { Bits: 16 }, + "F32": TypeFloat { Bits: 32 }, + "F64": TypeFloat { Bits: 64 }, + "F128": TypeFloat { Bits: 128 }, + "F256": TypeFloat { Bits: 256 }, + "String": TypeString { }, + "Buffer": TypeBuffer { }, + "Table": TypeTable { }, + }, + } +} + +func ParseReader(reader io.Reader) (*Protocol, error) { + lx, err := Lex("test.pdl", reader) + if err != nil { return nil, err } + return Parse(lx) +} + +type parser struct { + parse.Parser + protocol *Protocol +} + +func (this *parser) parse() error { + err := this.Next() + if err != nil { return err } + for this.Token.Kind != parse.EOF { + err = this.parseTopLevel() + if err != nil { return err } + } + return nil +} + +func (this *parser) parseTopLevel() error { + err := this.ExpectDesc("message or typedef", TokenMethod, TokenIdent) + if err != nil { return err } + if this.EOF() { return nil } + + switch this.Kind() { + case TokenMethod: return this.parseMessage() + case TokenIdent: return this.parseTypedef() + } + panic("bug") +} + +func (this *parser) parseMessage() error { + err := this.Expect(TokenMethod) + if err != nil { return err } + method, err := this.parseHexNumber(this.Value(), 0xFFFF) + if err != nil { return err } + err = this.ExpectNext(TokenIdent) + if err != nil { return err } + name := this.Value() + err = this.Next() + if err != nil { return err } + typ, err := this.parseType() + if err != nil { return err } + this.protocol.Messages[uint16(method)] = Message { + Name: name, + Type: typ, + } + return nil +} + +func (this *parser) parseTypedef() error { + err := this.Expect(TokenIdent) + if err != nil { return err } + name := this.Value() + err = this.Next() + if err != nil { return err } + typ, err := this.parseType() + if err != nil { return err } + this.protocol.Types[name] = typ + return nil +} + +func (this *parser) parseType() (Type, error) { + err := this.ExpectDesc("type", TokenIdent, TokenLBracket, TokenLBrace) + if err != nil { return nil, err } + + switch this.Kind() { + case TokenIdent: + return this.parseTypeNamed() + case TokenLBracket: + return this.parseTypeArray() + case TokenLBrace: + return this.parseTypeTable() + } + panic("bug") +} + +func (this *parser) parseTypeNamed() (TypeNamed, error) { + err := this.Expect(TokenIdent) + if err != nil { return TypeNamed { }, err } + name := this.Value() + err = this.Next() + if err != nil { return TypeNamed { }, err } + return TypeNamed { Name: name }, nil +} + +func (this *parser) parseTypeArray() (TypeArray, error) { + err := this.Expect(TokenLBracket) + if err != nil { return TypeArray { }, err } + err = this.ExpectNext(TokenRBracket) + if err != nil { return TypeArray { }, err } + err = this.Next() + if err != nil { return TypeArray { }, err } + typ, err := this.parseType() + if err != nil { return TypeArray { }, err } + return TypeArray { Element: typ }, nil +} + +func (this *parser) parseTypeTable() (TypeTableDefined, error) { + err := this.Expect(TokenLBrace) + if err != nil { return TypeTableDefined { }, err } + err = this.Next() + if err != nil { return TypeTableDefined { }, err } + typ := TypeTableDefined { + Fields: make(map[uint16] Field), + } + for { + err := this.ExpectDesc("table field", TokenKey, TokenRBrace) + if err != nil { return TypeTableDefined { }, err } + if this.Is(TokenRBrace) { + break + } + key, field, err := this.parseField() + if err != nil { return TypeTableDefined { }, err } + typ.Fields[key] = field + err = this.Expect(TokenComma, TokenRBrace) + if err != nil { return TypeTableDefined { }, err } + if this.Is(TokenRBrace) { + break + } + err = this.Next() + if err != nil { return TypeTableDefined { }, err } + } + err = this.Next() + if err != nil { return TypeTableDefined { }, err } + return typ, nil +} + +func (this *parser) parseField() (uint16, Field, error) { + err := this.Expect(TokenKey) + if err != nil { return 0, Field { }, err } + key, err := this.parseHexNumber(this.Value(), 0xFFFF) + if err != nil { return 0, Field { }, err } + err = this.ExpectNext(TokenIdent) + if err != nil { return 0, Field { }, err } + name := this.Value() + err = this.Next() + if err != nil { return 0, Field { }, err } + typ, err := this.parseType() + if err != nil { return 0, Field { }, err } + return uint16(key), Field { + Name: name, + Type: typ, + }, nil +} + +func (this *parser) parseHexNumber(input string, maxValue int64) (int64, error) { + number, err := strconv.ParseInt(input, 16, 64) + if err != nil { + return 0, parse.Errorf(this.Pos(), "%v", err) + } + if maxValue > 0 && number > maxValue { + return 0, parse.Errorf(this.Pos(), "value too large (max %X)", maxValue) + } + return number, nil +} diff --git a/generate/parse_test.go b/generate/parse_test.go new file mode 100644 index 0000000..39faabe --- /dev/null +++ b/generate/parse_test.go @@ -0,0 +1,69 @@ +package generate + +import "fmt" +import "strings" +import "testing" +// import "reflect" +import "git.tebibyte.media/sashakoshka/goparse" + +func TestParse(test *testing.T) { + correct := defaultProtocol() + correct.Messages[0x0000] = Message { + Name: "Connect", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + }, + }, + } + correct.Messages[0x0001] = Message { + Name: "UserList", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } }, + }, + }, + } + correct.Types["User"] = TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, + 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + }, + } + test.Log("CORRECT:", &correct) + + got, err := ParseReader(strings.NewReader(` + M0000 Connect { + 0000 Name String, + 0001 Password String, + } + + M0001 UserList { + 0000 Users []User, + } + + User { + 0000 Name String, + 0001 Bio String, + 0002 Followers U32, + } + `)) + if err != nil { test.Fatal(parse.Format(err)) } + test.Log("GOT: ", got) + + correctStr := fmt.Sprint(&correct) + gotStr := fmt.Sprint(got) + + if correctStr != gotStr { + test.Error("not equal") + for index := range min(len(correctStr), len(gotStr)) { + if correctStr[index] == gotStr[index] { continue } + test.Log("C:", correctStr[max(0, index - 8):min(len(correctStr), index + 8)]) + test.Log("G:", gotStr[max(0, index - 8):min(len(gotStr), index + 8)]) + break + } + test.FailNow() + } +} -- 2.46.1 From 6bc98b3f772327df13f2e21e6b62c16046f397c5 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 7 Jun 2025 22:38:20 -0400 Subject: [PATCH 079/132] generate: Add PDL language structures --- generate/protocol.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 generate/protocol.go diff --git a/generate/protocol.go b/generate/protocol.go new file mode 100644 index 0000000..1c3ebc4 --- /dev/null +++ b/generate/protocol.go @@ -0,0 +1,47 @@ +package generate + +type Protocol struct { + Messages map[uint16] Message + Types map[string] Type +} + +type Message struct { + Name string + Type Type +} + +type Type interface { + +} + +type TypeInt struct { + Bits int + Signed bool +} + +type TypeFloat struct { + Bits int +} + +type TypeString struct { } + +type TypeBuffer struct { } + +type TypeArray struct { + Element Type +} + +type TypeTable struct { } + +type TypeTableDefined struct { + Fields map[uint16] Field +} + +type Field struct { + Name string + Type Type +} + +type TypeNamed struct { + Name string +} -- 2.46.1 From 272a4da3c21ea296489466780140a6bbbfb15d57 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 7 Jun 2025 22:39:12 -0400 Subject: [PATCH 080/132] Remove markdown, add goparse from go.modo --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b443b6..af2d24c 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.23.0 require ( git.tebibyte.media/sashakoshka/go-util v0.9.1 - github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 + git.tebibyte.media/sashakoshka/goparse v0.2.0 ) diff --git a/go.sum b/go.sum index bb15dd1..e3ad650 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ git.tebibyte.media/sashakoshka/go-util v0.9.1 h1:eGAbLwYhOlh4aq/0w+YnJcxT83yPhXtxnYMzz6K7xGo= git.tebibyte.media/sashakoshka/go-util v0.9.1/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y= -github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g= -github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI= +git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk= -- 2.46.1 From a1f297e5b5ecf74d63b57db680662a989a474762 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 8 Jun 2025 06:01:28 -0400 Subject: [PATCH 081/132] generate: Remove commented out import --- generate/parse_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/generate/parse_test.go b/generate/parse_test.go index 39faabe..415fcf7 100644 --- a/generate/parse_test.go +++ b/generate/parse_test.go @@ -3,7 +3,6 @@ package generate import "fmt" import "strings" import "testing" -// import "reflect" import "git.tebibyte.media/sashakoshka/goparse" func TestParse(test *testing.T) { -- 2.46.1 From ce503c4689f22b565a26ef711d245115380dd100 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 20 Jun 2025 15:05:58 -0400 Subject: [PATCH 082/132] Big nasty commit to add code generation for encoding --- codec/decode.go | 25 +- codec/encode.go | 26 +- codec/measure.go | 11 + codec/measure_test.go | 21 ++ design/pdl-compiler.md | 19 +- generate/generate.go | 603 +++++++++++++++++++++++++++++++++++++++++ generate/parse.go | 23 +- tape/dynamic.go | 48 ++++ tape/tag.go | 45 +++ 9 files changed, 786 insertions(+), 35 deletions(-) create mode 100644 codec/measure_test.go create mode 100644 generate/generate.go create mode 100644 tape/dynamic.go create mode 100644 tape/tag.go diff --git a/codec/decode.go b/codec/decode.go index 1c27312..c4cdb9f 100644 --- a/codec/decode.go +++ b/codec/decode.go @@ -81,7 +81,7 @@ func (this *Decoder) ReadUint64() (value uint64, n int, err error) { n, err = this.ReadFull(buffer[:]) return uint64(buffer[0]) << 56 | uint64(buffer[1]) << 48 | - uint64(buffer[2]) << 48 | + uint64(buffer[2]) << 40 | uint64(buffer[3]) << 32 | uint64(buffer[4]) << 24 | uint64(buffer[5]) << 16 | @@ -89,14 +89,33 @@ func (this *Decoder) ReadUint64() (value uint64, n int, err error) { uint64(buffer[7]), n, err } +// ReadIntN decodes an N-byte signed integer from the input reader. +func (this *Decoder) ReadIntN(bytes int) (value int64, n int, err error) { + uncasted, n, err := this.ReadUintN(bytes) + return int64(uncasted), n, err +} + +// ReadUintN decodes an N-byte unsigned integer from the input reader. +func (this *Decoder) ReadUintN(bytes int) (value uint64, n int, err error) { + // TODO: don't make multiple read calls (without allocating) + buffer := [1]byte { } + for bytesLeft := bytes; bytesLeft > 0; bytesLeft -- { + nn, err := this.ReadFull(buffer[:]) + n += nn; if err != nil { return 0, n, err } + value |= uint64(buffer[0]) << ((bytesLeft - 1) * 8) + } + // *read* integers too big, but don't return them. + if bytes > 8 { value = 0 } + return value, n, nil +} + // ReadGBEU decodes a growing unsigned integer of up to 64 bits from the input // reader. func (this *Decoder) ReadGBEU() (value uint64, n int, err error) { var fullValue uint64 for { chunk, nn, err := this.ReadByte() - if err != nil { return 0, n, err } - n += nn + n += nn; if err != nil { return 0, n, err } fullValue *= 0x80 fullValue += uint64(chunk & 0x7F) diff --git a/codec/encode.go b/codec/encode.go index 2c96bba..125c11a 100644 --- a/codec/encode.go +++ b/codec/encode.go @@ -76,13 +76,31 @@ func (this *Encoder) WriteUint64(value uint64) (n int, err error) { }) } +// WriteIntN encodes an N-byte signed integer to the output writer. +func (this *Encoder) WriteIntN(value int64, bytes int) (n int, err error) { + return this.WriteUintN(uint64(value), bytes) +} + +// for below functions, increase buffers if go somehow gets support for over 64 +// bit integers. we could also make an expanding int type in goutil to use here, +// or maybe there is one in the stdlib. keep the int64 versions as well though +// because its ergonomic. + +// WriteUintN encodes an N-byte unsigned integer to the output writer. +func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) { + // TODO: don't make multiple write calls (without allocating) + buffer := [1]byte { } + for bytesLeft := bytes; bytesLeft > 0; bytesLeft -- { + buffer[0] = byte(buffer[0]) >> ((bytesLeft - 1) * 8) + nn, err := this.Write(buffer[:]) + n += nn; if err != nil { return n, err } + } + return n, nil +} + // EncodeGBEU encodes a growing unsigned integer of up to 64 bits to the output // writer. func (this *Encoder) EncodeGBEU(value uint64) (n int, err error) { - // increase if go somehow gets support for over 64 bit integers. we - // could also make an expanding int type in goutil to use here, or maybe - // there is one in the stdlib. keep this int64 version as well though - // because its ergonomic. buffer := [16]byte { } window := (GBEUSize(value) - 1) * 7 diff --git a/codec/measure.go b/codec/measure.go index 777a982..5c2e134 100644 --- a/codec/measure.go +++ b/codec/measure.go @@ -9,3 +9,14 @@ func GBEUSize(value uint64) int { if value == 0 { return length } } } + +// IntBytes returns the number of bytes required to hold a given unsigned +// integer. +func IntBytes(value uint64) int { + bytes := 0 + for value > 0 || bytes == 0 { + value >>= 8; + bytes ++ + } + return bytes +} diff --git a/codec/measure_test.go b/codec/measure_test.go new file mode 100644 index 0000000..a1f4291 --- /dev/null +++ b/codec/measure_test.go @@ -0,0 +1,21 @@ +package codec + +import "testing" + +func TestIntBytes(test *testing.T) { + if correct, got := 1, IntBytes(0); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(1); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(16); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 1, IntBytes(255); correct != got { + test.Fatal("wrong:", got) + } + if correct, got := 2, IntBytes(256); correct != got { + test.Fatal("wrong:", got) + } +} diff --git a/design/pdl-compiler.md b/design/pdl-compiler.md index 364961d..b87e3f3 100644 --- a/design/pdl-compiler.md +++ b/design/pdl-compiler.md @@ -56,20 +56,27 @@ static section. For each defined type, the compiler shall generate a Go type with the same name as written in its definition. The Go type shall be encodable, and shall have -`Encode` and `Decode` methods as described below. +`EncodeValue`, `DecodeValue`, and `Tag` methods as described below. ## Encoding and Decoding Methods -Each encodable type shall be given an `Encode` method and a `Decode` method, -which will take in a `codec.Encoder` and a `codec.Decoder` respectively. Both +Each message shall be given an `Encode` method and a `Decode` method, +which shall take in a `codec.Encoder` and a `codec.Decoder` respectively. Both return an `(int, error)` pair describing the amount of bytes written and an -error if the write stopped early. `Encode` will encode the data within the -message to the given encoder, and `Decode` will decode data from the given +error if the write stopped early. `Encode` shall encode the data within the +message to the given encoder, and `Decode` shall decode data from the given decoder and place it in the type's value. The methods shall not retain or close any encoders or decoders they are given. Both methods shall have pointer -receivers. In effect, these methods will satisfy `codec.Encodable` and +receivers. In effect, these methods shall satisfy `codec.Encodable` and `codec.Decodable`. +Each defined type shall be given an `EncodeValue` method and a `DecodeValue` +method, which shall both take in a `tape.Tag`, then a `codec.Encoder` and a +`codec.Decoder` respectively. These methods shall encode and decode the value +according to the CN given by the tag. The TN shall be ignored. The message shall +also have a method `Tag` that takes no arguments and returns the preferred tag +of the type including the TN and CN. + ## Connection The compiler shall generate a `Conn` struct which embeds a `hopp.Conn`, which diff --git a/generate/generate.go b/generate/generate.go new file mode 100644 index 0000000..e3e262b --- /dev/null +++ b/generate/generate.go @@ -0,0 +1,603 @@ +package generate + +import "io" +import "fmt" +import "maps" +import "math" +import "slices" +import "strings" +import "git.tebibyte.media/sashakoshka/hopp/codec" + +const imports = +` +import "git.teibibyte.media/sashakoshka/hopp/tape" +import "git.teibibyte.media/sashakoshka/hopp/codec" +` + +const preamble = ` +/* # Do not edit this package by hand! + * + * This file was automatically generated by the Holanet PDL compiler. The + * source file is located at + * Please edit that file instead, and re-compile it to this location. + * + * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + */ +` + +const static = ` +// Table is a KTV table with an undefined schema. +type Table map[uint16] any + +// Message is any message that can be sent along this protocol. +type Message interface { + codec.Encodable + codec.Decodable + + // Method returns the method code of the message. + Method() uint16 +} +` + +// Generator converts protocols into Go code. +type Generator struct { + // Output is where the generated code will be sent. + Output io.Writer + // PackageName is the package name that will be used in the file. If + // left empty, the default is "protocol". + PackageName string + + nestingLevel int + protocol *Protocol +} + +func (this *Generator) Generate(protocol *Protocol) (n int, err error) { + this.nestingLevel = 0 + this.protocol = protocol + defer func() { this.protocol = nil }() + + // preamble and static section + packageName := "protocol" + if this.PackageName != "" { + packageName = this.PackageName + } + nn, err := this.iprintf("package %s\n", packageName) + n += nn; if err != nil { return n, err } + nn, err = this.print(preamble) + n += nn; if err != nil { return n, err } + nn, err = this.print(imports) + n += nn; if err != nil { return n, err } + nn, err = this.print(static) + n += nn; if err != nil { return n, err } + + // type definitions + for _, name := range slices.Sorted(maps.Keys(protocol.Types)) { + nn, err := this.generateTypedef(name, protocol.Types[name]) + n += nn; if err != nil { return n, err } + } + + // messages + for _, method := range slices.Sorted(maps.Keys(protocol.Messages)) { + nn, err := this.generateMessage(method, protocol.Messages[method]) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) { + // type definition + nn, err := this.iprintf( + "\n// %s represents the protocol data type %s.\n", + name, name) + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("type %s ", name) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(typ) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + + // Tag method + // to be honest we probably don't need this method at all + // nn, err = this.iprintf("\n// Tag returns the preferred TAPE tag.\n") + // n += nn; if err != nil { return n, err } + // nn, err = this.iprintf("func (this *%s) Tag() tape.Tag {\n", name) + // n += nn; if err != nil { return n, err } + // this.push() + // nn, err = this.iprintf("return ") + // n += nn; if err != nil { return n, err } + // nn, err = this.generateTag(typ, "(*this)") + // n += nn; if err != nil { return n, err } + // nn, err = this.println() + // n += nn; if err != nil { return n, err } + // this.pop() + // nn, err = this.iprintf("}\n") + // n += nn; if err != nil { return n, err } + + // EncodeValue method + nn, err = this.iprintf( + "\n// EncodeValue encodes the value of this type without the " + + "tag. The value is\n// encoded according to the parameters " + + "specified by the tag, if possible.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func (this *%s) EncodeValue(encoder *codec.Encoder, tag tape.Tag) (n int, err error) {\n", + name) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("var nn int\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateEncodeValue(typ, "(*this)", "tag") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // TODO DecodeValue method + + return n, nil +} + +func (this *Generator) generateMessage(method uint16, message Message) (n int, err error) { + nn, err := this.iprintf( + "\n// %s represents the protocol message M%04X %s.\n", + message.Name, method, message.Name) + nn, err = this.iprintf("type %s ", this.resolveMessageName(message.Name)) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(message.Type) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + + // Encode method + nn, err = this.iprintf("\n// Encode encodes this message's tag and value.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func(this %s) Encode(encoder *codec.Encoder) (n int, err error) {\n", + this.resolveMessageName(message.Name)) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("tag := ") + n += nn; if err != nil { return n, err } + nn, err = this.generateTag(message.Type, "(*this)") + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err := encoder.WriteUint8()\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.generateEncodeValue(message.Type, "(*this)", "tag") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + + // TODO decode method + + return n, nil +} + +func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + // SI: (none) + // LI: + if typ.Bits <= 5 { + // SI stores the value in the tag, so we write nothing here + break + } + nn, err := this.iprintf("nn, err = encoder.WriteInt%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeFloat: + // FP: + nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeString: + // see TypeBuffer + nn, err := this.generateEncodeValue(TypeBuffer { }, valueSource, tagSource) + n += nn; if err != nil { return n, err } + case TypeBuffer: + // SBA: * + // LBA: * + nn, err := this.iprintf("if %s.Is(tape.LBA) {\n", tagSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n", tagSource) + n += nn; if err != nil { return n, err } + + nn, err = this.iprintf("nn, err = encoder.Write([]byte(%s))\n", valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + case TypeArray: + // OTA: * + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("{\n") + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("itemTag := ") + n += nn; if err != nil { return n, err } + nn, err = this.generateTN(typ.Element) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for _, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("tag := ") + n += nn; if err != nil { return n, err } + nn, err = this.generateTag(typ.Element, "item") + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag.Is(tape.SBA) { continue }\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag.CN() > itemTag.CN() { largest = tag }\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("if itemTag.Is(tape.SBA) { itemTag += 1 << 5 }\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for _, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.generateEncodeValue(typ.Element, "item", "itemTag") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + case TypeTable: + // KTV: ( )* + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + tagSource, valueSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("for key, item := range %s {\n", valueSource) + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("nn, err = encoder.WriteUint16(key)\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag := tape.TagAny(tag)\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(tag))\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err = tape.EncodeAny(tag)\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + // KTV: ( )* + nn, err := this.iprintf( + "nn, err = encoder.WriteUintN(%s.CN(), %d)\n", + tagSource, len(typ.Fields)) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("{\n") + n += nn; if err != nil { return n, err } + this.push() + nn, err = this.iprintf("var tag tape.Tag\n") + n += nn; if err != nil { return n, err } + for key, field := range typ.Fields { + nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("tag = ") + n += nn; if err != nil { return n, err } + fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name) + nn, err = this.generateTag(field.Type, fieldSource) + n += nn; if err != nil { return n, err } + nn, err = this.println() + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(tag))\n") + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + nn, err = this.generateEncodeValue(field.Type, fieldSource, "tag") + n += nn; if err != nil { return n, err } + } + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } + case TypeNamed: + // WHATEVER: [WHATEVER] + nn, err := this.iprintf("nn, err = %s.EncodeValue(encoder, %s)\n", valueSource, tagSource) + n += nn; if err != nil { return n, err } + nn, err = this.generateErrorCheck() + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateErrorCheck() (n int, err error) { + return this.iprintf("n += nn; if err != nil { return n, err }\n") +} + +// generateTag generates the preferred TN and CN for the given type and value. +// The generated code is INLINE. +func (this *Generator) generateTag(typ Type, source string) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if typ.Bits <= 5 { + nn, err := this.printf("tape.TagSI") + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("tape.TagLI.WithCN(%d)", bitsToCN(typ.Bits)) + n += nn; if err != nil { return n, err } + } + case TypeFloat: + nn, err := this.printf("tape.TagFP.WithCN(%d)", bitsToCN(typ.Bits)) + n += nn; if err != nil { return n, err } + case TypeString: + nn, err := this.generateTag(TypeBuffer { }, source) + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.printf("bufferTag(%s)", source) + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.printf("arrayTag(tape.TagOTA.WithCN(tape.IntBytes(uint64(len(%s))))", source) + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.printf("tape.TagKTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.printf("tape.TagKTV.WithCN(%d)", codec.IntBytes(uint64(len(typ.Fields)))) + n += nn; if err != nil { return n, err } + case TypeNamed: + resolved, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateTag(resolved, source) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +// generateTN generates the appropriate TN for the given type. The generated +// code is INLINE. The generated tag will have a CN as zero. For types that +// change TN based on their length, the TN capable of supporting more +// information is chosen. +func (this *Generator) generateTN(typ Type) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if typ.Bits <= 5 { + nn, err := this.printf("tape.TagSI") + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("tape.TagLI") + n += nn; if err != nil { return n, err } + } + case TypeFloat: + nn, err := this.printf("tape.TagFP",) + n += nn; if err != nil { return n, err } + case TypeString: + nn, err := this.generateTN(TypeBuffer { }) + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.printf("tape.TagLBA") + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.printf("tape.TagOTA") + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.printf("tape.TagKTV") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.printf("tape.TagKTV") + n += nn; if err != nil { return n, err } + case TypeNamed: + resolved, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateTN(resolved) + n += nn; if err != nil { return n, err } + } + + return n, nil +} + +func (this *Generator) generateType(typ Type) (n int, err error) { + switch typ := typ.(type) { + case TypeInt: + if err := this.validateIntBitSize(typ.Bits); err != nil { + return n, err + } + if typ.Signed { + nn, err := this.printf("int%d", typ.Bits) + n += nn; if err != nil { return n, err } + } else { + nn, err := this.printf("uint%d", typ.Bits) + n += nn; if err != nil { return n, err } + } + case TypeFloat: + switch typ.Bits { + case 16: + nn, err := this.print("float32") + n += nn; if err != nil { return n, err } + case 32, 64: + nn, err := this.printf("float%d", typ.Bits) + n += nn; if err != nil { return n, err } + default: + return n, fmt.Errorf("floats of size %d are unsupported on this platform", typ.Bits) + } + case TypeString: + nn, err := this.print("string") + n += nn; if err != nil { return n, err } + case TypeBuffer: + nn, err := this.print("[]byte") + n += nn; if err != nil { return n, err } + case TypeArray: + nn, err := this.print("[]") + n += nn; if err != nil { return n, err } + nn, err = this.generateType(typ.Element) + n += nn; if err != nil { return n, err } + case TypeTable: + nn, err := this.print("Table") + n += nn; if err != nil { return n, err } + case TypeTableDefined: + nn, err := this.generateTypeTableDefined(typ) + n += nn; if err != nil { return n, err } + case TypeNamed: + actual, err := this.resolveTypeName(typ.Name) + if err != nil { return n, err } + nn, err := this.generateType(actual) + n += nn; if err != nil { return n, err } + } + return n, nil +} + +func (this *Generator) generateTypeTableDefined(typ TypeTableDefined) (n int, err error) { + nn, err := this.print("struct {\n") + n += nn; if err != nil { return n, err } + this.push() + + for _, key := range slices.Sorted(maps.Keys(typ.Fields)) { + field := typ.Fields[key] + nn, err := this.iprintf("%s ", field.Name) + n += nn; if err != nil { return n, err } + nn, err = this.generateType(field.Type) + n += nn; if err != nil { return n, err } + nn, err = this.print("\n") + n += nn; if err != nil { return n, err } + } + + this.pop() + nn, err = this.iprint("}") + n += nn; if err != nil { return n, err } + return n, nil +} + +func (this *Generator) validateIntBitSize(size int) error { + switch size { + case 8, 16, 32, 64: return nil + default: return fmt.Errorf("integers of size %d are unsupported on this platform", size) + } +} + +func (this *Generator) validateFloatBitSize(size int) error { + switch size { + case 16, 32, 64: return nil + default: return fmt.Errorf("floats of size %d are unsupported on this platform", size) + } +} + +func (this *Generator) push() { + this.nestingLevel ++ +} + +func (this *Generator) pop() { + if this.nestingLevel < 1 { + panic("cannot pop when nesting level is less than 1") + } + this.nestingLevel -- +} + +func (this *Generator) indent() string { + return strings.Repeat("\t", this.nestingLevel) +} + +func (this *Generator) print(args ...any) (n int, err error) { + return fmt.Fprint(this.Output, args...) +} + +func (this *Generator) println(args ...any) (n int, err error) { + return fmt.Fprintln(this.Output, args...) +} + +func (this *Generator) printf(format string, args ...any) (n int, err error) { + return fmt.Fprintf(this.Output, format, args...) +} + +func (this *Generator) iprint(args ...any) (n int, err error) { + return fmt.Fprint(this.Output, this.indent() + fmt.Sprint(args...)) +} + +func (this *Generator) iprintln(args ...any) (n int, err error) { + return fmt.Fprintln(this.Output, this.indent() + fmt.Sprint(args...)) +} + +func (this *Generator) iprintf(format string, args ...any) (n int, err error) { + return fmt.Fprintf(this.Output, this.indent() + format, args...) +} + +func (this *Generator) resolveMessageName(message string) string { + return "Message" + message +} + +func (this *Generator) resolveTypeName(name string) (Type, error) { + switch name { + case "U8": return TypeInt { Bits: 8 }, nil + case "U16": return TypeInt { Bits: 16 }, nil + case "U32": return TypeInt { Bits: 32 }, nil + case "U64": return TypeInt { Bits: 64 }, nil + case "U128": return TypeInt { Bits: 128 }, nil + case "U256": return TypeInt { Bits: 256 }, nil + case "I8": return TypeInt { Bits: 8, Signed: true }, nil + case "I16": return TypeInt { Bits: 16, Signed: true }, nil + case "I32": return TypeInt { Bits: 32, Signed: true }, nil + case "I64": return TypeInt { Bits: 64, Signed: true }, nil + case "I128": return TypeInt { Bits: 128, Signed: true }, nil + case "I256": return TypeInt { Bits: 256, Signed: true }, nil + case "F16": return TypeFloat { Bits: 16 }, nil + case "F32": return TypeFloat { Bits: 32 }, nil + case "F64": return TypeFloat { Bits: 64 }, nil + case "F128": return TypeFloat { Bits: 128 }, nil + case "F256": return TypeFloat { Bits: 256 }, nil + case "String": return TypeString { }, nil + case "Buffer": return TypeBuffer { }, nil + case "Table": return TypeTable { }, nil + } + + if typ, ok := this.protocol.Types[name]; ok { + if typ, ok := typ.(TypeNamed); ok { + return this.resolveTypeName(typ.Name) + } + + return typ, nil + } + return nil, fmt.Errorf("no type exists called %s", name) +} + +func bitsToBytes(bits int) int { + return int(math.Ceil(float64(bits) / 8.0)) +} + +func bitsToCN(bits int) int { + return bitsToBytes(bits) - 1 +} diff --git a/generate/parse.go b/generate/parse.go index 9d945e5..287a693 100644 --- a/generate/parse.go +++ b/generate/parse.go @@ -21,28 +21,7 @@ func Parse(lx parse.Lexer) (*Protocol, error) { func defaultProtocol() Protocol { return Protocol { Messages: make(map[uint16] Message), - Types: map[string] Type { - "U8": TypeInt { Bits: 8 }, - "U16": TypeInt { Bits: 16 }, - "U32": TypeInt { Bits: 32 }, - "U64": TypeInt { Bits: 64 }, - "U128": TypeInt { Bits: 128 }, - "U256": TypeInt { Bits: 256 }, - "I8": TypeInt { Bits: 8, Signed: true }, - "I16": TypeInt { Bits: 16, Signed: true }, - "I32": TypeInt { Bits: 32, Signed: true }, - "I64": TypeInt { Bits: 64, Signed: true }, - "I128": TypeInt { Bits: 128, Signed: true }, - "I256": TypeInt { Bits: 256, Signed: true }, - "F16": TypeFloat { Bits: 16 }, - "F32": TypeFloat { Bits: 32 }, - "F64": TypeFloat { Bits: 64 }, - "F128": TypeFloat { Bits: 128 }, - "F256": TypeFloat { Bits: 256 }, - "String": TypeString { }, - "Buffer": TypeBuffer { }, - "Table": TypeTable { }, - }, + Types: map[string] Type { }, } } diff --git a/tape/dynamic.go b/tape/dynamic.go new file mode 100644 index 0000000..4564e2e --- /dev/null +++ b/tape/dynamic.go @@ -0,0 +1,48 @@ +package tape + +import "fmt" +import "reflect" +import "git.tebibyte.media/sashakoshka/hopp/codec" + +// EncodeAny encodes an "any" value. Returns an error if the underlying type is +// unsupported. Supported types are: +// +// - int +// - int +// - uint +// - uint +// - string +// - [] +// - map[uint16] +func EncodeAny(encoder *codec.Encoder, value any) (Tag, error) { + // TODO +} + +// TagAny returns the correct tag for an "any" value. Returns an error if the +// underlying type is unsupported. See [EncodeAny] for a list of supported +// types. +func TagAny(value any) (Tag, error) { + // primitives + switch value := value.(type) { + case int, uint: return LI.WithCN(3), nil + case int8, uint8: return LI.WithCN(0), nil + case int16, uint16: return LI.WithCN(1), nil + case int32, uint32: return LI.WithCN(3), nil + case int64, uint64: return LI.WithCN(8), nil + case string: return bufferLenTag(len(value)), nil + case []byte: return bufferLenTag(len(value)), nil + } + + // aggregates + reflectType := reflect.TypeOf(value) + switch reflectType.Kind() { + case reflect.Slice: return OTA.WithCN(reflect.ValueOf(value).Len()), nil + case reflect.Array: return OTA.WithCN(reflectType.Len()), nil + case reflect.Map: + if reflectType.Key() == reflect.TypeOf(uint16(0)) { + return OTA.WithCN(reflect.ValueOf(value).Len()), nil + } + return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) + } + return 0, fmt.Errorf("cannot encode type %T", value) +} diff --git a/tape/tag.go b/tape/tag.go new file mode 100644 index 0000000..fb603df --- /dev/null +++ b/tape/tag.go @@ -0,0 +1,45 @@ +package tape + +import "git.tebibyte.media/sashakoshka/hopp/codec" + +type Tag byte; const ( + SI Tag = 0 << 5 // Small integer + LI Tag = 1 << 5 // Large integer + FP Tag = 2 << 5 // Floating point + SBA Tag = 3 << 5 // Small byte array + LBA Tag = 4 << 5 // Large byte array + OTA Tag = 5 << 5 // One-tag array + KTV Tag = 6 << 5 // Key-tag-value table + TNMask Tag = 0xE0 // The entire TN bitfield + CNMask Tag = 0x20 // The entire CN bitfield + CNLimit Tag = 32 // All valid CNs are < CNLimit +) + +func (tag Tag) TN() int { + return int(tag >> 5) +} + +func (tag Tag) CN() int { + return int(tag & CNMask) +} + +func (tag Tag) WithCN(cn int) Tag { + return (tag & TNMask) | Tag(cn % 32) +} + +func (tag Tag) Is(other Tag) bool { + return tag.TN() == other.TN() +} + +// BufferTag returns the appropriate tag for a buffer. +func BufferTag(value []byte) Tag { + return bufferLenTag(len(value)) +} + +func bufferLenTag(length int) Tag { + if length < int(CNLimit) { + return SBA.WithCN(length) + } else { + return LBA.WithCN(codec.IntBytes(uint64(length))) + } +} -- 2.46.1 From 285e83d9951b490033cfbfb4672fbb4c7869d924 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 20 Jun 2025 15:55:37 -0400 Subject: [PATCH 083/132] Merge codec and tape packages --- generate/generate.go | 29 +++++++++++++++++++++-------- {codec => tape}/decode.go | 2 +- tape/dynamic.go | 3 +-- {codec => tape}/encode.go | 2 +- {codec => tape}/measure.go | 2 +- {codec => tape}/measure_test.go | 2 +- tape/tag.go | 4 +--- 7 files changed, 27 insertions(+), 17 deletions(-) rename {codec => tape}/decode.go (99%) rename {codec => tape}/encode.go (99%) rename {codec => tape}/measure.go (96%) rename {codec => tape}/measure_test.go (97%) diff --git a/generate/generate.go b/generate/generate.go index e3e262b..939d399 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -6,12 +6,11 @@ import "maps" import "math" import "slices" import "strings" -import "git.tebibyte.media/sashakoshka/hopp/codec" +import "git.tebibyte.media/sashakoshka/hopp/tape" const imports = ` import "git.teibibyte.media/sashakoshka/hopp/tape" -import "git.teibibyte.media/sashakoshka/hopp/codec" ` const preamble = ` @@ -31,8 +30,8 @@ type Table map[uint16] any // Message is any message that can be sent along this protocol. type Message interface { - codec.Encodable - codec.Decodable + tape.Encodable + tape.Decodable // Method returns the method code of the message. Method() uint16 @@ -122,7 +121,7 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) "specified by the tag, if possible.\n") n += nn; if err != nil { return n, err } nn, err = this.iprintf( - "func (this *%s) EncodeValue(encoder *codec.Encoder, tag tape.Tag) (n int, err error) {\n", + "func (this *%s) EncodeValue(encoder *tape.Encoder, tag tape.Tag) (n int, err error) {\n", name) n += nn; if err != nil { return n, err } this.push() @@ -136,7 +135,21 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) nn, err = this.iprintf("}\n") n += nn; if err != nil { return n, err } - // TODO DecodeValue method + // DecodeValue method + nn, err = this.iprintf( + "\n // DecodeValue decodes the value of this type without " + + "the tag. The value is\n// decoded according to the " + + "parameters specified by the tag, if possible.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func (this *%s) DecodeValue(decoder *tape.Decoder, tag tape.Tag) (n int, err error) {\n", + name) + n += nn; if err != nil { return n, err } + this.push() + // TODO + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } return n, nil } @@ -156,7 +169,7 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e nn, err = this.iprintf("\n// Encode encodes this message's tag and value.\n") n += nn; if err != nil { return n, err } nn, err = this.iprintf( - "func(this %s) Encode(encoder *codec.Encoder) (n int, err error) {\n", + "func(this %s) Encode(encoder *tape.Encoder) (n int, err error) {\n", this.resolveMessageName(message.Name)) n += nn; if err != nil { return n, err } this.push() @@ -378,7 +391,7 @@ func (this *Generator) generateTag(typ Type, source string) (n int, err error) { nn, err := this.printf("tape.TagKTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) n += nn; if err != nil { return n, err } case TypeTableDefined: - nn, err := this.printf("tape.TagKTV.WithCN(%d)", codec.IntBytes(uint64(len(typ.Fields)))) + nn, err := this.printf("tape.TagKTV.WithCN(%d)", tape.IntBytes(uint64(len(typ.Fields)))) n += nn; if err != nil { return n, err } case TypeNamed: resolved, err := this.resolveTypeName(typ.Name) diff --git a/codec/decode.go b/tape/decode.go similarity index 99% rename from codec/decode.go rename to tape/decode.go index c4cdb9f..466324b 100644 --- a/codec/decode.go +++ b/tape/decode.go @@ -1,4 +1,4 @@ -package codec +package tape import "io" diff --git a/tape/dynamic.go b/tape/dynamic.go index 4564e2e..fd35dfd 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -2,7 +2,6 @@ package tape import "fmt" import "reflect" -import "git.tebibyte.media/sashakoshka/hopp/codec" // EncodeAny encodes an "any" value. Returns an error if the underlying type is // unsupported. Supported types are: @@ -14,7 +13,7 @@ import "git.tebibyte.media/sashakoshka/hopp/codec" // - string // - [] // - map[uint16] -func EncodeAny(encoder *codec.Encoder, value any) (Tag, error) { +func EncodeAny(encoder *Encoder, value any) (Tag, error) { // TODO } diff --git a/codec/encode.go b/tape/encode.go similarity index 99% rename from codec/encode.go rename to tape/encode.go index 125c11a..121f1e9 100644 --- a/codec/encode.go +++ b/tape/encode.go @@ -1,4 +1,4 @@ -package codec +package tape import "io" diff --git a/codec/measure.go b/tape/measure.go similarity index 96% rename from codec/measure.go rename to tape/measure.go index 5c2e134..ded5153 100644 --- a/codec/measure.go +++ b/tape/measure.go @@ -1,4 +1,4 @@ -package codec +package tape // GBEUSize returns the size (in octets) of a GBEU integer. func GBEUSize(value uint64) int { diff --git a/codec/measure_test.go b/tape/measure_test.go similarity index 97% rename from codec/measure_test.go rename to tape/measure_test.go index a1f4291..4cce9dd 100644 --- a/codec/measure_test.go +++ b/tape/measure_test.go @@ -1,4 +1,4 @@ -package codec +package tape import "testing" diff --git a/tape/tag.go b/tape/tag.go index fb603df..a2a9fd4 100644 --- a/tape/tag.go +++ b/tape/tag.go @@ -1,7 +1,5 @@ package tape -import "git.tebibyte.media/sashakoshka/hopp/codec" - type Tag byte; const ( SI Tag = 0 << 5 // Small integer LI Tag = 1 << 5 // Large integer @@ -40,6 +38,6 @@ func bufferLenTag(length int) Tag { if length < int(CNLimit) { return SBA.WithCN(length) } else { - return LBA.WithCN(codec.IntBytes(uint64(length))) + return LBA.WithCN(IntBytes(uint64(length))) } } -- 2.46.1 From c4407d9759ee62489bc5c29ca57ccf560e9aa320 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 20 Jun 2025 18:39:16 -0400 Subject: [PATCH 084/132] tape: Implement encoding for "any" values --- tape/dynamic.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index fd35dfd..6dc0380 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -13,8 +13,43 @@ import "reflect" // - string // - [] // - map[uint16] -func EncodeAny(encoder *Encoder, value any) (Tag, error) { - // TODO +func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { + // primitives + switch value := value.(type) { + case int: return encoder.WriteInt32(int32(value)) + case uint: return encoder.WriteUint32(uint32(value)) + case int8: return encoder.WriteInt8(value) + case uint8: return encoder.WriteUint8(value) + case int16: return encoder.WriteInt16(value) + case uint16: return encoder.WriteUint16(value) + case int32: return encoder.WriteInt32(value) + case uint32: return encoder.WriteUint32(value) + case int64: return encoder.WriteInt64(value) + case uint64: return encoder.WriteUint64(value) + case string: return EncodeAny(encoder, []byte(value), tag) + case []byte: + if tag.Is(LBA) { + nn, err := encoder.WriteUintN(uint64(len(value)), tag.CN() + 1) + n += nn; if err != nil { return n, err } + } + nn, err := encoder.Write(value) + n += nn; if err != nil { return n, err } + } + + // aggregates + reflectType := reflect.TypeOf(value) + switch reflectType.Kind() { + case reflect.Slice: + return encodeAnySlice(encoder, value, tag) + case reflect.Array: + return encodeAnySlice(encoder, reflect.ValueOf(value).Slice(0, reflectType.Len()).Interface(), tag) + case reflect.Map: + if reflectType.Key() == reflect.TypeOf(uint16(0)) { + return encodeAnyMap(encoder, value, tag) + } + return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) + } + return 0, fmt.Errorf("cannot encode type %T", value) } // TagAny returns the correct tag for an "any" value. Returns an error if the @@ -27,7 +62,7 @@ func TagAny(value any) (Tag, error) { case int8, uint8: return LI.WithCN(0), nil case int16, uint16: return LI.WithCN(1), nil case int32, uint32: return LI.WithCN(3), nil - case int64, uint64: return LI.WithCN(8), nil + case int64, uint64: return LI.WithCN(7), nil case string: return bufferLenTag(len(value)), nil case []byte: return bufferLenTag(len(value)), nil } @@ -43,5 +78,51 @@ func TagAny(value any) (Tag, error) { } return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) } - return 0, fmt.Errorf("cannot encode type %T", value) + return 0, fmt.Errorf("cannot get tag of type %T", value) +} + +func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) { + // OTA: * + reflectValue := reflect.ValueOf(value) + nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1) + n += nn; if err != nil { return n, err } + reflectType := reflect.TypeOf(value) + oneTag, err := TagAny(reflect.Zero(reflectType.Elem()).Interface()) + if err != nil { return n, err } + for index := 0; index <= reflectValue.Len(); index += 1 { + item := reflectValue.Index(index).Interface() + itemTag, err := TagAny(item) + if err != nil { return n, err } + if itemTag.CN() > oneTag.CN() { oneTag = itemTag } + } + if oneTag.Is(SBA) { oneTag += 1 << 5 } + nn, err = encoder.WriteUint8(uint8(oneTag)) + n += nn; if err != nil { return n, err } + for index := 0; index <= reflectValue.Len(); index += 1 { + item := reflectValue.Index(index).Interface() + nn, err = EncodeAny(encoder, item, oneTag) + n += nn; if err != nil { return n, err } + } + return n, err +} + +func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) { + // KTV: ( )* + reflectValue := reflect.ValueOf(value) + nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1) + n += nn; if err != nil { return n, err } + iter := reflectValue.MapRange() + for iter.Next() { + key := iter.Key().Interface().(uint16) + value := iter.Value().Interface() + nn, err = encoder.WriteUint16(key) + n += nn; if err != nil { return n, err } + itemTag, err := TagAny(value) + n += nn; if err != nil { return n, err } + nn, err = encoder.WriteUint8(uint8(itemTag)) + n += nn; if err != nil { return n, err } + nn, err = EncodeAny(encoder, value, itemTag) + n += nn; if err != nil { return n, err } + } + return n, nil } -- 2.46.1 From 376a3f1b463b46c2cd2c9441fe412d74e3bdd505 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 20 Jun 2025 18:41:11 -0400 Subject: [PATCH 085/132] generate: Use tape.EncodeAny for encoding undefined tables --- generate/generate.go | 55 ++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 939d399..67328b8 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -146,7 +146,10 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) name) n += nn; if err != nil { return n, err } this.push() - // TODO + nn, err = this.generateDecodeValue(typ, "this", "tag") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf("return n, nil\n") + n += nn; if err != nil { return n, err } this.pop() nn, err = this.iprintf("}\n") n += nn; if err != nil { return n, err } @@ -154,6 +157,8 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) return n, nil } +// generateMessage generates the structure, as well as encoding decoding +// functions for the given message. func (this *Generator) generateMessage(method uint16, message Message) (n int, err error) { nn, err := this.iprintf( "\n// %s represents the protocol message M%04X %s.\n", @@ -196,6 +201,16 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e return n, nil } +// generateEncodeValue generates code to encode a value of a specified type. It +// pulls from the variable (or parenthetical statement) specified by +// valueSource, and the value will be encoded according to the tag stored in +// the variable (or parenthetical statement) specified by tagSource. +// the code generated is a BLOCK and expects these variables to be defined: +// +// - encoder *tape.Encoder +// - n int +// - err error +// - nn int func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) { switch typ := typ.(type) { case TypeInt: @@ -288,31 +303,11 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri case TypeTable: // KTV: ( )* nn, err := this.iprintf( - "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", - tagSource, valueSource) + "nn, err = tape.EncodeAny(encoder, %s, %s)\n", + valueSource, tagSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } - nn, err = this.iprintf("for key, item := range %s {\n", valueSource) - n += nn; if err != nil { return n, err } - this.push() - nn, err = this.iprintf("nn, err = encoder.WriteUint16(key)\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("tag := tape.TagAny(tag)\n") - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(tag))\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - nn, err = this.iprintf("nn, err = tape.EncodeAny(tag)\n") - n += nn; if err != nil { return n, err } - nn, err = this.generateErrorCheck() - n += nn; if err != nil { return n, err } - this.pop() - nn, err = this.iprintf("}\n") - n += nn; if err != nil { return n, err } case TypeTableDefined: // KTV: ( )* nn, err := this.iprintf( @@ -359,6 +354,20 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri return n, nil } +// generateDencodeValue generates code to decode a value of a specified type. It +// overwrites memory pointed to by the variable (or parenthetical statement) +// specified by valueSource, and the value will be encoded according to the tag +// stored in the variable (or parenthetical statement) specified by tagSource. +// the code generated is a BLOCK and expects these variables to be defined: +// +// - decoder *tape.Decoder +// - n int +// - err error +// - nn int +func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource string) (n int, err error) { + // TODO +} + func (this *Generator) generateErrorCheck() (n int, err error) { return this.iprintf("n += nn; if err != nil { return n, err }\n") } -- 2.46.1 From 663cab6b7709de021389bb263fb9ff54865229ec Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 21 Jun 2025 18:33:25 -0400 Subject: [PATCH 086/132] tape: Add float functions to the encoder --- tape/decode.go | 15 +++++++++++++++ tape/encode.go | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/tape/decode.go b/tape/decode.go index 466324b..bcf3fd1 100644 --- a/tape/decode.go +++ b/tape/decode.go @@ -1,6 +1,7 @@ package tape import "io" +import "math" // Decodable is any type that can decode itself from a decoder. type Decodable interface { @@ -125,3 +126,17 @@ func (this *Decoder) ReadGBEU() (value uint64, n int, err error) { } } } + +// ReadFloat32 decldes a 32-bit floating point value from the input reader. +func (this *Decoder) ReadFloat32() (value float32, n int, err error) { + bits, nn, err := this.ReadUint32() + n += nn; if err != nil { return 0, n, err } + return math.Float32frombits(bits), n, nil +} + +// ReadFloat64 decldes a 64-bit floating point value from the input reader. +func (this *Decoder) ReadFloat64() (value float64, n int, err error) { + bits, nn, err := this.ReadUint64() + n += nn; if err != nil { return 0, n, err } + return math.Float64frombits(bits), n, nil +} diff --git a/tape/encode.go b/tape/encode.go index 121f1e9..b84c70f 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -1,6 +1,7 @@ package tape import "io" +import "math" // Encodable is any type that can write itself to an encoder. type Encodable interface { @@ -118,3 +119,13 @@ func (this *Encoder) EncodeGBEU(value uint64) (n int, err error) { return this.Write(buffer[:]) } + +// WriteFloat32 encodes a 32-bit floating point value to the output writer. +func (this *Encoder) WriteFloat32(value float32) (n int, err error) { + return this.WriteUint32(math.Float32bits(value)) +} + +// WriteFloat64 encodes a 64-bit floating point value to the output writer. +func (this *Encoder) WriteFloat64(value float64) (n int, err error) { + return this.WriteUint64(math.Float64bits(value)) +} -- 2.46.1 From 7b8240cec6c0925a98937c8b22cfbede68e17e28 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 21 Jun 2025 19:26:15 -0400 Subject: [PATCH 087/132] tape: Add tag functions to the encoder --- tape/decode.go | 7 +++++++ tape/encode.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/tape/decode.go b/tape/decode.go index bcf3fd1..990418b 100644 --- a/tape/decode.go +++ b/tape/decode.go @@ -140,3 +140,10 @@ func (this *Decoder) ReadFloat64() (value float64, n int, err error) { n += nn; if err != nil { return 0, n, err } return math.Float64frombits(bits), n, nil } + +// ReadTag decodes a [Tag] from the input reader. +func (this *Decoder) ReadTag() (value Tag, n int, err error) { + uncasted, nn, err := this.ReadUint8() + n += nn; if err != nil { return 0, n, err } + return Tag(uncasted), n, nil +} diff --git a/tape/encode.go b/tape/encode.go index b84c70f..878697a 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -129,3 +129,8 @@ func (this *Encoder) WriteFloat32(value float32) (n int, err error) { func (this *Encoder) WriteFloat64(value float64) (n int, err error) { return this.WriteUint64(math.Float64bits(value)) } + +// WriteTag encodes a [Tag] to the output writer. +func (this *Encoder) WriteTag(value Tag) (n int, err error) { + return this.WriteUint8(uint8(value)) +} -- 2.46.1 From 65e8d51590245684b88d8b545434754b89f4ef98 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 21 Jun 2025 19:27:31 -0400 Subject: [PATCH 088/132] tape: Remove GBEU --- tape/decode.go | 17 ----------------- tape/encode.go | 21 --------------------- tape/measure.go | 10 ---------- 3 files changed, 48 deletions(-) diff --git a/tape/decode.go b/tape/decode.go index 990418b..9a2ad58 100644 --- a/tape/decode.go +++ b/tape/decode.go @@ -110,23 +110,6 @@ func (this *Decoder) ReadUintN(bytes int) (value uint64, n int, err error) { return value, n, nil } -// ReadGBEU decodes a growing unsigned integer of up to 64 bits from the input -// reader. -func (this *Decoder) ReadGBEU() (value uint64, n int, err error) { - var fullValue uint64 - for { - chunk, nn, err := this.ReadByte() - n += nn; if err != nil { return 0, n, err } - - fullValue *= 0x80 - fullValue += uint64(chunk & 0x7F) - ccb := chunk >> 7 - if ccb == 0 { - return fullValue, n, nil - } - } -} - // ReadFloat32 decldes a 32-bit floating point value from the input reader. func (this *Decoder) ReadFloat32() (value float32, n int, err error) { bits, nn, err := this.ReadUint32() diff --git a/tape/encode.go b/tape/encode.go index 878697a..8d225f0 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -99,27 +99,6 @@ func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) { return n, nil } -// EncodeGBEU encodes a growing unsigned integer of up to 64 bits to the output -// writer. -func (this *Encoder) EncodeGBEU(value uint64) (n int, err error) { - buffer := [16]byte { } - - window := (GBEUSize(value) - 1) * 7 - index := 0 - for window >= 0 { - chunk := uint8(value >> window) & 0x7F - if window > 0 { - chunk |= 0x80 - } - buffer[index] = chunk - - index += 1 - window -= 7 - } - - return this.Write(buffer[:]) -} - // WriteFloat32 encodes a 32-bit floating point value to the output writer. func (this *Encoder) WriteFloat32(value float32) (n int, err error) { return this.WriteUint32(math.Float32bits(value)) diff --git a/tape/measure.go b/tape/measure.go index ded5153..b57fd9d 100644 --- a/tape/measure.go +++ b/tape/measure.go @@ -1,15 +1,5 @@ package tape -// GBEUSize returns the size (in octets) of a GBEU integer. -func GBEUSize(value uint64) int { - length := 0 - for { - value >>= 7 - length ++ - if value == 0 { return length } - } -} - // IntBytes returns the number of bytes required to hold a given unsigned // integer. func IntBytes(value uint64) int { -- 2.46.1 From 89153dd7bd6c15d75e00c6b7fd0b18f2830a3258 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 21 Jun 2025 19:27:58 -0400 Subject: [PATCH 089/132] tape: Add Tag.WithoutCN for easier comparison --- tape/tag.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tape/tag.go b/tape/tag.go index a2a9fd4..4e9db37 100644 --- a/tape/tag.go +++ b/tape/tag.go @@ -25,6 +25,10 @@ func (tag Tag) WithCN(cn int) Tag { return (tag & TNMask) | Tag(cn % 32) } +func (tag Tag) WithoutCN() Tag { + return tag.WithCN(0) +} + func (tag Tag) Is(other Tag) bool { return tag.TN() == other.TN() } -- 2.46.1 From e3487d26a181e4b081efaf941f0d549be15be0cb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 14:37:06 -0400 Subject: [PATCH 090/132] internal/testutil: Add test utility package --- internal/testutil/testutil.go | 86 ++++++++++++++++++++++++++++++ internal/testutil/testutil_test.go | 64 ++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 internal/testutil/testutil.go create mode 100644 internal/testutil/testutil_test.go diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go new file mode 100644 index 0000000..e6602a6 --- /dev/null +++ b/internal/testutil/testutil.go @@ -0,0 +1,86 @@ +package testutil + +import "fmt" +import "slices" +import "strings" + +// Snake lets you compare blocks of data where the ordering of certain parts may +// be swapped every which way. It is designed for comparing the encoding of +// maps where the ordering of individual elements is inconsistent. +// +// The snake is divided into sectors, which hold a number of variations. For a +// sector to be satisfied by some data, some ordering of it must match the data +// exactly. for the snake to be satisfied by some data, its sectors must match +// the data in order, but the internal ordering of each sector doesn't matter. +type Snake [] [] []byte +// snake sector variation + +// S returns a new snake. +func S(data ...byte) Snake { + return (Snake { }).Add(data...) +} + +// AddVar returns a new snake with the given sector added on to it. Successive +// calls of this method can be chained together to create a big ass snake. +func (sn Snake) AddVar(sector ...[]byte) Snake { + slice := make(Snake, len(sn) + 1) + copy(slice, sn) + slice[len(slice) - 1] = sector + return slice +} + +// Add is like AddVar, but adds a sector with only one variation, which means it +// does not vary, hence why the method is called that. +func (sn Snake) Add(data ...byte) Snake { + return sn.AddVar(data) +} + +// Check determines if the data satisfies the snake. +func (sn Snake) Check(data []byte) (ok bool, n int) { + left := data + variations := map[int] []byte { } + for _, sector := range sn { + clear(variations) + for key, variation := range sector { + variations[key] = variation + } + for len(variations) > 0 { + found := false + for key, variation := range variations { + if len(left) < len(variation) { continue } + if !slices.Equal(left[:len(variation)], variation) { continue } + n += len(variation) + left = data[n:] + delete(variations, key) + found = true + } + if !found { return false, n } + } + } + return true, n +} + +func (sn Snake) String() string { + out := strings.Builder { } + for index, sector := range sn { + if index > 0 { out.WriteString(" : ") } + out.WriteRune('[') + for index, variation := range sector { + if index > 0 { out.WriteString(" / ") } + for _, byt := range variation { + fmt.Fprintf(&out, "%02x", byt) + } + } + out.WriteRune(']') + } + return out.String() +} + +// HexBytes formats bytes into a hexadecimal string. +func HexBytes(data []byte) string { + out := strings.Builder { } + for _, byt := range data { + fmt.Fprintf(&out, "%02x", byt) + } + return out.String() +} diff --git a/internal/testutil/testutil_test.go b/internal/testutil/testutil_test.go new file mode 100644 index 0000000..e8fa5e0 --- /dev/null +++ b/internal/testutil/testutil_test.go @@ -0,0 +1,64 @@ +package testutil + +import "testing" + +func TestSnakeA(test *testing.T) { + snake := S(1, 6).AddVar( + []byte { 1 }, + []byte { 2 }, + []byte { 3 }, + []byte { 4 }, + []byte { 5 }, + ).Add(9) + + test.Log(snake) + + ok, n := snake.Check([]byte { 1, 6, 1, 2, 3, 4, 5, 9 }) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 5, 4, 3, 2, 1, 9 }) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 3, 1, 4, 2, 5, 9 }) + if !ok { test.Fatal("false negative:", n) } + + ok, n = snake.Check([]byte { 1, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 3, 4, 5, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 0, 2, 3, 4, 5, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 }) + if ok { test.Fatal("false positive:", n) } +} + +func TestSnakeB(test *testing.T) { + snake := S(1, 6).AddVar( + []byte { 1 }, + []byte { 2 }, + ).Add(9).AddVar( + []byte { 3, 2 }, + []byte { 0 }, + []byte { 1, 1, 2, 3 }, + ) + + test.Log(snake) + + ok, n := snake.Check([]byte { 1, 6, 1, 2, 9, 3, 2, 0, 1, 1, 2, 3}) + if !ok { test.Fatal("false negative:", n) } + ok, n = snake.Check([]byte { 1, 6, 2, 1, 9, 0, 1, 1, 2, 3, 3, 2}) + if !ok { test.Fatal("false negative:", n) } + + ok, n = snake.Check([]byte { 1, 6, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 9 }) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 9, 3, 2, 0, 1, 1, 2, 3}) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 2, 9, 0, 1, 1, 2, 3, 3, 2}) + if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 9, 3, 2, 1, 1, 2, 3}) + if ok { test.Fatal("false positive:", n) } +} -- 2.46.1 From 477e56d3592fd0e81fccb3d8b65e409cfa539f95 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 14:37:45 -0400 Subject: [PATCH 091/132] tape: Add String method to Tag --- tape/tag.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tape/tag.go b/tape/tag.go index 4e9db37..b32898d 100644 --- a/tape/tag.go +++ b/tape/tag.go @@ -1,16 +1,18 @@ package tape +import "fmt" + type Tag byte; const ( - SI Tag = 0 << 5 // Small integer - LI Tag = 1 << 5 // Large integer - FP Tag = 2 << 5 // Floating point - SBA Tag = 3 << 5 // Small byte array - LBA Tag = 4 << 5 // Large byte array - OTA Tag = 5 << 5 // One-tag array - KTV Tag = 6 << 5 // Key-tag-value table - TNMask Tag = 0xE0 // The entire TN bitfield - CNMask Tag = 0x20 // The entire CN bitfield - CNLimit Tag = 32 // All valid CNs are < CNLimit + SI Tag = 0 << 5 // Small integer + LI Tag = 1 << 5 // Large integer + FP Tag = 2 << 5 // Floating point + SBA Tag = 3 << 5 // Small byte array + LBA Tag = 4 << 5 // Large byte array + OTA Tag = 5 << 5 // One-tag array + KTV Tag = 6 << 5 // Key-tag-value table + TNMask Tag = 0xE0 // The entire TN bitfield + CNMask Tag = 0x1F // The entire CN bitfield + CNLimit Tag = 32 // All valid CNs are < CNLimit ) func (tag Tag) TN() int { @@ -33,6 +35,20 @@ func (tag Tag) Is(other Tag) bool { return tag.TN() == other.TN() } +func (tag Tag) String() string { + tn := fmt.Sprint(tag.TN()) + switch tag.WithoutCN() { + case SI: tn = "SI" + case LI: tn = "LI" + case FP: tn = "FP" + case SBA: tn = "SBA" + case LBA: tn = "LBA" + case OTA: tn = "OTA" + case KTV: tn = "KTV" + } + return fmt.Sprintf("%s:%d", tn, tag.CN()) +} + // BufferTag returns the appropriate tag for a buffer. func BufferTag(value []byte) Tag { return bufferLenTag(len(value)) -- 2.46.1 From 1bc0788ff2243ae20cbccce44d66904cf297f936 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 14:38:01 -0400 Subject: [PATCH 092/132] tape: Fix Encoder.WriteUintN not using the value AT ALL! --- tape/encode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tape/encode.go b/tape/encode.go index 8d225f0..7ff0fc5 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -92,7 +92,7 @@ func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) { // TODO: don't make multiple write calls (without allocating) buffer := [1]byte { } for bytesLeft := bytes; bytesLeft > 0; bytesLeft -- { - buffer[0] = byte(buffer[0]) >> ((bytesLeft - 1) * 8) + buffer[0] = byte(value) >> ((bytesLeft - 1) * 8) nn, err := this.Write(buffer[:]) n += nn; if err != nil { return n, err } } -- 2.46.1 From 9932abd6c490321a13405c8af11147686b1ac57c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 14:39:16 -0400 Subject: [PATCH 093/132] tape: Implement dynamic decoding (untested) --- tape/dynamic.go | 237 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 231 insertions(+), 6 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 6dc0380..67d1a88 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -1,5 +1,11 @@ package tape +// dont smoke reflection, kids!!!!!!!!! +// totally reflectric, reflectrified, etc. this is probably souper slow but +// certainly no slower than the built in json encoder i'd imagine. +// TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get +// transformed into tables with a defined schema + import "fmt" import "reflect" @@ -14,6 +20,7 @@ import "reflect" // - [] // - map[uint16] func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { + // TODO use reflection for all of this to ignore type names // primitives switch value := value.(type) { case int: return encoder.WriteInt32(int32(value)) @@ -41,21 +48,128 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { switch reflectType.Kind() { case reflect.Slice: return encodeAnySlice(encoder, value, tag) - case reflect.Array: - return encodeAnySlice(encoder, reflect.ValueOf(value).Slice(0, reflectType.Len()).Interface(), tag) + // case reflect.Array: + // return encodeAnySlice(encoder, reflect.ValueOf(value).Slice(0, reflectType.Len()).Interface(), tag) case reflect.Map: if reflectType.Key() == reflect.TypeOf(uint16(0)) { return encodeAnyMap(encoder, value, tag) } - return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) + return n, fmt.Errorf("cannot encode map key %T, key must be uint16", value) } - return 0, fmt.Errorf("cannot encode type %T", value) + return n, fmt.Errorf("cannot encode type %T", value) +} + +// DecodeAny decodes data and places it into destination, which must be a +// pointer to a supported type. See [EncodeAny] for a list of supported types. +func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) { + return decodeAny(decoder, reflect.ValueOf(destination), tag) +} + +func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) { + errWrongDestinationType := func(expected string) error { + return fmt.Errorf( + "expected %s destination, not %v", + expected, destination) + } + + if destination.Kind() != reflect.Pointer { + return n, errWrongDestinationType("pointer") + } + + switch tag.WithoutCN() { + case SI: + // SI: (none) + err = setIntPtr(destination, uint64(tag.CN())) + if err != nil { return n, err } + case LI: + // LI: + nn, err := decodeAndSetIntPtr(decoder, destination, tag.CN() - 1) + n += nn; if err != nil { return n, err } + case FP: + // FP: + nn, err := decodeAndSetFloatPtr(decoder, destination, tag.CN() - 1) + n += nn; if err != nil { return n, err } + case SBA: + // SBA: * + destination, err := asByteArrayPtr(destination) + if err != nil { return n, err } + buffer := make([]byte, tag.CN()) + nn, err := decoder.Read(buffer) + n += nn; if err != nil { return n, err } + *destination = buffer + case LBA: + // LBA: * + destination, err := asByteArrayPtr(destination) + if err != nil { return n, err } + length, nn, err := decoder.ReadUintN(tag.CN() - 1) + n += nn; if err != nil { return n, err } + buffer := make([]byte, length) + nn, err = decoder.Read(buffer) + n += nn; if err != nil { return n, err } + *destination = buffer + case OTA: + // OTA: * + length, nn, err := decoder.ReadUintN(tag.CN() - 1) + n += nn; if err != nil { return n, err } + oneTag, nn, err := decoder.ReadTag() + n += nn; if err != nil { return n, err } + var slice reflect.Value + needSet := false + elem := destination.Elem() + if elem.Kind() == reflect.Struct && elem.Type().Name() == "unknownSlicePlaceholder" { + needSet = true + slice, err = skeletonValueSlice(oneTag, int(length)) + if err != nil { return n, err } + } else { + slice = elem + if slice.Kind() != reflect.Slice { + return n, errWrongDestinationType("slice") + } + slice.SetLen(int(length)) + } + for index := range length { + nn, err := decodeAny(decoder, slice.Index(int(index)), oneTag) + n += nn; if err != nil { return n, err } + } + if needSet { + destination.Elem().Set(slice) + } + case KTV: + // KTV: ( )* + table := destination.Elem() + if table.Kind() != reflect.Map { + return n, errWrongDestinationType("map") + } + typ := table.Type() + if typ.Key().Kind() != reflect.Uint16 { + return n, errWrongDestinationType("map[uint16]") + } + if typ.Elem() != reflect.TypeOf(any(nil)) { + return n, errWrongDestinationType("map[uint16] any") + } + length, nn, err := decoder.ReadUintN(tag.CN() - 1) + n += nn; if err != nil { return n, err } + table.Clear() + for _ = range length { + key, nn, err := decoder.ReadUint16() + n += nn; if err != nil { return n, err } + itemTag, nn, err := decoder.ReadTag() + n += nn; if err != nil { return n, err } + value, err := skeletonValue(itemTag) + if err != nil { return n, err } + nn, err = decodeAny(decoder, value.Elem(), itemTag) + n += nn; if err != nil { return n, err } + table.SetMapIndex(reflect.ValueOf(key), value) + } + } + return n, fmt.Errorf("unknown TN %d", tag.TN()) } // TagAny returns the correct tag for an "any" value. Returns an error if the // underlying type is unsupported. See [EncodeAny] for a list of supported // types. func TagAny(value any) (Tag, error) { + // TODO use reflection for all of this to ignore type names // primitives switch value := value.(type) { case int, uint: return LI.WithCN(3), nil @@ -70,11 +184,11 @@ func TagAny(value any) (Tag, error) { // aggregates reflectType := reflect.TypeOf(value) switch reflectType.Kind() { - case reflect.Slice: return OTA.WithCN(reflect.ValueOf(value).Len()), nil + case reflect.Slice: return OTA.WithCN(reflect.ValueOf(value).Len() - 1), nil case reflect.Array: return OTA.WithCN(reflectType.Len()), nil case reflect.Map: if reflectType.Key() == reflect.TypeOf(uint16(0)) { - return OTA.WithCN(reflect.ValueOf(value).Len()), nil + return KTV.WithCN(IntBytes(uint64(reflect.ValueOf(value).Len())) - 1), nil } return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value) } @@ -126,3 +240,114 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) { } return n, nil } + +func setIntPtr(destination reflect.Value, value uint64) error { + elem := destination.Elem() + if !elem.CanInt() { + return fmt.Errorf("cannot assign integer to %T", elem.Interface()) + } + elem.Set(reflect.ValueOf(value).Convert(elem.Type())) + return nil +} + +func setFloatPtr(destination reflect.Value, value float64) error { + elem := destination.Elem() + if !elem.CanFloat() { + return fmt.Errorf("cannot assign float to %T", elem.Interface()) + } + elem.Set(reflect.ValueOf(value).Convert(elem.Type())) + return nil +} + +func decodeAndSetIntPtr(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { + value, nn, err := decoder.ReadUintN(bytes) + n += nn; if err != nil { return n, err } + return n, setIntPtr(destination, value) +} + +func decodeAndSetFloatPtr(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { + switch bytes { + case 8: + value, nn, err := decoder.ReadFloat64() + n += nn; if err != nil { return n, err } + return n, setFloatPtr(destination, float64(value)) + case 4: + value, nn, err := decoder.ReadFloat32() + n += nn; if err != nil { return n, err } + return n, setFloatPtr(destination, float64(value)) + } + return n, fmt.Errorf("cannot decode float%d", bytes * 8) +} + +func asByteArrayPtr(value reflect.Value) (*[]byte, error) { + typ := value.Type() + if typ.Kind() != reflect.Pointer { + return nil, fmt.Errorf("cannot convert %T to pointer", value) + } + if typ.Elem().Kind() != reflect.Slice { + return nil, fmt.Errorf("cannot convert %T to slice pointer", value) + } + if typ.Elem().Elem() != reflect.TypeOf(byte(0)) { + return nil, fmt.Errorf("cannot convert %T to *[]byte", value) + } + return value.Convert(reflect.PtrTo(reflect.SliceOf(reflect.TypeOf(byte(0))))).Interface().(*[]byte), nil +} + +func skeletonValue(tag Tag) (reflect.Value, error) { + switch tag.WithoutCN() { + case SI: + value := uint8(0) + return reflect.ValueOf(&value), nil + case LI: + switch tag.CN() { + case 0: value := uint8(0); return reflect.ValueOf(&value), nil + case 1: value := uint16(0); return reflect.ValueOf(&value), nil + case 3: value := uint32(0); return reflect.ValueOf(&value), nil + case 7: value := uint64(0); return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown CN %d for LI", tag.CN()) + case FP: + switch tag.CN() { + case 3: value := float32(0); return reflect.ValueOf(&value), nil + case 7: value := float64(0); return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown CN %d for FP", tag.CN()) + case SBA: value := []byte { }; return reflect.ValueOf(&value), nil + case LBA: value := []byte { }; return reflect.ValueOf(&value), nil + case OTA: value := unknownSlicePlaceholder { }; return reflect.ValueOf(&value), nil + case KTV: value := map[uint16] any { }; return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) +} + +func skeletonValueSlice(tag Tag, length int) (reflect.Value, error) { + switch tag.WithoutCN() { + case SI: + value := make([]uint8, length) + return reflect.ValueOf(&value), nil + case LI: + switch tag.CN() { + case 0: value := make([]uint8, length); return reflect.ValueOf(&value), nil + case 1: value := make([]uint16, length); return reflect.ValueOf(&value), nil + case 3: value := make([]uint32, length); return reflect.ValueOf(&value), nil + case 7: value := make([]uint64, length); return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown CN %d for LI OTA", tag.CN()) + case FP: + switch tag.CN() { + case 3: value := make([]float32, length); return reflect.ValueOf(&value), nil + case 7: value := make([]float64, length); return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown CN %d for FP OTA", tag.CN()) + case SBA: value := make([][]byte, length); return reflect.ValueOf(&value), nil + case LBA: value := make([][]byte, length); return reflect.ValueOf(&value), nil + case OTA: value := make([]any, length); return reflect.ValueOf(&value), nil + case KTV: value := make([]map[uint16] any, length); return reflect.ValueOf(&value), nil + } + return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) +} + +// unknownSlicePlaceholder is inserted by skeletonValue and informs the program +// that the destination for the slice needs to be generated based on the item +// tag in the OTA. +type unknownSlicePlaceholder struct { } -- 2.46.1 From 604faf0995c330b3b5d457dd19f26b55dd99ec56 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 14:43:03 -0400 Subject: [PATCH 094/132] tape: Fix comment --- tape/encode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tape/encode.go b/tape/encode.go index 7ff0fc5..19f9379 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -82,7 +82,7 @@ func (this *Encoder) WriteIntN(value int64, bytes int) (n int, err error) { return this.WriteUintN(uint64(value), bytes) } -// for below functions, increase buffers if go somehow gets support for over 64 +// for Write/ReadUintN, increase buffers if go somehow gets support for over 64 // bit integers. we could also make an expanding int type in goutil to use here, // or maybe there is one in the stdlib. keep the int64 versions as well though // because its ergonomic. -- 2.46.1 From 712b4f521cb81b60609f048d5d071b96bbf87cac Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 15:00:20 -0400 Subject: [PATCH 095/132] internal/testutil: Fix Snake giving false positives for long data --- internal/testutil/testutil.go | 3 +++ internal/testutil/testutil_test.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index e6602a6..2bba58e 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -57,6 +57,9 @@ func (sn Snake) Check(data []byte) (ok bool, n int) { if !found { return false, n } } } + if n < len(data) { + return false, n + } return true, n } diff --git a/internal/testutil/testutil_test.go b/internal/testutil/testutil_test.go index e8fa5e0..663831a 100644 --- a/internal/testutil/testutil_test.go +++ b/internal/testutil/testutil_test.go @@ -32,6 +32,8 @@ func TestSnakeA(test *testing.T) { if ok { test.Fatal("false positive:", n) } ok, n = snake.Check([]byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 }) if ok { test.Fatal("false positive:", n) } + ok, n = snake.Check([]byte { 1, 6, 1, 2, 3, 4, 5, 9, 10}) + if ok { test.Fatal("false positive:", n) } } func TestSnakeB(test *testing.T) { -- 2.46.1 From e16fec3a81e77f3ab9f293cf72a4451bb4f11cb9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 24 Jun 2025 16:08:35 -0400 Subject: [PATCH 096/132] tape: Fixes to dynamic encoding --- tape/dynamic.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 67d1a88..e2b1b86 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -41,6 +41,7 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { } nn, err := encoder.Write(value) n += nn; if err != nil { return n, err } + return n, nil } // aggregates @@ -184,7 +185,7 @@ func TagAny(value any) (Tag, error) { // aggregates reflectType := reflect.TypeOf(value) switch reflectType.Kind() { - case reflect.Slice: return OTA.WithCN(reflect.ValueOf(value).Len() - 1), nil + case reflect.Slice: return OTA.WithCN(IntBytes(uint64(reflect.ValueOf(value).Len())) - 1), nil case reflect.Array: return OTA.WithCN(reflectType.Len()), nil case reflect.Map: if reflectType.Key() == reflect.TypeOf(uint16(0)) { @@ -203,7 +204,7 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) { reflectType := reflect.TypeOf(value) oneTag, err := TagAny(reflect.Zero(reflectType.Elem()).Interface()) if err != nil { return n, err } - for index := 0; index <= reflectValue.Len(); index += 1 { + for index := 0; index < reflectValue.Len(); index += 1 { item := reflectValue.Index(index).Interface() itemTag, err := TagAny(item) if err != nil { return n, err } @@ -212,7 +213,7 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) { if oneTag.Is(SBA) { oneTag += 1 << 5 } nn, err = encoder.WriteUint8(uint8(oneTag)) n += nn; if err != nil { return n, err } - for index := 0; index <= reflectValue.Len(); index += 1 { + for index := 0; index < reflectValue.Len(); index += 1 { item := reflectValue.Index(index).Interface() nn, err = EncodeAny(encoder, item, oneTag) n += nn; if err != nil { return n, err } @@ -232,7 +233,7 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) { nn, err = encoder.WriteUint16(key) n += nn; if err != nil { return n, err } itemTag, err := TagAny(value) - n += nn; if err != nil { return n, err } + if err != nil { return n, err } nn, err = encoder.WriteUint8(uint8(itemTag)) n += nn; if err != nil { return n, err } nn, err = EncodeAny(encoder, value, itemTag) -- 2.46.1 From b174015319c4bd1040518231ba54f6996ffbbb92 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 27 Jun 2025 14:02:38 -0400 Subject: [PATCH 097/132] tape: Fix KTV decoding not recognizing the `any` type --- tape/dynamic.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index e2b1b86..c0e5301 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -69,7 +69,7 @@ func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) { func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) { errWrongDestinationType := func(expected string) error { return fmt.Errorf( - "expected %s destination, not %v", + "expected &%s destination, not %v", expected, destination) } @@ -138,14 +138,8 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err case KTV: // KTV: ( )* table := destination.Elem() - if table.Kind() != reflect.Map { - return n, errWrongDestinationType("map") - } - typ := table.Type() - if typ.Key().Kind() != reflect.Uint16 { - return n, errWrongDestinationType("map[uint16]") - } - if typ.Elem() != reflect.TypeOf(any(nil)) { + var dummyMap map[uint16] any + if table.Type() != reflect.TypeOf(dummyMap) { return n, errWrongDestinationType("map[uint16] any") } length, nn, err := decoder.ReadUintN(tag.CN() - 1) -- 2.46.1 From aa718cfe9f9d814a4a06d158969d6fc79e27ccbb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 27 Jun 2025 14:03:49 -0400 Subject: [PATCH 098/132] tape: DecodeAny only returns an error when there is one --- tape/dynamic.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index c0e5301..4c03dae 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -156,8 +156,10 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } table.SetMapIndex(reflect.ValueOf(key), value) } + default: + return n, fmt.Errorf("unknown TN %d", tag.TN()) } - return n, fmt.Errorf("unknown TN %d", tag.TN()) + return n, nil } // TagAny returns the correct tag for an "any" value. Returns an error if the -- 2.46.1 From 2a4e88d949b9a4637e41c2f5cd0027d1f9214f95 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 27 Jun 2025 17:04:20 -0400 Subject: [PATCH 099/132] tape: Fix size decoding math --- tape/dynamic.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 4c03dae..2c53dba 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -68,9 +68,11 @@ func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) { func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) { errWrongDestinationType := func(expected string) error { + // panic(fmt.Errorf( return fmt.Errorf( - "expected &%s destination, not %v", + "expected %s destination, not %v", expected, destination) + //) } if destination.Kind() != reflect.Pointer { @@ -84,11 +86,11 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err if err != nil { return n, err } case LI: // LI: - nn, err := decodeAndSetIntPtr(decoder, destination, tag.CN() - 1) + nn, err := decodeAndSetIntPtr(decoder, destination, tag.CN() + 1) n += nn; if err != nil { return n, err } case FP: // FP: - nn, err := decodeAndSetFloatPtr(decoder, destination, tag.CN() - 1) + nn, err := decodeAndSetFloatPtr(decoder, destination, tag.CN() + 1) n += nn; if err != nil { return n, err } case SBA: // SBA: * @@ -102,7 +104,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err // LBA: * destination, err := asByteArrayPtr(destination) if err != nil { return n, err } - length, nn, err := decoder.ReadUintN(tag.CN() - 1) + length, nn, err := decoder.ReadUintN(tag.CN() + 1) n += nn; if err != nil { return n, err } buffer := make([]byte, length) nn, err = decoder.Read(buffer) @@ -110,7 +112,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err *destination = buffer case OTA: // OTA: * - length, nn, err := decoder.ReadUintN(tag.CN() - 1) + length, nn, err := decoder.ReadUintN(tag.CN() + 1) n += nn; if err != nil { return n, err } oneTag, nn, err := decoder.ReadTag() n += nn; if err != nil { return n, err } @@ -142,7 +144,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err if table.Type() != reflect.TypeOf(dummyMap) { return n, errWrongDestinationType("map[uint16] any") } - length, nn, err := decoder.ReadUintN(tag.CN() - 1) + length, nn, err := decoder.ReadUintN(tag.CN() + 1) n += nn; if err != nil { return n, err } table.Clear() for _ = range length { @@ -152,7 +154,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } value, err := skeletonValue(itemTag) if err != nil { return n, err } - nn, err = decodeAny(decoder, value.Elem(), itemTag) + nn, err = decodeAny(decoder, value, itemTag) n += nn; if err != nil { return n, err } table.SetMapIndex(reflect.ValueOf(key), value) } -- 2.46.1 From 3eb826735bd51ed17bfacec0e7907689d55a2a24 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 27 Jun 2025 19:05:17 -0400 Subject: [PATCH 100/132] tape: Send reflect values where possible instead of pointers --- tape/dynamic.go | 125 ++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 2c53dba..6cd9faf 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -63,53 +63,58 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { // DecodeAny decodes data and places it into destination, which must be a // pointer to a supported type. See [EncodeAny] for a list of supported types. func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) { - return decodeAny(decoder, reflect.ValueOf(destination), tag) + reflectDestination := reflect.ValueOf(destination) + if reflectDestination.Kind() != reflect.Pointer { + return n, fmt.Errorf("expected pointer destination, not %v", destination) + } + return decodeAny(decoder, reflectDestination.Elem(), tag) } +// unknownSlicePlaceholder is inserted by skeletonValue and informs the program +// that the destination for the slice needs to be generated based on the item +// tag in the OTA. +type unknownSlicePlaceholder struct { } +var unknownSlicePlaceholderType = reflect.TypeOf(unknownSlicePlaceholder { }) + +// decodeAny is internal to [DecodeAny]. It takes in an addressable +// [reflect.Value] as the destination. func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) { errWrongDestinationType := func(expected string) error { - // panic(fmt.Errorf( - return fmt.Errorf( + panic(fmt.Errorf( + // return fmt.Errorf( "expected %s destination, not %v", - expected, destination) - //) - } - - if destination.Kind() != reflect.Pointer { - return n, errWrongDestinationType("pointer") + expected, destination)) } switch tag.WithoutCN() { case SI: // SI: (none) - err = setIntPtr(destination, uint64(tag.CN())) + err = setInt(destination, uint64(tag.CN())) if err != nil { return n, err } case LI: // LI: - nn, err := decodeAndSetIntPtr(decoder, destination, tag.CN() + 1) + nn, err := decodeAndSetInt(decoder, destination, tag.CN() + 1) n += nn; if err != nil { return n, err } case FP: // FP: - nn, err := decodeAndSetFloatPtr(decoder, destination, tag.CN() + 1) + nn, err := decodeAndSetFloat(decoder, destination, tag.CN() + 1) n += nn; if err != nil { return n, err } case SBA: // SBA: * - destination, err := asByteArrayPtr(destination) - if err != nil { return n, err } buffer := make([]byte, tag.CN()) nn, err := decoder.Read(buffer) n += nn; if err != nil { return n, err } - *destination = buffer + err = setByteArray(destination, buffer) + if err != nil { return n, err } case LBA: // LBA: * - destination, err := asByteArrayPtr(destination) - if err != nil { return n, err } length, nn, err := decoder.ReadUintN(tag.CN() + 1) n += nn; if err != nil { return n, err } buffer := make([]byte, length) nn, err = decoder.Read(buffer) n += nn; if err != nil { return n, err } - *destination = buffer + err = setByteArray(destination, buffer) + if err != nil { return n, err } case OTA: // OTA: * length, nn, err := decoder.ReadUintN(tag.CN() + 1) @@ -118,13 +123,13 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } var slice reflect.Value needSet := false - elem := destination.Elem() - if elem.Kind() == reflect.Struct && elem.Type().Name() == "unknownSlicePlaceholder" { + if destination.Kind() == reflect.Struct && destination.Type() == unknownSlicePlaceholderType { needSet = true slice, err = skeletonValueSlice(oneTag, int(length)) if err != nil { return n, err } + slice = slice.Elem() } else { - slice = elem + slice = destination if slice.Kind() != reflect.Slice { return n, errWrongDestinationType("slice") } @@ -135,11 +140,11 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } } if needSet { - destination.Elem().Set(slice) + destination.Set(slice) } case KTV: // KTV: ( )* - table := destination.Elem() + table := destination var dummyMap map[uint16] any if table.Type() != reflect.TypeOf(dummyMap) { return n, errWrongDestinationType("map[uint16] any") @@ -154,7 +159,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } value, err := skeletonValue(itemTag) if err != nil { return n, err } - nn, err = decodeAny(decoder, value, itemTag) + nn, err = decodeAny(decoder, value.Elem(), itemTag) n += nn; if err != nil { return n, err } table.SetMapIndex(reflect.ValueOf(key), value) } @@ -240,58 +245,65 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) { return n, nil } -func setIntPtr(destination reflect.Value, value uint64) error { - elem := destination.Elem() - if !elem.CanInt() { - return fmt.Errorf("cannot assign integer to %T", elem.Interface()) +// setInt expects a settable destination. +func setInt(destination reflect.Value, value uint64) error { + switch { + case destination.CanInt(): + destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type())) + case destination.CanUint(): + destination.Set(reflect.ValueOf(value).Convert(destination.Type())) + default: + return fmt.Errorf("cannot assign integer to %T", destination.Interface()) } - elem.Set(reflect.ValueOf(value).Convert(elem.Type())) return nil } -func setFloatPtr(destination reflect.Value, value float64) error { - elem := destination.Elem() - if !elem.CanFloat() { - return fmt.Errorf("cannot assign float to %T", elem.Interface()) +// setInt expects a settable destination. +func setFloat(destination reflect.Value, value float64) error { + if !destination.CanFloat() { + return fmt.Errorf("cannot assign float to %T", destination.Interface()) } - elem.Set(reflect.ValueOf(value).Convert(elem.Type())) + destination.Set(reflect.ValueOf(value).Convert(destination.Type())) return nil } -func decodeAndSetIntPtr(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { +// setByteArrayexpects a settable destination. +func setByteArray(destination reflect.Value, value []byte) error { + typ := destination.Type() + if typ.Kind() != reflect.Slice { + return fmt.Errorf("cannot assign %T to ", value) + } + if typ.Elem() != reflect.TypeOf(byte(0)) { + return fmt.Errorf("cannot convert %T to *[]byte", value) + } + destination.Set(reflect.ValueOf(value)) + return nil +} + +// decodeAndSetInt expects a settable destination. +func decodeAndSetInt(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { value, nn, err := decoder.ReadUintN(bytes) n += nn; if err != nil { return n, err } - return n, setIntPtr(destination, value) + return n, setInt(destination, value) } -func decodeAndSetFloatPtr(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { +// decodeAndSetInt expects a settable destination. +func decodeAndSetFloat(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) { switch bytes { case 8: value, nn, err := decoder.ReadFloat64() n += nn; if err != nil { return n, err } - return n, setFloatPtr(destination, float64(value)) + return n, setFloat(destination, float64(value)) case 4: value, nn, err := decoder.ReadFloat32() n += nn; if err != nil { return n, err } - return n, setFloatPtr(destination, float64(value)) + return n, setFloat(destination, float64(value)) } return n, fmt.Errorf("cannot decode float%d", bytes * 8) } -func asByteArrayPtr(value reflect.Value) (*[]byte, error) { - typ := value.Type() - if typ.Kind() != reflect.Pointer { - return nil, fmt.Errorf("cannot convert %T to pointer", value) - } - if typ.Elem().Kind() != reflect.Slice { - return nil, fmt.Errorf("cannot convert %T to slice pointer", value) - } - if typ.Elem().Elem() != reflect.TypeOf(byte(0)) { - return nil, fmt.Errorf("cannot convert %T to *[]byte", value) - } - return value.Convert(reflect.PtrTo(reflect.SliceOf(reflect.TypeOf(byte(0))))).Interface().(*[]byte), nil -} - +// skeletonValue returns a pointer value. In order for it to be set, it must be +// dereferenced using Elem(). func skeletonValue(tag Tag) (reflect.Value, error) { switch tag.WithoutCN() { case SI: @@ -319,6 +331,8 @@ func skeletonValue(tag Tag) (reflect.Value, error) { return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) } +// skeletonValueSlice returns a pointer value. In order for it to be set, it +// must be dereferenced using Elem(). func skeletonValueSlice(tag Tag, length int) (reflect.Value, error) { switch tag.WithoutCN() { case SI: @@ -345,8 +359,3 @@ func skeletonValueSlice(tag Tag, length int) (reflect.Value, error) { } return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) } - -// unknownSlicePlaceholder is inserted by skeletonValue and informs the program -// that the destination for the slice needs to be generated based on the item -// tag in the OTA. -type unknownSlicePlaceholder struct { } -- 2.46.1 From 08fe3d45ddd856ab9a2646b23bbdce8d5be660ed Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 28 Jun 2025 06:23:18 -0400 Subject: [PATCH 101/132] tape: Encoder inherits bufio.Writer, need to do same for decoder --- tape/encode.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tape/encode.go b/tape/encode.go index 19f9379..24f12e4 100644 --- a/tape/encode.go +++ b/tape/encode.go @@ -2,6 +2,7 @@ package tape import "io" import "math" +import "bufio" // Encodable is any type that can write itself to an encoder. type Encodable interface { @@ -10,14 +11,16 @@ type Encodable interface { Encode(encoder *Encoder) (n int, err error) } -// Encoder wraps an [io.Writer] and encodes data to it. +// Encoder encodes data to an io.Writer. type Encoder struct { - io.Writer + bufio.Writer } -// WriteByte encodes a single byte to the output writer. -func (this *Encoder) WriteByte(value byte) (n int, err error) { - return this.WriteByte(uint8(value)) +// NewEncoder creates a new encoder that writes to writer. +func NewEncoder(writer io.Writer) *Encoder { + encoder := &Encoder { } + encoder.Reset(writer) + return encoder } // WriteInt8 encodes an 8-bit signed integer to the output writer. -- 2.46.1 From 37eccc91c02bf1cb8c6162cd56dacbefbccb7bd1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 28 Jun 2025 06:23:51 -0400 Subject: [PATCH 102/132] tape: Progress on dynamically decoding OTAs --- tape/dynamic.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 6cd9faf..7e7333b 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -157,7 +157,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } itemTag, nn, err := decoder.ReadTag() n += nn; if err != nil { return n, err } - value, err := skeletonValue(itemTag) + value, err := skeletonValue(decoder, itemTag) if err != nil { return n, err } nn, err = decodeAny(decoder, value.Elem(), itemTag) n += nn; if err != nil { return n, err } @@ -304,7 +304,7 @@ func decodeAndSetFloat(decoder *Decoder, destination reflect.Value, bytes int) ( // skeletonValue returns a pointer value. In order for it to be set, it must be // dereferenced using Elem(). -func skeletonValue(tag Tag) (reflect.Value, error) { +func skeletonValue(decoder *Decoder, tag Tag) (reflect.Value, error) { switch tag.WithoutCN() { case SI: value := uint8(0) @@ -325,12 +325,20 @@ func skeletonValue(tag Tag) (reflect.Value, error) { return reflect.Value { }, fmt.Errorf("unknown CN %d for FP", tag.CN()) case SBA: value := []byte { }; return reflect.ValueOf(&value), nil case LBA: value := []byte { }; return reflect.ValueOf(&value), nil - case OTA: value := unknownSlicePlaceholder { }; return reflect.ValueOf(&value), nil + case OTA: return skeletonValueSlice(decoder, tag) case KTV: value := map[uint16] any { }; return reflect.ValueOf(&value), nil } return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) } +// skeletonValueSlice returns a pointer value. In order for it to be set, it +// must be dereferenced using Elem(). +func skeletonValueSlice(decoder *Decoder, tag Tag) (reflect.Value, error) { + // TODO +} + +// TODO: delete fucntion below + // skeletonValueSlice returns a pointer value. In order for it to be set, it // must be dereferenced using Elem(). func skeletonValueSlice(tag Tag, length int) (reflect.Value, error) { -- 2.46.1 From e1f58a194a109b30756373288af92b02e4e7f9cb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 28 Jun 2025 06:24:44 -0400 Subject: [PATCH 103/132] tape: In progress testing of dynamic encoding/decoding --- tape/dynamic_test.go | 136 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tape/dynamic_test.go diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go new file mode 100644 index 0000000..61d3546 --- /dev/null +++ b/tape/dynamic_test.go @@ -0,0 +1,136 @@ +package tape + +import "fmt" +import "bytes" +import "testing" +import "reflect" +import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil" + +func TestEncodeAnyInt(test *testing.T) { + err := testEncodeAny(test, uint8(0xCA), LI.WithCN(0), tu.S(0xCA)) + if err != nil { test.Fatal(err) } + err = testEncodeAny(test, 400, LI.WithCN(3), tu.S( + 0, 0, 0x1, 0x90, + )) + if err != nil { test.Fatal(err) } +} + +func TestEncodeAnyTable(test *testing.T) { + err := testEncodeAny(test, map[uint16] any { + 0xF3B9: 1, + 0x0102: 2, + 0x0000: "hi!", + 0xFFFF: []uint16 { 0xBEE5, 0x7777 }, + 0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} }, + }, KTV.WithCN(0), tu.S(5).AddVar( + []byte { + 0xF3, 0xB9, + byte(LI.WithCN(3)), + 0, 0, 0, 1, + }, + []byte { + 0x01, 0x02, + byte(LI.WithCN(3)), + 0, 0, 0, 2, + }, + []byte { + 0, 0, + byte(SBA.WithCN(3)), + 'h', 'i', '!', + }, + []byte { + 0xFF, 0xFF, + byte(OTA.WithCN(0)), 2, byte(LI.WithCN(1)), + 0xBE, 0xE5, 0x77, 0x77, + }, + []byte { + 0x12, 0x34, + byte(OTA.WithCN(0)), 2, byte(OTA.WithCN(0)), + 1, byte(LI.WithCN(1)), + 0, 0x5, + 2, byte(LI.WithCN(1)), + 0, 0x17, + 0xAA, 0xAA, + }, + )) + if err != nil { test.Fatal(err) } +} + +func TestEncodeDecodeAnyMap(test *testing.T) { + err := testEncodeDecodeAny(test, map[uint16] any { + 0xF3B9: 1, + 0x0102: 2, + 0x0000: "hi!", + 0xFFFF: []uint16 { 0xBEE5, 0x7777 }, + 0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} }, + }, nil) + if err != nil { test.Fatal(err) } +} + +func encAny(value any) ([]byte, Tag, int, error) { + tag, err := TagAny(value) + if err != nil { return nil, 0, 0, err } + buffer := bytes.Buffer { } + n, err := EncodeAny(&Encoder { + Writer: &buffer, + }, value, tag) + if err != nil { return nil, 0, n, err } + return buffer.Bytes(), tag, n, nil +} + +func decAny(data []byte) (Tag, any, int, error) { + destination := map[uint16] any { } + tag, err := TagAny(destination) + if err != nil { return 0, nil, 0, err } + n, err := DecodeAny(&Decoder { + Reader: bytes.NewBuffer(data), + }, &destination, tag) + if err != nil { return 0, nil, n, err } + return tag, destination, n, nil +} + +func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.Snake) error { + bytes, tag, n, err := encAny(value) + if err != nil { return err } + test.Log("tag: ", tag) + test.Log("got: ", tu.HexBytes(bytes)) + test.Log("correct:", correctBytes) + if tag != correctTag { + return fmt.Errorf("tag not equal") + } + if ok, n := correctBytes.Check(bytes); !ok { + return fmt.Errorf("bytes not equal: %d", n) + } + if n != len(bytes) { + return fmt.Errorf("n not equal: %d != %d", n, len(bytes)) + } + return nil +} + +func testEncodeDecodeAny(test *testing.T, value, correctValue any) error { + if correctValue == nil { + correctValue = value + } + + test.Log("encoding...") + bytes, tag, n, err := encAny(value) + if err != nil { return err } + test.Log("tag:", tag) + test.Log("got:", tu.HexBytes(bytes)) + test.Log("decoding...", tag) + if n != len(bytes) { + return fmt.Errorf("n not equal: %d != %d", n, len(bytes)) + } + + _, decoded, n, err := decAny(bytes) + if err != nil { return err } + test.Log("got: ", decoded) + test.Log("correct:", correctValue) + if !reflect.DeepEqual(decoded, correctValue) { + return fmt.Errorf("values not equal") + } + if n != len(bytes) { + return fmt.Errorf("n not equal: %d != %d", n, len(bytes)) + } + return nil +} -- 2.46.1 From 4930215166879177cca72fbf383635ea452912a5 Mon Sep 17 00:00:00 2001 From: "sashakoshka@tebibyte.media" Date: Sat, 28 Jun 2025 11:24:32 -0400 Subject: [PATCH 104/132] tape: Make decoder inherit bufio.Writer --- tape/decode.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tape/decode.go b/tape/decode.go index 9a2ad58..4dcfbd4 100644 --- a/tape/decode.go +++ b/tape/decode.go @@ -2,6 +2,7 @@ package tape import "io" import "math" +import "bufio" // Decodable is any type that can decode itself from a decoder. type Decodable interface { @@ -11,9 +12,16 @@ type Decodable interface { Decode(decoder *Decoder) (n int, err error) } -// Decoder wraps an [io.Reader] and decodes data from it. +// Decoder decodes data from an [io.Reader]. type Decoder struct { - io.Reader + bufio.Reader +} + +// NewDecoder creates a new decoder that reads from reader. +func NewDecoder(reader io.Reader) *Decoder { + decoder := &Decoder { } + decoder.Reader.Reset(reader) + return decoder } // ReadFull calls [io.ReadFull] on the reader. @@ -21,12 +29,6 @@ func (this *Decoder) ReadFull(buffer []byte) (n int, err error) { return io.ReadFull(this, buffer) } -// ReadByte decodes a single byte from the input reader. -func (this *Decoder) ReadByte() (value byte, n int, err error) { - uncasted, n, err := this.ReadUint8() - return byte(uncasted), n, err -} - // ReadInt8 decodes an 8-bit signed integer from the input reader. func (this *Decoder) ReadInt8() (value int8, n int, err error) { uncasted, n, err := this.ReadUint8() -- 2.46.1 From 81ac10508b39358d9184a1128494f802061eead1 Mon Sep 17 00:00:00 2001 From: "sashakoshka@tebibyte.media" Date: Sun, 29 Jun 2025 10:27:40 -0400 Subject: [PATCH 105/132] tape: Change how slice skeletons are generated, support nested OTAs --- tape/dynamic.go | 147 ++++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 66 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 7e7333b..e56da67 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -9,6 +9,8 @@ package tape import "fmt" import "reflect" +var dummyMap map[uint16] any + // EncodeAny encodes an "any" value. Returns an error if the underlying type is // unsupported. Supported types are: // @@ -121,31 +123,20 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err n += nn; if err != nil { return n, err } oneTag, nn, err := decoder.ReadTag() n += nn; if err != nil { return n, err } - var slice reflect.Value - needSet := false - if destination.Kind() == reflect.Struct && destination.Type() == unknownSlicePlaceholderType { - needSet = true - slice, err = skeletonValueSlice(oneTag, int(length)) - if err != nil { return n, err } - slice = slice.Elem() - } else { - slice = destination - if slice.Kind() != reflect.Slice { - return n, errWrongDestinationType("slice") - } - slice.SetLen(int(length)) + if destination.Kind() != reflect.Slice { + return n, errWrongDestinationType("slice") } + if destination.Cap() < int(length) { + destination.Grow(destination.Cap() - int(length)) + } + destination.SetLen(int(length)) for index := range length { - nn, err := decodeAny(decoder, slice.Index(int(index)), oneTag) + nn, err := decodeAny(decoder, destination.Index(int(index)), oneTag) n += nn; if err != nil { return n, err } } - if needSet { - destination.Set(slice) - } case KTV: // KTV: ( )* table := destination - var dummyMap map[uint16] any if table.Type() != reflect.TypeOf(dummyMap) { return n, errWrongDestinationType("map[uint16] any") } @@ -305,65 +296,89 @@ func decodeAndSetFloat(decoder *Decoder, destination reflect.Value, bytes int) ( // skeletonValue returns a pointer value. In order for it to be set, it must be // dereferenced using Elem(). func skeletonValue(decoder *Decoder, tag Tag) (reflect.Value, error) { + typ, err := typeOf(decoder, tag) + if err != nil { return reflect.Value { }, err } + return reflect.New(typ), nil +} + +// typeOf returns the type of the current tag being decoded. It does not use up +// the decoder, it only peeks. +func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) { switch tag.WithoutCN() { case SI: - value := uint8(0) - return reflect.ValueOf(&value), nil + return reflect.TypeOf(uint8(0)), nil case LI: switch tag.CN() { - case 0: value := uint8(0); return reflect.ValueOf(&value), nil - case 1: value := uint16(0); return reflect.ValueOf(&value), nil - case 3: value := uint32(0); return reflect.ValueOf(&value), nil - case 7: value := uint64(0); return reflect.ValueOf(&value), nil + case 0: return reflect.TypeOf(uint8(0)), nil + case 1: return reflect.TypeOf(uint16(0)), nil + case 3: return reflect.TypeOf(uint32(0)), nil + case 7: return reflect.TypeOf(uint64(0)), nil } - return reflect.Value { }, fmt.Errorf("unknown CN %d for LI", tag.CN()) + return nil, fmt.Errorf("unknown CN %d for LI", tag.CN()) case FP: switch tag.CN() { - case 3: value := float32(0); return reflect.ValueOf(&value), nil - case 7: value := float64(0); return reflect.ValueOf(&value), nil + case 3: return reflect.TypeOf(float32(0)), nil + case 7: return reflect.TypeOf(float64(0)), nil } - return reflect.Value { }, fmt.Errorf("unknown CN %d for FP", tag.CN()) - case SBA: value := []byte { }; return reflect.ValueOf(&value), nil - case LBA: value := []byte { }; return reflect.ValueOf(&value), nil - case OTA: return skeletonValueSlice(decoder, tag) - case KTV: value := map[uint16] any { }; return reflect.ValueOf(&value), nil + return nil, fmt.Errorf("unknown CN %d for FP", tag.CN()) + case SBA: return reflect.SliceOf(reflect.TypeOf(byte(0))), nil + case LBA: return reflect.SliceOf(reflect.TypeOf(byte(0))), nil + case OTA: + elemTag, dimension, err := peekSlice(decoder, tag) + if err != nil { return nil, err } + if elemTag.Is(OTA) { panic("peekSlice cannot return OTA") } + typ, err := typeOf(decoder, elemTag) + if err != nil { return nil, err } + for _ = range dimension { + typ = reflect.SliceOf(typ) + } + return typ, nil + case KTV: return reflect.TypeOf(dummyMap), nil } - return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) + return nil, fmt.Errorf("unknown TN %d", tag.TN()) } -// skeletonValueSlice returns a pointer value. In order for it to be set, it -// must be dereferenced using Elem(). -func skeletonValueSlice(decoder *Decoder, tag Tag) (reflect.Value, error) { - // TODO -} - -// TODO: delete fucntion below - -// skeletonValueSlice returns a pointer value. In order for it to be set, it -// must be dereferenced using Elem(). -func skeletonValueSlice(tag Tag, length int) (reflect.Value, error) { - switch tag.WithoutCN() { - case SI: - value := make([]uint8, length) - return reflect.ValueOf(&value), nil - case LI: - switch tag.CN() { - case 0: value := make([]uint8, length); return reflect.ValueOf(&value), nil - case 1: value := make([]uint16, length); return reflect.ValueOf(&value), nil - case 3: value := make([]uint32, length); return reflect.ValueOf(&value), nil - case 7: value := make([]uint64, length); return reflect.ValueOf(&value), nil +// peekSlice returns the element tag and dimension count of the OTA currently +// being decoded. It does not use up the decoder, it only peeks. +func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { + offset := 0 + dimension := 0 + for { + elem, populated, n, err := peekSliceOnce(decoder, tag, offset) + if err != nil { return 0, 0, err } + offset += n + dimension += 1 + if elem.Is(OTA) { + if !populated { + return LBA, dimension + 1, nil + } + } else { + return elem, dimension, nil } - return reflect.Value { }, fmt.Errorf("unknown CN %d for LI OTA", tag.CN()) - case FP: - switch tag.CN() { - case 3: value := make([]float32, length); return reflect.ValueOf(&value), nil - case 7: value := make([]float64, length); return reflect.ValueOf(&value), nil - } - return reflect.Value { }, fmt.Errorf("unknown CN %d for FP OTA", tag.CN()) - case SBA: value := make([][]byte, length); return reflect.ValueOf(&value), nil - case LBA: value := make([][]byte, length); return reflect.ValueOf(&value), nil - case OTA: value := make([]any, length); return reflect.ValueOf(&value), nil - case KTV: value := make([]map[uint16] any, length); return reflect.ValueOf(&value), nil } - return reflect.Value { }, fmt.Errorf("unknown TN %d", tag.TN()) +} + +// peekSliceOnce returns the element tag of the OTA located offset bytes ahead +// of the current position. It does not use up the decoder, it only peeks. The n +// return value denotes how far away from 0 it peeked. If the OTA has more than +// zero items, populated will be set to true. +func peekSliceOnce(decoder *Decoder, tag Tag, offset int) (elem Tag, populated bool, n int, err error) { + lengthStart := offset + lengthEnd := lengthStart + tag.CN() + 1 + elemTagStart := lengthEnd + elemTagEnd := elemTagStart + 1 + + headerBytes, err := decoder.Peek(elemTagEnd) + if err != nil { return 0, false, 0, err } + + elem = Tag(headerBytes[len(headerBytes)]) + for index := lengthStart; index < lengthEnd; index += 1 { + if headerBytes[index] > 0 { + populated = true + break + } + } + n = elemTagEnd + + return } -- 2.46.1 From 8f8cd91b5d52edb82993fbe193b003ad14ec6ea8 Mon Sep 17 00:00:00 2001 From: "sashakoshka@tebibyte.media" Date: Sun, 29 Jun 2025 10:30:32 -0400 Subject: [PATCH 106/132] tape: Fix usage of Encoder/Decoder in dynamic tests --- tape/dynamic_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index 61d3546..e88a104 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -71,9 +71,7 @@ func encAny(value any) ([]byte, Tag, int, error) { tag, err := TagAny(value) if err != nil { return nil, 0, 0, err } buffer := bytes.Buffer { } - n, err := EncodeAny(&Encoder { - Writer: &buffer, - }, value, tag) + n, err := EncodeAny(NewEncoder(&buffer), value, tag) if err != nil { return nil, 0, n, err } return buffer.Bytes(), tag, n, nil } @@ -82,9 +80,7 @@ func decAny(data []byte) (Tag, any, int, error) { destination := map[uint16] any { } tag, err := TagAny(destination) if err != nil { return 0, nil, 0, err } - n, err := DecodeAny(&Decoder { - Reader: bytes.NewBuffer(data), - }, &destination, tag) + n, err := DecodeAny(NewDecoder(bytes.NewBuffer(data)), &destination, tag) if err != nil { return 0, nil, n, err } return tag, destination, n, nil } -- 2.46.1 From dcf923b1f337ba1aa295ee637bca97222327eafa Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 29 Jun 2025 11:06:58 -0400 Subject: [PATCH 107/132] internal/testutil: Snake.String and HexBytes return "EMPTY" when input is empty --- internal/testutil/testutil.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 2bba58e..8f182be 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -64,6 +64,10 @@ func (sn Snake) Check(data []byte) (ok bool, n int) { } func (sn Snake) String() string { + if len(sn) == 0 || len(sn[0]) == 0 || len(sn[0][0]) == 0{ + return "EMPTY" + } + out := strings.Builder { } for index, sector := range sn { if index > 0 { out.WriteString(" : ") } @@ -81,6 +85,7 @@ func (sn Snake) String() string { // HexBytes formats bytes into a hexadecimal string. func HexBytes(data []byte) string { + if len(data) == 0 { return "EMPTY" } out := strings.Builder { } for _, byt := range data { fmt.Fprintf(&out, "%02x", byt) -- 2.46.1 From e9633770ad91ee44e2c8ba91435391baea03f5bf Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 29 Jun 2025 13:22:21 -0400 Subject: [PATCH 108/132] internal/testutil: Formatting fix --- internal/testutil/testutil.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 8f182be..401c9a2 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -64,7 +64,7 @@ func (sn Snake) Check(data []byte) (ok bool, n int) { } func (sn Snake) String() string { - if len(sn) == 0 || len(sn[0]) == 0 || len(sn[0][0]) == 0{ + if len(sn) == 0 || len(sn[0]) == 0 || len(sn[0][0]) == 0 { return "EMPTY" } -- 2.46.1 From 2138d47f074fa395c2515394ade5ebe4df529718 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 29 Jun 2025 13:22:46 -0400 Subject: [PATCH 109/132] tape: Flush writer after encoding for testing --- tape/dynamic_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index e88a104..274bf46 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -71,8 +71,10 @@ func encAny(value any) ([]byte, Tag, int, error) { tag, err := TagAny(value) if err != nil { return nil, 0, 0, err } buffer := bytes.Buffer { } - n, err := EncodeAny(NewEncoder(&buffer), value, tag) + encoder := NewEncoder(&buffer) + n, err := EncodeAny(encoder, value, tag) if err != nil { return nil, 0, n, err } + encoder.Flush() return buffer.Bytes(), tag, n, nil } @@ -88,6 +90,7 @@ func decAny(data []byte) (Tag, any, int, error) { func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.Snake) error { bytes, tag, n, err := encAny(value) if err != nil { return err } + test.Log("n: ", n) test.Log("tag: ", tag) test.Log("got: ", tu.HexBytes(bytes)) test.Log("correct:", correctBytes) @@ -111,6 +114,7 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error { test.Log("encoding...") bytes, tag, n, err := encAny(value) if err != nil { return err } + test.Log("n: ", n) test.Log("tag:", tag) test.Log("got:", tu.HexBytes(bytes)) test.Log("decoding...", tag) -- 2.46.1 From 07fc77c83e61b9af340ef33209105223b6a87025 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 2 Jul 2025 06:25:24 -0400 Subject: [PATCH 110/132] tape: WIP --- tape/dynamic.go | 8 ++++-- tape/dynamic_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index e56da67..ae2981c 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -6,6 +6,8 @@ package tape // TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get // transformed into tables with a defined schema +// TODO: test all of these smaller functions individually + import "fmt" import "reflect" @@ -346,10 +348,12 @@ func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { for { elem, populated, n, err := peekSliceOnce(decoder, tag, offset) if err != nil { return 0, 0, err } - offset += n + offset = n dimension += 1 if elem.Is(OTA) { if !populated { + // default to a large byte array, will be + // interpreted as a string. return LBA, dimension + 1, nil } } else { @@ -371,7 +375,7 @@ func peekSliceOnce(decoder *Decoder, tag Tag, offset int) (elem Tag, populated b headerBytes, err := decoder.Peek(elemTagEnd) if err != nil { return 0, false, 0, err } - elem = Tag(headerBytes[len(headerBytes)]) + elem = Tag(headerBytes[len(headerBytes) - 1]) for index := lengthStart; index < lengthEnd; index += 1 { if headerBytes[index] > 0 { populated = true diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index 274bf46..fbbea68 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -67,6 +67,65 @@ func TestEncodeDecodeAnyMap(test *testing.T) { if err != nil { test.Fatal(err) } } +func TestPeekSlice(test *testing.T) { + buffer := bytes.NewBuffer([]byte { + 2, byte(OTA.WithCN(3)), + 0, 0, 0, 1, byte(LI.WithCN(1)), + 0, 0x5, + 2, byte(LI.WithCN(1)), + 0, 0x17, + 0xAA, 0xAA, + }) + decoder := NewDecoder(buffer) + + elem, dimension, err := peekSlice(decoder, OTA.WithCN(0)) + if err != nil { test.Fatal(err) } + if elem != LI.WithCN(1) { + test.Fatalf("wrong element tag: %v %02X", elem, byte(elem)) + } + if got, correct := dimension, 2; got != correct { + test.Fatalf("wrong dimension: %d != %d", got, correct) + } +} + +func TestPeekSliceOnce(test *testing.T) { + buffer := bytes.NewBuffer([]byte { + 2, byte(OTA.WithCN(3)), + 0, 0, 0, 1, byte(LI.WithCN(1)), + 0, 0x5, + 2, byte(LI.WithCN(1)), + 0, 0x17, + 0xAA, 0xAA, + }) + decoder := NewDecoder(buffer) + + test.Log("--- stage 1") + elem, populated, n, err := peekSliceOnce(decoder, OTA.WithCN(0), 0) + if err != nil { test.Fatal(err) } + if elem != OTA.WithCN(3) { + test.Fatal("wrong element tag:", elem) + } + if !populated { + test.Fatal("wrong populated:", populated) + } + if got, correct := n, 2; got != correct { + test.Fatalf("wrong n: %d != %d", got, correct) + } + + test.Log("--- stage 2") + elem, populated, n, err = peekSliceOnce(decoder, elem, n) + if err != nil { test.Fatal(err) } + if elem != LI.WithCN(1) { + test.Fatal("wrong element tag:", elem) + } + if !populated { + test.Fatal("wrong populated:", populated) + } + if got, correct := n, 7; got != correct { + test.Fatalf("wrong n: %d != %d", got, correct) + } +} + func encAny(value any) ([]byte, Tag, int, error) { tag, err := TagAny(value) if err != nil { return nil, 0, 0, err } -- 2.46.1 From c8a2f03ca1c93b270c5c96e004392f6f8859854d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Jul 2025 12:22:49 -0400 Subject: [PATCH 111/132] tape: Fix peekSlice not using the correct tag --- tape/dynamic.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index ae2981c..2528ace 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -251,7 +251,7 @@ func setInt(destination reflect.Value, value uint64) error { return nil } -// setInt expects a settable destination. +// setFloat expects a settable destination. func setFloat(destination reflect.Value, value float64) error { if !destination.CanFloat() { return fmt.Errorf("cannot assign float to %T", destination.Interface()) @@ -345,11 +345,14 @@ func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) { func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { offset := 0 dimension := 0 + currentTag := tag for { - elem, populated, n, err := peekSliceOnce(decoder, tag, offset) + elem, populated, n, err := peekSliceOnce(decoder, currentTag, offset) if err != nil { return 0, 0, err } + currentTag = elem offset = n dimension += 1 + fmt.Println(n) if elem.Is(OTA) { if !populated { // default to a large byte array, will be -- 2.46.1 From 5989a82beee2f78f1d8bfefb442b84ec433f3801 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Jul 2025 14:18:30 -0400 Subject: [PATCH 112/132] tape: Fix negative slice length --- tape/dynamic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 2528ace..bded11f 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -129,7 +129,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err return n, errWrongDestinationType("slice") } if destination.Cap() < int(length) { - destination.Grow(destination.Cap() - int(length)) + destination.Grow(int(length) - destination.Cap()) } destination.SetLen(int(length)) for index := range length { -- 2.46.1 From 877698d402351ff025685f4c0f720ce9ee2987c1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Jul 2025 14:18:56 -0400 Subject: [PATCH 113/132] tape: Remove print statements --- tape/dynamic.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index bded11f..e92ad35 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -352,7 +352,6 @@ func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { currentTag = elem offset = n dimension += 1 - fmt.Println(n) if elem.Is(OTA) { if !populated { // default to a large byte array, will be -- 2.46.1 From c118a4d7ef08766e55ab076e312f4b0ed1ec8895 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 4 Jul 2025 14:30:23 -0400 Subject: [PATCH 114/132] tape: Change name of test to TestEncodeDecodeAnyTable --- tape/dynamic.go | 2 +- tape/dynamic_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index e92ad35..73a1e10 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -154,7 +154,7 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err if err != nil { return n, err } nn, err = decodeAny(decoder, value.Elem(), itemTag) n += nn; if err != nil { return n, err } - table.SetMapIndex(reflect.ValueOf(key), value) + table.SetMapIndex(reflect.ValueOf(key), value.Elem()) } default: return n, fmt.Errorf("unknown TN %d", tag.TN()) diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index fbbea68..afb93d7 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -56,11 +56,11 @@ func TestEncodeAnyTable(test *testing.T) { if err != nil { test.Fatal(err) } } -func TestEncodeDecodeAnyMap(test *testing.T) { +func TestEncodeDecodeAnyTable(test *testing.T) { err := testEncodeDecodeAny(test, map[uint16] any { 0xF3B9: 1, 0x0102: 2, - 0x0000: "hi!", + 0x0000: []byte("hi!"), 0xFFFF: []uint16 { 0xBEE5, 0x7777 }, 0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} }, }, nil) -- 2.46.1 From 6ba70ed046214de9b570a6257ff1c14f8b97647f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Jul 2025 18:47:08 -0400 Subject: [PATCH 115/132] internal/testutil: Create function to closely examine any data --- internal/testutil/testutil.go | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 401c9a2..4295b92 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -3,6 +3,7 @@ package testutil import "fmt" import "slices" import "strings" +import "reflect" // Snake lets you compare blocks of data where the ordering of certain parts may // be swapped every which way. It is designed for comparing the encoding of @@ -92,3 +93,69 @@ func HexBytes(data []byte) string { } return out.String() } + +// Describe returns a string representing the type and data of the given value. +func Describe(value any) string { + desc := describer { } + desc.describe(reflect.ValueOf(value)) + return desc.String() +} + +type describer struct { + strings.Builder + indent int +} + +func (this *describer) describe(value reflect.Value) { + value = reflect.ValueOf(value.Interface()) + switch value.Kind() { + case reflect.Array, reflect.Slice: + this.printf("[\n") + this.indent += 1 + for index := 0; index < value.Len(); index ++ { + this.iprintf("") + this.describe(value.Index(index)) + this.iprintf("\n") + } + this.indent -= 1 + this.iprintf("]") + case reflect.Struct: + this.printf("struct {\n") + this.indent += 1 + typ := value.Type() + for index := range typ.NumField() { + indexBuffer := [1]int { index } + this.iprintf("%s: ", typ.Field(index).Name) + this.describe(value.FieldByIndex(indexBuffer[:])) + this.iprintf("\n") + } + this.indent -= 1 + this.iprintf("}\n") + case reflect.Map: + this.printf("map {\n") + this.indent += 1 + iter := value.MapRange() + for iter.Next() { + this.iprintf("") + this.describe(iter.Key()) + this.printf(": ") + this.describe(iter.Value()) + this.iprintf("\n") + } + this.indent -= 1 + this.iprintf("}\n") + case reflect.Pointer: + this.printf("& ") + this.describe(value.Elem()) + default: + this.printf("<%v %v>", value.Type(), value.Interface()) + } +} + +func (this *describer) printf(format string, v ...any) { + fmt.Fprintf(this, format, v...) +} + +func (this *describer) iprintf(format string, v ...any) { + fmt.Fprintf(this, strings.Repeat("\t", this.indent) + format, v...) +} -- 2.46.1 From 1b82f2cd835afec888bb0c1d66c1e41c7c1d4112 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Jul 2025 18:47:57 -0400 Subject: [PATCH 116/132] tape: Use tu.Describe() in tests --- tape/dynamic_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index afb93d7..9daab51 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -183,8 +183,8 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error { _, decoded, n, err := decAny(bytes) if err != nil { return err } - test.Log("got: ", decoded) - test.Log("correct:", correctValue) + test.Log("got: ", tu.Describe(decoded)) + test.Log("correct:", tu.Describe(correctValue)) if !reflect.DeepEqual(decoded, correctValue) { return fmt.Errorf("values not equal") } -- 2.46.1 From 0f20c4cdab112e4db35e1f2343e64529f9e50608 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Jul 2025 19:14:25 -0400 Subject: [PATCH 117/132] tape: Fix TestEncodeDecodeAny using int instead of uint32 --- tape/dynamic_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tape/dynamic_test.go b/tape/dynamic_test.go index 9daab51..19575bd 100644 --- a/tape/dynamic_test.go +++ b/tape/dynamic_test.go @@ -58,8 +58,8 @@ func TestEncodeAnyTable(test *testing.T) { func TestEncodeDecodeAnyTable(test *testing.T) { err := testEncodeDecodeAny(test, map[uint16] any { - 0xF3B9: 1, - 0x0102: 2, + 0xF3B9: uint32(1), + 0x0102: uint32(2), 0x0000: []byte("hi!"), 0xFFFF: []uint16 { 0xBEE5, 0x7777 }, 0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} }, -- 2.46.1 From 76a8f9444a91313de0d99cca8e0e91b525b13140 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 5 Jul 2025 22:10:55 -0400 Subject: [PATCH 118/132] tape: Ignore type names when encoding primitives using reflection --- tape/dynamic.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tape/dynamic.go b/tape/dynamic.go index 73a1e10..dd89055 100644 --- a/tape/dynamic.go +++ b/tape/dynamic.go @@ -12,6 +12,7 @@ import "fmt" import "reflect" var dummyMap map[uint16] any +var dummyBuffer []byte // EncodeAny encodes an "any" value. Returns an error if the underlying type is // unsupported. Supported types are: @@ -24,26 +25,27 @@ var dummyMap map[uint16] any // - [] // - map[uint16] func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) { - // TODO use reflection for all of this to ignore type names // primitives - switch value := value.(type) { - case int: return encoder.WriteInt32(int32(value)) - case uint: return encoder.WriteUint32(uint32(value)) - case int8: return encoder.WriteInt8(value) - case uint8: return encoder.WriteUint8(value) - case int16: return encoder.WriteInt16(value) - case uint16: return encoder.WriteUint16(value) - case int32: return encoder.WriteInt32(value) - case uint32: return encoder.WriteUint32(value) - case int64: return encoder.WriteInt64(value) - case uint64: return encoder.WriteUint64(value) - case string: return EncodeAny(encoder, []byte(value), tag) - case []byte: + reflectValue := reflect.ValueOf(value) + switch reflectValue.Kind() { + case reflect.Int: return encoder.WriteInt32(int32(reflectValue.Int())) + case reflect.Uint: return encoder.WriteUint32(uint32(reflectValue.Uint())) + case reflect.Int8: return encoder.WriteInt8(int8(reflectValue.Int())) + case reflect.Uint8: return encoder.WriteUint8(uint8(reflectValue.Uint())) + case reflect.Int16: return encoder.WriteInt16(int16(reflectValue.Int())) + case reflect.Uint16: return encoder.WriteUint16(uint16(reflectValue.Uint())) + case reflect.Int32: return encoder.WriteInt32(int32(reflectValue.Int())) + case reflect.Uint32: return encoder.WriteUint32(uint32(reflectValue.Uint())) + case reflect.Int64: return encoder.WriteInt64(int64(reflectValue.Int())) + case reflect.Uint64: return encoder.WriteUint64(uint64(reflectValue.Uint())) + case reflect.String: return EncodeAny(encoder, []byte(reflectValue.String()), tag) + } + if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) { if tag.Is(LBA) { - nn, err := encoder.WriteUintN(uint64(len(value)), tag.CN() + 1) + nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1) n += nn; if err != nil { return n, err } } - nn, err := encoder.Write(value) + nn, err := encoder.Write(reflectValue.Bytes()) n += nn; if err != nil { return n, err } return n, nil } -- 2.46.1 From f1df5fa84ddc7835698fc3a6f13cb0635afe0f85 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 7 Jul 2025 15:13:18 -0400 Subject: [PATCH 119/132] generate: Add stub return to generateDecodeValue so it compiles --- generate/generate.go | 1 + 1 file changed, 1 insertion(+) diff --git a/generate/generate.go b/generate/generate.go index 67328b8..226abb0 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -366,6 +366,7 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri // - nn int func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource string) (n int, err error) { // TODO + return 0, nil } func (this *Generator) generateErrorCheck() (n int, err error) { -- 2.46.1 From a9d5bb83a2228f2e3cdf279905174ef24ce413a1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 7 Jul 2025 16:00:25 -0400 Subject: [PATCH 120/132] generate: Add framework for testing output of generated code --- generate/misc_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 generate/misc_test.go diff --git a/generate/misc_test.go b/generate/misc_test.go new file mode 100644 index 0000000..448f114 --- /dev/null +++ b/generate/misc_test.go @@ -0,0 +1,44 @@ +package generate + +import "os" +import "fmt" +import "os/exec" +import "testing" +import "path/filepath" + +func testGenerateRun(test *testing.T, protocol *Protocol, imports string, testCase string) { + // open files + dir, err := os.MkdirTemp(os.TempDir(), "hopp-generate-test-*") + if err != nil { test.Fatal(err) } + defer os.RemoveAll(dir) + sourceFile, err := os.Create(filepath.Join(dir, "protocol.go")) + if err != nil { test.Fatal(err) } + defer sourceFile.Close() + mainFile, err := os.Create(filepath.Join(dir, "main.go")) + if err != nil { test.Fatal(err) } + defer mainFile.Close() + + // generate protocol + generator := Generator { + Output: sourceFile, + PackageName: "main", + } + _, err = generator.Generate(protocol) + if err != nil { test.Fatal(err) } + + // build static source files + imports = ` + import "log" + ` + imports + setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")` + teardown := `log.Println("--- END TEST CASE OUTPUT ---")` + fmt.Fprintf( + mainFile, "package main\n%s\nfunc main() {%s\n%s\n%s\n}\n", + imports, setup, testCase, teardown) + + // build and run test + command := exec.Command("go", "run", filepath.Join(dir, "main.go")) + output, err := command.CombinedOutput() + test.Logf("output of %v:\n%s", command, output) + if err != nil { test.Fatal(err) } +} -- 2.46.1 From c70c23d1371ba09d1575afb886ddcaae196818df Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 11:38:00 -0400 Subject: [PATCH 121/132] generate: Fix testGenerateRun so that it actually works --- .gitignore | 1 + generate/misc_test.go | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f9c924 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/generate/test diff --git a/generate/misc_test.go b/generate/misc_test.go index 448f114..4ad4afb 100644 --- a/generate/misc_test.go +++ b/generate/misc_test.go @@ -7,10 +7,14 @@ import "testing" import "path/filepath" func testGenerateRun(test *testing.T, protocol *Protocol, imports string, testCase string) { - // open files - dir, err := os.MkdirTemp(os.TempDir(), "hopp-generate-test-*") + // reset data directory + dir := "test/generate-run" + err := os.RemoveAll(dir) if err != nil { test.Fatal(err) } - defer os.RemoveAll(dir) + err = os.MkdirAll(dir, 0750) + if err != nil { test.Fatal(err) } + + // open files sourceFile, err := os.Create(filepath.Join(dir, "protocol.go")) if err != nil { test.Fatal(err) } defer sourceFile.Close() @@ -29,15 +33,37 @@ func testGenerateRun(test *testing.T, protocol *Protocol, imports string, testCa // build static source files imports = ` import "log" + import "bytes" + import "slices" + import "git.tebibyte.media/sashakoshka/hopp/tape" ` + imports setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")` teardown := `log.Println("--- END TEST CASE OUTPUT ---")` + static := ` + func testEncode(message Message, correct ...byte) { + buffer := bytes.Buffer { } + encoder := tape.NewEncoder(&buffer) + n, err := message.Encode(encoder) + if err != nil { log.Fatalf("at %d: %v\n", n, err) } + got := buffer.Bytes() + if n != len(got) { + log.Fatalln("len incorrect: %d != %d", got, correct) + } + if !slices.Equal(got, correct) { + log.Fatalln("not equal:") + } + } + ` fmt.Fprintf( - mainFile, "package main\n%s\nfunc main() {%s\n%s\n%s\n}\n", - imports, setup, testCase, teardown) + mainFile, "package main\n%s\nfunc main() {\n%s\n%s\n%s\n}\n%s", + imports, setup, testCase, teardown, static) // build and run test - command := exec.Command("go", "run", filepath.Join(dir, "main.go")) + command := exec.Command("go", "run", "./generate/test/generate-run") + workingDirAbs, err := filepath.Abs("..") + if err != nil { test.Fatal(err) } + command.Dir = workingDirAbs + command.Env = os.Environ() output, err := command.CombinedOutput() test.Logf("output of %v:\n%s", command, output) if err != nil { test.Fatal(err) } -- 2.46.1 From 9bc90b0e179cbb58e07f05739093bb0dfbc94463 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 11:39:18 -0400 Subject: [PATCH 122/132] generate: What the fuck is a teibibyte --- generate/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate/generate.go b/generate/generate.go index 226abb0..c8cb751 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -10,7 +10,7 @@ import "git.tebibyte.media/sashakoshka/hopp/tape" const imports = ` -import "git.teibibyte.media/sashakoshka/hopp/tape" +import "git.tebibyte.media/sashakoshka/hopp/tape" ` const preamble = ` -- 2.46.1 From 8a0ae9b03f04f906d12715436319cdc689dc24bc Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 11:39:37 -0400 Subject: [PATCH 123/132] generate: Update tests to account for new changes --- generate/generate_test.go | 236 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 generate/generate_test.go diff --git a/generate/generate_test.go b/generate/generate_test.go new file mode 100644 index 0000000..ceb2ac9 --- /dev/null +++ b/generate/generate_test.go @@ -0,0 +1,236 @@ +package generate + +// import "fmt" +import "strings" +import "testing" +import "git.tebibyte.media/sashakoshka/goparse" + +var testGenerateCorrect = +`package protocol + +/* # Do not edit this package by hand! + * + * This file was automatically generated by the Holanet PDL compiler. The + * source file is located at input.pdl + * Please edit that file instead, and re-compile it to this location. + * + * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + */ + +import "git.tebibyte.media/sashakoshka/hopp/tape" + +// Table is a KTV table with an undefined schema. +type Table map[uint16] any + +// Message is any message that can be sent along this protocol. +type Message interface { + tape.Encodable + tape.Decodable + + // Method returns the method code of the message. + Method() uint16 +} + +// User represents the protocol data type User. +type User struct { + Name string + Bio string + Followers uint32 +} + +// EncodeValue encodes the value of this type without the tag. The value is +// encoded according to the parameters specified by the tag, if possible. +func (this *User) EncodeValue(encoder *tape.Encoder) (n int, err error) { + nn, err := tape.WriteTableHeader(2) + n += nn; if err != nil { return n, err } + nn, err := encoder.WriteUint16(0x0000) + n += nn; if err != nil { return n, err } + nn, err := tape.WriteString(encoder, this.Name) + n += nn; if err != nil { return n, err } + nn, err := encoder.WriteUint16(0x0001) + n += nn; if err != nil { return n, err } + nn, err := tape.WriteString(encoder, this.Bio) + n += nn; if err != nil { return n, err } + return n, nil +} + +// Decode replaces the data in this User with information from the decoder. +func (this *User) Decode(decoder *tape.Decoder) (n int, err error) { + pull, nn, err := tape.ReadTableHeader(decoder) + n += nn; if err != nil { return n, err } + + for { + key, tag, end, nn, err := pull() + n += nn; if err != nil { return n, err } + if end { break } + + switch key { + case 0x0000: + value, nn, err := tape.ReadString(decoder) + n += nn; if err != nil { return n, err } + this.Name = value + case 0x0001: + value, nn, err := tape.ReadString(decoder) + n += nn; if err != nil { return n, err } + this.Bio = value + } + } + + return n, nil +} + +// MessageConnect represents the protocol message M0000 Connect. +type MessageConnect struct { + Name string + Password string +} + +// Method returns the method code, M0000. +func (this *MessageConnect) Method() uint16 { + return 0x0000 +} + +// Encode encodes the message to the encoder. +func (this *MessageConnect) Encode(encoder *tape.Encoder) (n int, err error) { + nn, err := tape.WriteTableHeader(2) + n += nn; if err != nil { return n, err } + nn, err := encoder.WriteUint16(0x0000) + n += nn; if err != nil { return n, err } + nn, err := tape.WriteString(encoder, this.Name) + n += nn; if err != nil { return n, err } + nn, err := encoder.WriteUint16(0x0001) + n += nn; if err != nil { return n, err } + nn, err := tape.WriteString(encoder, this.Password) + n += nn; if err != nil { return n, err } + return n, nil +} + +// Decode replaces the data in this message with information from the decoder. +func (this *MessageConnect) Decode(decoder *tape.Decoder) (n int, err error) { + pull, nn, err := tape.ReadTableHeader(decoder) + n += nn; if err != nil { return n, err } + + for { + key, tag, end, nn, err := pull() + n += nn; if err != nil { return n, err } + if end { break } + + switch key { + case 0x0000: + value, nn, err := tape.ReadString(decoder) + n += nn; if err != nil { return n, err } + this.Name = value + case 0x0001: + value, nn, err := tape.ReadString(decoder) + n += nn; if err != nil { return n, err } + this.Password = value + } + } + + return n, nil +} + +// MessageUserList represents the protocol message M0001 UserList. +type MessageUserList struct { + Users []User +} + +// Method returns the method code, M0001. +func (this *MessageUserList) Method() uint16 { + return 0x0001 +} + +// TODO methods +` + +func TestGenerate(test *testing.T) { + protocol := defaultProtocol() + protocol.Messages[0x0000] = Message { + Name: "Connect", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + }, + }, + } + protocol.Messages[0x0001] = Message { + Name: "UserList", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } }, + }, + }, + } + protocol.Types["User"] = TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, + 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + }, + } + + correct := testGenerateCorrect + + builder := strings.Builder { } + generator := Generator { Output: &builder } + /* TODO test n: */ _, err := generator.Generate(&protocol) + if err != nil { test.Fatal(parse.Format(err)) } + got := builder.String() + + test.Log("CORRECT:") + test.Log(correct) + test.Log("GOT:") + test.Log(got) + + if correct != got { + test.Error("not equal") + for index := range min(len(correct), len(got)) { + if correct[index] == got[index] { continue } + test.Log("C:", correct[max(0, index - 8):min(len(correct), index + 8)]) + test.Log("G:", got[max(0, index - 8):min(len(got), index + 8)]) + break + } + test.FailNow() + } +} + +func TestGenerateRun(test *testing.T) { + protocol := defaultProtocol() + protocol.Messages[0x0000] = Message { + Name: "Connect", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + }, + }, + } + protocol.Messages[0x0001] = Message { + Name: "UserList", + Type: TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } }, + }, + }, + } + protocol.Types["User"] = TypeTableDefined { + Fields: map[uint16] Field { + 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, + 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, + 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + }, + } + testGenerateRun(test, &protocol, ` + // imports + `, ` + // test case + messageConnect := MessageConnect { + Name: "rarity", + Password: "gems", + } + testEncode( + messageConnect, + 0x0) // TODO + `) +} -- 2.46.1 From e75d7534c1111836cbeaf12ceeead99c0b847ca4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 12:01:21 -0400 Subject: [PATCH 124/132] generate: Fix syntax errors in generated code --- generate/generate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index c8cb751..cf83e33 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -280,9 +280,9 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } nn, err = this.println() n += nn; if err != nil { return n, err } - nn, err = this.iprintf("tag.Is(tape.SBA) { continue }\n") + nn, err = this.iprintf("if tag.Is(tape.SBA) { continue }\n") n += nn; if err != nil { return n, err } - nn, err = this.iprintf("tag.CN() > itemTag.CN() { largest = tag }\n") + nn, err = this.iprintf("if tag.CN() > itemTag.CN() { largest = tag }\n") n += nn; if err != nil { return n, err } this.pop() nn, err = this.iprintf("}\n") @@ -395,7 +395,7 @@ func (this *Generator) generateTag(typ Type, source string) (n int, err error) { nn, err := this.printf("bufferTag(%s)", source) n += nn; if err != nil { return n, err } case TypeArray: - nn, err := this.printf("arrayTag(tape.TagOTA.WithCN(tape.IntBytes(uint64(len(%s))))", source) + nn, err := this.printf("arrayTag(tape.TagOTA.WithCN(tape.IntBytes(uint64(len(%s)))))", source) n += nn; if err != nil { return n, err } case TypeTable: nn, err := this.printf("tape.TagKTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) -- 2.46.1 From cdba8ee601cf1fd629b01adc5724d543c73999e2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 14:52:05 -0400 Subject: [PATCH 125/132] generate: Fix a bunch of semantic issues with the generated code --- generate/generate.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index cf83e33..6e1fd01 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -174,7 +174,7 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e nn, err = this.iprintf("\n// Encode encodes this message's tag and value.\n") n += nn; if err != nil { return n, err } nn, err = this.iprintf( - "func(this %s) Encode(encoder *tape.Encoder) (n int, err error) {\n", + "func(this *%s) Encode(encoder *tape.Encoder) (n int, err error) {\n", this.resolveMessageName(message.Name)) n += nn; if err != nil { return n, err } this.push() @@ -311,7 +311,7 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri case TypeTableDefined: // KTV: ( )* nn, err := this.iprintf( - "nn, err = encoder.WriteUintN(%s.CN(), %d)\n", + "nn, err = encoder.WriteUintN(uint64(%s.CN()), %d)\n", tagSource, len(typ.Fields)) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() @@ -379,29 +379,29 @@ func (this *Generator) generateTag(typ Type, source string) (n int, err error) { switch typ := typ.(type) { case TypeInt: if typ.Bits <= 5 { - nn, err := this.printf("tape.TagSI") + nn, err := this.printf("tape.SI") n += nn; if err != nil { return n, err } } else { - nn, err := this.printf("tape.TagLI.WithCN(%d)", bitsToCN(typ.Bits)) + nn, err := this.printf("tape.LI.WithCN(%d)", bitsToCN(typ.Bits)) n += nn; if err != nil { return n, err } } case TypeFloat: - nn, err := this.printf("tape.TagFP.WithCN(%d)", bitsToCN(typ.Bits)) + nn, err := this.printf("tape.FP.WithCN(%d)", bitsToCN(typ.Bits)) n += nn; if err != nil { return n, err } case TypeString: nn, err := this.generateTag(TypeBuffer { }, source) n += nn; if err != nil { return n, err } case TypeBuffer: - nn, err := this.printf("bufferTag(%s)", source) + nn, err := this.printf("tape.BufferTag(%s)", source) n += nn; if err != nil { return n, err } case TypeArray: - nn, err := this.printf("arrayTag(tape.TagOTA.WithCN(tape.IntBytes(uint64(len(%s)))))", source) + nn, err := this.printf("arrayTag(tape.OTA.WithCN(tape.IntBytes(uint64(len(%s)))))", source) n += nn; if err != nil { return n, err } case TypeTable: - nn, err := this.printf("tape.TagKTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) + nn, err := this.printf("tape.KTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) n += nn; if err != nil { return n, err } case TypeTableDefined: - nn, err := this.printf("tape.TagKTV.WithCN(%d)", tape.IntBytes(uint64(len(typ.Fields)))) + nn, err := this.printf("tape.KTV.WithCN(%d)", tape.IntBytes(uint64(len(typ.Fields)))) n += nn; if err != nil { return n, err } case TypeNamed: resolved, err := this.resolveTypeName(typ.Name) -- 2.46.1 From 9ff317d443178323eab724da01212cd4091db07f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 15:44:24 -0400 Subject: [PATCH 126/132] generate: Change comment so it gets detected by the regex for generated files --- generate/generate.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 6e1fd01..728e7c5 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -14,14 +14,11 @@ import "git.tebibyte.media/sashakoshka/hopp/tape" ` const preamble = ` -/* # Do not edit this package by hand! - * - * This file was automatically generated by the Holanet PDL compiler. The - * source file is located at - * Please edit that file instead, and re-compile it to this location. - * - * HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz - */ +// Code generated by the Holanet PDL compiler. DO NOT EDIT. +// The source file is located at +// Please edit that file instead, and re-compile it to this location. +// HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz + ` const static = ` -- 2.46.1 From a210f6112cb121f9974d02bb34fcfdab2e342206 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 20:35:11 -0400 Subject: [PATCH 127/132] generate: Fix more semantic issues in generated code --- generate/generate.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 728e7c5..e2a8f89 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -181,7 +181,7 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e n += nn; if err != nil { return n, err } nn, err = this.println() n += nn; if err != nil { return n, err } - nn, err = this.iprintf("nn, err := encoder.WriteUint8()\n") + nn, err = this.iprintf("nn, err := encoder.WriteUint8(uint8(tag))\n") n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } @@ -238,7 +238,7 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } this.push() nn, err = this.iprintf( - "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + "nn, err = encoder.WriteUintN(%s.CN(), len(%s))\n", tagSource, valueSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() @@ -254,7 +254,7 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri case TypeArray: // OTA: * nn, err := this.iprintf( - "nn, err = encoder.WriteUintN(%s.CN(), uint64(len(%s)))\n", + "nn, err = encoder.WriteUintN(uint64(%s.CN()), len(%s))\n", tagSource, valueSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() @@ -268,9 +268,14 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } nn, err = this.println() n += nn; if err != nil { return n, err } + // TODO: we don't have to do this for loop for some + // types such as integers because the CN will be the + // same nn, err = this.iprintf("for _, item := range %s {\n", valueSource) n += nn; if err != nil { return n, err } this.push() + nn, err = this.iprintf("_ = item\n") + n += nn; if err != nil { return n, err } nn, err = this.iprintf("tag := ") n += nn; if err != nil { return n, err } nn, err = this.generateTag(typ.Element, "item") @@ -279,7 +284,7 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } nn, err = this.iprintf("if tag.Is(tape.SBA) { continue }\n") n += nn; if err != nil { return n, err } - nn, err = this.iprintf("if tag.CN() > itemTag.CN() { largest = tag }\n") + nn, err = this.iprintf("if tag.CN() > itemTag.CN() { itemTag = tag }\n") n += nn; if err != nil { return n, err } this.pop() nn, err = this.iprintf("}\n") @@ -386,13 +391,13 @@ func (this *Generator) generateTag(typ Type, source string) (n int, err error) { nn, err := this.printf("tape.FP.WithCN(%d)", bitsToCN(typ.Bits)) n += nn; if err != nil { return n, err } case TypeString: - nn, err := this.generateTag(TypeBuffer { }, source) + nn, err := this.printf("tape.StringTag(%s)", source) n += nn; if err != nil { return n, err } case TypeBuffer: nn, err := this.printf("tape.BufferTag(%s)", source) n += nn; if err != nil { return n, err } case TypeArray: - nn, err := this.printf("arrayTag(tape.OTA.WithCN(tape.IntBytes(uint64(len(%s)))))", source) + nn, err := this.printf("tape.OTA.WithCN(tape.IntBytes(uint64(len(%s))))", source) n += nn; if err != nil { return n, err } case TypeTable: nn, err := this.printf("tape.KTV.WithCN(tape.IntBytes(uint64(len(%s))))", source) @@ -418,29 +423,29 @@ func (this *Generator) generateTN(typ Type) (n int, err error) { switch typ := typ.(type) { case TypeInt: if typ.Bits <= 5 { - nn, err := this.printf("tape.TagSI") + nn, err := this.printf("tape.SI") n += nn; if err != nil { return n, err } } else { - nn, err := this.printf("tape.TagLI") + nn, err := this.printf("tape.LI") n += nn; if err != nil { return n, err } } case TypeFloat: - nn, err := this.printf("tape.TagFP",) + nn, err := this.printf("tape.FP",) n += nn; if err != nil { return n, err } case TypeString: nn, err := this.generateTN(TypeBuffer { }) n += nn; if err != nil { return n, err } case TypeBuffer: - nn, err := this.printf("tape.TagLBA") + nn, err := this.printf("tape.LBA") n += nn; if err != nil { return n, err } case TypeArray: - nn, err := this.printf("tape.TagOTA") + nn, err := this.printf("tape.OTA") n += nn; if err != nil { return n, err } case TypeTable: - nn, err := this.printf("tape.TagKTV") + nn, err := this.printf("tape.KTV") n += nn; if err != nil { return n, err } case TypeTableDefined: - nn, err := this.printf("tape.TagKTV") + nn, err := this.printf("tape.KTV") n += nn; if err != nil { return n, err } case TypeNamed: resolved, err := this.resolveTypeName(typ.Name) -- 2.46.1 From e48be0bc152c49c00361daa388f559ce82440691 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 21:32:34 -0400 Subject: [PATCH 128/132] generate: Fix more semantic issues with generated code --- generate/generate.go | 60 ++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index e2a8f89..85c5f7e 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -94,7 +94,7 @@ func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) nn, err = this.println() n += nn; if err != nil { return n, err } - // Tag method + // 'Tag' method // to be honest we probably don't need this method at all // nn, err = this.iprintf("\n// Tag returns the preferred TAPE tag.\n") // n += nn; if err != nil { return n, err } @@ -238,13 +238,13 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } this.push() nn, err = this.iprintf( - "nn, err = encoder.WriteUintN(%s.CN(), len(%s))\n", + "nn, err = encoder.WriteUintN(uint64(%s.CN()), len(%s))\n", tagSource, valueSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } this.pop() - nn, err = this.iprintf("}\n", tagSource) + nn, err = this.iprintf("}\n") n += nn; if err != nil { return n, err } nn, err = this.iprintf("nn, err = encoder.Write([]byte(%s))\n", valueSource) @@ -347,6 +347,11 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } case TypeNamed: // WHATEVER: [WHATEVER] + if builtin := this.resolveBuiltinType(typ.Name); builtin != nil { + nn, err := this.generateEncodeValue(builtin, valueSource, tagSource) + n += nn; if err != nil { return n, err } + return n, nil + } nn, err := this.iprintf("nn, err = %s.EncodeValue(encoder, %s)\n", valueSource, tagSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() @@ -586,27 +591,8 @@ func (this *Generator) resolveMessageName(message string) string { } func (this *Generator) resolveTypeName(name string) (Type, error) { - switch name { - case "U8": return TypeInt { Bits: 8 }, nil - case "U16": return TypeInt { Bits: 16 }, nil - case "U32": return TypeInt { Bits: 32 }, nil - case "U64": return TypeInt { Bits: 64 }, nil - case "U128": return TypeInt { Bits: 128 }, nil - case "U256": return TypeInt { Bits: 256 }, nil - case "I8": return TypeInt { Bits: 8, Signed: true }, nil - case "I16": return TypeInt { Bits: 16, Signed: true }, nil - case "I32": return TypeInt { Bits: 32, Signed: true }, nil - case "I64": return TypeInt { Bits: 64, Signed: true }, nil - case "I128": return TypeInt { Bits: 128, Signed: true }, nil - case "I256": return TypeInt { Bits: 256, Signed: true }, nil - case "F16": return TypeFloat { Bits: 16 }, nil - case "F32": return TypeFloat { Bits: 32 }, nil - case "F64": return TypeFloat { Bits: 64 }, nil - case "F128": return TypeFloat { Bits: 128 }, nil - case "F256": return TypeFloat { Bits: 256 }, nil - case "String": return TypeString { }, nil - case "Buffer": return TypeBuffer { }, nil - case "Table": return TypeTable { }, nil + if typ := this.resolveBuiltinType(name); typ != nil { + return typ, nil } if typ, ok := this.protocol.Types[name]; ok { @@ -619,6 +605,32 @@ func (this *Generator) resolveTypeName(name string) (Type, error) { return nil, fmt.Errorf("no type exists called %s", name) } +func (this *Generator) resolveBuiltinType(name string) Type { + switch name { + case "U8": return TypeInt { Bits: 8 } + case "U16": return TypeInt { Bits: 16 } + case "U32": return TypeInt { Bits: 32 } + case "U64": return TypeInt { Bits: 64 } + case "U128": return TypeInt { Bits: 128 } + case "U256": return TypeInt { Bits: 256 } + case "I8": return TypeInt { Bits: 8, Signed: true } + case "I16": return TypeInt { Bits: 16, Signed: true } + case "I32": return TypeInt { Bits: 32, Signed: true } + case "I64": return TypeInt { Bits: 64, Signed: true } + case "I128": return TypeInt { Bits: 128, Signed: true } + case "I256": return TypeInt { Bits: 256, Signed: true } + case "F16": return TypeFloat { Bits: 16 } + case "F32": return TypeFloat { Bits: 32 } + case "F64": return TypeFloat { Bits: 64 } + case "F128": return TypeFloat { Bits: 128 } + case "F256": return TypeFloat { Bits: 256 } + case "String": return TypeString { } + case "Buffer": return TypeBuffer { } + case "Table": return TypeTable { } + } + return nil +} + func bitsToBytes(bits int) int { return int(math.Ceil(float64(bits) / 8.0)) } -- 2.46.1 From 3bf365a7a9ec36596cc2c18b1e8ceae1ad9cb033 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 8 Jul 2025 21:50:29 -0400 Subject: [PATCH 129/132] generate: Fix more semantic errors in the generated code --- generate/generate.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 85c5f7e..5e33bdf 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -217,13 +217,17 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri // SI stores the value in the tag, so we write nothing here break } - nn, err := this.iprintf("nn, err = encoder.WriteInt%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + prefix := "WriteUint" + if typ.Signed { + prefix = "WriteInt" + } + nn, err := this.iprintf("nn, err = encoder.%s%d(%s)\n", prefix, typ.Bits, valueSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } case TypeFloat: // FP: - nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", bitsToBytes(typ.Bits), valueSource) + nn, err := this.iprintf("nn, err = encoder.WriteFloat%d(%s)\n", typ.Bits, valueSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() n += nn; if err != nil { return n, err } @@ -504,9 +508,12 @@ func (this *Generator) generateType(typ Type) (n int, err error) { nn, err := this.generateTypeTableDefined(typ) n += nn; if err != nil { return n, err } case TypeNamed: - actual, err := this.resolveTypeName(typ.Name) - if err != nil { return n, err } - nn, err := this.generateType(actual) + if builtin := this.resolveBuiltinType(typ.Name); builtin != nil { + nn, err := this.generateType(builtin) + n += nn; if err != nil { return n, err } + return n, nil + } + nn, err := this.print(typ.Name) n += nn; if err != nil { return n, err } } return n, nil -- 2.46.1 From 5a3296a842cd06ee5a9540fc9996be4b955c725e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 10 Jul 2025 21:53:21 -0400 Subject: [PATCH 130/132] generate: Parse primitive types into actual types rather than named types --- generate/parse.go | 22 ++++++++++++++++++++++ generate/parse_test.go | 10 +++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/generate/parse.go b/generate/parse.go index 287a693..cb4a7a5 100644 --- a/generate/parse.go +++ b/generate/parse.go @@ -95,6 +95,28 @@ func (this *parser) parseType() (Type, error) { switch this.Kind() { case TokenIdent: + switch this.Value() { + case "U8": return TypeInt { Bits: 8 }, this.Next() + case "U16": return TypeInt { Bits: 16 }, this.Next() + case "U32": return TypeInt { Bits: 32 }, this.Next() + case "U64": return TypeInt { Bits: 64 }, this.Next() + case "U128": return TypeInt { Bits: 128 }, this.Next() + case "U256": return TypeInt { Bits: 256 }, this.Next() + case "I8": return TypeInt { Bits: 8, Signed: true }, this.Next() + case "I16": return TypeInt { Bits: 16, Signed: true }, this.Next() + case "I32": return TypeInt { Bits: 32, Signed: true }, this.Next() + case "I64": return TypeInt { Bits: 64, Signed: true }, this.Next() + case "I128": return TypeInt { Bits: 128, Signed: true }, this.Next() + case "I256": return TypeInt { Bits: 256, Signed: true }, this.Next() + case "F16": return TypeFloat { Bits: 16 }, this.Next() + case "F32": return TypeFloat { Bits: 32 }, this.Next() + case "F64": return TypeFloat { Bits: 64 }, this.Next() + case "F128": return TypeFloat { Bits: 128 }, this.Next() + case "F256": return TypeFloat { Bits: 256 }, this.Next() + case "String": return TypeString { }, this.Next() + case "Buffer": return TypeBuffer { }, this.Next() + case "Table": return TypeTable { }, this.Next() + } return this.parseTypeNamed() case TokenLBracket: return this.parseTypeArray() diff --git a/generate/parse_test.go b/generate/parse_test.go index 415fcf7..1916580 100644 --- a/generate/parse_test.go +++ b/generate/parse_test.go @@ -11,8 +11,8 @@ func TestParse(test *testing.T) { Name: "Connect", Type: TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Password", Type: TypeString { } }, }, }, } @@ -26,9 +26,9 @@ func TestParse(test *testing.T) { } correct.Types["User"] = TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, - 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Bio", Type: TypeString { } }, + 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, }, } test.Log("CORRECT:", &correct) -- 2.46.1 From 2305814e10e5ef94c922b72b4149350f784eae4f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 10 Jul 2025 21:56:38 -0400 Subject: [PATCH 131/132] tape: Add StringTag --- tape/tag.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tape/tag.go b/tape/tag.go index b32898d..9a89f38 100644 --- a/tape/tag.go +++ b/tape/tag.go @@ -54,6 +54,11 @@ func BufferTag(value []byte) Tag { return bufferLenTag(len(value)) } +// StringTag returns the appropriate tag for a string. +func StringTag(value string) Tag { + return bufferLenTag(len(value)) +} + func bufferLenTag(length int) Tag { if length < int(CNLimit) { return SBA.WithCN(length) -- 2.46.1 From af7669c783d0dbd65b0ea1285d823028a8a25329 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 11 Jul 2025 20:08:43 -0400 Subject: [PATCH 132/132] generate: Fix more nonsense surrounding named types --- generate/generate.go | 59 +++++++++++---------------------------- generate/generate_test.go | 22 +++++++-------- 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 5e33bdf..34f86ba 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -193,7 +193,18 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e nn, err = this.iprintf("}\n") n += nn; if err != nil { return n, err } - // TODO decode method + // Decode method + nn, err = this.iprintf("\n// Decode decodes this message's tag and value.\n") + n += nn; if err != nil { return n, err } + nn, err = this.iprintf( + "func(this *%s) Decode(encoder *tape.Decoder) (n int, err error) {\n", + this.resolveMessageName(message.Name)) + n += nn; if err != nil { return n, err } + this.push() + // TODO + this.pop() + nn, err = this.iprintf("}\n") + n += nn; if err != nil { return n, err } return n, nil } @@ -351,11 +362,6 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri n += nn; if err != nil { return n, err } case TypeNamed: // WHATEVER: [WHATEVER] - if builtin := this.resolveBuiltinType(typ.Name); builtin != nil { - nn, err := this.generateEncodeValue(builtin, valueSource, tagSource) - n += nn; if err != nil { return n, err } - return n, nil - } nn, err := this.iprintf("nn, err = %s.EncodeValue(encoder, %s)\n", valueSource, tagSource) n += nn; if err != nil { return n, err } nn, err = this.generateErrorCheck() @@ -376,7 +382,9 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri // - err error // - nn int func (this *Generator) generateDecodeValue(typ Type, valueSource, tagSource string) (n int, err error) { - // TODO + // TODO generate stub so the code runs and we can test encoding at least+ + + return 0, nil } @@ -508,11 +516,6 @@ func (this *Generator) generateType(typ Type) (n int, err error) { nn, err := this.generateTypeTableDefined(typ) n += nn; if err != nil { return n, err } case TypeNamed: - if builtin := this.resolveBuiltinType(typ.Name); builtin != nil { - nn, err := this.generateType(builtin) - n += nn; if err != nil { return n, err } - return n, nil - } nn, err := this.print(typ.Name) n += nn; if err != nil { return n, err } } @@ -597,11 +600,7 @@ func (this *Generator) resolveMessageName(message string) string { return "Message" + message } -func (this *Generator) resolveTypeName(name string) (Type, error) { - if typ := this.resolveBuiltinType(name); typ != nil { - return typ, nil - } - +func (this *Generator) resolveTypeName(name string) (Type, error) { if typ, ok := this.protocol.Types[name]; ok { if typ, ok := typ.(TypeNamed); ok { return this.resolveTypeName(typ.Name) @@ -612,32 +611,6 @@ func (this *Generator) resolveTypeName(name string) (Type, error) { return nil, fmt.Errorf("no type exists called %s", name) } -func (this *Generator) resolveBuiltinType(name string) Type { - switch name { - case "U8": return TypeInt { Bits: 8 } - case "U16": return TypeInt { Bits: 16 } - case "U32": return TypeInt { Bits: 32 } - case "U64": return TypeInt { Bits: 64 } - case "U128": return TypeInt { Bits: 128 } - case "U256": return TypeInt { Bits: 256 } - case "I8": return TypeInt { Bits: 8, Signed: true } - case "I16": return TypeInt { Bits: 16, Signed: true } - case "I32": return TypeInt { Bits: 32, Signed: true } - case "I64": return TypeInt { Bits: 64, Signed: true } - case "I128": return TypeInt { Bits: 128, Signed: true } - case "I256": return TypeInt { Bits: 256, Signed: true } - case "F16": return TypeFloat { Bits: 16 } - case "F32": return TypeFloat { Bits: 32 } - case "F64": return TypeFloat { Bits: 64 } - case "F128": return TypeFloat { Bits: 128 } - case "F256": return TypeFloat { Bits: 256 } - case "String": return TypeString { } - case "Buffer": return TypeBuffer { } - case "Table": return TypeTable { } - } - return nil -} - func bitsToBytes(bits int) int { return int(math.Ceil(float64(bits) / 8.0)) } diff --git a/generate/generate_test.go b/generate/generate_test.go index ceb2ac9..a25b807 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -149,8 +149,8 @@ func TestGenerate(test *testing.T) { Name: "Connect", Type: TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Password", Type: TypeString { } }, }, }, } @@ -164,9 +164,9 @@ func TestGenerate(test *testing.T) { } protocol.Types["User"] = TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, - 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Bio", Type: TypeString { } }, + 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, }, } @@ -201,8 +201,8 @@ func TestGenerateRun(test *testing.T) { Name: "Connect", Type: TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Password", Type: TypeString { } }, }, }, } @@ -216,9 +216,9 @@ func TestGenerateRun(test *testing.T) { } protocol.Types["User"] = TypeTableDefined { Fields: map[uint16] Field { - 0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } }, - 0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } }, - 0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } }, + 0x0000: Field { Name: "Name", Type: TypeString { } }, + 0x0001: Field { Name: "Bio", Type: TypeString { } }, + 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, }, } testGenerateRun(test, &protocol, ` @@ -230,7 +230,7 @@ func TestGenerateRun(test *testing.T) { Password: "gems", } testEncode( - messageConnect, + &messageConnect, 0x0) // TODO `) } -- 2.46.1