summaryrefslogtreecommitdiff
path: root/proto
diff options
context:
space:
mode:
Diffstat (limited to 'proto')
-rw-r--r--proto/fail.go11
-rw-r--r--proto/id.go28
-rw-r--r--proto/protocol.go342
3 files changed, 381 insertions, 0 deletions
diff --git a/proto/fail.go b/proto/fail.go
new file mode 100644
index 0000000..0b6366b
--- /dev/null
+++ b/proto/fail.go
@@ -0,0 +1,11 @@
+package proto
+
+type Fail Object
+
+func (f Fail) Cmd() Command {
+ return NewCmd("fail", "", Object(f))
+}
+
+func (f Fail) Error() string {
+ return f.Kind
+}
diff --git a/proto/id.go b/proto/id.go
new file mode 100644
index 0000000..22bb395
--- /dev/null
+++ b/proto/id.go
@@ -0,0 +1,28 @@
+package proto
+
+import (
+ "time"
+ "strconv"
+)
+
+var counter = make(chan uint8)
+func init() {
+ go func() {
+ var i uint8
+ for i = 0; true; i++ {
+ counter <- i
+ }
+ }()
+}
+
+var epoch = time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
+
+func GenId() string {
+ t := time.Now().UnixMilli() - epoch.UnixMilli()
+ id := uint64(t << 6) | uint64(<-counter & 63)
+ return strconv.FormatUint(id, 36)
+}
+
+func Timestamp() string {
+ return strconv.FormatInt(time.Now().Unix(), 10)
+}
diff --git a/proto/protocol.go b/proto/protocol.go
new file mode 100644
index 0000000..b59cd70
--- /dev/null
+++ b/proto/protocol.go
@@ -0,0 +1,342 @@
+package proto
+
+import (
+ "bufio"
+ "strings"
+ "errors"
+)
+
+type Object struct {
+ Kind string
+ Id string
+ Fields map[string]string
+}
+
+type Command struct {
+ Kind string
+ Target string
+ Args []Object
+}
+
+type Line struct {
+ Kind rune
+ RequestId string
+ Cmd Command
+}
+
+func NewCmd(kind string, target string, args ...Object) Command {
+ return Command {kind, target, args}
+}
+
+var SyntaxError = errors.New("invalid syntax")
+
+func ReadLiteral(b *bufio.Reader) (string, error) {
+ var sb strings.Builder
+ for {
+ c, _, err := b.ReadRune()
+ if strings.IndexRune(",;\n", c) != -1 {
+ b.UnreadRune()
+ break
+ }
+ if c == '\\' {
+ c, _, err = b.ReadRune()
+ }
+ if err != nil {
+ return "", err
+ }
+ sb.WriteRune(c)
+ }
+ return sb.String(), nil
+}
+
+func ReadIdentifier(b *bufio.Reader) (string, error) {
+ var sb strings.Builder
+ for {
+ c, _, err := b.ReadRune()
+ if strings.IndexRune("?!*<>:\\,#@~;\n\t ", c) != -1 {
+ b.UnreadRune()
+ break
+ }
+ if err != nil {
+ return "", err
+ }
+ sb.WriteRune(c)
+ }
+ return sb.String(), nil
+}
+
+func ReadObject(b *bufio.Reader) (Object, error) {
+ var o Object
+ var err error
+ o.Kind, err = ReadIdentifier(b)
+ if err != nil {
+ return o, err
+ }
+
+ o.Fields = make(map[string]string)
+
+ c, _, err := b.ReadRune()
+ if err != nil {
+ return o, err
+ }
+
+ if c == '#' {
+ o.Id, err = ReadIdentifier(b)
+ if err != nil {
+ return o, err
+ }
+
+ c, _, err = b.ReadRune()
+ if err != nil {
+ return o, err
+ }
+ }
+
+ switch c {
+ case ' ':
+ for {
+ key, err := ReadIdentifier(b)
+ if err != nil {
+ return o, err
+ }
+ c, _, err := b.ReadRune()
+ if err != nil {
+ return o, err
+ } else if c != ':' {
+ return o, SyntaxError
+ }
+ value, err := ReadLiteral(b)
+ if err != nil {
+ return o, err
+ }
+ o.Fields[key] = value
+ c, _, err = b.ReadRune();
+ if c == ';' || c == '\n' {
+ b.UnreadRune()
+ break
+ }
+ }
+ case ';':
+ b.UnreadRune()
+ default:
+ return o, SyntaxError
+ }
+ return o, nil
+}
+
+func ReadLine(b *bufio.Reader) (Line, error) {
+ var l Line
+ var err error
+ l.Kind, _, err = b.ReadRune()
+ if err != nil {
+ return l, err
+ }
+ switch l.Kind {
+ case '?', '!':
+ l.RequestId, err = ReadIdentifier(b)
+ if err != nil {
+ return l, err
+ }
+ c, _, err := b.ReadRune()
+ if err != nil {
+ return l, err
+ } else if c != ' ' {
+ return l, SyntaxError
+ }
+ case '*':
+ default:
+ return l, SyntaxError
+ }
+
+ l.Cmd.Kind, err = ReadIdentifier(b)
+ if err != nil {
+ return l, err
+ }
+
+ c, _, err := b.ReadRune()
+ if err != nil {
+ return l, err
+ }
+
+ if c == '>' {
+ l.Cmd.Target, err = ReadIdentifier(b)
+ if err != nil {
+ return l, err
+ }
+
+ c, _, err = b.ReadRune()
+ if err != nil {
+ return l, err
+ }
+ }
+
+ if c == ' ' {
+ for {
+ o, err := ReadObject(b)
+ if err != nil {
+ return l, err
+ }
+ l.Cmd.Args = append(l.Cmd.Args, o)
+
+ c, _, err := b.ReadRune()
+ if err != nil {
+ return l, err
+ }
+ if c == ';' {
+ break
+ }
+ }
+ } else if c != ';' {
+ return l, SyntaxError
+ }
+
+ c, _, err = b.ReadRune()
+ if err != nil {
+ return l, err
+ } else if c != '\n' {
+ return l, SyntaxError
+ }
+
+ return l, nil
+}
+
+func WriteLiteral(b *bufio.Writer, s string) error {
+ for _, c := range s {
+ if strings.IndexRune(",;\n\\", c) != -1 {
+ _, err := b.WriteRune('\\');
+ if err != nil {
+ return err
+ }
+ }
+ _, err := b.WriteRune(c);
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func WriteIdentifier(b *bufio.Writer, s string) error {
+ for _, c := range s {
+ if strings.IndexRune("?!*<>:\\,#@~;\n\t ", c) != -1 {
+ panic("invalid character in identifier")
+ }
+ _, err := b.WriteRune(c);
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func WriteObject(b *bufio.Writer, o Object) error {
+ err := WriteIdentifier(b, o.Kind)
+ if err != nil {
+ return err
+ }
+ if o.Id != "" {
+ b.WriteRune('#')
+ err := WriteIdentifier(b, o.Id)
+ if err != nil {
+ return err
+ }
+ }
+ if len(o.Fields) != 0 {
+ b.WriteRune(' ')
+ }
+ comma := false;
+ for k, v := range o.Fields {
+ if comma {
+ b.WriteRune(',')
+ }
+ err := WriteIdentifier(b, k)
+ if err != nil {
+ return err
+ }
+ b.WriteRune(':')
+ err = WriteLiteral(b, v)
+ if err != nil {
+ return err
+ }
+ comma = true
+ }
+ return nil
+}
+
+func WriteLine(b *bufio.Writer, l Line) error {
+ _, err := b.WriteRune(l.Kind)
+ if err != nil {
+ return err
+ }
+ switch l.Kind {
+ case '!', '?':
+ err := WriteIdentifier(b, l.RequestId)
+ if err != nil {
+ return err
+ }
+ b.WriteRune(' ')
+ case '*':
+ }
+
+ err = WriteIdentifier(b, l.Cmd.Kind)
+ if err != nil {
+ return err
+ }
+ if l.Cmd.Target != "" {
+ b.WriteRune('>')
+ err = WriteIdentifier(b, l.Cmd.Target)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(l.Cmd.Args) != 0 {
+ b.WriteRune(' ')
+ }
+ for i, o := range l.Cmd.Args {
+ err := WriteObject(b, o)
+ if err != nil {
+ return err
+ }
+ if i < len(l.Cmd.Args) - 1 {
+ b.WriteRune('\n')
+ }
+ }
+ _, err = b.WriteString(";\n")
+ if err != nil {
+ return err
+ }
+ return b.Flush()
+}
+
+func ReadLines(b *bufio.Reader) (<-chan Line, <-chan error) {
+ out, errChan := make(chan Line, 1), make(chan error, 1)
+ go func() {
+ for {
+ l, err := ReadLine(b)
+ if err != nil {
+ errChan <- err
+ close(out)
+ return
+ } else {
+ out <- l
+ }
+ }
+ }()
+ return out, errChan
+}
+
+func WriteLines(b *bufio.Writer, lines <-chan Line) <-chan error {
+ errChan := make(chan error, 1)
+ go func() {
+ for l := range lines {
+ err := WriteLine(b, l)
+ if err != nil {
+ errChan <- err
+ return
+ }
+ }
+ close(errChan)
+ }()
+ return errChan
+}