summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-21 22:16:06 -0300
committerEuAndreh <eu@euandre.org>2026-04-21 22:16:06 -0300
commitf478620978ae884aa5e7a0ddb742faa5cb23ae3a (patch)
tree0e2c87b1e3d5d99aaccd99d24d47ca45d6d2c1fe
parentRemove Go code (diff)
downloadpapod-f478620978ae884aa5e7a0ddb742faa5cb23ae3a.tar.gz
papod-f478620978ae884aa5e7a0ddb742faa5cb23ae3a.tar.xz
m
-rw-r--r--.gitignore24
-rw-r--r--Makefile215
-rw-r--r--deps.mk77
-rwxr-xr-xmkdeps.sh49
-rw-r--r--src/labareda.edn0
-rwxr-xr-xsrc/papod.bin.in5
-rw-r--r--src/papod.clj266
-rwxr-xr-xtests/cli-opts.sh4
-rw-r--r--tests/fuzz/api.clj11
-rw-r--r--tests/integration.clj6
-rw-r--r--tests/lib.sh122
-rw-r--r--tests/unit.clj54
12 files changed, 608 insertions, 225 deletions
diff --git a/.gitignore b/.gitignore
index 3c4a921..91f95d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,19 +1,13 @@
/doc/*
!/doc/*.en.*.adoc
+/doc/*.en.3.adoc
/po/*/*.mo
-/src/meta.go
-/*.bin
-/*.db*
-/src/*.a
+/src/*/
+/src/*.class
+/tests/*.class
+/tests/fuzz/*.class
+/*.jar
/src/*.bin
-/src/*cgo*
-/tests/*.a
-/tests/*.bin
-/tests/functional/*/*.a
-/tests/functional/*/*.bin
-/tests/fuzz/*/*.a
-/tests/fuzz/*/*.bin
-/tests/benchmarks/*/*.a
-/tests/benchmarks/*/*.bin
-/tests/benchmarks/*/*.txt
-/tests/fuzz/corpus/
+/src/cli.edn
+/schema.edn
+/datomic.log
diff --git a/Makefile b/Makefile
index 1c8873b..52f9e4a 100644
--- a/Makefile
+++ b/Makefile
@@ -7,32 +7,43 @@ NAME_UC = $(NAME)
PREFIX = /usr
BINDIR = $(PREFIX)/bin
LIBDIR = $(PREFIX)/lib
-GOLIBDIR = $(LIBDIR)/go
INCLUDEDIR = $(PREFIX)/include
SRCDIR = $(PREFIX)/src/$(NAME)
SHAREDIR = $(PREFIX)/share
LOCALEDIR = $(SHAREDIR)/locale
MANDIR = $(SHAREDIR)/man
+JAVADIR = $(SHAREDIR)/java
EXEC = ./
## Where to store the installation. Empty by default.
DESTDIR =
-LDLIBS = --static -lscrypt-kdf
-GOCFLAGS = -I $(GOLIBDIR)
-GOLDFLAGS = -L $(GOLIBDIR)
-N = `nproc`
+LDLIBS =
+JAVA = java -client
+CLASSPATH = `printf '$(JARPATH)/%s.jar\n' $(JARDEPS) | paste -sd:`
+JARDEPS = \
+ cracha \
+ fiinha \
+ jasm \
+ labareda \
+ peer \
+ dtmc \
+ base \
+ clojure
.SUFFIXES:
-.SUFFIXES: .go .a .bin .bin-check .adoc .po .mo
+.SUFFIXES: .in .clj .class .class-check .jar .jar-check .adoc .po .mo
+.SUFFIXES: .sh .sh-check
-.go.a:
- go tool compile -I $(@D) $(GOCFLAGS) -o $@ -p $(*F) \
- `find $< $$(if [ $(*F) != main ]; then \
- echo src/$(NAME).go src/meta.go; fi) | uniq`
-
-.a.bin:
- go tool link -L $(@D) $(GOLDFLAGS) -o $@ --extldflags '$(LDLIBS)' $<
+.in:
+ sed \
+ -e 's:@VERSION@:$(VERSION):g' \
+ -e 's:@DATE@:$(DATE):g' \
+ -e 's:@NAME@:$(NAME):g' \
+ -e 's:@JAVADIR@:$(JAVADIR):g' \
+ -e "s_@CLASSPATH@_$(CLASSPATH)_g" \
+ < $< > $@
+ if [ -x $< ]; then chmod +x $@; fi
.adoc:
asciidoctor -b manpage -o $@ $<
@@ -46,39 +57,46 @@ all:
include deps.mk
-libs.a = $(libs.go:.go=.a)
-mains.a = $(mains.go:.go=.a)
-mains.bin = $(mains.go:.go=.bin)
-functional/lib.a = $(functional/lib.go:.go=.a)
-fuzz/lib.a = $(fuzz/lib.go:.go=.a)
-benchmarks/lib.a = $(benchmarks/lib.go:.go=.a)
+integration.sh-check = $(integration.sh:.sh=.sh-check)
+fuzz.class-check = $(fuzz.clj:.clj=.class-check)
+
+inits = \
+ src/$(NAME)__init.class \
+ tests/unit__init.class \
+ tests/integration__init.class \
+ $(fuzzinit.class) \
+
manpages.N.adoc = $(manpages.en.N.adoc) $(manpages.XX.N.adoc)
-manpages.N = $(manpages.N.adoc:.adoc=)
+manpages.N = $(manpages.N.adoc:.adoc=) doc/$(NAME).en.3
sources.mo = $(sources.po:.po=.mo)
sources = \
- src/$(NAME).go \
- src/meta.go \
- src/main.go \
+ src/$(NAME).bin.in \
+ src/$(NAME).clj \
+ src/labareda.edn \
derived-assets = \
- src/meta.go \
- $(libs.a) \
- $(mains.a) \
- $(mains.bin) \
- $(NAME).bin \
- $(manpages.XX.N.adoc) \
- $(manpages.N) \
- $(sources.mo) \
+ $(inits) \
+ $(NAME).jar \
+ unit.jar \
+ integration.jar \
+ src/$(NAME).bin \
+ schema.edn \
+ src/cli.edn \
+ $(manpages.XX.N.adoc) \
+ $(manpages.N) \
+ $(sources.mo) \
+ doc/$(NAME).en.3.adoc \
side-assets = \
- *$(NAME).db* \
- tests/functional/*/*.go.db* \
- tests/fuzz/corpus/ \
- tests/benchmarks/*/main.txt \
- $(NAME).daemon.sock \
- $(NAME).commander.sock \
+ src/$(NAME)/ \
+ src/*.class \
+ tests/*.class \
+ tests/fuzz/*.class \
+ datomic.log \
+ $(NAME).daemon.socket \
+ $(NAME).commander.socket \
@@ -87,47 +105,68 @@ side-assets = \
all: $(derived-assets)
-$(libs.a): Makefile deps.mk
-$(libs.a): src/$(NAME).go src/meta.go
+$(inits): Makefile deps.mk
+
+$(integration.sh): $(NAME).jar
+$(fuzzinit.class): $(NAME).jar
+
+src/$(NAME)__init.class: src/$(NAME).clj
+tests/unit__init.class: tests/unit.clj $(NAME).jar
+tests/integration__init.class: tests/integration.clj $(NAME).jar
+$(inits):
+ rm -f $@
+ JAR=`if [ $(@D) != 'src' ]; then echo $(NAME).jar:; fi`; \
+ DIR=`echo $@ | cut -d/ -f1`; \
+ NS=`echo $@ | cut -d/ -f2- | sed 's/__.*$$//' | tr / .`; \
+ $(JAVA) -ea --class-path $$DIR:$$JAR$(CLASSPATH) clojure.main -e \
+ "(binding [*compile-path* \"$$DIR\"] \
+ (compile '$$NS))"
-$(fuzz/lib.a):
- go tool compile $(GOCFLAGS) -o $@ -p $(NAME) -d=libfuzzer \
- $*.go src/$(NAME).go src/meta.go
+src/cli.edn: src/labareda.edn
+ labareda compile < src/labareda.edn > $@
-src/meta.go: Makefile
- echo 'package $(NAME)' > $@
- echo 'const Version = "$(VERSION)"' >> $@
- echo 'const Name = "$(NAME)"' >> $@
- echo 'const LOCALEDIR = "$(LOCALEDIR)"' >> $@
+$(NAME).jar: src/$(NAME)__init.class src/cli.edn
+ cd src && jar -cvf ../$@ `find . -name '*.class'` cli.edn
-$(NAME).bin: src/main.bin
- ln -fs src/main.bin $@
+unit.jar: tests/unit__init.class
+integration.jar: tests/integration__init.class
+unit.jar integration.jar:
+ cd tests && jar -cvf ../$@ $**.class
+
+schema.edn: $(NAME).jar
+ $(JAVA) -ea --class-path $(NAME).jar:$(CLASSPATH) clojure.main -e \
+ "(do (require '$(NAME) 'clojure.pprint) \
+ (clojure.pprint/pprint @#'$(NAME)/schema))" > $@
$(manpages.XX.N.adoc): po/doc/po4a.cfg
po4a --no-update --translate-only $@ po/doc/po4a.cfg
+doc/$(NAME).en.3.adoc: $(NAME).jar
+ env CLASSPATH="$(CLASSPATH)" base $(NAME).jar > $@
+
+
+
+src/schema.edn-check: schema.edn
+ diff -U10 src/schema.edn schema.edn
-tests.bin-check = \
- tests/main.bin-check \
- $(functional/main.go:.go=.bin-check) \
+unit.jar-check: unit.jar
+integration.jar-check: integration.jar
+unit.jar-check integration.jar-check:
+ $(JAVA) -ea --class-path $*.jar:$(NAME).jar:$(CLASSPATH) $*
-$(tests.bin-check):
- $(EXEC)$*.bin
-check-unit: $(tests.bin-check)
+check-unit: unit.jar-check
+check-unit: src/schema.edn-check
-integration-tests = \
- tests/cli-opts.sh \
- tests/integration.sh \
+$(integration.sh-check):
+ env CLASSPATH="$(CLASSPATH)" \
+ sh $*.sh
-.PRECIOUS: $(integration-tests)
-$(integration-tests): $(NAME).bin
-$(integration-tests): ALWAYS
- sh $@
-check-integration: $(integration-tests)
+check-integration: integration.jar-check
+check-integration: $(integration.sh-check)
check-integration: fuzz
@@ -139,33 +178,11 @@ check: check-unit check-integration
FUZZSEC=1
-fuzz/main.bin-check = $(fuzz/main.go:.go=.bin-check)
-$(fuzz/main.bin-check):
- $(EXEC)$*.bin --test.fuzztime=$(FUZZSEC)s --test.parallel=$N \
- --test.fuzz='.*' --test.fuzzcachedir=tests/fuzz/corpus
-
-fuzz: $(fuzz/main.bin-check)
-
-
-
-benchmarks/main.bin-check = $(benchmarks/main.go:.go=.bin-check)
-$(benchmarks/main.bin-check):
- printf '%s\n' '$(EXEC)$*.bin' > $*.txt
- LANG=POSIX.UTF-8 time -p $(EXEC)$*.bin 2>> $*.txt
- printf '%s\n' '$*.txt'
-
-bench: $(benchmarks/main.bin-check)
-
-
-
-i18n-doc:
- po4a po/doc/po4a.cfg
-
-i18n-code:
- gotext src/$(NAME).go > po/$(NAME)/$(NAME).pot
- po4a po/$(NAME)/po4a.cfg
+$(fuzz.class-check):
+ $(JAVA) -ea --class-path tests:$(NAME).jar:$(CLASSPATH) \
+ `echo $* | cut -d/ -f2- | tr / .` $(FUZZSEC)
-i18n: i18n-doc i18n-code
+fuzz: $(fuzz.class-check)
@@ -179,12 +196,12 @@ clean:
## ensures that all installable artifacts are crafted beforehand.
install: all
mkdir -p \
- '$(DESTDIR)$(BINDIR)' \
- '$(DESTDIR)$(GOLIBDIR)' \
- '$(DESTDIR)$(SRCDIR)' \
+ '$(DESTDIR)$(BINDIR)' \
+ '$(DESTDIR)$(JAVADIR)' \
+ '$(DESTDIR)$(SRCDIR)' \
- cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME)
- cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)'
+ cp src/$(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME)
+ cp $(NAME).jar '$(DESTDIR)$(JAVADIR)'
cp $(sources) '$(DESTDIR)$(SRCDIR)'
instool '$(DESTDIR)$(MANDIR)' install man $(manpages.N)
instool '$(DESTDIR)$(LOCALEDIR)' install mo $(sources.mo)
@@ -194,17 +211,13 @@ install: all
## A dedicated test asserts that this is always true.
uninstall:
rm -rf \
- '$(DESTDIR)$(BINDIR)'/$(NAME) \
- '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \
- '$(DESTDIR)$(SRCDIR)' \
+ '$(DESTDIR)$(BINDIR)'/$(NAME) \
+ '$(DESTDIR)$(JAVADIR)'/$(NAME).jar \
+ '$(DESTDIR)$(SRCDIR)' \
instool '$(DESTDIR)$(MANDIR)' uninstall man $(manpages.N)
instool '$(DESTDIR)$(LOCALEDIR)' uninstall mo $(sources.mo)
-## Run it locally.
-run: all
- rm -f $(NAME).daemon.sock $(NAME).commander.sock
- ./$(NAME).bin
ALWAYS:
diff --git a/deps.mk b/deps.mk
index 279faaa..5c426f0 100644
--- a/deps.mk
+++ b/deps.mk
@@ -1,74 +1,15 @@
-libs.go = \
- src/papod.go \
- tests/benchmarks/join-leave/papod.go \
- tests/functional/multiple-channels-and-users/papod.go \
- tests/fuzz/api-check/papod.go \
- tests/papod.go \
-
-mains.go = \
- src/main.go \
- tests/benchmarks/join-leave/main.go \
- tests/functional/multiple-channels-and-users/main.go \
- tests/fuzz/api-check/main.go \
- tests/main.go \
-
manpages.en.N.adoc = \
doc/papod.en.0.adoc \
-manpages.XX.N.adoc = \
- doc/papod.de.0.adoc \
- doc/papod.eo.0.adoc \
- doc/papod.es.0.adoc \
- doc/papod.fr.0.adoc \
- doc/papod.pt.0.adoc \
-
-sources.po = \
- po/papod/de.po \
- po/papod/eo.po \
- po/papod/es.po \
- po/papod/fr.po \
- po/papod/pt.po \
-
-functional/lib.go = \
- tests/functional/multiple-channels-and-users/papod.go \
-
-functional/main.go = \
- tests/functional/multiple-channels-and-users/main.go \
-
-fuzz/lib.go = \
- tests/fuzz/api-check/papod.go \
-
-fuzz/main.go = \
- tests/fuzz/api-check/main.go \
+integration.sh = \
+ tests/integration.sh \
-benchmarks/lib.go = \
- tests/benchmarks/join-leave/papod.go \
+fuzz.clj = \
+ tests/fuzz/api.clj \
-benchmarks/main.go = \
- tests/benchmarks/join-leave/main.go \
+fuzzinit.class = \
+ tests/fuzz/api__init.class \
-src/main.a: src/main.go
-src/papod.a: src/papod.go
-tests/benchmarks/join-leave/main.a: tests/benchmarks/join-leave/main.go
-tests/benchmarks/join-leave/papod.a: tests/benchmarks/join-leave/papod.go
-tests/functional/multiple-channels-and-users/main.a: tests/functional/multiple-channels-and-users/main.go
-tests/functional/multiple-channels-and-users/papod.a: tests/functional/multiple-channels-and-users/papod.go
-tests/fuzz/api-check/main.a: tests/fuzz/api-check/main.go
-tests/fuzz/api-check/papod.a: tests/fuzz/api-check/papod.go
-tests/main.a: tests/main.go
-tests/papod.a: tests/papod.go
-src/main.bin: src/main.a
-tests/benchmarks/join-leave/main.bin: tests/benchmarks/join-leave/main.a
-tests/functional/multiple-channels-and-users/main.bin: tests/functional/multiple-channels-and-users/main.a
-tests/fuzz/api-check/main.bin: tests/fuzz/api-check/main.a
-tests/main.bin: tests/main.a
-src/main.bin-check: src/main.bin
-tests/benchmarks/join-leave/main.bin-check: tests/benchmarks/join-leave/main.bin
-tests/functional/multiple-channels-and-users/main.bin-check: tests/functional/multiple-channels-and-users/main.bin
-tests/fuzz/api-check/main.bin-check: tests/fuzz/api-check/main.bin
-tests/main.bin-check: tests/main.bin
-src/main.a: src/$(NAME).a
-tests/benchmarks/join-leave/main.a: tests/benchmarks/join-leave/$(NAME).a
-tests/functional/multiple-channels-and-users/main.a: tests/functional/multiple-channels-and-users/$(NAME).a
-tests/fuzz/api-check/main.a: tests/fuzz/api-check/$(NAME).a
-tests/main.a: tests/$(NAME).a
+tests/integration.sh-check: tests/integration.sh
+tests/fuzz/api__init.class: tests/fuzz/api.clj
+tests/fuzz/api.class-check: tests/fuzz/api__init.class
diff --git a/mkdeps.sh b/mkdeps.sh
index ae6fffc..883fb90 100755
--- a/mkdeps.sh
+++ b/mkdeps.sh
@@ -1,49 +1,24 @@
#!/bin/sh
-set -eu
+set -euo pipefail
export LANG=POSIX.UTF-8
-libs() {
- find src tests -name '*.go' |
- grep -Ev '/(main|meta)\.go$' |
- grep -Ev '/_cgo_(import|gotypes)\.go$' |
- grep -Ev '\.cgo1\.go$'
+integrations() {
+ find tests/ -name '*.sh' -type f -perm -111
}
-mains() {
- find src tests -name '*.go' | grep '/main\.go$'
-}
-
-docs() {
- find doc/*.en.*.adoc
-}
-
-xdocs() {
- for l in `find po/doc/*.po | xargs -I% basename % .po`; do
- docs | sed 's|/\(.*\)\.en\.\(.*\)$|/\1.'"$l"'.\2|'
- done
-}
-
-pos() {
- find po/ -name '*.po' | grep -Ev '^po/(doc|tests)/'
+fuzzs() {
+ find tests/fuzz/*.clj
}
-libs | varlist 'libs.go'
-mains | varlist 'mains.go'
-docs | varlist 'manpages.en.N.adoc'
-xdocs | varlist 'manpages.XX.N.adoc'
-pos | varlist 'sources.po'
+find doc/*.en.*.adoc | varlist 'manpages.en.N.adoc'
-find tests/functional/*/*.go -not -name main.go | varlist 'functional/lib.go'
-find tests/functional/*/main.go | varlist 'functional/main.go'
-find tests/fuzz/*/*.go -not -name main.go | varlist 'fuzz/lib.go'
-find tests/fuzz/*/main.go | varlist 'fuzz/main.go'
-find tests/benchmarks/*/*.go -not -name main.go | varlist 'benchmarks/lib.go'
-find tests/benchmarks/*/main.go | varlist 'benchmarks/main.go'
+integrations | varlist 'integration.sh'
+fuzzs | varlist 'fuzz.clj'
+fuzzs | sed 's/\.clj$/__init.class/' | varlist 'fuzzinit.class'
-{ libs; mains; } | sort | sed 's/^\(.*\)\.go$/\1.a:\t\1.go/'
-mains | sort | sed 's/^\(.*\)\.go$/\1.bin:\t\1.a/'
-mains | sort | sed 's/^\(.*\)\.go$/\1.bin-check:\t\1.bin/'
-mains | sort | sed 's|^\(.*\)/main\.go$|\1/main.a:\t\1/$(NAME).a|'
+integrations | sed 's/^\(.*\)\.sh$/\1.sh-check:\t\1.sh/'
+fuzzs | sed 's/^\(.*\)\.clj$/\1__init.class:\t\1.clj/'
+fuzzs | sed 's/^\(.*\)\.clj$/\1.class-check:\t\1__init.class/'
diff --git a/src/labareda.edn b/src/labareda.edn
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/labareda.edn
diff --git a/src/papod.bin.in b/src/papod.bin.in
new file mode 100755
index 0000000..1a25c3b
--- /dev/null
+++ b/src/papod.bin.in
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -euo pipefail
+
+export CLASSPATH="@JAVADIR@/@NAME@.jar:@CLASSPATH@${CLASSPATH:+:}${CLASSPATH:-}"
+exec java -client @NAME@ "$@"
diff --git a/src/papod.clj b/src/papod.clj
new file mode 100644
index 0000000..e8aa5e3
--- /dev/null
+++ b/src/papod.clj
@@ -0,0 +1,266 @@
+(ns
+ ^{:sections ["bleh"]}
+ papod
+ (:require [base :refer [def- defconst- third]]
+ [clojure.set :as set]
+ [clojure.string :as string])
+ (:import (java.net StandardProtocolFamily UnixDomainSocketAddress)
+ (java.nio.channels ServerSocketChannel)
+ (java.nio.file Files)))
+
+
+
+(def- schema
+ [])
+
+(defconst- +delimiter+
+ "\r\n")
+
+(defconst- +separator+
+ \space)
+
+(defn- set-of-chars
+ [min max]
+ {:pre [(char? min)
+ (char? max)]
+ :post [(every? char? %)]}
+ (->> [(int min) (inc (int max))]
+ (apply range)
+ (map char)
+ (into #{})))
+
+(defconst- +digits+
+ (set-of-chars \0 \9))
+
+(defconst- +letters+
+ (set/union
+ (set-of-chars \A \Z)
+ (set-of-chars \a \z)))
+
+(defn- digit?
+ [c]
+ (+digits+ c))
+
+(defn- letter?
+ [c]
+ (+letters+ c))
+
+(defn- get-raw-message
+ [input]
+ {:pre [(string? input)]
+ :post [(map? %)
+ (string? (:input %))
+ (#{:needs-more :accepting} (:status %))
+ (let [input'old input
+ {:keys [status input raw-message]} %]
+ (if (= :needs-more status)
+ (and (= input'old input)
+ (nil? raw-message))
+ (or (false? raw-message)
+ (and (= input'old (str raw-message
+ +delimiter+
+ input))
+ (nil? (string/index-of raw-message +delimiter+))))))]}
+ (let [index (string/index-of input +delimiter+)]
+ (if-not index
+ {:status :needs-more
+ :input input}
+ (let [payload (subs input 0 index)
+ rest (subs input (+ index (count +delimiter+)))]
+ {:status :accepting
+ :input rest
+ :raw-message (and (not (string/blank? payload))
+ payload)}))))
+
+(defn- valid-err?
+ [[x offset {:keys [message type data] :as err}]]
+ (and (nil? x)
+ (nil? offset)
+ (map? err)
+ (string? message)
+ (keyword? type)
+ (map? data)))
+
+(defn- valid-prefix?
+ [x]
+;; FIXME
+ true)
+
+(defn- parse-prefix-components
+ [s]
+ {:pre []
+ :post []}
+;; FIXME
+ ())
+
+(defn- parse-prefix
+ [s]
+ {:pre [(string/starts-with? s ":")]
+ :post [(let [[prefix offset err] %]
+ (if err
+ (valid-err? %))
+ (valid-prefix? %))]}
+ (let [end (string/index-of s +separator+)]
+ (cond
+ (= 1 (count s))
+ [nil nil {:message "Empty prefix and empty message"
+ :type :bad-empty-prefix-and-message
+ :data {}}]
+ (= 1 end)
+ [nil nil {:message "Empty prefix"
+ :type :bad-empty-prefix
+ :data {}}]
+ (nil? end)
+ [nil nil {:message "Missing prefix separator"
+ :type :no-prefix-separator
+ :data {}}]
+ :else
+ (parse-prefix-components s))))
+
+(defn- parse-command
+ [s]
+ {:pre [(not (string/blank? s))]
+ :post [(let [[command offset err] %]
+ (if err
+ (valid-err? %)
+ (and (string? command)
+ (not (string/blank? command))
+ (pos? offset)
+ (= offset (count command))
+ (nil? err))))]}
+ (loop [chars []
+ index 0]
+ (if (or (= index (count s))
+ (= +separator+ (nth s index)))
+ [(apply str chars) (count chars)]
+ (let [c (nth s index)]
+ (if-not (letter? c)
+ [nil nil {:message "Bad char for non-numerical command"
+ :data {:c c}}]
+ (recur (conj chars c)
+ (inc index)))))))
+
+(defconst- params-re
+ (java.util.regex.Pattern/compile (str +separator+)))
+
+(defn- parse-params
+ [s]
+ {:pre [(string? s)]
+ :post [(nil? (third %))
+ (vector? (first %))
+ (every? string? (first %))
+ (= (count s) (second %))]}
+ (if (zero? (count s))
+ [[] 0]
+ (loop [[part & rest] (string/split s params-re)
+ out []]
+ (cond
+ (not part)
+ [out (count s)]
+ (string/starts-with? part ":")
+ [(conj out
+ (apply str
+ (concat [part] rest)))
+ (count s)]
+ :else
+ (recur rest (conj out part))))))
+
+(defn- valid-message?
+ [{:keys [prefix command params] :as message}]
+ (and (map? message)
+ (or (nil? prefix)
+ (valid-prefix? prefix))
+ (string? command)
+ (vector? params)
+ (every? string? params)))
+
+(defn- parse-message
+ [raw-message]
+ {:pre [(not (string/blank? raw-message))]
+ :post [(let [[message err] %]
+ (if err
+ (valid-err? [nil nil err])
+ (valid-message? message)))]}
+ (let [[prefix offset1 err] (if (string/starts-with? raw-message ":")
+ (parse-prefix raw-message)
+ [nil 0])]
+ (if err
+ [nil err]
+ (let [[command offset2 err] (parse-command (subs raw-message offset1))]
+ (if err
+ [nil err]
+ (let [[params _ err] (parse-params (subs raw-message
+ (+ offset1 offset2)))]
+ (if err
+ [nil err]
+ [{:prefix prefix
+ :command command
+ :params params}
+ nil])))))))
+
+(defn- send-replies!
+ [replies w]
+ ;; FIXME
+ )
+
+(defn- replies-for!
+ [{} components]
+ ;; wait-for
+ )
+
+(defn- handle-message!
+ [message w components]
+ (send-replies! (replies-for! message components)
+ w))
+
+(defn- process-message!
+ [raw-message w components]
+ (let [[message err] (parse-message raw-message)]
+ (if err
+ (:log :bad-message err)
+ (handle-message! message w components))))
+
+(defn- process-input!
+ [input w components]
+ (let [{:keys [status input raw-message]} (get-raw-message input)]
+ (if (= status :needs-more)
+ input
+ (do
+ (process-message! raw-message w components)
+ (recur input w components)))))
+
+(defn- db
+ [datomic]
+ :db)
+
+(defconst- +buffer-size+
+ 1024)
+
+(defconst- +charset+
+ "UTF-8")
+
+(defn- client-loop!
+ [socket components]
+ (let [r (.getInputStream socket)
+ w (.getOutputStream socket)
+ b (make-array Byte/TYPE +buffer-size+)]
+ (loop [acc ""]
+ (let [n (.read r b)]
+ (when (pos? n)
+ (recur (process-input! (str acc (String. b +charset+))
+ w
+ components)))))))
+
+(defconst- +socket-path+
+ "papod.socket")
+
+(defn- start
+ [components]
+ (let [address (UnixDomainSocketAddress/of +socket-path+)
+ server-channel (ServerSocketChannel/open StandardProtocolFamily/UNIX)]
+ (Files/deleteIfExists address)
+ (.bind server-channel address)
+ (while true
+ (let [client (.accept server-channel)]
+ (.start (Thread/ofVirtual)
+ #(client-loop! client components))))))
diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh
deleted file mode 100755
index fcb62ca..0000000
--- a/tests/cli-opts.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-set -eu
-
-exit
diff --git a/tests/fuzz/api.clj b/tests/fuzz/api.clj
new file mode 100644
index 0000000..d389c13
--- /dev/null
+++ b/tests/fuzz/api.clj
@@ -0,0 +1,11 @@
+(ns fuzz.api
+ (:require [base])
+ (:gen-class))
+
+
+
+(defn -main
+ [& args]
+ (base/run-fuzz-loop (System/currentTimeMillis)
+ (base/duration-ms-from-args args)
+ (fn [_i] (base/uuidv7))))
diff --git a/tests/integration.clj b/tests/integration.clj
new file mode 100644
index 0000000..18811a2
--- /dev/null
+++ b/tests/integration.clj
@@ -0,0 +1,6 @@
+(ns integration
+ (:gen-class))
+
+
+(defn -main
+ [& _args])
diff --git a/tests/lib.sh b/tests/lib.sh
new file mode 100644
index 0000000..649c61d
--- /dev/null
+++ b/tests/lib.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+
+export TMPDIR="${TMPDIR:-$XDG_RUNTIME_DIR}"
+
+end="\033[0m"
+red="\033[0;31m"
+green="\033[0;32m"
+yellow="\033[0;33m"
+
+N=
+OUT=
+ERR=
+STATUS=
+
+ERROR() {
+ # shellcheck disable=2059
+ printf "${red}ERROR${end}"
+}
+
+print_debug_info() {
+ # shellcheck disable=2016
+ printf 'LINENO: %s\n$OUT: %s\n$ERR: %s\n' \
+ "$N" "$OUT" "$ERR" >&2
+}
+
+assert_status() {
+ if [ "$STATUS" != "$1" ]; then
+ printf '\n%s: Bad status.\n\nexpected: %s\ngot: %s\n' \
+ "$(ERROR)" "$1" "$STATUS" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_usage() {
+ if ! grep -Fq 'Usage' "$1"; then
+ echo 'Expected to find "Usage" text, it was missing:' >&2
+ cat "$1" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_empty_stream() {
+ if [ -s "$2" ]; then
+ FMT='\n%s: Expected %s (%s) to be empty, but has content:\n%s\n'
+ # shellcheck disable=2059
+ printf "$FMT" \
+ "$(ERROR)" "$1" "$2" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_empty_stdout() {
+ assert_empty_stream STDOUT "$OUT"
+}
+
+assert_empty_stderr() {
+ assert_empty_stream STDERR "$ERR"
+}
+
+assert_stream() {
+ if [ "$(cat "$2")" != "$3" ]; then
+ printf '\n%s: Bad %s (%s)\n\nexpected: %s\ngot: %s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_stdout() {
+ assert_stream STDOUT "$OUT" "$1"
+}
+
+assert_stderr() {
+ assert_stream STDERR "$ERR" "$1"
+}
+
+assert_grep_stream() {
+ if ! grep -qE "$3" "$2"; then
+ printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_grep_stdout() {
+ assert_grep_stream STDOUT "$OUT" "$1"
+}
+
+assert_grep_stderr() {
+ assert_grep_stream STDERR "$ERR" "$1"
+}
+
+assert_fgrep_stream() {
+ if ! grep -Fq -- "$3" "$2"; then
+ printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_fgrep_stdout() {
+ assert_fgrep_stream STDOUT "$OUT" "$1"
+}
+
+assert_fgrep_stderr() {
+ assert_fgrep_stream STDERR "$ERR" "$1"
+}
+
+testing() {
+ printf "${yellow}testing${end}: %s..." "$1" >&2
+}
+
+test_ok() {
+ # shellcheck disable=2059
+ printf " ${green}OK${end}.\n" >&2
+}
diff --git a/tests/unit.clj b/tests/unit.clj
new file mode 100644
index 0000000..e92b389
--- /dev/null
+++ b/tests/unit.clj
@@ -0,0 +1,54 @@
+(ns unit
+ (:require [clojure.test :as t :refer [are deftest is testing]]
+ [papod])
+ (:gen-class))
+
+
+
+(def parse @#'papod/get-raw-message)
+(deftest test_get-raw-message
+ (testing ":needs-more states"
+ (is (= (parse "")
+ {:input ""
+ :status :needs-more}))
+ (is (= (parse "some text")
+ {:input "some text"
+ :status :needs-more}))
+ (is (= (parse "newline\nonly")
+ {:input "newline\nonly"
+ :status :needs-more}))
+ (is (= (parse "CR\rLF\n\r")
+ {:input "CR\rLF\n\r"
+ :status :needs-more})))
+ (testing "empty messages"
+ (is (= (parse " \r\nblah\r\n")
+ {:status :accepting
+ :input "blah\r\n"
+ :raw-message false})))
+ (testing "input splitting"
+ (is (= (parse "BEFORE\r\nAFTER\r\nTAIL")
+ {:status :accepting
+ :input "AFTER\r\nTAIL"
+ :raw-message "BEFORE"}))))
+
+(def parse-message @#'papod/parse-message)
+(deftest test_parse-message
+ (testing "only commands"
+ (is (= (parse-message "CMD")
+ [{:prefix nil
+ :command "CMD"
+ :params []}
+ nil]))))
+
+; (parse-message " CMD")
+
+
+(defn -main
+ [& _args]
+ (binding [*out* *err*]
+ (let [{:keys [fail error] :as res} (t/run-tests 'unit)
+ status (if (zero? (+ fail error))
+ 0
+ 1)]
+ (prn res)
+ (System/exit status))))