Compare commits

...

84 Commits

Author SHA1 Message Date
31ae400852 LibertyXDR: update VIM syntax highlight file
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
2025-05-07 19:47:43 +02:00
b69d3f8692 LibertyXDR: add support for default in unions 2025-05-07 19:42:46 +02:00
9a26284a64 wdye: clean up protected calls
Have a common way of catching Lua errors for resource cleanup purposes.
2025-01-15 02:21:23 +01:00
0f20cce9c8 wdye: pass script arguments
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-10 10:58:27 +01:00
017cb1d570 MPD client: tolerate usage while disconnected
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
If the client is password-protected, this will not automagically
make queued up commands work, but it's better than hitting
the poller assertion.
2025-01-08 08:07:46 +01:00
1642d387f3 wdye: rename the self-test
add_subdirectory imports it to parent projects, so be more indicative.
2025-01-08 06:24:05 +01:00
af889b733e wdye: ensure we find our own config.h
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-08 06:14:47 +01:00
51231d84ba wdye: clean up, add process.pid
All checks were successful
OpenBSD 7.5 Success
Alpine 3.20 Success
2025-01-07 03:16:37 +01:00
6c47e384f5 wdye: optionally produce asciicast v2 logs
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
I've been fairly disappointed with asciinema,
but it's slightly better than nothing.
2025-01-06 17:03:54 +01:00
914e743dc4 wdye: don't add the script path on error
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
Lua already provides this for us, including the line number.
2025-01-06 14:40:58 +01:00
37a8f16235 wdye: enable waiting for processes 2025-01-06 14:29:41 +01:00
9fe576ae9e wdye: read out the whole terminfo database
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
Also update LICENSE years.
2025-01-06 11:59:46 +01:00
5c02778ff8 wdye: improve portability
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-06 10:14:49 +01:00
e40d56152d Add an Expect-like tool
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
This is to provide an Expect utility with a minimal dependency tree
for C-based projects.  It also addresses some Tcl Expect design issues,
as perceived by me.
2025-01-06 08:30:14 +01:00
21379d4c02 Update README
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-01 23:36:55 +01:00
9268fb8eba help2adoc: fix nawk
All checks were successful
Alpine 3.20 Success
2024-12-31 20:34:48 +01:00
b01df19b80 asciiman: have fewer "unexpected EOF" situations
Some checks failed
Alpine 3.20 Scripts failed
Easily caused by the new help2adoc.
2024-12-31 20:25:51 +01:00
09e635cf97 Add a --help/--version to AsciiDoc convertor
liberty is now self-contained, from opt_handler to manual page.
2024-12-31 20:25:51 +01:00
7560e8700e cmake-parser: improve portability 2024-12-31 06:47:31 +01:00
1930f138d4 IconUtils: add Apple Icon Image format support
All checks were successful
Alpine 3.20 Success
2024-12-17 06:20:12 +01:00
32cbb15266 Serialize integer-ish config keys properly
All checks were successful
Alpine 3.20 Success
2024-12-16 09:09:03 +01:00
149938cc44 lxdrgen-cpp: add a Qt backend
All checks were successful
Alpine 3.20 Success
Motivation: some Android NDKs do not have iconv.
2024-12-15 06:44:06 +01:00
62f8a7d05f lxdrgen-cpp: fix test build on macOS
All checks were successful
Alpine 3.20 Success
2024-12-04 17:44:30 +01:00
492815c8fc lxdrgen-go: fix compatibility with 32-bit targets
All checks were successful
Alpine 3.20 Success
2024-11-09 17:06:46 +01:00
aacf1b1d47 lxdrgen-go: improve usability
All checks were successful
Alpine 3.20 Success
Turning union tags into read-only methods of actual types:
 - eliminates duplicated JSON unmarshalling of tags,
 - makes AppendTo/ConsumeFrom symmetrical in nature,
 - eliminates duplicated AppendTo code,
 - eliminates trivial AppendTo methods for subtypes without fields,
 - gives us an opportunity to use a more specific interface than "any"
   (the type being anonymous is an acknowledged inconvenience).

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

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

This makes use of a new related liberty-xdg module,
which can be used separately.
2024-02-10 10:07:09 +01:00
f32bcbd7f4 Add helper functions for reading binary numbers
And make use of them.
2024-02-10 07:02:54 +01:00
c5424e6992 Comment on write_file_safe()'s actual safety 2024-02-10 06:16:27 +01:00
cb9d162a26 Add a CMake module for icon conversions 2024-02-10 05:39:15 +01:00
db6357db9a CMakeLists.txt: declare compatibility with 3.27
Sadly, the 3.5 deprecation warning doesn't go away after this.
2023-08-01 03:22:17 +02:00
7a0cb13a1a MPD client: fix build on OpenIndiana 2023-07-24 08:33:45 +02:00
b6c54073cd Find ncursesw on OpenIndiana 2023-07-24 08:09:08 +02:00
62166f9679 lxdrgen-cpp-win32: fix return value handling 2023-07-10 09:34:42 +02:00
2edc9c6fd1 Add a C++ backend for LibertyXDR
Also change the C backend so that it also de/serializes
unions without any other fields besides the tag.
2023-07-07 16:43:52 +02:00
f78f8a70f1 lxdrgen-swift: fix prefix handling
"Any prefix will work, so long as it's 'Relay'."
2023-07-06 11:01:51 +02:00
be9a3e693e lxdrgen-swift: fix warnings with exhaustive unions 2023-07-06 06:54:23 +02:00
53197b51e5 Add a Swift backend for LibertyXDR 2023-07-06 06:54:22 +02:00
8466d0d850 CMakeLists.txt: link properly 2023-07-04 08:08:29 +02:00
4c2874649d liberty-xui: fix build on systems without A_ITALIC 2023-07-04 06:40:54 +02:00
717c301207 lxdrgen: fix decapitalization
decapitalize() is typically called on snaketocamel() output,
which always makes the first letter uppercase.
2023-06-28 16:24:59 +02:00
091f92bab3 liberty-xui: fix a build warning
On macOS, TIOCGWINSZ seems to be defined earlier.
2023-06-28 16:24:59 +02:00
556c25855e Fix a CMake warning 2023-06-20 01:24:29 +02:00
d01a1ff034 Turn liberty-tui into a terminal/X11 hybrid
Importing code from nncmpp, adjusting it to work with hex as well.
2023-06-19 13:06:12 +02:00
bd1013f16a Parse block attribute list lines
This code is of strategic importance, but its output is so far unused.
2023-06-11 10:02:16 +02:00
29bf109a51 asciiman: improve attribute handling 2022-10-09 18:43:37 +02:00
0e86ffe7c3 asciiman: fixes, improve mandoc compatibility 2022-10-09 01:01:08 +02:00
cbeb4e3133 Improve documentation 2022-10-04 01:46:41 +02:00
0f3ed14575 asciiman: support attribute passing 2022-09-30 18:13:01 +02:00
089593bb0f asciiman: render libertyxdr.adoc properly 2022-09-30 15:01:14 +02:00
035bfe5e81 Document the recently added scripts 2022-09-30 03:09:04 +02:00
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
4c3837ae2c cmake-parser: clean up 2022-09-28 00:06:51 +02:00
af2756ee01 Add a rudimentary CMake script parser 2022-09-27 23:27:06 +02:00
688c458095 asciiman: fix the first line of output 2022-09-25 21:11:45 +02:00
34460ca715 asciiman: improve command escaping 2022-09-25 20:55:51 +02:00
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
22a121383f Allow for overriding feature test macros 2022-09-11 00:44:14 +02:00
0e89bb9f46 Add some consts to function arguments 2022-09-01 12:44:58 +02:00
63aed8f0fd Fix up the PEG change from the last commit
This is not a regular expression.
2022-08-14 19:09:52 +02:00
f545be725d Extend string syntax in config
And actually test the results of string parsing.
2022-08-14 18:14:21 +02:00
7e8e085c97 Remove pointless, wrong constant
C99 allows trailing commas.
2021-12-18 00:25:13 +01:00
782a9a5977 Import libpulse poller integration, add tests 2021-11-07 15:37:21 +01:00
34f86651f6 Update .gitignore 2021-10-30 03:31:08 +02:00
5dec46df2c Add clang-format configuration, clean up 2021-10-30 03:10:17 +02:00
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
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
960420df3e Escape DEL character in config_item_write_string() 2020-10-31 21:28:29 +01:00
d71c47f8ce CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 15:32:26 +01:00
425ea57b17 CMakeLists.txt: clean up OpenBSD support
A few things might have changed.
2020-10-29 15:31:05 +01:00
8822d06091 Don't suppress -Wimplicit-fallthrough
Might have already been resolved by: 9494e8e da75b6f
2020-10-26 18:25:32 +01:00
44 changed files with 9350 additions and 389 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,26 +1,25 @@
project (liberty C)
cmake_minimum_required (VERSION 2.8.5)
cmake_minimum_required (VERSION 3.0...3.27)
project (liberty C CXX)
# Moar warnings
set (CMAKE_CXX_STANDARD 11)
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
# -Wunused-function is pretty annoying here, as everything is static
set (wdisabled "-Wno-unused-function -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)
set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
include (AddThreads)
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 +30,107 @@ 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})
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 the --help/--version to AsciiDoc convertor
add_test (test-help2adoc
env LC_ALL=C "${PROJECT_SOURCE_DIR}/tests/help2adoc.sh")
# 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_link_libraries (test-lxdrgen-cpp ${common_libraries})
target_include_directories (test-lxdrgen-cpp PUBLIC ${PROJECT_BINARY_DIR})
add_test (NAME test-lxdrgen-cpp COMMAND test-lxdrgen-cpp)
find_program (GO_EXECUTABLE go)
if (GO_EXECUTABLE)
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 - 2025, 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,75 @@
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 a lower priority, however,
and don't receive as much testing.
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.
help2adoc.awk::
Produces AsciiDoc manual pages from --version/--help output.
These can then be processed by _asciiman.awk_.
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-posix.cpp::
lxdrgen-cpp-qt.cpp::
lxdrgen-cpp-win32.cpp::
LibertyXDR backend for C++, primarily targeting Win32 and its wide strings.
Link the result together with one of the accompanied source files.
lxdrgen-go.awk::
LibertyXDR backend for Go, supporting _encoding/json_ interfaces. It also
produces optimized JSON marshallers (however, note that the _json.Marshaler_
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.
wdye::
Compiled Lua-based Expect-like utility, intended purely for build checks.
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)

84
cmake/IconUtils.cmake Normal file
View File

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

View File

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

View File

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

View File

@@ -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[] =
@@ -1382,7 +1381,7 @@ struct mpd_client
// Protocol:
bool got_hello; ///< Got the OK MPD hello message
char *got_hello; ///< Version from OK MPD hello message
bool idling; ///< Sent idle as the last command
unsigned idling_subsystems; ///< Subsystems we're idling for
@@ -1483,7 +1482,7 @@ mpd_client_reset (struct mpd_client *self)
str_reset (&self->read_buffer);
str_reset (&self->write_buffer);
self->got_hello = false;
cstr_set (&self->got_hello, NULL);
self->idling = false;
self->idling_subsystems = 0;
self->in_list = false;
@@ -1550,7 +1549,8 @@ mpd_client_parse_hello (struct mpd_client *self, const char *line)
// TODO: call "on_connected" now. We should however also set up a timer
// so that we don't wait on this message forever.
return self->got_hello = true;
cstr_set (&self->got_hello, xstrdup (line + sizeof hello - 1));
return true;
}
static bool
@@ -1594,6 +1594,8 @@ mpd_client_parse_kv (char *line, char **value)
static void
mpd_client_update_poller (struct mpd_client *self)
{
if (self->state != MPD_CONNECTED)
return;
poller_fd_set (&self->socket_event,
self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
}
@@ -1635,30 +1637,30 @@ mpd_client_on_ready (const struct pollfd *pfd, void *user_data)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
mpd_client_must_quote_char (char c)
{
return (unsigned char) c <= ' ' || c == '"' || c == '\'';
}
static bool
mpd_client_must_quote (const char *s)
{
if (!*s)
return true;
for (; *s; s++)
if (mpd_client_must_quote_char (*s))
if ((unsigned char) *s <= ' ' || *s == '"' || *s == '\'')
return true;
return false;
}
static bool
mpd_client_must_escape_in_quote (char c)
{
return c == '"' || c == '\'' || c == '\\';
}
static void
mpd_client_quote (const char *s, struct str *output)
{
str_append_c (output, '"');
for (; *s; s++)
{
if (mpd_client_must_quote_char (*s))
if (mpd_client_must_escape_in_quote (*s))
str_append_c (output, '\\');
str_append_c (output, *s);
}
@@ -1912,14 +1914,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

178
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 *
@@ -310,7 +316,7 @@ xstrndup (const char *s, size_t n)
#define ARRAY(type, name) type *name; size_t name ## _len, name ## _alloc;
#define ARRAY_INIT_SIZED(a, n) \
BLOCK_START \
(a) = xcalloc (sizeof *(a), (a ## _alloc) = (n)); \
(a) = xcalloc ((a ## _alloc) = (n), sizeof *(a)); \
(a ## _len) = 0; \
BLOCK_END
#define ARRAY_INIT(a) ARRAY_INIT_SIZED (a, 16)
@@ -392,7 +398,7 @@ strv_make (void)
struct strv self;
self.alloc = 4;
self.len = 0;
self.vector = xcalloc (sizeof *self.vector, self.alloc);
self.vector = xcalloc (self.alloc, sizeof *self.vector);
return self;
}
@@ -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;
}
@@ -3247,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);
}
@@ -3272,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);
}
@@ -3548,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)))
@@ -4413,7 +4462,9 @@ 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]?
//
@@ -4450,7 +4501,7 @@ struct config_item
}
value; ///< The value of this item
struct config_schema *schema; ///< Schema describing this value
const struct config_schema *schema; ///< Schema describing this value
void *user_data; ///< User value attached by schema owner
};
@@ -4602,7 +4653,7 @@ config_item_object (void)
static bool
config_schema_accepts_type
(struct config_schema *self, enum config_item_type type)
(const struct config_schema *self, enum config_item_type type)
{
if (self->type == type)
return true;
@@ -4615,7 +4666,7 @@ config_schema_accepts_type
static bool
config_item_validate_by_schema (struct config_item *self,
struct config_schema *schema, struct error **e)
const struct config_schema *schema, struct error **e)
{
struct error *error = NULL;
if (!config_schema_accepts_type (schema, self->type))
@@ -4636,7 +4687,7 @@ static bool
config_item_set_from (struct config_item *self, struct config_item *source,
struct error **e)
{
struct config_schema *schema = self->schema;
const struct config_schema *schema = self->schema;
if (!schema)
{
// Easy, we don't know what this item is
@@ -4713,8 +4764,10 @@ config_item_write_string (struct str *output, const struct str *s)
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);
else if (iscntrl_ascii (c))
str_append_printf (output, "\\x%02x", c);
else
str_append_c (output, c);
}
str_append_c (output, '"');
}
@@ -4780,7 +4833,8 @@ config_item_write_kv_pair (struct config_writer *self,
str_append_printf (self->output,
"%s# %s\n", indent, value->schema->comment);
bool can_use_word = true;
char *end = NULL;
bool can_use_word = ((void) strtoll (key, &end, 10), end == key);
for (const char *p = key; *p; p++)
if (!config_tokenizer_is_word_char (*p))
can_use_word = false;
@@ -5038,10 +5092,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)) == '"')
@@ -5055,6 +5109,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)
{
@@ -5079,7 +5171,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;
@@ -5392,7 +5484,7 @@ end:
/// "user_data" is passed to allow its immediate use in validation callbacks
static struct config_item *
config_schema_initialize_item (struct config_schema *schema,
config_schema_initialize_item (const struct config_schema *schema,
struct config_item *parent, void *user_data, struct error **warning,
struct error **e)
{
@@ -5449,7 +5541,7 @@ keep_current:
/// Assign schemas and user_data to multiple items at once;
/// feel free to copy over and modify to suit your particular needs
static void
config_schema_apply_to_object (struct config_schema *schema_array,
config_schema_apply_to_object (const struct config_schema *schema_array,
struct config_item *object, void *user_data)
{
while (schema_array->name)

110
libertyxdr.adoc Normal file
View File

@@ -0,0 +1,110 @@
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;
default: void;
};
There is no *case* fall-through.
Unless *default* is present, only the listed enumeration values are valid.
Any *default* must currently be empty.
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 default
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;

214
tests/help2adoc.sh Executable file
View File

@@ -0,0 +1,214 @@
#!/bin/sh -e
# This test very exactly matches the output,
# but help2adoc is more or less feature-complete already.
self=$(realpath "$0")
help2adoc=$(realpath "$(dirname "$0")/../tools/help2adoc.awk")
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_oneline_help() {
cat <<END
Usage: $self [--brightness [+-]BRIGHTNESS] [--input NAME] [--restart]
END
}
test_oneline_version() {
cat <<'END'
eizoctl 1.0
END
}
test_oneline_out() {
cat <<'END'
eizoctl(1)
==========
:doctype: manpage
:manmanual: eizoctl Manual
:mansource: eizoctl 1.0
Name
----
eizoctl - manual page for eizoctl 1.0
Synopsis
--------
*eizoctl* [**--brightness** [+-]__BRIGHTNESS__] [**--input** __NAME__] [**--restart**]
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_simple_help() {
cat <<'END'
Usage: elksmart-comm [OPTION]... [COMMAND...]
Usage: elksmart-comm
Transmit or receive infrared commands.
-d, --debug elksmart-comm will run in debug mode
-f, --frequency HZ frequency (38000 Hz by default)
-n, --nec use the NEC transmission format
-h, --help display this help and exit
-V, --version output version information and exit
END
}
test_simple_version() {
cat <<'END'
elksmart-comm (usb-drivers) dev
END
}
test_simple_out() {
cat <<'END'
elksmart-comm(1)
================
:doctype: manpage
:manmanual: elksmart-comm Manual
:mansource: usb-drivers dev
Name
----
elksmart-comm - manual page for elksmart-comm dev
Synopsis
--------
*elksmart-comm* [__OPTION__]... [__COMMAND__...]
*elksmart-comm*
Description
-----------
Transmit or receive infrared commands.
*-d*, **--debug**::
**elksmart-comm** will run in debug mode
*-f*, **--frequency** __HZ__::
frequency (38000 Hz by default)
*-n*, **--nec**::
use the NEC transmission format
*-h*, **--help**::
display this help and exit
*-V*, **--version**::
output version information and exit
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_wild_help() {
cat <<'END'
Usage:
wild [option]… <command>… — Be wild
What's happening?
-f, --frequency hz-2-foo frequency to --foo at
--foo=bar
Foobar.
Boo far.
Subsection:
--help
--version
Oh my.
Major section:
And now for something completely different.
Very wild
END
}
test_wild_version() {
cat <<'END'
wild 1
Copies left and right.
END
}
test_wild_out() {
cat <<'END'
wild(1)
=======
:doctype: manpage
:manmanual: wild Manual
:mansource: wild 1
Name
----
wild - manual page for wild 1
Synopsis
--------
*wild* [__option__]... <__command__>...
Description
-----------
Be **wild**
What's happening?
*-f*, **--frequency** __hz-2-foo__::
frequency to **--foo** at
*--foo*=__bar__::
Foobar.
Boo far.
Subsection
~~~~~~~~~~
*--help*::
*--version*::
Oh my.
Major section
-------------
And now for something completely different.
Very wild
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
run() {
echo "-- help2adoc/$1"
local selfquoted=$(echo "$self" | sed 's/\\/&&/g')
local output=$(TEST=$1 awk -f "$help2adoc" -v Target="$selfquoted")
local expect="$($1_out)"
if [ "$output" = "$expect" ]
then return
fi
echo "== Expected"
sed 's/^/ /' <<-END
$expect
END
echo "== Received"
sed 's/^/ /' <<-END
$output
END
exit 1
}
if [ -z "$TEST" ]
then
run test_oneline
run test_simple
run test_wild
echo "-- OK"
elif [ "$1" = "--help" ]
then ${TEST}_help
elif [ "$1" = "--version" ]
then ${TEST}_version
else
echo "Wrong usage"
exit 1
fi

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

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

31
tests/lxdrgen.lxdr Normal file
View File

@@ -0,0 +1,31 @@
/*
* 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;
default:
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);
}

340
tools/asciiman.awk Normal file
View File

@@ -0,0 +1,340 @@
# 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, ok) {
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) && (ok = getline) <= 0) {
if (ok < 0)
fatal("getline failed")
$0 = ""
} else 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) && (ok = getline) <= 0) {
if (ok < 0)
fatal("getline failed")
$0 = ""
} else 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())
}

234
tools/help2adoc.awk Normal file
View File

@@ -0,0 +1,234 @@
# help2adoc.awk: convert --version/--help to AsciiDoc manual pages
#
# Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Usage: awk -f help2adoc.awk -v Target=cat
#
# This is not intended to produce great output, merely useful output,
# if only because there is no real standard of what the input should look like.
#
# The only target that needs to work is liberty's own opt_handler.
# The expected input format is roughly that of GNU utilites.
function fatal(message) {
print "// " message
print "fatal error: " message > "/dev/stderr"
exit 1
}
# The input model of this script is that function take the next line on $0,
# read further lines as necessary, and leave the next line in $0 again.
function readline( ok) {
if ((ok = (Command | getline)) < 0)
fatal("read error")
if (!ok)
exit
}
function emboldenoptions(line) {
# -N, --newer=DATE-OR-FILE, --after-date=DATE-OR-FILE
sub(/^-[^-=,[:space:]{[<]/, "*&*", line)
while (match(line, /[^-_[:alnum:]*'+]-[^-=,[:space:]{[<]/)) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH)
}
sub(/^--[-_[:alnum:]]+/, "*&*", line)
while (match(line, /[^-_[:alnum:]*'+]--[-_[:alnum:]]+/)) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH)
}
return line
}
function formatinline(line, programname, last, i) {
# Go the extra step of emboldening the program name at word boundaries.
programname = ProgramName
gsub(/[][\\.^$(){}|*+?]/, "\\\\&", programname)
if (match(line, "^" programname "[^-_[:alnum:]*'+/]")) {
line = "**" substr(line, RSTART, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH - 1)
}
while (match(line, "[^-_[:alnum:]*'+/]" programname "[^-_[:alnum:]*'+/]")) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 2) "**" \
substr(line, RSTART + RLENGTH - 1)
}
if (match(line, "[^-_[:alnum:]*'+/]" programname "$")) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**"
}
return emboldenoptions(line)
}
function printusage(usage, description) {
gsub(/…/, "...", usage)
gsub(/—|/, "-", usage)
# --help output will more likely than not simply include argv[0],
# or perhaps program_invocation_short_name (not addressed here).
if (substr(usage, 1, length(Target) + 1) == Target " ")
usage = ProgramName substr(usage, length(Target) + 1)
# A lot of GNOME software includes the description here.
if (match(usage, / +- +/) && usage !~ / - [^[:alnum:]]/) {
description = substr(usage, RSTART + RLENGTH)
usage = substr(usage, 1, RSTART - 1)
}
while (match(usage, /[^-_[:alnum:]*'+.][[:alnum:]][-_[:alnum:]]+/)) {
usage = substr(usage, 1, RSTART) \
"__" substr(usage, RSTART + 1, RLENGTH - 1) "__" \
substr(usage, RSTART + RLENGTH)
}
sub(/^[^[:space:]]+/, "*&*", usage)
print emboldenoptions(usage)
print ""
if (description) {
flushsections()
print formatinline(description)
print ""
}
}
# We're going with Setext headers, because that's what asciiman.awk supports.
function printheader(text, underline) {
print text
gsub(/./, underline, text)
print text
}
BEGIN {
if (!Target)
fatal("missing Target")
TargetQuoted = Target
gsub(/'/, "'\\''", TargetQuoted)
TargetQuoted = "'" TargetQuoted "'"
# Remaining --version lines could be about copyright (GNU),
# or something else entirely.
Command = TargetQuoted " --version"
if ((Command | getline) > 0) {
# GNU --version output can place the package name in parentheses.
Package = $0
if (match($0, /[[:space:]][(][^)]*[)]/)) {
Package = substr($0, RSTART + 2, RLENGTH - 3) \
substr($0, RSTART + RLENGTH)
sub(/[[:space:]]+[(][^)]*[)]/, "")
}
Version = $0
sub(/[[:space:]]+[^[:space:]]+$/, "")
Name = $0
} else {
fatal("failed to get --version output")
}
if (Name !~ /[[:space:]]/)
ProgramName = Name
else if (match(Target, /[^\/]+$/))
ProgramName = substr(Target, RSTART, RLENGTH)
printheader(ProgramName "(1)", "=")
print ":doctype: manpage"
print ":manmanual: " Name " Manual"
print ":mansource: " Package
print ""
printheader("Name", "-")
print ProgramName " - manual page for " Version
print ""
close(Command)
Command = TargetQuoted " --help"
if ((Command | getline) <= 0)
fatal("failed to get --help output")
NextSection = "Description"
NextSubsection = ""
# The SYNOPSIS section is mandatory, so just put it there.
printheader("Synopsis", "-")
while (1) {
if (match($0, /^[Uu]sage:[[:space:]]*/)) {
if (($0 = substr($0, RSTART + RLENGTH)))
printusage($0)
} else if (match($0, /^[[:space:]]+/) && !/^[[:space:]]*-/) {
if (($0 = substr($0, RSTART + RLENGTH)))
printusage($0)
} else if ($0) {
break
}
readline()
}
while (1) {
if (match($0, /^[[:alpha:]][-[:alnum:][:space:]]+:$/)) {
# We don't flush sections here,
# so that we don't unnecessarily enforce DESCRIPTION first.
NextSection = substr($0, RSTART, RLENGTH - 1)
} else if (match($0, /^ [[:alpha:]][-[:alnum:][:space:]]+:$/)) {
flushsections()
NextSubsection = substr($0, RSTART + 1, RLENGTH - 2)
} else if (match($0, /^ +-/)) {
flushsections()
parseoption(substr($0, RSTART + RLENGTH - 1))
continue
} else if ($0) {
flushsections()
# That will be probably interpreted as a literal block.
if (!/^[[:space:]]/)
$0 = formatinline($0)
print
} else {
print
}
readline()
}
}
function flushsections() {
if (NextSection) {
print ""
printheader(NextSection, "-")
NextSection = ""
}
if (NextSubsection) {
print ""
printheader(NextSubsection, "~")
NextSubsection = ""
}
}
function parseoption(line, usage) {
# Often enough you will see it separated with only one space,
# which will simply not work for us.
if (match(line, /[[:space:]]{2,}/)) {
usage = substr(line, 1, RSTART - 1)
line = substr(line, RSTART + RLENGTH)
} else {
usage = line
line = ""
}
usage = emboldenoptions(usage)
while (match(usage, /[=<, ][[:alnum:]][-_[:alnum:]]*/)) {
usage = substr(usage, 1, RSTART) \
"__" substr(usage, RSTART + 1, RLENGTH - 1) "__" \
substr(usage, RSTART + RLENGTH)
}
print ""
print usage "::"
if (line)
print "\t" formatinline(line)
readline()
while (match($0, /^ +[^-[:space:]]|^ {7,}./)) {
print "\t" formatinline(substr($0, RSTART + RLENGTH - 1))
readline()
}
}

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

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

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

View File

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

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

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

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

@@ -0,0 +1,548 @@
# lxdrgen-go.awk: Go backend for lxdrgen.awk.
#
# Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# This backend also enables proxying to other endpoints using JSON.
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 int64(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, isaccessor, camel, f, marshal) {
camel = snaketocamel(d["name"])
f = "s." camel
if (isaccessor)
f = f "()"
if (!d["isarray"]) {
append(cg, "marshal",
"\tb = append(b, `,\"" decapitalize(camel) "\":`...)\n" \
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, 0)
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) {
codegen_struct_field_marshal(d, cg, 1)
# Do not serialize or 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)
print "func (u *" CodegenGoType[structname] ") " cg["tagname"] "() " \
CodegenGoType[cg["tagtype"]] " {"
print "\treturn " CodegenGoType[cg["tagtype"]] snaketocamel(casename)
print "}"
print ""
init = CodegenGoType[structname] "{}"
append(cg, "unmarshal",
"\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \
"\t\ts := " init "\n" \
"\t\terr = json.Unmarshal(data, &s)\n" \
"\t\tu.Variant = &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.Variant = &s\n")
}
function codegen_union(name, cg, exhaustive, gotype, tagvar) {
gotype = PrefixCamel name
# This must be a struct, so that UnmarshalJSON can create concrete types.
print "type " gotype " struct {"
print "\tVariant interface {"
print "\t\t" cg["tagname"] "() " CodegenGoType[cg["tagtype"]]
print "\t}"
print "}"
print ""
# This cannot be a pointer method, it wouldn't work recursively.
CodegenIsMarshaler[name] = 1
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
print "\treturn u.Variant.(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 rather testing the type for having an AppendTo method,
# which would eliminate this type case switch entirely.
print "func (u *" gotype ") AppendTo(data []byte) ([]byte, bool) {"
print "\tok := true"
print sprintf(CodegenSerialize[cg["tagtype"]],
"u.Variant." cg["tagname"] "()") \
"\tswitch union := u.Variant.(type) {"
print cg["serialize"] "\tdefault:"
print "\t\t_ = union"
print "\t\treturn nil, false"
print "\t}"
print "\treturn data, ok"
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]
}

317
tools/lxdrgen.awk Normal file
View File

@@ -0,0 +1,317 @@
# lxdrgen.awk: an XDR-derived code generator for network protocols.
#
# Copyright (c) 2022 - 2025, 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, defaulted, 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 (accept("default")) {
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
expect(accept(":"))
if (defaulted)
fatal("duplicate default")
tagvalue = ""
defaulted = 1
} else if (tagvalue) {
if (readfield(d))
codegen_struct_field(d, scg)
} else if (defaulted) {
if (readfield(d))
fatal("default must not contain fields")
} else {
fatal("union fields must fall under a case")
}
}
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
# Unseen cases are only recognized/allowed when default is present.
exhaustive = 1
for (i in unseen)
if (i && unseen[i]) {
if (defaulted) {
codegen_struct_tag(tag, scg)
codegen_union_struct(name, i, cg, scg)
} else {
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(";"))
}
}

42
tools/wdye/CMakeLists.txt Normal file
View File

@@ -0,0 +1,42 @@
cmake_minimum_required (VERSION 3.18)
project (wdye VERSION 1 DESCRIPTION "What did you expect?" LANGUAGES C)
set (CMAKE_C_STANDARD 99)
set (CMAKE_C_STANDARD_REQUIRED ON)
set (CMAKE_C_EXTENSIONS OFF)
# -Wunused-function is pretty annoying here, as everything is static
set (options -Wall -Wextra -Wno-unused-function)
add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:${options}>")
add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:${options}>")
set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../cmake")
find_package (Curses)
find_package (PkgConfig REQUIRED)
pkg_search_module (lua REQUIRED
lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua>=5.3)
option (WITH_CURSES "Offer terminal sequences using Curses" "${CURSES_FOUND}")
# -liconv may or may not be a part of libc
find_path (iconv_INCLUDE_DIRS iconv.h)
include_directories (BEFORE "${PROJECT_BINARY_DIR}" ${iconv_INCLUDE_DIRS})
file (CONFIGURE OUTPUT "${PROJECT_BINARY_DIR}/config.h" CONTENT [[
#define PROGRAM_NAME "${PROJECT_NAME}"
#define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine WITH_CURSES
]])
add_executable (wdye wdye.c)
target_include_directories (wdye PUBLIC ${lua_INCLUDE_DIRS})
target_link_directories (wdye PUBLIC ${lua_LIBRARY_DIRS})
target_link_libraries (wdye PUBLIC ${lua_LIBRARIES})
if (WITH_CURSES)
target_include_directories (wdye PUBLIC ${CURSES_INCLUDE_DIRS})
target_link_libraries (wdye PUBLIC ${CURSES_LIBRARIES})
endif ()
add_test (NAME wdye COMMAND wdye "${PROJECT_SOURCE_DIR}/test.lua")
include (CTest)

33
tools/wdye/test.lua Normal file
View File

@@ -0,0 +1,33 @@
for k, v in pairs(wdye) do _G[k] = v end
-- The terminal echoes back, we don't want to read the same stuff twice.
local cat = spawn {"sh", "-c", "cat > /dev/null", environ={TERM="xterm"}}
assert(cat, "failed to spawn process")
assert(cat.term.key_left, "bad terminfo")
cat:send("Hello\r")
local m = expect(cat:exact {"Hello\r", function (p) return p[0] end})
assert(m == "Hello\r", "exact match failed, or value expansion mismatch")
local t = table.pack(expect(timeout {.5, 42}))
assert(#t == 1 and t[1] == 42, "timeout match failed, or value mismatch")
cat:send("abc123\r")
expect(cat:regex {"A(.*)3", nocase=true, function (p)
assert(p[0] == "abc123", "wrong regex group #0")
assert(p[1] == "bc12", "wrong regex group #1")
end})
assert(not cat:wait (true), "process reports exiting early")
-- Send EOF (^D), test method chaining.
cat:send("Closing...\r"):send("\004")
local v = expect(cat:eof {true},
cat:default {.5, function (p) error "expected EOF, got a timeout" end})
assert(cat.pid > 0, "process has no ID")
local s1, exit, signal = cat:wait ()
assert(s1 == 0 and exit == 0 and not signal, "unexpected exit status")
assert(cat.pid < 0, "process still has an ID")
local s2 = cat:wait (true)
assert(s1 == s2, "exit status not remembered")

146
tools/wdye/wdye.adoc Normal file
View File

@@ -0,0 +1,146 @@
wdye(1)
=======
:doctype: manpage
:manmanual: wdye Manual
:mansource: wdye {release-version}
Name
----
wdye - what did you expect: Lua-based Expect tool
Synopsis
--------
*wdye* _program.lua_
Description
-----------
*wdye* executes a Lua script, providing an *expect*(1)-like API targeted
at application testing.
API
---
This list is logically ordered. Uppercase names represent object types.
wdye.spawn {file [, arg1, ...] [, environ=env]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Creates a new pseudoterminal, spawns the given program in it,
and returns a _process_ object. When *file* doesn't contain slashes,
the program will be searched for in _PATH_.
The *env* map may be used to override environment variables, notably _TERM_.
Variables evaluating to _false_ will be removed from the environment.
The program's whole process group receives SIGKILL when the _process_
is garbage-collected, unless *wait* has collected the process group leader.
wdye.expect ([pattern1, ...])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Waits until any pattern is ready, in order.
When no *timeout* (or *default*) patterns are included, one is added implicitly.
The function returns the matching _pattern_'s values, while replacing
any included functions with the results of their immediate evaluation,
passing the matching _pattern_ as their sole argument.
wdye.timeout {[timeout, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new timeout _pattern_. When no *timeout* is given, which is specified
in seconds, a default timeout value is assumed. Any further values
are remembered to be later processed by *expect*.
wdye.continue ()
~~~~~~~~~~~~~~~~
Raises a _nil_ error, which is interpreted by *expect* as a signal to restart
all processing.
PROCESS.buffer
~~~~~~~~~~~~~~
A string with the _process_' current read buffer contents.
PROCESS.pid
~~~~~~~~~~~
An integer with the _process_' process ID, or -1 if *wait* has collected it.
PROCESS.term
~~~~~~~~~~~~
A table with the _process_' *terminfo*(5) capabilities,
notably containing all **key_...** codes.
This functionality may not be enabled, then this table will always be empty.
PROCESS:send ([string, ...])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Writes the given strings to the _process_' terminal slave,
and returns the _process_ for method chaining.
Beware of echoing and deadlocks, as only *expect* can read from the _process_,
and thus consume the terminal slave's output queue.
PROCESS:regex {pattern [, nocase=true] [, notransfer=true] [, value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new regular expression _pattern_. The *pattern* is a POSIX
Extended Regular Expression. Whether it can match NUL bytes depends on your
system C library.
When the *nocase* option is _true_, the expression will be matched
case-insensitively.
Unless the *notransfer* option is _true_, all data up until the end of the match
will be erased from the _process_' read buffer upon a successful match.
PROCESS:exact {literal [, nocase=true] [, notransfer=true] [, value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new literal string _pattern_. This behaves as if the *literal*
had its ERE special characters quoted, and was then passed to *regex*.
This _pattern_ can always match NUL bytes.
PROCESS:eof {[notransfer=true, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new end-of-file _pattern_, which matches the entire read buffer
contents once the child process closes the terminal.
PROCESS:wait ([nowait])
~~~~~~~~~~~~~~~~~~~~~~~
Waits for the program to terminate, and returns three values:
a combined status as used by `$?` in shells,
an exit status, and a termination signal number.
One of the latter two values will be _nil_, as appropriate.
When the *nowait* option is _true_, the function returns immediately.
If the process hasn't terminated yet, the function then returns no values.
PROCESS:default {[timeout, ] [notransfer=true, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new _pattern_ combining *wdye.timeout* with *eof*.
PATTERN.process
~~~~~~~~~~~~~~~
A reference to the _pattern_'s respective process, or _nil_.
PATTERN[group]
~~~~~~~~~~~~~~
For patterns that can match data, the zeroth group will be the whole matched
input sequence.
For *regex* patterns, positive groups relate to regular expression subgroups.
Missing groups evaluate to _nil_.
Example
-------
for k, v in pairs(wdye) do _G[k] = v end
local rot13 = spawn {"tr", "A-Za-z", "N-ZA-Mn-za-m", environ={TERM="dumb"}}
rot13:send "Hello\r"
expect(rot13:exact {"Uryyb\r"})
Environment
-----------
*WDYE_LOGGING*::
When this environment variable is present, *wdye* produces asciicast v2
files for every spawned program, in the current working directory.
Reporting bugs
--------------
Use https://git.janouch.name/p/liberty to report bugs, request features,
or submit pull requests.
See also
--------
*expect*(1), *terminfo*(5), *regex*(7)

1420
tools/wdye/wdye.c Normal file

File diff suppressed because it is too large Load Diff