Compare commits

...

42 Commits

Author SHA1 Message Date
1930f138d4 IconUtils: add Apple Icon Image format support
All checks were successful
Alpine 3.20 Success
2024-12-17 06:20:12 +01:00
32cbb15266 Serialize integer-ish config keys properly
All checks were successful
Alpine 3.20 Success
2024-12-16 09:09:03 +01:00
149938cc44 lxdrgen-cpp: add a Qt backend
All checks were successful
Alpine 3.20 Success
Motivation: some Android NDKs do not have iconv.
2024-12-15 06:44:06 +01:00
62f8a7d05f lxdrgen-cpp: fix test build on macOS
All checks were successful
Alpine 3.20 Success
2024-12-04 17:44:30 +01:00
492815c8fc lxdrgen-go: fix compatibility with 32-bit targets
All checks were successful
Alpine 3.20 Success
2024-11-09 17:06:46 +01:00
aacf1b1d47 lxdrgen-go: improve usability
All checks were successful
Alpine 3.20 Success
Turning union tags into read-only methods of actual types:
 - eliminates duplicated JSON unmarshalling of tags,
 - makes AppendTo/ConsumeFrom symmetrical in nature,
 - eliminates duplicated AppendTo code,
 - eliminates trivial AppendTo methods for subtypes without fields,
 - gives us an opportunity to use a more specific interface than "any"
   (the type being anonymous is an acknowledged inconvenience).

Implementing our own json.Marshalers some time ago
(for performance reasons) has made this easier to implement.

Also rename "Interface" fields to more suitable "Variant".
2024-11-07 11:01:41 +01:00
49d7cb12bb Fix calloc argument order
All checks were successful
Alpine 3.20 Success
2024-08-08 09:34:33 +02:00
fdf845d0bd const-qualify configuration schema items in tests 2024-08-08 09:21:16 +02:00
75fc6f1c37 const-qualify configuration schema items
All checks were successful
Alpine 3.20 Success
2024-08-08 08:53:49 +02:00
8a8437634a MPD client: fix argument quoting
All checks were successful
Alpine 3.20 Success
2024-08-07 22:04:00 +02:00
e78b410a6a MPD client: save the protocol version 2024-08-07 22:03:08 +02:00
bf44e827e8 liberty-xui: mention libgrapheme 2024-07-10 17:38:39 +02:00
8386af0420 Silence an OpenBSD linker warning
All checks were successful
Alpine 3.19 Success
2024-04-10 17:54:34 +02:00
f04cc2c61e Add MinGW-w64 CMake toolchain files
All checks were successful
Alpine 3.19 Success
2024-04-09 17:01:07 +02:00
969a4cfc3e liberty-xui: clip terminal drawing 2024-02-27 00:27:54 +01:00
ad5b2fb8cd asciiman: mildly improve compatibility
git manual pages render a little bit more sensibly now.
2024-02-12 10:57:23 +01:00
2a1f17a8f7 liberty-xdg: add desktop entry parser tests
And fix a discovered bug.
2024-02-10 12:49:01 +01:00
8d56fae41b liberty-xdg: actually make libpng optional 2024-02-10 12:20:44 +01:00
0239a4242a liberty-xdg: fix usage of volatile 2024-02-10 10:16:27 +01:00
1966b81b4d liberty-xui: tolerate zero-area ConfigureNotify
Happens when launching from a fullscreen window in i3.
2024-02-10 10:07:09 +01:00
f8c6ac2ed1 Make liberty-xui load PNG program icons
X11 applications now have a dependency on libpng.

This makes use of a new related liberty-xdg module,
which can be used separately.
2024-02-10 10:07:09 +01:00
f32bcbd7f4 Add helper functions for reading binary numbers
And make use of them.
2024-02-10 07:02:54 +01:00
c5424e6992 Comment on write_file_safe()'s actual safety 2024-02-10 06:16:27 +01:00
cb9d162a26 Add a CMake module for icon conversions 2024-02-10 05:39:15 +01:00
db6357db9a CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:22:17 +02:00
7a0cb13a1a MPD client: fix build on OpenIndiana 2023-07-24 08:33:45 +02:00
b6c54073cd Find ncursesw on OpenIndiana 2023-07-24 08:09:08 +02:00
62166f9679 lxdrgen-cpp-win32: fix return value handling 2023-07-10 09:34:42 +02:00
2edc9c6fd1 Add a C++ backend for LibertyXDR
Also change the C backend so that it also de/serializes
unions without any other fields besides the tag.
2023-07-07 16:43:52 +02:00
f78f8a70f1 lxdrgen-swift: fix prefix handling
"Any prefix will work, so long as it's 'Relay'."
2023-07-06 11:01:51 +02:00
be9a3e693e lxdrgen-swift: fix warnings with exhaustive unions 2023-07-06 06:54:23 +02:00
53197b51e5 Add a Swift backend for LibertyXDR 2023-07-06 06:54:22 +02:00
8466d0d850 CMakeLists.txt: link properly 2023-07-04 08:08:29 +02:00
4c2874649d liberty-xui: fix build on systems without A_ITALIC 2023-07-04 06:40:54 +02:00
717c301207 lxdrgen: fix decapitalization
decapitalize() is typically called on snaketocamel() output,
which always makes the first letter uppercase.
2023-06-28 16:24:59 +02:00
091f92bab3 liberty-xui: fix a build warning
On macOS, TIOCGWINSZ seems to be defined earlier.
2023-06-28 16:24:59 +02:00
556c25855e Fix a CMake warning 2023-06-20 01:24:29 +02:00
d01a1ff034 Turn liberty-tui into a terminal/X11 hybrid
Importing code from nncmpp, adjusting it to work with hex as well.
2023-06-19 13:06:12 +02:00
bd1013f16a Parse block attribute list lines
This code is of strategic importance, but its output is so far unused.
2023-06-11 10:02:16 +02:00
29bf109a51 asciiman: improve attribute handling 2022-10-09 18:43:37 +02:00
0e86ffe7c3 asciiman: fixes, improve mandoc compatibility 2022-10-09 01:01:08 +02:00
cbeb4e3133 Improve documentation 2022-10-04 01:46:41 +02:00
30 changed files with 4431 additions and 433 deletions

View File

@@ -1,7 +1,8 @@
project (liberty C)
cmake_minimum_required (VERSION 2.8.12)
cmake_minimum_required (VERSION 3.0...3.27)
project (liberty C CXX)
# Moar warnings
set (CMAKE_CXX_STANDARD 11)
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# -Wunused-function is pretty annoying here, as everything is static
set (wdisabled "-Wno-unused-function")
@@ -29,14 +30,14 @@ link_directories (${libssl_LIBRARY_DIRS})
foreach (extra iconv rt)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND common_libraries ${extra})
list (APPEND common_libraries ${extra_lib_${extra}})
endif ()
endforeach ()
# Build some unit tests
include_directories (${PROJECT_SOURCE_DIR})
enable_testing ()
set (tests liberty proto)
set (tests liberty proto xdg)
pkg_check_modules (libpulse libpulse)
if (libpulse_FOUND)
@@ -72,7 +73,7 @@ add_test (test-cmake-parser
# Test protocol code generation
set (lxdrgen_outputs)
set (lxdrgen_base "${PROJECT_BINARY_DIR}/lxdrgen.lxdr")
foreach (backend c go mjs)
foreach (backend c cpp go mjs swift)
list (APPEND lxdrgen_outputs ${lxdrgen_base}.${backend})
add_custom_command (OUTPUT ${lxdrgen_base}.${backend}
COMMAND env LC_ALL=C awk
@@ -91,9 +92,22 @@ add_custom_target (test-lxdrgen-outputs ALL DEPENDS ${lxdrgen_outputs})
set_source_files_properties (${lxdrgen_base}.c
PROPERTIES HEADER_FILE_ONLY TRUE)
add_executable (test-lxdrgen tests/lxdrgen.c ${lxdrgen_base}.c)
target_include_directories (test-lxdrgen PUBLIC ${PROJECT_BINARY_DIR})
add_test (NAME test-lxdrgen-c COMMAND test-lxdrgen)
add_executable (test-lxdrgen-c tests/lxdrgen.c ${lxdrgen_base}.c)
target_include_directories (test-lxdrgen-c PUBLIC ${PROJECT_BINARY_DIR})
add_test (NAME test-lxdrgen-c COMMAND test-lxdrgen-c)
set_source_files_properties (${lxdrgen_base}.cpp
PROPERTIES HEADER_FILE_ONLY TRUE)
if (WIN32)
add_executable (test-lxdrgen-cpp tests/lxdrgen.cpp
${lxdrgen_base}.cpp tools/lxdrgen-cpp-win32.cpp)
else ()
add_executable (test-lxdrgen-cpp tests/lxdrgen.cpp
${lxdrgen_base}.cpp tools/lxdrgen-cpp-posix.cpp)
endif ()
target_link_libraries (test-lxdrgen-cpp ${common_libraries})
target_include_directories (test-lxdrgen-cpp PUBLIC ${PROJECT_BINARY_DIR})
add_test (NAME test-lxdrgen-cpp COMMAND test-lxdrgen-cpp)
find_program (GO_EXECUTABLE go)
if (GO_EXECUTABLE)
@@ -108,3 +122,11 @@ if (NODE_EXECUTABLE)
else ()
message (WARNING "Cannot test generated protocol code for Javascript")
endif ()
find_program (SWIFTC_EXECUTABLE swiftc)
if (SWIFTC_EXECUTABLE)
add_test (test-lxdrgen-swift
${SWIFTC_EXECUTABLE} -typecheck ${lxdrgen_base}.swift)
else ()
message (WARNING "Cannot test generated protocol code for Swift")
endif ()

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2014 - 2024, 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.

View File

@@ -1,21 +1,22 @@
liberty
=======
'liberty' is a pseudolibrary of all the common C code I have written for various
projects. I used to copy-paste large swaths of code with minimal changes to it
'liberty' is a pseudolibrary largely consisting of reusable C code for my
various projects. I used to copy-paste large swaths of it with minimal changes,
and it slowly became awfully painful to synchronize. The project can be thought
of as a successor to my other C library, libxtnd.
You are supposed to import it as a git submodule and include the main source
file directly everywhere you need it. Everything is declared "static". I have
come to the conclusion that this style of C programming suits me the best, as it
allows me to nearly forget about the mess that are header files.
file directly everywhere you need it, setting feature flags as appropriate.
Everything is declared "static". I have come to the conclusion that this style
of C programming suits me the best, as it allows me to nearly forget about the
mess that are header files.
The API is intentionally unstable, which allows for easy refactoring.
All development is done on Linux, but other POSIX-compatible operating systems
should be supported as well. They have an extremely low priority, however, and
I'm not testing them at all, with the exception of OpenBSD.
should be generally supported as well. They have an extremely low priority,
however, and I'm not testing them at all, perhaps with the exception of macOS.
Tools
-----
@@ -24,7 +25,7 @@ This project also hosts a number of supporting scripts written in portable AWK:
asciiman.awk::
A fallback manual page generator for AsciiDoc documents,
motivated by the hugeness of AsciiDoc's and Asciidoctor's dependency trees.
It uses the _man_ macro package.
Just like them, it uses the _man_ macro package.
cmake-parser.awk::
Parses the CMake language to the extent that is necessary to reliably
@@ -43,6 +44,13 @@ lxdrgen.awk::
lxdrgen-c.awk::
LibertyXDR backend that builds on top of the C pseudolibrary.
lxdrgen-cpp.awk::
lxdrgen-cpp-posix.cpp::
lxdrgen-cpp-qt.cpp::
lxdrgen-cpp-win32.cpp::
LibertyXDR backend for C++, primarily targeting Win32 and its wide strings.
Link the result together with one of the accompanied source files.
lxdrgen-go.awk::
LibertyXDR backend for Go, supporting _encoding/json_ interfaces. It also
produces optimized JSON marshallers (however, note that the _json.Marshaler_
@@ -53,6 +61,9 @@ lxdrgen-mjs.awk::
LibertyXDR backend for Javascript, currently for decoding only.
It cuts a corner by not using BigInts, on par with `JSON.parse()`.
lxdrgen-swift.awk::
LibertyXDR backend for the Swift programming language.
Contributing and Support
------------------------
Use https://git.janouch.name/p/liberty to report any bugs, request features,

View File

@@ -9,15 +9,15 @@ find_package (Threads)
function (add_threads target)
if (NOT Threads_FOUND OR NOT CMAKE_USE_PTHREADS_INIT)
message (FATAL_ERROR "pthreads not found")
endif (NOT Threads_FOUND OR NOT CMAKE_USE_PTHREADS_INIT)
endif ()
if (THREADS_HAVE_PTHREAD_ARG)
set_property (TARGET ${target} PROPERTY
COMPILE_OPTIONS "-pthread")
set_property (TARGET ${target} PROPERTY
INTERFACE_COMPILE_OPTIONS "-pthread")
endif (THREADS_HAVE_PTHREAD_ARG)
endif ()
if (CMAKE_THREAD_LIBS_INIT)
target_link_libraries (${target} "${CMAKE_THREAD_LIBS_INIT}")
endif (CMAKE_THREAD_LIBS_INIT)
endfunction (add_threads)
endif ()
endfunction ()

View File

@@ -7,7 +7,7 @@ pkg_check_modules (Ncursesw QUIET ncursesw)
set (required_vars Ncursesw_LIBRARIES)
if (NOT Ncursesw_FOUND)
find_library (Ncursesw_LIBRARIES NAMES ncursesw)
find_path (Ncursesw_INCLUDE_DIRS ncurses.h)
find_path (Ncursesw_INCLUDE_DIRS ncurses.h PATH_SUFFIXES ncurses)
list (APPEND required_vars Ncursesw_INCLUDE_DIRS)
endif (NOT Ncursesw_FOUND)

84
cmake/IconUtils.cmake Normal file
View File

@@ -0,0 +1,84 @@
# Public Domain
function (icon_to_png name svg size output_dir output)
set (_dimensions "${size}x${size}")
set (_png_path "${output_dir}/hicolor/${_dimensions}/apps")
set (_png "${_png_path}/${name}.png")
set (${output} "${_png}" PARENT_SCOPE)
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (rsvg_convert_EXECUTABLE rsvg-convert ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${_png}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_png_path}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png}"
"--width=${size}" "--height=${size}" -- "${svg}"
DEPENDS "${svg}"
COMMENT "Generating ${name} ${_dimensions} application icon" VERBATIM)
endfunction ()
# You should include a 256x256 icon--which takes less space as raw PNG.
function (icon_for_win32 ico pngs pngs_raw)
set (_raws)
foreach (png ${pngs_raw})
list (APPEND _raws "--raw=${png}")
endforeach ()
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (icotool_EXECUTABLE icotool ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${ico}"
COMMAND ${icotool_EXECUTABLE} -c -o "${ico}" ${_raws} -- ${pngs}
DEPENDS ${pngs} ${pngs_raw}
COMMENT "Generating Windows program icon" VERBATIM)
endfunction ()
function (icon_to_iconset_size name svg size iconset outputs)
math (EXPR _size2x "${size} * 2")
set (_dimensions "${size}x${size}")
set (_png1x "${iconset}/icon_${_dimensions}.png")
set (_png2x "${iconset}/icon_${_dimensions}@2x.png")
set (${outputs} "${_png1x};${_png2x}" PARENT_SCOPE)
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (rsvg_convert_EXECUTABLE rsvg-convert ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${_png1x}" "${_png2x}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${iconset}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png1x}"
"--width=${size}" "--height=${size}" -- "${svg}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png2x}"
"--width=${_size2x}" "--height=${_size2x}" -- "${svg}"
DEPENDS "${svg}"
COMMENT "Generating ${name} ${_dimensions} icons" VERBATIM)
endfunction ()
function (icon_to_icns svg output_basename output)
get_filename_component (_name "${output_basename}" NAME_WE)
set (_iconset "${PROJECT_BINARY_DIR}/${_name}.iconset")
set (_icon "${PROJECT_BINARY_DIR}/${output_basename}")
set (${output} "${_icon}" PARENT_SCOPE)
set (_icon_png_list)
foreach (_icon_size 16 32 128 256 512)
icon_to_iconset_size ("${_name}" "${svg}"
"${_icon_size}" "${_iconset}" _icon_pngs)
list (APPEND _icon_png_list ${_icon_pngs})
endforeach ()
# XXX: This will not normally work from within Nix.
add_custom_command (OUTPUT "${_icon}"
COMMAND iconutil -c icns -o "${_icon}" "${_iconset}"
DEPENDS ${_icon_png_list}
COMMENT "Generating ${_name} icon" VERBATIM)
set_source_files_properties ("${_icon}" PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
endfunction ()

View File

@@ -0,0 +1,15 @@
set (CMAKE_SYSTEM_NAME "Windows")
set (CMAKE_SYSTEM_PROCESSOR "x86_64")
set (CMAKE_C_COMPILER "x86_64-w64-mingw32-gcc")
set (CMAKE_CXX_COMPILER "x86_64-w64-mingw32-g++")
set (CMAKE_RC_COMPILER "x86_64-w64-mingw32-windres")
# Remember to set WINEPATH for library dependencies
set (CMAKE_CROSSCOMPILING_EMULATOR "wine64")
set (CMAKE_FIND_ROOT_PATH "/usr/x86_64-w64-mingw32")
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

View File

@@ -0,0 +1,15 @@
set (CMAKE_SYSTEM_NAME "Windows")
set (CMAKE_SYSTEM_PROCESSOR "x86")
set (CMAKE_C_COMPILER "i686-w64-mingw32-gcc")
set (CMAKE_CXX_COMPILER "i686-w64-mingw32-g++")
set (CMAKE_RC_COMPILER "i686-w64-mingw32-windres")
# Remember to set WINEPATH for library dependencies
set (CMAKE_CROSSCOMPILING_EMULATOR "wine")
set (CMAKE_FIND_ROOT_PATH "/usr/i686-w64-mingw32")
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

View File

@@ -1381,7 +1381,7 @@ struct mpd_client
// Protocol:
bool got_hello; ///< Got the OK MPD hello message
char *got_hello; ///< Version from OK MPD hello message
bool idling; ///< Sent idle as the last command
unsigned idling_subsystems; ///< Subsystems we're idling for
@@ -1482,7 +1482,7 @@ mpd_client_reset (struct mpd_client *self)
str_reset (&self->read_buffer);
str_reset (&self->write_buffer);
self->got_hello = false;
cstr_set (&self->got_hello, NULL);
self->idling = false;
self->idling_subsystems = 0;
self->in_list = false;
@@ -1549,7 +1549,8 @@ mpd_client_parse_hello (struct mpd_client *self, const char *line)
// TODO: call "on_connected" now. We should however also set up a timer
// so that we don't wait on this message forever.
return self->got_hello = true;
cstr_set (&self->got_hello, xstrdup (line + sizeof hello - 1));
return true;
}
static bool
@@ -1634,30 +1635,30 @@ mpd_client_on_ready (const struct pollfd *pfd, void *user_data)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
mpd_client_must_quote_char (char c)
{
return (unsigned char) c <= ' ' || c == '"' || c == '\'';
}
static bool
mpd_client_must_quote (const char *s)
{
if (!*s)
return true;
for (; *s; s++)
if (mpd_client_must_quote_char (*s))
if ((unsigned char) *s <= ' ' || *s == '"' || *s == '\'')
return true;
return false;
}
static bool
mpd_client_must_escape_in_quote (char c)
{
return c == '"' || c == '\'' || c == '\\';
}
static void
mpd_client_quote (const char *s, struct str *output)
{
str_append_c (output, '"');
for (; *s; s++)
{
if (mpd_client_must_quote_char (*s))
if (mpd_client_must_escape_in_quote (*s))
str_append_c (output, '\\');
str_append_c (output, *s);
}
@@ -1911,14 +1912,14 @@ mpd_client_connect_unix (struct mpd_client *self, const char *address,
// Expand tilde if needed
char *expanded = resolve_filename (address, xstrdup);
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strncpy (sun.sun_path, expanded, sizeof sun.sun_path);
sun.sun_path[sizeof sun.sun_path - 1] = 0;
struct sockaddr_un sau;
sau.sun_family = AF_UNIX;
strncpy (sau.sun_path, expanded, sizeof sau.sun_path);
sau.sun_path[sizeof sau.sun_path - 1] = 0;
free (expanded);
if (connect (fd, (struct sockaddr *) &sun, sizeof sun))
if (connect (fd, (struct sockaddr *) &sau, sizeof sau))
{
error_set (e, "%s: %s", "connect", strerror (errno));
xclose (fd);

View File

@@ -1,270 +0,0 @@
/*
* liberty-tui.c: the ultimate C unlibrary: TUI
*
* Copyright (c) 2016 - 2017, 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.
*
*/
// This file includes some common stuff to build TUI applications with
#include <ncurses.h>
// It is surprisingly hard to find a good library to handle Unicode shenanigans,
// and there's enough of those for it to be impractical to reimplement them.
//
// GLib ICU libunistring utf8proc
// Decently sized . . x x
// Grapheme breaks . x . x
// Character width x . x x
// Locale handling . . x .
// Liberal license . x . x
//
// Also note that the ICU API is icky and uses UTF-16 for its primary encoding.
//
// Currently we're chugging along with libunistring but utf8proc seems viable.
// Non-Unicode locales can mostly be handled with simple iconv like in sdtui.
// Similarly grapheme breaks can be guessed at using character width (a basic
// test here is Zalgo text).
//
// None of this is ever going to work too reliably anyway because terminals
// and Unicode don't go awfully well together. In particular, character cell
// devices have some problems with double-wide characters.
#include <unistr.h>
#include <uniwidth.h>
#include <uniconv.h>
#include <unicase.h>
// --- Configurable display attributes -----------------------------------------
struct attrs
{
short fg; ///< Foreground colour index
short bg; ///< Background colour index
chtype attrs; ///< Other attributes
};
/// Decode attributes in the value using a subset of the git config format,
/// ignoring all errors since it doesn't affect functionality
static struct attrs
attrs_decode (const char *value)
{
struct strv v = strv_make ();
cstr_split (value, " ", true, &v);
int colors = 0;
struct attrs attrs = { -1, -1, 0 };
for (char **it = v.vector; *it; it++)
{
char *end = NULL;
long n = strtol (*it, &end, 10);
if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
{
if (colors == 0) attrs.fg = n;
if (colors == 1) attrs.bg = n;
colors++;
}
else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD;
else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM;
else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE;
else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK;
else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE;
#ifdef A_ITALIC
else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC;
#endif // A_ITALIC
}
strv_free (&v);
return attrs;
}
// --- Terminal output ---------------------------------------------------------
// Necessary abstraction to simplify aligned, formatted character output
// This callback you need to implement in the application
static bool app_is_character_in_locale (ucs4_t ch);
struct row_char
{
ucs4_t c; ///< Unicode codepoint
chtype attrs; ///< Special attributes
int width; ///< How many cells this takes
};
struct row_buffer
{
ARRAY (struct row_char, chars) ///< Characters
int total_width; ///< Total width of all characters
};
static struct row_buffer
row_buffer_make (void)
{
struct row_buffer self = {};
ARRAY_INIT_SIZED (self.chars, 256);
return self;
}
static void
row_buffer_free (struct row_buffer *self)
{
free (self->chars);
}
/// Replace invalid chars and push all codepoints to the array w/ attributes.
static void
row_buffer_append (struct row_buffer *self, const char *str, chtype attrs)
{
// The encoding is only really used internally for some corner cases
const char *encoding = locale_charset ();
// Note that this function is a hotspot, try to keep it decently fast
struct row_char current = { .attrs = attrs };
struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
const uint8_t *next = (const uint8_t *) str;
while ((next = u8_next (&current.c, next)))
{
current.width = uc_width (current.c, encoding);
if (current.width < 0 || !app_is_character_in_locale (current.c))
current = invalid;
ARRAY_RESERVE (self->chars, 1);
self->chars[self->chars_len++] = current;
self->total_width += current.width;
}
}
static void
row_buffer_append_args (struct row_buffer *self, const char *s, ...)
ATTRIBUTE_SENTINEL;
static void
row_buffer_append_args (struct row_buffer *self, const char *s, ...)
{
va_list ap;
va_start (ap, s);
while (s)
{
row_buffer_append (self, s, va_arg (ap, chtype));
s = va_arg (ap, const char *);
}
va_end (ap);
}
static void
row_buffer_append_buffer (struct row_buffer *self, const struct row_buffer *rb)
{
ARRAY_RESERVE (self->chars, rb->chars_len);
memcpy (self->chars + self->chars_len, rb->chars,
rb->chars_len * sizeof *rb->chars);
self->chars_len += rb->chars_len;
self->total_width += rb->total_width;
}
/// Pop as many codepoints as needed to free up "space" character cells.
/// Given the suffix nature of combining marks, this should work pretty fine.
static int
row_buffer_pop_cells (struct row_buffer *self, int space)
{
int made = 0;
while (self->chars_len && made < space)
made += self->chars[--self->chars_len].width;
self->total_width -= made;
return made;
}
static void
row_buffer_space (struct row_buffer *self, int width, chtype attrs)
{
if (width < 0)
return;
ARRAY_RESERVE (self->chars, (size_t) width);
struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 };
self->total_width += width;
while (width-- > 0)
self->chars[self->chars_len++] = space;
}
static void
row_buffer_ellipsis (struct row_buffer *self, int target)
{
if (self->total_width <= target
|| !row_buffer_pop_cells (self, self->total_width - target))
return;
// We use attributes from the last character we've removed,
// assuming that we don't shrink the array (and there's no real need)
ucs4_t ellipsis = 0x2026; // …
if (app_is_character_in_locale (ellipsis))
{
if (self->total_width >= target)
row_buffer_pop_cells (self, 1);
if (self->total_width + 1 <= target)
row_buffer_append (self, "", self->chars[self->chars_len].attrs);
}
else if (target >= 3)
{
if (self->total_width >= target)
row_buffer_pop_cells (self, 3);
if (self->total_width + 3 <= target)
row_buffer_append (self, "...", self->chars[self->chars_len].attrs);
}
}
static void
row_buffer_align (struct row_buffer *self, int target, chtype attrs)
{
row_buffer_ellipsis (self, target);
row_buffer_space (self, target - self->total_width, attrs);
}
static void
row_buffer_print (uint32_t *ucs4, chtype attrs)
{
// This assumes that we can reset the attribute set without consequences
char *str = u32_strconv_to_locale (ucs4);
if (str)
{
attrset (attrs);
addstr (str);
attrset (0);
free (str);
}
}
static void
row_buffer_flush (struct row_buffer *self)
{
if (!self->chars_len)
return;
// We only NUL-terminate the chunks because of the libunistring API
uint32_t chunk[self->chars_len + 1], *insertion_point = chunk;
for (size_t i = 0; i < self->chars_len; i++)
{
struct row_char *iter = self->chars + i;
if (i && iter[0].attrs != iter[-1].attrs)
{
row_buffer_print (chunk, iter[-1].attrs);
insertion_point = chunk;
}
*insertion_point++ = iter->c;
*insertion_point = 0;
}
row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs);
}

772
liberty-xdg.c Normal file
View File

@@ -0,0 +1,772 @@
/*
* liberty-xdg.c: the ultimate C unlibrary: freedesktop.org specifications
*
* Copyright (c) 2023 - 2024, 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.
*
*/
// This files assumes you've already included liberty.c.
#ifdef LIBERTY_XDG_WANT_X11
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif
#ifdef LIBERTY_XDG_WANT_ICONS
#include <png.h>
#endif
// --- XSettings ---------------------------------------------------------------
#ifdef LIBERTY_XDG_WANT_X11
struct xdg_xsettings_setting
{
enum xdg_xsettings_type
{
XDG_XSETTINGS_INTEGER,
XDG_XSETTINGS_STRING,
XDG_XSETTINGS_COLOR,
}
type; ///< What's stored in the union
uint32_t serial; ///< Serial of the last change
union
{
int32_t integer;
struct str string;
struct { uint16_t red, green, blue, alpha; } color;
};
};
static void
xdg_xsettings_setting_destroy (struct xdg_xsettings_setting *self)
{
if (self->type == XDG_XSETTINGS_STRING)
str_free (&self->string);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct xdg_xsettings
{
struct str_map settings; ///< Name -> xdg_xsettings_setting
};
static void
xdg_xsettings_free (struct xdg_xsettings *self)
{
str_map_free (&self->settings);
}
static struct xdg_xsettings
xdg_xsettings_make (void)
{
return (struct xdg_xsettings)
{
.settings =
str_map_make ((str_map_free_fn) xdg_xsettings_setting_destroy),
};
}
static void
xdg_xsettings_update (struct xdg_xsettings *self, Display *dpy)
{
// TODO: We're supposed to lock the server.
// TODO: We're supposed to trap X errors.
char *selection = xstrdup_printf ("_XSETTINGS_S%d", DefaultScreen (dpy));
Window owner
= XGetSelectionOwner (dpy, XInternAtom (dpy, selection, True));
free (selection);
if (!owner)
return;
Atom actual_type = None;
int actual_format = 0;
unsigned long nitems = 0, bytes_after = 0;
unsigned char *buffer = NULL;
Atom xsettings = XInternAtom (dpy, "_XSETTINGS_SETTINGS", True);
int status = XGetWindowProperty (dpy,
owner,
xsettings,
0L,
LONG_MAX,
False,
xsettings,
&actual_type,
&actual_format,
&nitems,
&bytes_after,
&buffer);
if (status != Success || !buffer)
return;
if (actual_type != xsettings
|| actual_format != 8
|| nitems < 12)
goto fail;
const struct peeker *peeker = NULL;
if (buffer[0] == LSBFirst)
peeker = &peeker_le;
else if (buffer[0] == MSBFirst)
peeker = &peeker_be;
else
goto fail;
// We're ignoring the serial for now.
uint32_t n_settings = peeker->u32 (buffer + 8);
size_t offset = 12;
struct str name = str_make ();
struct xdg_xsettings_setting *setting = xcalloc (1, sizeof *setting);
while (n_settings--)
{
if (nitems < offset + 4)
goto fail_item;
setting->type = buffer[offset];
uint16_t name_len = peeker->u16 (buffer + offset + 2);
offset += 4;
if (nitems < offset + name_len)
goto fail_item;
str_append_data (&name, buffer + offset, name_len);
offset += ((name_len + 3) & ~3);
if (nitems < offset + 4)
goto fail_item;
setting->serial = peeker->u32 (buffer + offset);
offset += 4;
switch (setting->type)
{
case XDG_XSETTINGS_INTEGER:
if (nitems < offset + 4)
goto fail_item;
setting->integer = (int32_t) peeker->u32 (buffer + offset);
offset += 4;
break;
case XDG_XSETTINGS_STRING:
{
if (nitems < offset + 4)
goto fail_item;
uint32_t value_len = peeker->u32 (buffer + offset);
offset += 4;
if (nitems < offset + value_len)
goto fail_item;
setting->string = str_make ();
str_append_data (&setting->string, buffer + offset, value_len);
offset += ((value_len + 3) & ~3);
break;
}
case XDG_XSETTINGS_COLOR:
if (nitems < offset + 8)
goto fail_item;
setting->color.red = peeker->u16 (buffer + offset);
setting->color.green = peeker->u16 (buffer + offset + 2);
setting->color.blue = peeker->u16 (buffer + offset + 4);
setting->color.alpha = peeker->u16 (buffer + offset + 6);
offset += 8;
break;
default:
goto fail_item;
}
// TODO(p): Change detection, by comparing existence and serials.
str_map_set (&self->settings, name.str, setting);
setting = xcalloc (1, sizeof *setting);
str_reset (&name);
}
fail_item:
xdg_xsettings_setting_destroy (setting);
str_free (&name);
fail:
XFree (buffer);
}
#endif // LIBERTY_XDG_WANT_X11
// --- Desktop file parser -----------------------------------------------------
// Useful for parsing desktop-entry-spec, icon-theme-spec, trash-spec,
// mime-apps-spec. This code is not designed for making changes to the files.
struct desktop_file
{
struct str_map groups; ///< Group name → Key → Value
};
static void
desktop_file_free_group (void *value)
{
str_map_free (value);
free (value);
}
static void
desktop_file_free (struct desktop_file *self)
{
str_map_free (&self->groups);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
desktop_file_parse_line (struct desktop_file *self,
char **group_name, const char *line, const char *end)
{
struct str_map *group = NULL;
if (*group_name)
group = str_map_find (&self->groups, *group_name);
if (*line == '[')
{
bool ok = *--end == ']';
for (const char *p = ++line; ok && p != end; p++)
ok = (unsigned char) *p >= 32 && (unsigned char) *p <= 127
&& *p != '[' && *p != ']';
if (!ok)
{
cstr_set (group_name, NULL);
print_debug ("invalid desktop file group header");
return;
}
cstr_set (group_name, xstrndup (line, end - line));
if (str_map_find (&self->groups, *group_name))
{
print_debug ("duplicate desktop file group: %s", *group_name);
return;
}
group = xcalloc (1, sizeof *group);
*group = str_map_make (free);
str_map_set (&self->groups, *group_name, group);
return;
}
if (!group)
{
print_debug ("unexpected desktop file entry outside of a group");
return;
}
const char *key_end = line;
while (key_end != end && (isalnum_ascii (*key_end) || *key_end == '-'))
key_end++;
// We could validate these further, but we just search in them anyway.
if (key_end != end && *key_end == '[')
{
while (++key_end != end && *key_end != ']')
;
if (key_end != end && *key_end == ']')
key_end++;
}
const char *value = key_end;
while (value != end && *value == ' ')
value++;
if (value == end || *value++ != '=')
{
print_debug ("invalid desktop file entry");
return;
}
while (value != end && *value == ' ')
value++;
char *key = xstrndup (line, key_end - line);
if (str_map_find (group, key))
print_debug ("duplicate desktop file entry for: %s", key);
else
str_map_set (group, key, xstrndup (value, end - value));
free (key);
}
static struct desktop_file
desktop_file_make (const char *data, size_t len)
{
struct desktop_file self = (struct desktop_file)
{ .groups = str_map_make (desktop_file_free_group) };
char *group_name = NULL;
const char *p = data, *data_end = p + len;
while (p != data_end)
{
const char *line = p, *line_end = line;
while (line_end != data_end && *line_end != '\n')
line_end++;
if ((p = line_end) != data_end && *p == '\n')
p++;
if (line != line_end && *line != '#')
desktop_file_parse_line (&self, &group_name, line, line_end);
}
free (group_name);
return self;
}
static const char *
desktop_file_get (struct desktop_file *self, const char *group, const char *key)
{
// TODO(p): Ideally, also implement localised keys.
struct str_map *group_map = str_map_find (&self->groups, group);
if (!group_map)
return NULL;
return str_map_find (group_map, key);
}
static struct strv
desktop_file_unescape (const char *value, bool is_list)
{
struct strv result = strv_make ();
struct str s = str_make ();
// XXX: The unescaping behaviour is underspecified.
// It might make sense to warn about unrecognised escape sequences.
bool escape = false;
for (const char *p = value; *p; p++)
{
if (escape)
{
switch (*p)
{
break; case 's': str_append_c (&s, ' ');
break; case 'n': str_append_c (&s, '\n');
break; case 't': str_append_c (&s, '\t');
break; case 'r': str_append_c (&s, '\r');
break; default: str_append_c (&s, *p);
}
escape = false;
}
else if (*p == '\\' && p[1])
escape = true;
else if (*p == ';' && is_list)
{
strv_append_owned (&result, str_steal (&s));
s = str_make ();
}
else
str_append_c (&s, *p);
}
if (!is_list || s.len != 0)
strv_append_owned (&result, str_steal (&s));
else
str_free (&s);
return result;
}
static char *
desktop_file_get_string (struct desktop_file *self,
const char *group, const char *key)
{
const char *value = desktop_file_get (self, group, key);
if (!value)
return NULL;
struct strv values = desktop_file_unescape (value, false /* is_list */);
char *unescaped = strv_steal (&values, 0);
strv_free (&values);
return unescaped;
}
static struct strv
desktop_file_get_stringv (struct desktop_file *self,
const char *group, const char *key)
{
const char *value = desktop_file_get (self, group, key);
if (!value)
return strv_make ();
return desktop_file_unescape (value, true /* is_list */);
}
static bool
desktop_file_get_bool (struct desktop_file *self,
const char *group, const char *key)
{
const char *value = desktop_file_get (self, group, key);
if (!value)
return false;
// Let's be compatible with pre-1.0 files when it costs us so little.
if (!strcmp (value, "true")
|| !strcmp (value, "1"))
return true;
if (!strcmp (value, "false")
|| !strcmp (value, "0"))
return false;
print_debug ("invalid desktop file boolean for '%s': %s", key, value);
return false;
}
// Nothing uses the "numeric" type.
// "icon-theme-spec" uses "integer" and doesn't say what it is.
static long
desktop_file_get_integer (struct desktop_file *self,
const char *group, const char *key)
{
const char *value = desktop_file_get (self, group, key);
if (!value)
return 0;
char *end = NULL;
long parsed = (errno = 0, strtol (value, &end, 10));
if (errno != 0 || *end)
print_debug ("invalid desktop file integer for '%s': %s", key, value);
return parsed;
}
// --- Icon themes -------------------------------------------------------------
// This implements part of the Icon Theme Specification.
#ifdef LIBERTY_XDG_WANT_ICONS
struct icon_theme_icon
{
uint32_t width; ///< Width of argb in pixels
uint32_t height; ///< Height of argb in pixels
uint32_t argb[]; ///< ARGB32 data, unassociated alpha
};
static void
icon_theme_open_on_error (png_structp pngp, const char *error)
{
print_debug ("%s: %s", (const char *) png_get_error_ptr (pngp), error);
png_longjmp (pngp, 1);
}
static void
icon_theme_open_on_warning (png_structp pngp, const char *warning)
{
(void) pngp;
(void) warning;
// Fuck your "gamma value does not match libpng estimate".
}
// For simplicity, only support PNG icons, using the most popular library.
static struct icon_theme_icon *
icon_theme_open (const char *path)
{
volatile png_bytep buffer = NULL;
volatile png_bytepp row_pointers = NULL;
struct icon_theme_icon *volatile result = NULL;
FILE *fp = fopen (path, "rb");
if (!fp)
{
if (errno != ENOENT)
print_debug ("%s: %s", path, strerror (errno));
return NULL;
}
// The simplified and high-level APIs aren't powerful enough.
png_structp pngp = png_create_read_struct (PNG_LIBPNG_VER_STRING,
(png_voidp) path, icon_theme_open_on_error, icon_theme_open_on_warning);
png_infop infop = png_create_info_struct (pngp);
if (!infop)
{
print_debug ("%s: %s", path, strerror (errno));
goto fail;
}
if (setjmp (png_jmpbuf (pngp)))
goto fail;
png_init_io (pngp, fp);
png_read_info (pngp, infop);
// Asking for at least 8-bit channels. This call is a superset of:
// - png_set_palette_to_rgb(),
// - png_set_tRNS_to_alpha(),
// - png_set_expand_gray_1_2_4_to_8().
png_set_expand (pngp);
// Reduce the possibilities further to RGB or RGBA...
png_set_gray_to_rgb (pngp);
// ...and /exactly/ 8-bit channels.
// Alternatively, use png_set_expand_16() above to obtain 16-bit channels.
png_set_scale_16 (pngp);
// PNG uses RGBA order, let's change that to ARGB (both in memory order).
// This doesn't change a row's `color_type` in png_do_read_filler(),
// and the following transformation thus ignores it.
png_set_add_alpha (pngp, 0xFFFF, PNG_FILLER_BEFORE);
png_set_swap_alpha (pngp);
(void) png_set_interlace_handling (pngp);
png_read_update_info (pngp, infop);
if (png_get_bit_depth (pngp, infop) != 8
|| png_get_channels (pngp, infop) != 4
|| png_get_color_type (pngp, infop) != PNG_COLOR_TYPE_RGB_ALPHA)
png_error (pngp, "result not A8R8G8B8");
size_t row_bytes = png_get_rowbytes (pngp, infop);
size_t height = png_get_image_height (pngp, infop);
buffer = xcalloc (row_bytes, height);
row_pointers = xcalloc (height, sizeof buffer);
for (size_t y = 0; y < height; y++)
row_pointers[y] = buffer + y * row_bytes;
png_read_image (pngp, row_pointers);
result = xcalloc (1, sizeof *result + row_bytes * height);
result->width = png_get_image_width (pngp, infop);
result->height = height;
uint32_t *dst = (uint32_t *) result->argb, *src = (uint32_t *) buffer;
for (size_t pixels = result->width * result->height; pixels--; )
*dst++ = ntohl (*src++);
fail:
free (buffer);
free (row_pointers);
png_destroy_read_struct (&pngp, &infop, NULL);
fclose (fp);
return result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct icon_theme_find_context
{
struct strv base; ///< Base directories
struct str_map visited; ///< Cycle prevention
ARRAY (struct icon_theme_icon *, icons)
};
static void
icon_theme_find__fallback (struct icon_theme_find_context *ctx,
const char *name)
{
for (size_t i = 0; i < ctx->base.len; i++)
{
char *path = xstrdup_printf ("%s/%s.png", ctx->base.vector[i], name);
struct icon_theme_icon *icon = icon_theme_open (path);
free (path);
if (icon)
{
ARRAY_RESERVE (ctx->icons, 1);
ctx->icons[ctx->icons_len++] = icon;
return;
}
}
}
static struct desktop_file
icon_theme_find__index (struct icon_theme_find_context *ctx, const char *theme)
{
struct str data = str_make ();
for (size_t i = 0; i < ctx->base.len; i++)
{
struct error *e = NULL;
char *path = xstrdup_printf ("%s/%s/index.theme",
ctx->base.vector[i], theme);
read_file (path, &data, &e);
free (path);
if (!e)
break;
if (errno != ENOENT)
print_debug ("%s", e->message);
error_free (e);
}
struct desktop_file index = desktop_file_make (data.str, data.len);
str_free (&data);
return index;
}
static void
icon_theme_find__named (struct icon_theme_find_context *ctx,
const char *theme, const char *name)
{
// Either a cycle, or a common ancestor of inherited themes, which is valid.
if (str_map_find (&ctx->visited, theme))
return;
str_map_set (&ctx->visited, theme, (void *) (intptr_t) 1);
struct desktop_file index = icon_theme_find__index (ctx, theme);
char *directories =
desktop_file_get_string (&index, "Icon Theme", "Directories");
if (!directories)
goto out;
// NOTE: The sizes are not deduplicated, and priorities are uncertain.
struct strv dirs = strv_make ();
cstr_split (directories, ",", true, &dirs);
free (directories);
for (size_t d = 0; d < dirs.len; d++)
{
// The hicolor icon theme stuffs everything in Directories.
if (desktop_file_get (&index, dirs.vector[d], "Scale")
&& desktop_file_get_integer (&index, dirs.vector[d], "Scale") != 1)
continue;
for (size_t i = 0; i < ctx->base.len; i++)
{
char *path = xstrdup_printf ("%s/%s/%s/%s.png",
ctx->base.vector[i], theme, dirs.vector[d], name);
struct icon_theme_icon *icon = icon_theme_open (path);
free (path);
if (icon)
{
ARRAY_RESERVE (ctx->icons, 1);
ctx->icons[ctx->icons_len++] = icon;
break;
}
}
}
strv_free (&dirs);
if (ctx->icons_len)
goto out;
char *inherits =
desktop_file_get_string (&index, "Icon Theme", "Inherits");
if (inherits)
{
struct strv parents = strv_make ();
cstr_split (inherits, ",", true, &parents);
free (inherits);
for (size_t i = 0; i < parents.len; i++)
{
icon_theme_find__named (ctx, parents.vector[i], name);
if (ctx->icons_len)
break;
}
strv_free (&parents);
}
out:
desktop_file_free (&index);
}
/// Return all base directories appropriate for icon search.
static struct strv
icon_theme_get_base_directories (void)
{
struct strv dirs = strv_make ();
struct str icons = str_make ();
(void) str_append_env_path (&icons, "HOME", false);
str_append (&icons, "/.icons");
strv_append_owned (&dirs, str_steal (&icons));
// Note that we use XDG_CONFIG_HOME as well, which might be intended.
struct strv xdg = strv_make ();
get_xdg_data_dirs (&xdg);
for (size_t i = 0; i < xdg.len; i++)
strv_append_owned (&dirs, xstrdup_printf ("%s/icons", xdg.vector[i]));
strv_free (&xdg);
strv_append (&dirs, "/usr/share/pixmaps");
return dirs;
}
static int
icon_theme_find__compare (const void *a, const void *b)
{
const struct icon_theme_icon **ia = (const struct icon_theme_icon **) a;
const struct icon_theme_icon **ib = (const struct icon_theme_icon **) b;
double pa = (double) (*ia)->width * (*ia)->height;
double pb = (double) (*ib)->width * (*ib)->height;
return (pa > pb) - (pa < pb);
}
/// Return all sizes of the named icon. When the theme name is not NULL,
/// use it as the preferred theme. Always consult fallbacks locations.
/// Ignore icon scales other than 1.
static struct icon_theme_icon **
icon_theme_find (const char *theme, const char *name, size_t *len)
{
struct icon_theme_find_context ctx = {};
ctx.base = icon_theme_get_base_directories ();
ctx.visited = str_map_make (NULL);
ARRAY_INIT (ctx.icons);
if (theme)
icon_theme_find__named (&ctx, theme, name);
if (!ctx.icons_len)
icon_theme_find__named (&ctx, "hicolor", name);
if (!ctx.icons_len)
icon_theme_find__fallback (&ctx, name);
strv_free (&ctx.base);
str_map_free (&ctx.visited);
ARRAY_RESERVE (ctx.icons, 1);
ctx.icons[ctx.icons_len] = NULL;
if (!ctx.icons_len)
{
free (ctx.icons);
return NULL;
}
qsort (ctx.icons,
ctx.icons_len, sizeof *ctx.icons, icon_theme_find__compare);
*len = ctx.icons_len;
return ctx.icons;
}
static void
icon_theme_free (struct icon_theme_icon **icons)
{
for (struct icon_theme_icon **p = icons; *p; p++)
free (*p);
free (icons);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ifdef LIBERTY_XDG_WANT_X11
static void
icon_theme_set_window_icon (Display *dpy,
Window window, const char *theme, const char *name)
{
size_t icons_len = 0;
struct icon_theme_icon **icons = icon_theme_find (theme, name, &icons_len);
if (!icons)
return;
size_t n = 0;
for (size_t i = 0; i < icons_len; i++)
n += 2 + icons[i]->width * icons[i]->height;
unsigned long *data = xcalloc (n, sizeof *data), *p = data;
for (size_t i = 0; i < icons_len; i++)
{
*p++ = icons[i]->width;
*p++ = icons[i]->height;
uint32_t *q = icons[i]->argb;
for (size_t k = icons[i]->width * icons[i]->height; k--; )
*p++ = *q++;
}
XChangeProperty (dpy, window, XInternAtom (dpy, "_NET_WM_ICON", False),
XA_CARDINAL, 32, PropModeReplace, (const unsigned char *) data, n);
free (data);
icon_theme_free (icons);
}
#endif // LIBERTY_XDG_WANT_X11
#endif // LIBERTY_XDG_WANT_ICONS

2237
liberty-xui.c Normal file

File diff suppressed because it is too large Load Diff

100
liberty.c
View File

@@ -1,7 +1,7 @@
/*
* liberty.c: the ultimate C unlibrary
*
* Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2014 - 2024, 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.
@@ -292,7 +292,8 @@ xreallocarray (void *o, size_t n, size_t m)
static char *
xstrdup (const char *s)
{
return strcpy (xmalloc (strlen (s) + 1), s);
size_t len = strlen (s) + 1;
return memcpy (xmalloc (len), s, len);
}
static char *
@@ -315,7 +316,7 @@ xstrndup (const char *s, size_t n)
#define ARRAY(type, name) type *name; size_t name ## _len, name ## _alloc;
#define ARRAY_INIT_SIZED(a, n) \
BLOCK_START \
(a) = xcalloc (sizeof *(a), (a ## _alloc) = (n)); \
(a) = xcalloc ((a ## _alloc) = (n), sizeof *(a)); \
(a ## _len) = 0; \
BLOCK_END
#define ARRAY_INIT(a) ARRAY_INIT_SIZED (a, 16)
@@ -397,7 +398,7 @@ strv_make (void)
struct strv self;
self.alloc = 4;
self.len = 0;
self.vector = xcalloc (sizeof *self.vector, self.alloc);
self.vector = xcalloc (self.alloc, sizeof *self.vector);
return self;
}
@@ -652,6 +653,60 @@ str_pack_u64 (struct str *self, uint64_t x)
#define str_pack_i32(self, x) str_pack_u32 ((self), (uint32_t) (x))
#define str_pack_i64(self, x) str_pack_u64 ((self), (uint64_t) (x))
// --- Reading binary numbers --------------------------------------------------
// Doing this byte by byte prevents unaligned memory access issues.
static uint64_t
peek_u64be (const uint8_t *p)
{
return (uint64_t) p[0] << 56 | (uint64_t) p[1] << 48
| (uint64_t) p[2] << 40 | (uint64_t) p[3] << 32
| (uint64_t) p[4] << 24 | (uint64_t) p[5] << 16 | p[6] << 8 | p[7];
}
static uint32_t
peek_u32be (const uint8_t *p)
{
return (uint32_t) p[0] << 24 | (uint32_t) p[1] << 16 | p[2] << 8 | p[3];
}
static uint16_t
peek_u16be (const uint8_t *p)
{
return (uint16_t) p[0] << 8 | p[1];
}
static uint64_t
peek_u64le (const uint8_t *p)
{
return (uint64_t) p[7] << 56 | (uint64_t) p[6] << 48
| (uint64_t) p[5] << 40 | (uint64_t) p[4] << 32
| (uint64_t) p[3] << 24 | (uint64_t) p[2] << 16 | p[1] << 8 | p[0];
}
static uint32_t
peek_u32le (const uint8_t *p)
{
return (uint32_t) p[3] << 24 | (uint32_t) p[2] << 16 | p[1] << 8 | p[0];
}
static uint16_t
peek_u16le (const uint8_t *p)
{
return (uint16_t) p[1] << 8 | p[0];
}
struct peeker
{
uint64_t (*u64) (const uint8_t *);
uint32_t (*u32) (const uint8_t *);
uint16_t (*u16) (const uint8_t *);
};
static const struct peeker peeker_be = {peek_u64be, peek_u32be, peek_u16be};
static const struct peeker peeker_le = {peek_u64le, peek_u32le, peek_u16le};
// --- Errors ------------------------------------------------------------------
// Error reporting utilities. Inspired by GError, only much simpler.
@@ -2503,12 +2558,7 @@ msg_reader_get (struct msg_reader *self, size_t *len)
return NULL;
uint8_t *x = (uint8_t *) self->buf.str + self->offset;
uint64_t msg_len
= (uint64_t) x[0] << 56 | (uint64_t) x[1] << 48
| (uint64_t) x[2] << 40 | (uint64_t) x[3] << 32
| (uint64_t) x[4] << 24 | (uint64_t) x[5] << 16
| (uint64_t) x[6] << 8 | (uint64_t) x[7];
uint64_t msg_len = peek_u64be (x);
if (msg_len < sizeof msg_len)
{
// The message is shorter than its header
@@ -2573,8 +2623,7 @@ static bool
msg_unpacker_u16 (struct msg_unpacker *self, uint16_t *value)
{
UNPACKER_INT_BEGIN
*value
= (uint16_t) x[0] << 8 | (uint16_t) x[1];
*value = peek_u16be (x);
return true;
}
@@ -2582,9 +2631,7 @@ static bool
msg_unpacker_u32 (struct msg_unpacker *self, uint32_t *value)
{
UNPACKER_INT_BEGIN
*value
= (uint32_t) x[0] << 24 | (uint32_t) x[1] << 16
| (uint32_t) x[2] << 8 | (uint32_t) x[3];
*value = peek_u32be (x);
return true;
}
@@ -2592,11 +2639,7 @@ static bool
msg_unpacker_u64 (struct msg_unpacker *self, uint64_t *value)
{
UNPACKER_INT_BEGIN
*value
= (uint64_t) x[0] << 56 | (uint64_t) x[1] << 48
| (uint64_t) x[2] << 40 | (uint64_t) x[3] << 32
| (uint64_t) x[4] << 24 | (uint64_t) x[5] << 16
| (uint64_t) x[6] << 8 | (uint64_t) x[7];
*value = peek_u64be (x);
return true;
}
@@ -3552,6 +3595,8 @@ write_file_safe (const char *filename, const void *data, size_t data_len,
{
// XXX: ideally we would also open the directory, use *at() versions
// of functions and call fsync() on the directory as appropriate
// FIXME: this should behave similarly to mkstemp(), just with 0666;
// as it is, this function is not particularly safe
char *temp = xstrdup_printf ("%s.new", filename);
bool success = write_file (temp, data, data_len, e);
if (success && !(success = !rename (temp, filename)))
@@ -4456,7 +4501,7 @@ struct config_item
}
value; ///< The value of this item
struct config_schema *schema; ///< Schema describing this value
const struct config_schema *schema; ///< Schema describing this value
void *user_data; ///< User value attached by schema owner
};
@@ -4608,7 +4653,7 @@ config_item_object (void)
static bool
config_schema_accepts_type
(struct config_schema *self, enum config_item_type type)
(const struct config_schema *self, enum config_item_type type)
{
if (self->type == type)
return true;
@@ -4621,7 +4666,7 @@ config_schema_accepts_type
static bool
config_item_validate_by_schema (struct config_item *self,
struct config_schema *schema, struct error **e)
const struct config_schema *schema, struct error **e)
{
struct error *error = NULL;
if (!config_schema_accepts_type (schema, self->type))
@@ -4642,7 +4687,7 @@ static bool
config_item_set_from (struct config_item *self, struct config_item *source,
struct error **e)
{
struct config_schema *schema = self->schema;
const struct config_schema *schema = self->schema;
if (!schema)
{
// Easy, we don't know what this item is
@@ -4788,7 +4833,8 @@ config_item_write_kv_pair (struct config_writer *self,
str_append_printf (self->output,
"%s# %s\n", indent, value->schema->comment);
bool can_use_word = true;
char *end = NULL;
bool can_use_word = ((void) strtoll (key, &end, 10), end == key);
for (const char *p = key; *p; p++)
if (!config_tokenizer_is_word_char (*p))
can_use_word = false;
@@ -5438,7 +5484,7 @@ end:
/// "user_data" is passed to allow its immediate use in validation callbacks
static struct config_item *
config_schema_initialize_item (struct config_schema *schema,
config_schema_initialize_item (const struct config_schema *schema,
struct config_item *parent, void *user_data, struct error **warning,
struct error **e)
{
@@ -5495,7 +5541,7 @@ keep_current:
/// Assign schemas and user_data to multiple items at once;
/// feel free to copy over and modify to suit your particular needs
static void
config_schema_apply_to_object (struct config_schema *schema_array,
config_schema_apply_to_object (const struct config_schema *schema_array,
struct config_item *object, void *user_data)
{
while (schema_array->name)

View File

@@ -9,7 +9,7 @@ LibertyXDR - an XDR-derived IDL and data serialization format
Description
-----------
*LibertyXDR* is an interface description language, as well as a data
serialization format, that has been largely derived from XDR, though notably
serialization format. It is largely derived from XDR, though notably
simplified.
Conventions

View File

@@ -32,13 +32,6 @@
#define LIBERTY_WANT_PROTO_MPD
#include "../liberty.c"
#include "../liberty-tui.c"
static bool
app_is_character_in_locale (ucs4_t ch)
{
return ch < 128;
}
// --- UTF-8 -------------------------------------------------------------------
@@ -211,19 +204,6 @@ test_config_item_parse (const uint8_t *data, size_t size)
config_item_destroy (item);
}
// --- TUI ---------------------------------------------------------------------
static void
test_attrs_decode (const uint8_t *data, size_t size)
{
struct str wrap = str_make ();
str_append_data (&wrap, data, size);
attrs_decode (wrap.str);
str_free (&wrap);
}
// --- MPD ---------------------------------------------------------------------
static void
@@ -266,7 +246,6 @@ LLVMFuzzerInitialize (int *argcp, char ***argvp)
REGISTER (fcgi_parser_push)
REGISTER (fcgi_nv_parser_push)
REGISTER (config_item_parse)
REGISTER (attrs_decode)
REGISTER (mpd_client_process_input)
char **argv = *argvp, *option = "-test=", *name = NULL;

View File

@@ -636,7 +636,7 @@ test_config_validate_nonnegative
return false;
}
static struct config_schema g_config_test[] =
static const struct config_schema g_config_test[] =
{
{ .name = "foo",
.comment = "baz",
@@ -647,7 +647,7 @@ static struct config_schema g_config_test[] =
.type = CONFIG_ITEM_INTEGER,
.validate = test_config_validate_nonnegative,
.default_ = "1" },
{ .name = "foobar",
{ .name = "123",
.type = CONFIG_ITEM_STRING,
.default_ = "\"qux\\x01`\" \"\"`a`" },
{}
@@ -676,10 +676,11 @@ test_config (void)
config_item_destroy (invalid);
hard_assert (!strcmp ("qux\001`a",
config_item_get (config.root, "top.foobar", NULL)->value.string.str));
config_item_get (config.root, "top.123", NULL)->value.string.str));
struct str s = str_make ();
config_item_write (config.root, true, &s);
print_debug ("%s", s.str);
struct config_item *parsed = config_item_parse (s.str, s.len, false, NULL);
hard_assert (parsed);
config_item_destroy (parsed);

View File

@@ -53,6 +53,10 @@ test_ser_deser_free (void)
u->others.bar = str_make ();
for (int i = rand () % 0x30; i > 0; i--)
str_append_c (&u->others.bar, 0x30 + i);
u->others.baz_len = rand () % 0x30;
u->others.baz = xcalloc (1, u->others.baz_len);
for (uint32_t i = 0; i < u->others.baz_len; i++)
u->others.baz[i] = 0x30 + i;
break;
case 2:
u->tag = PROTO_GEN_ENUM_NOTHING;
@@ -62,6 +66,8 @@ test_ser_deser_free (void)
}
}
a.o.tag = PROTO_GEN_ENUM_NOTHING;
struct str buf = str_make ();
hard_assert (proto_gen_struct_serialize (&a, &buf));
struct msg_unpacker r = msg_unpacker_make (buf.str, buf.len);
@@ -92,6 +98,9 @@ test_ser_deser_free (void)
hard_assert (ua->others.bar.len == ub->others.bar.len);
hard_assert (!memcmp (ua->others.bar.str, ub->others.bar.str,
ua->others.bar.len));
hard_assert (ua->others.baz_len == ub->others.baz_len);
hard_assert (!memcmp (ua->others.baz, ub->others.baz,
ua->others.baz_len));
break;
case PROTO_GEN_ENUM_NOTHING:
break;
@@ -100,6 +109,8 @@ test_ser_deser_free (void)
}
}
hard_assert (a.o.tag == b.o.tag);
// Emulate partially deserialized data to test disposal of that.
for (size_t i = b.u_len - CASES; i < b.u_len; i++)
{

132
tests/lxdrgen.cpp Normal file
View File

@@ -0,0 +1,132 @@
/*
* tests/lxdrgen.cpp
*
* Copyright (c) 2023, 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 "lxdrgen.lxdr.cpp"
#include <cstdlib>
static void
hard_assert (bool condition, const char *description)
{
if (!condition)
{
fprintf (stderr, "assertion failed: %s\n", description);
abort ();
}
}
#define hard_assert(condition) hard_assert (condition, #condition)
int
main (int argc, char *argv[])
{
hard_assert (ProtoGen::VERSION == 1);
enum { CASES = 3 };
ProtoGen::Struct a = {}, b = {};
a.u.resize (CASES + rand () % 100);
for (size_t i = 0; i < a.u.size (); i++)
{
std::unique_ptr<ProtoGen::Union> &u = a.u[i];
switch (i % CASES)
{
case 0:
{
auto numbers = new ProtoGen::Union_Numbers ();
numbers->a = rand () % UINT8_MAX;
numbers->b = rand () % UINT16_MAX;
numbers->c = rand () % UINT32_MAX;
numbers->d = rand () % UINT64_MAX;
numbers->e = rand () % UINT8_MAX;
numbers->f = rand () % UINT16_MAX;
numbers->g = rand () % UINT32_MAX;
numbers->h = rand () % UINT64_MAX;
u.reset (numbers);
break;
}
case 1:
{
auto others = new ProtoGen::Union_Others ();
others->foo = rand () % 2;
for (int i = rand () % 0x30; i > 0; i--)
others->bar += 0x30 + i;
for (int i = rand () % 0x30; i > 0; i--)
others->baz.push_back (0x30 + i);
u.reset (others);
break;
}
case 2:
u.reset (new ProtoGen::Union_Nothing ());
break;
default:
hard_assert (!"unhandled case");
}
}
a.o.reset (new ProtoGen::Onion_Nothing ());
LibertyXDR::Writer buf;
hard_assert (a.serialize (buf));
LibertyXDR::Reader r;
r.data = buf.data.data ();
r.length = buf.data.size ();
hard_assert (b.deserialize (r));
hard_assert (!r.length);
hard_assert (a.u.size () == b.u.size ());
for (size_t i = 0; i < a.u.size (); i++)
{
ProtoGen::Union *ua = a.u[i].get ();
ProtoGen::Union *ub = b.u[i].get ();
hard_assert (ua->tag == ub->tag);
switch (ua->tag)
{
case ProtoGen::Enum::NUMBERS:
{
auto a = dynamic_cast<ProtoGen::Union_Numbers *> (ua);
auto b = dynamic_cast<ProtoGen::Union_Numbers *> (ub);
hard_assert (a->a == b->a);
hard_assert (a->b == b->b);
hard_assert (a->c == b->c);
hard_assert (a->d == b->d);
hard_assert (a->e == b->e);
hard_assert (a->f == b->f);
hard_assert (a->g == b->g);
hard_assert (a->h == b->h);
break;
}
case ProtoGen::Enum::OTHERS:
{
auto a = dynamic_cast<ProtoGen::Union_Others *> (ua);
auto b = dynamic_cast<ProtoGen::Union_Others *> (ub);
hard_assert (a->foo == b->foo);
hard_assert (a->bar == b->bar);
hard_assert (a->baz == b->baz);
break;
}
case ProtoGen::Enum::NOTHING:
break;
default:
hard_assert (!"unexpected case");
}
}
hard_assert (a.o->tag == b.o->tag);
return 0;
}

View File

@@ -17,7 +17,13 @@ struct Struct {
case OTHERS:
bool foo;
string bar;
u8 baz<>;
case NOTHING:
void;
} u<>;
union Onion switch (Enum tag) {
case NOTHING:
void;
} o;
};

67
tests/xdg.c Normal file
View File

@@ -0,0 +1,67 @@
/*
* tests/xdg.c
*
* Copyright (c) 2024, 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.
*
*/
#define PROGRAM_NAME "test"
#define PROGRAM_VERSION "0"
#include "../liberty.c"
#include "../liberty-xdg.c"
static const char file[] =
"# This only tests the happy paths\n"
"[Desktop Entry]\n"
"Version = 1.0\n"
"Name=\\s\\n\\t\\r\\\\\n"
"Name[fr]=Nom\n"
"Hidden=true\n"
"Categories=Utility;TextEditor;\n"
"Number=42";
static void
test_desktop_file (void)
{
struct desktop_file entry = desktop_file_make (file, sizeof file - 1);
const char *group = "Desktop Entry";
char *value = desktop_file_get_string (&entry, group, "Version");
hard_assert (!strcmp (value, "1.0"));
cstr_set (&value, desktop_file_get_string (&entry, group, "Name"));
hard_assert (!strcmp (value, " \n\t\r\\"));
free (value);
hard_assert (desktop_file_get_bool (&entry, group, "Hidden"));
struct strv values = desktop_file_get_stringv (&entry, group, "Categories");
hard_assert (values.len == 2);
hard_assert (!strcmp (values.vector[0], "Utility"));
hard_assert (!strcmp (values.vector[1], "TextEditor"));
strv_free (&values);
hard_assert (desktop_file_get_integer (&entry, group, "Number") == 42);
desktop_file_free (&entry);
}
int
main (int argc, char *argv[])
{
struct test test;
test_init (&test, argc, argv);
test_add_simple (&test, "/desktop-file", NULL, test_desktop_file);
return test_run (&test);
}

View File

@@ -1,6 +1,6 @@
# asciiman.awk: stupid AsciiDoc to manual page converter
# asciiman.awk: simplified AsciiDoc to manual page converter
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This is not intended to produce great output, merely useful output.
@@ -11,7 +11,7 @@
# - Heading underlines must match in byte length exactly.
# - Only a small subset of syntax is supported overall.
#
# Also beware that the output has only been tested with GNU troff.
# Also beware that the output has only been tested with GNU troff and mandoc.
# Attributes can be passed via environment variables starting with "asciidoc-".
function fatal(message) {
@@ -20,20 +20,22 @@ function fatal(message) {
exit 1
}
function getattribute(name) {
if (!(name in Attrs) && ("asciidoc-" name) in ENVIRON)
Attrs[name] = ENVIRON["asciidoc-" name]
return Attrs[name]
BEGIN {
for (name in ENVIRON)
if (match(name, /^asciidoc-/))
Attrs[substr(name, RSTART + RLENGTH)] = ENVIRON[name]
}
function expand(s, attr, v) {
# TODO: This should not expand unknown attribute names.
while (match(s, /[{][^{}]*[}]/)) {
s = substr(s, 1, RSTART - 1) \
getattribute(substr(s, RSTART + 1, RLENGTH - 2)) \
substr(s, RSTART + RLENGTH)
function expand(s, attrname, v) {
while (match(s, /[{][^{}]+[}]/)) {
attrname = substr(s, RSTART + 1, RLENGTH - 2)
if (attrname in Attrs)
v = v substr(s, 1, RSTART - 1) Attrs[attrname]
else
v = v substr(s, 1, RSTART + RLENGTH - 1)
s = substr(s, RSTART + RLENGTH)
}
return s
return v s
}
function escape(s) {
@@ -43,13 +45,20 @@ function escape(s) {
return s
}
function readattribute(line, attrname, attrvalue) {
if (match(line, /^:[^:]*: /)) {
function readattribute(line, attrname) {
if (match(line, /^:[^:]+:$/)) {
Attrs[substr(line, RSTART + 1, RLENGTH - 2)] = ""
} else if (match(line, /^:[^:]+!:$/)) {
delete Attrs[substr(line, RSTART + 1, RLENGTH - 3)]
} else if (match(line, /^:![^:]+:$/)) {
delete Attrs[substr(line, RSTART + 2, RLENGTH - 3)]
} else if (match(line, /^:[^:]+: /)) {
attrname = substr(line, RSTART + 1, RLENGTH - 3)
attrvalue = substr(line, RSTART + RLENGTH)
Attrs[attrname] = expand(attrvalue)
return 1
Attrs[attrname] = expand(substr(line, RSTART + RLENGTH))
} else {
return 0
}
return 1
}
NR == 1 {
@@ -74,15 +83,56 @@ NR == 1 {
# Requesting tbl(1), even though we currently do not support tables.
print "'\\\" t"
printf ".TH \"%s\" \"%s\" \"\" \"%s\"",
toupper(name), section, getattribute("mansource")
if (getattribute("manmanual"))
printf " \"%s\"", getattribute("manmanual")
toupper(name), section, Attrs["mansource"]
if ("manmanual" in Attrs)
printf " \"%s\"", Attrs["manmanual"]
print ""
# Hyphenation is indeed rather annoying, in particular with long links.
print ".nh"
}
function readattrlist(line, posattrs, namedattrs, name, value, n) {
if (!match(line, /^\[.*\]$/))
return 0
line = expand(substr(line, RSTART + 1, RLENGTH - 2))
while (line) {
name = ""
if (match(line, /^[[:alnum:]][[:alnum:]-]*/)) {
value = substr(line, RSTART, RLENGTH)
if (match(substr(line, RSTART + RLENGTH),
/^[[:space:]]*=[[:space:]]*/)) {
name = value
line = substr(line, 1 + length(name) + RLENGTH)
}
}
# The quoting syntax actually is awful like this.
if (match(line, /^"(\\.|[^"\\])*"/)) {
value = substr(line, RSTART + 1, RLENGTH - 2)
gsub(/\\"/, "\"", value)
} else if (match(line, /^'(\\.|[^'\\])*'/)) {
value = substr(line, RSTART + 1, RLENGTH - 2)
gsub(/\\'/, "'", value)
} else {
match(line, /^[^,]*/)
value = substr(line, RSTART, RLENGTH)
sub(/[[:space:]]*$/, "", value)
}
line = substr(line, RSTART + RLENGTH)
sub(/^[[:space:]]*,[[:space:]]*/, "", line)
if (!name)
posattrs[++n] = value
else if (value == "None")
delete namedattrs[name]
else
namedattrs[name] = value
}
return 1
}
function format(line, v) {
# Pass-through, otherwise useful for hacks, is a bit of a lie here,
# and formatting doesn't fully respect word boundaries.
@@ -99,6 +149,11 @@ function format(line, v) {
} else if (match(line, /^[*][^*]+[*]/) &&
substr(line, RSTART + RLENGTH) !~ /^[[:alnum:]]/) {
v = v "\\fB" substr(line, RSTART + 1, RLENGTH - 2) "\\fP"
} else if (match(line, /^`[^`]+`/) &&
substr(line, RSTART + RLENGTH) !~ /^[[:alnum:]]/) {
# Manual pages are usually already rendered in monospace;
# follow others, and render this in boldface.
v = v "\\fB" substr(line, RSTART + 1, RLENGTH - 2) "\\fP"
} else {
v = v substr(line, 1, 1)
line = substr(line, 2)
@@ -109,12 +164,20 @@ function format(line, v) {
return v
}
function flushspace() {
if (NeedSpace) {
print ".sp"
NeedSpace = 0
}
}
function inline(line) {
if (!line) {
print ".sp"
NeedSpace = 1
return
}
flushspace()
line = format(escape(expand(line)))
# Strip empty URL descriptions, otherwise useful for demarking the end.
@@ -137,7 +200,7 @@ function inline(line) {
}
# Returns 1 iff the left-over $0 should be processed further.
function process(firstline) {
function process(firstline, posattrs, namedattrs) {
if (readattribute(firstline))
return 0
if (getline <= 0) {
@@ -145,15 +208,39 @@ function process(firstline) {
return 0
}
# Block attribute list lines.
delete posattrs[0]
delete namedattrs[0]
while (readattrlist(firstline, posattrs, namedattrs)) {
firstline = $0
if (getline <= 0) {
inline(firstline)
return 0
}
}
# mandoc(1) automatically precedes section headers with blank lines.
if (length(firstline) == length($0) && /^-+$/) {
print ".SH \"" escape(toupper(expand(firstline))) "\""
NeedSpace = 0
return 0
}
if (length(firstline) == length($0) && /^~+$/) {
print ".SS \"" escape(expand(firstline)) "\""
NeedSpace = 0
return 0
}
if (firstline ~ /^--$/) {
flushspace()
# For now, recognize, but do not process open block delimiters.
InOpenBlock = !InOpenBlock
return 1
}
if (firstline ~ /^(-{4,}|[.]{4,})$/) {
flushspace()
print ".if n .RS 4"
print ".nf"
print ".fam C"
@@ -172,12 +259,14 @@ function process(firstline) {
return 0
}
if (match(firstline, /^\/\//)) {
print ".\\\" " firstline
print ".\\\"" substr(firstline, RSTART + RLENGTH)
return 1
}
# We generally assume these block end with a blank line.
# We generally assume these blocks end with a blank line.
if (match(firstline, /^[[:space:]]*[*][[:space:]]+/)) {
flushspace()
# Bullet magic copied over from AsciiDoc/Asciidoctor generators.
print ".RS 4"
print ".ie n \\{\\"
@@ -198,10 +287,12 @@ function process(firstline) {
break
}
print ".RE"
print ".sp"
NeedSpace = 1
return !!$0
}
if (match(firstline, /^[[:space:]]+/)) {
flushspace()
print ".if n .RS 4"
print ".nf"
print ".fam C"
@@ -233,7 +324,7 @@ function process(firstline) {
break
}
print ".RE"
print ".sp"
NeedSpace = 1
return !!$0
}
inline(firstline)

View File

@@ -222,7 +222,7 @@ function codegen_struct(name, cg, ctype, funcname) {
delete cg[i]
}
function codegen_union_tag(d, cg) {
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
@@ -259,7 +259,7 @@ function codegen_union_struct( \
"\t\tbreak;\n")
}
function codegen_union(name, cg, f, ctype, funcname) {
function codegen_union(name, cg, exhaustive, f, ctype, funcname) {
ctype = "union " PrefixLower cameltosnake(name)
print ""
print ctype " {"
@@ -281,7 +281,7 @@ function codegen_union(name, cg, f, ctype, funcname) {
CodegenDispose[name] = "\t" funcname "(&%s);\n"
}
if (cg["serialize"]) {
{
funcname = PrefixLower cameltosnake(name) "_serialize"
print ""
print "static bool\n" \
@@ -299,7 +299,7 @@ function codegen_union(name, cg, f, ctype, funcname) {
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
"\t\treturn false;\n"
}
if (cg["deserialize"]) {
{
funcname = PrefixLower cameltosnake(name) "_deserialize"
print ""
print "static bool\n" \

View File

@@ -0,0 +1,67 @@
// lxdrgen-cpp-posix.cpp: POSIX support code for lxdrgen-cpp.awk.
//
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
#include <iconv.h>
#include <cstdint>
#include <string>
// Various BSD derivatives may have a problem here.
// Linux defines __STDC_ISO_10646__, but also supports "WCHAR_T".
#ifdef APPLE
#define ICONV_WCHAR "UTF-32"
#else
#define ICONV_WCHAR "WCHAR_T"
#endif
namespace LibertyXDR {
bool utf8_to_wstring(const uint8_t *utf8, size_t length, std::wstring &wide) {
iconv_t conv = iconv_open(ICONV_WCHAR, "UTF-8");
if (conv == (iconv_t) -1)
return false;
wchar_t buffer[1024] = {};
char *start = (char *) buffer, *out = start, *in = (char *) utf8;
size_t available = sizeof buffer;
wide.clear();
while (iconv(conv, &in, &length, &out, &available) == (size_t) -1) {
if (errno != E2BIG) {
iconv_close(conv);
return false;
}
wide.append(buffer, (out - start) / sizeof *buffer);
out = start;
available = sizeof buffer;
}
wide.append(buffer, (out - start) / sizeof *buffer);
iconv_close(conv);
return true;
}
bool wstring_to_utf8(const std::wstring &wide, std::string &utf8) {
iconv_t conv = iconv_open("UTF-8", ICONV_WCHAR);
if (conv == (iconv_t) -1)
return false;
char buffer[1024] = {}, *out = buffer, *in = (char *) wide.data();
size_t available = sizeof buffer, length = wide.size() * sizeof wide[0];
utf8.clear();
while (iconv(conv, &in, &length, &out, &available) == (size_t) -1) {
if (errno != E2BIG) {
iconv_close(conv);
return false;
}
utf8.append(buffer, out - buffer);
out = buffer;
available = sizeof buffer;
}
utf8.append(buffer, out - buffer);
iconv_close(conv);
return true;
}
} // namespace LibertyXDR

23
tools/lxdrgen-cpp-qt.cpp Normal file
View File

@@ -0,0 +1,23 @@
// lxdrgen-cpp-qt.cpp: Qt support code for lxdrgen-cpp.awk.
//
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
#include <QString>
#include <string>
namespace LibertyXDR {
bool utf8_to_wstring(const uint8_t *utf8, size_t length, std::wstring &wide) {
QByteArrayView view(reinterpret_cast<const char *>(utf8), length);
if (!view.isValidUtf8())
return false;
wide = QString::fromUtf8(view).toStdWString();
return true;
}
bool wstring_to_utf8(const std::wstring &wide, std::string &utf8) {
utf8 = QString::fromStdWString(wide).toUtf8().toStdString();
return true;
}
} // namespace LibertyXDR

View File

@@ -0,0 +1,47 @@
// lxdrgen-cpp-win32.cpp: Win32 support code for lxdrgen-cpp.awk.
//
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
#include <windows.h>
#include <climits>
#include <cstdint>
#include <string>
namespace LibertyXDR {
bool utf8_to_wstring(const uint8_t *utf8, size_t length, std::wstring &wide) {
wide.clear();
if (!length)
return true;
if (length > INT_MAX)
return false;
int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
(LPCCH) utf8, length, nullptr, 0);
if (size <= 0)
return false;
wide.resize(size);
return !!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
(LPCCH) utf8, length, wide.data(), size);
}
bool wstring_to_utf8(const std::wstring &wide, std::string &utf8) {
utf8.clear();
if (wide.empty())
return true;
if (wide.size() > INT_MAX)
return false;
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
(LPCWCH) wide.data(), wide.size(), nullptr, 0, NULL, NULL);
if (size <= 0)
return false;
utf8.resize(size);
return !!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
(LPCWCH) wide.data(), wide.size(), utf8.data(), size, NULL, NULL);
}
} // namespace LibertyXDR

350
tools/lxdrgen-cpp.awk Normal file
View File

@@ -0,0 +1,350 @@
# lxdrgen-cpp.awk: C++ backend for lxdrgen.awk.
#
# This backend is intended for Windows, it just happens to have a fallback
# that will probably work on Unices, of which we make use in tests.
#
# Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
function define_internal(name, ctype) {
Types[name] = "internal"
CodegenCType[name] = ctype
CodegenSerialize[name] = \
"\tw.append(%s);\n"
CodegenDeserialize[name] = \
"\tif (!r.read(%s))\n" \
"\t\treturn false;\n"
}
function define_int(shortname, ctype) {
define_internal(shortname, ctype)
}
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", "std::wstring")
define_internal("bool", "bool")
CodegenSerialize["string"] = \
"\tif (!w.append(%s))\n" \
"\t\treturn false;\n"
print "// Code generated from " FILENAME ". DO NOT EDIT."
print ""
print "#include <cstdint>"
print "#include <memory>"
print "#include <string>"
print "#include <vector>"
print ""
print "namespace LibertyXDR {"
print ""
print "bool utf8_to_wstring("
print "\tconst uint8_t *utf8, size_t length, std::wstring &wide);"
print "bool wstring_to_utf8("
print "\tconst std::wstring &wide, std::string &utf8);"
print ""
print "struct Reader {"
print "\tconst uint8_t *data = {};"
print "\tsize_t length = {};"
print ""
print "\ttemplate<typename T> bool read(T &number) {"
print "\t\tif (length < sizeof number)"
print "\t\t\treturn false;"
print ""
print "\t\tnumber = 0;"
print "\t\tfor (size_t i = 0; i < sizeof number; i++) {"
print "\t\t\tnumber = number << 8 | *data++;"
print "\t\t\tlength--;"
print "\t\t}"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool read(bool &boolean) {"
print "\t\tuint8_t number = 0;"
print "\t\tif (!read(number))"
print "\t\t\treturn false;"
print ""
print "\t\tboolean = number != 0;"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool read(std::wstring &string) {"
print "\t\tuint32_t size = 0;"
print "\t\tif (!read(size) || size > length)"
print "\t\t\treturn false;"
print "\t\tif (!utf8_to_wstring(data, size, string))"
print "\t\t\treturn false;"
print ""
print "\t\tdata += size;"
print "\t\tlength -= size;"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool read(std::vector<uint8_t> &vector) {"
print "\t\tuint32_t size = 0;"
print "\t\tif (!read(size) || size > length)"
print "\t\t\treturn false;"
print "\t\tvector.assign(data, data + size);"
print ""
print "\t\tdata += size;"
print "\t\tlength -= size;"
print "\t\treturn true;"
print "\t}"
print "};"
print ""
print "struct Writer {"
print "\tstd::vector<uint8_t> data;"
print ""
print "\ttemplate<typename T> bool append(T number) {"
print "\t\tuint8_t buffer[sizeof number], *p = buffer + sizeof buffer;"
print "\t\twhile (p != buffer) {"
print "\t\t\t*--p = number;"
print "\t\t\tnumber >>= 8;"
print "\t\t}"
print "\t\tdata.insert(data.end(), buffer, buffer + sizeof buffer);"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool append(int8_t number) {"
print "\t\tdata.push_back(number);"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool append(uint8_t number) {"
print "\t\tdata.push_back(number);"
print "\t\treturn true;"
print "\t}"
print ""
print "\tbool append(bool boolean) {"
print "\t\treturn append(uint8_t(boolean));"
print "\t}"
print ""
print "\tbool append(const std::wstring &string) {"
print "\t\tif (string.size() > UINT32_MAX)"
print "\t\t\treturn false;"
print ""
print "\t\tstd::string utf8;"
print "\t\tif (!wstring_to_utf8(string, utf8))"
print "\t\t\treturn false;"
print ""
print "\t\tappend<uint32_t>(utf8.size());"
print "\t\tdata.insert(data.end(), utf8.begin(), utf8.end());"
print "\t\treturn true;"
print "\t}"
print "};"
print ""
print "} // namespace LibertyXDR"
print "namespace " PrefixCamel " {"
}
END {
print ""
print "} // namespace " PrefixCamel
}
function codegen_constant(name, value) {
print ""
print "enum { " name " = " value " };"
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields", "\t" subname " = " value ",\n")
}
function codegen_enum(name, cg) {
print ""
print "enum struct " name " : int8_t {"
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] = \
"\tw.append(static_cast<int8_t>(%s));\n"
CodegenDeserialize[name] = \
"\t{\n" \
"\t\tint8_t v = 0;\n" \
"\t\tif (!r.read(v) || !v)\n" \
"\t\t\treturn false;\n" \
"\t\t%s = static_cast<" name ">(v);\n" \
"\t}\n"
CodegenCType[name] = name
for (i in cg)
delete cg[i]
}
# Some identifiers do not pose a problem in C, but do in our C++.
function codegen_struct_sanitize(name) {
if (name ~ /^(serialize|deserialize)_*$/ ||
name ~ /^(catch|class|delete|except|finally|friend|new|operator)_*$/ ||
name ~ /^(private|protected|public|template|this|throw|try|virtual)_*$/)
return name "_"
return name
}
function codegen_struct_tag(d, cg, name, f) {
name = codegen_struct_sanitize(d["name"])
f = "this->" name
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
# Do not deserialize here, that would be out of order.
}
function codegen_struct_field(d, cg, name, f, serialize, deserialize) {
name = codegen_struct_sanitize(d["name"])
f = "this->" name
serialize = CodegenSerialize[d["type"]]
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "fields",
"\t" CodegenCType[d["type"]] " " name " = {};\n")
append(cg, "serialize", sprintf(serialize, f))
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "fields",
"\tstd::vector<" CodegenCType[d["type"]] "> " name ";\n")
# XXX: We should probably pedantically check for overflows.
append(cg, "serialize",
sprintf(CodegenSerialize["u32"], "uint32_t(" f ".size())") \
"\tfor (const auto &it : " f ")\n" \
indent(sprintf(serialize, "it")))
if (d["type"] == "u8") {
append(cg, "deserialize",
"\tif (!r.read(" f "))\n" \
"\t\treturn false;\n")
} else if (deserialize) {
append(cg, "deserialize",
"\t{\n" \
"\t\tuint32_t size = 0;\n" \
indent(sprintf(CodegenDeserialize["u32"], "size")) \
"\t\t" f ".resize(size);\n" \
"\t}\n" \
"\tfor (auto &it : " f ")\n" \
indent(sprintf(deserialize, "it")))
}
}
function codegen_struct(name, cg) {
print ""
print "struct " name " {"
print cg["fields"]
print "\tbool serialize(LibertyXDR::Writer &w) const {"
print indent(cg["serialize"]) "\t\treturn true;"
print "\t}"
print ""
print "\tbool deserialize([[maybe_unused]] LibertyXDR::Reader &r) {"
print indent(cg["deserialize"]) "\t\treturn true;"
print "\t}"
print "};"
CodegenSerialize[name] = "\tif (!%s->serialize(w))\n" \
"\t\treturn false;\n"
CodegenDeserialize[name] = "\tif (!%s->deserialize(r))\n" \
"\t\treturn false;\n"
CodegenCType[name] = name
for (i in cg)
delete cg[i]
}
function codegen_union_tag(name, d, cg, tagname) {
cg["tagtype"] = d["type"]
cg["tagname"] = tagname = codegen_struct_sanitize(d["name"])
print ""
print "struct " name " {"
print "\t" CodegenCType[d["type"]] " " tagname " = {};"
print "\tvirtual ~" name "() = 0;"
print "\tvirtual bool serialize(LibertyXDR::Writer &w) const = 0;"
print "\tvirtual bool deserialize(LibertyXDR::Reader &r) = 0;"
print "};"
print ""
print name "::~" name "() {}"
}
function codegen_union_struct(name, casename, cg, scg, structname) {
# And thus not all generated structs are present in Types.
structname = name "_" snaketocamel(casename)
print ""
print "struct " structname " : virtual public " name " {"
print scg["fields"]
print "\t" structname "() {"
print "\t\tthis->" cg["tagname"] " = " \
CodegenCType[cg["tagtype"]] "::" casename ";"
print "\t}"
print ""
print "\tvirtual bool serialize(LibertyXDR::Writer &w) const {"
print indent(scg["serialize"]) "\t\treturn true;"
print "\t}"
print ""
print "\tvirtual bool deserialize([[maybe_unused]] LibertyXDR::Reader &r) {"
print indent(scg["deserialize"]) "\t\treturn true;"
print "\t}"
print "};"
append(cg, "deserialize",
"\tcase " CodegenCType[cg["tagtype"]] "::" casename ":\n" \
"\t\treturn new " structname "();\n")
CodegenSerialize[structname] = "\tif (!%s->serialize(w))\n" \
"\t\treturn false;\n"
CodegenDeserialize[structname] = "\tif (!%s->deserialize(r))\n" \
"\t\treturn false;\n"
CodegenCType[structname] = structname
for (i in scg)
delete scg[i]
}
function codegen_union(name, cg, exhaustive, ctype) {
CodegenSerialize[name] = "\tif (!%s->serialize(w))\n" \
"\t\treturn false;\n"
ctype = "std::unique_ptr<" name ">"
if (cg["deserialize"]) {
print ""
print "static " name " *read" name "(" \
CodegenCType[cg["tagtype"]] " " cg["tagname"] ") {"
print "\tswitch (" cg["tagname"] ") {"
print cg["deserialize"] "\tdefault:"
print "\t\treturn nullptr;"
print "\t}"
print "}"
print ""
print "static " ctype " read" name "(LibertyXDR::Reader &r) {"
print "\tint8_t v = 0;"
print "\tif (!r.read(v) || !v)"
print "\t\treturn nullptr;"
print ""
print "\t" ctype " result(read" name "(static_cast<" \
CodegenCType[cg["tagtype"]] ">(v)));"
print "\tif (!result || !result->deserialize(r))"
print "\t\treturn nullptr;"
print "\treturn result;"
print "}"
CodegenDeserialize[name] = "\tif (!(%s = read" name "(r)))\n" \
"\t\treturn false;\n"
}
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}

View File

@@ -1,6 +1,6 @@
# lxdrgen-go.awk: Go backend for lxdrgen.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend also enables proxying to other endpoints using JSON.
@@ -145,7 +145,7 @@ function codegen_begin( funcname) {
print "// " funcname " tries to serialize a string value,"
print "// appending it to the end of a byte stream."
print "func " funcname "(data []byte, s string) ([]byte, bool) {"
print "\tif len(s) > math.MaxUint32 {"
print "\tif int64(len(s)) > math.MaxUint32 {"
print "\t\treturn nil, false"
print "\t}"
print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
@@ -301,9 +301,11 @@ function codegen_marshal(type, f, marshal) {
"\t}\n"
}
function codegen_struct_field_marshal(d, cg, camel, f, marshal) {
function codegen_struct_field_marshal(d, cg, isaccessor, camel, f, marshal) {
camel = snaketocamel(d["name"])
f = "s." camel
if (isaccessor)
f = f "()"
if (!d["isarray"]) {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \
@@ -333,7 +335,7 @@ function codegen_struct_field_marshal(d, cg, camel, f, marshal) {
}
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
codegen_struct_field_marshal(d, cg)
codegen_struct_field_marshal(d, cg, 0)
camel = snaketocamel(d["name"])
f = "s." camel
@@ -384,15 +386,11 @@ function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
}
}
function codegen_struct_tag(d, cg, camel, f) {
codegen_struct_field_marshal(d, cg)
function codegen_struct_tag(d, cg) {
codegen_struct_field_marshal(d, cg, 1)
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.
# Do not serialize or deserialize here,
# that is already done by the containing union.
}
function codegen_struct(name, cg, gotype) {
@@ -439,9 +437,9 @@ function codegen_struct(name, cg, gotype) {
delete cg[i]
}
function codegen_union_tag(d, cg) {
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
cg["tagname"] = snaketocamel(d["name"])
# The tag is implied from the type of struct stored in the interface.
}
@@ -450,13 +448,18 @@ function codegen_union_struct(name, casename, cg, scg, structname, init) {
structname = name snaketocamel(casename)
codegen_struct(structname, scg)
init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \
": " decapitalize(snaketocamel(cg["tagname"])) "}"
print "func (u *" CodegenGoType[structname] ") " cg["tagname"] "() " \
CodegenGoType[cg["tagtype"]] " {"
print "\treturn " CodegenGoType[cg["tagtype"]] snaketocamel(casename)
print "}"
print ""
init = CodegenGoType[structname] "{}"
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")
"\t\tu.Variant = &s\n")
append(cg, "serialize",
"\tcase *" CodegenGoType[structname] ":\n" \
indent(sprintf(CodegenSerialize[structname], "union")))
@@ -464,35 +467,37 @@ function codegen_union_struct(name, casename, cg, scg, structname, init) {
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
indent(sprintf(CodegenDeserialize[structname], "s")) \
"\t\tu.Interface = &s\n")
"\t\tu.Variant = &s\n")
}
function codegen_union(name, cg, gotype, tagfield, tagvar) {
function codegen_union(name, cg, exhaustive, gotype, tagvar) {
gotype = PrefixCamel name
# This must be a struct, so that UnmarshalJSON can create concrete types.
print "type " gotype " struct {"
print "\tInterface any"
print "\tVariant interface {"
print "\t\t" cg["tagname"] "() " CodegenGoType[cg["tagtype"]]
print "\t}"
print "}"
print ""
# This cannot be a pointer method, it wouldn't work recursively.
CodegenIsMarshaler[name] = 1
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
print "\treturn u.Interface.(json.Marshaler).MarshalJSON()"
print "\treturn u.Variant.(json.Marshaler).MarshalJSON()"
print "}"
print ""
tagfield = snaketocamel(cg["tagname"])
tagvar = decapitalize(tagfield)
tagvar = decapitalize(cg["tagname"])
print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
print "\tvar t struct {"
print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \
print "\t\t" cg["tagname"] " " 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 "\tswitch " tagvar " := t." cg["tagname"] "; " tagvar " {"
print cg["unmarshal"] "\tdefault:"
print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
print "\t}"
@@ -500,13 +505,15 @@ function codegen_union(name, cg, gotype, tagfield, tagvar) {
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.
# XXX: Consider rather testing the type for having an AppendTo method,
# which would eliminate this type case switch entirely.
print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print "\tswitch union := u.Interface.(type) {"
print sprintf(CodegenSerialize[cg["tagtype"]],
"u.Variant." cg["tagname"] "()") \
"\tswitch union := u.Variant.(type) {"
print cg["serialize"] "\tdefault:"
print "\t\t_ = union"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"

View File

@@ -183,16 +183,16 @@ function codegen_struct(name, cg) {
delete cg[i]
}
function codegen_union_tag(d, cg) {
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
cg["tagname"] = decapitalize(snaketocamel(d["name"]))
}
function codegen_union_struct(name, casename, cg, scg, structname) {
append(scg, "methods",
"\n" \
"\tconstructor() {\n" \
"\t\tthis." decapitalize(snaketocamel(cg["tagname"])) \
"\t\tthis." cg["tagname"] \
" = " cg["tagtype"] "." snaketocamel(casename) "\n" \
"\t}\n")
@@ -208,8 +208,8 @@ function codegen_union_struct(name, casename, cg, scg, structname) {
"\t}\n")
}
function codegen_union(name, cg, tagvar) {
tagvar = decapitalize(snaketocamel(cg["tagname"]))
function codegen_union(name, cg, exhaustive, tagvar) {
tagvar = cg["tagname"]
print ""
print "export function deserialize" name "(r) {"

277
tools/lxdrgen-swift.awk Normal file
View File

@@ -0,0 +1,277 @@
# lxdrgen-swift.awk: Swift backend for lxdrgen.awk.
#
# Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
function define_internal(name, swifttype) {
Types[name] = "internal"
CodegenSwiftType[name] = swifttype
CodegenDeserialize[name] = "%s.read()"
}
function define_sint(size, shortname, swifttype) {
shortname = "i" size
swifttype = "Int" size
define_internal(shortname, swifttype)
}
function define_uint(size, shortname, swifttype) {
shortname = "u" size
swifttype = "UInt" size
define_internal(shortname, swifttype)
}
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")
define_internal("string", "String")
print "// Code generated from " FILENAME ". DO NOT EDIT."
print "import Foundation"
print ""
print "public struct " PrefixCamel "Reader {"
print "\tpublic var data: Data"
print ""
print "\tpublic enum ReadError: Error {"
print "\t\tcase unexpectedEOF"
print "\t\tcase invalidEncoding"
print "\t\tcase overflow"
print "\t\tcase unexpectedValue"
print "\t}"
print ""
print "\tpublic mutating func read<T: FixedWidthInteger>() throws -> T {"
print "\t\tlet size = MemoryLayout<T>.size"
print "\t\tguard data.count >= size else {"
print "\t\t\tthrow ReadError.unexpectedEOF"
print "\t\t}"
print "\t\tvar acc: T = 0"
print "\t\tdata.prefix(size).forEach { acc = acc << 8 | T($0) }"
print "\t\tdata = data.dropFirst(size)"
print "\t\treturn acc"
print "\t}"
print ""
print "\tpublic mutating func read() throws -> Bool {"
print "\t\ttry read() != UInt8(0)"
print "\t}"
print ""
print "\tpublic mutating func read() throws -> String {"
print "\t\tlet size: UInt32 = try self.read()"
print "\t\tguard let count = Int(exactly: size) else {"
print "\t\t\tthrow ReadError.overflow"
print "\t\t}"
print "\t\tguard data.count >= count else {"
print "\t\t\tthrow ReadError.unexpectedEOF"
print "\t\t}"
print "\t\tdefer {"
print "\t\t\tdata = data.dropFirst(count)"
print "\t\t}"
print "\t\tif let s = String(data: data.prefix(count), encoding: .utf8) {"
print "\t\t\treturn s"
print "\t\t} else {"
print "\t\t\tthrow ReadError.invalidEncoding"
print "\t\t}"
print "\t}"
print ""
print "\tpublic mutating func read<" \
"T: RawRepresentable<Int8>>() throws -> T {"
print "\t\tguard let value = T(rawValue: try read()) else {"
print "\t\t\tthrow ReadError.unexpectedValue"
print "\t\t}"
print "\t\treturn value"
print "\t}"
print ""
print "\tpublic mutating func read<T>("
print "\t\t\t_ read: (inout Self) throws -> T) throws -> [T] {"
print "\t\tlet size: UInt32 = try self.read()"
print "\t\tguard let count = Int(exactly: size) else {"
print "\t\t\tthrow ReadError.overflow"
print "\t\t}"
print "\t\tvar array = [T]()"
print "\t\tarray.reserveCapacity(count)"
print "\t\tfor _ in 0..<count {"
print "\t\t\tarray.append(try read(&self))"
print "\t\t}"
print "\t\treturn array"
print "\t}"
print "}"
print ""
print "public struct " PrefixCamel "Writer {"
print "\tpublic var data = Data()"
print ""
print "\tpublic mutating func append<T: FixedWidthInteger>(_ number: T) {"
print "\t\tvar n = number.byteSwapped"
print "\t\tfor _ in 0..<MemoryLayout<T>.size {"
print "\t\t\tdata.append(UInt8(truncatingIfNeeded: n))"
print "\t\t\tn >>= 8"
print "\t\t}"
print "\t}"
print ""
print "\tpublic mutating func append(_ bool: Bool) {"
print "\t\tappend(UInt8(bool ? 1 : 0))"
print "\t}"
print ""
print "\tpublic mutating func append(_ string: String) {"
print "\t\tlet bytes = string.data(using: .utf8)!"
print "\t\tappend(UInt32(bytes.count))"
print "\t\tdata.append(bytes)"
print "\t}"
print ""
print "\tpublic mutating func append<T: " \
"RawRepresentable<Int8>>(_ value: T) {"
print "\t\tappend(value.rawValue)"
print "\t}"
print ""
print "\tpublic mutating func append<T>("
print "\t\t\t_ array: Array<T>, _ write: (inout Self, T) -> ()) {"
print "\t\tappend(UInt32(array.count))"
print "\t\tfor i in 0..<array.count {"
print "\t\t\twrite(&self, array[i])"
print "\t\t}"
print "\t}"
print ""
print "\tpublic mutating func append<T: " \
PrefixCamel "Encodable>(_ value: T) {"
print "\t\tvalue.encode(to: &self)"
print "\t}"
print "}"
print ""
print "public protocol " PrefixCamel "Encodable { " \
"func encode(to: inout " PrefixCamel "Writer) }"
}
function codegen_constant(name, value) {
print ""
print "public let " decapitalize(PrefixCamel snaketocamel(name)) " = " value
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields",
"\tcase " decapitalize(snaketocamel(subname)) " = " value "\n")
}
function codegen_enum(name, cg, swifttype) {
swifttype = PrefixCamel name
print ""
print "public enum " swifttype ": Int8 {"
print cg["fields"] "}"
CodegenSwiftType[name] = swifttype
CodegenDeserialize[name] = "%s.read()"
for (i in cg)
delete cg[i]
}
function codegen_struct_field(d, cg, camel) {
camel = decapitalize(snaketocamel(d["name"]))
if (!d["isarray"]) {
append(cg, "fields",
"\tpublic var " camel ": " CodegenSwiftType[d["type"]] "\n")
append(cg, "deserialize",
"\t\tself." camel " = try " \
sprintf(CodegenDeserialize[d["type"]], "from") "\n")
append(cg, "serialize",
"\t\tto.append(self." camel ")\n")
return
}
append(cg, "fields",
"\tpublic var " camel ": [" CodegenSwiftType[d["type"]] "]\n")
append(cg, "deserialize",
"\t\tself." camel " = try from.read() { r in try " \
sprintf(CodegenDeserialize[d["type"]], "r") " }\n")
append(cg, "serialize",
"\t\tto.append(self." camel ") { (w, value) in w.append(value) }\n")
}
function codegen_struct_tag(d, cg, camel) {
camel = decapitalize(snaketocamel(d["name"]))
append(cg, "serialize",
"\t\tto.append(self." camel ")\n")
}
function codegen_struct(name, cg, swifttype) {
swifttype = PrefixCamel name
print ""
print "public struct " swifttype " {\n" cg["fields"] "}"
print ""
print "extension " swifttype ": " PrefixCamel "Encodable {"
print "\tpublic init(from: inout " PrefixCamel "Reader) throws {"
print cg["deserialize"] "\t}"
print ""
print "\tpublic func encode(to: inout " PrefixCamel "Writer) {"
print cg["serialize"] "\t}"
print "}"
CodegenSwiftType[name] = swifttype
CodegenDeserialize[name] = "%s.read()"
for (i in cg)
delete cg[i]
}
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = decapitalize(snaketocamel(d["name"]))
}
function codegen_union_struct(name, casename, cg, scg, swifttype) {
# And thus not all generated structs are present in Types.
swifttype = PrefixCamel name snaketocamel(casename)
casename = decapitalize(snaketocamel(casename))
print ""
print "public struct " swifttype ": " PrefixCamel name " {"
print "\tpublic var " cg["tagname"] \
": " CodegenSwiftType[cg["tagtype"]] " { ." casename " }"
print scg["fields"] "}"
print ""
print "extension " swifttype ": " PrefixCamel "Encodable {"
print "\tfileprivate init(from: inout " PrefixCamel "Reader) throws {"
print scg["deserialize"] "\t}"
print ""
print "\tpublic func encode(to: inout " PrefixCamel "Writer) {"
print scg["serialize"] "\t}"
print "}"
append(cg, "cases", "\tcase ." casename ":\n" \
"\t\treturn try " swifttype "(from: &from)\n")
CodegenSwiftType[name] = swifttype
CodegenDeserialize[name] = "%s.read()"
for (i in scg)
delete scg[i]
}
function codegen_union(name, cg, exhaustive, swifttype, init) {
# Classes don't have automatic member-wise initializers,
# thus using structs and protocols.
swifttype = PrefixCamel name
print ""
print "public protocol " swifttype ": " PrefixCamel "Encodable {"
print "\tvar " cg["tagname"] ": " CodegenSwiftType[cg["tagtype"]] " { get }"
print "}"
if (!exhaustive)
append(cg, "cases", "\tdefault:\n" \
"\t\tthrow " PrefixCamel "Reader.ReadError.unexpectedValue\n")
init = decapitalize(swifttype)
print ""
print "public func " init \
"(from: inout " PrefixCamel "Reader) throws -> " swifttype " {"
print "\tlet " cg["tagname"] ": " CodegenSwiftType[cg["tagtype"]] \
" = try from.read()"
print "\tswitch " cg["tagname"] " {"
print cg["cases"] "\t}"
print "}"
CodegenSwiftType[name] = swifttype
CodegenDeserialize[name] = init "(from: &%s)"
for (i in cg)
delete cg[i]
}

View File

@@ -1,6 +1,6 @@
# lxdrgen.awk: an XDR-derived code generator for network protocols.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# Copyright (c) 2022 - 2023, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Usage: env LC_ALL=C awk -f lxdrgen.awk -f lxdrgen-{c,go,mjs}.awk \
@@ -28,9 +28,10 @@ function snaketocamel(s) {
}
function decapitalize(s) {
if (match(s, /[[:upper:]][[:lower:]]/)) {
if (match(s, /^[[:upper:]][[:lower:]]/))
return tolower(substr(s, 1, 1)) substr(s, 2)
}
if (match(s, /^[[:upper:]]$/))
return tolower(s)
return s
}
@@ -216,7 +217,8 @@ function defstruct( name, d, cg) {
return name
}
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i,
unseen, exhaustive) {
delete cg[0]
delete scg[0]
delete d[0]
@@ -230,7 +232,7 @@ function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
if (Types[tagtype] != "enum")
fatal("not an enum type: " tagtype)
codegen_union_tag(tag, cg)
codegen_union_tag(name, tag, cg)
split(EnumValues[tagtype], a, SUBSEP)
for (i in a)
@@ -257,9 +259,14 @@ function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, unseen) {
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
# What remains non-zero in unseen[2..] is simply not recognized/allowed.
# Unseen cases are simply not recognized/allowed.
exhaustive = 1
for (i in unseen)
if (i && unseen[i])
exhaustive = 0
Types[name] = "union"
codegen_union(name, cg)
codegen_union(name, cg, exhaustive)
return name
}