Repos / gorts / 54cb328165
commit 54cb328165e3a9294226cfecf0f2c8a349fcfe24
Author: Nhân <hi@imnhan.com>
Date:   Wed Jun 21 18:55:48 2023 +0700

    implement netstring helpers on both Go & Tcl

diff --git a/Makefile b/Makefile
index 885462b..31971fd 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,10 @@ dist/GORTS-Linux.zip: linux
 watch:
 	find . -name '*.go' -o -name '*.tcl' | entr -rc go run .
 
+# I only have tests on the netstring package now
+test:
+	find ./netstring -name '*.go' | entr -rc go test ./netstring
+
 gorts.png: gorts.svg
 	convert -background transparent -density 300 -resize 256x256 gorts.svg gorts.png
 
diff --git a/netstring/netstring.go b/netstring/netstring.go
new file mode 100644
index 0000000..20c2506
--- /dev/null
+++ b/netstring/netstring.go
@@ -0,0 +1,38 @@
+// This package implements just enough [netstrings]
+// for our homebrew IPC solution.
+//
+// [netstrings]: https://cr.yp.to/proto/netstrings.txt
+package netstring
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+)
+
+func Encode(s string) string {
+	return fmt.Sprintf("%d:%s,", len(s), s)
+}
+
+// A SplitFunc to be used in a bufio.Scanner
+func SplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
+	colonIndex := bytes.IndexRune(data, ':')
+	if colonIndex == -1 {
+		// Haven't fully read the length part yet => skip:
+		return 0, nil, nil
+	}
+
+	length, err := strconv.Atoi(string(data[:colonIndex]))
+	if err != nil {
+		return 0, nil, fmt.Errorf("split netstring: %w", err)
+	}
+
+	rest := data[colonIndex+1:]
+	if len(rest) < length+1 { // +1 for "," at the end
+		// Haven't read the whole netstring yet => skip:
+		return 0, nil, nil
+	}
+
+	// The whole netstring should now be within the buffer
+	return colonIndex + 1 + length + 1, rest[:length], nil
+}
diff --git a/netstring/netstring_test.go b/netstring/netstring_test.go
new file mode 100644
index 0000000..5636180
--- /dev/null
+++ b/netstring/netstring_test.go
@@ -0,0 +1,50 @@
+package netstring
+
+import (
+	"bufio"
+	"strings"
+	"testing"
+)
+
+func TestEncode(t *testing.T) {
+	var tests = []struct {
+		in  string
+		out string
+	}{
+		{"hello world!", "12:hello world!,"},
+		{"", "0:,"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.in, func(t *testing.T) {
+			ans := Encode(tt.in)
+			if ans != tt.out {
+				t.Errorf("Encode(): got %s | want %s", ans, tt.out)
+			}
+		})
+	}
+}
+
+func TestSplit(t *testing.T) {
+	in := strings.NewReader("5:hello,6:world!,0:,")
+	want := []string{"hello", "world!", ""}
+
+	t.Run("Split", func(t *testing.T) {
+		var results []string
+		scanner := bufio.NewScanner(in)
+		scanner.Split(SplitFunc)
+		for scanner.Scan() {
+			netstring := scanner.Text()
+			results = append(results, netstring)
+		}
+
+		if len(results) != len(want) {
+			t.Errorf("Split(): got %s | want %s", results, want)
+		}
+		for i, actual := range results {
+			if actual != want[i] {
+				t.Errorf("Split()[%d]: got %s | want %s", i, results, want)
+			}
+		}
+	})
+}
diff --git a/tcl/netstring.tcl b/tcl/netstring.tcl
new file mode 100644
index 0000000..fa510b1
--- /dev/null
+++ b/tcl/netstring.tcl
@@ -0,0 +1,25 @@
+proc netstring {s} {
+    set len [string length $s]
+    return "$len:$s,"
+}
+
+proc netstrings {strings} {
+    set result ""
+    foreach s $strings {
+        set result [string cat $result [netstring $s]]
+    }
+    return [netstring $result]
+}
+
+proc readnetstring {chan} {
+    set data ""
+    set char ""
+    while {$char != ":"} {
+        set char [read $chan 1]
+        set data [string cat $data $char]
+    }
+    set nslen [string range $data 0 {end-1}]
+    set nstr [read $chan $nslen]
+    read $chan 1; # consume the trailing ","
+    return $nstr
+}