Compare commits

..

No commits in common. "main" and "unify-byte-counts" have entirely different histories.

47 changed files with 811 additions and 3335 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,66 +1,48 @@
package main package main
import "os" import "os"
import "fmt"
import "strings" import "strings"
import "path/filepath" import "path/filepath"
import "git.tebibyte.media/sashakoshka/go-cli"
import "git.tebibyte.media/sashakoshka/goparse" import "git.tebibyte.media/sashakoshka/goparse"
import "git.tebibyte.media/sashakoshka/hopp/generate" import "git.tebibyte.media/sashakoshka/hopp/generate"
func main() { func main() {
flagOutput := cli.NewInputFlag('o', "output", "The output file", "", cli.ValString) name := os.Args[0]
flagPackageName := cli.NewInputFlag('p', "package-name", "The package name of the file", "", cli.ValString) if len(os.Args) != 3 {
command := cli.New("Compile PDL files to program source code", fmt.Fprintf(os.Stderr, "Usage: %s SOURCE DESTINATION\n", name)
flagOutput,
flagPackageName)
command.Syntax = "FILE [OPTIONS]..."
command.ParseOrExit(os.Args)
if len(command.Args) != 1 {
command.Usage()
os.Exit(2) os.Exit(2)
} }
source := command.Args[0] source := os.Args[1]
destination := flagOutput.Value destination := os.Args[2]
if destination == "" {
destination = "protocol.go"
}
input, err := os.Open(source) input, err := os.Open(source)
handleErr(command, 1, err) handleErr(1, err)
defer input.Close() defer input.Close()
protocol, err := generate.ParseReader(source, input) protocol, err := generate.ParseReader(source, input)
handleErr(command, 1, err) handleErr(1, err)
packageName := flagPackageName.Value
if packageName == "" {
absDestination, err := filepath.Abs(destination) absDestination, err := filepath.Abs(destination)
handleErr(command, 1, err) handleErr(1, err)
base := filepath.Base(absDestination) packageName := cleanPackageName(strings.ReplaceAll(
if scrounged, ok := scroungeForPackageName(filepath.Dir(absDestination)); ok { strings.ToLower(filepath.Base(absDestination)),
packageName = scrounged " ", "_"))
} else { destination = filepath.Join(os.Args[2], "generated.go")
packageName = strings.ReplaceAll(
strings.ToLower(base),
" ", "_")
}
}
packageName = cleanPackageName(packageName)
output, err := os.Create(destination) output, err := os.Create(destination)
handleErr(command, 1, err) handleErr(1, err)
generator := generate.Generator { generator := generate.Generator {
Output: output, Output: output,
PackageName: packageName, PackageName: packageName,
} }
_, err = generator.Generate(protocol) _, err = generator.Generate(protocol)
handleErr(command, 1, err) handleErr(1, err)
command.Println(destination, "OK") fmt.Fprintf(os.Stderr, "%s: OK\n", name)
} }
func handleErr(command *cli.Cli, code int, err error) { func handleErr(code int, err error) {
if err != nil { if err != nil {
command.Errorln(parse.Format(err)) fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], parse.Format(err))
os.Exit(code) os.Exit(code)
} }
} }
@ -79,32 +61,3 @@ func cleanPackageName(str string) string {
} }
return string(buffer[:j]) return string(buffer[:j])
} }
func scroungeForPackageName(dir string) (string, bool) {
entries, err := os.ReadDir(dir)
if err != nil { return "", false}
for _, entry := range entries {
if !entry.Type().IsRegular() { continue }
file, err := os.Open(filepath.Join(dir, entry.Name()))
if err != nil { continue }
defer file.Close()
// FIXME: it is entirely possible that the only file will have
// a shitload of doc comments preceeding the package name, and
// those comments are usually huge so this is bad
buffer := [512]byte { }
n, _ := file.Read(buffer[:])
text := string(buffer[:n])
packageIndex := strings.Index(text, "package")
if packageIndex < 0 { continue }
text = text[packageIndex:]
newlineIndex := strings.Index(text, "\n")
if packageIndex > 0 { text = text[:newlineIndex] }
fields := strings.Fields(text)
if len(fields) < 2 { continue }
return fields[1], true
}
return "", false
}

View File

@ -34,8 +34,8 @@ type Conn interface {
} }
// Trans is a HOPP transaction. Methods of this interface are not safe for // Trans is a HOPP transaction. Methods of this interface are not safe for
// concurrent use with the exception of the Write, SendWriter, Close, and ID // concurrent use with the exception of the Close and ID methods. The
// methods. The recommended use case is one goroutine per transaction. // recommended use case is one goroutine per transaction.
type Trans interface { type Trans interface {
// Close closes the transaction. Any blocked operations will be // Close closes the transaction. Any blocked operations will be
// unblocked and return errors. This method is safe for concurrent use. // unblocked and return errors. This method is safe for concurrent use.
@ -45,13 +45,13 @@ type Trans interface {
// unique within the connection. This method is safe for concurrent use. // unique within the connection. This method is safe for concurrent use.
ID() int64 ID() int64
// Send sends a message. This method is safe for concurrent use. // Send sends a message. This method is not safe for concurrent use.
Send(method uint16, data []byte) error Send(method uint16, data []byte) error
// SendWriter sends data written to an [io.Writer]. The writer must be // SendWriter sends data written to an [io.Writer]. The writer must be
// closed after use. Closing the writer flushes any data that hasn't // closed after use. Closing the writer flushes any data that hasn't
// been written yet. Any writer previously opened through this function // been written yet. Any writer previously opened through this function
// will be discarded. This method is safe for concurrent use, but its // will be discarded. This method is not safe for concurrent use, and
// result isn't. // neither is its result.
SendWriter(method uint16) (io.WriteCloser, error) SendWriter(method uint16) (io.WriteCloser, error)
// Receive receives a message. This method is not safe for concurrent // Receive receives a message. This method is not safe for concurrent
// use. // use.

View File

@ -25,7 +25,6 @@ PDL allows defining a protocol using HOPP and TAPE.
| F64 | FP | 7 | | F64 | FP | 7 |
| F128[^2] | FP | 15 | | F128[^2] | FP | 15 |
| F256[^2] | FP | 31 | | F256[^2] | FP | 31 |
| Bool | SI | |
| String | SBA/LBA | * | UTF-8 string | String | SBA/LBA | * | UTF-8 string
| Buffer | SBA/LBA | * | Byte array | Buffer | SBA/LBA | * | Byte array
| []\<TYPE\> | OTA | * | Array of any type[^1] | []\<TYPE\> | OTA | * | Array of any type[^1]
@ -33,11 +32,6 @@ PDL allows defining a protocol using HOPP and TAPE.
| {...} | KTV | * | Table with defined schema | {...} | KTV | * | Table with defined schema
| Any | * | * | Value of an undefined type | Any | * | * | Value of an undefined type
Tables with a defined schema can specify some fields as optional using a
question mark before the type. This will wrap the field the go-util
ucontainer.Option type. When encoding, void fields will not be included in the
output, and when decoding, unspecified fields are left void.
[^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and [^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. Buffer are simply forced to use their "long" variant.
@ -53,13 +47,11 @@ structures. They are separated by whitespace.
| Method | `M[0-9A-Fa-f]{4}` | A 16-bit hexadecimal method code. | 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. | Key | `[0-9A-Fa-f]{4}` | A 16-bit hexadecimal table key.
| Ident | `[A-Z][A-Za-z0-9]` | An identifier. | Ident | `[A-Z][A-Za-z0-9]` | An identifier.
| Option | `?` | A question mark.
| Comma | `,` | A comma separator. | Comma | `,` | A comma separator.
| LBrace | `{` | A left curly brace. | LBrace | `{` | A left curly brace.
| RBrace | `}` | A right curly brace. | RBrace | `}` | A right curly brace.
| LBracket | `[` | A left square bracket. | LBracket | `[` | A left square bracket.
| RBracket | `]` | A right square bracket. | RBracket | `]` | A right square bracket.
| Comment | `\/\/.*$` | A doc comment starting with a double-slash.
## Syntax ## Syntax
@ -76,27 +68,18 @@ 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 message name (Ident), and the message's root type. This is usually a table, but
can be anything. can be anything.
Messages, types, and table fields can all have doc comments preceding them,
which are used to generate documentation for the protocol. The syntax is the
same as Go's (for now). Comments aren't allowed anywhere else.
Here is an example of all that: Here is an example of all that:
``` ```
// Connect is sent from the client to the server as the first message of an
// authenticated transaction.
M0000 Connect { M0000 Connect {
0000 Name String, 0000 Name String,
0001 Password String, 0001 Password String,
} }
// UserList is sent from the server to the client in response to a Connect
// message.
M0001 UserList { M0001 UserList {
0000 Users []User, 0000 Users []User,
} }
// User holds profile information about a single user.
User { User {
0000 Name String, 0000 Name String,
0001 Bio String, 0001 Bio String,
@ -113,10 +96,10 @@ Below is an EBNF description of the language.
<method> -> /M[0-9A-Fa-f]{4}/ <method> -> /M[0-9A-Fa-f]{4}/
<key> -> /[0-9A-Fa-f]{4}/ <key> -> /[0-9A-Fa-f]{4}/
<ident> -> /[A-Z][A-Za-z0-9]/ <ident> -> /[A-Z][A-Za-z0-9]/
<field> -> <key> <ident> ["?"] <type> <field> -> <key> <ident> <type>
<type> -> <ident> <type> -> <ident>
| "[" "]" <type> | "[" "]" <type>
| "{" (<comment>* <field> ",")* [<comment>* <field>] "}" | "{" (<field> ",")* [<field>] "}"
<message> -> <comment>* <method> <ident> <type> <message> -> <method> <ident> <type>
<typedef> -> <comment>* <ident> <type> <typedef> -> <ident> <type>
``` ```

View File

@ -132,8 +132,6 @@ METADAPT-B is used over QUIC for communication over networks such as the
Internet. Internet.
### METADAPT-A ### METADAPT-A
![Diagram of a METADAPT-A MMB.](../assets/metadapt-a-mmb-diagram.png)
METADAPT-A requires a transport which offers a single full-duplex data stream METADAPT-A requires a transport which offers a single full-duplex data stream
that persists for the duration of the connection. All transactions are that persists for the duration of the connection. All transactions are
multiplexed onto this single stream. Each MMB contains a 12-octet long header, multiplexed onto this single stream. Each MMB contains a 12-octet long header,

77
dial.go
View File

@ -5,59 +5,38 @@ import "errors"
import "context" import "context"
import "crypto/tls" import "crypto/tls"
// Dial opens a connection to a server. The network must be one of: // 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
// - "quic" // "quic6" don't do anything as the quic-go package doesn't seem to support this
// - "quic4" (IPv4-only) // behavior.
// - "quic6" (IPv6-only) func Dial(ctx context.Context, network, address string) (Conn, error) {
// - "tls" return (Dialer { }).Dial(ctx, network, address)
// - "tls4" (IPv4-only) }
// - "tls6" (IPv6-only)
// - "tcp" // Dialer allows for further configuration of the dialing process.
// - "tcp4" (IPv4-only) type Dialer struct {
// - "tcp6" (IPv6-only) TLSConfig *tls.Config
// - "unix" }
//
// For now, QUIC is unsupported. // Dial opens a connection to a server. The network must be one of "quic",
func Dial(ctx context.Context, network, address string, tlsConf *tls.Config) (Conn, error) { // "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 { switch network {
case "quic", "quic4", "quic6": return DialQUIC(ctx, network, address, tlsConf) case "quic", "quic4", "quic6": return diale.dialQUIC(ctx, network, address)
case "tls", "tls4", "tls6": return DialTLS(ctx, network, address, tlsConf) case "unix": return diale.dialUnix(ctx, network, address)
case "tcp", "tcp4", "tcp6":
addr, err := net.ResolveTCPAddr(network, address)
if err != nil { return nil, err }
return DialTCP(ctx, network, nil, addr)
case "unix":
addr, err := net.ResolveUnixAddr(network, address)
if err != nil { return nil, err }
return DialUnix(ctx, network, addr)
default: return nil, ErrUnknownNetwork default: return nil, ErrUnknownNetwork
} }
} }
// DialQUIC opens a connection to a server over QUIC. func (diale Dialer) dialQUIC(ctx context.Context, network, address string) (Conn, error) {
func DialQUIC(ctx context.Context, network, address string, tlsConf *tls.Config) (Conn, error) {
return nil, errors.New("quic is not yet implemented") return nil, errors.New("quic is not yet implemented")
} }
// DialTLS opens a connection to a server over TLS. func (diale Dialer) dialUnix(ctx context.Context, network, address string) (Conn, error) {
func DialTLS(ctx context.Context, network, address string, tlsConf *tls.Config) (Conn, error) { if network != "unix" { return nil, ErrUnknownNetwork }
network, err := tlsNetworkToTCPNetwork(network) addr, err := net.ResolveUnixAddr(network, address)
if err != nil { return nil, err } if err != nil { return nil, err }
conn, err := tls.Dial(network, address, tlsConf)
if err != nil { return nil, err }
return AdaptA(conn, ClientSide), nil
}
// DialTCP opens a connection to a server over TCP.
func DialTCP(ctx context.Context, network string, laddr, raddr *net.TCPAddr) (Conn, error) {
conn, err := net.DialTCP(network, laddr, raddr)
if err != nil { return nil, err }
return AdaptA(conn, ClientSide), nil
}
// DialUnix opens a connection to a server over a Unix domain socket.
func DialUnix(ctx context.Context, network string, addr *net.UnixAddr) (Conn, error) {
conn, err := net.DialUnix(network, nil, addr) conn, err := net.DialUnix(network, nil, addr)
if err != nil { return nil, err } if err != nil { return nil, err }
return AdaptA(conn, ClientSide), nil return AdaptA(conn, ClientSide), nil
@ -75,6 +54,7 @@ func tlsConfig(conf *tls.Config) *tls.Config {
return conf return conf
} }
func quicNetworkToUDPNetwork(network string) (string, error) { func quicNetworkToUDPNetwork(network string) (string, error) {
switch network { switch network {
case "quic4": return "udp4", nil case "quic4": return "udp4", nil
@ -83,12 +63,3 @@ func quicNetworkToUDPNetwork(network string) (string, error) {
default: return "", ErrUnknownNetwork default: return "", ErrUnknownNetwork
} }
} }
func tlsNetworkToTCPNetwork(network string) (string, error) {
switch network {
case "tls4": return "tcp4", nil
case "tls6": return "tcp6", nil
case "tls": return "tcp", nil
default: return "", ErrUnknownNetwork
}
}

View File

@ -1,86 +0,0 @@
package main
import "io"
import "log"
import "time"
import "errors"
import "context"
import "git.tebibyte.media/sashakoshka/hopp"
var network = "tcp"
var addr = "localhost:7959"
func main() {
go func() {
defer log.Println("SERVER closing")
listener, err := hopp.Listen(network, addr, nil)
if err != nil { log.Println("SERVER", err); return }
log.Println("SERVER listening")
conn, err := listener.Accept()
if err != nil { log.Println("SERVER", err); return }
defer conn.Close()
trans, err := conn.AcceptTrans()
if err != nil { log.Println("SERVER", err); return }
defer trans.Close()
for {
method, data, err := trans.Receive()
if err != nil { log.Println("SERVER", err); return }
log.Println("SERVER got", method, data)
log.Println("SERVER send", method, data)
err = trans.Send(1, data[:])
if err != nil { log.Println("SERVER", err); return }
}
}()
time.Sleep(time.Second * 2)
func() {
log.Println("CLIENT dialing")
conn, err := hopp.Dial(context.Background(), network, addr, nil)
if err != nil { log.Fatalln("CLIENT", err) }
log.Println("CLIENT dialed")
trans, err := conn.OpenTrans()
if err != nil {
log.Println("CLIENT", err)
return
}
go func() {
for {
method, data, err := trans.Receive()
if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("CLIENT failed to receive message: %v", err)
}
return
}
log.Println("CLIENT got", method, data)
}
}()
data := [1]byte { }
for {
log.Println("CLIENT send", 1, data)
err := trans.Send(1, data[:])
if err != nil {
log.Println("CLIENT", err)
return
}
data[0] ++
time.Sleep(time.Second)
}
log.Println("CLIENT waiting for connection close...")
trans, err = conn.AcceptTrans()
if !errors.Is(err, io.EOF) {
log.Println("CLIENT wrong error:", err)
log.Fatalln("CLIENT trans:", trans)
}
log.Println("CLIENT DONE")
conn.Close()
}()
}

View File

@ -1,11 +1,9 @@
package main package main
import "os" import "os"
import "io"
import "fmt" import "fmt"
import "time" import "time"
import "bufio" import "bufio"
import "errors"
import "context" import "context"
import "crypto/tls" import "crypto/tls"
import "git.tebibyte.media/sashakoshka/hopp" import "git.tebibyte.media/sashakoshka/hopp"
@ -19,12 +17,11 @@ func main() {
} }
address := os.Args[1] address := os.Args[1]
room := os.Args[2] room := os.Args[2]
nickname := "Anonymous"; if len(os.Args) >= 4 { var nickname hopp.Option[string]; if len(os.Args) >= 4 {
nickname = os.Args[3] nickname = hopp.O(os.Args[3])
} }
trans, err := join(address, room, nickname) trans, err := join(address, room, nickname)
handleErr(1, err) handleErr(1, err)
fmt.Fprintf(os.Stdout, "(i) connected to %s/%s\n", address, room)
go func() { go func() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
@ -34,49 +31,67 @@ func main() {
} }
}() }()
for { for {
message, _, err := chat.Receive(trans) message, err := chat.Receive(trans)
if err != nil {
if !errors.Is(err, io.EOF) {
handleErr(1, err) handleErr(1, err)
}
break
}
switch message := message.(type) { switch message := message.(type) {
case chat.MessageChat: case *chat.MessageChat:
fmt.Fprintf(os.Stdout, "%s: %s\n", message.Nickname, message.Content) nickname := "Anonymous"
case chat.MessageJoinNotify: if value, ok := message.Nickname.Get(); ok {
nickname = value
}
fmt.Fprintf(os.Stdout, "%s: %s\n", nickname, message.Content)
case *chat.MessageJoinNotify:
fmt.Fprintf(os.Stdout, "(i) %s joined the room\n", message.Nickname) fmt.Fprintf(os.Stdout, "(i) %s joined the room\n", message.Nickname)
case chat.MessageLeaveNotify: case *chat.MessageLeaveNotify:
fmt.Fprintf(os.Stdout, "(i) %s left the room\n", message.Nickname) fmt.Fprintf(os.Stdout, "(i) %s left the room\n", message.Nickname)
} }
} }
fmt.Fprintf(os.Stdout, "(i) disconnected\n")
} }
func join(address string, room string, nickname string) (hopp.Trans, error) { func join(address string, room string, nickname hopp.Option[string]) (hopp.Trans, error) {
ctx, done := context.WithTimeout(context.Background(), 16 * time.Second) ctx, done := context.WithTimeout(context.Background(), 16 * time.Second)
defer done() defer done()
conn, err := hopp.Dial(ctx, "tls", address, &tls.Config { dialer := hopp.Dialer {
TLSConfig: &tls.Config {
// don't actually do this in real life // don't actually do this in real life
InsecureSkipVerify: true, InsecureSkipVerify: true,
}) },
}
conn, err := dialer.Dial(ctx, "quic", address)
if err != nil { return nil, err }
err = updateProfile(conn, nickname)
if err != nil { return nil, err } if err != nil { return nil, err }
transRoom, err := conn.OpenTrans() transRoom, err := conn.OpenTrans()
if err != nil { return nil, err } if err != nil { return nil, err }
_, err = chat.Send(transRoom, &chat.MessageJoin { err = chat.Send(transRoom, &chat.MessageJoin {
Room: room, Room: room,
Nickname: nickname,
}) })
if err != nil { return nil, err } if err != nil { return nil, err }
return transRoom, nil return transRoom, nil
} }
func send(trans hopp.Trans, content string) error { func send(trans hopp.Trans, content string) error {
_, err := chat.Send(trans, &chat.MessageChat { return chat.Send(trans, &chat.MessageChat {
Content: content, Content: content,
}) })
return err }
func updateProfile(conn hopp.Conn, nickname hopp.Option[string]) error {
trans, err := conn.OpenTrans()
if err != nil { return err }
defer trans.Close()
err = chat.Send(trans, &chat.MessageUpdateProfile {
Nickname: nickname,
})
if err != nil { return err }
message, err := chat.Receive(trans)
if err != nil { return err }
switch message := message.(type) {
case *chat.MessageError: return message
default: return nil
}
} }
func handleErr(code int, err error) { func handleErr(code int, err error) {

View File

@ -1,6 +1,6 @@
// Package chat demonstrates a simple chat protocol. // Package chat implements a simple chat protocol over HOPP. To re-generate the
// source files, run this command from within the root directory of the
// repository:
//
// go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol
package chat package chat
// To use this in your own project, replace "go run ../../cmd/hopp-generate"
// with just "hopp-generate".
//go:generate go run ../../cmd/hopp-generate protocol.pdl -o protocol.go

View File

@ -3,8 +3,8 @@ package chat
import "fmt" import "fmt"
func (msg *MessageError) Error() string { func (msg *MessageError) Error() string {
if description, ok := msg.Description.Value(); ok { if description, ok := msg.Description.Get(); ok {
return fmt.Sprintf("other party sent error: %d %s", msg.Code, description) return fmt.Sprintf("other party sent error: %d %s", msg.Error, description)
} else { } else {
return fmt.Sprintf("other party sent error: %d", msg.Code) return fmt.Sprintf("other party sent error: %d", msg.Code)
} }

369
examples/chat/generated.go Normal file
View File

@ -0,0 +1,369 @@
package chat
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/tape"
// 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)
}
// Receive receives one message from a transaction.
func Receive(trans hopp.Trans) (hopp.Message, error) {
method, data, err := trans.Receive()
if err != nil { return nil, err }
switch method {
case 0x0000:
message := &MessageError { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0001:
message := &MessageSuccess { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0100:
message := &MessageUpdateProfile { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0200:
message := &MessageJoin { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0201:
message := &MessageChat { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0300:
message := &MessageJoinNotify { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
case 0x0301:
message := &MessageLeaveNotify { }
err := message.UnmarshalBinary(data)
if err != nil { return nil, err }
return message, nil
default: return nil, hopp.ErrUnknownMethod
}
}
// (0) Error is sent by a party when the other party has done something erroneous. The valid error codes are:
//
// 0: General, unspecified error
//
// The description field, if specified, determines a human-readable error to be shown to the user. The sending party must immediately close the transaction after this message is sent.
type MessageError struct {
/* 0 */ Code uint16
/* 1 */ Description hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageError) Method() uint16 {
return 0
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageError) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetCode := size
{ value := msg.Code
size += 2; _ = value }
offsetDescription := size
if value, ok := msg.Description.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
{ value := msg.Code
tape.EncodeI16(buffer[offsetCode:], value)}
if value, ok := msg.Description.Get(); ok {
tape.EncodeString(buffer[offsetDescription:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageError) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeI16[uint16](data)
if err != nil { return err }
msg.Code = value
foundRequired ++
case 1:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Description = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (1) Success is sent by a party when it has successfully completed a task given to it by the other party. The sending party must immediately close the transaction after this message is sent.
type MessageSuccess struct {
}
// Method returns the method number of the message.
func (msg MessageSuccess) Method() uint16 {
return 1
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageSuccess) MarshalBinary() ([]byte, error) {
size := 0
count := 0
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageSuccess) UnmarshalBinary(buffer []byte) error {
// no fields
return nil
}
// (256) UpdateProfile is sent by the client in a new transaction to update the profile details that will be shown to other connected clients.
type MessageUpdateProfile struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageUpdateProfile) Method() uint16 {
return 256
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageUpdateProfile) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageUpdateProfile) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (512) Join is sent by the client when it wishes to join a room. It must begin a new transaction, and that transaction will persist while the user is in that room. Messages having to do with the room will be sent along this transaction. To leave the room, the client must close the transaction.
type MessageJoin struct {
/* 0 */ Room string
}
// Method returns the method number of the message.
func (msg MessageJoin) Method() uint16 {
return 512
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageJoin) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetRoom := size
{ value := msg.Room
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
{ value := msg.Room
tape.EncodeString(buffer[offsetRoom:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageJoin) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Room = value
}
}
return nil
}
// (513) Chat is sent by the client when it wishes to post a message to the room. It is also relayed by the server to other clients to notify them of the message. It must be sent within a room transaction.
type MessageChat struct {
/* 0 */ Nickname hopp.Option[string]
/* 1 */ Content string
}
// Method returns the method number of the message.
func (msg MessageChat) Method() uint16 {
return 513
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageChat) MarshalBinary() ([]byte, error) {
size := 0
count := 1
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
offsetContent := size
{ value := msg.Content
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
{ value := msg.Content
tape.EncodeString(buffer[offsetContent:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageChat) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
case 1:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Content = value
foundRequired ++
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (768) JoinNotify is sent by the server when another client joins the room. It must be sent within a room transaction.
type MessageJoinNotify struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageJoinNotify) Method() uint16 {
return 768
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageJoinNotify) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageJoinNotify) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}
// (769) LeaveNotify is sent by the server when another client leaves the room. It must be sent within a room transaction.
type MessageLeaveNotify struct {
/* 0 */ Nickname hopp.Option[string]
}
// Method returns the method number of the message.
func (msg MessageLeaveNotify) Method() uint16 {
return 769
}
// MarshalBinary encodes the data in this message into a buffer.
func (msg *MessageLeaveNotify) MarshalBinary() ([]byte, error) {
size := 0
count := 0
offsetNickname := size
if value, ok := msg.Nickname.Get(); ok {
count ++
size += len(value) }
if size > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
if count > 0xFFFF { return nil, hopp.ErrPayloadTooLarge}
buffer := make([]byte, 2 + 4 * count + size)
tape.EncodeI16(buffer[:2], uint16(count))
if value, ok := msg.Nickname.Get(); ok {
tape.EncodeString(buffer[offsetNickname:], value)}
return buffer, nil
}
// UnmarshalBinary dencodes the data from a buffer int this message.
func (msg *MessageLeaveNotify) UnmarshalBinary(buffer []byte) error {
pairs, err := tape.DecodePairs(buffer)
if err != nil { return err }
foundRequired := 0
for tag, data := range pairs {
switch tag {
case 0:
value, err := tape.DecodeString[string](data)
if err != nil { return err }
msg.Nickname = hopp.O(value)
}
}
if foundRequired != 1 { return hopp.ErrTablePairMissing }
return nil
}

View File

@ -1,758 +0,0 @@
package chat
// Code generated by the Holanet PDL compiler. DO NOT EDIT.
// The source file is located at <path>
// Please edit that file instead, and re-compile it to this location.
// HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
import "fmt"
import "git.tebibyte.media/sashakoshka/hopp"
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
}
// Send encodes a message and sends it along a transaction.
func Send(trans hopp.Trans, message Message) (n int, err error) {
writer, err := trans.SendWriter(message.Method())
if err != nil { return n, err }
defer writer.Close()
encoder := tape.NewEncoder(writer)
n, err = message.Encode(encoder)
if err != nil { return n, err }
return n, encoder.Flush()
}
// canAssign determines if data from the given source tag can be assigned to
// a Go type represented by destination. It is designed to receive destination
// values from [generate.Generator.generateCanAssign]. The eventual Go type and
// the destination tag must come from the same (or hash-equivalent) PDL type.
func canAssign(destination, source tape.Tag) bool {
if destination.Is(source) { return true }
if (destination.Is(tape.SBA) || destination.Is(tape.LBA)) &&
(source.Is(tape.SBA) || source.Is(tape.LBA)) {
return true
}
return false
}
// boolInt converts a bool to an integer.
func boolInt(input bool) int {
if input {
return 1
} else {
return 0
}
}
// ensure ucontainer is always imported
var _ hopp.Option[int]
// ReceivedMessage is a sealed interface representing the value of a
// message in this package. To determine what kind of message it is,
// use a type switch like this:
//
// switch message := message.(type) {
// case MessageError:
// doSomething()
// case MessageSuccess:
// doSomething()
// case MessageJoin:
// doSomething()
//
// ...
//
// }
type ReceivedMessage interface {
isReceivedMessage()
}
// Error is sent by a party when the other party has done something erroneous. The
// valid error codes are:
//
// - 0: General, unspecified error
//
// The description field, if specified, determines a human-readable error to be
// shown to the user. The sending party must immediately close the transaction
// after this message is sent.
type MessageError struct {
Code uint16
Description hopp.Option[string]
}
// Method returns the message's method number.
func(this *MessageError) Method() uint16 { return 0x0000 }
// Encode encodes this message's tag and value.
func(this *MessageError) Encode(encoder *tape.Encoder) (n int, err error) {
tag_1 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_1)
n += nn; if err != nil { return n, err }
if 2 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(2, tag_1.CN() + 1)
n += nn; if err != nil { return n, err }
{
if value, ok := (*this).Description.Value(); ok {
nn, err = encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
tag_2 := tape.StringTag(string(value))
nn, err = encoder.WriteUint8(uint8(tag_2))
n += nn; if err != nil { return n, err }
if len(value) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_2.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len(value)), tag_2.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte(value))
n += nn; if err != nil { return n, err }
}
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_3 := tape.LI.WithCN(1)
nn, err = encoder.WriteUint8(uint8(tag_3))
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteUint16(uint16((*this).Code))
n += nn; if err != nil { return n, err }
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageError) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_1d505103df99c95e6bed0800d0ea881a_MessageError(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageError) isReceivedMessage() { }
// Success is sent by a party when it has successfully completed a task given to it
// by the other party. The sending party must immediately close the transaction
// after this message is sent.
type MessageSuccess struct {
}
// Method returns the message's method number.
func(this *MessageSuccess) Method() uint16 { return 0x0001 }
// Encode encodes this message's tag and value.
func(this *MessageSuccess) Encode(encoder *tape.Encoder) (n int, err error) {
tag_4 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_4)
n += nn; if err != nil { return n, err }
if 0 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(0, tag_4.CN() + 1)
n += nn; if err != nil { return n, err }
{
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageSuccess) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_99914b932bd37a50b983c5e7c90ae93b_MessageSuccess(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageSuccess) isReceivedMessage() { }
// Join is sent by the client when it wishes to join a room. It must begin a new
// transaction, and that transaction will persist while the user is in that room.
// Messages having to do with the room will be sent along this transaction. To
// leave the room, the client must close the transaction.
type MessageJoin struct {
Room string
Nickname string
}
// Method returns the message's method number.
func(this *MessageJoin) Method() uint16 { return 0x0200 }
// Encode encodes this message's tag and value.
func(this *MessageJoin) Encode(encoder *tape.Encoder) (n int, err error) {
tag_5 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_5)
n += nn; if err != nil { return n, err }
if 2 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(2, tag_5.CN() + 1)
n += nn; if err != nil { return n, err }
{
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_6 := tape.StringTag(string((*this).Room))
nn, err = encoder.WriteUint8(uint8(tag_6))
n += nn; if err != nil { return n, err }
if len((*this).Room) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_6.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Room)), tag_6.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Room))
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
tag_7 := tape.StringTag(string((*this).Nickname))
nn, err = encoder.WriteUint8(uint8(tag_7))
n += nn; if err != nil { return n, err }
if len((*this).Nickname) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_7.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Nickname)), tag_7.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Nickname))
n += nn; if err != nil { return n, err }
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageJoin) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_2c5f22d9503118676b4c5584211a4a95_MessageJoin(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageJoin) isReceivedMessage() { }
// Chat is sent by the client when it wishes to post a message to the room. It is
// also relayed by the server to other clients to notify them of the message. It
// must be sent within a room transaction.
type MessageChat struct {
Content string
Nickname string
}
// Method returns the message's method number.
func(this *MessageChat) Method() uint16 { return 0x0300 }
// Encode encodes this message's tag and value.
func(this *MessageChat) Encode(encoder *tape.Encoder) (n int, err error) {
tag_8 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_8)
n += nn; if err != nil { return n, err }
if 2 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(2, tag_8.CN() + 1)
n += nn; if err != nil { return n, err }
{
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_9 := tape.StringTag(string((*this).Content))
nn, err = encoder.WriteUint8(uint8(tag_9))
n += nn; if err != nil { return n, err }
if len((*this).Content) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_9.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Content)), tag_9.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Content))
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
tag_10 := tape.StringTag(string((*this).Nickname))
nn, err = encoder.WriteUint8(uint8(tag_10))
n += nn; if err != nil { return n, err }
if len((*this).Nickname) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_10.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Nickname)), tag_10.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Nickname))
n += nn; if err != nil { return n, err }
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageChat) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_5c1cf9347bb6d9f41cee64b186392d24_MessageChat(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageChat) isReceivedMessage() { }
// JoinNotify is sent by the server when another client joins the room. It must be
// sent within a room transaction.
type MessageJoinNotify struct {
Nickname string
}
// Method returns the message's method number.
func(this *MessageJoinNotify) Method() uint16 { return 0x0400 }
// Encode encodes this message's tag and value.
func(this *MessageJoinNotify) Encode(encoder *tape.Encoder) (n int, err error) {
tag_11 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_11)
n += nn; if err != nil { return n, err }
if 1 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(1, tag_11.CN() + 1)
n += nn; if err != nil { return n, err }
{
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_12 := tape.StringTag(string((*this).Nickname))
nn, err = encoder.WriteUint8(uint8(tag_12))
n += nn; if err != nil { return n, err }
if len((*this).Nickname) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_12.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Nickname)), tag_12.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Nickname))
n += nn; if err != nil { return n, err }
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageJoinNotify) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_68c536511e6d598462efc482144438e9_MessageJoinNotify(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageJoinNotify) isReceivedMessage() { }
// LeaveNotify is sent by the server when another client leaves the room. It must
// be sent within a room transaction.
type MessageLeaveNotify struct {
Nickname string
}
// Method returns the message's method number.
func(this *MessageLeaveNotify) Method() uint16 { return 0x0401 }
// Encode encodes this message's tag and value.
func(this *MessageLeaveNotify) Encode(encoder *tape.Encoder) (n int, err error) {
tag_13 := tape.KTV.WithCN(0)
nn, err := encoder.WriteTag(tag_13)
n += nn; if err != nil { return n, err }
if 1 > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
nn, err = encoder.WriteUintN(1, tag_13.CN() + 1)
n += nn; if err != nil { return n, err }
{
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_14 := tape.StringTag(string((*this).Nickname))
nn, err = encoder.WriteUint8(uint8(tag_14))
n += nn; if err != nil { return n, err }
if len((*this).Nickname) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_14.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Nickname)), tag_14.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Nickname))
n += nn; if err != nil { return n, err }
}
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessageLeaveNotify) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.KTV, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
nn, err = decodeBranch_68c536511e6d598462efc482144438e9_MessageLeaveNotify(this, decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
func (this MessageLeaveNotify) isReceivedMessage() { }
func decodeBranch_1d505103df99c95e6bed0800d0ea881a_MessageError(this *MessageError, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_15 uint64
if length_15 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_15, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_15 {
var fieldKey_16 uint16
fieldKey_16, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_17 tape.Tag
fieldTag_17, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_16 {
case 0x0000:
if !(canAssign(tape.LI, fieldTag_17)) {
tape.Skim(decoder, fieldTag_17)
continue
}
destination_18, nn, err := decoder.ReadUint16()
n += nn; if err != nil { return n, err }
*(&(this.Code)) = destination_18
case 0x0001:
if !(canAssign(tape.LBA, fieldTag_17)) {
tape.Skim(decoder, fieldTag_17)
continue
}
var destination_19 string
var length_20 uint64
if fieldTag_17.Is(tape.LBA) {
length_20, nn, err = decoder.ReadUintN(int(fieldTag_17.CN()))
n += nn; if err != nil { return n, err }
} else {
length_20 = uint64(fieldTag_17.CN())
}
if length_20 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_20)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&destination_19) = string(buffer)
this.Description = hopp.O(destination_19)
default:
tape.Skim(decoder, fieldTag_17)
continue
}
}
return n, nil
}
func decodeBranch_99914b932bd37a50b983c5e7c90ae93b_MessageSuccess(this *MessageSuccess, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_21 uint64
if length_21 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_21, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_21 {
var fieldKey_22 uint16
fieldKey_22, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_23 tape.Tag
fieldTag_23, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_22 {
default:
tape.Skim(decoder, fieldTag_23)
continue
}
}
return n, nil
}
func decodeBranch_2c5f22d9503118676b4c5584211a4a95_MessageJoin(this *MessageJoin, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_24 uint64
if length_24 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_24, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_24 {
var fieldKey_25 uint16
fieldKey_25, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_26 tape.Tag
fieldTag_26, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_25 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_26)) {
tape.Skim(decoder, fieldTag_26)
continue
}
var length_27 uint64
if fieldTag_26.Is(tape.LBA) {
length_27, nn, err = decoder.ReadUintN(int(fieldTag_26.CN()))
n += nn; if err != nil { return n, err }
} else {
length_27 = uint64(fieldTag_26.CN())
}
if length_27 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_27)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Room)) = string(buffer)
case 0x0001:
if !(canAssign(tape.LBA, fieldTag_26)) {
tape.Skim(decoder, fieldTag_26)
continue
}
var length_28 uint64
if fieldTag_26.Is(tape.LBA) {
length_28, nn, err = decoder.ReadUintN(int(fieldTag_26.CN()))
n += nn; if err != nil { return n, err }
} else {
length_28 = uint64(fieldTag_26.CN())
}
if length_28 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_28)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_26)
continue
}
}
return n, nil
}
func decodeBranch_5c1cf9347bb6d9f41cee64b186392d24_MessageChat(this *MessageChat, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_29 uint64
if length_29 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_29, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_29 {
var fieldKey_30 uint16
fieldKey_30, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_31 tape.Tag
fieldTag_31, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_30 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_31)) {
tape.Skim(decoder, fieldTag_31)
continue
}
var length_32 uint64
if fieldTag_31.Is(tape.LBA) {
length_32, nn, err = decoder.ReadUintN(int(fieldTag_31.CN()))
n += nn; if err != nil { return n, err }
} else {
length_32 = uint64(fieldTag_31.CN())
}
if length_32 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_32)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Content)) = string(buffer)
case 0x0001:
if !(canAssign(tape.LBA, fieldTag_31)) {
tape.Skim(decoder, fieldTag_31)
continue
}
var length_33 uint64
if fieldTag_31.Is(tape.LBA) {
length_33, nn, err = decoder.ReadUintN(int(fieldTag_31.CN()))
n += nn; if err != nil { return n, err }
} else {
length_33 = uint64(fieldTag_31.CN())
}
if length_33 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_33)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_31)
continue
}
}
return n, nil
}
func decodeBranch_68c536511e6d598462efc482144438e9_MessageJoinNotify(this *MessageJoinNotify, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_34 uint64
if length_34 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_34, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_34 {
var fieldKey_35 uint16
fieldKey_35, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_36 tape.Tag
fieldTag_36, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_35 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_36)) {
tape.Skim(decoder, fieldTag_36)
continue
}
var length_37 uint64
if fieldTag_36.Is(tape.LBA) {
length_37, nn, err = decoder.ReadUintN(int(fieldTag_36.CN()))
n += nn; if err != nil { return n, err }
} else {
length_37 = uint64(fieldTag_36.CN())
}
if length_37 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_37)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_36)
continue
}
}
return n, nil
}
func decodeBranch_68c536511e6d598462efc482144438e9_MessageLeaveNotify(this *MessageLeaveNotify, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_38 uint64
if length_38 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_38, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_38 {
var fieldKey_39 uint16
fieldKey_39, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_40 tape.Tag
fieldTag_40, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_39 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_40)) {
tape.Skim(decoder, fieldTag_40)
continue
}
var length_41 uint64
if fieldTag_40.Is(tape.LBA) {
length_41, nn, err = decoder.ReadUintN(int(fieldTag_40.CN()))
n += nn; if err != nil { return n, err }
} else {
length_41 = uint64(fieldTag_40.CN())
}
if length_41 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_41)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_40)
continue
}
}
return n, nil
}
// Receive decodes a message from a transaction and returns it as a value.
// Use a type switch to determine what type of message it is.
func Receive(trans hopp.Trans) (message ReceivedMessage, n int, err error) {
method, reader, err := trans.ReceiveReader()
if err != nil { return nil, n, err }
decoder := tape.NewDecoder(reader)
switch method {
case 0x0000:
var message MessageError
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0001:
var message MessageSuccess
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0200:
var message MessageJoin
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0300:
var message MessageChat
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0400:
var message MessageJoinNotify
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0401:
var message MessageLeaveNotify
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
}
return nil, n, fmt.Errorf("%w: M%04X", hopp.ErrUnknownMethod, method)
}

70
examples/chat/protocol.md Normal file
View File

@ -0,0 +1,70 @@
# Chat Protocol
This document describes a simple chat protocol. To re-generate the source files,
run `go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol`
## Messages
### 0000 Error
| Tag | Name | Type | Required |
| --: | ----------- | ------ | -------- |
| 0 | Code | U16 | Yes |
| 1 | Description | String | No |
Error is sent by a party when the other party has done something erroneous. The
valid error codes are:
- 0: General, unspecified error
The description field, if specified, determines a human-readable error to be
shown to the user. The sending party must immediately close the transaction
after this message is sent.
### 0001 Success
Success is sent by a party when it has successfully completed a task given to it
by the other party. The sending party must immediately close the transaction
after this message is sent.
### 0100 UpdateProfile
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
UpdateProfile is sent by the client in a new transaction to update the profile
details that will be shown to other connected clients.
### 0200 Join
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Room | String | Yes |
Join is sent by the client when it wishes to join a room. It must begin a new
transaction, and that transaction will persist while the user is in that room.
Messages having to do with the room will be sent along this transaction. To
leave the room, the client must close the transaction.
### 0201 Chat
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
| 1 | Content | String | Yes |
Chat is sent by the client when it wishes to post a message to the room. It is
also relayed by the server to other clients to notify them of the message. It
must be sent within a room transaction.
### 0300 JoinNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
JoinNotify is sent by the server when another client joins the room. It must be
sent within a room transaction.
### 0301 LeaveNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
LeaveNotify is sent by the server when another client leaves the room. It must
be sent within a room transaction.

View File

@ -1,48 +0,0 @@
// Error is sent by a party when the other party has done something erroneous. The
// valid error codes are:
//
// - 0: General, unspecified error
//
// The description field, if specified, determines a human-readable error to be
// shown to the user. The sending party must immediately close the transaction
// after this message is sent.
M0000 Error {
0000 Code U16,
0001 Description ?String,
}
// Success is sent by a party when it has successfully completed a task given to it
// by the other party. The sending party must immediately close the transaction
// after this message is sent.
M0001 Success {
}
// Join is sent by the client when it wishes to join a room. It must begin a new
// transaction, and that transaction will persist while the user is in that room.
// Messages having to do with the room will be sent along this transaction. To
// leave the room, the client must close the transaction.
M0200 Join {
0000 Room String,
0001 Nickname String,
}
// Chat is sent by the client when it wishes to post a message to the room. It is
// also relayed by the server to other clients to notify them of the message. It
// must be sent within a room transaction.
M0300 Chat {
0000 Content String,
0001 Nickname String,
}
// JoinNotify is sent by the server when another client joins the room. It must be
// sent within a room transaction.
M0400 JoinNotify {
0000 Nickname String,
}
// LeaveNotify is sent by the server when another client leaves the room. It must
// be sent within a room transaction.
M0401 LeaveNotify {
0000 Nickname String,
}

View File

@ -1,7 +1,6 @@
package main package main
import "os" import "os"
import "io"
import "fmt" import "fmt"
import "log" import "log"
import "errors" import "errors"
@ -29,7 +28,7 @@ func main() {
func host(address string, certPath, keyPath string) error { func host(address string, certPath, keyPath string) error {
keyPair, err := tls.LoadX509KeyPair(certPath, keyPath) keyPair, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil { return err } if err != nil { return err }
listener, err := hopp.Listen("tls", address, &tls.Config { listener, err := hopp.ListenQUIC("quic", address, &tls.Config {
InsecureSkipVerify: true, InsecureSkipVerify: true,
Certificates: []tls.Certificate { keyPair }, Certificates: []tls.Certificate { keyPair },
}) })
@ -49,7 +48,7 @@ func host(address string, certPath, keyPath string) error {
type client struct { type client struct {
conn hopp.Conn conn hopp.Conn
nickname string nickname hopp.Option[string]
rooms usync.RWMonitor[map[string] hopp.Trans] rooms usync.RWMonitor[map[string] hopp.Trans]
} }
@ -59,20 +58,19 @@ func (this *client) run() {
defer this.conn.Close() defer this.conn.Close()
for { for {
log.Println("accepting transaction")
trans, err := this.conn.AcceptTrans() trans, err := this.conn.AcceptTrans()
log.Println("accepted transaction")
if err != nil { if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("XXX %v failed: %v", this.conn.RemoteAddr(), err) log.Printf("XXX %v failed: %v", this.conn.RemoteAddr(), err)
} }
return
}
go this.runTrans(trans) go this.runTrans(trans)
} }
} }
func (this *client) runTrans(trans hopp.Trans) { func (this *client) runTrans(trans hopp.Trans) {
defer trans.Close() defer trans.Close()
message, _, err := chat.Receive(trans) message, err := chat.Receive(trans)
if err != nil { if err != nil {
log.Printf( log.Printf(
"XXX %v transaction failed: %v", "XXX %v transaction failed: %v",
@ -98,28 +96,21 @@ func (this *client) transTalk(trans hopp.Trans, initial *chat.MessageJoin) error
err := this.joinRoom(trans, room) err := this.joinRoom(trans, room)
if err != nil { return err } if err != nil { return err }
defer this.leaveRoom(trans, room) defer this.leaveRoom(trans, room)
_, err = chat.Send(trans, &chat.MessageChat {
Content: "(i) joined " + room,
Nickname: "SYSTEM",
})
if err != nil { return err }
for { for {
message, _, err := chat.Receive(trans) message, err := chat.Receive(trans)
if err != nil { return err } if err != nil { return err }
switch message := message.(type) { switch message := message.(type) {
case chat.MessageChat: case *chat.MessageChat:
err := this.handleMessageChat(trans, room, message) err := this.handleMessageChat(trans, room, message)
if err != nil { return err } if err != nil { return err }
case chat.MessageError: case *chat.MessageError:
return &message return message
} }
} }
} }
func (this *client) handleMessageChat(trans hopp.Trans, room string, message chat.MessageChat) error { func (this *client) handleMessageChat(trans hopp.Trans, room string, message *chat.MessageChat) error {
log.Printf("(). %s #%s: %s", this.nickname, room, message.Content) log.Println("(). %s #%s: %s", this.nickname.Default("Anonymous"), room, message.Content)
clients, done := clients.RBorrow() clients, done := clients.RBorrow()
defer done() defer done()
for client := range clients { for client := range clients {
@ -131,11 +122,11 @@ func (this *client) handleMessageChat(trans hopp.Trans, room string, message cha
return nil return nil
} }
func (this *client) relayMessage(room string, message chat.MessageChat) error { func (this *client) relayMessage(room string, message *chat.MessageChat) error {
rooms, done := this.rooms.RBorrow() rooms, done := this.rooms.RBorrow()
defer done() defer done()
if trans, ok := rooms[room]; ok { if trans, ok := rooms[room]; ok {
_, err := chat.Send(trans, &message) err := chat.Send(trans, message)
if err != nil { if err != nil {
return fmt.Errorf("could not relay message: %w", err) return fmt.Errorf("could not relay message: %w", err)
} }
@ -150,7 +141,7 @@ func (this *client) joinRoom(trans hopp.Trans, room string) error {
return fmt.Errorf("already joined %s", room) return fmt.Errorf("already joined %s", room)
} }
rooms[room] = trans rooms[room] = trans
log.Printf("--> user %s joined #%s", this.nickname, room) log.Printf("--> user %s joined #%s", this.nickname.Default("Anonymous"), room)
return nil return nil
} }
@ -161,7 +152,7 @@ func (this *client) leaveRoom(trans hopp.Trans, room string) error {
return fmt.Errorf("not in %s", room) return fmt.Errorf("not in %s", room)
} }
delete(rooms, room) delete(rooms, room)
log.Printf("<-- user %s left #%s", this.nickname, room) log.Printf("<-- user %s left #%s", this.nickname.Default("Anonymous"), room)
return nil return nil
} }

View File

@ -1,84 +0,0 @@
package main
import "io"
import "os"
import "log"
import "fmt"
import "time"
import "context"
import "git.tebibyte.media/sashakoshka/hopp"
// import "git.tebibyte.media/sashakoshka/hopp/examples/ping"
func main() {
name := os.Args[0]
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT\n", name)
os.Exit(2)
}
address := os.Args[1]
conn, err := dial(address)
handleErr(1, err)
trans, err := conn.OpenTrans()
handleErr(1, err)
go func() {
defer fmt.Fprintf(os.Stdout, "(i) disconnected\n")
for {
// message, _, err := ping.Receive(trans)
// if err != nil {
// if !errors.Is(err, io.EOF) {
// handleErr(1, err)
// }
// break
// }
// switch message := message.(type) {
// case *ping.MessagePong:
// log.Printf("--> pong (%d) from %v", message, address)
// }
method, reader, err := trans.ReceiveReader()
if err != nil {
log.Printf("CLIENT recv: %v", err)
return
}
data, err := io.ReadAll(reader)
if err != nil {
log.Printf("CLIENT recv: %v", err)
return
}
log.Println("CLIENT got", method, data)
}
}()
// message := ping.MessagePing(0)
// for {
// log.Printf("<-- ping (%d)", message)
// _, err := ping.Send(trans, &message)
// handleErr(1, err)
// message ++
// time.Sleep(time.Second)
// }
data := [1]byte { }
for {
log.Println("CLIENT send", 1, data)
err := trans.Send(1, data[:])
handleErr(1, err)
data[0] ++
time.Sleep(time.Second)
}
}
func dial(address string) (hopp.Conn, error) {
ctx, done := context.WithTimeout(context.Background(), 16 * time.Second)
defer done()
conn, err := hopp.Dial(ctx, "tcp", address, nil)
if err != nil { return nil, err }
return conn, nil
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}

View File

@ -1,97 +0,0 @@
package main
import "io"
import "os"
import "fmt"
import "log"
import "errors"
import "git.tebibyte.media/sashakoshka/hopp"
// import "git.tebibyte.media/sashakoshka/hopp/examples/ping"
var network = "tcp"
var addr = "localhost:7959"
func main() {
name := os.Args[0]
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT\n", name)
os.Exit(2)
}
address := os.Args[1]
err := listen(address)
handleErr(1, err)
}
func listen(addr string) error {
defer log.Println("(i) closing")
listener, err := hopp.Listen(network, addr, nil)
if err != nil { return err }
log.Printf("(i) hosting on %s", addr)
for {
conn, err := listener.Accept()
if err != nil { return err }
go run(conn)
}
}
func run(conn hopp.Conn) {
log.Printf("-=E %v connected", conn.RemoteAddr())
defer log.Printf("X=- %v disconnected", conn.RemoteAddr())
defer conn.Close()
for {
trans, err := conn.AcceptTrans()
if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("XXX %v failed: %v", conn.RemoteAddr(), err)
}
return
}
go runTrans(conn, trans)
}
}
func runTrans(conn hopp.Conn, trans hopp.Trans) {
defer trans.Close()
for {
// message, _, err := ping.Receive(trans)
// if err != nil {
// if !errors.Is(err, io.EOF) {
// log.Printf("XXX failed to receive message: %v", err)
// }
// return
// }
// switch message := message.(type) {
// case *ping.MessagePing:
// log.Printf("--> ping (%d) from %v", message, conn.RemoteAddr())
// response := ping.MessagePong(*message)
// _, err := ping.Send(trans, &response)
// if err != nil {
// log.Printf("XXX failed to send message: %v", err)
// return
// }
// }
method, reader, err := trans.ReceiveReader()
if err != nil { log.Println("SERVER", err); return }
data, err := io.ReadAll(reader)
if err != nil { log.Println("SERVER", err); return }
log.Println("SERVER got", method, data)
log.Println("SERVER send", method, data)
func (){
writer, err := trans.SendWriter(1)
if err != nil { log.Println("SERVER", err); return }
defer writer.Close()
_, err = writer.Write(data[:])
if err != nil { log.Println("SERVER", err); return }
}()
}
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}

View File

@ -1,65 +0,0 @@
package main
import "os"
import "io"
import "fmt"
import "log"
import "time"
import "errors"
import "context"
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/examples/ping"
func main() {
name := os.Args[0]
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT\n", name)
os.Exit(2)
}
address := os.Args[1]
conn, err := dial(address)
handleErr(1, err)
trans, err := conn.OpenTrans()
handleErr(1, err)
go func() {
message := ping.MessagePing(0)
for _ = range time.Tick(time.Second) {
log.Printf("<-- ping (%d)", message)
_, err := ping.Send(trans, &message)
handleErr(1, err)
message ++
}
}()
defer fmt.Fprintf(os.Stdout, "(i) disconnected\n")
for {
message, _, err := ping.Receive(trans)
if err != nil {
if !errors.Is(err, io.EOF) {
handleErr(1, err)
}
break
}
switch message := message.(type) {
case ping.MessagePong:
log.Printf("--> pong (%d) from %v", message, address)
}
}
}
func dial(address string) (hopp.Conn, error) {
ctx, done := context.WithTimeout(context.Background(), 16 * time.Second)
defer done()
conn, err := hopp.Dial(ctx, "tcp", address, nil)
if err != nil { return nil, err }
return conn, nil
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}

View File

@ -1,6 +0,0 @@
// Example ping demonstrates a simple ping/pong protocol.
package ping
// To use this in your own project, replace "go run ../../cmd/hopp-generate"
// with just "hopp-generate".
//go:generate go run ../../cmd/hopp-generate protocol.pdl -o protocol.go

View File

@ -1,159 +0,0 @@
package ping
// Code generated by the Holanet PDL compiler. DO NOT EDIT.
// The source file is located at <path>
// Please edit that file instead, and re-compile it to this location.
// HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
import "fmt"
import "git.tebibyte.media/sashakoshka/hopp"
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
}
// Send encodes a message and sends it along a transaction.
func Send(trans hopp.Trans, message Message) (n int, err error) {
writer, err := trans.SendWriter(message.Method())
if err != nil { return n, err }
defer writer.Close()
encoder := tape.NewEncoder(writer)
n, err = message.Encode(encoder)
if err != nil { return n, err }
return n, encoder.Flush()
}
// canAssign determines if data from the given source tag can be assigned to
// a Go type represented by destination. It is designed to receive destination
// values from [generate.Generator.generateCanAssign]. The eventual Go type and
// the destination tag must come from the same (or hash-equivalent) PDL type.
func canAssign(destination, source tape.Tag) bool {
if destination.Is(source) { return true }
if (destination.Is(tape.SBA) || destination.Is(tape.LBA)) &&
(source.Is(tape.SBA) || source.Is(tape.LBA)) {
return true
}
return false
}
// boolInt converts a bool to an integer.
func boolInt(input bool) int {
if input {
return 1
} else {
return 0
}
}
// ensure ucontainer is always imported
var _ hopp.Option[int]
// ReceivedMessage is a sealed interface representing the value of a
// message in this package. To determine what kind of message it is,
// use a type switch like this:
//
// switch message := message.(type) {
// case MessagePing:
// doSomething()
// case MessagePong:
// doSomething()
// }
type ReceivedMessage interface {
isReceivedMessage()
}
// Ping is sent by the client to the server. It may contain any number. This
// number will be returned to the client via a [Pong] message.
type MessagePing int32
// Method returns the message's method number.
func(this *MessagePing) Method() uint16 { return 0x0000 }
// Encode encodes this message's tag and value.
func(this *MessagePing) Encode(encoder *tape.Encoder) (n int, err error) {
tag_1 := tape.LSI.WithCN(3)
nn, err := encoder.WriteTag(tag_1)
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteInt32(int32((*this)))
n += nn; if err != nil { return n, err }
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessagePing) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.LSI, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
destination_2, nn, err := decoder.ReadInt32()
n += nn; if err != nil { return n, err }
*this = MessagePing(destination_2)
return n, nil
}
func (this MessagePing) isReceivedMessage() { }
// Pong is sent by the server to the client in response to a [Ping] message, It
// will contain the same number as that message.
type MessagePong int32
// Method returns the message's method number.
func(this *MessagePong) Method() uint16 { return 0x0001 }
// Encode encodes this message's tag and value.
func(this *MessagePong) Encode(encoder *tape.Encoder) (n int, err error) {
tag_3 := tape.LSI.WithCN(3)
nn, err := encoder.WriteTag(tag_3)
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteInt32(int32((*this)))
n += nn; if err != nil { return n, err }
return n, nil
}
// Decode decodes this message's tag and value.
func(this *MessagePong) Decode(decoder *tape.Decoder) (n int, err error) {
tag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if !(canAssign(tape.LSI, tag)) {
nn, err = tape.Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
destination_4, nn, err := decoder.ReadInt32()
n += nn; if err != nil { return n, err }
*this = MessagePong(destination_4)
return n, nil
}
func (this MessagePong) isReceivedMessage() { }
// Receive decodes a message from a transaction and returns it as a value.
// Use a type switch to determine what type of message it is.
func Receive(trans hopp.Trans) (message ReceivedMessage, n int, err error) {
method, reader, err := trans.ReceiveReader()
if err != nil { return nil, n, err }
decoder := tape.NewDecoder(reader)
switch method {
case 0x0000:
var message MessagePing
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0x0001:
var message MessagePong
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
}
return nil, n, fmt.Errorf("%w: M%04X", hopp.ErrUnknownMethod, method)
}

View File

@ -1,7 +0,0 @@
// Ping is sent by the client to the server. It may contain any number. This
// number will be returned to the client via a [Pong] message.
M0000 Ping I32
// Pong is sent by the server to the client in response to a [Ping] message, It
// will contain the same number as that message.
M0001 Pong I32

View File

@ -1,77 +0,0 @@
package main
import "os"
import "io"
import "fmt"
import "log"
import "errors"
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/examples/ping"
func main() {
name := os.Args[0]
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s HOST:PORT\n", name)
os.Exit(2)
}
address := os.Args[1]
err := listen(address)
handleErr(1, err)
}
func listen(address string) error {
listener, err := hopp.Listen("tcp", address, nil)
if err != nil { return err }
log.Printf("(i) hosting on %s", address)
for {
conn, err := listener.Accept()
if err != nil { return err }
go run(conn)
}
}
func run(conn hopp.Conn) {
log.Printf("-=E %v connected", conn.RemoteAddr())
defer log.Printf("X=- %v disconnected", conn.RemoteAddr())
defer conn.Close()
for {
trans, err := conn.AcceptTrans()
if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("XXX %v failed: %v", conn.RemoteAddr(), err)
}
return
}
go runTrans(conn, trans)
}
}
func runTrans(conn hopp.Conn, trans hopp.Trans) {
for {
message, _, err := ping.Receive(trans)
if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("XXX failed to receive message: %v", err)
}
return
}
switch message := message.(type) {
case ping.MessagePing:
log.Printf("--> ping (%d) from %v", message, conn.RemoteAddr())
response := ping.MessagePong(message)
_, err := ping.Send(trans, &response)
if err != nil {
log.Printf("XXX failed to send message: %v", err)
return
}
}
}
}
func handleErr(code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(code)
}
}

View File

@ -11,8 +11,6 @@ import "git.tebibyte.media/sashakoshka/hopp/tape"
const imports = const imports =
` `
import "fmt"
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/tape" import "git.tebibyte.media/sashakoshka/hopp/tape"
` `
@ -21,6 +19,7 @@ const preamble = `
// The source file is located at <path> // The source file is located at <path>
// Please edit that file instead, and re-compile it to this location. // Please edit that file instead, and re-compile it to this location.
// HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz // HOPP, TAPE, METADAPT, PDL/0 (c) 2025 holanet.xyz
` `
const static = ` const static = `
@ -36,17 +35,6 @@ type Message interface {
Method() uint16 Method() uint16
} }
// Send encodes a message and sends it along a transaction.
func Send(trans hopp.Trans, message Message) (n int, err error) {
writer, err := trans.SendWriter(message.Method())
if err != nil { return n, err }
defer writer.Close()
encoder := tape.NewEncoder(writer)
n, err = message.Encode(encoder)
if err != nil { return n, err }
return n, encoder.Flush()
}
// canAssign determines if data from the given source tag can be assigned to // canAssign determines if data from the given source tag can be assigned to
// a Go type represented by destination. It is designed to receive destination // a Go type represented by destination. It is designed to receive destination
// values from [generate.Generator.generateCanAssign]. The eventual Go type and // values from [generate.Generator.generateCanAssign]. The eventual Go type and
@ -59,18 +47,6 @@ func canAssign(destination, source tape.Tag) bool {
} }
return false return false
} }
// boolInt converts a bool to an integer.
func boolInt(input bool) int {
if input {
return 1
} else {
return 0
}
}
// ensure ucontainer is always imported
var _ hopp.Option[int]
` `
// Generator converts protocols into Go code. // Generator converts protocols into Go code.
@ -114,8 +90,6 @@ func (this *Generator) Generate(protocol *Protocol) (n int, err error) {
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// type definitions // type definitions
nn, err = this.generateMessageValueInterface()
n += nn; if err != nil { return n, err }
for _, name := range slices.Sorted(maps.Keys(protocol.Types)) { for _, name := range slices.Sorted(maps.Keys(protocol.Types)) {
nn, err := this.generateTypedef(name, protocol.Types[name]) nn, err := this.generateTypedef(name, protocol.Types[name])
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -135,63 +109,16 @@ func (this *Generator) Generate(protocol *Protocol) (n int, err error) {
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
} }
// receive
nn, err = this.generateReceive()
n += nn; if err != nil { return n, err }
return n, nil return n, nil
} }
func (this *Generator) generateMessageValueInterface() (n int, err error) { func (this *Generator) generateTypedef(name string, typ Type) (n int, err error) {
keys := slices.Sorted(maps.Keys(this.protocol.Messages))
nn, err := this.iprint(
"\n// ReceivedMessage is a sealed interface representing the value of a\n" +
"// message in this package. To determine what kind of message it is,\n" +
"// use a type switch like this:\n" +
"// \n" +
"// switch message := message.(type) {\n")
n += nn; if err != nil { return n, err }
for index, method := range keys {
if index > 2 {
nn, err := this.iprint("// \n// ...\n// \n")
n += nn; if err != nil { return n, err }
break
}
nn, err := this.iprintf("// case %s:\n",
this.resolveMessageName(this.protocol.Messages[method].Name))
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("// doSomething()\n")
n += nn; if err != nil { return n, err }
}
nn, err = this.iprintf("// }\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("type ReceivedMessage interface {\n")
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf("isReceivedMessage()\n")
n += nn; if err != nil { return n, err }
this.pop()
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
return n, nil
}
func (this *Generator) generateTypedef(name string, typedef Typedef) (n int, err error) {
typ := typedef.Type
// type definition // type definition
if typedef.Doc == "" {
nn, err := this.iprintf( nn, err := this.iprintf(
"\n// %s represents the protocol data type %s.\n", "\n// %s represents the protocol data type %s.\n",
name, name) name, name)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
} else { nn, err = this.iprintf("type %s ", name)
nn, err := this.iprintf("\n%s\n", this.formatComment(typedef.Doc))
n += nn; if err != nil { return n, err }
}
nn, err := this.iprintf("type %s ", name)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateType(typ) nn, err = this.generateType(typ)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -281,16 +208,10 @@ func (this *Generator) generateTypedef(name string, typedef Typedef) (n int, err
// generateMessage generates the structure, as well as encoding decoding // generateMessage generates the structure, as well as encoding decoding
// functions for the given message. // functions for the given message.
func (this *Generator) generateMessage(method uint16, message Message) (n int, err error) { func (this *Generator) generateMessage(method uint16, message Message) (n int, err error) {
if message.Doc == "" {
nn, err := this.iprintf( nn, err := this.iprintf(
"\n// %s represents the protocol message M%04X %s.\n", "\n// %s represents the protocol message M%04X %s.\n",
message.Name, method, message.Name) message.Name, method, message.Name)
n += nn; if err != nil { return n, err } nn, err = this.iprintf("type %s ", this.resolveMessageName(message.Name))
} else {
nn, err := this.iprintf("\n%s\n", this.formatComment(message.Doc))
n += nn; if err != nil { return n, err }
}
nn, err := this.iprintf("type %s ", this.resolveMessageName(message.Name))
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateType(message.Type) nn, err = this.generateType(message.Type)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -366,10 +287,6 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// isReceivedMessage method
nn, err = this.iprintf("func (this %s) isReceivedMessage() { }\n", this.resolveMessageName(message.Name))
n += nn; if err != nil { return n, err }
return n, nil return n, nil
} }
@ -385,9 +302,6 @@ func (this *Generator) generateMessage(method uint16, message Message) (n int, e
// - nn int // - nn int
func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) { func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource string) (n int, err error) {
switch typ := typ.(type) { switch typ := typ.(type) {
case TypeBool:
// SI: (none)
// SI stores the value in the tag, so we write nothing here
case TypeInt: case TypeInt:
// SI: (none) // SI: (none)
// LI/LSI: <value: IntN> // LI/LSI: <value: IntN>
@ -540,33 +454,19 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
this.push() this.push()
for key, field := range typ.Fields { for key, field := range typ.Fields {
fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name)
if field.Option {
nn, err = this.iprintf("if value, ok := %s.Value(); ok {\n", fieldSource)
n += nn; if err != nil { return n, err }
fieldSource = "value"
this.push()
}
nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key) nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name)
tagVar, nn, err := this.generateTag(field.Type, fieldSource) tagVar, nn, err := this.generateTag(field.Type, fieldSource)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(%s))\n", tagVar) nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(%s))\n", tagVar)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateEncodeValue(field.Type, fieldSource, tagVar) nn, err = this.generateEncodeValue(field.Type, fieldSource, tagVar)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
if field.Option {
this.pop()
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
}
} }
this.pop() this.pop()
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
@ -605,11 +505,6 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
// for [Generator.generateDecodeBranch]. // for [Generator.generateDecodeBranch].
func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagSource string) (n int, err error) { func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagSource string) (n int, err error) {
switch typ := typ.(type) { switch typ := typ.(type) {
case TypeBool:
// SI: (none)
// SI stores the value in the tag
nn, err := this.iprintf("*%s = %s.CN() > 0\n", valueSource, tagSource)
n += nn; if err != nil { return n, err }
case TypeInt: case TypeInt:
// SI: (none) // SI: (none)
// LI/LSI: <value: IntN> // LI/LSI: <value: IntN>
@ -629,7 +524,13 @@ func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagS
prefix = "ReadInt" prefix = "ReadInt"
} }
destinationVar := this.newTemporaryVar("destination") destinationVar := this.newTemporaryVar("destination")
nn, err := this.iprintf("%s, nn, err := decoder.%s%d()\n", destinationVar, prefix, typ.Bits) nn, err := this.iprintf("var %s ", destinationVar)
n += nn; if err != nil { return n, err }
nn, err = this.generateType(typ)
n += nn; if err != nil { return n, err }
nn, err = this.print("\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("%s, nn, err = decoder.%s%d()\n", destinationVar, prefix, typ.Bits)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -643,7 +544,13 @@ func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagS
case TypeFloat: case TypeFloat:
// FP: <value: FloatN> // FP: <value: FloatN>
destinationVar := this.newTemporaryVar("destination") destinationVar := this.newTemporaryVar("destination")
nn, err := this.iprintf("%s, nn, err := decoder.ReadFloat%d()\n", destinationVar, typ.Bits) nn, err := this.iprintf("var %s ", destinationVar)
n += nn; if err != nil { return n, err }
nn, err = this.generateType(typ)
n += nn; if err != nil { return n, err }
nn, err = this.print("\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("%s, nn, err = decoder.ReadFloat%d()\n", destinationVar, typ.Bits)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -866,7 +773,6 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
case TypeTableDefined: case TypeTableDefined:
// TODO: options
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)* // KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
// read header // read header
lengthVar := this.newTemporaryVar("length") lengthVar := this.newTemporaryVar("length")
@ -943,25 +849,10 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// decode payload // decode payload
if field.Option {
destination := this.newTemporaryVar("destination")
nn, err = this.iprintf("var %s ", destination)
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.printf("\n")
n += nn; if err != nil { return n, err }
nn, err = this.generateDecodeValue(
field.Type, "", fmt.Sprintf("(&%s)", destination), fieldTagVar)
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("this.%s = hopp.O(%s)\n", field.Name, destination)
n += nn; if err != nil { return n, err }
} else {
nn, err = this.generateDecodeValue( nn, err = this.generateDecodeValue(
field.Type, "", field.Type, "",
fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar) fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
}
this.pop() this.pop()
} }
nn, err = this.iprintf("default:\n") nn, err = this.iprintf("default:\n")
@ -974,6 +865,16 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
this.pop() this.pop()
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// TODO once options are implemented, have a set of
// bools for each non-optional field, and check here
// that they are all true. a counter will not work
// because if someone specifies a non-optional field
// twice, they can neglect to specify another
// non-optional field and we won't even know because the
// count will still be even. we shouldn't use a map
// either because its an allocation and its way more
// memory than just, like 5 bools (on the stack no less)
default: return n, fmt.Errorf("unexpected type: %T", typ) default: return n, fmt.Errorf("unexpected type: %T", typ)
} }
@ -1030,9 +931,6 @@ func (this *Generator) generateBareErrorCheck() (n int, err error) {
func (this *Generator) generateTag(typ Type, source string) (tagVar string, n int, err error) { func (this *Generator) generateTag(typ Type, source string) (tagVar string, n int, err error) {
tagVar = this.newTemporaryVar("tag") tagVar = this.newTemporaryVar("tag")
switch typ := typ.(type) { switch typ := typ.(type) {
case TypeBool:
nn, err := this.iprintf("%s := tape.SI.WithCN(boolInt(bool(%s)))\n", tagVar, source)
n += nn; if err != nil { return tagVar, n, err }
case TypeInt: case TypeInt:
if typ.Bits <= 5 { if typ.Bits <= 5 {
nn, err := this.iprintf("%s := tape.SI.WithCN(int(%s))\n", tagVar, source) nn, err := this.iprintf("%s := tape.SI.WithCN(int(%s))\n", tagVar, source)
@ -1086,9 +984,6 @@ func (this *Generator) generateTag(typ Type, source string) (tagVar string, n in
// information is chosen. // information is chosen.
func (this *Generator) generateTN(typ Type) (n int, err error) { func (this *Generator) generateTN(typ Type) (n int, err error) {
switch typ := typ.(type) { switch typ := typ.(type) {
case TypeBool:
nn, err := this.printf("tape.SI")
n += nn; if err != nil { return n, err }
case TypeInt: case TypeInt:
if typ.Bits <= 5 { if typ.Bits <= 5 {
nn, err := this.printf("tape.SI") nn, err := this.printf("tape.SI")
@ -1132,9 +1027,6 @@ func (this *Generator) generateTN(typ Type) (n int, err error) {
func (this *Generator) generateType(typ Type) (n int, err error) { func (this *Generator) generateType(typ Type) (n int, err error) {
switch typ := typ.(type) { switch typ := typ.(type) {
case TypeBool:
nn, err := this.printf("bool")
n += nn; if err != nil { return n, err }
case TypeInt: case TypeInt:
if err := this.validateIntBitSize(typ.Bits); err != nil { if err := this.validateIntBitSize(typ.Bits); err != nil {
return n, err return n, err
@ -1198,22 +1090,10 @@ func (this *Generator) generateTypeTableDefined(typ TypeTableDefined) (n int, er
for _, key := range slices.Sorted(maps.Keys(typ.Fields)) { for _, key := range slices.Sorted(maps.Keys(typ.Fields)) {
field := typ.Fields[key] field := typ.Fields[key]
if field.Doc != "" { nn, err := this.iprintf("%s ", field.Name)
nn, err := this.iprintf("%s\n", this.formatComment(field.Doc))
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
}
nn, err = this.iprintf("%s ", field.Name)
n += nn; if err != nil { return n, err }
if field.Option {
nn, err = this.print("hopp.Option[")
n += nn; if err != nil { return n, err }
}
nn, err = this.generateType(field.Type) nn, err = this.generateType(field.Type)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
if field.Option {
nn, err = this.print("]")
n += nn; if err != nil { return n, err }
}
nn, err = this.print("\n") nn, err = this.print("\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
} }
@ -1242,53 +1122,6 @@ func (this *Generator) generateCanAssign(typ Type, tagSource string) (n int, err
return n, nil return n, nil
} }
// generateReceive generates a function Receive(hopp.Trans) (Message, int, error)
func (this *Generator) generateReceive() (n int, err error) {
nn, err := this.iprintf(
"\n// Receive decodes a message from a transaction and returns it as a value.\n" +
"// Use a type switch to determine what type of message it is.\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf(
"func Receive(trans hopp.Trans) (message ReceivedMessage, n int, err error) {\n")
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf("method, reader, err := trans.ReceiveReader()\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("if err != nil { return nil, n, err }\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("decoder := tape.NewDecoder(reader)\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("switch method {\n")
n += nn; if err != nil { return n, err }
for method, message := range this.protocol.Messages {
nn, err = this.iprintf("case 0x%04X:\n", method)
n += nn; if err != nil { return n, err }
this.push()
nn, err = this.iprintf(
"var message %s\n",
this.resolveMessageName(message.Name))
n += nn; if err != nil { return n, err }
nn, err := this.iprintf(
"nn, err := message.Decode(decoder)\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("n += nn; if err != nil { return nil, n, err }\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("return message, n, nil\n")
n += nn; if err != nil { return n, err }
this.pop()
}
nn, err = this.iprint("}\n")
n += nn; if err != nil { return n, err }
// fuck off go vet
str := `return nil, n, fmt.Errorf("%w: M%04X", hopp.ErrUnknownMethod, method)`
nn, err = this.iprintln(str)
n += nn; if err != nil { return n, err }
this.pop()
nn, err = this.iprint("}\n")
n += nn; if err != nil { return n, err }
return n, nil
}
func (this *Generator) validateIntBitSize(size int) error { func (this *Generator) validateIntBitSize(size int) error {
switch size { switch size {
case 5, 8, 16, 32, 64: return nil case 5, 8, 16, 32, 64: return nil
@ -1342,21 +1175,17 @@ func (this *Generator) iprintf(format string, args ...any) (n int, err error) {
return fmt.Fprintf(this.Output, this.indent() + format, args...) return fmt.Fprintf(this.Output, this.indent() + format, args...)
} }
func (this *Generator) formatComment(comment string) string {
return "// " + strings.ReplaceAll(comment, "\n", "\n" + this.indent() + "// ")
}
func (this *Generator) resolveMessageName(message string) string { func (this *Generator) resolveMessageName(message string) string {
return "Message" + message return "Message" + message
} }
func (this *Generator) resolveTypeName(name string) (Type, error) { func (this *Generator) resolveTypeName(name string) (Type, error) {
if typedef, ok := this.protocol.Types[name]; ok { if typ, ok := this.protocol.Types[name]; ok {
if typ, ok := typedef.Type.(TypeNamed); ok { if typ, ok := typ.(TypeNamed); ok {
return this.resolveTypeName(typ.Name) return this.resolveTypeName(typ.Name)
} }
return typedef.Type, nil return typ, nil
} }
return nil, fmt.Errorf("no type exists called %s", name) return nil, fmt.Errorf("no type exists called %s", name)
} }

View File

@ -7,10 +7,8 @@ import "testing"
// generator is equal to something specific // generator is equal to something specific
var exampleProtocol = defaultProtocol() var exampleProtocol = defaultProtocol()
var pingProtocol = defaultProtocol()
func init() { func init() {
// example protocol
exampleProtocol.Messages[0x0000] = Message { exampleProtocol.Messages[0x0000] = Message {
Name: "Connect", Name: "Connect",
Type: TypeTableDefined { Type: TypeTableDefined {
@ -61,7 +59,6 @@ func init() {
0x000C: Field { Name: "NI16",Type: TypeInt { Bits: 16, Signed: true } }, 0x000C: Field { Name: "NI16",Type: TypeInt { Bits: 16, Signed: true } },
0x000D: Field { Name: "NI32",Type: TypeInt { Bits: 32, Signed: true } }, 0x000D: Field { Name: "NI32",Type: TypeInt { Bits: 32, Signed: true } },
0x000E: Field { Name: "NI64",Type: TypeInt { Bits: 64, Signed: true } }, 0x000E: Field { Name: "NI64",Type: TypeInt { Bits: 64, Signed: true } },
0x000F: Field { Name: "Bool",Type: TypeBool { } },
}, },
}, },
} }
@ -86,52 +83,12 @@ func init() {
}, },
}, },
} }
exampleProtocol.Messages[0x0006] = Message { exampleProtocol.Types["User"] = TypeTableDefined {
Name: "Option",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "OU5", Option: true, Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "OU8", Option: true, Type: TypeInt { Bits: 8 } },
0x0002: Field { Name: "OU16", Option: true, Type: TypeInt { Bits: 16 } },
0x0003: Field { Name: "OU32", Option: true, Type: TypeInt { Bits: 32 } },
0x0004: Field { Name: "OU64", Option: true, Type: TypeInt { Bits: 64 } },
0x0005: Field { Name: "OI8", Option: true, Type: TypeInt { Bits: 8, Signed: true } },
0x0006: Field { Name: "OI16", Option: true, Type: TypeInt { Bits: 16, Signed: true } },
0x0007: Field { Name: "OI32", Option: true, Type: TypeInt { Bits: 32, Signed: true } },
0x0008: Field { Name: "OI64", Option: true, Type: TypeInt { Bits: 64, Signed: true } },
0x0009: Field { Name: "OF32", Option: true, Type: TypeFloat { Bits: 32 } },
0x000A: Field { Name: "OF64", Option: true, Type: TypeFloat { Bits: 64 } },
0x000B: Field { Name: "OBool", Option: true, Type: TypeBool { } },
0x000C: Field { Name: "OString", Option: true, Type: TypeString { } },
0x000D: Field { Name: "OArray", Option: true, Type: TypeArray {
Element: TypeInt { Bits: 16, Signed: true } } },
0x000E: Field { Name: "OTable", Option: true, Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0001: Field { Name: "A", Type: TypeInt { Bits: 8, Signed: true }, },
0x0002: Field { Name: "B", Type: TypeFloat { Bits: 64 }, },
} } },
0x000F: Field { Name: "T0", Option: true, Type: TypeTable { } },
},
},
}
exampleProtocol.Types["User"] = Typedef {
Type: TypeTableDefined {
Fields: map[uint16] Field { Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } }, 0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } }, 0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
}, },
},
}
// ping protocol
pingProtocol.Messages[0x0000] = Message {
Name: "Ping",
Type: TypeInt { Bits: 32, Signed: true },
}
pingProtocol.Messages[0x0001] = Message {
Name: "Pong",
Type: TypeInt { Bits: 32, Signed: true },
} }
} }
@ -242,11 +199,10 @@ func TestGenerateRunEncodeDecode(test *testing.T) {
NI16: -0x34C9, NI16: -0x34C9,
NI32: -0x10E134C9, NI32: -0x10E134C9,
NI64: -0x639109BC10E134C9, NI64: -0x639109BC10E134C9,
Bool: true,
} }
testEncodeDecode( testEncodeDecode(
&messageIntegers, &messageIntegers,
tu.S(0xE0, 14).AddVar( tu.S(0xE0, 13).AddVar(
[]byte { 0x00, 0x00, 0x13 }, []byte { 0x00, 0x00, 0x13 },
[]byte { 0x00, 0x01, 0x20, 0xC9 }, []byte { 0x00, 0x01, 0x20, 0xC9 },
[]byte { 0x00, 0x02, 0x21, 0x34, 0xC9 }, []byte { 0x00, 0x02, 0x21, 0x34, 0xC9 },
@ -260,7 +216,6 @@ func TestGenerateRunEncodeDecode(test *testing.T) {
[]byte { 0x00, 0x0C, 0x41, 0xCB, 0x37 }, []byte { 0x00, 0x0C, 0x41, 0xCB, 0x37 },
[]byte { 0x00, 0x0D, 0x43, 0xEF, 0x1E, 0xCB, 0x37 }, []byte { 0x00, 0x0D, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
[]byte { 0x00, 0x0E, 0x47, 0x9C, 0x6E, 0xF6, 0x43, 0xEF, 0x1E, 0xCB, 0x37 }, []byte { 0x00, 0x0E, 0x47, 0x9C, 0x6E, 0xF6, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
[]byte { 0x00, 0x0F, 0x01 },
)) ))
log.Println("MessageDynamic") log.Println("MessageDynamic")
messageDynamic := MessageDynamic { messageDynamic := MessageDynamic {
@ -288,32 +243,31 @@ func TestGenerateRunEncodeDecode(test *testing.T) {
} }
testEncodeDecode( testEncodeDecode(
&messageDynamic, &messageDynamic,
snake.O().AddL(0xE0, 14).AddS( tu.S(0xE0, 14).AddVar(
snake.L(0x00, 0x00, 0x20, 0x23), []byte { 0x00, 0x00, 0x20, 0x23 },
snake.L(0x00, 0x01, 0x21, 0x32, 0x47), []byte { 0x00, 0x01, 0x21, 0x32, 0x47 },
snake.L(0x00, 0x02, 0x23, 0x87, 0x32, 0x45, 0x23), []byte { 0x00, 0x02, 0x23, 0x87, 0x32, 0x45, 0x23 },
snake.L(0x00, 0x03, 0x27, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34), []byte { 0x00, 0x03, 0x27, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34 },
snake.L(0x00, 0x04, 0x40, 0x23), []byte { 0x00, 0x04, 0x40, 0x23 },
snake.L(0x00, 0x05, 0x41, 0x32, 0x47), []byte { 0x00, 0x05, 0x41, 0x32, 0x47 },
snake.L(0x00, 0x06, 0x43, 0x57, 0x32, 0x45, 0x23), []byte { 0x00, 0x06, 0x43, 0x57, 0x32, 0x45, 0x23 },
snake.L(0x00, 0x07, 0x47, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34), []byte { 0x00, 0x07, 0x47, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34 },
snake.L(0x00, 0x08, 0x63, 0x45, 0x12, 0x63, 0xCE), []byte { 0x00, 0x08, 0x63, 0x45, 0x12, 0x63, 0xCE },
snake.L(0x00, 0x09, 0x67, 0x40, 0x74, 0x4E, 0x3D, 0x6F, 0xCD, 0x17, 0x75), []byte { 0x00, 0x09, 0x67, 0x40, 0x74, 0x4E, 0x3D, 0x6F, 0xCD, 0x17, 0x75 },
snake.L(0x00, 0x0A, 0x87, 'f', 'o', 'x', ' ', 'b', 'e', 'd'), []byte { 0x00, 0x0A, 0x87, 'f', 'o', 'x', ' ', 'b', 'e', 'd' },
snake.L(0x00, 0x0B, 0xC0, 0x04, 0x41, []byte { 0x00, 0x0B, 0xC0, 0x04, 0x41,
0x00, 0x07, 0x00, 0x07,
0x00, 0x06, 0x00, 0x06,
0x00, 0x05, 0x00, 0x05,
0x00, 0x04), 0x00, 0x04 },
snake.L(0x00, 0x0C, 0xE0, 0x02, []byte { 0x00, 0x0C, 0xE0, 0x02,
0x00, 0x01, 0x40, 0x08, 0x00, 0x01, 0x40, 0x08,
0x00, 0x02, 0x67, 0x40, 0x11, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A), 0x00, 0x02, 0x67, 0x40, 0x11, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A },
snake.O(snake.L(0x00, 0x0D, 0xE0, 0x03), []byte { 0x00, 0x0D, 0xE0, 0x03, // ERR
snake.S( 0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00,
snake.L(0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00), 0x00, 0x02, 0x82, 'h', 'i',
snake.L(0x00, 0x02, 0x82, 'h', 'i'), 0x00, 0x03, 0x21, 0x39, 0x92 },
snake.L(0x00, 0x03, 0x21, 0x39, 0x92)), ))
)))
`) `)
} }
@ -497,119 +451,3 @@ func TestGenerateRunDecodeWrongType(test *testing.T) {
} }
`) `)
} }
func TestGenerateRunSendReceive(test *testing.T) {
testGenerateRun(test, &pingProtocol, "send-receive", `
// imports
import "git.tebibyte.media/sashakoshka/hopp/internal/mock"
`, `
log.Println("Send"); {
message := MessagePing(77)
trans := mock.Trans { }
_, err := Send(&trans, &message)
if err != nil { log.Fatal(err) }
gotMethod, gotPayload := trans.Methods[0], trans.Messages[0]
log.Printf("method M%04X", gotMethod)
log.Println("payload", tu.HexBytes(gotPayload))
if gotMethod != 0x0000 {
log.Fatalln("wrong method")
}
if ok, n := snake.L(0x43, 0x00, 0x00, 0x00, 0x4D).Check(gotPayload); !ok {
log.Fatalln("not equal at:", n)
}
}
log.Println("Receive"); {
trans := mock.Trans {
Methods: []uint16 { 1 },
Messages: [][]byte { []byte { 0x43, 0x00, 0x00, 0x00, 0x4E } },
}
gotMessage, n, err := Receive(&trans)
if err != nil { log.Fatal(err) }
log.Println("message", gotMessage)
log.Println("n", n)
casted, ok := gotMessage.(MessagePong)
if !ok { log.Fatalln("expected MessagePong") }
if casted != 78 { log.Fatalln("wrong message value") }
if n != 5 { log.Fatalln("wrong n value") }
}
`)
}
func TestGenerateRunConn(test *testing.T) {
testGenerateRun(test, &pingProtocol, "send-receive", `
// imports
import "sync"
import "context"
import "git.tebibyte.media/sashakoshka/hopp"
`, `
group := sync.WaitGroup { }
group.Add(2)
// server
listener, err := hopp.Listen("tcp", "localhost:43957", nil)
if err != nil { log.Fatalln("SERVER listen:", err) }
go func() {
defer listener.Close()
defer group.Done()
conn, err := listener.Accept()
if err != nil { log.Fatalln("SERVER accept:", err) }
trans, err := conn.AcceptTrans()
if err != nil { log.Fatalln("SERVER accept trans:", err) }
message, n, err := Receive(trans)
if err != nil { log.Fatalln("SERVER receive:", err) }
log.Println("SERVER got message", message)
log.Println("SERVER got n", n)
casted, ok := message.(MessagePing)
if !ok { log.Fatalln("SERVER expected MessagePong") }
if casted != 77 { log.Fatalln("SERVER wrong message value") }
if n != 5 { log.Fatalln("SERVER wrong n value") }
message, n, err = Receive(trans)
if err != nil { log.Fatalln("SERVER receive:", err) }
log.Println("SERVER got message", message)
log.Println("SERVER got n", n)
casted, ok = message.(MessagePing)
if !ok { log.Fatalln("SERVER expected MessagePong") }
if casted != 78 { log.Fatalln("SERVER wrong message value") }
if n != 5 { log.Fatalln("SERVER wrong n value") }
}()
// client
go func() {
defer group.Done()
log.Println("CLIENT dialing")
conn, err := hopp.Dial(
context.Background(),
"tcp", "localhost:43957",
nil)
if err != nil { log.Fatalln("CLIENT dial:", err) }
defer conn.Close()
log.Println("CLIENT connected")
log.Println("CLIENT opening trans")
trans, err := conn.OpenTrans()
if err != nil { log.Fatalln("CLIENT open trans:", err) }
message := MessagePing(77)
log.Println("CLIENT sending message")
n, err := Send(trans, &message)
if err != nil { log.Fatalln("CLIENT send:", err) }
log.Println("CLIENT sent n", n)
if n != 5 { log.Fatalln("CLIENT wrong n value") }
message = MessagePing(78)
log.Println("CLIENT sending message")
n, err = Send(trans, &message)
if err != nil { log.Fatalln("CLIENT send:", err) }
log.Println("CLIENT sent n", n)
if n != 5 { log.Fatalln("CLIENT wrong n value") }
}()
group.Wait()
`)
}

View File

@ -10,26 +10,22 @@ const (
TokenMethod parse.TokenKind = iota TokenMethod parse.TokenKind = iota
TokenKey TokenKey
TokenIdent TokenIdent
TokenOption
TokenComma TokenComma
TokenLBrace TokenLBrace
TokenRBrace TokenRBrace
TokenLBracket TokenLBracket
TokenRBracket TokenRBracket
TokenComment
) )
var tokenNames = map[parse.TokenKind] string { var tokenNames = map[parse.TokenKind] string {
TokenMethod: "Method", TokenMethod: "Method",
TokenKey: "Key", TokenKey: "Key",
TokenIdent: "Ident", TokenIdent: "Ident",
TokenOption: "Option",
TokenComma: "Comma", TokenComma: "Comma",
TokenLBrace: "LBrace", TokenLBrace: "LBrace",
TokenRBrace: "RBrace", TokenRBrace: "RBrace",
TokenLBracket: "LBracket", TokenLBracket: "LBracket",
TokenRBracket: "RBracket", TokenRBracket: "RBracket",
TokenComment: "Comment",
} }
func Lex(fileName string, reader io.Reader) (parse.Lexer, error) { func Lex(fileName string, reader io.Reader) (parse.Lexer, error) {
@ -85,18 +81,6 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
} }
} }
unexpected := func() error {
if unicode.IsPrint(this.rune) {
return parse.Errorf (
this.pos(), "unexpected rune '%c'",
this.rune)
} else {
return parse.Errorf (
this.pos(), "unexpected rune %U",
this.rune)
}
}
defer func () { defer func () {
newPos := this.pos() newPos := this.pos()
newPos.End -- // TODO figure out why tf we have to do this newPos.End -- // TODO figure out why tf we have to do this
@ -124,11 +108,6 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
if this.eof { err = nil; return } if this.eof { err = nil; return }
if err != nil { return } if err != nil { return }
} }
// Option
case this.rune == '?':
token.Kind = TokenOption
appendRune()
if this.eof { err = nil; return }
// Comma // Comma
case this.rune == ',': case this.rune == ',':
token.Kind = TokenComma token.Kind = TokenComma
@ -154,21 +133,14 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
token.Kind = TokenRBracket token.Kind = TokenRBracket
appendRune() appendRune()
if this.eof { err = nil; return } if this.eof { err = nil; return }
// Comment case unicode.IsPrint(this.rune):
case this.rune == '/': err = parse.Errorf (
token.Kind = TokenComment this.pos(), "unexpected rune '%c'",
appendRune() this.rune)
if this.eof { return }
if this.rune != '/' {
err = unexpected()
return
}
for this.rune != '\n' {
appendRune()
if this.eof { err = nil; return }
}
default: default:
err = unexpected() err = parse.Errorf (
this.pos(), "unexpected rune %U",
this.rune)
} }
return return

View File

@ -6,22 +6,14 @@ import "git.tebibyte.media/sashakoshka/goparse"
func TestLex(test *testing.T) { func TestLex(test *testing.T) {
lexer, err := Lex("test.pdl", strings.NewReader(` lexer, err := Lex("test.pdl", strings.NewReader(`
// User holds profile information about a single user.
M0001 User { M0001 User {
0000 Name String, 0000 Name String,
// dog water comment
// Users is asdkjsagkj why
//
// wow
0001 Users []User, 0001 Users []User,
0002 Followers U32, 0002 Followers U32,
0003 Wings ?Int,
}`)) }`))
if err != nil { test.Fatal(parse.Format(err)) } if err != nil { test.Fatal(parse.Format(err)) }
correctTokens := []parse.Token { correctTokens := []parse.Token {
tok(TokenComment, "// User holds profile information about a single user."),
tok(TokenMethod, "0001"), tok(TokenMethod, "0001"),
tok(TokenIdent, "User"), tok(TokenIdent, "User"),
tok(TokenLBrace, "{"), tok(TokenLBrace, "{"),
@ -29,10 +21,6 @@ func TestLex(test *testing.T) {
tok(TokenIdent, "Name"), tok(TokenIdent, "Name"),
tok(TokenIdent, "String"), tok(TokenIdent, "String"),
tok(TokenComma, ","), tok(TokenComma, ","),
tok(TokenComment, "// dog water comment"),
tok(TokenComment, "// Users is asdkjsagkj why"),
tok(TokenComment, "// "),
tok(TokenComment, "// wow"),
tok(TokenKey, "0001"), tok(TokenKey, "0001"),
tok(TokenIdent, "Users"), tok(TokenIdent, "Users"),
tok(TokenLBracket, "["), tok(TokenLBracket, "["),
@ -43,11 +31,6 @@ func TestLex(test *testing.T) {
tok(TokenIdent, "Followers"), tok(TokenIdent, "Followers"),
tok(TokenIdent, "U32"), tok(TokenIdent, "U32"),
tok(TokenComma, ","), tok(TokenComma, ","),
tok(TokenKey, "0003"),
tok(TokenIdent, "Wings"),
tok(TokenOption, "?"),
tok(TokenIdent, "Int"),
tok(TokenComma, ","),
tok(TokenRBrace, "}"), tok(TokenRBrace, "}"),
tok(parse.EOF, ""), tok(parse.EOF, ""),
} }

View File

@ -37,7 +37,6 @@ func testGenerateRun(test *testing.T, protocol *Protocol, title, imports, testCa
import "reflect" import "reflect"
import "git.tebibyte.media/sashakoshka/hopp/tape" import "git.tebibyte.media/sashakoshka/hopp/tape"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil" import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
import "git.tebibyte.media/sashakoshka/hopp/internal/testutil/snake"
` + imports ` + imports
setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")` setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")`
teardown := `log.Println("--- END TEST CASE OUTPUT ---")` teardown := `log.Println("--- END TEST CASE OUTPUT ---")`
@ -64,7 +63,6 @@ func testGenerateRun(test *testing.T, protocol *Protocol, title, imports, testCa
switch data := data.(type) { switch data := data.(type) {
case []byte: flat = data case []byte: flat = data
case tu.Snake: flat = data.Flatten() case tu.Snake: flat = data.Flatten()
case snake.Snake: flat = data.Flatten()
} }
message := reflect.New(reflect.ValueOf(correct).Elem().Type()).Interface().(Message) message := reflect.New(reflect.ValueOf(correct).Elem().Type()).Interface().(Message)
log.Println("before: ", message) log.Println("before: ", message)
@ -81,7 +79,9 @@ func testGenerateRun(test *testing.T, protocol *Protocol, title, imports, testCa
} }
} }
func testEncodeDecode(message Message, data any) {buffer := bytes.Buffer { } // TODO: possibly combine the two above functions into this one,
// also take a data parameter here (snake)
func testEncodeDecode(message Message, data tu.Snake) {buffer := bytes.Buffer { }
log.Println("encoding:") log.Println("encoding:")
encoder := tape.NewEncoder(&buffer) encoder := tape.NewEncoder(&buffer)
n, err := message.Encode(encoder) n, err := message.Encode(encoder)
@ -93,30 +93,13 @@ func testGenerateRun(test *testing.T, protocol *Protocol, title, imports, testCa
if n != len(got) { if n != len(got) {
log.Fatalf("n incorrect: %d != %d\n", n, len(got)) log.Fatalf("n incorrect: %d != %d\n", n, len(got))
} }
var flat []byte
switch data := data.(type) {
case []byte:
flat = data
if ok, n := snake.Check(snake.L(data...), got); !ok {
log.Fatalln("not equal at", n)
}
case tu.Snake:
flat = data.Flatten()
if ok, n := data.Check(got); !ok { if ok, n := data.Check(got); !ok {
log.Fatalln("not equal at", n) log.Fatalln("not equal at", n)
} }
case snake.Node:
flat = data.Flatten()
if ok, n := snake.Check(data, got); !ok {
log.Fatalln("not equal at", n)
}
default:
panic("AUSIAUGH AAAUUGUHGHGHH OUHGHGJDSGK")
}
log.Println("decoding:") log.Println("decoding:")
destination := reflect.New(reflect.ValueOf(message).Elem().Type()).Interface().(Message) destination := reflect.New(reflect.ValueOf(message).Elem().Type()).Interface().(Message)
flat := data.Flatten()
log.Println("before: ", tu.Describe(destination)) log.Println("before: ", tu.Describe(destination))
decoder := tape.NewDecoder(bytes.NewBuffer(flat)) decoder := tape.NewDecoder(bytes.NewBuffer(flat))
n, err = destination.Decode(decoder) n, err = destination.Decode(decoder)

View File

@ -1,7 +1,6 @@
package generate package generate
import "io" import "io"
import "strings"
import "strconv" import "strconv"
import "git.tebibyte.media/sashakoshka/goparse" import "git.tebibyte.media/sashakoshka/goparse"
@ -22,7 +21,7 @@ func Parse(lx parse.Lexer) (*Protocol, error) {
func defaultProtocol() Protocol { func defaultProtocol() Protocol {
return Protocol { return Protocol {
Messages: make(map[uint16] Message), Messages: make(map[uint16] Message),
Types: map[string] Typedef { }, Types: map[string] Type { },
} }
} }
@ -48,28 +47,18 @@ func (this *parser) parse() error {
} }
func (this *parser) parseTopLevel() error { func (this *parser) parseTopLevel() error {
doc := "" err := this.ExpectDesc("message or typedef", TokenMethod, TokenIdent)
for {
err := this.ExpectDesc("message or typedef", TokenMethod, TokenIdent, TokenComment)
if err != nil { return err } if err != nil { return err }
if this.EOF() { return nil } if this.EOF() { return nil }
if this.Kind() == TokenComment {
if doc != "" { doc += "\n" }
doc += this.parseComment(this.Value())
this.Next()
} else {
break
}
}
switch this.Kind() { switch this.Kind() {
case TokenMethod: return this.parseMessage(doc) case TokenMethod: return this.parseMessage()
case TokenIdent: return this.parseTypedef(doc) case TokenIdent: return this.parseTypedef()
} }
panic("bug") panic("bug")
} }
func (this *parser) parseMessage(doc string) error { func (this *parser) parseMessage() error {
err := this.Expect(TokenMethod) err := this.Expect(TokenMethod)
if err != nil { return err } if err != nil { return err }
method, err := this.parseHexNumber(this.Value(), 0xFFFF) method, err := this.parseHexNumber(this.Value(), 0xFFFF)
@ -83,13 +72,12 @@ func (this *parser) parseMessage(doc string) error {
if err != nil { return err } if err != nil { return err }
this.protocol.Messages[uint16(method)] = Message { this.protocol.Messages[uint16(method)] = Message {
Name: name, Name: name,
Doc: doc,
Type: typ, Type: typ,
} }
return nil return nil
} }
func (this *parser) parseTypedef(doc string) error { func (this *parser) parseTypedef() error {
err := this.Expect(TokenIdent) err := this.Expect(TokenIdent)
if err != nil { return err } if err != nil { return err }
name := this.Value() name := this.Value()
@ -97,10 +85,7 @@ func (this *parser) parseTypedef(doc string) error {
if err != nil { return err } if err != nil { return err }
typ, err := this.parseType() typ, err := this.parseType()
if err != nil { return err } if err != nil { return err }
this.protocol.Types[name] = Typedef { this.protocol.Types[name] = typ
Doc: doc,
Type: typ,
}
return nil return nil
} }
@ -132,7 +117,6 @@ func (this *parser) parseType() (Type, error) {
case "Buffer": return TypeBuffer { }, this.Next() case "Buffer": return TypeBuffer { }, this.Next()
case "Table": return TypeTable { }, this.Next() case "Table": return TypeTable { }, this.Next()
case "Any": return TypeAny { }, this.Next() case "Any": return TypeAny { }, this.Next()
case "Bool": return TypeBool { }, this.Next()
} }
return this.parseTypeNamed() return this.parseTypeNamed()
case TokenLBracket: case TokenLBracket:
@ -173,22 +157,12 @@ func (this *parser) parseTypeTable() (TypeTableDefined, error) {
Fields: make(map[uint16] Field), Fields: make(map[uint16] Field),
} }
for { for {
doc := "" err := this.ExpectDesc("table field", TokenKey, TokenRBrace)
for {
err := this.ExpectDesc("table field", TokenKey, TokenRBrace, TokenComment)
if err != nil { return TypeTableDefined { }, err } if err != nil { return TypeTableDefined { }, err }
if this.Kind() == TokenComment {
if doc != "" { doc += "\n" }
doc += this.parseComment(this.Value())
this.Next()
} else {
break
}
}
if this.Is(TokenRBrace) { if this.Is(TokenRBrace) {
break break
} }
key, field, err := this.parseField(doc) key, field, err := this.parseField()
if err != nil { return TypeTableDefined { }, err } if err != nil { return TypeTableDefined { }, err }
typ.Fields[key] = field typ.Fields[key] = field
err = this.Expect(TokenComma, TokenRBrace) err = this.Expect(TokenComma, TokenRBrace)
@ -204,7 +178,7 @@ func (this *parser) parseTypeTable() (TypeTableDefined, error) {
return typ, nil return typ, nil
} }
func (this *parser) parseField(doc string) (uint16, Field, error) { func (this *parser) parseField() (uint16, Field, error) {
err := this.Expect(TokenKey) err := this.Expect(TokenKey)
if err != nil { return 0, Field { }, err } if err != nil { return 0, Field { }, err }
key, err := this.parseHexNumber(this.Value(), 0xFFFF) key, err := this.parseHexNumber(this.Value(), 0xFFFF)
@ -214,19 +188,11 @@ func (this *parser) parseField(doc string) (uint16, Field, error) {
name := this.Value() name := this.Value()
err = this.Next() err = this.Next()
if err != nil { return 0, Field { }, err } if err != nil { return 0, Field { }, err }
option := false
if this.Kind() == TokenOption {
option = true
err = this.Next()
if err != nil { return 0, Field { }, err }
}
typ, err := this.parseType() typ, err := this.parseType()
if err != nil { return 0, Field { }, err } if err != nil { return 0, Field { }, err }
return uint16(key), Field { return uint16(key), Field {
Name: name, Name: name,
Doc: doc,
Type: typ, Type: typ,
Option: option,
}, nil }, nil
} }
@ -240,7 +206,3 @@ func (this *parser) parseHexNumber(input string, maxValue int64) (int64, error)
} }
return number, nil return number, nil
} }
func (this *parser) parseComment(input string) string {
return strings.TrimPrefix(strings.TrimPrefix(input, "//"), " ")
}

View File

@ -9,7 +9,6 @@ func TestParse(test *testing.T) {
correct := defaultProtocol() correct := defaultProtocol()
correct.Messages[0x0000] = Message { correct.Messages[0x0000] = Message {
Name: "Connect", Name: "Connect",
Doc: "Connect is sent from the client to the server as the first message of an\nauthenticated transaction.",
Type: TypeTableDefined { Type: TypeTableDefined {
Fields: map[uint16] Field { Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } }, 0x0000: Field { Name: "Name", Type: TypeString { } },
@ -19,52 +18,36 @@ func TestParse(test *testing.T) {
} }
correct.Messages[0x0001] = Message { correct.Messages[0x0001] = Message {
Name: "UserList", Name: "UserList",
Doc: "UserList is sent from the server to the client in response to a Connect\nmessage.",
Type: TypeTableDefined { Type: TypeTableDefined {
Fields: map[uint16] Field { Fields: map[uint16] Field {
0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } }, 0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } },
}, },
}, },
} }
correct.Types["User"] = Typedef { correct.Types["User"] = TypeTableDefined {
Doc: "User holds profile information about a single user.",
Type: TypeTableDefined {
Fields: map[uint16] Field { Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } }, 0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } }, 0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
0x0003: Field { Name: "Bouncy", Type: TypeBool { } },
0x0004: Field { Name: "Wings", Type: TypeInt { Bits: 32 } },
},
}, },
} }
correct.Types["Anything"] = Typedef { correct.Types["Anything"] = TypeAny { }
Type: TypeAny { },
}
test.Log("CORRECT:", &correct) test.Log("CORRECT:", &correct)
got, err := ParseReader("test.pdl", strings.NewReader(` got, err := ParseReader("test.pdl", strings.NewReader(`
// Connect is sent from the client to the server as the first message of an
// authenticated transaction.
M0000 Connect { M0000 Connect {
0000 Name String, 0000 Name String,
// Password is where you put your secrets, your shameful secrets
0001 Password String, 0001 Password String,
} }
// UserList is sent from the server to the client in response to a Connect
// message.
M0001 UserList { M0001 UserList {
0000 Users []User, 0000 Users []User,
} }
// User holds profile information about a single user.
User { User {
0000 Name String, 0000 Name String,
0001 Bio String, 0001 Bio String,
0002 Followers U32, 0002 Followers U32,
0003 Bouncy Bool,
0004 Wings ?U32,
} }
Anything Any Anything Any

View File

@ -7,17 +7,11 @@ import "crypto/md5"
type Protocol struct { type Protocol struct {
Messages map[uint16] Message Messages map[uint16] Message
Types map[string] Typedef Types map[string] Type
} }
type Message struct { type Message struct {
Name string Name string
Doc string
Type Type
}
type Typedef struct {
Doc string
Type Type Type Type
} }
@ -49,12 +43,6 @@ func (typ TypeFloat) String() string {
return fmt.Sprintf("F%d", typ.Bits) return fmt.Sprintf("F%d", typ.Bits)
} }
type TypeBool struct { }
func (TypeBool) String() string {
return "Bool"
}
type TypeString struct { } type TypeString struct { }
func (TypeString) String() string { func (TypeString) String() string {
@ -96,9 +84,7 @@ func (typ TypeTableDefined) String() string {
type Field struct { type Field struct {
Name string Name string
Doc string
Type Type Type Type
Option bool
} }
func (field Field) String() string { func (field Field) String() string {

5
go.mod
View File

@ -1,9 +1,8 @@
module git.tebibyte.media/sashakoshka/hopp module git.tebibyte.media/sashakoshka/hopp
go 1.24.0 go 1.23.0
require ( require (
git.tebibyte.media/sashakoshka/go-cli v0.1.3 git.tebibyte.media/sashakoshka/go-util v0.9.1
git.tebibyte.media/sashakoshka/go-util v0.11.0
git.tebibyte.media/sashakoshka/goparse v0.2.0 git.tebibyte.media/sashakoshka/goparse v0.2.0
) )

6
go.sum
View File

@ -1,6 +1,4 @@
git.tebibyte.media/sashakoshka/go-cli v0.1.3 h1:tSkWjyx2JrGu6KotbXWSTKSYGGS1D4O3qwCrRoZuwbs= git.tebibyte.media/sashakoshka/go-util v0.9.1 h1:eGAbLwYhOlh4aq/0w+YnJcxT83yPhXtxnYMzz6K7xGo=
git.tebibyte.media/sashakoshka/go-cli v0.1.3/go.mod h1:JFA3wSdRkXxa4iQJWHfe3DokiG7Dh2XUJBzPmuVlbuY= git.tebibyte.media/sashakoshka/go-util v0.9.1/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y=
git.tebibyte.media/sashakoshka/go-util v0.11.0 h1:ZxLJWHr0ecgVRV5O4MV7EGHK5xHJMppKd1P4x70AtYQ=
git.tebibyte.media/sashakoshka/go-util v0.11.0/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y=
git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI= git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI=
git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk= git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk=

View File

@ -1,56 +0,0 @@
package connshark
import "os"
import "io"
import "fmt"
import "net"
import "log"
import "sync"
import "math/rand"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
type insert struct {
net.Conn
output io.WriteCloser
lock sync.Mutex
}
func LogDebugFile(underlying net.Conn) net.Conn {
file, err := os.Create(fmt.Sprintf("connshark-%08X.log", rand.Uint32()))
if err != nil {
log.Println("XXX COULD NOT OPEN DEBUG FILE! reason: ", err)
return underlying
}
return Log(underlying, file)
}
func Log(underlying net.Conn, output io.WriteCloser) net.Conn {
return &insert {
Conn: underlying,
output: output,
}
}
func (this *insert) Read(buffer []byte) (n int, err error) {
if n > 0 {
this.lock.Lock()
defer this.lock.Unlock()
fmt.Fprintf(this.output, "TX: %s\n", tu.HexBytes(buffer[:n]))
}
return n, err
}
func (this *insert) Write(buffer []byte) (n int, err error) {
n, err = this.Conn.Write(buffer)
if n > 0 {
this.lock.Lock()
defer this.lock.Unlock()
fmt.Fprintf(this.output, "RX: %s\n", tu.HexBytes(buffer[:n]))
}
return n, err
}
func (this *insert) Close() error {
this.output.Close()
return this.Conn.Close()
}

View File

@ -1,64 +0,0 @@
package mock
import "io"
import "time"
import "bytes"
import "git.tebibyte.media/sashakoshka/hopp"
var _ hopp.Trans = new(Trans)
// Trans is a mock transaction implementation.
type Trans struct {
// These arrays must be the same length. You can load this up
// with messages to read, or deposit messages and retrieve them
// here later.
Methods []uint16
Messages [][]byte
}
func (this *Trans) Close() error { return nil }
func (this *Trans) ID() int64 { return 56 }
func (this *Trans) Send(method uint16, data []byte) error {
this.Methods = append(this.Methods, method)
this.Messages = append(this.Messages, data)
return nil
}
func (this *Trans) SendWriter(method uint16) (io.WriteCloser, error) {
return &transWriter {
Buffer: new(bytes.Buffer),
method: method,
parent: this,
}, nil
}
func (this *Trans) Receive() (method uint16, data []byte, err error) {
if len(this.Methods) == 0 {
return 0, nil, io.EOF
}
method = this.Methods[0]
data = this.Messages[0]
this.Methods = this.Methods[1:]
this.Messages = this.Messages[1:]
return method, data, nil
}
func (this *Trans) ReceiveReader() (method uint16, reader io.Reader, err error) {
method, data, err := this.Receive()
if err != nil { return 0, nil, err }
return method, bytes.NewBuffer(data), nil
}
func (this *Trans) SetDeadline(time.Time) error { return nil }
type transWriter struct {
*bytes.Buffer
method uint16
parent *Trans
}
func (this *transWriter) Close() error {
return this.parent.Send(this.method, this.Bytes())
}

View File

@ -1,45 +0,0 @@
package testutil
import "net"
import "fmt"
import "strings"
var _ net.Conn = new(ConnRecorder)
// ConnRecorder records write/flush actions performed on a net.Conn.
type ConnRecorder struct {
net.Conn
// A []byte means data was written, and untyped nil
// means data was flushed.
Log []any
}
func RecordConn(underlying net.Conn) *ConnRecorder {
return &ConnRecorder {
Conn: underlying,
}
}
func (this *ConnRecorder) Write(data []byte) (n int, err error) {
this.Log = append(this.Log, data)
return len(data), nil
}
func (this *ConnRecorder) Flush() error {
this.Log = append(this.Log, nil)
return nil
}
func (this *ConnRecorder) Dump() string {
builder := strings.Builder { }
for index, item := range this.Log {
fmt.Fprintf(&builder, "%06d ", index)
switch item := item.(type) {
case nil:
fmt.Fprintln(&builder, "FLUSH")
case []byte:
fmt.Fprintln(&builder, HexBytes(item))
}
}
return builder.String()
}

View File

@ -1,44 +0,0 @@
package testutil
import "net"
import "testing"
func TestConnRecorder(test *testing.T) {
// server
listener, err := net.Listen("tcp", "localhost:9999")
if err != nil { test.Fatal(err) }
defer listener.Close()
go func() {
conn, err := listener.Accept()
defer conn.Close()
if err != nil { test.Fatal(err) }
buf := [16]byte { }
for {
_, err := conn.Read(buf[:])
if err != nil { break }
}
}()
// client
conn, err := net.Dial("tcp", "localhost:9999")
if err != nil { test.Fatal(err) }
defer conn.Close()
recorder := RecordConn(conn)
_, err = recorder.Write([]byte("hello"))
if err != nil { test.Fatal(err) }
_, err = recorder.Write([]byte("world!"))
if err != nil { test.Fatal(err) }
err = recorder.Flush()
if err != nil { test.Fatal(err) }
test.Log("GOT:\n" + recorder.Dump())
if len(recorder.Log) != 3 { test.Fatal("wrong length") }
if string(recorder.Log[0].([]byte)) != "hello" {
test.Fatal("not equal")
}
if string(recorder.Log[1].([]byte)) != "world!" {
test.Fatal("not equal")
}
}

View File

@ -1,214 +0,0 @@
// Package 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.
package snake
import "fmt"
import "strings"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
var _ Node = Order { }
var _ Node = Snake { }
var _ Node = Leaf { }
// Check checks the data against the specified node. If the data doesn't satisfy
// the node, or the comparison succeded but didn't consume all the data, this
// function returns false, and the index of the byte where the inequality is.
func Check(node Node, data []byte) (ok bool, n int) {
ok, n = node.Check(data)
if !ok {
return false, n
}
if n != len(data) {
return false, n
}
return true, n
}
// O returns a new order given a vararg node slice.
func O(nodes ...Node) Order {
return Order(nodes)
}
// S returns a new snake given a vararg node slice.
func S(nodes ...Node) Snake {
return Snake(nodes)
}
// L returns a new leaf given a vararg byte slice.
func L(data ...byte) Leaf {
return Leaf([]byte(data))
}
// Order is satisfied when the data satisfies each of its nodes in the order
// that they are specified in the slice.
type Order []Node
// Check determines if the data satisfies the Order.
func (this Order) Check(data []byte) (ok bool, n int) {
left := data
for _, node := range this {
ok, nn := node.Check(left)
n += nn; if !ok { return false, n }
left = left[nn:]
}
return true, n
}
// Flatten returns the Order flattened to a byte array. The result of this
// function always satisfies the Order.
func (this Order) Flatten() []byte {
flat := []byte { }
for _, node := range this {
flat = append(flat, node.Flatten()...)
}
return flat
}
func (this Order) String() string {
out := strings.Builder { }
for index, node := range this {
if index > 0 {
fmt.Fprint(&out, " :")
}
fmt.Fprintf(&out, " %v", node)
}
return out.String()
}
// Add returns a new order with the given nodes appended to it.
func (this Order) Add(nodes ...Node) Order {
newOrder := make(Order, len(this) + len(nodes))
copy(newOrder, this)
copy(newOrder[len(this):], Order(nodes))
return newOrder
}
// AddO returns a new order with the given order appended to it.
func (this Order) AddO(nodes ...Node) Order {
return this.Add(O(nodes...))
}
// AddS returns a new order with the given snake appended to it.
func (this Order) AddS(nodes ...Node) Order {
return this.Add(S(nodes...))
}
// AddL returns a new order with the given leaf appended to it.
func (this Order) AddL(data ...byte) Order {
return this.Add(L(data...))
}
// Snake is satisfied when the data satisfies each of its nodes in no particular
// order.
type Snake []Node
// Check determines if the data satisfies the snake.
func (this Snake) Check(data []byte) (ok bool, n int) {
fmt.Println("CHECKING SNAKE")
left := data
nodes := map[int] Node { }
for key, node := range this {
nodes[key] = node
}
for len(nodes) > 0 {
found := false
for key, node := range nodes {
fmt.Println(left, key, node)
ok, nn := node.Check(left)
fmt.Println(ok, nn)
if !ok { continue }
n += nn
left = data[n:]
delete(nodes, key)
found = true
break
}
if !found { return false, n }
}
return true, n
}
// Flatten returns the snake flattened to a byte array. The result of this
// function always satisfies the snake.
func (this Snake) Flatten() []byte {
flat := []byte { }
for _, node := range this {
flat = append(flat, node.Flatten()...)
}
return flat
}
func (this Snake) String() string {
out := strings.Builder { }
out.WriteString("[")
for index, node := range this {
if index > 0 {
fmt.Fprint(&out, " /")
}
fmt.Fprintf(&out, " %v", node)
}
out.WriteString(" ]")
return out.String()
}
// Add returns a new snake with the given nodes appended to it.
func (this Snake) Add(nodes ...Node) Snake {
newSnake := make(Snake, len(this) + len(nodes))
copy(newSnake, this)
copy(newSnake[len(this):], Snake(nodes))
return newSnake
}
// AddO returns a new snake with the given order appended to it.
func (this Snake) AddO(nodes ...Node) Snake {
return this.Add(O(nodes...))
}
// AddS returns a new snake with the given snake appended to it.
func (this Snake) AddS(nodes ...Node) Snake {
return this.Add(S(nodes...))
}
// AddL returns a new snake with the given leaf appended to it.
func (this Snake) AddL(data ... byte) Snake {
return this.Add(L(data...))
}
// Leaf is satisfied when the data matches it exactly.
type Leaf []byte
// Check determines if the data is equal to the leaf.
func (this Leaf) Check(data []byte) (ok bool, n int) {
if len(data) < len(this) {
return false, len(data)
}
for index, byt := range this {
if byt != data[index] {
return false, index
}
}
return true, len(this)
}
// This one's easy.
func (this Leaf) Flatten() []byte {
return []byte(this)
}
func (this Leaf) String() string {
return tu.HexBytes([]byte(this))
}
// Node represents a snake node.
type Node interface {
// Check determines if the data satisfies the node. If satisfied, the function
// will return true, and the index at which it stopped. If not, the
// function will return false, and the index of the first byte that
// didn't match. As long as the start of the data satisfies the node,
// whatever comes after it doesn't matter.
Check(data []byte) (ok bool, n int)
// Flatten returns the node flattened to a byte array. The result of
// this function always satisfies the node.
Flatten() []byte
}

View File

@ -1,89 +0,0 @@
package snake
import "testing"
func TestSnakeA(test *testing.T) {
snake := O().AddL(1, 6).AddS(
L(1),
L(2),
L(3),
L(4),
L(5),
).AddL(9)
test.Log(snake)
ok, n := Check(snake, []byte { 1, 6, 1, 2, 3, 4, 5, 9 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 6, 5, 4, 3, 2, 1, 9 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 6, 3, 1, 4, 2, 5, 9 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 6, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 1, 2, 3, 4, 5, 6, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 0, 2, 3, 4, 5, 6, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 7, 1, 4, 2, 5, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 7, 3, 1, 4, 2, 5, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 1, 2, 3, 4, 5, 9, 10})
if ok { test.Fatal("false positive:", n) }
}
func TestSnakeB(test *testing.T) {
snake := O().AddO(L(1), L(6)).AddS(
L(1),
L(2),
).AddL(9).AddS(
L(3, 2),
L(0),
L(1, 1, 2, 3),
)
test.Log(snake)
ok, n := Check(snake, []byte { 1, 6, 1, 2, 9, 3, 2, 0, 1, 1, 2, 3})
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 6, 2, 1, 9, 0, 1, 1, 2, 3, 3, 2})
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 6, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 1, 2, 9 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 9, 3, 2, 0, 1, 1, 2, 3})
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 2, 9, 0, 1, 1, 2, 3, 3, 2})
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 1, 6, 1, 2, 9, 3, 2, 1, 1, 2, 3})
if ok { test.Fatal("false positive:", n) }
}
func TestSnakeC(test *testing.T) {
snake := S(
L(1, 2, 3),
S(L(6), L(7), L(8)),
)
test.Log(snake)
ok, n := Check(snake, []byte { 1, 2, 3, 6, 7, 8 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 6, 7, 8, 1, 2, 3 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 7, 8, 6, 1, 2, 3 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 1, 2, 3, 8, 6, 7 })
if !ok { test.Fatal("false negative:", n) }
ok, n = Check(snake, []byte { 2, 1, 3, 6, 7, 8 })
if ok { test.Fatal("false positive:", n) }
ok, n = Check(snake, []byte { 6, 1, 2, 3, 7, 8 })
if ok { test.Fatal("false positive:", n) }
}

View File

@ -97,32 +97,6 @@ func (sn Snake) String() string {
return out.String() return out.String()
} }
func (sn Snake) CharsString() 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(" : ") }
out.WriteRune('[')
for index, variation := range sector {
if index > 0 { out.WriteString(" / ") }
for _, byt := range variation {
run := rune(byt)
if unicode.IsPrint(run) && run < 0x7F {
out.WriteRune(run)
} else {
out.WriteRune('.')
}
out.WriteRune(' ')
}
}
out.WriteRune(']')
}
return out.String()
}
// HexBytes formats bytes into a hexadecimal string. // HexBytes formats bytes into a hexadecimal string.
func HexBytes(data []byte) string { func HexBytes(data []byte) string {
if len(data) == 0 { return "EMPTY" } if len(data) == 0 { return "EMPTY" }
@ -133,24 +107,6 @@ func HexBytes(data []byte) string {
return out.String() return out.String()
} }
// HexChars returns all printable bytes in the string, with non-printable ones
// replaced with a dot. Each character has an extra space after it for placing
// underneath the result of HexBytes.
func HexChars(data []byte) string {
if len(data) == 0 { return "EMPTY" }
out := strings.Builder { }
for _, byt := range data {
run := rune(byt)
if unicode.IsPrint(run) && run < 0x7F {
out.WriteRune(run)
} else {
out.WriteRune('.')
}
out.WriteRune(' ')
}
return out.String()
}
// Describe returns a string representing the type and data of the given value. // Describe returns a string representing the type and data of the given value.
func Describe(value any) string { func Describe(value any) string {
desc := describer { } desc := describer { }

View File

@ -15,34 +15,14 @@ type Listener interface {
Addr() net.Addr Addr() net.Addr
} }
// Listen listens for incoming HOPP connections. The network must be one of: // Listen listens for incoming HOPP connections. The network must be one of
// // "quic", "quic4", (IPv4-only) "quic6" (IPv6-only), or "unix". For now, quic is
// - "quic" // not supported.
// - "quic4" (IPv4-only) func Listen(network, address string) (Listener, error) {
// - "quic6" (IPv6-only)
// - "tls"
// - "tls4" (IPv4-only)
// - "tls6" (IPv6-only)
// - "tcp"
// - "tcp4" (IPv4-only)
// - "tcp6" (IPv6-only)
// - "unix"
//
// For now, QUIC is unsupported.
func Listen(network, address string, tlsConf *tls.Config) (Listener, error) {
switch network { switch network {
case "quic", "quic4", "quic6": return ListenQUIC(network, address, tlsConf) case "quic", "quic4", "quic6": return ListenQUIC(network, address, nil)
case "tls", "tls4", "tls6": return ListenTLS(network, address, tlsConf) case "unix": return ListenUnix(network, address)
case "tcp", "tcp4", "tcp6": default: return nil, ErrUnknownNetwork
addr, err := net.ResolveTCPAddr(network, address)
if err != nil { return nil, err }
return ListenTCP(network, addr)
case "unix":
addr, err := net.ResolveUnixAddr(network, address)
if err != nil { return nil, err }
return ListenUnix(network, addr)
default:
return nil, ErrUnknownNetwork
} }
} }
@ -54,52 +34,33 @@ func ListenQUIC(network, address string, tlsConf *tls.Config) (Listener, error)
return nil, errors.New("quic is not yet implemented") return nil, errors.New("quic is not yet implemented")
} }
// ListenTLS listens for incoming HOPP connections using a TLS socket as a
// transport. The network must be "tcp".
func ListenTLS(network, address string, tlsConf *tls.Config) (Listener, error) {
network, err := tlsNetworkToTCPNetwork(network)
if err != nil { return nil, err }
listener, err := tls.Listen(network, address, tlsConf)
if err != nil { return nil, err }
return &netListenerWrapper {
underlying: listener,
}, nil
}
// ListenTCP listens for incoming HOPP connections using a TCP socket as a
// transport. The network must be "tcp".
func ListenTCP(network string, laddr *net.TCPAddr) (Listener, error) {
listener, err := net.ListenTCP(network, laddr)
if err != nil { return nil, err }
return &netListenerWrapper {
underlying: listener,
}, nil
}
// ListenUnix listens for incoming HOPP connections using a Unix domain socket // ListenUnix listens for incoming HOPP connections using a Unix domain socket
// as a transport. The network must be "unix". // as a transport. The network must be "unix".
func ListenUnix(network string, addr *net.UnixAddr) (Listener, error) { func ListenUnix(network, address string) (Listener, error) {
listener, err := net.ListenUnix(network, addr) if network != "unix" { return nil, ErrUnknownNetwork }
addr, err := net.ResolveUnixAddr(network, address)
if err != nil { return nil, err } if err != nil { return nil, err }
return &netListenerWrapper { unixListener, err := net.ListenUnix(network, addr)
underlying: listener, if err != nil { return nil, err }
return &listenerUnix {
underlying: unixListener,
}, nil }, nil
} }
type netListenerWrapper struct { type listenerUnix struct {
underlying net.Listener underlying *net.UnixListener
} }
func (this *netListenerWrapper) Accept() (Conn, error) { func (this *listenerUnix) Accept() (Conn, error) {
conn, err := this.underlying.Accept() conn, err := this.underlying.Accept()
if err != nil { return nil, err } if err != nil { return nil, err }
return AdaptA(conn, ServerSide), nil return AdaptA(conn, ServerSide), nil
} }
func (this *netListenerWrapper) Close() error { func (this *listenerUnix) Close() error {
return this.underlying.Close() return this.underlying.Close()
} }
func (this *netListenerWrapper) Addr() net.Addr { func (this *listenerUnix) Addr() net.Addr {
return this.underlying.Addr() return this.underlying.Addr()
} }

View File

@ -6,8 +6,6 @@ import "fmt"
import "net" import "net"
import "sync" import "sync"
import "time" import "time"
import "bytes"
import "context"
import "sync/atomic" import "sync/atomic"
import "git.tebibyte.media/sashakoshka/go-util/sync" import "git.tebibyte.media/sashakoshka/go-util/sync"
@ -18,12 +16,6 @@ const closeMethod = 0xFFFF
const int64Max = int64((^uint64(0)) >> 1) const int64Max = int64((^uint64(0)) >> 1)
const defaultChunkSize = 0x1000 const defaultChunkSize = 0x1000
var bufferPool = sync.Pool {
New: func() any {
return &bytes.Buffer { }
},
}
// Party represents a side of a connection. // Party represents a side of a connection.
type Party bool; const ( type Party bool; const (
ServerSide Party = false ServerSide Party = false
@ -47,23 +39,20 @@ type a struct {
sendLock sync.Mutex sendLock sync.Mutex
transMap map[int64] *transA transMap map[int64] *transA
transChan chan *transA transChan chan *transA
ctx context.Context done chan struct { }
done func()
err error err error
} }
// AdaptA returns a connection implementing METADAPT-A over a singular stream- // AdaptA returns a connection implementing METADAPT-A over a singular stream-
// oriented transport such as TCP or UNIX domain stream sockets. // oriented transport such as TCP or UNIX domain stream sockets.
func AdaptA(underlying net.Conn, party Party) Conn { func AdaptA(underlying net.Conn, party Party) Conn {
ctx, done := context.WithCancel(context.Background())
conn := &a { conn := &a {
sizeLimit: defaultSizeLimit, sizeLimit: defaultSizeLimit,
underlying: underlying, underlying: underlying,
party: party, party: party,
transMap: make(map[int64] *transA), transMap: make(map[int64] *transA),
transChan: make(chan *transA), transChan: make(chan *transA),
ctx: ctx, done: make(chan struct { }),
done: done,
} }
if party == ClientSide { if party == ClientSide {
conn.transID = 1 conn.transID = 1
@ -71,15 +60,11 @@ func AdaptA(underlying net.Conn, party Party) Conn {
conn.transID = -1 conn.transID = -1
} }
go conn.receive() go conn.receive()
go func() {
<- ctx.Done()
underlying.Close()
}()
return conn return conn
} }
func (this *a) Close() error { func (this *a) Close() error {
this.done() close(this.done)
return nil return nil
} }
@ -120,7 +105,7 @@ func (this *a) AcceptTrans() (Trans, error) {
return nil, eof return nil, eof
} }
return trans, nil return trans, nil
case <- this.ctx.Done(): case <- this.done:
return nil, eof return nil, eof
} }
} }
@ -139,10 +124,10 @@ func (this *a) unlistTransactionSafe(id int64) {
delete(this.transMap, id) delete(this.transMap, id)
} }
func (this *a) sendMessageSafe(trans int64, method uint16, ccb uint64, data []byte) error { func (this *a) sendMessageSafe(trans int64, method uint16, data []byte) error {
this.sendLock.Lock() this.sendLock.Lock()
defer this.sendLock.Unlock() defer this.sendLock.Unlock()
return encodeMessageA(this.underlying, this.sizeLimit, trans, method, 0, data) return encodeMessageA(this.underlying, this.sizeLimit, trans, method, data)
} }
func (this *a) receive() { func (this *a) receive() {
@ -229,12 +214,12 @@ type transA struct {
parent *a parent *a
id int64 id int64
incoming usync.Gate[incomingMessage] incoming usync.Gate[incomingMessage]
currentReader io.Reader
currentWriter io.Closer
writeBuffer []byte
closed atomic.Bool closed atomic.Bool
closeErr error closeErr error
currentReader io.Reader
currentWriter usync.Monitor[io.Closer]
deadline *time.Timer deadline *time.Timer
deadlineLock sync.Mutex deadlineLock sync.Mutex
} }
@ -266,28 +251,27 @@ func (this *transA) ID() int64 {
} }
func (this *transA) Send(method uint16, data []byte) error { func (this *transA) Send(method uint16, data []byte) error {
return this.parent.sendMessageSafe(this.id, method, 0, data) return this.parent.sendMessageSafe(this.id, method, data)
} }
func (this *transA) SendWriter(method uint16) (io.WriteCloser, error) { func (this *transA) SendWriter(method uint16) (io.WriteCloser, error) {
currentWriter, done := this.currentWriter.BorrowReturn()
defer done(&currentWriter)
// close previous writer if necessary // close previous writer if necessary
if currentWriter != nil { if this.currentWriter != nil {
currentWriter.Close() this.currentWriter.Close()
currentWriter = nil this.currentWriter = nil
} }
// create new writer // create new writer
writer := &writerA { writer := &writerA {
parent: this, parent: this,
buffer: bufferPool.Get().(*bytes.Buffer), // there is only ever one writer at a time, so they can all
// share a buffer
buffer: this.writeBuffer[:0],
method: method, method: method,
chunkSize: defaultChunkSize, chunkSize: defaultChunkSize,
open: true, open: true,
} }
currentWriter = writer this.currentWriter = writer
return writer, nil return writer, nil
} }
@ -398,7 +382,7 @@ func (this *readerA) pull() (uint16, error) {
// close and return error on failure // close and return error on failure
this.eof = true this.eof = true
this.parent.Close() this.parent.Close()
return 0, this.parent.bestErr() return 0, fmt.Errorf("could not receive message: %w", this.parent.bestErr())
} }
func (this *readerA) Read(buffer []byte) (int, error) { func (this *readerA) Read(buffer []byte) (int, error) {
@ -414,14 +398,14 @@ func (this *readerA) Read(buffer []byte) (int, error) {
type writerA struct { type writerA struct {
parent *transA parent *transA
buffer *bytes.Buffer buffer []byte
method uint16 method uint16
chunkSize int64 chunkSize int64
open bool open bool
} }
func (this *writerA) Write(data []byte) (n int, err error) { func (this *writerA) Write(data []byte) (n int, err error) {
if !this.open || this.parent.closed.Load() { return 0, io.EOF } if !this.open { return 0, io.EOF }
toSend := data toSend := data
for len(toSend) > 0 { for len(toSend) > 0 {
nn, err := this.writeOne(toSend) nn, err := this.writeOne(toSend)
@ -433,18 +417,7 @@ func (this *writerA) Write(data []byte) (n int, err error) {
} }
func (this *writerA) Close() error { func (this *writerA) Close() error {
if this.buffer != nil {
// flush if needed
if this.buffer.Len() > 0 {
this.flush(0)
}
this.open = false this.open = false
// reset the buffer and put it back in the pool
this.buffer.Reset()
bufferPool.Put(this.buffer)
this.buffer = nil
}
return nil return nil
} }
@ -452,31 +425,26 @@ func (this *writerA) writeOne(data []byte) (n int, err error) {
data = data[:min(len(data), int(this.chunkSize))] data = data[:min(len(data), int(this.chunkSize))]
// if there is more room, append to the buffer and exit // if there is more room, append to the buffer and exit
if int64(this.buffer.Len() + len(data)) <= this.chunkSize { if int64(len(this.buffer) + len(data)) <= this.chunkSize {
this.buffer.Write(data) this.buffer = append(this.buffer, data...)
n = len(data) n = len(data)
// if have a full chunk, flush // if have a full chunk, flush
if int64(this.buffer.Len()) == this.chunkSize { if int64(len(this.buffer)) == this.chunkSize {
err = this.flush(1) err = this.flush()
if err != nil { return n, err } if err != nil { return n, err }
} }
return n, nil return n, nil
} }
// if not, flush and store as much as we can in the buffer // if not, flush and store as much as we can in the buffer
err = this.flush(1) err = this.flush()
if err != nil { return n, err } if err != nil { return n, err }
n = int(min(int64(len(data)), this.chunkSize)) this.buffer = append(this.buffer, data...)
this.buffer.Write(data[:n])
return n, nil return n, nil
} }
func (this *writerA) flush(ccb uint64) error { func (this *writerA) flush() error {
err := this.parent.parent.sendMessageSafe( return this.parent.parent.sendMessageSafe(this.parent.id, this.method, this.buffer)
this.parent.id, this.method, ccb,
this.buffer.Bytes())
this.buffer.Reset()
return err
} }
type incomingMessage struct { type incomingMessage struct {
@ -490,19 +458,15 @@ func encodeMessageA(
sizeLimit int64, sizeLimit int64,
trans int64, trans int64,
method uint16, method uint16,
ccb uint64,
data []byte, data []byte,
) error { ) error {
if int64(len(data)) > sizeLimit { if int64(len(data)) > sizeLimit {
return ErrPayloadTooLarge return ErrPayloadTooLarge
} }
buffer := make([]byte, 18 + len(data)) buffer := make([]byte, 18 + len(data))
// transaction ID field
encodeI64(buffer[:8], trans) encodeI64(buffer[:8], trans)
// method field
encodeI16(buffer[8:10], method) encodeI16(buffer[8:10], method)
// payload size field encodeI64(buffer[10:18], uint64(len(data)))
encodeI64(buffer[10:18], uint64(len(data)) & 0x7FFFFFFFFFFFFFFF | ccb << 63)
copy(buffer[18:], data) copy(buffer[18:], data)
_, err := writer.Write(buffer) _, err := writer.Write(buffer)
return err return err
@ -521,7 +485,6 @@ func decodeMessageA(
headerBuffer := [18]byte { } headerBuffer := [18]byte { }
_, err = io.ReadFull(reader, headerBuffer[:]) _, err = io.ReadFull(reader, headerBuffer[:])
if err != nil { return 0, 0, false, nil, err } if err != nil { return 0, 0, false, nil, err }
transID, err = decodeI64[int64](headerBuffer[:8]) transID, err = decodeI64[int64](headerBuffer[:8])
if err != nil { return 0, 0, false, nil, err } if err != nil { return 0, 0, false, nil, err }
method, err = decodeI16[uint16](headerBuffer[8:10]) method, err = decodeI16[uint16](headerBuffer[8:10])

View File

@ -2,13 +2,10 @@ package hopp
import "io" import "io"
import "net" import "net"
import "sync"
import "bytes" import "bytes"
import "errors" import "errors"
import "slices" import "slices"
import "testing" import "testing"
import "context"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
// some of these tests spawn goroutines that can signal a failure. // some of these tests spawn goroutines that can signal a failure.
// abide by the documentation for testing.T (https://pkg.go.dev/testing#T): // abide by the documentation for testing.T (https://pkg.go.dev/testing#T):
@ -55,7 +52,6 @@ func TestConnA(test *testing.T) {
test.Error("CLIENT payload:", gotPayload) test.Error("CLIENT payload:", gotPayload)
test.Fatal("CLIENT ok byeeeeeeeeeeeee") test.Fatal("CLIENT ok byeeeeeeeeeeeee")
} }
test.Log("CLIENT transaction has closed")
} }
serverFunc := func(a Conn) { serverFunc := func(a Conn) {
@ -74,6 +70,20 @@ func TestConnA(test *testing.T) {
} }
func TestTransOpenCloseA(test *testing.T) { 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) { clientFunc := func(conn Conn) {
// 10 // 10
trans, err := conn.OpenTrans() trans, err := conn.OpenTrans()
@ -132,71 +142,10 @@ func TestTransOpenCloseA(test *testing.T) {
clientServerEnvironment(test, clientFunc, serverFunc) clientServerEnvironment(test, clientFunc, serverFunc)
} }
func TestReadWriteA(test *testing.T) {
payloads := []string {
"hello",
"world",
"When the impostor is sus!",
}
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, gotReader, err := trans.ReceiveReader()
if err != nil { test.Fatal("CLIENT", err) }
gotPayloadBytes, err := io.ReadAll(gotReader)
if err != nil { test.Fatal("CLIENT", err) }
gotPayload := string(gotPayloadBytes)
test.Log("CLIENT m:", gotMethod, "p:", tu.HexBytes(gotPayloadBytes))
if int(gotMethod) != method {
test.Error("CLIENT method not equal, expected", method)
}
if gotPayload != payload {
test.Error(
"CLIENT payload not equal, expected",
tu.HexBytes([]byte(payload)))
}
}
test.Log("CLIENT waiting for transaction close...")
gotMethod, gotPayload, err := trans.Receive()
if !errors.Is(err, io.EOF) {
test.Error("CLIENT wrong error:", err)
test.Error("CLIENT method:", gotMethod)
test.Error("CLIENT payload:", tu.HexBytes(gotPayload))
test.Fatal("CLIENT (expected io.EOF and no message)")
}
test.Log("CLIENT transaction has closed")
}
serverFunc := func(a Conn) {
defer test.Log("SERVER closing connection")
trans, err := a.OpenTrans()
if err != nil { test.Error("SERVER", err); return }
test.Cleanup(func() { trans.Close() })
for method, payload := range payloads {
test.Log("SERVER m:", method, "p:", tu.HexBytes([]byte(payload)))
func() {
writer, err := trans.SendWriter(uint16(method))
if err != nil { test.Error("SERVER", err); return }
defer writer.Close()
_, err = writer.Write([]byte(payload))
if err != nil { test.Error("SERVER", err); return }
}()
}
}
clientServerEnvironment(test, clientFunc, serverFunc)
}
func TestEncodeMessageA(test *testing.T) { func TestEncodeMessageA(test *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 } payload := []byte { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }
err := encodeMessageA(buffer, defaultSizeLimit, 0x5800FEABC3104F04, 0x6B12, 0, payload) err := encodeMessageA(buffer, defaultSizeLimit, 0x5800FEABC3104F04, 0x6B12, payload)
correct := []byte { correct := []byte {
0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04, 0x58, 0x00, 0xFE, 0xAB, 0xC3, 0x10, 0x4F, 0x04,
0x6B, 0x12, 0x6B, 0x12,
@ -214,7 +163,7 @@ func TestEncodeMessageA(test *testing.T) {
func TestEncodeMessageAErr(test *testing.T) { func TestEncodeMessageAErr(test *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
payload := make([]byte, 0x10000) payload := make([]byte, 0x10000)
err := encodeMessageA(buffer, 0x20, 0x5800FEABC3104F04, 0x6B12, 0, payload) err := encodeMessageA(buffer, 0x20, 0x5800FEABC3104F04, 0x6B12, payload)
if !errors.Is(err, ErrPayloadTooLarge) { if !errors.Is(err, ErrPayloadTooLarge) {
test.Fatalf("wrong error: %v", err) test.Fatalf("wrong error: %v", err)
} }
@ -259,7 +208,7 @@ func TestEncodeDecodeMessageA(test *testing.T) {
correctMethod := uint16(30) correctMethod := uint16(30)
correctPayload := []byte("good") correctPayload := []byte("good")
buffer := bytes.Buffer { } buffer := bytes.Buffer { }
err := encodeMessageA(&buffer, defaultSizeLimit, correctTransID, correctMethod, 0, correctPayload) err := encodeMessageA(&buffer, defaultSizeLimit, correctTransID, correctMethod, correctPayload)
if err != nil { test.Fatal(err) } if err != nil { test.Fatal(err) }
transID, method, chunked, payload, err := decodeMessageA(&buffer, defaultSizeLimit) transID, method, chunked, payload, err := decodeMessageA(&buffer, defaultSizeLimit)
if got, correct := transID, int64(2); got != correct { if got, correct := transID, int64(2); got != correct {
@ -276,255 +225,39 @@ func TestEncodeDecodeMessageA(test *testing.T) {
} }
} }
func TestConsecutiveSend(test *testing.T) {
packets := [][]byte {
[]byte {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x07 },
[]byte {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x08 },
}
payloads := [][]byte {
[]byte { 0x43, 0x00, 0x00, 0x00, 0x07 },
[]byte { 0x43, 0x00, 0x00, 0x00, 0x08 },
}
var group sync.WaitGroup
group.Add(2)
// server
listener, err := net.Listen("tcp", "localhost:9999")
if err != nil { test.Fatal("SERVER", err) }
go func() {
defer group.Done()
defer listener.Close()
conn, err := listener.Accept()
if err != nil { test.Fatal("SERVER", err) }
defer conn.Close()
buf := [16]byte { }
for {
_, err := conn.Read(buf[:])
if err != nil { break }
}
}()
// client
go func() {
defer group.Done()
conn, err := net.Dial("tcp", "localhost:9999")
if err != nil { test.Fatal("CLIENT", err) }
defer conn.Close()
recorder := tu.RecordConn(conn)
a := AdaptA(recorder, ClientSide)
trans, err := a.OpenTrans()
if err != nil { test.Fatal("CLIENT", err) }
for _, payload := range payloads {
err := trans.Send(0x0000, payload)
if err != nil { test.Fatal("CLIENT", err) }
}
test.Log("CLIENT recorded output:\n" + recorder.Dump())
if len(recorder.Log) != 2 { test.Fatal("wrong length") }
if !slices.Equal(recorder.Log[0].([]byte), packets[0]) {
test.Fatal("not equal")
}
if !slices.Equal(recorder.Log[1].([]byte), packets[1]) {
test.Fatal("not equal")
}
}()
group.Wait()
}
func TestConsecutiveWrite(test *testing.T) {
packets := [][]byte {
[]byte {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x07 },
[]byte {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x08 },
}
payloads := [][]byte {
[]byte { 0x43, 0x00, 0x00, 0x00, 0x07 },
[]byte { 0x43, 0x00, 0x00, 0x00, 0x08 },
}
var group sync.WaitGroup
group.Add(2)
// server
listener, err := net.Listen("tcp", "localhost:9999")
if err != nil { test.Fatal("SERVER", err) }
go func() {
defer group.Done()
defer listener.Close()
conn, err := listener.Accept()
if err != nil { test.Fatal("SERVER", err) }
defer conn.Close()
buf := [16]byte { }
for {
_, err := conn.Read(buf[:])
if err != nil { break }
}
}()
// client
go func() {
defer group.Done()
conn, err := net.Dial("tcp", "localhost:9999")
if err != nil { test.Fatal("CLIENT", err) }
defer conn.Close()
recorder := tu.RecordConn(conn)
a := AdaptA(recorder, ClientSide)
trans, err := a.OpenTrans()
if err != nil { test.Fatal("CLIENT", err) }
for _, payload := range payloads {
func() {
writer, err := trans.SendWriter(0x0000)
if err != nil { test.Fatal("CLIENT", err) }
_, err = writer.Write(payload)
if err != nil { test.Fatal("CLIENT", err) }
writer.Close()
}()
}
test.Log("CLIENT recorded output:\n" + recorder.Dump())
if len(recorder.Log) != 2 { test.Fatal("wrong length") }
if !slices.Equal(recorder.Log[0].([]byte), packets[0]) {
test.Fatal("not equal")
}
if !slices.Equal(recorder.Log[1].([]byte), packets[1]) {
test.Fatal("not equal")
}
}()
group.Wait()
}
func TestConsecutiveReceive(test *testing.T) {
stream := []byte {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x43, 0x00, 0x00, 0x00, 0x08,
}
payloads := [][]byte {
[]byte { 0x43, 0x00, 0x00, 0x00, 0x07 },
[]byte { 0x43, 0x00, 0x00, 0x00, 0x08 },
}
var group sync.WaitGroup
group.Add(2)
// server
listener, err := net.Listen("tcp", "localhost:9999")
if err != nil { test.Fatal("SERVER", err) }
go func() {
defer group.Done()
defer listener.Close()
conn, err := listener.Accept()
if err != nil { test.Fatal("SERVER", err) }
defer conn.Close()
a := AdaptA(conn, ServerSide)
trans, err := a.AcceptTrans()
if err != nil { test.Fatal("SERVER", err) }
index := 0
for {
method, data, err := trans.Receive()
if err != nil {
if !errors.Is(err, io.EOF) {
test.Fatal("SERVER", err)
}
break
}
test.Logf("SERVER GOT: M%04X %s", method, tu.HexBytes(data))
if index >= len(payloads) {
test.Fatalf(
"SERVER we weren't supposed to receive %d messages",
index + 1)
}
if method != 0 {
test.Fatal("SERVER", "method not equal")
}
if !slices.Equal(data, payloads[index]) {
test.Fatal("SERVER", "data not equal")
}
index ++
}
if index != len(payloads) {
test.Fatalf(
"SERVER we weren't supposed to receive %d messages",
index + 1)
}
}()
// client
go func() {
defer group.Done()
conn, err := net.Dial("tcp", "localhost:9999")
if err != nil { test.Fatal("CLIENT", err) }
defer conn.Close()
_, err = conn.Write(stream)
if err != nil { test.Fatal("CLIENT", err) }
}()
group.Wait()
}
func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), serverFunc func(conn Conn)) { func clientServerEnvironment(test *testing.T, clientFunc func(conn Conn), serverFunc func(conn Conn)) {
network := "tcp" network := "tcp"
addr := "localhost:7959" addr := "localhost:7959"
// server // server
listener, err := Listen(network, addr, nil) listener, err := net.Listen(network, addr)
if err != nil { test.Fatal(err) }
test.Cleanup(func() { listener.Close() }) test.Cleanup(func() { listener.Close() })
go func() { go func() {
test.Log("SERVER listening") test.Log("SERVER listening")
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { test.Error("SERVER", err); return } if err != nil { test.Error("SERVER", err); return }
defer conn.Close() defer conn.Close()
test.Cleanup(func() { conn.Close() }) test.Cleanup(func() { conn.Close() })
a := AdaptA(conn, ServerSide)
test.Cleanup(func() { a.Close() })
serverFunc(conn) serverFunc(a)
test.Log("SERVER closing") test.Log("SERVER closing")
}() }()
// client // client
test.Log("CLIENT dialing") test.Log("CLIENT dialing")
conn, err := Dial(context.Background(), network, addr, nil) conn, err := net.Dial(network, addr)
if err != nil { test.Fatal("CLIENT", err) } if err != nil { test.Fatal("CLIENT", err) }
test.Cleanup(func() { conn.Close() })
test.Log("CLIENT dialed") test.Log("CLIENT dialed")
a := AdaptA(conn, ClientSide)
test.Cleanup(func() { a.Close() })
clientFunc(conn) clientFunc(a)
test.Log("CLIENT waiting for connection close...") test.Log("CLIENT waiting for connection close...")
trans, err := conn.AcceptTrans() trans, err := a.AcceptTrans()
if !errors.Is(err, io.EOF) { if !errors.Is(err, io.EOF) {
test.Error("CLIENT wrong error:", err) test.Error("CLIENT wrong error:", err)
test.Fatal("CLIENT trans:", trans) test.Fatal("CLIENT trans:", trans)

View File

@ -2,10 +2,29 @@ package hopp
import "git.tebibyte.media/sashakoshka/go-util/container" import "git.tebibyte.media/sashakoshka/go-util/container"
// Option is an alias for ucontainer.Option, defined here for convenience // Option allows an optional value to be defined without using a pointer.
type Option[T any] = ucontainer.Option[T] // TODO make generic alias once go 1.24 releases
type Option[T any] ucontainer.Optional[T]
// O is an alias for ucontainer.O, defined here for convenience
func O[T any](value T) Option[T] { func O[T any](value T) Option[T] {
return ucontainer.O(value) return Option[T](ucontainer.O(value))
}
func Void[T any]() Option[T] {
return Option[T](ucontainer.Void[T]())
}
func (option Option[T]) Ok() bool {
return (ucontainer.Optional[T])(option).Exists()
}
func (option Option[T]) Get() (T, bool) {
return (ucontainer.Optional[T])(option).Value()
}
func (option Option[T]) Default(defaul T) T {
if value, ok := option.Get(); ok {
return value
}
return defaul
} }

View File

@ -1,20 +0,0 @@
filetype: pdl
detect:
filename: "\\.pdl$"
rules:
- preproc: "\\bM[0-9a-fA-F]{4}\\b"
- type: "\\b((U|I)(5|8|16|32|64|128|256)|F(16|32|64|128|256)|Bool|String|Buffer|Table|Any)\\b"
- symbol.brackets: "(\\{|\\}|\\[\\])"
- symbol.operator: "\\?"
- constant.number: "\\b[0-9a-fA-F]{4}\\b"
- comment:
start: "//"
end: "$"
rules:
- todo: "(TODO|XXX|FIXME|BUG):?"

View File

@ -6,10 +6,6 @@ package tape
// TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get // TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get
// transformed into tables with a defined schema // transformed into tables with a defined schema
// TODO: support special behavior for options in structs: don't just write a
// zero value if the option is void, write no field at all. also consider doing
// this for maps, and maybe slices.
// TODO: test all of these smaller functions individually // TODO: test all of these smaller functions individually
// For an explanation as to why this package always treats LBA/SBA as strings, // For an explanation as to why this package always treats LBA/SBA as strings,
@ -63,7 +59,6 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
case reflect.Uint64: return encoder.WriteUint64(uint64(reflectValue.Uint())) case reflect.Uint64: return encoder.WriteUint64(uint64(reflectValue.Uint()))
case reflect.Float32: return encoder.WriteFloat32(float32(reflectValue.Float())) case reflect.Float32: return encoder.WriteFloat32(float32(reflectValue.Float()))
case reflect.Float64: return encoder.WriteFloat64(float64(reflectValue.Float())) case reflect.Float64: return encoder.WriteFloat64(float64(reflectValue.Float()))
case reflect.Bool: return // SI has no payload
case reflect.String: case reflect.String:
if reflectValue.Len() > MaxStructureLength { if reflectValue.Len() > MaxStructureLength {
return 0, ErrTooLong return 0, ErrTooLong
@ -83,12 +78,6 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
return n, nil return n, nil
} }
// option
if isTypeOption(reflectValue.Type()) {
elemValue, _ := optionValue(reflectValue) // zero value for free
return EncodeAny(encoder, elemValue, tag)
}
// aggregates // aggregates
reflectType := reflect.TypeOf(value) reflectType := reflect.TypeOf(value)
switch reflectType.Kind() { switch reflectType.Kind() {
@ -263,8 +252,8 @@ func decodeAnyOrError(decoder *Decoder, destination reflect.Value, tag Tag) (n i
// not we receive an empty any value) actually makes it fucking // not we receive an empty any value) actually makes it fucking
// work. go figure(). // work. go figure().
// //
// (the map allocation functionality in skeletonPointer was // (the map allocation functionality in skeletonPointer has been
// removed after this comment was written) // removed)
value := reflect.MakeMapWithSize(reflect.TypeOf(dummyMap), lengthCast) value := reflect.MakeMapWithSize(reflect.TypeOf(dummyMap), lengthCast)
destination.Set(value) destination.Set(value)
destination = value destination = value
@ -309,24 +298,12 @@ func tagAny(reflectValue reflect.Value) (Tag, error) {
case reflect.Uint64: return LI.WithCN(7), nil case reflect.Uint64: return LI.WithCN(7), nil
case reflect.Float32: return FP.WithCN(3), nil case reflect.Float32: return FP.WithCN(3), nil
case reflect.Float64: return FP.WithCN(7), nil case reflect.Float64: return FP.WithCN(7), nil
case reflect.Bool:
if reflectValue.Bool() {
return SI.WithCN(1), nil
} else {
return SI.WithCN(0), nil
}
case reflect.String: return bufferLenTag(reflectValue.Len()), nil case reflect.String: return bufferLenTag(reflectValue.Len()), nil
} }
if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) { if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) {
return bufferLenTag(reflectValue.Len()), nil return bufferLenTag(reflectValue.Len()), nil
} }
// option
if isTypeOption(reflectValue.Type()) {
elem, _ := optionValue(reflectValue) // zero value for free
return tagAny(elem)
}
// aggregates // aggregates
reflectType := reflectValue.Type() reflectType := reflectValue.Type()
switch reflectType.Kind() { switch reflectType.Kind() {
@ -352,14 +329,9 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) {
for index := 0; index < reflectValue.Len(); index += 1 { for index := 0; index < reflectValue.Len(); index += 1 {
itemTag, err := tagAny(reflectValue.Index(index)) itemTag, err := tagAny(reflectValue.Index(index))
if err != nil { return n, err } if err != nil { return n, err }
if itemTag.Is(SBA) {
// SBA data in an LBA will always have the tag LBA:0,
// because 32 <= 256
continue
}
if itemTag.CN() > oneTag.CN() { oneTag = itemTag } if itemTag.CN() > oneTag.CN() { oneTag = itemTag }
} }
if oneTag.Is(SBA) { oneTag = LBA.WithCN(oneTag.CN()) } if oneTag.Is(SBA) { oneTag += 1 << 5 }
nn, err = encoder.WriteUint8(uint8(oneTag)) nn, err = encoder.WriteUint8(uint8(oneTag))
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
for index := 0; index < reflectValue.Len(); index += 1 { for index := 0; index < reflectValue.Len(); index += 1 {
@ -402,8 +374,7 @@ func canSet(destination reflect.Type, tag Tag) error {
switch destination.Kind() { switch destination.Kind() {
case case
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
reflect.Bool:
default: default:
return errCantAssignf("cannot assign integer to %v", destination) return errCantAssignf("cannot assign integer to %v", destination)
} }
@ -461,8 +432,6 @@ func setInt(destination reflect.Value, value int64, bytes int) {
// setUint expects a settable destination. // setUint expects a settable destination.
func setUint(destination reflect.Value, value uint64, bytes int) { func setUint(destination reflect.Value, value uint64, bytes int) {
switch { switch {
case destination.Kind() == reflect.Bool:
destination.Set(reflect.ValueOf(value > 0))
case destination.CanInt(): case destination.CanInt():
destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type())) destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type()))
case destination.CanUint(): case destination.CanUint():
@ -597,21 +566,6 @@ func isTypeAny(typ reflect.Type) bool {
return typ.Kind() == reflect.Interface && typ.NumMethod() == 0 return typ.Kind() == reflect.Interface && typ.NumMethod() == 0
} }
// isTypeOption returns whether the given reflect.Type is a ucontainer.Option,
// and returns the element type if true.
func isTypeOption(typ reflect.Type) bool {
// TODO: change when needed
goutilPath := "git.tebibyte.media/sashakoshka/go-util"
return typ.Name() == "Option" && typ.PkgPath() == goutilPath + "/container"
}
// optionValue returns the value of an option. The value MUST be an option, or
// this function will panic.
func optionValue(value reflect.Value) (elem reflect.Value, ok bool) {
result := value.MethodByName("Value").Call([]reflect.Value { })
return result[0], result[1].Bool()
}
// peekSlice returns the element tag and dimension count of the OTA currently // peekSlice returns the element tag and dimension count of the OTA currently
// being decoded. It does not use up the decoder, it only peeks. // being decoded. It does not use up the decoder, it only peeks.
func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {

View File

@ -3,10 +3,37 @@ package tape
import "bytes" import "bytes"
import "testing" import "testing"
import "reflect" import "reflect"
import "git.tebibyte.media/sashakoshka/go-util/container"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil" import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
var samplePayloads [][]byte var samplePayloads = [][]byte {
/* int8 */ []byte { byte(LSI.WithCN(0)), 0x45 },
/* int16 */ []byte { byte(LSI.WithCN(1)), 0x45, 0x67 },
/* int32 */ []byte { byte(LSI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* int64 */ []byte { byte(LSI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* uint5 */ []byte { byte(SI.WithCN(12)) },
/* uint8 */ []byte { byte(LI.WithCN(0)), 0x45 },
/* uint16 */ []byte { byte(LI.WithCN(1)), 0x45, 0x67 },
/* uint32 */ []byte { byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* uint64 */ []byte { byte(LI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* string */ []byte { byte(SBA.WithCN(7)), 'p', 'u', 'p', 'e', 'v', 'e', 'r' },
/* []byte */ []byte { byte(SBA.WithCN(5)), 'b', 'l', 'a', 'r', 'g' },
/* []string */ []byte {
byte(OTA.WithCN(0)), 2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 2,
0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67,
0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 3,
0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00,
0x00, 0x02, 0x82, 'h', 'i',
0x00, 0x03, 0x21, 0x39, 0x92,
},
}
var sampleValues = []any { var sampleValues = []any {
/* int8 */ int8(0x45), /* int8 */ int8(0x45),
@ -18,8 +45,6 @@ var sampleValues = []any {
/* uint16 */ uint16(0x4567), /* uint16 */ uint16(0x4567),
/* uint32 */ uint32(0x456789AB), /* uint32 */ uint32(0x456789AB),
/* uint64 */ uint64(0x456789ABCDEF0123), /* uint64 */ uint64(0x456789ABCDEF0123),
/* bool */ false,
/* bool */ true,
/* string */ "pupever", /* string */ "pupever",
/* []byte */ "blarg", /* []byte */ "blarg",
/* []string */ []string { /* []string */ []string {
@ -35,64 +60,6 @@ var sampleValues = []any {
0x0002: "hi", 0x0002: "hi",
0x0003: uint16(0x3992), 0x0003: uint16(0x3992),
}, },
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
}
type sample struct {
t Tag
v any
s tu.Snake
}
var samples = []sample {
/* int8 */ sample { t: LSI.WithCN(0), s: tu.S(0x45) },
/* int16 */ sample { t: LSI.WithCN(1), s: tu.S(0x45, 0x67) },
/* int32 */ sample { t: LSI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* int64 */ sample { t: LSI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* uint5 */ sample { t: SI.WithCN(12), s: tu.S() },
/* uint8 */ sample { t: LI.WithCN(0), s: tu.S(0x45) },
/* uint16 */ sample { t: LI.WithCN(1), s: tu.S(0x45, 0x67) },
/* uint32 */ sample { t: LI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* uint64 */ sample { t: LI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* bool */ sample { t: SI.WithCN(0), s: tu.S() },
/* bool */ sample { t: SI.WithCN(1), s: tu.S() },
/* string */ sample { t: SBA.WithCN(7), s: tu.S('p', 'u', 'p', 'e', 'v', 'e', 'r') },
/* []byte */ sample { t: SBA.WithCN(5), s: tu.S('b', 'l', 'a', 'r', 'g') },
/* []string */ sample {
t: OTA.WithCN(0),
s: tu.S(2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(2).AddVar(
[]byte { 0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67 },
[]byte { 0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB }),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(3).AddVar(
[]byte { 0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00 },
[]byte { 0x00, 0x02, 0x82, 'h', 'i' },
[]byte { 0x00, 0x03, 0x21, 0x39, 0x92 }),
},
}
var sampleOptionValues []any
func init() {
sampleOptionValues = make([]any, len(sampleValues))
for index, value := range sampleValues {
sampleOptionValues[index] = ucontainer.O(value)
samples[index].v = value
}
samplePayloads = make([][]byte, len(samples))
for index, sample := range samples {
item := append([]byte { byte(sample.t) }, sample.s.Flatten()...)
samplePayloads[index] = item
}
} }
type userDefinedInteger int16 type userDefinedInteger int16
@ -117,9 +84,7 @@ func TestEncodeAnyTable(test *testing.T) {
0x3456: userDefinedInteger(0x3921), 0x3456: userDefinedInteger(0x3921),
0x1F1F: float32(67.26), 0x1F1F: float32(67.26),
0x0F0F: float64(5.3), 0x0F0F: float64(5.3),
0xAAAA: false, }, KTV.WithCN(0), tu.S(9).AddVar(
0xBBBB: true,
}, KTV.WithCN(0), tu.S(11).AddVar(
[]byte { []byte {
0xF3, 0xB9, 0xF3, 0xB9,
byte(LSI.WithCN(3)), byte(LSI.WithCN(3)),
@ -173,14 +138,6 @@ func TestEncodeAnyTable(test *testing.T) {
byte(FP.WithCN(7)), byte(FP.WithCN(7)),
0x40, 0x15, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x40, 0x15, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
}, },
[]byte {
0xAA, 0xAA,
byte(SI.WithCN(0)),
},
[]byte {
0xBB, 0xBB,
byte(SI.WithCN(1)),
},
)) ))
if err != nil { test.Fatal(err) } if err != nil { test.Fatal(err) }
} }
@ -189,7 +146,7 @@ func TestDecodeWrongType(test *testing.T) {
for index, data := range samplePayloads { for index, data := range samplePayloads {
test.Logf("data %2d %v [%s]", index, Tag(data[0]), tu.HexBytes(data[1:])) test.Logf("data %2d %v [%s]", index, Tag(data[0]), tu.HexBytes(data[1:]))
// integers should only assign to other integers // integers should only assign to other integers
if index > 10 { if index > 8 {
cas := func(destination any) { cas := func(destination any) {
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0])) n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0]))
if err != nil { test.Fatalf("error: %v | n: %d", err, n) } if err != nil { test.Fatalf("error: %v | n: %d", err, n) }
@ -198,7 +155,7 @@ func TestDecodeWrongType(test *testing.T) {
if reflectValue.Int() != 0 { if reflectValue.Int() != 0 {
test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface()) test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface())
} }
} else if reflectValue.Kind() != reflect.Bool { } else {
if reflectValue.Uint() != 0 { if reflectValue.Uint() != 0 {
test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface()) test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface())
} }
@ -223,8 +180,6 @@ func TestDecodeWrongType(test *testing.T) {
{ var dest uint32; cas(&dest) } { var dest uint32; cas(&dest) }
test.Log("- uint64") test.Log("- uint64")
{ var dest uint64; cas(&dest) } { var dest uint64; cas(&dest) }
test.Log("- bool")
{ var dest bool; cas(&dest) }
} }
arrayCase := func(destination any) { arrayCase := func(destination any) {
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0])) n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0]))
@ -239,19 +194,19 @@ func TestDecodeWrongType(test *testing.T) {
} }
} }
// SBA/LBA types should only assign to other SBA/LBA types // SBA/LBA types should only assign to other SBA/LBA types
if index != 11 && index != 12 { if index != 9 && index != 10 {
test.Log("- string") test.Log("- string")
{ var dest string; arrayCase(&dest) } { var dest string; arrayCase(&dest) }
test.Log("- []byte") test.Log("- []byte")
{ var dest []byte; arrayCase(&dest) } { var dest []byte; arrayCase(&dest) }
} }
// arrays should only assign to other arrays // arrays should only assign to other arrays
if index != 13 { if index != 11 {
test.Log("- []string") test.Log("- []string")
{ var dest []string; arrayCase(&dest) } { var dest []string; arrayCase(&dest) }
} }
// tables should only assign to other tables // tables should only assign to other tables
if index != 14 && index != 15 { if index != 12 && index != 13 {
test.Log("- map[uint16] any") test.Log("- map[uint16] any")
{ var dest = map[uint16] any { }; arrayCase(&dest) } { var dest = map[uint16] any { }; arrayCase(&dest) }
} }
@ -276,12 +231,6 @@ func TestEncodeDecodeAnyTable(test *testing.T) {
func TestEncodeDecodeAnyDestination(test *testing.T) { func TestEncodeDecodeAnyDestination(test *testing.T) {
var destination any var destination any
for index, data := range samplePayloads { for index, data := range samplePayloads {
if _, isBool := sampleValues[index].(bool); isBool {
// test is invalid for bools because they are never
// created as a skeleton value
continue
}
tag := Tag(data[0]) tag := Tag(data[0])
payload := data[1:] payload := data[1:]
test.Logf("data %2d %v [%s]", index, tag, tu.HexBytes(payload)) test.Logf("data %2d %v [%s]", index, tag, tu.HexBytes(payload))
@ -300,20 +249,6 @@ func TestEncodeDecodeAnyDestination(test *testing.T) {
} }
} }
func TestEncodeOption(test *testing.T) {
for _, sample := range samples {
snake := sample.s
tag := sample.t
value := sample.v
if _, ok := value.(bool); tag.Is(SI) && !ok {
// we will never encode an SI unless its a bool
continue
}
err := testEncodeAny(test, value, tag, snake)
if err != nil { test.Fatal(err) }
}
}
func TestPeekSlice(test *testing.T) { func TestPeekSlice(test *testing.T) {
buffer := bytes.NewBuffer([]byte { buffer := bytes.NewBuffer([]byte {
2, byte(OTA.WithCN(3)), 2, byte(OTA.WithCN(3)),
@ -414,12 +349,6 @@ func TestTagAny(test *testing.T) {
func TestDecodeAny(test *testing.T) { func TestDecodeAny(test *testing.T) {
for index, payload := range samplePayloads { for index, payload := range samplePayloads {
if _, isBool := sampleValues[index].(bool); isBool {
// test is invalid for bools because they are never
// created as a skeleton value
continue
}
correctValue := sampleValues[index] correctValue := sampleValues[index]
data := payload[1:] data := payload[1:]
decoder := NewDecoder(bytes.NewBuffer(data)) decoder := NewDecoder(bytes.NewBuffer(data))

View File

@ -32,9 +32,7 @@ func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.S
test.Log("n: ", n) test.Log("n: ", n)
test.Log("tag: ", tag) test.Log("tag: ", tag)
test.Log("got: ", tu.HexBytes(bytes)) test.Log("got: ", tu.HexBytes(bytes))
test.Log(" : ", tu.HexChars(bytes))
test.Log("correct:", correctBytes) test.Log("correct:", correctBytes)
test.Log(" :", correctBytes.CharsString())
if tag != correctTag { if tag != correctTag {
return fmt.Errorf("tag not equal: %v != %v", tag, correctTag) return fmt.Errorf("tag not equal: %v != %v", tag, correctTag)
} }
@ -58,7 +56,6 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
test.Log("n: ", n) test.Log("n: ", n)
test.Log("tag:", tag) test.Log("tag:", tag)
test.Log("got:", tu.HexBytes(bytes)) test.Log("got:", tu.HexBytes(bytes))
test.Log(" :", tu.HexChars(bytes))
test.Log("decoding...", tag) test.Log("decoding...", tag)
if n != len(bytes) { if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes)) return fmt.Errorf("n not equal: %d != %d", n, len(bytes))