Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options, it was decided to implement an XDR-like protocol code generator in portable AWK. It now has two backends, per each of: - xF, the X11 frontend, is in C, and is meant to be the primary user interface in the future. - xP, the web frontend, relies on a protocol proxy written in Go, and is meant for use on-the-go (no pun intended). They are very much work-in-progress proofs of concept right now, and the relay protocol is certain to change.
This commit is contained in:
parent
2160d03794
commit
1639235a48
@ -1,10 +1,12 @@
|
||||
# Ubuntu 18.04 LTS and OpenBSD 6.4
|
||||
cmake_minimum_required (VERSION 3.10)
|
||||
project (xK VERSION 1.5.0 DESCRIPTION "IRC client, daemon and bot" LANGUAGES C)
|
||||
project (xK VERSION 1.5.0
|
||||
DESCRIPTION "IRC daemon, bot, TUI client and X11/web frontends" LANGUAGES C)
|
||||
|
||||
# Options
|
||||
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
|
||||
option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
|
||||
option (WANT_XF "Build xF" OFF)
|
||||
|
||||
# Moar warnings
|
||||
set (CMAKE_C_STANDARD 99)
|
||||
@ -143,7 +145,8 @@ set (HAVE_EDITLINE "${WANT_LIBEDIT}")
|
||||
set (HAVE_LUA "${WITH_LUA}")
|
||||
|
||||
include (GNUInstallDirs)
|
||||
configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)
|
||||
set (project_config ${PROJECT_BINARY_DIR}/config.h)
|
||||
configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${project_config})
|
||||
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
|
||||
|
||||
# Generate IRC replies--we need a custom target because of the multiple outputs
|
||||
@ -157,17 +160,40 @@ add_custom_command (OUTPUT xD-replies.c xD.msg
|
||||
COMMENT "Generating files from the list of server numerics")
|
||||
add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c)
|
||||
|
||||
add_custom_command (OUTPUT xC-proto.c
|
||||
COMMAND env LC_ALL=C awk
|
||||
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
|
||||
-f ${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
|
||||
${PROJECT_SOURCE_DIR}/xC-proto > xC-proto.c
|
||||
DEPENDS
|
||||
${PROJECT_SOURCE_DIR}/xC-gen-proto.awk
|
||||
${PROJECT_SOURCE_DIR}/xC-gen-proto-c.awk
|
||||
${PROJECT_SOURCE_DIR}/xC-proto
|
||||
COMMENT "Generating xC relay protocol code")
|
||||
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
|
||||
|
||||
# Build
|
||||
foreach (name xB xC xD)
|
||||
add_executable (${name} ${name}.c ${PROJECT_BINARY_DIR}/config.h)
|
||||
add_executable (${name} ${name}.c ${project_config})
|
||||
target_link_libraries (${name} ${project_libraries})
|
||||
add_threads (${name})
|
||||
endforeach ()
|
||||
|
||||
add_dependencies (xD replies)
|
||||
add_dependencies (xC replies)
|
||||
add_dependencies (xC replies xC-proto)
|
||||
target_link_libraries (xC ${xC_libraries})
|
||||
|
||||
if (WANT_XF)
|
||||
pkg_check_modules (x11 REQUIRED x11 xrender xft fontconfig)
|
||||
include_directories (${x11_INCLUDE_DIRS})
|
||||
link_directories (${x11_LIBRARY_DIRS})
|
||||
|
||||
add_executable (xF xF.c ${project_config})
|
||||
add_dependencies (xF xC-proto)
|
||||
target_link_libraries (xF ${x11_LIBRARIES} ${project_libraries})
|
||||
add_threads (xF)
|
||||
endif ()
|
||||
|
||||
# Tests
|
||||
include (CTest)
|
||||
if (BUILD_TESTING)
|
||||
|
12
NEWS
12
NEWS
@ -1,5 +1,9 @@
|
||||
Unreleased
|
||||
|
||||
* xD: implemented WALLOPS, choosing to make it target even non-operators
|
||||
|
||||
* xC: made it show WALLOPS messages, as PRIVMSG for the server buffer
|
||||
|
||||
* xC: all behaviour.* configuration options have been renamed to general.*,
|
||||
with the exception of editor_command/editor, backlog_helper/pager,
|
||||
and backlog_helper_strip_formatting/pager_strip_formatting
|
||||
@ -10,11 +14,13 @@ Unreleased
|
||||
|
||||
* xC: normalized editline's history behaviour, making it a viable frontend
|
||||
|
||||
* xC: made it show WALLOPS messages, as PRIVMSG for the server buffer
|
||||
|
||||
* xC: various bugfixes
|
||||
|
||||
* xD: implemented WALLOPS, choosing to make it target even non-operators
|
||||
* xC: added a relay interface, enabled through the general.relay_bind option
|
||||
|
||||
* Added an experimental X11 frontend for xC called xF
|
||||
|
||||
* Added an experimental web frontend for xC called xP
|
||||
|
||||
|
||||
1.5.0 (2021-12-21) "The Show Must Go On"
|
||||
|
36
README.adoc
36
README.adoc
@ -1,9 +1,9 @@
|
||||
xK
|
||||
==
|
||||
|
||||
'xK' (chat kit) is an IRC software suite consisting of a terminal client,
|
||||
daemon, and bot. It's all you're ever going to need for chatting,
|
||||
so long as you can make do with slightly minimalist software.
|
||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal
|
||||
client, and X11/web frontends for the client. It's all you're ever going to
|
||||
need for chatting, so long as you can make do with slightly minimalist software.
|
||||
|
||||
They're all lean on dependencies, and offer a maximally permissive licence.
|
||||
|
||||
@ -20,8 +20,18 @@ a powerful configuration system, integrated help, text formatting, automatic
|
||||
message splitting, multiline editing, bracketed paste support, word wrapping
|
||||
that doesn't break links, autocomplete, logging, CTCP queries, auto-away,
|
||||
command aliases, SOCKS proxying, SASL EXTERNAL authentication using TLS client
|
||||
certificates, or basic support for Lua scripting. As a unique bonus, you can
|
||||
launch a full text editor from within.
|
||||
certificates, a remote relay interface, or basic support for Lua scripting.
|
||||
As a unique bonus, you can launch a full text editor from within.
|
||||
|
||||
xF
|
||||
--
|
||||
The X11 frontend for 'xC', making use of its networked relay interface.
|
||||
It's currently in development, and is hidden behind a CMake option.
|
||||
|
||||
xP
|
||||
--
|
||||
The web frontend for 'xC', making use of its networked relay interface.
|
||||
It's currently rather elementary, and can be built from within its directory.
|
||||
|
||||
xD
|
||||
--
|
||||
@ -38,9 +48,8 @@ What it notably doesn't support is online changes to configuration, any limits
|
||||
besides the total number of connections and mode `+l`, or server linking
|
||||
(which also means no services).
|
||||
|
||||
This program has been
|
||||
https://git.janouch.name/p/haven/src/branch/master/hid[ported to Go],
|
||||
and development continues over there.
|
||||
This program has been https://git.janouch.name/p/haven/src/branch/master/hid[
|
||||
ported to Go] in a different project, and development continues over there.
|
||||
|
||||
xB
|
||||
--
|
||||
@ -60,11 +69,12 @@ a package with the latest development version from Archlinux's AUR.
|
||||
|
||||
Building
|
||||
--------
|
||||
Build dependencies: CMake, pkg-config, asciidoctor or asciidoc, awk,
|
||||
liberty (included) +
|
||||
Runtime dependencies: openssl +
|
||||
Additionally for 'xC': curses, libffi, lua >= 5.3 (optional),
|
||||
readline >= 6.0 or libedit >= 2013-07-12
|
||||
Build-only dependencies:
|
||||
CMake, pkg-config, asciidoctor or asciidoc, awk, liberty (included) +
|
||||
Common runtime dependencies: openssl +
|
||||
Additionally for 'xC': curses, libffi, +
|
||||
readline >= 6.0 or libedit >= 2013-07-12, lua >= 5.3 (optional) +
|
||||
Additionally for 'xF': x11, xft
|
||||
|
||||
$ git clone --recursive https://git.janouch.name/p/xK.git
|
||||
$ mkdir xK/build
|
||||
|
71
common.c
71
common.c
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* common.c: common functionality
|
||||
*
|
||||
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
* Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted.
|
||||
@ -48,6 +48,66 @@ init_openssl (void)
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *
|
||||
gai_reconstruct_address (struct addrinfo *ai)
|
||||
{
|
||||
char host[NI_MAXHOST] = {}, port[NI_MAXSERV] = {};
|
||||
int err = getnameinfo (ai->ai_addr, ai->ai_addrlen,
|
||||
host, sizeof host, port, sizeof port,
|
||||
NI_NUMERICHOST | NI_NUMERICSERV);
|
||||
if (err)
|
||||
{
|
||||
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
|
||||
return xstrdup ("?");
|
||||
}
|
||||
return format_host_port_pair (host, port);
|
||||
}
|
||||
|
||||
static bool
|
||||
accept_error_is_transient (int err)
|
||||
{
|
||||
// OS kernels may return a wide range of unforeseeable errors.
|
||||
// Assuming that they're either transient or caused by
|
||||
// a connection that we've just extracted from the queue.
|
||||
switch (err)
|
||||
{
|
||||
case EBADF:
|
||||
case EINVAL:
|
||||
case ENOTSOCK:
|
||||
case EOPNOTSUPP:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Destructively tokenize an address into a host part, and a port part.
|
||||
/// The port is only overwritten if that part is found, allowing for defaults.
|
||||
static const char *
|
||||
tokenize_host_port (char *address, const char **port)
|
||||
{
|
||||
// Unwrap IPv6 addresses in format_host_port_pair() format.
|
||||
char *rbracket = strchr (address, ']');
|
||||
if (*address == '[' && rbracket)
|
||||
{
|
||||
if (rbracket[1] == ':')
|
||||
{
|
||||
*port = rbracket + 2;
|
||||
return *rbracket = 0, address + 1;
|
||||
}
|
||||
if (!rbracket[1])
|
||||
return *rbracket = 0, address + 1;
|
||||
}
|
||||
|
||||
char *colon = strchr (address, ':');
|
||||
if (colon)
|
||||
{
|
||||
*port = colon + 1;
|
||||
return *colon = 0, address;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
// --- To be moved to liberty --------------------------------------------------
|
||||
|
||||
// FIXME: in xssl_get_error() we rely on error reasons never being NULL (i.e.,
|
||||
@ -74,6 +134,15 @@ xerr_describe_error (void)
|
||||
return reason;
|
||||
}
|
||||
|
||||
static struct str
|
||||
str_from_cstr (const char *cstr)
|
||||
{
|
||||
struct str self;
|
||||
self.alloc = (self.len = strlen (cstr)) + 1;
|
||||
self.str = memcpy (xmalloc (self.alloc), cstr, self.alloc);
|
||||
return self;
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
strv_find (const struct strv *v, const char *s)
|
||||
{
|
||||
|
325
xC-gen-proto-c.awk
Normal file
325
xC-gen-proto-c.awk
Normal file
@ -0,0 +1,325 @@
|
||||
# xC-gen-proto-c.awk: C backend for xC-gen-proto.awk.
|
||||
#
|
||||
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
#
|
||||
# Neither *_new() nor *_destroy() functions are provided, because they'd only
|
||||
# be useful for top-levels, and are merely extra malloc()/free() calls.
|
||||
# Users are expected to reuse buffers.
|
||||
#
|
||||
# Similarly, no constructors are produced--those are easy to write manually.
|
||||
#
|
||||
# All arrays are deserialized zero-terminated, so u8<> and i8<> can be directly
|
||||
# used as C strings.
|
||||
#
|
||||
# All types must be able to dispose partially zero values going from the back,
|
||||
# i.e., in the reverse order of deserialization.
|
||||
|
||||
function define_internal(name, ctype) {
|
||||
Types[name] = "internal"
|
||||
CodegenCType[name] = ctype
|
||||
}
|
||||
|
||||
function define_int(shortname, ctype) {
|
||||
define_internal(shortname, ctype)
|
||||
CodegenSerialize[shortname] = \
|
||||
"\tstr_pack_" shortname "(w, %s);\n"
|
||||
CodegenDeserialize[shortname] = \
|
||||
"\tif (!msg_unpacker_" shortname "(r, &%s))\n" \
|
||||
"\t\treturn false;\n"
|
||||
}
|
||||
|
||||
function define_sint(size) { define_int("i" size, "int" size "_t") }
|
||||
function define_uint(size) { define_int("u" size, "uint" size "_t") }
|
||||
|
||||
function codegen_begin() {
|
||||
define_sint("8")
|
||||
define_sint("16")
|
||||
define_sint("32")
|
||||
define_sint("64")
|
||||
define_uint("8")
|
||||
define_uint("16")
|
||||
define_uint("32")
|
||||
define_uint("64")
|
||||
|
||||
define_internal("string", "struct str")
|
||||
CodegenDispose["string"] = "\tstr_free(&%s);\n"
|
||||
CodegenSerialize["string"] = \
|
||||
"\tif (!proto_string_serialize(&%s, w))\n" \
|
||||
"\t\treturn false;\n"
|
||||
CodegenDeserialize["string"] = \
|
||||
"\tif (!proto_string_deserialize(&%s, r))\n" \
|
||||
"\t\treturn false;\n"
|
||||
|
||||
define_internal("bool", "bool")
|
||||
CodegenSerialize["bool"] = \
|
||||
"\tstr_pack_u8(w, !!%s);\n"
|
||||
CodegenDeserialize["bool"] = \
|
||||
"\t{\n" \
|
||||
"\t\tuint8_t v = 0;\n" \
|
||||
"\t\tif (!msg_unpacker_u8(r, &v))\n" \
|
||||
"\t\t\treturn false;\n" \
|
||||
"\t\t%s = !!v;\n" \
|
||||
"\t}\n"
|
||||
|
||||
print "// This file directly depends on liberty.c, but doesn't include it."
|
||||
|
||||
print ""
|
||||
print "static bool"
|
||||
print "proto_string_serialize(const struct str *s, struct str *w) {"
|
||||
print "\tif (s->len > UINT32_MAX)"
|
||||
print "\t\treturn false;"
|
||||
print "\tstr_pack_u32(w, s->len);"
|
||||
print "\tstr_append_str(w, s);"
|
||||
print "\treturn true;"
|
||||
print "}"
|
||||
|
||||
print ""
|
||||
print "static bool"
|
||||
print "proto_string_deserialize(struct str *s, struct msg_unpacker *r) {"
|
||||
print "\tuint32_t len = 0;"
|
||||
print "\tif (!msg_unpacker_u32(r, &len))"
|
||||
print "\t\treturn false;"
|
||||
print "\tif (msg_unpacker_get_available(r) < len)"
|
||||
print "\t\treturn false;"
|
||||
print "\t*s = str_make();"
|
||||
print "\tstr_append_data(s, r->data + r->offset, len);"
|
||||
print "\tr->offset += len;"
|
||||
print "\tif (!utf8_validate (s->str, s->len))"
|
||||
print "\t\treturn false;"
|
||||
print "\treturn true;"
|
||||
print "}"
|
||||
}
|
||||
|
||||
function codegen_constant(name, value) {
|
||||
print ""
|
||||
print "enum { " PrefixUpper name " = " value " };"
|
||||
}
|
||||
|
||||
function codegen_enum_value(name, subname, value, cg) {
|
||||
append(cg, "fields",
|
||||
"\t" PrefixUpper toupper(cameltosnake(name)) "_" subname \
|
||||
" = " value ",\n")
|
||||
}
|
||||
|
||||
function codegen_enum(name, cg, ctype) {
|
||||
ctype = "enum " PrefixLower cameltosnake(name)
|
||||
print ""
|
||||
print ctype " {"
|
||||
print cg["fields"] "};"
|
||||
|
||||
# XXX: This should also check if it isn't out-of-range for any reason,
|
||||
# but our usage of sprintf() stands in the way a bit.
|
||||
CodegenSerialize[name] = "\tstr_pack_i32(w, %s);\n"
|
||||
CodegenDeserialize[name] = \
|
||||
"\t{\n" \
|
||||
"\t\tint32_t v = 0;\n" \
|
||||
"\t\tif (!msg_unpacker_i32(r, &v) || !v)\n" \
|
||||
"\t\t\treturn false;\n" \
|
||||
"\t\t%s = v;\n" \
|
||||
"\t}\n"
|
||||
|
||||
CodegenCType[name] = ctype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
||||
|
||||
function codegen_struct_tag(d, cg, f) {
|
||||
f = "self->" d["name"]
|
||||
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
|
||||
append(cg, "dispose", sprintf(CodegenDispose[d["type"]], f))
|
||||
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
|
||||
# Do not deserialize here, that would be out of order.
|
||||
}
|
||||
|
||||
function codegen_struct_field(d, cg, f, dispose, serialize, deserialize) {
|
||||
f = "self->" d["name"]
|
||||
dispose = CodegenDispose[d["type"]]
|
||||
serialize = CodegenSerialize[d["type"]]
|
||||
deserialize = CodegenDeserialize[d["type"]]
|
||||
if (!d["isarray"]) {
|
||||
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
|
||||
append(cg, "dispose", sprintf(dispose, f))
|
||||
append(cg, "serialize", sprintf(serialize, f))
|
||||
append(cg, "deserialize", sprintf(deserialize, f))
|
||||
return
|
||||
}
|
||||
|
||||
append(cg, "fields",
|
||||
"\t" CodegenCType["u32"] " " d["name"] "_len;\n" \
|
||||
"\t" CodegenCType[d["type"]] " *" d["name"] ";\n")
|
||||
|
||||
if (dispose)
|
||||
append(cg, "dispose", "\tif (" f ")\n" \
|
||||
"\t\tfor (size_t i = 0; i < " f "_len; i++)\n" \
|
||||
indent(indent(sprintf(dispose, f "[i]"))))
|
||||
append(cg, "dispose", "\tfree(" f ");\n")
|
||||
|
||||
append(cg, "serialize", sprintf(CodegenSerialize["u32"], f "_len"))
|
||||
if (d["type"] == "u8" || d["type"] == "i8") {
|
||||
append(cg, "serialize",
|
||||
"\tstr_append_data(w, " f ", " f "_len);\n")
|
||||
} else if (serialize) {
|
||||
append(cg, "serialize",
|
||||
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
|
||||
indent(sprintf(serialize, f "[i]")))
|
||||
}
|
||||
|
||||
append(cg, "deserialize", sprintf(CodegenDeserialize["u32"], f "_len") \
|
||||
"\tif (!(" f " = calloc(" f "_len + 1, sizeof *" f ")))\n" \
|
||||
"\t\treturn false;\n")
|
||||
if (d["type"] == "u8" || d["type"] == "i8") {
|
||||
append(cg, "deserialize",
|
||||
"\tif (msg_unpacker_get_available(r) < " f "_len)\n" \
|
||||
"\t\treturn false;\n" \
|
||||
"\tmemcpy(" f ", r->data + r->offset, " f "_len);\n" \
|
||||
"\tr->offset += " f "_len;\n")
|
||||
} else if (deserialize) {
|
||||
append(cg, "deserialize",
|
||||
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
|
||||
indent(sprintf(deserialize, f "[i]")))
|
||||
}
|
||||
}
|
||||
|
||||
function codegen_struct(name, cg, ctype, funcname) {
|
||||
ctype = "struct " PrefixLower cameltosnake(name)
|
||||
print ""
|
||||
print ctype " {"
|
||||
print cg["fields"] "};"
|
||||
|
||||
if (cg["dispose"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_free"
|
||||
print ""
|
||||
print "static void\n" funcname "(" ctype " *self) {"
|
||||
print cg["dispose"] "}"
|
||||
|
||||
CodegenDispose[name] = "\t" funcname "(&%s);\n"
|
||||
}
|
||||
if (cg["serialize"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_serialize"
|
||||
print ""
|
||||
print "static bool\n" \
|
||||
funcname "(\n\t\t" ctype " *self, struct str *w) {"
|
||||
print cg["serialize"] "\treturn true;"
|
||||
print "}"
|
||||
|
||||
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
|
||||
"\t\treturn false;\n"
|
||||
}
|
||||
if (cg["deserialize"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_deserialize"
|
||||
print ""
|
||||
print "static bool\n" \
|
||||
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
|
||||
print cg["deserialize"] "\treturn true;"
|
||||
print "}"
|
||||
|
||||
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
|
||||
"\t\treturn false;\n"
|
||||
}
|
||||
|
||||
CodegenCType[name] = ctype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
||||
|
||||
function codegen_union_tag(d, cg) {
|
||||
cg["tagtype"] = d["type"]
|
||||
cg["tagname"] = d["name"]
|
||||
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
|
||||
}
|
||||
|
||||
function codegen_union_struct( \
|
||||
name, casename, cg, scg, structname, fieldname, fullcasename) {
|
||||
# Don't generate obviously useless structs.
|
||||
fullcasename = toupper(cameltosnake(cg["tagtype"])) "_" casename
|
||||
if (!scg["dispose"] && !scg["deserialize"]) {
|
||||
append(cg, "structless", "\tcase " PrefixUpper fullcasename ":\n")
|
||||
for (i in scg)
|
||||
delete scg[i]
|
||||
return
|
||||
}
|
||||
|
||||
# And thus not all generated structs are present in Types.
|
||||
structname = name "_" casename
|
||||
fieldname = tolower(casename)
|
||||
codegen_struct(structname, scg)
|
||||
|
||||
append(cg, "fields", "\t" CodegenCType[structname] " " fieldname ";\n")
|
||||
if (CodegenDispose[structname])
|
||||
append(cg, "dispose", "\tcase " PrefixUpper fullcasename ":\n" \
|
||||
indent(sprintf(CodegenDispose[structname], "self->" fieldname)) \
|
||||
"\t\tbreak;\n")
|
||||
|
||||
# With no de/serialization code, this will simply recognize the tag.
|
||||
append(cg, "serialize", "\tcase " PrefixUpper fullcasename ":\n" \
|
||||
indent(sprintf(CodegenSerialize[structname], "self->" fieldname)) \
|
||||
"\t\tbreak;\n")
|
||||
append(cg, "deserialize", "\tcase " PrefixUpper fullcasename ":\n" \
|
||||
indent(sprintf(CodegenDeserialize[structname], "self->" fieldname)) \
|
||||
"\t\tbreak;\n")
|
||||
}
|
||||
|
||||
function codegen_union(name, cg, f, ctype, funcname) {
|
||||
ctype = "union " PrefixLower cameltosnake(name)
|
||||
print ""
|
||||
print ctype " {"
|
||||
print cg["fields"] "};"
|
||||
|
||||
f = "self->" cg["tagname"]
|
||||
if (cg["dispose"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_free"
|
||||
print ""
|
||||
print "static void\n" funcname "(" ctype " *self) {"
|
||||
print "\tswitch (" f ") {"
|
||||
if (cg["structless"])
|
||||
print cg["structless"] \
|
||||
indent(sprintf(CodegenDispose[cg["tagtype"]], f)) "\t\tbreak;"
|
||||
print cg["dispose"] "\tdefault:"
|
||||
print "\t\tbreak;"
|
||||
print "\t}"
|
||||
print "}"
|
||||
|
||||
CodegenDispose[name] = "\t" funcname "(&%s);\n"
|
||||
}
|
||||
if (cg["serialize"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_serialize"
|
||||
print ""
|
||||
print "static bool\n" \
|
||||
funcname "(\n\t\t" ctype " *self, struct str *w) {"
|
||||
print "\tswitch (" f ") {"
|
||||
if (cg["structless"])
|
||||
print cg["structless"] \
|
||||
indent(sprintf(CodegenSerialize[cg["tagtype"]], f)) "\t\tbreak;"
|
||||
print cg["serialize"] "\tdefault:"
|
||||
print "\t\treturn false;"
|
||||
print "\t}"
|
||||
print "\treturn true;"
|
||||
print "}"
|
||||
|
||||
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
|
||||
"\t\treturn false;\n"
|
||||
}
|
||||
if (cg["deserialize"]) {
|
||||
funcname = PrefixLower cameltosnake(name) "_deserialize"
|
||||
print ""
|
||||
print "static bool\n" \
|
||||
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
|
||||
print sprintf(CodegenDeserialize[cg["tagtype"]], f)
|
||||
print "\tswitch (" f ") {"
|
||||
if (cg["structless"])
|
||||
print cg["structless"] "\t\tbreak;"
|
||||
print cg["deserialize"] "\tdefault:"
|
||||
print "\t\treturn false;"
|
||||
print "\t}"
|
||||
print "\treturn true;"
|
||||
print "}"
|
||||
|
||||
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
|
||||
"\t\treturn false;\n"
|
||||
}
|
||||
|
||||
CodegenCType[name] = ctype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
447
xC-gen-proto-go.awk
Normal file
447
xC-gen-proto-go.awk
Normal file
@ -0,0 +1,447 @@
|
||||
# xC-gen-proto-go.awk: Go backend for xC-gen-proto.awk.
|
||||
#
|
||||
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
#
|
||||
# This backend also enables proxying to other endpoints using JSON.
|
||||
|
||||
function define_internal(name, gotype) {
|
||||
Types[name] = "internal"
|
||||
CodegenGoType[name] = gotype
|
||||
}
|
||||
|
||||
function define_sint(size, shortname, gotype) {
|
||||
shortname = "i" size
|
||||
gotype = "int" size
|
||||
define_internal(shortname, gotype)
|
||||
|
||||
if (size == 8) {
|
||||
CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n"
|
||||
CodegenDeserialize[shortname] = \
|
||||
"\tif len(data) >= 1 {\n" \
|
||||
"\t\t%s, data = int8(data[0]), data[1:]\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\treturn nil, false\n" \
|
||||
"\t}\n"
|
||||
return
|
||||
}
|
||||
|
||||
CodegenSerialize[shortname] = \
|
||||
"\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n"
|
||||
CodegenDeserialize[shortname] = \
|
||||
"\tif len(data) >= " (size / 8) " {\n" \
|
||||
"\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \
|
||||
"\t\tdata = data[" (size / 8) ":]\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\treturn nil, false\n" \
|
||||
"\t}\n"
|
||||
}
|
||||
|
||||
function define_uint(size, shortname, gotype) {
|
||||
shortname = "u" size
|
||||
gotype = "uint" size
|
||||
define_internal(shortname, gotype)
|
||||
|
||||
# Both byte and uint8 luckily marshal as base64-encoded JSON strings.
|
||||
if (size == 8) {
|
||||
CodegenSerialize[shortname] = "\tdata = append(data, %s)\n"
|
||||
CodegenDeserialize[shortname] = \
|
||||
"\tif len(data) >= 1 {\n" \
|
||||
"\t\t%s, data = data[0], data[1:]\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\treturn nil, false\n" \
|
||||
"\t}\n"
|
||||
return
|
||||
}
|
||||
|
||||
CodegenSerialize[shortname] = \
|
||||
"\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n"
|
||||
CodegenDeserialize[shortname] = \
|
||||
"\tif len(data) >= " (size / 8) " {\n" \
|
||||
"\t\t%s = binary.BigEndian.Uint" size "(data)\n" \
|
||||
"\t\tdata = data[" (size / 8) ":]\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\treturn nil, false\n" \
|
||||
"\t}\n"
|
||||
}
|
||||
|
||||
function codegen_begin() {
|
||||
define_sint("8")
|
||||
define_sint("16")
|
||||
define_sint("32")
|
||||
define_sint("64")
|
||||
define_uint("8")
|
||||
define_uint("16")
|
||||
define_uint("32")
|
||||
define_uint("64")
|
||||
|
||||
define_internal("bool", "bool")
|
||||
CodegenSerialize["bool"] = \
|
||||
"\tif %s {\n" \
|
||||
"\t\tdata = append(data, 1)\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\tdata = append(data, 0)\n" \
|
||||
"\t}\n"
|
||||
CodegenDeserialize["bool"] = \
|
||||
"\tif data, ok = protoConsumeBoolFrom(data, &%s); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
|
||||
define_internal("string", "string")
|
||||
CodegenSerialize["string"] = \
|
||||
"\tif data, ok = protoAppendStringTo(data, %s); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
CodegenDeserialize["string"] = \
|
||||
"\tif data, ok = protoConsumeStringFrom(data, &%s); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
|
||||
print "package main"
|
||||
print ""
|
||||
print "import ("
|
||||
print "\t`encoding/binary`"
|
||||
print "\t`encoding/json`"
|
||||
print "\t`errors`"
|
||||
print "\t`math`"
|
||||
print "\t`strconv`"
|
||||
print "\t`unicode/utf8`"
|
||||
print ")"
|
||||
print ""
|
||||
|
||||
print "// protoConsumeBoolFrom tries to deserialize a boolean value"
|
||||
print "// from the beginning of a byte stream. When successful,"
|
||||
print "// it returns a subslice with any data that might follow."
|
||||
print "func protoConsumeBoolFrom(data []byte, b *bool) ([]byte, bool) {"
|
||||
print "\tif len(data) < 1 {"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\tif data[0] != 0 {"
|
||||
print "\t\t*b = true"
|
||||
print "\t} else {"
|
||||
print "\t\t*b = false"
|
||||
print "\t}"
|
||||
print "\treturn data[1:], true"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "// protoAppendStringTo tries to serialize a string value,"
|
||||
print "// appending it to the end of a byte stream."
|
||||
print "func protoAppendStringTo(data []byte, s string) ([]byte, bool) {"
|
||||
print "\tif len(s) > math.MaxUint32 {"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
|
||||
print "\treturn append(data, s...), true"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "// protoConsumeStringFrom tries to deserialize a string value"
|
||||
print "// from the beginning of a byte stream. When successful,"
|
||||
print "// it returns a subslice with any data that might follow."
|
||||
print "func protoConsumeStringFrom(data []byte, s *string) ([]byte, bool) {"
|
||||
print "\tif len(data) < 4 {"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\tlength := binary.BigEndian.Uint32(data)"
|
||||
print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\t*s = string(data[:length])"
|
||||
print "\tif !utf8.ValidString(*s) {"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\treturn data[length:], true"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "// protoUnmarshalEnumJSON converts a JSON fragment to an integer,"
|
||||
print "// ensuring that it's within the expected range of enum values."
|
||||
print "func protoUnmarshalEnumJSON(data []byte) (int64, error) {"
|
||||
print "\tvar n int64"
|
||||
print "\tif err := json.Unmarshal(data, &n); err != nil {"
|
||||
print "\t\treturn 0, err"
|
||||
print "\t} else if n > math.MaxInt32 || n < math.MinInt32 {"
|
||||
print "\t\treturn 0, errors.New(`integer out of range`)"
|
||||
print "\t} else {"
|
||||
print "\t\treturn n, nil"
|
||||
print "\t}"
|
||||
print "}"
|
||||
print ""
|
||||
}
|
||||
|
||||
function codegen_constant(name, value) {
|
||||
print "const " PrefixCamel snaketocamel(name) " = " value
|
||||
print ""
|
||||
}
|
||||
|
||||
function codegen_enum_value(name, subname, value, cg, goname) {
|
||||
goname = PrefixCamel name snaketocamel(subname)
|
||||
append(cg, "fields",
|
||||
"\t" goname " = " value "\n")
|
||||
append(cg, "stringer",
|
||||
"\tcase " goname ":\n" \
|
||||
"\t\treturn `" snaketocamel(subname) "`\n")
|
||||
append(cg, "marshal",
|
||||
goname ",\n")
|
||||
append(cg, "unmarshal",
|
||||
"\tcase `" snaketocamel(subname) "`:\n" \
|
||||
"\t\t*v = " goname "\n")
|
||||
}
|
||||
|
||||
function codegen_enum(name, cg, gotype, fields) {
|
||||
gotype = PrefixCamel name
|
||||
print "type " gotype " int"
|
||||
print ""
|
||||
|
||||
print "const ("
|
||||
print cg["fields"] ")"
|
||||
print ""
|
||||
|
||||
print "func (v " gotype ") String() string {"
|
||||
print "\tswitch v {"
|
||||
print cg["stringer"] "\tdefault:"
|
||||
print "\t\treturn strconv.Itoa(int(v))"
|
||||
print "\t}"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
fields = cg["marshal"]
|
||||
sub(/,\n$/, ":", fields)
|
||||
gsub(/\n/, "\n\t", fields)
|
||||
print "func (v " gotype ") MarshalJSON() ([]byte, error) {"
|
||||
print "\tswitch v {"
|
||||
print indent("case " fields)
|
||||
print "\t\treturn json.Marshal(v.String())"
|
||||
print "\t}"
|
||||
print "\treturn json.Marshal(int(v))"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "func (v *" gotype ") UnmarshalJSON(data []byte) error {"
|
||||
print "\tvar s string"
|
||||
print "\tif json.Unmarshal(data, &s) == nil {"
|
||||
print "\t\t// Handled below."
|
||||
print "\t} else if n, err := protoUnmarshalEnumJSON(data); err != nil {"
|
||||
print "\t\treturn err"
|
||||
print "\t} else {"
|
||||
print "\t\t*v = " gotype "(n)"
|
||||
print "\t\treturn nil"
|
||||
print "\t}"
|
||||
print ""
|
||||
print "\tswitch s {"
|
||||
print cg["unmarshal"] "\tdefault:"
|
||||
print "\t\treturn errors.New(`unrecognized value: ` + s)"
|
||||
print "\t}"
|
||||
print "\treturn nil"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
# XXX: This should also check if it isn't out-of-range for any reason,
|
||||
# but our usage of sprintf() stands in the way a bit.
|
||||
CodegenSerialize[name] = \
|
||||
"\tdata = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
|
||||
CodegenDeserialize[name] = \
|
||||
"\tif len(data) >= 4 {\n" \
|
||||
"\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \
|
||||
"\t\tdata = data[4:]\n" \
|
||||
"\t} else {\n" \
|
||||
"\t\treturn nil, false\n" \
|
||||
"\t}\n"
|
||||
|
||||
CodegenGoType[name] = gotype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
||||
|
||||
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
|
||||
camel = snaketocamel(d["name"])
|
||||
f = "s." camel
|
||||
serialize = CodegenSerialize[d["type"]]
|
||||
deserialize = CodegenDeserialize[d["type"]]
|
||||
if (!d["isarray"]) {
|
||||
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
|
||||
" `json:\"" decapitalize(camel) "\"`\n")
|
||||
append(cg, "serialize", sprintf(serialize, f))
|
||||
append(cg, "deserialize", sprintf(deserialize, f))
|
||||
return
|
||||
}
|
||||
|
||||
append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \
|
||||
" `json:\"" decapitalize(camel) "\"`\n")
|
||||
|
||||
# XXX: This should also check if it isn't out-of-range for any reason.
|
||||
append(cg, "serialize",
|
||||
sprintf(CodegenSerialize["u32"], "uint32(len(" f "))"))
|
||||
if (d["type"] == "u8") {
|
||||
append(cg, "serialize",
|
||||
"\tdata = append(data, " f "...)\n")
|
||||
} else {
|
||||
append(cg, "serialize",
|
||||
"\tfor i := 0; i < len(" f "); i++ {\n" \
|
||||
indent(sprintf(serialize, f "[i]")) \
|
||||
"\t}\n")
|
||||
}
|
||||
|
||||
append(cg, "deserialize",
|
||||
"\t{\n" \
|
||||
"\t\tvar length uint32\n" \
|
||||
indent(sprintf(CodegenDeserialize["u32"], "length")))
|
||||
if (d["type"] == "u8") {
|
||||
append(cg, "deserialize",
|
||||
"\t\tif uint64(len(data)) < uint64(length) {\n" \
|
||||
"\t\t\treturn nil, false\n" \
|
||||
"\t\t}\n" \
|
||||
"\t\t" f ", data = data[:length], data[length:]\n" \
|
||||
"\t}\n")
|
||||
} else {
|
||||
append(cg, "deserialize",
|
||||
"\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \
|
||||
"\t}\n" \
|
||||
"\tfor i := 0; i < len(" f "); i++ {\n" \
|
||||
indent(sprintf(deserialize, f "[i]")) \
|
||||
"\t}\n")
|
||||
}
|
||||
}
|
||||
|
||||
function codegen_struct_tag(d, cg, camel, f) {
|
||||
camel = snaketocamel(d["name"])
|
||||
f = "s." camel
|
||||
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
|
||||
" `json:\"" decapitalize(camel) "\"`\n")
|
||||
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
|
||||
# Do not deserialize here, that is already done by the containing union.
|
||||
}
|
||||
|
||||
function codegen_struct(name, cg, gotype) {
|
||||
gotype = PrefixCamel name
|
||||
print "type " gotype " struct {\n" cg["fields"] "}\n"
|
||||
|
||||
if (cg["serialize"]) {
|
||||
print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
|
||||
print "\tok := true"
|
||||
print cg["serialize"] "\treturn data, ok"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
CodegenSerialize[name] = \
|
||||
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
}
|
||||
if (cg["deserialize"]) {
|
||||
print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
|
||||
print "\tok := true"
|
||||
print cg["deserialize"] "\treturn data, ok"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
CodegenDeserialize[name] = \
|
||||
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
}
|
||||
|
||||
CodegenGoType[name] = gotype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
||||
|
||||
function codegen_union_tag(d, cg) {
|
||||
cg["tagtype"] = d["type"]
|
||||
cg["tagname"] = d["name"]
|
||||
# The tag is implied from the type of struct stored in the interface.
|
||||
}
|
||||
|
||||
function codegen_union_struct(name, casename, cg, scg, structname, init) {
|
||||
# And thus not all generated structs are present in Types.
|
||||
structname = name snaketocamel(casename)
|
||||
codegen_struct(structname, scg)
|
||||
|
||||
init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \
|
||||
": " decapitalize(snaketocamel(cg["tagname"])) "}"
|
||||
append(cg, "unmarshal",
|
||||
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
|
||||
"\t\ts := " init "\n" \
|
||||
"\t\terr = json.Unmarshal(data, &s)\n" \
|
||||
"\t\tu.Interface = s\n")
|
||||
append(cg, "serialize",
|
||||
"\tcase " CodegenGoType[structname] ":\n" \
|
||||
indent(sprintf(CodegenSerialize[structname], "union")))
|
||||
append(cg, "deserialize",
|
||||
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
|
||||
"\t\ts := " init "\n" \
|
||||
indent(sprintf(CodegenDeserialize[structname], "s")) \
|
||||
"\t\tu.Interface = s\n")
|
||||
}
|
||||
|
||||
function codegen_union(name, cg, gotype, tagfield, tagvar) {
|
||||
gotype = PrefixCamel name
|
||||
print "type " gotype " struct {"
|
||||
print "\tInterface any"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "func (u *" gotype ") MarshalJSON() ([]byte, error) {"
|
||||
print "\treturn json.Marshal(u.Interface)"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
tagfield = snaketocamel(cg["tagname"])
|
||||
tagvar = decapitalize(tagfield)
|
||||
print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
|
||||
print "\tvar t struct {"
|
||||
print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \
|
||||
" `json:\"" tagvar "\"`"
|
||||
print "\t}"
|
||||
print "\tif err := json.Unmarshal(data, &t); err != nil {"
|
||||
print "\t\treturn err"
|
||||
print "\t}"
|
||||
print ""
|
||||
print "\tswitch " tagvar " := t." tagfield "; " tagvar " {"
|
||||
print cg["unmarshal"] "\tdefault:"
|
||||
print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
|
||||
print "\t}"
|
||||
print "\treturn err"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
# XXX: Consider changing the interface into an AppendTo/ConsumeFrom one,
|
||||
# that would eliminate these type case switches entirely.
|
||||
# On the other hand, it would make it possible to send unsuitable structs.
|
||||
print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
|
||||
print "\tok := true"
|
||||
print "\tswitch union := u.Interface.(type) {"
|
||||
print cg["serialize"] "\tdefault:"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\treturn data, ok"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
CodegenSerialize[name] = \
|
||||
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
|
||||
print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
|
||||
print "\tok := true"
|
||||
print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]]
|
||||
print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar)
|
||||
print "\tswitch " tagvar " {"
|
||||
print cg["deserialize"] "\tdefault:"
|
||||
print "\t\treturn nil, false"
|
||||
print "\t}"
|
||||
print "\treturn data, ok"
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
CodegenDeserialize[name] = \
|
||||
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
|
||||
"\t\treturn nil, ok\n" \
|
||||
"\t}\n"
|
||||
|
||||
CodegenGoType[name] = gotype
|
||||
for (i in cg)
|
||||
delete cg[i]
|
||||
}
|
303
xC-gen-proto.awk
Normal file
303
xC-gen-proto.awk
Normal file
@ -0,0 +1,303 @@
|
||||
# xC-gen-proto.awk: an XDR-derived code generator for network protocols.
|
||||
#
|
||||
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
#
|
||||
# You may read RFC 4506 for context, however it is only a source of inspiration.
|
||||
# Grammar is easy to deduce from the parser.
|
||||
#
|
||||
# Native types: bool, u{8,16,32,64}, i{8,16,32,64}, string
|
||||
#
|
||||
# Don't define any new types, unless you hate yourself, then it's okay to do so.
|
||||
# Both backends are a pain in the arse, for different reasons.
|
||||
#
|
||||
# All numbers are encoded in big-endian byte order.
|
||||
# Booleans are one byte each.
|
||||
# Strings must be valid UTF-8, use u8<> to lift that restriction.
|
||||
# String and array lengths are encoded as u32.
|
||||
# Enumeration values automatically start at 1, and are encoded as i32.
|
||||
# Any struct or union field may be a variable-length array.
|
||||
#
|
||||
# Message framing is done externally, but also happens to prefix u32 lengths.
|
||||
#
|
||||
# Usage: env LC_ALL=C awk -v prefix=Relay \
|
||||
# -f xC-gen-proto.awk < xC-proto \
|
||||
# -f xC-gen-proto-{c,go}.awk > xC-proto.{c,go} | {clang-format,gofmt}
|
||||
|
||||
# --- Utilities ----------------------------------------------------------------
|
||||
|
||||
function cameltosnake(s) {
|
||||
while (match(s, /[[:lower:]][[:upper:]]/)) {
|
||||
s = substr(s, 1, RSTART) "_" \
|
||||
tolower(substr(s, RSTART + 1, RLENGTH - 1)) \
|
||||
substr(s, RSTART + RLENGTH)
|
||||
}
|
||||
return tolower(s)
|
||||
}
|
||||
|
||||
function snaketocamel(s) {
|
||||
s = toupper(substr(s, 1, 1)) tolower(substr(s, 2))
|
||||
while (match(s, /_[[:alnum:]]/)) {
|
||||
s = substr(s, 1, RSTART - 1) \
|
||||
toupper(substr(s, RSTART + 1, RLENGTH - 1)) \
|
||||
substr(s, RSTART + RLENGTH)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function decapitalize(s) {
|
||||
if (match(s, /[[:upper:]][[:lower:]]/)) {
|
||||
return tolower(substr(s, 1, 1)) substr(s, 2)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function indent(s) {
|
||||
if (!s)
|
||||
return s
|
||||
|
||||
gsub(/\n/, "\n\t", s)
|
||||
sub(/\t*$/, "", s)
|
||||
return "\t" s
|
||||
}
|
||||
|
||||
function append(a, key, value) {
|
||||
a[key] = a[key] value
|
||||
}
|
||||
|
||||
# --- Parsing ------------------------------------------------------------------
|
||||
|
||||
function fatal(message) {
|
||||
print "// " FILENAME ":" FNR ": fatal error: " message
|
||||
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function skipcomment() {
|
||||
do {
|
||||
if (match($0, /[*][/]/)) {
|
||||
$0 = substr($0, RSTART + RLENGTH)
|
||||
return
|
||||
}
|
||||
} while (getline > 0)
|
||||
fatal("unterminated block comment")
|
||||
}
|
||||
|
||||
function nexttoken() {
|
||||
do {
|
||||
if (match($0, /^[[:space:]]+/)) {
|
||||
$0 = substr($0, RLENGTH + 1)
|
||||
} else if (match($0, /^[/][/].*/)) {
|
||||
$0 = ""
|
||||
} else if (match($0, /^[/][*]/)) {
|
||||
$0 = substr($0, RLENGTH + 1)
|
||||
skipcomment()
|
||||
} else if (match($0, /^[[:alpha:]][[:alnum:]_]*/)) {
|
||||
Token = substr($0, 1, RLENGTH)
|
||||
$0 = substr($0, RLENGTH + 1)
|
||||
return Token
|
||||
} else if (match($0, /^(0[xX][0-9a-fA-F]+|[1-9][0-9]*)/)) {
|
||||
Token = substr($0, 1, RLENGTH)
|
||||
$0 = substr($0, RLENGTH + 1)
|
||||
return Token
|
||||
} else if (/./) {
|
||||
Token = substr($0, 1, 1)
|
||||
$0 = substr($0, 2)
|
||||
return Token
|
||||
}
|
||||
} while (/./ || getline > 0)
|
||||
Token = ""
|
||||
return Token
|
||||
}
|
||||
|
||||
function expect(v) {
|
||||
if (!v)
|
||||
fatal("broken expectations at `" Token "' before `" $0 "'")
|
||||
return v
|
||||
}
|
||||
|
||||
function accept(what) {
|
||||
if (Token != what)
|
||||
return 0
|
||||
nexttoken()
|
||||
return 1
|
||||
}
|
||||
|
||||
function identifier( v) {
|
||||
if (Token !~ /^[[:alpha:]]/)
|
||||
return 0
|
||||
v = Token
|
||||
nexttoken()
|
||||
return v
|
||||
}
|
||||
|
||||
function number( v) {
|
||||
if (Token !~ /^[0-9]/)
|
||||
return 0
|
||||
v = Token
|
||||
nexttoken()
|
||||
return v
|
||||
}
|
||||
|
||||
function readnumber( ident) {
|
||||
ident = identifier()
|
||||
if (!ident)
|
||||
return expect(number())
|
||||
if (!(ident in Consts))
|
||||
fatal("unknown constant: " ident)
|
||||
return Consts[ident]
|
||||
}
|
||||
|
||||
function defconst( ident, num) {
|
||||
if (!accept("const"))
|
||||
return 0
|
||||
|
||||
ident = expect(identifier())
|
||||
expect(accept("="))
|
||||
num = readnumber()
|
||||
if (ident in Consts)
|
||||
fatal("constant redefined: " ident)
|
||||
|
||||
Consts[ident] = num
|
||||
codegen_constant(ident, num)
|
||||
return 1
|
||||
}
|
||||
|
||||
function readtype( ident) {
|
||||
ident = deftype()
|
||||
if (ident)
|
||||
return ident
|
||||
|
||||
ident = identifier()
|
||||
if (!ident)
|
||||
return 0
|
||||
|
||||
if (!(ident in Types))
|
||||
fatal("unknown type: " ident)
|
||||
return ident
|
||||
}
|
||||
|
||||
function defenum( name, ident, value, cg) {
|
||||
delete cg[0]
|
||||
|
||||
name = expect(identifier())
|
||||
expect(accept("{"))
|
||||
while (!accept("}")) {
|
||||
ident = expect(identifier())
|
||||
value = value + 1
|
||||
if (accept("="))
|
||||
value = readnumber()
|
||||
if (!value)
|
||||
fatal("enumeration values cannot be zero")
|
||||
expect(accept(","))
|
||||
append(EnumValues, name, SUBSEP ident)
|
||||
if (EnumValues[name, ident]++)
|
||||
fatal("duplicate enum value: " ident)
|
||||
codegen_enum_value(name, ident, value, cg)
|
||||
}
|
||||
|
||||
Types[name] = "enum"
|
||||
codegen_enum(name, cg)
|
||||
return name
|
||||
}
|
||||
|
||||
function readfield(out, nonvoid) {
|
||||
nonvoid = !accept("void")
|
||||
if (nonvoid) {
|
||||
out["type"] = expect(readtype())
|
||||
out["name"] = expect(identifier())
|
||||
# TODO: Consider supporting XDR's VLA length limits here.
|
||||
# TODO: Consider supporting XDR's fixed-length syntax for string limits.
|
||||
out["isarray"] = accept("<") && expect(accept(">"))
|
||||
}
|
||||
expect(accept(";"))
|
||||
return nonvoid
|
||||
}
|
||||
|
||||
function defstruct( name, d, cg) {
|
||||
delete d[0]
|
||||
delete cg[0]
|
||||
|
||||
name = expect(identifier())
|
||||
expect(accept("{"))
|
||||
while (!accept("}")) {
|
||||
if (readfield(d))
|
||||
codegen_struct_field(d, cg)
|
||||
}
|
||||
|
||||
Types[name] = "struct"
|
||||
codegen_struct(name, cg)
|
||||
return name
|
||||
}
|
||||
|
||||
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
|
||||
delete cg[0]
|
||||
delete scg[0]
|
||||
delete d[0]
|
||||
|
||||
name = expect(identifier())
|
||||
expect(accept("switch"))
|
||||
expect(accept("("))
|
||||
tag["type"] = tagtype = expect(readtype())
|
||||
tag["name"] = expect(identifier())
|
||||
expect(accept(")"))
|
||||
|
||||
if (Types[tagtype] != "enum")
|
||||
fatal("not an enum type: " tagtype)
|
||||
codegen_union_tag(tag, cg)
|
||||
|
||||
split(EnumValues[tagtype], a, SUBSEP)
|
||||
for (i in a)
|
||||
unseen[a[i]]++
|
||||
|
||||
expect(accept("{"))
|
||||
while (!accept("}")) {
|
||||
if (accept("case")) {
|
||||
if (tagvalue)
|
||||
codegen_union_struct(name, tagvalue, cg, scg)
|
||||
|
||||
tagvalue = expect(identifier())
|
||||
expect(accept(":"))
|
||||
if (!unseen[tagvalue]--)
|
||||
fatal("no such value or duplicate case: " tagtype "." tagvalue)
|
||||
codegen_struct_tag(tag, scg)
|
||||
} else if (tagvalue) {
|
||||
if (readfield(d))
|
||||
codegen_struct_field(d, scg)
|
||||
} else {
|
||||
fatal("union fields must fall under a case")
|
||||
}
|
||||
}
|
||||
if (tagvalue)
|
||||
codegen_union_struct(name, tagvalue, cg, scg)
|
||||
|
||||
# What remains non-zero in unseen[2..] is simply not recognized/allowed.
|
||||
Types[name] = "union"
|
||||
codegen_union(name, cg)
|
||||
return name
|
||||
}
|
||||
|
||||
function deftype() {
|
||||
if (accept("enum"))
|
||||
return defenum()
|
||||
if (accept("struct"))
|
||||
return defstruct()
|
||||
if (accept("union"))
|
||||
return defunion()
|
||||
return 0
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
PrefixLower = "relay_"
|
||||
PrefixUpper = "RELAY_"
|
||||
PrefixCamel = "Relay"
|
||||
|
||||
print "// Generated by xC-gen-proto.awk. DO NOT MODIFY."
|
||||
codegen_begin()
|
||||
|
||||
nexttoken()
|
||||
while (Token != "") {
|
||||
expect(defconst() || deftype())
|
||||
expect(accept(";"))
|
||||
}
|
||||
}
|
120
xC-proto
Normal file
120
xC-proto
Normal file
@ -0,0 +1,120 @@
|
||||
// Backwards-compatible protocol version.
|
||||
const VERSION = 1;
|
||||
|
||||
// From the frontend to the relay.
|
||||
struct CommandMessage {
|
||||
u32 command_seq;
|
||||
union CommandData switch (enum Command {
|
||||
HELLO,
|
||||
PING,
|
||||
ACTIVE,
|
||||
BUFFER_COMPLETE,
|
||||
BUFFER_INPUT,
|
||||
BUFFER_ACTIVATE,
|
||||
BUFFER_LOG,
|
||||
} command) {
|
||||
case HELLO:
|
||||
u32 version;
|
||||
case PING:
|
||||
void;
|
||||
case ACTIVE:
|
||||
void;
|
||||
case BUFFER_COMPLETE:
|
||||
string buffer_name;
|
||||
string text;
|
||||
u32 position;
|
||||
case BUFFER_INPUT:
|
||||
string buffer_name;
|
||||
string text;
|
||||
case BUFFER_ACTIVATE:
|
||||
string buffer_name;
|
||||
case BUFFER_LOG:
|
||||
string buffer_name;
|
||||
} data;
|
||||
};
|
||||
|
||||
// From the relay to the frontend.
|
||||
struct EventMessage {
|
||||
u32 event_seq;
|
||||
union EventData switch (enum Event {
|
||||
PING,
|
||||
BUFFER_UPDATE,
|
||||
BUFFER_RENAME,
|
||||
BUFFER_REMOVE,
|
||||
BUFFER_ACTIVATE,
|
||||
BUFFER_LINE,
|
||||
BUFFER_CLEAR,
|
||||
ERROR,
|
||||
RESPONSE,
|
||||
} event) {
|
||||
case PING:
|
||||
void;
|
||||
case BUFFER_UPDATE:
|
||||
string buffer_name;
|
||||
case BUFFER_RENAME:
|
||||
string buffer_name;
|
||||
string new;
|
||||
case BUFFER_REMOVE:
|
||||
string buffer_name;
|
||||
case BUFFER_ACTIVATE:
|
||||
string buffer_name;
|
||||
case BUFFER_LINE:
|
||||
string buffer_name;
|
||||
bool is_unimportant;
|
||||
bool is_highlight;
|
||||
enum Rendition {
|
||||
BARE,
|
||||
INDENT,
|
||||
STATUS,
|
||||
ERROR,
|
||||
JOIN,
|
||||
PART,
|
||||
} rendition;
|
||||
// Unix timestamp in seconds.
|
||||
u64 when;
|
||||
// Broken-up text, with in-band formatting.
|
||||
union ItemData switch (enum Item {
|
||||
TEXT,
|
||||
RESET,
|
||||
FG_COLOR,
|
||||
BG_COLOR,
|
||||
FLIP_BOLD,
|
||||
FLIP_ITALIC,
|
||||
FLIP_UNDERLINE,
|
||||
FLIP_INVERSE,
|
||||
FLIP_CROSSED_OUT,
|
||||
FLIP_MONOSPACE,
|
||||
} kind) {
|
||||
case TEXT:
|
||||
string text;
|
||||
case RESET:
|
||||
void;
|
||||
case FG_COLOR:
|
||||
i16 color;
|
||||
case BG_COLOR:
|
||||
i16 color;
|
||||
case FLIP_BOLD:
|
||||
case FLIP_ITALIC:
|
||||
case FLIP_UNDERLINE:
|
||||
case FLIP_INVERSE:
|
||||
case FLIP_CROSSED_OUT:
|
||||
case FLIP_MONOSPACE:
|
||||
void;
|
||||
} items<>;
|
||||
case BUFFER_CLEAR:
|
||||
string buffer_name;
|
||||
case ERROR:
|
||||
u32 command_seq;
|
||||
string error;
|
||||
case RESPONSE:
|
||||
u32 command_seq;
|
||||
union ResponseData switch (Command command) {
|
||||
case BUFFER_COMPLETE:
|
||||
u32 start;
|
||||
string completions<>;
|
||||
case BUFFER_LOG:
|
||||
// UTF-8, but not guaranteed.
|
||||
u8 log<>;
|
||||
} data;
|
||||
} data;
|
||||
};
|
785
xC.c
785
xC.c
@ -50,6 +50,7 @@ enum
|
||||
|
||||
#include "common.c"
|
||||
#include "xD-replies.c"
|
||||
#include "xC-proto.c"
|
||||
|
||||
#include <math.h>
|
||||
#include <langinfo.h>
|
||||
@ -1526,6 +1527,7 @@ enum buffer_line_flags
|
||||
BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this
|
||||
};
|
||||
|
||||
// NOTE: This sequence must match up with xC-proto, only one lower.
|
||||
enum buffer_line_rendition
|
||||
{
|
||||
BUFFER_LINE_BARE, ///< Unadorned
|
||||
@ -1666,6 +1668,50 @@ buffer_destroy (struct buffer *self)
|
||||
REF_COUNTABLE_METHODS (buffer)
|
||||
#define buffer_ref do_not_use_dangerous
|
||||
|
||||
// ~~~ Relay ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
struct client
|
||||
{
|
||||
LIST_HEADER (struct client)
|
||||
struct app_context *ctx; ///< Application context
|
||||
|
||||
// TODO: Convert this all to TLS, and only TLS, with required client cert.
|
||||
// That means replacing plumbing functions with the /other/ set from xD.
|
||||
|
||||
int socket_fd; ///< The TCP socket
|
||||
struct str read_buffer; ///< Unprocessed input
|
||||
struct str write_buffer; ///< Output yet to be sent out
|
||||
|
||||
uint32_t event_seq; ///< Outgoing message counter
|
||||
bool initialized; ///< Initial sync took place
|
||||
|
||||
struct poller_fd socket_event; ///< The socket can be read/written to
|
||||
};
|
||||
|
||||
static struct client *
|
||||
client_new (void)
|
||||
{
|
||||
struct client *self = xcalloc (1, sizeof *self);
|
||||
self->socket_fd = -1;
|
||||
self->read_buffer = str_make ();
|
||||
self->write_buffer = str_make ();
|
||||
return self;
|
||||
}
|
||||
|
||||
static void
|
||||
client_destroy (struct client *self)
|
||||
{
|
||||
if (!soft_assert (self->socket_fd == -1))
|
||||
xclose (self->socket_fd);
|
||||
|
||||
str_free (&self->read_buffer);
|
||||
str_free (&self->write_buffer);
|
||||
free (self);
|
||||
}
|
||||
|
||||
static void client_kill (struct client *c);
|
||||
static bool client_process_buffer (struct client *c);
|
||||
|
||||
// ~~~ Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
// The only real purpose of this is to abstract away TLS
|
||||