Compare commits
	
		
			4 Commits
		
	
	
		
			3af1765261
			...
			1639235a48
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1639235a48 | |||
| 2160d03794 | |||
| 36f8c7639f | |||
| 74470f1aa4 | 
| @ -1,10 +1,12 @@ | |||||||
| # Ubuntu 18.04 LTS and OpenBSD 6.4 | # Ubuntu 18.04 LTS and OpenBSD 6.4 | ||||||
| cmake_minimum_required (VERSION 3.10) | 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 | # Options | ||||||
| option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) | option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) | ||||||
| option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF) | option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF) | ||||||
|  | option (WANT_XF "Build xF" OFF) | ||||||
| 
 | 
 | ||||||
| # Moar warnings | # Moar warnings | ||||||
| set (CMAKE_C_STANDARD 99) | set (CMAKE_C_STANDARD 99) | ||||||
| @ -143,28 +145,55 @@ set (HAVE_EDITLINE "${WANT_LIBEDIT}") | |||||||
| set (HAVE_LUA      "${WITH_LUA}") | set (HAVE_LUA      "${WITH_LUA}") | ||||||
| 
 | 
 | ||||||
| include (GNUInstallDirs) | 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}) | include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) | ||||||
| 
 | 
 | ||||||
| # Generate IRC replies--we need a custom target because of the multiple outputs | # Generate IRC replies--we need a custom target because of the multiple outputs | ||||||
| add_custom_command (OUTPUT xD-replies.c xD.msg | add_custom_command (OUTPUT xD-replies.c xD.msg | ||||||
| 	COMMAND env LC_ALL=C awk -f ${PROJECT_SOURCE_DIR}/xD-gen-replies.awk | 	COMMAND env LC_ALL=C awk | ||||||
| 		> xD-replies.c < ${PROJECT_SOURCE_DIR}/xD-replies | 		-f ${PROJECT_SOURCE_DIR}/xD-gen-replies.awk | ||||||
| 	DEPENDS ${PROJECT_SOURCE_DIR}/xD-replies | 		${PROJECT_SOURCE_DIR}/xD-replies > xD-replies.c | ||||||
|  | 	DEPENDS | ||||||
|  | 		${PROJECT_SOURCE_DIR}/xD-gen-replies.awk | ||||||
|  | 		${PROJECT_SOURCE_DIR}/xD-replies | ||||||
| 	COMMENT "Generating files from the list of server numerics") | 	COMMENT "Generating files from the list of server numerics") | ||||||
| add_custom_target (replies DEPENDS ${PROJECT_BINARY_DIR}/xD-replies.c) | 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 | # Build | ||||||
| foreach (name xB xC xD) | 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}) | 	target_link_libraries (${name} ${project_libraries}) | ||||||
| 	add_threads (${name}) | 	add_threads (${name}) | ||||||
| endforeach () | endforeach () | ||||||
| 
 | 
 | ||||||
| add_dependencies (xD replies) | add_dependencies (xD replies) | ||||||
| add_dependencies (xC replies) | add_dependencies (xC replies xC-proto) | ||||||
| target_link_libraries (xC ${xC_libraries}) | 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 | # Tests | ||||||
| include (CTest) | include (CTest) | ||||||
| if (BUILD_TESTING) | if (BUILD_TESTING) | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								NEWS
									
									
									
									
									
								
							| @ -1,5 +1,9 @@ | |||||||
| Unreleased | 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.*, |  * xC: all behaviour.* configuration options have been renamed to general.*, | ||||||
|    with the exception of editor_command/editor, backlog_helper/pager, |    with the exception of editor_command/editor, backlog_helper/pager, | ||||||
|    and backlog_helper_strip_formatting/pager_strip_formatting |    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: 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 |  * 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" | 1.5.0 (2021-12-21) "The Show Must Go On" | ||||||
|  | |||||||
							
								
								
									
										36
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								README.adoc
									
									
									
									
									
								
							| @ -1,9 +1,9 @@ | |||||||
| xK | xK | ||||||
| == | == | ||||||
| 
 | 
 | ||||||
| 'xK' (chat kit) is an IRC software suite consisting of a terminal client, | 'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, terminal | ||||||
| daemon, and bot.  It's all you're ever going to need for chatting, | client, and X11/web frontends for the client.  It's all you're ever going to | ||||||
| so long as you can make do with slightly minimalist software. | 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. | 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 | message splitting, multiline editing, bracketed paste support, word wrapping | ||||||
| that doesn't break links, autocomplete, logging, CTCP queries, auto-away, | that doesn't break links, autocomplete, logging, CTCP queries, auto-away, | ||||||
| command aliases, SOCKS proxying, SASL EXTERNAL authentication using TLS client | command aliases, SOCKS proxying, SASL EXTERNAL authentication using TLS client | ||||||
| certificates, or basic support for Lua scripting.  As a unique bonus, you can | certificates, a remote relay interface, or basic support for Lua scripting. | ||||||
| launch a full text editor from within. | 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 | 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 | besides the total number of connections and mode `+l`, or server linking | ||||||
| (which also means no services). | (which also means no services). | ||||||
| 
 | 
 | ||||||
| This program has been | This program has been https://git.janouch.name/p/haven/src/branch/master/hid[ | ||||||
| https://git.janouch.name/p/haven/src/branch/master/hid[ported to Go], | ported to Go] in a different project, and development continues over there. | ||||||
| and development continues over there. |  | ||||||
| 
 | 
 | ||||||
| xB | xB | ||||||
| -- | -- | ||||||
| @ -60,11 +69,12 @@ a package with the latest development version from Archlinux's AUR. | |||||||
| 
 | 
 | ||||||
| Building | Building | ||||||
| -------- | -------- | ||||||
| Build dependencies: CMake, pkg-config, asciidoctor or asciidoc, awk, | Build-only dependencies: | ||||||
|                     liberty (included) + |  CMake, pkg-config, asciidoctor or asciidoc, awk, liberty (included) + | ||||||
| Runtime dependencies: openssl + | Common runtime dependencies: openssl + | ||||||
| Additionally for 'xC': curses, libffi, lua >= 5.3 (optional), | Additionally for 'xC': curses, libffi, + | ||||||
|                        readline >= 6.0 or libedit >= 2013-07-12 |  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 |  $ git clone --recursive https://git.janouch.name/p/xK.git | ||||||
|  $ mkdir xK/build |  $ mkdir xK/build | ||||||
|  | |||||||
							
								
								
									
										71
									
								
								common.c
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								common.c
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| /*
 | /*
 | ||||||
|  * common.c: common functionality |  * 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 |  * Permission to use, copy, modify, and/or distribute this software for any | ||||||
|  * purpose with or without fee is hereby granted. |  * purpose with or without fee is hereby granted. | ||||||
| @ -48,6 +48,66 @@ init_openssl (void) | |||||||
| #endif | #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 --------------------------------------------------
 | // --- To be moved to liberty --------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| // FIXME: in xssl_get_error() we rely on error reasons never being NULL (i.e.,
 | // 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; | 	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 | static ssize_t | ||||||
| strv_find (const struct strv *v, const char *s) | 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; | ||||||
|  | }; | ||||||
							
								
								
									
										39
									
								
								xD.c
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								xD.c
									
									
									
									
									
								
							| @ -853,8 +853,6 @@ client_send_str (struct client *c, const struct str *s) | |||||||
| 	str_append_data (&c->write_buffer, s->str, | 	str_append_data (&c->write_buffer, s->str, | ||||||
| 		MIN (s->len, IRC_MAX_MESSAGE_LENGTH)); | 		MIN (s->len, IRC_MAX_MESSAGE_LENGTH)); | ||||||
| 	str_append (&c->write_buffer, "\r\n"); | 	str_append (&c->write_buffer, "\r\n"); | ||||||
| 	// XXX: we might want to move this elsewhere, so that it doesn't get called
 |  | ||||||
| 	//   as often; it's going to cause a lot of syscalls with epoll.
 |  | ||||||
| 	client_update_poller (c, NULL); | 	client_update_poller (c, NULL); | ||||||
| 
 | 
 | ||||||
| 	// Technically we haven't sent it yet but that's a minor detail
 | 	// Technically we haven't sent it yet but that's a minor detail
 | ||||||
| @ -3095,6 +3093,7 @@ irc_try_read (struct client *c) | |||||||
| 		{ | 		{ | ||||||
| 			buf->str[buf->len += n_read] = '\0'; | 			buf->str[buf->len += n_read] = '\0'; | ||||||
| 			// TODO: discard characters above the 512 character limit
 | 			// TODO: discard characters above the 512 character limit
 | ||||||
|  | 			// FIXME: we should probably discard the data if closing_link
 | ||||||
| 			irc_process_buffer (buf, irc_process_message, c); | 			irc_process_buffer (buf, irc_process_message, c); | ||||||
| 			continue; | 			continue; | ||||||
| 		} | 		} | ||||||
| @ -3136,6 +3135,7 @@ irc_try_read_tls (struct client *c) | |||||||
| 		case SSL_ERROR_NONE: | 		case SSL_ERROR_NONE: | ||||||
| 			buf->str[buf->len += n_read] = '\0'; | 			buf->str[buf->len += n_read] = '\0'; | ||||||
| 			// TODO: discard characters above the 512 character limit
 | 			// TODO: discard characters above the 512 character limit
 | ||||||
|  | 			// FIXME: we should probably discard the data if closing_link
 | ||||||
| 			irc_process_buffer (buf, irc_process_message, c); | 			irc_process_buffer (buf, irc_process_message, c); | ||||||
| 			continue; | 			continue; | ||||||
| 		case SSL_ERROR_ZERO_RETURN: | 		case SSL_ERROR_ZERO_RETURN: | ||||||
| @ -3421,16 +3421,10 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd) | |||||||
| 		if (errno == EINTR) | 		if (errno == EINTR) | ||||||
| 			return true; | 			return true; | ||||||
| 
 | 
 | ||||||
| 		if (errno == EBADF | 		if (accept_error_is_transient (errno)) | ||||||
| 		 || errno == EINVAL |  | ||||||
| 		 || errno == ENOTSOCK |  | ||||||
| 		 || errno == EOPNOTSUPP) |  | ||||||
| 			print_fatal ("%s: %s", "accept", strerror (errno)); |  | ||||||
| 
 |  | ||||||
| 		// 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.
 |  | ||||||
| 			print_warning ("%s: %s", "accept", strerror (errno)); | 			print_warning ("%s: %s", "accept", strerror (errno)); | ||||||
|  | 		else | ||||||
|  | 			print_fatal ("%s: %s", "accept", strerror (errno)); | ||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -3814,10 +3808,9 @@ irc_lock_pid_file (struct server_context *ctx, struct error **e) | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static int | static int | ||||||
| irc_listen (struct addrinfo *gai_iter) | irc_listen (struct addrinfo *ai) | ||||||
| { | { | ||||||
| 	int fd = socket (gai_iter->ai_family, | 	int fd = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol); | ||||||
| 		gai_iter->ai_socktype, gai_iter->ai_protocol); |  | ||||||
| 	if (fd == -1) | 	if (fd == -1) | ||||||
| 		return -1; | 		return -1; | ||||||
| 	set_cloexec (fd); | 	set_cloexec (fd); | ||||||
| @ -3831,21 +3824,13 @@ irc_listen (struct addrinfo *gai_iter) | |||||||
| #if defined SOL_IPV6 && defined IPV6_V6ONLY | #if defined SOL_IPV6 && defined IPV6_V6ONLY | ||||||
| 	// Make NULL always bind to both IPv4 and IPv6, irrespectively of the order
 | 	// Make NULL always bind to both IPv4 and IPv6, irrespectively of the order
 | ||||||
| 	// of results; only INADDR6_ANY seems to be affected by this
 | 	// of results; only INADDR6_ANY seems to be affected by this
 | ||||||
| 	if (gai_iter->ai_family == AF_INET6) | 	if (ai->ai_family == AF_INET6) | ||||||
| 		soft_assert (setsockopt (fd, SOL_IPV6, IPV6_V6ONLY, | 		soft_assert (setsockopt (fd, SOL_IPV6, IPV6_V6ONLY, | ||||||
| 			&yes, sizeof yes) != -1); | 			&yes, sizeof yes) != -1); | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| 	char host[NI_MAXHOST], port[NI_MAXSERV]; | 	char *address = gai_reconstruct_address (ai); | ||||||
| 	host[0] = port[0] = '\0'; | 	if (bind (fd, ai->ai_addr, ai->ai_addrlen)) | ||||||
| 	int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, |  | ||||||
| 		host, sizeof host, port, sizeof port, |  | ||||||
| 		NI_NUMERICHOST | NI_NUMERICSERV); |  | ||||||
| 	if (err) |  | ||||||
| 		print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); |  | ||||||
| 
 |  | ||||||
| 	char *address = format_host_port_pair (host, port); |  | ||||||
| 	if (bind (fd, gai_iter->ai_addr, gai_iter->ai_addrlen)) |  | ||||||
| 		print_error ("bind to %s failed: %s", address, strerror (errno)); | 		print_error ("bind to %s failed: %s", address, strerror (errno)); | ||||||
| 	else if (listen (fd, 16 /* arbitrary number */)) | 	else if (listen (fd, 16 /* arbitrary number */)) | ||||||
| 		print_error ("listen on %s failed: %s", address, strerror (errno)); | 		print_error ("listen on %s failed: %s", address, strerror (errno)); | ||||||
| @ -3865,12 +3850,12 @@ static void | |||||||
| irc_listen_resolve (struct server_context *ctx, | irc_listen_resolve (struct server_context *ctx, | ||||||
| 	const char *host, const char *port, struct addrinfo *gai_hints) | 	const char *host, const char *port, struct addrinfo *gai_hints) | ||||||
| { | { | ||||||
| 	struct addrinfo *gai_result, *gai_iter; | 	struct addrinfo *gai_result = NULL, *gai_iter = NULL; | ||||||
| 	int err = getaddrinfo (host, port, gai_hints, &gai_result); | 	int err = getaddrinfo (host, port, gai_hints, &gai_result); | ||||||
| 	if (err) | 	if (err) | ||||||
| 	{ | 	{ | ||||||
| 		char *address = format_host_port_pair (host, port); | 		char *address = format_host_port_pair (host, port); | ||||||
| 		print_error ("bind to %s failed: %s: %s", | 		print_error ("binding to %s failed: %s: %s", | ||||||
| 			address, "getaddrinfo", gai_strerror (err)); | 			address, "getaddrinfo", gai_strerror (err)); | ||||||
| 		free (address); | 		free (address); | ||||||
| 		return; | 		return; | ||||||
|  | |||||||
							
								
								
									
										172
									
								
								xF.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								xF.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | |||||||
|  | /*
 | ||||||
|  |  * xF.c: a toothless IRC client frontend | ||||||
|  |  * | ||||||
|  |  * Copyright (c) 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. | ||||||
|  |  * | ||||||
|  |  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||||||
|  |  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||||||
|  |  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | ||||||
|  |  * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||||||
|  |  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION | ||||||
|  |  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | ||||||
|  |  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | #include "config.h" | ||||||
|  | #define PROGRAM_NAME "xF" | ||||||
|  | 
 | ||||||
|  | #include "common.c" | ||||||
|  | #include "xC-proto.c" | ||||||
|  | 
 | ||||||
|  | #include <X11/Xatom.h> | ||||||
|  | #include <X11/Xlib.h> | ||||||
|  | #include <X11/keysym.h> | ||||||
|  | #include <X11/XKBlib.h> | ||||||
|  | #include <X11/Xft/Xft.h> | ||||||
|  | 
 | ||||||
|  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
|  | 
 | ||||||
|  | static struct | ||||||
|  | { | ||||||
|  | 	bool polling; | ||||||
|  | 	struct connector connector; | ||||||
|  | 	int socket; | ||||||
|  | } | ||||||
|  | g; | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | on_connector_connecting (void *user_data, const char *address) | ||||||
|  | { | ||||||
|  | 	(void) user_data; | ||||||
|  | 	print_status ("connecting to %s...", address); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | on_connector_error (void *user_data, const char *error) | ||||||
|  | { | ||||||
|  | 	(void) user_data; | ||||||
|  | 	print_status ("connection failed: %s", error); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | on_connector_failure (void *user_data) | ||||||
|  | { | ||||||
|  | 	(void) user_data; | ||||||
|  | 	exit_fatal ("giving up"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | on_connector_connected (void *user_data, int socket, const char *hostname) | ||||||
|  | { | ||||||
|  | 	(void) user_data; | ||||||
|  | 	(void) hostname; | ||||||
|  | 	g.polling = false; | ||||||
|  | 	g.socket = socket; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | protocol_test (const char *host, const char *port) | ||||||
|  | { | ||||||
|  | 	struct poller poller = {}; | ||||||
|  | 	poller_init (&poller); | ||||||
|  | 
 | ||||||
|  | 	connector_init (&g.connector, &poller); | ||||||
|  | 	g.connector.on_connecting = on_connector_connecting; | ||||||
|  | 	g.connector.on_error      = on_connector_error; | ||||||
|  | 	g.connector.on_connected  = on_connector_connected; | ||||||
|  | 	g.connector.on_failure    = on_connector_failure; | ||||||
|  | 
 | ||||||
|  | 	connector_add_target (&g.connector, host, port); | ||||||
|  | 
 | ||||||
|  | 	g.polling = true; | ||||||
|  | 	while (g.polling) | ||||||
|  | 		poller_run (&poller); | ||||||
|  | 
 | ||||||
|  | 	connector_free (&g.connector); | ||||||
|  | 
 | ||||||
|  | 	struct str s = str_make (); | ||||||
|  | 	str_pack_u32 (&s, 0); | ||||||
|  | 	struct relay_command_message m = {}; | ||||||
|  | 	m.data.hello.command = RELAY_COMMAND_HELLO; | ||||||
|  | 	m.data.hello.version = RELAY_VERSION; | ||||||
|  | 	if (!relay_command_message_serialize (&m, &s)) | ||||||
|  | 		exit_fatal ("serialization failed"); | ||||||
|  | 
 | ||||||
|  | 	uint32_t len = htonl (s.len - sizeof len); | ||||||
|  | 	memcpy (s.str, &len, sizeof len); | ||||||
|  | 	if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len) | ||||||
|  | 		exit_fatal ("short send or error: %s", strerror (errno)); | ||||||
|  | 
 | ||||||
|  | 	char buf[1 << 20] = ""; | ||||||
|  | 	while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len) | ||||||
|  | 	{ | ||||||
|  | 		len = ntohl (len); | ||||||
|  | 		if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len) | ||||||
|  | 			exit_fatal ("short read or error: %s", strerror (errno)); | ||||||
|  | 
 | ||||||
|  | 		struct msg_unpacker r = msg_unpacker_make (buf, len); | ||||||
|  | 		struct relay_event_message m = {}; | ||||||
|  | 		if (!relay_event_message_deserialize (&m, &r)) | ||||||
|  | 			exit_fatal ("deserialization failed"); | ||||||
|  | 		if (msg_unpacker_get_available (&r)) | ||||||
|  | 			exit_fatal ("trailing data"); | ||||||
|  | 
 | ||||||
|  | 		printf ("event: %d\n", m.data.event); | ||||||
|  | 		relay_event_message_free (&m); | ||||||
|  | 	} | ||||||
|  | 	exit_fatal ("short read or error: %s", strerror (errno)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
|  | 
 | ||||||
|  | int | ||||||
|  | main (int argc, char *argv[]) | ||||||
|  | { | ||||||
|  | 	static const struct opt opts[] = | ||||||
|  | 	{ | ||||||
|  | 		{ 'h', "help", NULL, 0, "display this help and exit" }, | ||||||
|  | 		{ 'V', "version", NULL, 0, "output version information and exit" }, | ||||||
|  | 		{ 0, NULL, NULL, 0, NULL } | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct opt_handler oh = opt_handler_make (argc, argv, opts, | ||||||
|  | 		"HOST:PORT", "X11 frontend for xC."); | ||||||
|  | 
 | ||||||
|  | 	int c; | ||||||
|  | 	while ((c = opt_handler_get (&oh)) != -1) | ||||||
|  | 	switch (c) | ||||||
|  | 	{ | ||||||
|  | 	case 'h': | ||||||
|  | 		opt_handler_usage (&oh, stdout); | ||||||
|  | 		exit (EXIT_SUCCESS); | ||||||
|  | 	case 'V': | ||||||
|  | 		printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); | ||||||
|  | 		exit (EXIT_SUCCESS); | ||||||
|  | 	default: | ||||||
|  | 		print_error ("wrong options"); | ||||||
|  | 		opt_handler_usage (&oh, stderr); | ||||||
|  | 		exit (EXIT_FAILURE); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	argc -= optind; | ||||||
|  | 	argv += optind; | ||||||
|  | 	if (argc != 1) | ||||||
|  | 	{ | ||||||
|  | 		opt_handler_usage (&oh, stderr); | ||||||
|  | 		exit (EXIT_FAILURE); | ||||||
|  | 	} | ||||||
|  | 	opt_handler_free (&oh); | ||||||
|  | 
 | ||||||
|  | 	char *address = xstrdup (argv[0]); | ||||||
|  | 	const char *port = NULL, *host = tokenize_host_port (address, &port); | ||||||
|  | 	if (!port) | ||||||
|  | 		exit_fatal ("missing port number/service name"); | ||||||
|  | 
 | ||||||
|  | 	// TODO: Actually implement an X11-based user interface.
 | ||||||
|  | 	protocol_test (host, port); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								xF.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								xF.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <svg version="1.1" width="48" height="48" viewBox="0 0 48 48" | ||||||
|  |    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg"> | ||||||
|  | 
 | ||||||
|  |   <defs> | ||||||
|  |     <linearGradient id="background" x1="0" y1="0" x2="1" y2="1"> | ||||||
|  |       <stop stop-color="#808080" offset="0" /> | ||||||
|  |       <stop stop-color="#000000" offset="1" /> | ||||||
|  |     </linearGradient> | ||||||
|  |     <!-- librsvg screws up the filter's orientation in a weird way | ||||||
|  |          otherwise a larger blur value would look better --> | ||||||
|  |     <filter id="shadow" color-interpolation-filters="sRGB"> | ||||||
|  |       <feOffset dy="0.5" /> | ||||||
|  |       <feGaussianBlur stdDeviation="0.5" /> | ||||||
|  |       <feComposite in2="SourceGraphic" operator="in" /> | ||||||
|  |     </filter> | ||||||
|  |     <clipPath id="clip"> | ||||||
|  |       <rect x="-7" y="-10" width="14" height="20" /> | ||||||
|  |     </clipPath> | ||||||
|  |   </defs> | ||||||
|  | 
 | ||||||
|  |   <circle cx="24" cy="24" r="20" | ||||||
|  |      fill="url(#background)" stroke="#404040" stroke-width="2" /> | ||||||
|  | 
 | ||||||
|  |   <g transform="rotate(-45 24 24)" filter="url(#shadow)"> | ||||||
|  |     <path d="m 12,25 h 24 v 11 h -5 v -8 h -4.5 v 6 h -5 v -6 h -9.5 z" | ||||||
|  |        fill="#ffffff" /> | ||||||
|  |     <g stroke-width="4" transform="translate(24, 16)" clip-path="url(#clip)" | ||||||
|  |        stroke="#ffffff"> | ||||||
|  |       <line x1="-8" x2="8" y1="-5" y2="5" /> | ||||||
|  |       <line x1="-8" x2="8" y1="5" y2="-5" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										3
									
								
								xP/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								xP/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | /xP | ||||||
|  | /proto.go | ||||||
|  | /public/mithril.js | ||||||
							
								
								
									
										14
									
								
								xP/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								xP/Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | .POSIX: | ||||||
|  | .SUFFIXES: | ||||||
|  | 
 | ||||||
|  | outputs = xP proto.go public/mithril.js | ||||||
|  | all: $(outputs) | ||||||
|  | 
 | ||||||
|  | xP: xP.go proto.go | ||||||
|  | 	go build -o $@ | ||||||
|  | proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto | ||||||
|  | 	awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@ | ||||||
|  | public/mithril.js: | ||||||
|  | 	curl -Lo $@ https://unpkg.com/mithril/mithril.js | ||||||
|  | clean: | ||||||
|  | 	rm -f $(outputs) | ||||||
							
								
								
									
										5
									
								
								xP/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								xP/go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | module janouch.name/xK | ||||||
|  | 
 | ||||||
|  | go 1.18 | ||||||
|  | 
 | ||||||
|  | require golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d | ||||||
							
								
								
									
										2
									
								
								xP/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								xP/go.sum
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= | ||||||
|  | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
							
								
								
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | |||||||
|  | body { | ||||||
|  | 	margin: 0; | ||||||
|  | 	padding: 0; | ||||||
|  | 	font-family: sans-serif; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .xP { | ||||||
|  | 	height: 100vh; | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	overflow: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .title, .status { | ||||||
|  | 	background: #f8f8f8; | ||||||
|  | 	border-bottom: 1px solid #ccc; | ||||||
|  | 	padding: .05rem .3rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .middle { | ||||||
|  | 	flex: auto; | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: row; | ||||||
|  | 	overflow: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .list { | ||||||
|  | 	overflow-y: auto; | ||||||
|  | 	border-right: 1px solid #ccc; | ||||||
|  | 	min-width: 10rem; | ||||||
|  | } | ||||||
|  | .item { | ||||||
|  | 	padding: .05rem .3rem; | ||||||
|  | 	cursor: default; | ||||||
|  | } | ||||||
|  | .item.active { | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Only Firefox currently supports align-content: safe end, thus this. */ | ||||||
|  | .buffer-container { | ||||||
|  | 	flex: auto; | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	overflow: hidden; | ||||||
|  | } | ||||||
|  | .filler { | ||||||
|  | 	flex: auto; | ||||||
|  | } | ||||||
|  | .buffer { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: max-content auto; | ||||||
|  | 	overflow-y: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date { | ||||||
|  | 	padding: .3rem; | ||||||
|  | 	grid-column: span 2; | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | .time { | ||||||
|  | 	padding: .1rem .3rem; | ||||||
|  | 	background: #f8f8f8; | ||||||
|  | 	color: #bbb; | ||||||
|  | 	border-right: 1px solid #ccc; | ||||||
|  | } | ||||||
|  | .mark { | ||||||
|  | 	padding-right: .3rem; | ||||||
|  | 	text-align: center; | ||||||
|  | 	display: inline-block; | ||||||
|  | 	min-width: 2rem; | ||||||
|  | } | ||||||
|  | .mark.error { | ||||||
|  | 	color: red; | ||||||
|  | } | ||||||
|  | .mark.join { | ||||||
|  | 	color: green; | ||||||
|  | } | ||||||
|  | .mark.part { | ||||||
|  | 	color: red; | ||||||
|  | } | ||||||
|  | .content { | ||||||
|  | 	padding: .1rem .3rem; | ||||||
|  | 	white-space: pre-wrap; | ||||||
|  | } | ||||||
|  | .content span.b { | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | .content span.i { | ||||||
|  | 	font-style: italic; | ||||||
|  | } | ||||||
|  | .content span.u { | ||||||
|  | 	text-decoration: underline; | ||||||
|  | } | ||||||
|  | .content span.s { | ||||||
|  | 	text-decoration: line-through; | ||||||
|  | } | ||||||
|  | .content span.m { | ||||||
|  | 	font-family: monospace; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .status { | ||||||
|  | 	border-top: 2px solid #fff; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | textarea { | ||||||
|  | 	padding: .05rem .3rem; | ||||||
|  | 	font-family: inherit; | ||||||
|  | } | ||||||
							
								
								
									
										188
									
								
								xP/public/xP.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								xP/public/xP.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | |||||||
|  | // TODO: Probably reset state on disconnect, and indicate to user.
 | ||||||
|  | let socket = new WebSocket(proxy) | ||||||
|  | 
 | ||||||
|  | let commandSeq = 0 | ||||||
|  | function send(command) { | ||||||
|  | 	socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command})) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | socket.onopen = function(event) { | ||||||
|  | 	send({command: 'Hello', version: 1}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let buffers = new Map() | ||||||
|  | let bufferCurrent = undefined | ||||||
|  | 
 | ||||||
|  | socket.onmessage = function(event) { | ||||||
|  | 	console.log(event.data) | ||||||
|  | 
 | ||||||
|  | 	let e = JSON.parse(event.data).data | ||||||
|  | 	switch (e.event) { | ||||||
|  | 	case 'BufferUpdate': | ||||||
|  | 	{ | ||||||
|  | 		let b = buffers.get(e.bufferName) | ||||||
|  | 		if (b === undefined) { | ||||||
|  | 			b = {lines: []} | ||||||
|  | 			buffers.set(e.bufferName, b) | ||||||
|  | 		} | ||||||
|  | 		// TODO: Update any buffer properties.
 | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 	case 'BufferRename': | ||||||
|  | 		buffers.set(e.new, buffers.get(e.bufferName)) | ||||||
|  | 		buffers.delete(e.bufferName) | ||||||
|  | 		break | ||||||
|  | 	case 'BufferRemove': | ||||||
|  | 		buffers.delete(e.bufferName) | ||||||
|  | 		break | ||||||
|  | 	case 'BufferActivate': | ||||||
|  | 		bufferCurrent = e.bufferName | ||||||
|  | 		// TODO: Somehow scroll to the end of it immediately.
 | ||||||
|  | 		// TODO: Focus the textarea.
 | ||||||
|  | 		break | ||||||
|  | 	case 'BufferLine': | ||||||
|  | 	{ | ||||||
|  | 		let b = buffers.get(e.bufferName) | ||||||
|  | 		if (b !== undefined) | ||||||
|  | 			b.lines.push({when: e.when, rendition: e.rendition, items: e.items}) | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 	case 'BufferClear': | ||||||
|  | 	{ | ||||||
|  | 		let b = buffers.get(e.bufferName) | ||||||
|  | 		if (b !== undefined) | ||||||
|  | 			b.lines.length = 0 | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	m.redraw() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let BufferList = { | ||||||
|  | 	view: vnode => { | ||||||
|  | 		let items = [] | ||||||
|  | 		buffers.forEach((b, name) => { | ||||||
|  | 			let attrs = { | ||||||
|  | 				onclick: e => { | ||||||
|  | 					send({command: 'BufferActivate', bufferName: name}) | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			if (name == bufferCurrent) | ||||||
|  | 				attrs.class = 'active' | ||||||
|  | 			items.push(m('.item', attrs, name)) | ||||||
|  | 		}) | ||||||
|  | 		return m('.list', {}, items) | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let Content = { | ||||||
|  | 	view: vnode => { | ||||||
|  | 		let line = vnode.children[0] | ||||||
|  | 		let content = [] | ||||||
|  | 		switch (line.rendition) { | ||||||
|  | 		case 'Indent': content.push(m('span.mark',       {}, ''));  break | ||||||
|  | 		case 'Status': content.push(m('span.mark',       {}, '–')); break | ||||||
|  | 		case 'Error':  content.push(m('span.mark.error', {}, '⚠')); break | ||||||
|  | 		case 'Join':   content.push(m('span.mark.join',  {}, '→')); break | ||||||
|  | 		case 'Part':   content.push(m('span.mark.part',  {}, '←')); break | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		let classes = new Set() | ||||||
|  | 		let flip = c => { | ||||||
|  | 			if (classes.has(c)) | ||||||
|  | 				classes.delete(c) | ||||||
|  | 			else | ||||||
|  | 				classes.add(c) | ||||||
|  | 		} | ||||||
|  | 		line.items.forEach(item => { | ||||||
|  | 			// TODO: Colours.
 | ||||||
|  | 			switch (item.kind) { | ||||||
|  | 			case 'Text': | ||||||
|  | 				// TODO: Detect and transform links.
 | ||||||
|  | 				content.push(m('span', { | ||||||
|  | 					class: Array.from(classes.keys()).join(' '), | ||||||
|  | 				}, item.text)) | ||||||
|  | 				break | ||||||
|  | 			case 'Reset': | ||||||
|  | 				classes.clear() | ||||||
|  | 				break | ||||||
|  | 			case 'FlipBold':       flip('b'); break | ||||||
|  | 			case 'FlipItalic':     flip('i'); break | ||||||
|  | 			case 'FlipUnderline':  flip('u'); break | ||||||
|  | 			case 'FlipInverse':    flip('i'); break | ||||||
|  | 			case 'FlipCrossedOut': flip('s'); break | ||||||
|  | 			case 'FlipMonospace':  flip('m'); break | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 		return m('.content', {}, content) | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let Buffer = { | ||||||
|  | 	view: vnode => { | ||||||
|  | 		let lines = [] | ||||||
|  | 		let b = buffers.get(bufferCurrent) | ||||||
|  | 		if (b === undefined) | ||||||
|  | 			return | ||||||
|  | 
 | ||||||
|  | 		let lastDateMark = undefined | ||||||
|  | 		b.lines.forEach(line => { | ||||||
|  | 			let date = new Date(line.when * 1000) | ||||||
|  | 			let dateMark = date.toLocaleDateString() | ||||||
|  | 			if (dateMark !== lastDateMark) { | ||||||
|  | 				lines.push(m('.date', {}, dateMark)) | ||||||
|  | 				lastDateMark = dateMark | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			lines.push(m('.time', {}, date.toLocaleTimeString())) | ||||||
|  | 			lines.push(m(Content, {}, line)) | ||||||
|  | 		}) | ||||||
|  | 		return m('.buffer-container', {}, [ | ||||||
|  | 			m('.filler'), | ||||||
|  | 			m('.buffer', {}, lines), | ||||||
|  | 		]) | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: This should be remembered across buffer switches,
 | ||||||
|  | // and we'll probably have to intercept /all/ key presses.
 | ||||||
|  | let Input = { | ||||||
|  | 	view: vnode => { | ||||||
|  | 		return m('textarea', { | ||||||
|  | 			rows: 1, | ||||||
|  | 			onkeydown: e => { | ||||||
|  | 				// TODO: And perhaps on other actions, too.
 | ||||||
|  | 				send({command: 'Active'}) | ||||||
|  | 				if (e.keyCode !== 13) | ||||||
|  | 					return | ||||||
|  | 
 | ||||||
|  | 				send({ | ||||||
|  | 					command: 'BufferInput', | ||||||
|  | 					bufferName: bufferCurrent, | ||||||
|  | 					text: e.currentTarget.value, | ||||||
|  | 				}) | ||||||
|  | 				e.preventDefault() | ||||||
|  | 				e.currentTarget.value = '' | ||||||
|  | 			}, | ||||||
|  | 		}) | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let Main = { | ||||||
|  | 	view: vnode => { | ||||||
|  | 		return m('.xP', {}, [ | ||||||
|  | 			m('.title', {}, "xP"), | ||||||
|  | 			m('.middle', {}, [m(BufferList), m(Buffer)]), | ||||||
|  | 			m('.status', {}, bufferCurrent), | ||||||
|  | 			m(Input), | ||||||
|  | 		]) | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: Buffer names should work as routes.
 | ||||||
|  | window.addEventListener('load', () => { | ||||||
|  | 	m.route(document.body, '/', { | ||||||
|  | 		'/': Main, | ||||||
|  | 	}) | ||||||
|  | }) | ||||||
							
								
								
									
										2
									
								
								xP/xP.example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								xP/xP.example.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | { | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								xP/xP.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								xP/xP.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,186 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"html/template" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/net/websocket" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	addressBind    string | ||||||
|  | 	addressConnect string | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func clientToRelay( | ||||||
|  | 	ctx context.Context, ws *websocket.Conn, conn net.Conn) bool { | ||||||
|  | 	var j string | ||||||
|  | 	if err := websocket.Message.Receive(ws, &j); err != nil { | ||||||
|  | 		log.Println("Command receive failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("?> %s\n", j) | ||||||
|  | 
 | ||||||
|  | 	var m RelayCommandMessage | ||||||
|  | 	if err := json.Unmarshal([]byte(j), &m); err != nil { | ||||||
|  | 		log.Println("Command unmarshalling failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, ok := m.AppendTo(make([]byte, 4)) | ||||||
|  | 	if !ok { | ||||||
|  | 		log.Println("Command serialization failed") | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4)) | ||||||
|  | 	if _, err := conn.Write(b); err != nil { | ||||||
|  | 		log.Println("Command send failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("-> %v\n", b) | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func relayToClient( | ||||||
|  | 	ctx context.Context, ws *websocket.Conn, conn net.Conn) bool { | ||||||
|  | 	var length uint32 | ||||||
|  | 	if err := binary.Read(conn, binary.BigEndian, &length); err != nil { | ||||||
|  | 		log.Println("Event receive failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	b := make([]byte, length) | ||||||
|  | 	if _, err := io.ReadFull(conn, b); err != nil { | ||||||
|  | 		log.Println("Event receive failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("<? %v\n", b) | ||||||
|  | 
 | ||||||
|  | 	var m RelayEventMessage | ||||||
|  | 	if after, ok := m.ConsumeFrom(b); !ok { | ||||||
|  | 		log.Println("Event deserialization failed") | ||||||
|  | 		return false | ||||||
|  | 	} else if len(after) != 0 { | ||||||
|  | 		log.Println("Event deserialization failed: trailing data") | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	j, err := json.Marshal(&m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Println("Event marshalling failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	if err := websocket.Message.Send(ws, string(j)); err != nil { | ||||||
|  | 		log.Println("Event send failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Printf("<- %s\n", j) | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func errorToClient(ws *websocket.Conn, err error) bool { | ||||||
|  | 	j, err := json.Marshal(&RelayEventMessage{ | ||||||
|  | 		EventSeq: 0, | ||||||
|  | 		Data: RelayEventData{ | ||||||
|  | 			Interface: RelayEventDataError{ | ||||||
|  | 				Event:      RelayEventError, | ||||||
|  | 				CommandSeq: 0, | ||||||
|  | 				Error:      err.Error(), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Println("Event marshalling failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	if err := websocket.Message.Send(ws, string(j)); err != nil { | ||||||
|  | 		log.Println("Event send failed: " + err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func handleWebSocket(ws *websocket.Conn) { | ||||||
|  | 	conn, err := net.Dial("tcp", addressConnect) | ||||||
|  | 	if err != nil { | ||||||
|  | 		errorToClient(ws, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// We don't need to intervene, so it's just two separate pipes so far. | ||||||
|  | 	ctx, cancel := context.WithCancel(ws.Request().Context()) | ||||||
|  | 	go func() { | ||||||
|  | 		for clientToRelay(ctx, ws, conn) { | ||||||
|  | 		} | ||||||
|  | 		cancel() | ||||||
|  | 	}() | ||||||
|  | 	go func() { | ||||||
|  | 		for relayToClient(ctx, ws, conn) { | ||||||
|  | 		} | ||||||
|  | 		cancel() | ||||||
|  | 	}() | ||||||
|  | 	<-ctx.Done() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var staticHandler = http.FileServer(http.Dir(".")) | ||||||
|  | 
 | ||||||
|  | var page = template.Must(template.New("/").Parse(`<!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<title>xP</title> | ||||||
|  | 	<meta charset="utf-8" /> | ||||||
|  | 	<link rel="stylesheet" href="xP.css" /> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<script src="mithril.js"> | ||||||
|  | 	</script> | ||||||
|  | 	<script> | ||||||
|  | 	let proxy = '{{ . }}' | ||||||
|  | 	</script> | ||||||
|  | 	<script src="xP.js"> | ||||||
|  | 	</script> | ||||||
|  | </body> | ||||||
|  | </html>`)) | ||||||
|  | 
 | ||||||
|  | func handleDefault(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	if r.URL.Path != "/" { | ||||||
|  | 		staticHandler.ServeHTTP(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	wsURI := fmt.Sprintf("ws://%s/ws", r.Host) | ||||||
|  | 	if err := page.Execute(w, wsURI); err != nil { | ||||||
|  | 		log.Println("Template execution failed: " + err.Error()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	if len(os.Args) != 3 { | ||||||
|  | 		log.Fatalf("usage: %s BIND CONNECT\n", os.Args[0]) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	addressBind, addressConnect = os.Args[1], os.Args[2] | ||||||
|  | 
 | ||||||
|  | 	http.Handle("/ws", websocket.Handler(handleWebSocket)) | ||||||
|  | 	http.Handle("/", http.HandlerFunc(handleDefault)) | ||||||
|  | 
 | ||||||
|  | 	s := &http.Server{ | ||||||
|  | 		Addr:           addressBind, | ||||||
|  | 		ReadTimeout:    60 * time.Second, | ||||||
|  | 		WriteTimeout:   60 * time.Second, | ||||||
|  | 		MaxHeaderBytes: 32 << 10, | ||||||
|  | 	} | ||||||
|  | 	log.Fatal(s.ListenAndServe()) | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user