Compare commits

...

55 Commits
c++ ... master

Author SHA1 Message Date
Přemysl Eric Janouch 8386af0420
Silence an OpenBSD linker warning
Alpine 3.19 Success Details
2024-04-10 17:54:34 +02:00
Přemysl Eric Janouch f04cc2c61e
Add MinGW-w64 CMake toolchain files
Alpine 3.19 Success Details
2024-04-09 17:01:07 +02:00
Přemysl Eric Janouch 969a4cfc3e
liberty-xui: clip terminal drawing 2024-02-27 00:27:54 +01:00
Přemysl Eric Janouch ad5b2fb8cd
asciiman: mildly improve compatibility
git manual pages render a little bit more sensibly now.
2024-02-12 10:57:23 +01:00
Přemysl Eric Janouch 2a1f17a8f7
liberty-xdg: add desktop entry parser tests
And fix a discovered bug.
2024-02-10 12:49:01 +01:00
Přemysl Eric Janouch 8d56fae41b
liberty-xdg: actually make libpng optional 2024-02-10 12:20:44 +01:00
Přemysl Eric Janouch 0239a4242a
liberty-xdg: fix usage of volatile 2024-02-10 10:16:27 +01:00
Přemysl Eric Janouch 1966b81b4d
liberty-xui: tolerate zero-area ConfigureNotify
Happens when launching from a fullscreen window in i3.
2024-02-10 10:07:09 +01:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch f32bcbd7f4
Add helper functions for reading binary numbers
And make use of them.
2024-02-10 07:02:54 +01:00
Přemysl Eric Janouch c5424e6992
Comment on write_file_safe()'s actual safety 2024-02-10 06:16:27 +01:00
Přemysl Eric Janouch cb9d162a26
Add a CMake module for icon conversions 2024-02-10 05:39:15 +01:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 7a0cb13a1a
MPD client: fix build on OpenIndiana 2023-07-24 08:33:45 +02:00
Přemysl Eric Janouch b6c54073cd
Find ncursesw on OpenIndiana 2023-07-24 08:09:08 +02:00
Přemysl Eric Janouch 62166f9679
lxdrgen-cpp-win32: fix return value handling 2023-07-10 09:34:42 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch f78f8a70f1
lxdrgen-swift: fix prefix handling
"Any prefix will work, so long as it's 'Relay'."
2023-07-06 11:01:51 +02:00
Přemysl Eric Janouch be9a3e693e
lxdrgen-swift: fix warnings with exhaustive unions 2023-07-06 06:54:23 +02:00
Přemysl Eric Janouch 53197b51e5
Add a Swift backend for LibertyXDR 2023-07-06 06:54:22 +02:00
Přemysl Eric Janouch 8466d0d850
CMakeLists.txt: link properly 2023-07-04 08:08:29 +02:00
Přemysl Eric Janouch 4c2874649d
liberty-xui: fix build on systems without A_ITALIC 2023-07-04 06:40:54 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 091f92bab3
liberty-xui: fix a build warning
On macOS, TIOCGWINSZ seems to be defined earlier.
2023-06-28 16:24:59 +02:00
Přemysl Eric Janouch 556c25855e
Fix a CMake warning 2023-06-20 01:24:29 +02:00
Přemysl Eric Janouch 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
Přemysl Eric Janouch 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
Přemysl Eric Janouch 29bf109a51
asciiman: improve attribute handling 2022-10-09 18:43:37 +02:00
Přemysl Eric Janouch 0e86ffe7c3
asciiman: fixes, improve mandoc compatibility 2022-10-09 01:01:08 +02:00
Přemysl Eric Janouch cbeb4e3133
Improve documentation 2022-10-04 01:46:41 +02:00
Přemysl Eric Janouch 0f3ed14575
asciiman: support attribute passing 2022-09-30 18:13:01 +02:00
Přemysl Eric Janouch 089593bb0f
asciiman: render libertyxdr.adoc properly 2022-09-30 15:01:14 +02:00
Přemysl Eric Janouch 035bfe5e81
Document the recently added scripts 2022-09-30 03:09:04 +02:00
Přemysl Eric Janouch ebbe7a1672
Import protocol code generator from xK, add tests
Also add a VIM syntax highlighting file.

This also fixes some previously untriggered bugs.
2022-09-30 03:06:36 +02:00
Přemysl Eric Janouch 4c3837ae2c
cmake-parser: clean up 2022-09-28 00:06:51 +02:00
Přemysl Eric Janouch af2756ee01
Add a rudimentary CMake script parser 2022-09-27 23:27:06 +02:00
Přemysl Eric Janouch 688c458095
asciiman: fix the first line of output 2022-09-25 21:11:45 +02:00
Přemysl Eric Janouch 34460ca715
asciiman: improve command escaping 2022-09-25 20:55:51 +02:00
Přemysl Eric Janouch 9883caf849
Add a stupid AsciiDoc to manual page converter
Most of my projects that could need it make use of liberty.
2022-09-25 20:07:10 +02:00
Přemysl Eric Janouch 22a121383f
Allow for overriding feature test macros 2022-09-11 00:44:14 +02:00
Přemysl Eric Janouch 0e89bb9f46
Add some consts to function arguments 2022-09-01 12:44:58 +02:00
Přemysl Eric Janouch 63aed8f0fd
Fix up the PEG change from the last commit
This is not a regular expression.
2022-08-14 19:09:52 +02:00
Přemysl Eric Janouch f545be725d
Extend string syntax in config
And actually test the results of string parsing.
2022-08-14 18:14:21 +02:00
Přemysl Eric Janouch 7e8e085c97
Remove pointless, wrong constant
C99 allows trailing commas.
2021-12-18 00:25:13 +01:00
Přemysl Eric Janouch 782a9a5977
Import libpulse poller integration, add tests 2021-11-07 15:37:21 +01:00
Přemysl Eric Janouch 34f86651f6
Update .gitignore 2021-10-30 03:31:08 +02:00
Přemysl Eric Janouch 5dec46df2c
Add clang-format configuration, clean up 2021-10-30 03:10:17 +02:00
Přemysl Eric Janouch 1b9d89cab3
Use kqueue on Darwin as well
Since poll() is implemented in terms of kqueue() there,
it doesn't seem like this could have improved anything.

Besides man 3 ev, libevent code, and [1],
I haven't managed to find much relevant information.

[1] https://daniel.haxx.se/blog/2016/10/11/poll-on-mac-10-12-is-broken/
2021-09-29 12:07:25 +02:00
Přemysl Eric Janouch a3ad5e7751
Ignore empty XDG_*_DIRS env. variables
As the specification says we should.  GLib does this as well.

It is still possible to achieve an empty set by using ":",
which are two non-absolute paths that should be ignored.
GLib doesn't implement this.  Thus, we're now better than GLib.
2021-09-26 08:49:51 +02:00
Přemysl Eric Janouch 960420df3e
Escape DEL character in config_item_write_string() 2020-10-31 21:28:29 +01:00
Přemysl Eric Janouch d71c47f8ce
CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 15:32:26 +01:00
Přemysl Eric Janouch 425ea57b17
CMakeLists.txt: clean up OpenBSD support
A few things might have changed.
2020-10-29 15:31:05 +01:00
Přemysl Eric Janouch 8822d06091
Don't suppress -Wimplicit-fallthrough
Might have already been resolved by: 9494e8e da75b6f
2020-10-26 18:25:32 +01:00
Přemysl Eric Janouch 9639777814
Fix validation of overlong UTF-8
It was too strict and Egyptian dicks didn't want to pass,
so we'll do it half-arsedly for a subset.
2020-10-24 19:09:09 +02:00
Přemysl Eric Janouch 929229a1d7
Fix config PEG grammar to match strtoll() 2020-10-24 08:05:17 +02:00
37 changed files with 7123 additions and 374 deletions

32
.clang-format Normal file
View File

@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2
# liberty-specific macro body wrappers.
MacroBlockBegin: "BLOCK_START"
MacroBlockEnd: "BLOCK_END"
ForEachMacros: ["LIST_FOR_EACH"]

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
/liberty.files
/liberty.creator*
/liberty.includes
/liberty.cflags
/liberty.cxxflags

View File

@ -1,12 +1,12 @@
project (liberty C)
cmake_minimum_required (VERSION 2.8.5)
cmake_minimum_required (VERSION 3.0...3.27)
project (liberty C CXX)
# Moar warnings
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 -Wno-implicit-fallthrough")
set (wdisabled "-Wno-unused-function")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra ${wdisabled}")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
endif ()
# Dependencies
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
@ -16,11 +16,9 @@ find_package (PkgConfig REQUIRED)
pkg_check_modules (libssl REQUIRED libssl libcrypto)
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
include_directories (/usr/local/include)
link_directories (/usr/local/lib)
# Our POSIX version macros make these undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
endif ()
set (common_libraries ${libssl_LIBRARIES})
include_directories (${libssl_INCLUDE_DIRS})
@ -31,16 +29,102 @@ link_directories (${libssl_LIBRARY_DIRS})
foreach (extra iconv rt)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND common_libraries ${extra})
endif (extra_lib_${extra})
endforeach (extra)
list (APPEND common_libraries ${extra_lib_${extra}})
endif ()
endforeach ()
# Build some unit tests
include_directories (${PROJECT_SOURCE_DIR})
enable_testing ()
foreach (name liberty proto)
set (tests liberty proto xdg)
pkg_check_modules (libpulse libpulse)
if (libpulse_FOUND)
list (APPEND tests pulse)
list (APPEND common_libraries ${libpulse_LIBRARIES})
include_directories (${libpulse_INCLUDE_DIRS})
link_directories (${libpulse_LIBRARY_DIRS})
endif ()
foreach (name ${tests})
add_executable (test-${name} tests/${name}.c ${common_sources})
add_threads (test-${name})
target_link_libraries (test-${name} ${common_libraries})
add_test (NAME test-${name} COMMAND test-${name})
endforeach (name)
endforeach ()
# --- Tools --------------------------------------------------------------------
# Test the AsciiDoc manual page generator for a successful parse
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/tools/asciiman.awk)
add_custom_command (OUTPUT libertyxdr.7
COMMAND env LC_ALL=C awk -f ${ASCIIMAN}
"${PROJECT_SOURCE_DIR}/libertyxdr.adoc" > libertyxdr.7
DEPENDS libertyxdr.adoc ${ASCIIMAN}
COMMENT "Generating man page for libertyxdr" VERBATIM)
add_custom_target (docs ALL DEPENDS libertyxdr.7)
# Test CMake script parsing
add_test (test-cmake-parser
env LC_ALL=C awk -f ${PROJECT_SOURCE_DIR}/tools/cmake-parser.awk
-f ${PROJECT_SOURCE_DIR}/tools/cmake-dump.awk ${CMAKE_CURRENT_LIST_FILE})
# Test protocol code generation
set (lxdrgen_outputs)
set (lxdrgen_base "${PROJECT_BINARY_DIR}/lxdrgen.lxdr")
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
-f ${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk
-f ${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk
-v PrefixCamel=ProtoGen
${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr
> ${lxdrgen_base}.${backend}
DEPENDS
${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk
${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk
${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr
COMMENT "Generating test protocol code (${backend})" VERBATIM)
endforeach ()
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-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_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)
add_test (test-lxdrgen-go ${GO_EXECUTABLE} vet ${lxdrgen_base}.go)
else ()
message (WARNING "Cannot test generated protocol code for Go")
endif ()
find_program (NODE_EXECUTABLE node)
if (NODE_EXECUTABLE)
add_test (test-lxdrgen-mjs ${NODE_EXECUTABLE} -c ${lxdrgen_base}.mjs)
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 - 2020, 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,67 @@
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
-----
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.
Just like them, it uses the _man_ macro package.
cmake-parser.awk::
Parses the CMake language to the extent that is necessary to reliably
extract project versions. Its greatest limitation is its inability
to expand variables, which would require a full interpreter.
cmake-dump.awk::
This can be used in conjunction with the previous script to dump CMake
scripts in a normalized format for further processing.
lxdrgen.awk::
Protocol code generator for a variant of XDR,
which is link:libertyxdr.adoc[documented separately].
Successfully employed in https://git.janouch.name/p/xK[xK].
lxdrgen-c.awk::
LibertyXDR backend that builds on top of the C pseudolibrary.
lxdrgen-cpp.awk::
lxdrgen-cpp-win32.cpp::
lxdrgen-cpp-posix.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_
interface is bound to be underperforming, due to the amount of otherwise
avoidable memory allocations it necessitates).
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
------------------------

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)

40
cmake/IconUtils.cmake Normal file
View File

@ -0,0 +1,40 @@
# 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 ()

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

@ -148,7 +148,7 @@ irc_free_message (struct irc_message *msg)
static void
irc_process_buffer (struct str *buf,
void (*callback)(const struct irc_message *, const char *, void *),
void (*callback) (const struct irc_message *, const char *, void *),
void *user_data)
{
char *start = buf->str, *end = start + buf->len;
@ -1321,7 +1321,6 @@ enum mpd_subsystem
#define XX(a, b, c) MPD_SUBSYSTEM_ ## a = (1 << b),
MPD_SUBSYSTEM_TABLE (XX)
#undef XX
MPD_SUBSYSTEM_MAX
};
static const char *mpd_subsystem_names[] =
@ -1912,14 +1911,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);

348
liberty-pulse.c Normal file
View File

@ -0,0 +1,348 @@
/*
* liberty-pulse.c: PulseAudio mainloop abstraction
*
* Copyright (c) 2016 - 2021, 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 <pulse/mainloop.h>
// --- PulseAudio mainloop abstraction -----------------------------------------
struct pa_io_event
{
LIST_HEADER (pa_io_event)
pa_mainloop_api *api; ///< Parent structure
struct poller_fd fd; ///< Underlying FD event
pa_io_event_cb_t dispatch; ///< Dispatcher
pa_io_event_destroy_cb_t free; ///< Destroyer
void *user_data; ///< User data
};
struct pa_time_event
{
LIST_HEADER (pa_time_event)
pa_mainloop_api *api; ///< Parent structure
struct poller_timer timer; ///< Underlying timer event
pa_time_event_cb_t dispatch; ///< Dispatcher
pa_time_event_destroy_cb_t free; ///< Destroyer
void *user_data; ///< User data
};
struct pa_defer_event
{
LIST_HEADER (pa_defer_event)
pa_mainloop_api *api; ///< Parent structure
struct poller_idle idle; ///< Underlying idle event
pa_defer_event_cb_t dispatch; ///< Dispatcher
pa_defer_event_destroy_cb_t free; ///< Destroyer
void *user_data; ///< User data
};
struct poller_pa
{
struct poller *poller; ///< The underlying event loop
pa_io_event *io_list; ///< I/O events
pa_time_event *time_list; ///< Timer events
pa_defer_event *defer_list; ///< Deferred events
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static short
poller_pa_flags_to_events (pa_io_event_flags_t flags)
{
short result = 0;
if (flags & PA_IO_EVENT_ERROR) result |= POLLERR;
if (flags & PA_IO_EVENT_HANGUP) result |= POLLHUP;
if (flags & PA_IO_EVENT_INPUT) result |= POLLIN;
if (flags & PA_IO_EVENT_OUTPUT) result |= POLLOUT;
return result;
}
static pa_io_event_flags_t
poller_pa_events_to_flags (short events)
{
pa_io_event_flags_t result = 0;
if (events & POLLERR) result |= PA_IO_EVENT_ERROR;
if (events & POLLHUP) result |= PA_IO_EVENT_HANGUP;
if (events & POLLIN) result |= PA_IO_EVENT_INPUT;
if (events & POLLOUT) result |= PA_IO_EVENT_OUTPUT;
return result;
}
static struct timeval
poller_pa_get_current_time (void)
{
struct timeval tv;
#ifdef _POSIX_TIMERS
struct timespec tp;
hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
tv.tv_sec = tp.tv_sec;
tv.tv_usec = tp.tv_nsec / 1000;
#else
gettimeofday (&tv, NULL);
#endif
return tv;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
poller_pa_io_dispatcher (const struct pollfd *pfd, void *user_data)
{
pa_io_event *self = user_data;
self->dispatch (self->api, self,
pfd->fd, poller_pa_events_to_flags (pfd->revents), self->user_data);
}
static void
poller_pa_io_enable (pa_io_event *self, pa_io_event_flags_t events)
{
struct poller_fd *fd = &self->fd;
if (events)
poller_fd_set (fd, poller_pa_flags_to_events (events));
else
poller_fd_reset (fd);
}
static pa_io_event *
poller_pa_io_new (pa_mainloop_api *api, int fd_, pa_io_event_flags_t events,
pa_io_event_cb_t cb, void *userdata)
{
pa_io_event *self = xcalloc (1, sizeof *self);
self->api = api;
self->dispatch = cb;
self->user_data = userdata;
struct poller_pa *data = api->userdata;
self->fd = poller_fd_make (data->poller, fd_);
self->fd.user_data = self;
self->fd.dispatcher = poller_pa_io_dispatcher;
// FIXME: under x2go PA tries to register twice for the same FD,
// which fails with our curent poller implementation;
// we could maintain a list of { poller_fd, listeners } structures;
// or maybe we're doing something wrong, which is yet to be determined
poller_pa_io_enable (self, events);
LIST_PREPEND (data->io_list, self);
return self;
}
static void
poller_pa_io_free (pa_io_event *self)
{
if (self->free)
self->free (self->api, self, self->user_data);
struct poller_pa *data = self->api->userdata;
poller_fd_reset (&self->fd);
LIST_UNLINK (data->io_list, self);
free (self);
}
static void
poller_pa_io_set_destroy (pa_io_event *self, pa_io_event_destroy_cb_t cb)
{
self->free = cb;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
poller_pa_time_dispatcher (void *user_data)
{
pa_time_event *self = user_data;
// XXX: the meaning of the time argument is undocumented,
// so let's just put current Unix time in there
struct timeval now = poller_pa_get_current_time ();
self->dispatch (self->api, self, &now, self->user_data);
}
static void
poller_pa_time_restart (pa_time_event *self, const struct timeval *tv)
{
struct poller_timer *timer = &self->timer;
if (tv)
{
struct timeval now = poller_pa_get_current_time ();
poller_timer_set (timer,
(tv->tv_sec - now.tv_sec) * 1000 +
(tv->tv_usec - now.tv_usec) / 1000);
}
else
poller_timer_reset (timer);
}
static pa_time_event *
poller_pa_time_new (pa_mainloop_api *api, const struct timeval *tv,
pa_time_event_cb_t cb, void *userdata)
{
pa_time_event *self = xcalloc (1, sizeof *self);
self->api = api;
self->dispatch = cb;
self->user_data = userdata;
struct poller_pa *data = api->userdata;
self->timer = poller_timer_make (data->poller);
self->timer.user_data = self;
self->timer.dispatcher = poller_pa_time_dispatcher;
poller_pa_time_restart (self, tv);
LIST_PREPEND (data->time_list, self);
return self;
}
static void
poller_pa_time_free (pa_time_event *self)
{
if (self->free)
self->free (self->api, self, self->user_data);
struct poller_pa *data = self->api->userdata;
poller_timer_reset (&self->timer);
LIST_UNLINK (data->time_list, self);
free (self);
}
static void
poller_pa_time_set_destroy (pa_time_event *self, pa_time_event_destroy_cb_t cb)
{
self->free = cb;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
poller_pa_defer_dispatcher (void *user_data)
{
pa_defer_event *self = user_data;
self->dispatch (self->api, self, self->user_data);
}
static pa_defer_event *
poller_pa_defer_new (pa_mainloop_api *api,
pa_defer_event_cb_t cb, void *userdata)
{
pa_defer_event *self = xcalloc (1, sizeof *self);
self->api = api;
self->dispatch = cb;
self->user_data = userdata;
struct poller_pa *data = api->userdata;
self->idle = poller_idle_make (data->poller);
self->idle.user_data = self;
self->idle.dispatcher = poller_pa_defer_dispatcher;
poller_idle_set (&self->idle);
LIST_PREPEND (data->defer_list, self);
return self;
}
static void
poller_pa_defer_enable (pa_defer_event *self, int enable)
{
struct poller_idle *idle = &self->idle;
if (enable)
poller_idle_set (idle);
else
poller_idle_reset (idle);
}
static void
poller_pa_defer_free (pa_defer_event *self)
{
if (self->free)
self->free (self->api, self, self->user_data);
struct poller_pa *data = self->api->userdata;
poller_idle_reset (&self->idle);
LIST_UNLINK (data->defer_list, self);
free (self);
}
static void
poller_pa_defer_set_destroy (pa_defer_event *self,
pa_defer_event_destroy_cb_t cb)
{
self->free = cb;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
poller_pa_quit (pa_mainloop_api *api, int retval)
{
(void) api;
(void) retval;
// This is not called from within libpulse
hard_assert (!"quitting the libpulse event loop is unimplemented");
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct pa_mainloop_api g_poller_pa_template =
{
.io_new = poller_pa_io_new,
.io_enable = poller_pa_io_enable,
.io_free = poller_pa_io_free,
.io_set_destroy = poller_pa_io_set_destroy,
.time_new = poller_pa_time_new,
.time_restart = poller_pa_time_restart,
.time_free = poller_pa_time_free,
.time_set_destroy = poller_pa_time_set_destroy,
.defer_new = poller_pa_defer_new,
.defer_enable = poller_pa_defer_enable,
.defer_free = poller_pa_defer_free,
.defer_set_destroy = poller_pa_defer_set_destroy,
.quit = poller_pa_quit,
};
static struct pa_mainloop_api *
poller_pa_new (struct poller *self)
{
struct poller_pa *data = xcalloc (1, sizeof *data);
data->poller = self;
struct pa_mainloop_api *api = xmalloc (sizeof *api);
*api = g_poller_pa_template;
api->userdata = data;
return api;
}
static void
poller_pa_destroy (struct pa_mainloop_api *api)
{
struct poller_pa *data = api->userdata;
LIST_FOR_EACH (pa_io_event, iter, data->io_list)
poller_pa_io_free (iter);
LIST_FOR_EACH (pa_time_event, iter, data->time_list)
poller_pa_time_free (iter);
LIST_FOR_EACH (pa_defer_event, iter, data->defer_list)
poller_pa_defer_free (iter);
free (data);
free (api);
}

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

179
liberty.c
View File

@ -1,7 +1,7 @@
/*
* liberty.c: the ultimate C unlibrary
*
* Copyright (c) 2014 - 2020, 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.
@ -16,8 +16,13 @@
*
*/
#define _POSIX_C_SOURCE 199309L
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 600
#endif
#include <stdio.h>
#include <stddef.h>
@ -287,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 *
@ -647,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.
@ -1749,10 +1809,9 @@ poller_run (struct poller *self)
self->revents_len = 0;
}
#elif defined (BSD)
// Mac OS X's kqueue is fatally broken, or so I've been told; leaving it out.
// Otherwise this is sort of similar to the epoll version.
// Sort of similar to the epoll version. Let's hope Darwin isn't broken,
// that'd mean reimplementing this in terms of select() just because of Crapple.
#elif defined (BSD) || defined (__APPLE__)
#include <sys/types.h>
#include <sys/event.h>
@ -2451,7 +2510,7 @@ write_queue_processed (struct write_queue *self, size_t len)
}
static bool
write_queue_is_empty (struct write_queue *self)
write_queue_is_empty (const struct write_queue *self)
{
return self->head == NULL;
}
@ -2499,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
@ -2546,7 +2600,7 @@ msg_unpacker_make (const void *data, size_t len)
}
static size_t
msg_unpacker_get_available (struct msg_unpacker *self)
msg_unpacker_get_available (const struct msg_unpacker *self)
{
return self->len - self->offset;
}
@ -2569,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;
}
@ -2578,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;
}
@ -2588,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;
}
@ -2748,16 +2795,12 @@ utf8_decode (const char **s, size_t len)
}
// In the middle of a character
if (sequence_len == 1)
// or an overlong sequence (subset, possibly MUTF-8, not supported)
if (sequence_len == 1 || *p == 0xC0 || *p == 0xC1)
return -1;
// Check the rest of the sequence
uint32_t cp = *p++ & ~mask;
// Overlong sequence (possibly MUTF-8, not supported)
if (!cp && sequence_len)
return -1;
while (sequence_len && --sequence_len)
{
if (p == end)
@ -3251,7 +3294,7 @@ get_xdg_config_dirs (struct strv *out)
str_free (&config_home);
const char *xdg_config_dirs;
if (!(xdg_config_dirs = getenv ("XDG_CONFIG_DIRS")))
if (!(xdg_config_dirs = getenv ("XDG_CONFIG_DIRS")) || !*xdg_config_dirs)
xdg_config_dirs = "/etc/xdg";
cstr_split (xdg_config_dirs, ":", true, out);
}
@ -3276,7 +3319,7 @@ get_xdg_data_dirs (struct strv *out)
str_free (&data_home);
const char *xdg_data_dirs;
if (!(xdg_data_dirs = getenv ("XDG_DATA_DIRS")))
if (!(xdg_data_dirs = getenv ("XDG_DATA_DIRS")) || !*xdg_data_dirs)
xdg_data_dirs = "/usr/local/share/:/usr/share/";
cstr_split (xdg_data_dirs, ":", true, out);
}
@ -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)))
@ -4417,11 +4462,13 @@ socket_io_try_write (int socket_fd, struct str *wb)
// object = lws '{' entries endobj
// endobj = lws '}'
//
// string = lws '"' ('\\' escape / ![\\"] char)* '"'
// quoted = lws '"' (!["\\] char / '\\' escape)* '"'
// / lws '`' (![`] char)* '`'
// string = (quoted)+
// char = [\0-\177] # or any Unicode codepoint in the UTF-8 encoding
// escape = [\\"abfnrtv] / [xX][0-9A-Fa-f][0-9A-Fa-f]? / [0-7][0-7]?[0-7]?
//
// integer = lws '-'? [0-9]+ # whatever strtoll() accepts on your system
// integer = lws [-+]? [0-9]+ # whatever strtoll() accepts on your system
// null = lws 'null'
// boolean = lws 'yes' / lws 'YES' / lws 'no' / lws 'NO'
// / lws 'on' / lws 'ON' / lws 'off' / lws 'OFF'
@ -4712,13 +4759,15 @@ config_item_write_string (struct str *output, const struct str *s)
for (size_t i = 0; i < s->len; i++)
{
unsigned char c = s->str[i];
if (c == '\n') str_append (output, "\\n");
else if (c == '\r') str_append (output, "\\r");
else if (c == '\t') str_append (output, "\\t");
else if (c == '\\') str_append (output, "\\\\");
else if (c == '"') str_append (output, "\\\"");
else if (c < 32) str_append_printf (output, "\\x%02x", c);
else str_append_c (output, c);
if (c == '\n') str_append (output, "\\n");
else if (c == '\r') str_append (output, "\\r");
else if (c == '\t') str_append (output, "\\t");
else if (c == '\\') str_append (output, "\\\\");
else if (c == '"') str_append (output, "\\\"");
else if (iscntrl_ascii (c))
str_append_printf (output, "\\x%02x", c);
else
str_append_c (output, c);
}
str_append_c (output, '"');
}
@ -5042,10 +5091,10 @@ config_tokenizer_escape_sequence
}
static bool
config_tokenizer_string
(struct config_tokenizer *self, struct str *output, struct error **e)
config_tokenizer_dq_string (struct config_tokenizer *self, struct str *output,
struct error **e)
{
unsigned char c;
unsigned char c = config_tokenizer_advance (self);
while (self->len)
{
if ((c = config_tokenizer_advance (self)) == '"')
@ -5059,6 +5108,44 @@ config_tokenizer_string
return false;
}
static bool
config_tokenizer_bt_string (struct config_tokenizer *self, struct str *output,
struct error **e)
{
unsigned char c = config_tokenizer_advance (self);
while (self->len)
{
if ((c = config_tokenizer_advance (self)) == '`')
return true;
str_append_c (output, c);
}
config_tokenizer_error (self, e, "premature end of string");
return false;
}
static bool
config_tokenizer_string (struct config_tokenizer *self, struct str *output,
struct error **e)
{
// Go-like strings, with C/AWK-like automatic concatenation
while (self->len)
{
bool ok = true;
if (isspace_ascii (*self->p) && *self->p != '\n')
config_tokenizer_advance (self);
else if (*self->p == '"')
ok = config_tokenizer_dq_string (self, output, e);
else if (*self->p == '`')
ok = config_tokenizer_bt_string (self, output, e);
else
break;
if (!ok)
return false;
}
return true;
}
static enum config_token
config_tokenizer_next (struct config_tokenizer *self, struct error **e)
{
@ -5083,7 +5170,7 @@ config_tokenizer_next (struct config_tokenizer *self, struct error **e)
return CONFIG_T_ABORT;
case '"':
config_tokenizer_advance (self);
case '`':
str_reset (&self->string);
if (!config_tokenizer_string (self, &self->string, e))
return CONFIG_T_ABORT;

108
libertyxdr.adoc Normal file
View File

@ -0,0 +1,108 @@
libertyxdr(7)
=============
:doctype: manpage
Name
----
LibertyXDR - an XDR-derived IDL and data serialization format
Description
-----------
*LibertyXDR* is an interface description language, as well as a data
serialization format. It is largely derived from XDR, though notably
simplified.
Conventions
~~~~~~~~~~~
User-defined types should be named in *CamelCase*, field names in *snake_case*,
and constants in *SCREAMING_SNAKE_CASE*. Code generators will convert these to
whatever is appropriate in their target language.
Primitive data types
~~~~~~~~~~~~~~~~~~~~
Like in XDR, all data is serialized in the network byte order, i.e., big-endian.
* *void*: 0 bytes
+
This is a dummy type that cannot be assigned a field name.
* *bool*: 1 byte
+
This is a boolean value: 0 means _false_, any other value means _true_.
* *u8*, *u16*, *u32*, *u64*: 1, 2, 4, and 8 bytes respectively
+
These are unsigned integers.
* *i8*, *i16*, *i32*, *i64*: 1, 2, 4, and 8 bytes respectively
+
These are signed integers in two's complement.
* *string*: implicitly prefixed by its length as a *u32*,
then immediately followed by its contents, with no trailing NUL byte
+
This is a valid UTF-8 string without a byte order mark. Note that strings are
always unbounded, unlike in XDR.
Constants
~~~~~~~~~
At the top level of a document, outside other definitions, you can define
typeless integer constants:
const VERSION = 1;
The value can be either a name of another previously defined constant,
or an immediate decimal value, which may not contain leading zeros.
Enumerations
~~~~~~~~~~~~
An *enum* is an *i8* with uniquely named values, in their own namespace.
Values can be either specified explicitly, in the same way as with a constant,
or they can be left implicit, in which case names assume a value that is one
larger than their predecessor. Zero is reserved for internal use, thus
enumerations implicitly begin with a value of one. For example, these form
a sequence from one to three:
enum Vehicle { CAR, LORRY = 2, PLANE, };
Structures
~~~~~~~~~~
A *struct* is a sequence of fields, specified by their type, and their chosen
name. You can add a *<>* suffix to change a field to an array, in which case
it is implicitly preceded by a *u32* specifying its length in terms of its
elements.
Unlike in XDR, there is no padding between subsequent fields, and type
definitions can be arbitrarily syntactically nested, as in C.
struct StockReport {
u8 version; // Version of this report.
struct Item {
Vehicle kind; // The vehicle in question.
i32 count; // How many vehicle of that kind there are.
} items<>; // Reported items.
};
Unions
~~~~~~
A *union* is a kind of structure whose fields depend on the value of its first
and always-present field, which must be a tag *enum*:
union VehicleDetails switch (Vehicle kind) {
case CAR: void;
case LORRY: i8 axles;
case PLANE: i8 engines;
};
All possible enumeration values must be named, and there is no *case*
fall-through.
Framing
-------
Unless this role is already filled by, e.g., WebSocket, _LibertyXDR_ structures
should be prefixed by their byte length in the *u32* format, once serialized.
See also
--------
_XDR: External Data Representation Standard_, RFC 4506

21
libertyxdr.vim Normal file
View File

@ -0,0 +1,21 @@
" filetype.vim: au! BufNewFile,BufRead *.lxdr setf libertyxdr
if exists("b:current_syntax")
finish
endif
syn match libertyxdrError "[^[:space:]:;,(){}<>=]\+"
syn region libertyxdrBlockComment start=+/[*]+ end=+[*]/+
syn match libertyxdrComment "//.*"
syn match libertyxdrIdentifier "\<[[:alpha:]][[:alnum:]_]*\>"
syn match libertyxdrNumber "\<0\>\|\(-\|\<\)[1-9][[:digit:]]*\>"
syn keyword libertyxdrKeyword const enum struct union switch case
syn keyword libertyxdrType bool u8 u16 u32 u64 i8 i16 i32 i64 string void
let b:current_syntax = "libertyxdr"
hi def link libertyxdrError Error
hi def link libertyxdrBlockComment Comment
hi def link libertyxdrComment Comment
hi def link libertyxdrIdentifier Identifier
hi def link libertyxdrNumber Number
hi def link libertyxdrKeyword Statement
hi def link libertyxdrType Type

View File

@ -82,4 +82,3 @@ siphash (const unsigned char key[16], const unsigned char *m, size_t len)
return v0 ^ v1 ^ v2 ^ v3;
}

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

@ -1,7 +1,7 @@
/*
* tests/liberty.c
*
* Copyright (c) 2015 - 2016, Přemysl Eric Janouch <p@janouch.name>
* Copyright (c) 2015 - 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@ -331,10 +331,12 @@ test_utf8 (void)
soft_assert (utf8_decode (&partial, 1) == -2);
soft_assert (utf8_decode (&empty, 0) == -1);
const char valid[] = "2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm";
const char valid_1[] = "2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm";
const char valid_2[] = "\xf0\x93\x82\xb9";
const char invalid_1[] = "\xf0\x90\x28\xbc";
const char invalid_2[] = "\xc0\x80";
soft_assert ( utf8_validate (valid, sizeof valid));
soft_assert ( utf8_validate (valid_1, sizeof valid_1));
soft_assert ( utf8_validate (valid_2, sizeof valid_2));
soft_assert (!utf8_validate (invalid_1, sizeof invalid_1));
soft_assert (!utf8_validate (invalid_2, sizeof invalid_2));
@ -647,7 +649,7 @@ static struct config_schema g_config_test[] =
.default_ = "1" },
{ .name = "foobar",
.type = CONFIG_ITEM_STRING,
.default_ = "\"qux\\x01\"" },
.default_ = "\"qux\\x01`\" \"\"`a`" },
{}
};
@ -673,6 +675,9 @@ test_config (void)
"top.bar", NULL), invalid, NULL));
config_item_destroy (invalid);
hard_assert (!strcmp ("qux\001`a",
config_item_get (config.root, "top.foobar", NULL)->value.string.str));
struct str s = str_make ();
config_item_write (config.root, true, &s);
struct config_item *parsed = config_item_parse (s.str, s.len, false, NULL);

134
tests/lxdrgen.c Normal file
View File

@ -0,0 +1,134 @@
/*
* tests/lxdrgen.c
*
* Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#define PROGRAM_NAME "test"
#define PROGRAM_VERSION "0"
#include "../liberty.c"
#include "lxdrgen.lxdr.c"
static void
test_ser_deser_free (void)
{
hard_assert (PROTO_GEN_VERSION == 1);
enum { CASES = 3 };
struct proto_gen_struct a = {}, b = {};
a.u = xcalloc ((a.u_len = CASES + rand () % 100), sizeof *a.u);
for (size_t i = 0; i < a.u_len; i++)
{
union proto_gen_union *u = a.u + i;
switch (i % CASES)
{
case 0:
u->tag = PROTO_GEN_ENUM_NUMBERS;
u->numbers.a = rand () % UINT8_MAX;
u->numbers.b = rand () % UINT16_MAX;
u->numbers.c = rand () % UINT32_MAX;
u->numbers.d = rand () % UINT64_MAX;
u->numbers.e = rand () % UINT8_MAX;
u->numbers.f = rand () % UINT16_MAX;
u->numbers.g = rand () % UINT32_MAX;
u->numbers.h = rand () % UINT64_MAX;
break;
case 1:
u->tag = PROTO_GEN_ENUM_OTHERS;
u->others.foo = rand () % 2;
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;
break;
default:
hard_assert (!"unhandled case");
}
}
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);
hard_assert (proto_gen_struct_deserialize (&b, &r));
hard_assert (!msg_unpacker_get_available (&r));
str_free (&buf);
hard_assert (a.u_len == b.u_len);
for (size_t i = 0; i < a.u_len; i++)
{
union proto_gen_union *ua = a.u + i;
union proto_gen_union *ub = b.u + i;
hard_assert (ua->tag == ub->tag);
switch (ua->tag)
{
case PROTO_GEN_ENUM_NUMBERS:
hard_assert (ua->numbers.a == ub->numbers.a);
hard_assert (ua->numbers.b == ub->numbers.b);
hard_assert (ua->numbers.c == ub->numbers.c);
hard_assert (ua->numbers.d == ub->numbers.d);
hard_assert (ua->numbers.e == ub->numbers.e);
hard_assert (ua->numbers.f == ub->numbers.f);
hard_assert (ua->numbers.g == ub->numbers.g);
hard_assert (ua->numbers.h == ub->numbers.h);
break;
case PROTO_GEN_ENUM_OTHERS:
hard_assert (ua->others.foo == ub->others.foo);
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;
default:
hard_assert (!"unexpected case");
}
}
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++)
{
proto_gen_union_free (&b.u[i]);
memset (&b.u[i], 0, sizeof b.u[i]);
}
proto_gen_struct_free (&a);
proto_gen_struct_free (&b);
}
int
main (int argc, char *argv[])
{
struct test test;
test_init (&test, argc, argv);
test_add_simple (&test, "/ser-deser-free", NULL, test_ser_deser_free);
return test_run (&test);
}

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;
}

29
tests/lxdrgen.lxdr Normal file
View File

@ -0,0 +1,29 @@
/*
* tests/lxdrgen.lxdr: a test protocol for the generator
*/
const VERSION = 1;
const NOISREV = -1;
// TODO: Test failure paths, and in general go for full coverage.
struct Struct {
union Union switch (enum Enum {
NUMBERS = VERSION,
OTHERS = 2,
NOTHING,
} tag) {
case NUMBERS:
i8 a; i16 b; i32 c; i64 d;
u8 e; u16 f; u32 g; u64 h;
case OTHERS:
bool foo;
string bar;
u8 baz<>;
case NOTHING:
void;
} u<>;
union Onion switch (Enum tag) {
case NOTHING:
void;
} o;
};

127
tests/pulse.c Normal file
View File

@ -0,0 +1,127 @@
/*
* tests/pulse.c
*
* Copyright (c) 2021, 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"
#define LIBERTY_WANT_POLLER
#include "../liberty.c"
#include "../liberty-pulse.c"
// --- Tests -------------------------------------------------------------------
enum
{
EVENT_IO = 1 << 0,
EVENT_TIME = 1 << 1,
EVENT_DEFER = 1 << 2,
EVENT_ALL = (1 << 3) - 1
};
static intptr_t g_events = 0;
static intptr_t g_destroys = 0;
static void
io_event_cb (pa_mainloop_api *a,
pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata)
{
(void) a; (void) e; (void) fd; (void) events;
g_events |= (intptr_t) userdata;
}
static void
io_event_destroy_cb (pa_mainloop_api *a, pa_io_event *e, void *userdata)
{
(void) a; (void) e;
g_destroys += (intptr_t) userdata;
}
static void
time_event_cb (pa_mainloop_api *a,
pa_time_event *e, const struct timeval *tv, void *userdata)
{
(void) a; (void) e; (void) tv;
g_events |= (intptr_t) userdata;
}
static void
time_event_destroy_cb (pa_mainloop_api *a, pa_time_event *e, void *userdata)
{
(void) a; (void) e;
g_destroys += (intptr_t) userdata;
}
static void
defer_event_cb (pa_mainloop_api *a, pa_defer_event *e, void *userdata)
{
(void) a; (void) e;
g_events |= (intptr_t) userdata;
}
static void
defer_event_destroy_cb (pa_mainloop_api *a, pa_defer_event *e, void *userdata)
{
(void) a; (void) e;
g_destroys += (intptr_t) userdata;
}
static void
test_pulse (void)
{
struct poller poller;
poller_init (&poller);
// Let's just get this over with, not aiming for high test coverage here
pa_mainloop_api *api = poller_pa_new (&poller);
pa_io_event *ie = api->io_new (api, STDOUT_FILENO, PA_IO_EVENT_OUTPUT,
io_event_cb, (void *) EVENT_IO);
api->io_set_destroy (ie, io_event_destroy_cb);
const struct timeval tv = poller_pa_get_current_time ();
pa_time_event *te = api->time_new (api, &tv,
time_event_cb, (void *) EVENT_TIME);
api->time_set_destroy (te, time_event_destroy_cb);
api->time_restart (te, &tv);
pa_defer_event *de = api->defer_new (api,
defer_event_cb, (void *) EVENT_DEFER);
api->defer_set_destroy (de, defer_event_destroy_cb);
api->defer_enable (api->defer_new (api,
defer_event_cb, (void *) EVENT_DEFER), false);
alarm (1);
while (g_events != EVENT_ALL)
poller_run (&poller);
poller_pa_destroy (api);
soft_assert (g_destroys == EVENT_ALL);
poller_free (&poller);
}
// --- Main --------------------------------------------------------------------
int
main (int argc, char *argv[])
{
struct test test;
test_init (&test, argc, argv);
test_add_simple (&test, "/pulse", NULL, test_pulse);
return test_run (&test);
}

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);
}

336
tools/asciiman.awk Normal file
View File

@ -0,0 +1,336 @@
# asciiman.awk: simplified AsciiDoc to manual page converter
#
# 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.
# As such, input documents should restrict themselves as follows:
#
# - In-line formatting sequences must not overlap,
# cannot be escaped, and cannot span lines.
# - 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 and mandoc.
# Attributes can be passed via environment variables starting with "asciidoc-".
function fatal(message) {
print ".\\\" " FILENAME ":" FNR ": fatal error: " message
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
exit 1
}
BEGIN {
for (name in ENVIRON)
if (match(name, /^asciidoc-/))
Attrs[substr(name, RSTART + RLENGTH)] = ENVIRON[name]
}
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 v s
}
function escape(s) {
gsub(/\\/, "\\\\", s)
gsub(/-/, "\\-", s)
sub(/^[.']/, "\\\\\\&&", s)
return s
}
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)
Attrs[attrname] = expand(substr(line, RSTART + RLENGTH))
} else {
return 0
}
return 1
}
NR == 1 {
nameline = $0
if (match(nameline, /[(][[:digit:]][)]$/)) {
name = substr(nameline, 1, RSTART - 1)
section = substr(nameline, RSTART + 1, RLENGTH - 2)
} else {
fatal("invalid header line")
}
getline
if (length(nameline) != length($0) || /[^=]/)
fatal("invalid header underline")
getline
while (readattribute($0))
getline
if ($0)
fatal("expected an empty line after the header")
# Requesting tbl(1), even though we currently do not support tables.
print "'\\\" t"
printf ".TH \"%s\" \"%s\" \"\" \"%s\"",
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.
while (line) {
if (match(line, /^[+][+][+][^+]+[+][+][+]/)) {
v = v substr(line, RSTART + 3, RLENGTH - 6)
} else if (match(line, /^__[^_]+__/)) {
v = v "\\fI" substr(line, RSTART + 2, RLENGTH - 4) "\\fP"
} else if (match(line, /^[*][*][^*]+[*][*]/)) {
v = v "\\fB" substr(line, RSTART + 2, RLENGTH - 4) "\\fP"
} else if (match(line, /^_[^_]+_/) &&
substr(line, RSTART + RLENGTH) !~ /^[[:alnum:]]/) {
v = v "\\fI" substr(line, RSTART + 1, RLENGTH - 2) "\\fP"
} 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)
continue
}
line = substr(line, RSTART + RLENGTH)
}
return v
}
function flushspace() {
if (NeedSpace) {
print ".sp"
NeedSpace = 0
}
}
function inline(line) {
if (!line) {
NeedSpace = 1
return
}
flushspace()
line = format(escape(expand(line)))
# Strip empty URL descriptions, otherwise useful for demarking the end.
while (match(line, /[^[:space:]]+\[\]/)) {
line = substr(line, 1, RSTART + RLENGTH - 3) \
substr(line, RSTART + RLENGTH)
}
# Enable double-spacing after the end of a sentence.
gsub(/[.][[:space:]]+/, ".\n", line)
gsub(/[!][[:space:]]+/, "!\n", line)
gsub(/[?][[:space:]]+/, "?\n", line)
# Quote commands resulting from that, as well as from expand().
gsub(/\n[.]/, "\n\\\\\\&.", line)
gsub(/\n[']/, "\n\\\\\\&'", line)
sub(/[[:space:]]+[+]$/, "\n.br", line)
print line
}
# Returns 1 iff the left-over $0 should be processed further.
function process(firstline, posattrs, namedattrs) {
if (readattribute(firstline))
return 0
if (getline <= 0) {
inline(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"
do {
print escape($0)
} while (getline > 0 && $0 != firstline)
print ".fam"
print ".fi"
print ".if n .RE"
return 0
}
if (firstline ~ /^\/{4,}$/) {
do {
print ".\\\" " $0
} while (getline > 0 && $0 != firstline)
return 0
}
if (match(firstline, /^\/\//)) {
print ".\\\"" substr(firstline, RSTART + RLENGTH)
return 1
}
# 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 \\{\\"
print "\\h'-04'\\(bu\\h'+03'\\c"
print ".\\}"
print ".el \\{\\"
print ".sp -1"
print ".IP \\(bu 2.3"
print ".\\}"
inline(substr(firstline, RSTART + RLENGTH))
while ($0) {
sub(/^[[:space:]]+/, "")
sub(/^[+]$/, "")
if (!process($0) && getline <= 0)
fatal("unexpected EOF")
if (match($0, /^[[:space:]]*[*][[:space:]]+/))
break
}
print ".RE"
NeedSpace = 1
return !!$0
}
if (match(firstline, /^[[:space:]]+/)) {
flushspace()
print ".if n .RS 4"
print ".nf"
print ".fam C"
do {
print escape(substr(firstline, RLENGTH + 1))
firstline = $0
} while ($0 && getline > 0)
print ".fam"
print ".fi"
print ".if n .RE"
return 1
}
if (match(firstline, /::$/)) {
inline(substr(firstline, 1, RSTART - 1))
while (match($0, /::$/)) {
print ".br"
inline(substr($0, 1, RSTART - 1))
if (getline <= 0)
fatal("unexpected EOF")
}
print ".RS 4"
while ($0) {
sub(/^[[:space:]]+/, "")
sub(/^[+]$/, "")
if (!process($0) && getline <= 0)
fatal("unexpected EOF")
if (match($0, /::$/))
break
}
print ".RE"
NeedSpace = 1
return !!$0
}
inline(firstline)
return 1
}
{
while (process($0)) {}
}

24
tools/cmake-dump.awk Normal file
View File

@ -0,0 +1,24 @@
# cmake-dump.awk: dump parsed CMake scripts as tables
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Parsed scripts are output in a table, with commands separated using ASCII
# Record Separators, and arguments using Unit Separators.
#
# Example usage: awk -f cmake-parser.awk -f cmake-dump.awk CMakeLists.txt \
# | sed 'y/\x1F\x1E\t\n/\t\n /' \
# | sed -n '/^project\t\([^\t]*\).*\tVERSION\t\([^\t]*\).*/{s//\1 \2/p;q;}'
function sanitize(s) {
if (s ~ /[\x1E\x1F]/)
fatal("conflicting ASCII control characters found in source")
return s
}
Command {
out = sanitize(Command)
for (i in Args)
out = out "\x1F" sanitize(Args[i])
printf "%s\x1E", out
}

252
tools/cmake-parser.awk Normal file
View File

@ -0,0 +1,252 @@
# cmake-parser.awk: rudimentary CMake script parser
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Implemented roughly according to the grammar described in cmake-language(7),
# which is self-conflicting, and not an accurate description.
#
# The result of parsing is stored in the case-normalized Command variable,
# and the Args array. These can be used by subsequent scripts.
function warning(message) {
print FILENAME ":" FNR ": warning: " message > "/dev/stderr"
}
function fatal(message) {
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
exit 1
}
function expect(v) {
if (!v && v == 0)
fatal("broken expectations at `" $0 "'")
return v
}
function literal(v) {
if (substr($0, 1, length(v)) != v)
return 0
$0 = substr($0, length(v) + 1)
return 1
}
function regexp(re) {
if (!match($0, "^" re))
return 0
$0 = substr($0, RLENGTH + 1)
return 1
}
function space() {
return regexp("[ \t]+")
}
function unbracket(len, v) {
do {
if (match($0, "]={" len "}]")) {
v = v substr($0, 1, RSTART - 1)
$0 = substr($0, RSTART + RLENGTH)
return v
}
v = v $0 RS
} while (getline > 0)
fatal("unterminated bracket")
}
function bracket_comment() {
if (!match($0, /^#\[=*\[/))
return 0
$0 = substr($0, RSTART + RLENGTH)
unbracket(RLENGTH - 3)
return 1
}
function line_ending() {
while (space() || bracket_comment()) {}
if (/^#/)
$0 = ""
return !$0
}
# ------------------------------------------------------------------------------
# While elementary expansion of previously set variables is implementable,
# it doesn't seem to be worth the effort.
function expand(s, v) {
v = s
while (match(v, /\\*[$](|ENV|CACHE)[{]/)) {
if (index(substr(v, RSTART), "$") % 2 != 0) {
warning("variable expansion is not supported: " s)
return s
}
v = substr(v, RSTART + RLENGTH)
}
return s
}
function escape_sequence( v) {
if (!literal("\\"))
return 0
if (literal("t")) return "\t"
if (literal("r")) return "\r"
if (literal("n")) return "\n"
# escape_semicolon isn't treated any specially here.
if (regexp("[A-Za-z0-9]"))
fatal("unsupported escape sequence")
if ($0) {
v = substr($0, 1, 1)
$0 = substr($0, 2)
return v
}
if (getline > 0)
return ""
fatal("premature end of file")
}
function quoted_argument( v, unescaped) {
if (!literal("\""))
return 0
v = ""
while (!literal("\"")) {
if (!$0) {
if (getline <= 0)
fatal("premature end of file")
v = v RS
} else if ((unescaped = escape_sequence())) {
if (unescaped == "\\" || unescaped == "$")
v = v "\\"
else if (unescaped == ";")
v = v "\\\\"
v = v unescaped
} else if (unescaped == "") {
# quoted_continuation
} else {
v = v substr($0, 1, 1)
$0 = substr($0, 2)
}
}
return v
}
function finalize_quoted(expanded, v) {
while (match(expanded, /\\./)) {
v = v substr(expanded, 1, RSTART - 1) \
substr(expanded, RSTART + 1, 1)
expanded = substr(expanded, RSTART + RLENGTH)
}
Args[++N] = v expanded
}
function unquoted_argument( v, unescaped) {
while (1) {
if (match($0, /^[^[:space:]()#"\\]+/)) {
v = v substr($0, RSTART, RLENGTH)
$0 = substr($0, RSTART + RLENGTH)
} else if ((unescaped = escape_sequence())) {
if (unescaped == "\\" || unescaped == "$" || unescaped == ";")
v = v "\\"
v = v unescaped
} else if (unescaped == "") {
fatal("unexpected backslash in an unquoted argument")
} else {
# unquoted_legacy is not supported.
return v
}
}
}
function finalize_unquoted(expanded, v) {
while (expanded) {
if (expanded ~ /^;/) {
if (v)
Args[++N] = v
v = ""
expanded = substr(expanded, 2)
} else if (expanded ~ /^\\./) {
v = v substr(expanded, 2, 1)
expanded = substr(expanded, 3)
} else {
v = v substr(expanded, 1, 1)
expanded = substr(expanded, 2)
}
}
if (v)
Args[++N] = v
}
# We keep and reprocess some escape sequences in here.
function argument( arg, expanded, v) {
if (regexp("\\[=*\\["))
Args[++N] = unbracket(RLENGTH - 2)
else if ((arg = quoted_argument()) || arg == "")
finalize_quoted(expand(arg))
else if ((arg = unquoted_argument()))
finalize_unquoted(expand(arg))
else
return 0
return 1
}
# ------------------------------------------------------------------------------
function identifier( v) {
if (!match($0, /^[A-Za-z_][A-Za-z0-9_]*/))
return 0
v = substr($0, 1, RLENGTH)
$0 = substr($0, RLENGTH + 1)
return v
}
function separation() {
if (space() || bracket_comment())
return 1
if (!line_ending())
return 0
if (getline > 0)
return 1
fatal("premature end of file")
}
function command_invocation( level) {
while (space()) {}
Command = identifier()
if (!Command)
return 0
while (space()) {}
Command = tolower(Command)
for (N in Args)
delete Args[N]
N = 0
expect(literal("("))
while (1) {
while (separation()) {}
if (literal(")")) {
if (!level--)
break
Args[++N] = ")"
continue
}
if (literal("(")) {
level++
Args[++N] = "("
continue
}
expect(argument())
if (!/^[()]/)
expect(separation())
}
return 1
}
{
command_invocation()
expect(line_ending())
}

324
tools/lxdrgen-c.awk Normal file
View File

@ -0,0 +1,324 @@
# lxdrgen-c.awk: C backend for lxdrgen.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Neither *_new() nor *_destroy() functions are provided, because they'd only
# be useful for top-levels, and are merely extra malloc()/free() calls.
# Users are expected to reuse buffers.
#
# Similarly, no constructors are produced--those are easy to write manually.
#
# All arrays are deserialized zero-terminated, so u8<> and i8<> can be directly
# used as C strings.
#
# All types must be able to dispose partially zero values going from the back,
# i.e., in the reverse order of deserialization.
function define_internal(name, ctype) {
Types[name] = "internal"
CodegenCType[name] = ctype
}
function define_int(shortname, ctype) {
define_internal(shortname, ctype)
CodegenSerialize[shortname] = \
"\tstr_pack_" shortname "(w, %s);\n"
CodegenDeserialize[shortname] = \
"\tif (!msg_unpacker_" shortname "(r, &%s))\n" \
"\t\treturn false;\n"
}
function define_sint(size) { define_int("i" size, "int" size "_t") }
function define_uint(size) { define_int("u" size, "uint" size "_t") }
function codegen_begin() {
define_sint("8")
define_sint("16")
define_sint("32")
define_sint("64")
define_uint("8")
define_uint("16")
define_uint("32")
define_uint("64")
define_internal("string", "struct str")
CodegenDispose["string"] = "\tstr_free(&%s);\n"
CodegenSerialize["string"] = \
"\tif (!proto_string_serialize(&%s, w))\n" \
"\t\treturn false;\n"
CodegenDeserialize["string"] = \
"\tif (!proto_string_deserialize(&%s, r))\n" \
"\t\treturn false;\n"
define_internal("bool", "bool")
CodegenSerialize["bool"] = \
"\tstr_pack_u8(w, !!%s);\n"
CodegenDeserialize["bool"] = \
"\t{\n" \
"\t\tuint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_u8(r, &v))\n" \
"\t\t\treturn false;\n" \
"\t\t%s = !!v;\n" \
"\t}\n"
print "// Code generated from " FILENAME ". DO NOT EDIT."
print "// This file directly depends on liberty.c, but doesn't include it."
print ""
print "static bool"
print "proto_string_serialize(const struct str *s, struct str *w) {"
print "\tif (s->len > UINT32_MAX)"
print "\t\treturn false;"
print "\tstr_pack_u32(w, s->len);"
print "\tstr_append_str(w, s);"
print "\treturn true;"
print "}"
print ""
print "static bool"
print "proto_string_deserialize(struct str *s, struct msg_unpacker *r) {"
print "\tuint32_t len = 0;"
print "\tif (!msg_unpacker_u32(r, &len))"
print "\t\treturn false;"
print "\tif (msg_unpacker_get_available(r) < len)"
print "\t\treturn false;"
print "\t*s = str_make();"
print "\tstr_append_data(s, r->data + r->offset, len);"
print "\tr->offset += len;"
print "\tif (!utf8_validate (s->str, s->len))"
print "\t\treturn false;"
print "\treturn true;"
print "}"
}
function codegen_constant(name, value) {
print ""
print "enum { " PrefixUpper name " = " value " };"
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields",
"\t" PrefixUpper toupper(cameltosnake(name)) "_" subname \
" = " value ",\n")
}
function codegen_enum(name, cg, ctype) {
ctype = "enum " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tstr_pack_i8(w, %s);\n"
CodegenDeserialize[name] = \
"\t{\n" \
"\t\tint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_i8(r, &v) || !v)\n" \
"\t\t\treturn false;\n" \
"\t\t%s = v;\n" \
"\t}\n"
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}
function codegen_struct_tag(d, cg, f) {
f = "self->" d["name"]
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
append(cg, "dispose", sprintf(CodegenDispose[d["type"]], f))
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
# Do not deserialize here, that would be out of order.
}
function codegen_struct_field(d, cg, f, dispose, serialize, deserialize) {
f = "self->" d["name"]
dispose = CodegenDispose[d["type"]]
serialize = CodegenSerialize[d["type"]]
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
append(cg, "dispose", sprintf(dispose, f))
append(cg, "serialize", sprintf(serialize, f))
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "fields",
"\t" CodegenCType["u32"] " " d["name"] "_len;\n" \
"\t" CodegenCType[d["type"]] " *" d["name"] ";\n")
if (dispose)
append(cg, "dispose", "\tif (" f ")\n" \
"\t\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(indent(sprintf(dispose, f "[i]"))))
append(cg, "dispose", "\tfree(" f ");\n")
append(cg, "serialize", sprintf(CodegenSerialize["u32"], f "_len"))
if (d["type"] == "u8" || d["type"] == "i8") {
append(cg, "serialize",
"\tstr_append_data(w, " f ", " f "_len);\n")
} else if (serialize) {
append(cg, "serialize",
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(sprintf(serialize, f "[i]")))
}
append(cg, "deserialize", sprintf(CodegenDeserialize["u32"], f "_len") \
"\tif (!(" f " = calloc(" f "_len + 1, sizeof *" f ")))\n" \
"\t\treturn false;\n")
if (d["type"] == "u8" || d["type"] == "i8") {
append(cg, "deserialize",
"\tif (msg_unpacker_get_available(r) < " f "_len)\n" \
"\t\treturn false;\n" \
"\tmemcpy(" f ", r->data + r->offset, " f "_len);\n" \
"\tr->offset += " f "_len;\n")
} else if (deserialize) {
append(cg, "deserialize",
"\tfor (size_t i = 0; i < " f "_len; i++)\n" \
indent(sprintf(deserialize, f "[i]")))
}
}
function codegen_struct(name, cg, ctype, funcname) {
ctype = "struct " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
if (cg["dispose"]) {
funcname = PrefixLower cameltosnake(name) "_free"
print ""
print "static void\n" funcname "(" ctype " *self) {"
print cg["dispose"] "}"
CodegenDispose[name] = "\t" funcname "(&%s);\n"
}
if (cg["serialize"]) {
funcname = PrefixLower cameltosnake(name) "_serialize"
print ""
print "static bool\n" \
funcname "(\n\t\tconst " ctype " *self, struct str *w) {"
print cg["serialize"] "\treturn true;"
print "}"
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
"\t\treturn false;\n"
}
if (cg["deserialize"]) {
funcname = PrefixLower cameltosnake(name) "_deserialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
print cg["deserialize"] "\treturn true;"
print "}"
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
"\t\treturn false;\n"
}
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = d["name"]
append(cg, "fields", "\t" CodegenCType[d["type"]] " " d["name"] ";\n")
}
function codegen_union_struct( \
name, casename, cg, scg, structname, fieldname, fullcasename) {
# Don't generate obviously useless structs.
fullcasename = toupper(cameltosnake(cg["tagtype"])) "_" casename
if (!scg["dispose"] && !scg["deserialize"]) {
append(cg, "structless", "\tcase " PrefixUpper fullcasename ":\n")
for (i in scg)
delete scg[i]
return
}
# And thus not all generated structs are present in Types.
structname = name "_" casename
fieldname = tolower(casename)
codegen_struct(structname, scg)
append(cg, "fields", "\t" CodegenCType[structname] " " fieldname ";\n")
if (CodegenDispose[structname])
append(cg, "dispose", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenDispose[structname], "self->" fieldname)) \
"\t\tbreak;\n")
# With no de/serialization code, this will simply recognize the tag.
append(cg, "serialize", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenSerialize[structname], "self->" fieldname)) \
"\t\tbreak;\n")
append(cg, "deserialize", "\tcase " PrefixUpper fullcasename ":\n" \
indent(sprintf(CodegenDeserialize[structname], "self->" fieldname)) \
"\t\tbreak;\n")
}
function codegen_union(name, cg, exhaustive, f, ctype, funcname) {
ctype = "union " PrefixLower cameltosnake(name)
print ""
print ctype " {"
print cg["fields"] "};"
f = "self->" cg["tagname"]
if (cg["dispose"]) {
funcname = PrefixLower cameltosnake(name) "_free"
print ""
print "static void\n" funcname "(" ctype " *self) {"
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] \
indent(sprintf(CodegenDispose[cg["tagtype"]], f)) "\t\tbreak;"
print cg["dispose"] "\tdefault:"
print "\t\tbreak;"
print "\t}"
print "}"
CodegenDispose[name] = "\t" funcname "(&%s);\n"
}
{
funcname = PrefixLower cameltosnake(name) "_serialize"
print ""
print "static bool\n" \
funcname "(\n\t\tconst " ctype " *self, struct str *w) {"
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] \
indent(sprintf(CodegenSerialize[cg["tagtype"]], f)) "\t\tbreak;"
print cg["serialize"] "\tdefault:"
print "\t\treturn false;"
print "\t}"
print "\treturn true;"
print "}"
CodegenSerialize[name] = "\tif (!" funcname "(&%s, w))\n" \
"\t\treturn false;\n"
}
{
funcname = PrefixLower cameltosnake(name) "_deserialize"
print ""
print "static bool\n" \
funcname "(\n\t\t" ctype " *self, struct msg_unpacker *r) {"
print sprintf(CodegenDeserialize[cg["tagtype"]], f)
print "\tswitch (" f ") {"
if (cg["structless"])
print cg["structless"] "\t\tbreak;"
print cg["deserialize"] "\tdefault:"
print "\t\treturn false;"
print "\t}"
print "\treturn true;"
print "}"
CodegenDeserialize[name] = "\tif (!" funcname "(&%s, r))\n" \
"\t\treturn false;\n"
}
CodegenCType[name] = ctype
for (i in cg)
delete cg[i]
}

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

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]
}

540
tools/lxdrgen-go.awk Normal file
View File

@ -0,0 +1,540 @@
# lxdrgen-go.awk: Go backend for lxdrgen.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend also enables proxying to other endpoints using JSON.
function define_internal(name, gotype) {
Types[name] = "internal"
CodegenGoType[name] = gotype
}
function define_sint(size, shortname, gotype) {
shortname = "i" size
gotype = "int" size
define_internal(shortname, gotype)
CodegenAppendJSON[shortname] = \
"\tb = strconv.AppendInt(b, int64(%s), 10)\n"
if (size == 8) {
CodegenSerialize[shortname] = "\tdata = append(data, uint8(%s))\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = int8(data[0]), data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
return
}
CodegenSerialize[shortname] = \
"\tdata = binary.BigEndian.AppendUint" size "(data, uint" size "(%s))\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= " (size / 8) " {\n" \
"\t\t%s = " gotype "(binary.BigEndian.Uint" size "(data))\n" \
"\t\tdata = data[" (size / 8) ":]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
}
function define_uint(size, shortname, gotype) {
# Both []byte and []uint8 luckily marshal as base64-encoded JSON strings,
# so there's no need to rename the type as an exception.
shortname = "u" size
gotype = "uint" size
define_internal(shortname, gotype)
CodegenAppendJSON[shortname] = \
"\tb = strconv.AppendUint(b, uint64(%s), 10)\n"
if (size == 8) {
CodegenSerialize[shortname] = "\tdata = append(data, %s)\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = data[0], data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
return
}
CodegenSerialize[shortname] = \
"\tdata = binary.BigEndian.AppendUint" size "(data, %s)\n"
CodegenDeserialize[shortname] = \
"\tif len(data) >= " (size / 8) " {\n" \
"\t\t%s = binary.BigEndian.Uint" size "(data)\n" \
"\t\tdata = data[" (size / 8) ":]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
}
# Currently two outputs cannot coexist within the same package.
function codegen_private(name) {
return "proto" name
}
function codegen_begin( funcname) {
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")
# Cater to "go generate", for what it's worth.
CodegenPackage = ENV["GOPACKAGE"]
if (!CodegenPackage)
CodegenPackage = "main"
print "// Code generated from " FILENAME ". DO NOT EDIT."
print ""
print "package " CodegenPackage
print ""
print "import ("
print "\t`encoding/base64`"
print "\t`encoding/binary`"
print "\t`encoding/json`"
print "\t`errors`"
print "\t`math`"
print "\t`strconv`"
print "\t`unicode/utf8`"
print ")"
print ""
print "// This is a hack to always use the base64 import."
print "var _ = base64.StdEncoding"
print ""
CodegenAppendJSON["bool"] = \
"\tb = strconv.AppendBool(b, %s)\n"
CodegenSerialize["bool"] = \
"\tif %s {\n" \
"\t\tdata = append(data, 1)\n" \
"\t} else {\n" \
"\t\tdata = append(data, 0)\n" \
"\t}\n"
funcname = codegen_private("ConsumeBoolFrom")
print "// " funcname " tries to deserialize a boolean value"
print "// from the beginning of a byte stream. When successful,"
print "// it returns a subslice with any data that might follow."
print "func " funcname "(data []byte, b *bool) ([]byte, bool) {"
print "\tif len(data) < 1 {"
print "\t\treturn nil, false"
print "\t}"
print "\tif data[0] != 0 {"
print "\t\t*b = true"
print "\t} else {"
print "\t\t*b = false"
print "\t}"
print "\treturn data[1:], true"
print "}"
print ""
CodegenDeserialize["bool"] = \
"\tif data, ok = " funcname "(data, &%s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
funcname = codegen_private("AppendStringTo")
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 "\t\treturn nil, false"
print "\t}"
print "\tdata = binary.BigEndian.AppendUint32(data, uint32(len(s)))"
print "\treturn append(data, s...), true"
print "}"
print ""
CodegenSerialize["string"] = \
"\tif data, ok = " funcname "(data, %s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
funcname = codegen_private("ConsumeStringFrom")
print "// " funcname " tries to deserialize a string value"
print "// from the beginning of a byte stream. When successful,"
print "// it returns a subslice with any data that might follow."
print "func " funcname "(data []byte, s *string) ([]byte, bool) {"
print "\tif len(data) < 4 {"
print "\t\treturn nil, false"
print "\t}"
print "\tlength := binary.BigEndian.Uint32(data)"
print "\tif data = data[4:]; uint64(len(data)) < uint64(length) {"
print "\t\treturn nil, false"
print "\t}"
print "\t*s = string(data[:length])"
print "\tif !utf8.ValidString(*s) {"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data[length:], true"
print "}"
print ""
CodegenDeserialize["string"] = \
"\tif data, ok = " funcname "(data, &%s); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
funcname = codegen_private("UnmarshalEnumJSON")
print "// " funcname " converts a JSON fragment to an integer,"
print "// ensuring that it's within the expected range of enum values."
print "func " funcname "(data []byte) (int64, error) {"
print "\tvar n int64"
print "\tif err := json.Unmarshal(data, &n); err != nil {"
print "\t\treturn 0, err"
print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {"
print "\t\treturn 0, errors.New(`integer out of range`)"
print "\t} else {"
print "\t\treturn n, nil"
print "\t}"
print "}"
print ""
}
function codegen_constant(name, value) {
print "const " PrefixCamel snaketocamel(name) " = " value
print ""
}
function codegen_enum_value(name, subname, value, cg, goname) {
goname = PrefixCamel name snaketocamel(subname)
append(cg, "fields",
"\t" goname " = " value "\n")
append(cg, "stringer",
"\tcase " goname ":\n" \
"\t\treturn `" snaketocamel(subname) "`\n")
append(cg, "marshal",
goname ",\n")
append(cg, "unmarshal",
"\tcase `" snaketocamel(subname) "`:\n" \
"\t\t*v = " goname "\n")
}
function codegen_enum(name, cg, gotype, fields, funcname) {
gotype = PrefixCamel name
print "type " gotype " int8"
print ""
print "const ("
print cg["fields"] ")"
print ""
print "func (v " gotype ") String() string {"
print "\tswitch v {"
print cg["stringer"] "\tdefault:"
print "\t\treturn strconv.Itoa(int(v))"
print "\t}"
print "}"
print ""
CodegenIsMarshaler[name] = 1
fields = cg["marshal"]
sub(/,\n$/, ":", fields)
gsub(/\n/, "\n\t", fields)
print "func (v " gotype ") MarshalJSON() ([]byte, error) {"
print "\tswitch v {"
print indent("case " fields)
print "\t\treturn []byte(`\"` + v.String() + `\"`), nil"
print "\t}"
print "\treturn json.Marshal(int(v))"
print "}"
print ""
funcname = codegen_private("UnmarshalEnumJSON")
print "func (v *" gotype ") UnmarshalJSON(data []byte) error {"
print "\tvar s string"
print "\tif json.Unmarshal(data, &s) == nil {"
print "\t\t// Handled below."
print "\t} else if n, err := " funcname "(data); err != nil {"
print "\t\treturn err"
print "\t} else {"
print "\t\t*v = " gotype "(n)"
print "\t\treturn nil"
print "\t}"
print ""
print "\tswitch s {"
print cg["unmarshal"] "\tdefault:"
print "\t\treturn errors.New(`unrecognized value: ` + s)"
print "\t}"
print "\treturn nil"
print "}"
print ""
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n"
CodegenDeserialize[name] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = " gotype "(data[0]), data[1:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}
function codegen_marshal(type, f, marshal) {
if (CodegenAppendJSON[type])
return sprintf(CodegenAppendJSON[type], f)
# Complex types are json.Marshalers, there's no need to json.Marshal(&f).
if (CodegenIsMarshaler[type])
marshal = f ".MarshalJSON()"
else
marshal = "json.Marshal(" f ")"
return \
"\tif j, err := " marshal "; err != nil {\n" \
"\t\treturn nil, err\n" \
"\t} else {\n" \
"\t\tb = append(b, j...)\n" \
"\t}\n"
}
function codegen_struct_field_marshal(d, cg, camel, f, marshal) {
camel = snaketocamel(d["name"])
f = "s." camel
if (!d["isarray"]) {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \
codegen_marshal(d["type"], f))
return
}
# Note that we do not produce `null` for nil slices, unlike encoding/json.
# And arrays never get deserialized as such.
if (d["type"] == "u8") {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":\"`...)\n" \
"\tb = append(b, base64.StdEncoding.EncodeToString(" f ")...)\n" \
"\tb = append(b, '\"')\n")
return
}
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":[`...)\n" \
"\tfor i := 0; i < len(" f "); i++ {\n" \
"\t\tif i > 0 {\n" \
"\t\t\tb = append(b, ',')\n" \
"\t\t}\n" \
indent(codegen_marshal(d["type"], f "[i]")) \
"\t}\n" \
"\tb = append(b, ']')\n")
}
function codegen_struct_field(d, cg, camel, f, serialize, deserialize) {
codegen_struct_field_marshal(d, cg)
camel = snaketocamel(d["name"])
f = "s." camel
serialize = CodegenSerialize[d["type"]]
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
append(cg, "serialize", sprintf(serialize, f))
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "fields", "\t" camel " []" CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
# XXX: This should also check if it isn't out-of-range for any reason.
append(cg, "serialize",
sprintf(CodegenSerialize["u32"], "uint32(len(" f "))"))
if (d["type"] == "u8") {
append(cg, "serialize",
"\tdata = append(data, " f "...)\n")
} else {
append(cg, "serialize",
"\tfor i := 0; i < len(" f "); i++ {\n" \
indent(sprintf(serialize, f "[i]")) \
"\t}\n")
}
append(cg, "deserialize",
"\t{\n" \
"\t\tvar length uint32\n" \
indent(sprintf(CodegenDeserialize["u32"], "length")))
if (d["type"] == "u8") {
append(cg, "deserialize",
"\t\tif uint64(len(data)) < uint64(length) {\n" \
"\t\t\treturn nil, false\n" \
"\t\t}\n" \
"\t\t" f ", data = data[:length], data[length:]\n" \
"\t}\n")
} else {
append(cg, "deserialize",
"\t\t" f " = make([]" CodegenGoType[d["type"]] ", length)\n" \
"\t}\n" \
"\tfor i := 0; i < len(" f "); i++ {\n" \
indent(sprintf(deserialize, f "[i]")) \
"\t}\n")
}
}
function codegen_struct_tag(d, cg, camel, f) {
codegen_struct_field_marshal(d, cg)
camel = snaketocamel(d["name"])
f = "s." camel
append(cg, "fields", "\t" camel " " CodegenGoType[d["type"]] \
" `json:\"" decapitalize(camel) "\"`\n")
append(cg, "serialize", sprintf(CodegenSerialize[d["type"]], f))
# Do not deserialize here, that is already done by the containing union.
}
function codegen_struct(name, cg, gotype) {
gotype = PrefixCamel name
print "type " gotype " struct {\n" cg["fields"] "}\n"
if (cg["marshal"]) {
CodegenIsMarshaler[name] = 1
print "func (s *" gotype ") MarshalJSON() ([]byte, error) {"
print "\tb := []byte{}"
print cg["marshal"] "\tb[0] = '{'"
print "\treturn append(b, '}'), nil"
print "}"
print ""
}
if (cg["serialize"]) {
print "func (s *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print cg["serialize"] "\treturn data, ok"
print "}"
print ""
CodegenSerialize[name] = \
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
}
if (cg["deserialize"]) {
print "func (s *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
print "\tok := true"
print cg["deserialize"] "\treturn data, ok"
print "}"
print ""
CodegenDeserialize[name] = \
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
}
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}
function codegen_union_tag(name, d, cg) {
cg["tagtype"] = d["type"]
cg["tagname"] = snaketocamel(d["name"])
# The tag is implied from the type of struct stored in the interface.
}
function codegen_union_struct(name, casename, cg, scg, structname, init) {
# And thus not all generated structs are present in Types.
structname = name snaketocamel(casename)
codegen_struct(structname, scg)
init = CodegenGoType[structname] "{" cg["tagname"] \
": " decapitalize(cg["tagname"]) "}"
append(cg, "unmarshal",
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
"\t\terr = json.Unmarshal(data, &s)\n" \
"\t\tu.Interface = &s\n")
append(cg, "serialize",
"\tcase *" CodegenGoType[structname] ":\n" \
indent(sprintf(CodegenSerialize[structname], "union")))
append(cg, "deserialize",
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
indent(sprintf(CodegenDeserialize[structname], "s")) \
"\t\tu.Interface = &s\n")
}
function codegen_union(name, cg, exhaustive, gotype, tagvar) {
gotype = PrefixCamel name
print "type " gotype " struct {"
print "\tInterface any"
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 "}"
print ""
tagvar = decapitalize(cg["tagname"])
print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {"
print "\tvar t struct {"
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." cg["tagname"] "; " tagvar " {"
print cg["unmarshal"] "\tdefault:"
print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())"
print "\t}"
print "\treturn err"
print "}"
print ""
# XXX: Consider changing the interface into an AppendTo/ConsumeFrom one,
# that would eliminate these type case switches entirely.
# On the other hand, it would make it possible to send unsuitable structs.
print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print "\tswitch union := u.Interface.(type) {"
print cg["serialize"] "\tdefault:"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"
print "}"
print ""
CodegenSerialize[name] = \
"\tif data, ok = %s.AppendTo(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
print "func (u *" gotype ") ConsumeFrom(data []byte) ([]byte, bool) {"
print "\tok := true"
print "\tvar " tagvar " " CodegenGoType[cg["tagtype"]]
print sprintf(CodegenDeserialize[cg["tagtype"]], tagvar)
print "\tswitch " tagvar " {"
print cg["deserialize"] "\tdefault:"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"
print "}"
print ""
CodegenDeserialize[name] = \
"\tif data, ok = %s.ConsumeFrom(data); !ok {\n" \
"\t\treturn nil, ok\n" \
"\t}\n"
CodegenGoType[name] = gotype
for (i in cg)
delete cg[i]
}

226
tools/lxdrgen-mjs.awk Normal file
View File

@ -0,0 +1,226 @@
# lxdrgen-mjs.awk: Javascript backend for lxdrgen.awk.
#
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend is currently for decoding the binary format only.
# (JSON is way too expensive to process and transfer.)
#
# Import the resulting script as a Javascript module.
# Identifiers intentionally aren't prefixed.
function define_internal(name) {
Types[name] = "internal"
}
function define_sint(size, shortname) {
shortname = "i" size
define_internal(shortname)
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
print ""
print "\t" shortname "() {"
if (size == "64") {
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
print "\t\tconst " shortname \
" = Number(this.getBigInt" size "(this.offset))"
} else {
print "\t\tconst " shortname " = this.getInt" size "(this.offset)"
}
print "\t\tthis.offset += " (size / 8)
print "\t\treturn " shortname
print "\t}"
}
function define_uint(size, shortname) {
shortname = "u" size
define_internal(shortname)
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
print ""
print "\t" shortname "() {"
if (size == "64") {
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
print "\t\tconst " shortname \
" = Number(this.getBigUint" size "(this.offset))"
} else {
print "\t\tconst " shortname " = this.getUint" size "(this.offset)"
}
print "\t\tthis.offset += " (size / 8)
print "\t\treturn " shortname
print "\t}"
}
function codegen_begin() {
print "// Code generated from " FILENAME ". DO NOT EDIT."
print ""
print "export class Reader extends DataView {"
print "\tconstructor() {"
print "\t\tsuper(...arguments)"
print "\t\tthis.offset = 0"
print "\t\tthis.decoder = new TextDecoder('utf-8', {fatal: true})"
print "\t}"
print ""
print "\tget empty() {"
print "\t\treturn this.byteLength <= this.offset"
print "\t}"
print ""
print "\trequire(len) {"
print "\t\tif (this.byteLength - this.offset < len)"
print "\t\t\tthrow `Premature end of data`"
print "\t\treturn this.byteOffset + this.offset"
print "\t}"
define_internal("string")
CodegenDeserialize["string"] = "\t%s = r.string()\n"
print ""
print "\tstring() {"
print "\t\tconst len = this.getUint32(this.offset)"
print "\t\tthis.offset += 4"
print "\t\tconst array = new Uint8Array("
print "\t\t\tthis.buffer, this.require(len), len)"
print "\t\tthis.offset += len"
print "\t\treturn this.decoder.decode(array)"
print "\t}"
define_internal("bool")
CodegenDeserialize["bool"] = "\t%s = r.bool()\n"
print ""
print "\tbool() {"
print "\t\tconst u8 = this.getUint8(this.offset)"
print "\t\tthis.offset += 1"
print "\t\treturn u8 != 0"
print "\t}"
define_sint("8")
define_sint("16")
define_sint("32")
define_sint("64")
define_uint("8")
define_uint("16")
define_uint("32")
define_uint("64")
print "}"
}
function codegen_constant(name, value) {
print ""
print "export const " decapitalize(snaketocamel(name)) " = " value
}
function codegen_enum_value(name, subname, value, cg) {
append(cg, "fields", "\t" snaketocamel(subname) ": " value ",\n")
}
function codegen_enum(name, cg) {
print ""
print "export const " name " = Object.freeze({"
print cg["fields"] "})"
CodegenDeserialize[name] = "\t%s = r.i8()\n"
for (i in cg)
delete cg[i]
}
function codegen_struct_field(d, cg, camel, f, deserialize) {
camel = decapitalize(snaketocamel(d["name"]))
f = "s." camel
append(cg, "fields", "\t" camel "\n")
deserialize = CodegenDeserialize[d["type"]]
if (!d["isarray"]) {
append(cg, "deserialize", sprintf(deserialize, f))
return
}
append(cg, "deserialize",
"\t{\n" \
indent(sprintf(CodegenDeserialize["u32"], "const len")))
if (d["type"] == "u8") {
append(cg, "deserialize",
"\t\t" f " = new Uint8Array(\n" \
"\t\t\tr.buffer, r.require(len), len)\n" \
"\t\tr.offset += len\n" \
"\t}\n")
return
}
if (d["type"] == "i8") {
append(cg, "deserialize",
"\t\t" f " = new Int8Array(\n" \
"\t\t\tr.buffer, r.require(len), len)\n" \
"\t\tr.offset += len\n" \
"\t}\n")
return
}
append(cg, "deserialize",
"\t\t" f " = new Array(len)\n" \
"\t}\n" \
"\tfor (let i = 0; i < " f ".length; i++)\n" \
indent(sprintf(deserialize, f "[i]")))
}
function codegen_struct_tag(d, cg) {
append(cg, "fields", "\t" decapitalize(snaketocamel(d["name"])) "\n")
# Do not deserialize here, that is already done by the containing union.
}
function codegen_struct(name, cg) {
print ""
print "export class " name " {"
print cg["fields"] cg["methods"]
print "\tstatic deserialize(r) {"
print "\t\tconst s = new " name "()"
print indent(cg["deserialize"]) "\t\treturn s"
print "\t}"
print "}"
CodegenDeserialize[name] = "\t%s = " name ".deserialize(r)\n"
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, structname) {
append(scg, "methods",
"\n" \
"\tconstructor() {\n" \
"\t\tthis." cg["tagname"] \
" = " cg["tagtype"] "." snaketocamel(casename) "\n" \
"\t}\n")
# And thus not all generated structs are present in Types.
structname = name snaketocamel(casename)
codegen_struct(structname, scg)
append(cg, "deserialize",
"\tcase " cg["tagtype"] "." snaketocamel(casename) ":\n" \
"\t{\n" \
indent(sprintf(CodegenDeserialize[structname], "const s")) \
"\t\treturn s\n" \
"\t}\n")
}
function codegen_union(name, cg, exhaustive, tagvar) {
tagvar = cg["tagname"]
print ""
print "export function deserialize" name "(r) {"
print sprintf(CodegenDeserialize[cg["tagtype"]], "const " tagvar) \
"\tswitch (" tagvar ") {"
print cg["deserialize"] "\tdefault:"
print "\t\tthrow `Unknown " cg["tagtype"] " (${tagvar})`"
print "\t}"
print "}"
CodegenDeserialize[name] = "\t%s = deserialize" name "(r)\n"
for (i in cg)
delete cg[i]
}

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]
}

298
tools/lxdrgen.awk Normal file
View File

@ -0,0 +1,298 @@
# lxdrgen.awk: an XDR-derived code generator for network protocols.
#
# 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 \
# -v PrefixCamel=Foo foo.lxdr > foo.{c,go,mjs} | {clang-format,gofmt,...}
# --- Utilities ----------------------------------------------------------------
function cameltosnake(s) {
while (match(s, /[[:lower:]][[:upper:]]/)) {
s = substr(s, 1, RSTART) "_" \
tolower(substr(s, RSTART + 1, RLENGTH - 1)) \
substr(s, RSTART + RLENGTH)
}
return tolower(s)
}
function snaketocamel(s) {
s = toupper(substr(s, 1, 1)) tolower(substr(s, 2))
while (match(s, /_[[:alnum:]]/)) {
s = substr(s, 1, RSTART - 1) \
toupper(substr(s, RSTART + 1, RLENGTH - 1)) \
substr(s, RSTART + RLENGTH)
}
return s
}
function decapitalize(s) {
if (match(s, /^[[:upper:]][[:lower:]]/))
return tolower(substr(s, 1, 1)) substr(s, 2)
if (match(s, /^[[:upper:]]$/))
return tolower(s)
return s
}
function indent(s) {
if (!s)
return s
gsub(/\n/, "\n\t", s)
sub(/\t*$/, "", s)
return "\t" s
}
function append(a, key, value) {
a[key] = a[key] value
}
# --- Parsing ------------------------------------------------------------------
function fatal(message) {
print "// " FILENAME ":" FNR ": fatal error: " message
print FILENAME ":" FNR ": fatal error: " message > "/dev/stderr"
exit 1
}
function skipcomment() {
do {
if (match($0, /[*]\//)) {
$0 = substr($0, RSTART + RLENGTH)
return
}
} while (getline > 0)
fatal("unterminated block comment")
}
function nexttoken() {
do {
if (match($0, /^[[:space:]]+/)) {
$0 = substr($0, RLENGTH + 1)
} else if (match($0, /^\/\/.*/)) {
$0 = ""
} else if (match($0, /^\/[*]/)) {
$0 = substr($0, RLENGTH + 1)
skipcomment()
} else if (match($0, /^[[:alpha:]][[:alnum:]_]*/)) {
Token = substr($0, 1, RLENGTH)
$0 = substr($0, RLENGTH + 1)
return Token
# AWK implementations rarely support non-decimal notations
# in their implicit string-to-number conversions.
} else if (match($0, /^(0|-?[1-9][0-9]*)/)) {
Token = substr($0, 1, RLENGTH)
$0 = substr($0, RLENGTH + 1)
return Token
} else if ($0) {
Token = substr($0, 1, 1)
$0 = substr($0, 2)
return Token
}
} while ($0 || getline > 0)
Token = ""
return Token
}
function expect(v) {
if (!v)
fatal("broken expectations at `" Token "' before `" $0 "'")
return v
}
function accept(what) {
if (Token != what)
return 0
nexttoken()
return 1
}
function identifier( v) {
if (Token !~ /^[[:alpha:]]/)
return 0
v = Token
nexttoken()
return v
}
function number( v) {
if (Token !~ /^(0|-?[1-9])/)
return 0
v = Token
nexttoken()
return v
}
function readnumber( ident) {
ident = identifier()
if (!ident)
return expect(number())
if (!(ident in Consts))
fatal("unknown constant: " ident)
return Consts[ident]
}
function defconst( ident, num) {
if (!accept("const"))
return 0
ident = expect(identifier())
expect(accept("="))
num = readnumber()
if (ident in Consts)
fatal("constant redefined: " ident)
Consts[ident] = num
codegen_constant(ident, num)
return 1
}
function readtype( ident) {
ident = deftype()
if (ident)
return ident
ident = identifier()
if (!ident)
return 0
if (!(ident in Types))
fatal("unknown type: " ident)
return ident
}
function defenum( name, ident, value, cg) {
delete cg[0]
name = expect(identifier())
expect(accept("{"))
while (!accept("}")) {
ident = expect(identifier())
value = value + 1
if (accept("="))
value = readnumber() + 0
if (!value)
fatal("enumeration values cannot be zero")
if (value < -128 || value > 127)
fatal("enumeration value out of range")
expect(accept(","))
append(EnumValues, name, SUBSEP ident)
if (EnumValues[name, ident]++)
fatal("duplicate enum value: " ident)
codegen_enum_value(name, ident, value, cg)
}
Types[name] = "enum"
codegen_enum(name, cg)
return name
}
function readfield(out, nonvoid) {
nonvoid = !accept("void")
if (nonvoid) {
out["type"] = expect(readtype())
out["name"] = expect(identifier())
# TODO: Consider supporting XDR's VLA length limits here.
# TODO: Consider supporting XDR's fixed-length syntax for string limits.
out["isarray"] = accept("<") && expect(accept(">"))
}
expect(accept(";"))
return nonvoid
}
function defstruct( name, d, cg) {
delete d[0]
delete cg[0]
name = expect(identifier())
expect(accept("{"))
while (!accept("}")) {
if (readfield(d))
codegen_struct_field(d, cg)
}
Types[name] = "struct"
codegen_struct(name, cg)
return name
}
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i,
unseen, exhaustive) {
delete cg[0]
delete scg[0]
delete d[0]
name = expect(identifier())
expect(accept("switch"))
expect(accept("("))
tag["type"] = tagtype = expect(readtype())
tag["name"] = expect(identifier())
expect(accept(")"))
if (Types[tagtype] != "enum")
fatal("not an enum type: " tagtype)
codegen_union_tag(name, tag, cg)
split(EnumValues[tagtype], a, SUBSEP)
for (i in a)
unseen[a[i]]++
expect(accept("{"))
while (!accept("}")) {
if (accept("case")) {
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
tagvalue = expect(identifier())
expect(accept(":"))
if (!unseen[tagvalue]--)
fatal("no such value or duplicate case: " tagtype "." tagvalue)
codegen_struct_tag(tag, scg)
} else if (tagvalue) {
if (readfield(d))
codegen_struct_field(d, scg)
} else {
fatal("union fields must fall under a case")
}
}
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
# 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, exhaustive)
return name
}
function deftype() {
if (accept("enum"))
return defenum()
if (accept("struct"))
return defstruct()
if (accept("union"))
return defunion()
return 0
}
{
if (PrefixCamel) {
PrefixLower = tolower(cameltosnake(PrefixCamel)) "_"
PrefixUpper = toupper(cameltosnake(PrefixCamel)) "_"
}
# This is not in a BEGIN clause (even though it consumes all input),
# so that the code generator can insert the first FILENAME.
codegen_begin()
nexttoken()
while (Token != "") {
expect(defconst() || deftype())
expect(accept(";"))
}
}