diff --git a/CMakeLists.txt b/CMakeLists.txt index 217132afcd..ae24c13793 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,13 @@ endif() message(STATUS "Project version: ${PROJECT_VERSION}") message(STATUS "Protocol version: ${DRAWPILE_PROTO_SERVER_VERSION}.${DRAWPILE_PROTO_MAJOR_VERSION}.${DRAWPILE_PROTO_MINOR_VERSION}") +if(TOOLS) + set(RUST_LIBS ON) + include(Cargo) +else() + set(RUST_LIBS OFF) +endif() + # https://doc.qt.io/qt-6/cmake-qt5-and-qt6-compatibility.html find_package(QT ${DP_MIN_QT_VERSION} NAMES Qt6 Qt5 REQUIRED) message(STATUS "Using Qt version ${QT_VERSION_MAJOR}") @@ -195,7 +202,8 @@ if(SERVER) endif() if(TOOLS) - message(STATUS "Someone deleted the tools") + message(STATUS "Adding tools") + add_subdirectory(src/tools) endif() # This must run once all target creation is finished since it walks the list of diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..e9c6407904 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,296 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "dprectool" +version = "2.2.0-pre" +dependencies = [ + "drawdance", + "xflags", +] + +[[package]] +name = "drawdance" +version = "2.2.0-pre" +dependencies = [ + "bindgen", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xflags" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4554b580522d0ca238369c16b8f6ce34524d61dafe7244993754bbd05f2c2ea" +dependencies = [ + "xflags-macros", +] + +[[package]] +name = "xflags-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58e7b3ca8977093aae6b87b6a7730216fc4c53a6530bab5c43a783cd810c1a8" diff --git a/Cargo.toml b/Cargo.toml index e179b58fd7..3ec297a196 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -members = [] +resolver = "2" +members = [ + "src/drawdance/rust", + "src/tools/dprectool", +] [workspace.package] version = "2.2.0-pre" diff --git a/cmake/Cargo.cmake b/cmake/Cargo.cmake index c67e00b25d..928f7aed58 100644 --- a/cmake/Cargo.cmake +++ b/cmake/Cargo.cmake @@ -1,28 +1,93 @@ -#[[ -Adds an imported Rust executable target that is built by Cargo. -#]] -function(add_cargo_executable target) - _add_cargo(${target} TRUE ${ARGN}) +# SPDX-License-Identifier: MIT +find_program(CARGO_COMMAND cargo REQUIRED + PATHS $ENV{HOME}/.cargo/bin + DOC "Cargo executable" +) + +# SPDX-SnippetBegin +# SPDX-License-Identifier: MIT +# SDPX—SnippetName: static libs detection from corrosion +function(_corrosion_determine_libs_new target_triple out_libs) + set(package_dir "${CMAKE_BINARY_DIR}/corrosion/required_libs") + # Cleanup on reconfigure to get a cleans state (in case we change something in the future) + file(REMOVE_RECURSE "${package_dir}") + file(MAKE_DIRECTORY "${package_dir}") + set(manifest "[package]\nname = \"required_libs\"\nedition = \"2018\"\nversion = \"0.1.0\"\n") + string(APPEND manifest "\n[lib]\ncrate-type=[\"staticlib\"]\npath = \"lib.rs\"\n") + string(APPEND manifest "\n[workspace]\n") + file(WRITE "${package_dir}/Cargo.toml" "${manifest}") + file(WRITE "${package_dir}/lib.rs" "pub fn add(left: usize, right: usize) -> usize {left + right}\n") + + set(args rustc --verbose --color never) + if(target_triple) + list(APPEND args --target=${target_triple}) + endif() + list(APPEND args -- --print=native-static-libs) + execute_process( + COMMAND ${CARGO_COMMAND} ${args} + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/corrosion/required_libs" + RESULT_VARIABLE cargo_build_result + ERROR_VARIABLE cargo_build_error_message + ) + + if(cargo_build_result) + message(SEND_ERROR "Determining required native libraries - failed: ${cargo_build_result}.") + message(SEND_ERROR "The cargo build error was: ${cargo_build_error_message}") + return() + else() + # The pattern starts with `native-static-libs:` and goes to the end of the line. + if(cargo_build_error_message MATCHES "native-static-libs: ([^\r\n]+)\r?\n") + string(REPLACE " " ";" "libs_list" "${CMAKE_MATCH_1}") + set(stripped_lib_list "") + foreach(lib ${libs_list}) + # Strip leading `-l` (unix) and potential .lib suffix (windows) + string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}") + string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}") + list(APPEND stripped_lib_list "${stripped_lib}") + endforeach() + set(libs_list "${stripped_lib_list}") + # Special case `msvcrt` to link with the debug version in Debug mode. + list(TRANSFORM libs_list REPLACE "^msvcrt$" "\$<\$:msvcrtd>") + else() + message(SEND_ERROR "Determining required native libraries - failed: Regex match failure.") + message(SEND_ERROR "`native-static-libs` not found in: `${cargo_build_error_message}`") + return() + endif() + endif() + set("${out_libs}" "${libs_list}" PARENT_SCOPE) +endfunction() +# SPDX-SnippetEnd + +function(_cargo_set_libs target triple) + if(NOT "${triple}") + set(cache_var "CARGO_LIBS_${triple}") + else() + set(cache_var "CARGO_LIBS_HOST") + endif() + + set(libs "${${cache_var}}") + if(libs) + message(STATUS "Re-used Rust libraries: ${libs}") + else() + _corrosion_determine_libs_new("${triple}" libs) + set("${cache_var}" "${libs}" CACHE INTERNAL "") + message(STATUS "Determined Rust libraries: ${libs}") + endif() + + set_target_properties("${target}" PROPERTIES + INTERFACE_LINK_LIBRARIES "${libs}" + ) endfunction() #[[ Adds an imported Rust static library target that is built by Cargo. #]] -function(add_cargo_library target) - _add_cargo(${target} FALSE ${ARGN}) -endfunction() - -function(_add_cargo target is_exe) +function(add_cargo_library target package) set(options ALL_FEATURES NO_DEFAULT_FEATURES) - set(singleValueArgs PACKAGE MANIFEST_PATH) + set(singleValueArgs MANIFEST_PATH) set(multiValueArgs FEATURES) cmake_parse_arguments(PARSE_ARGV 2 ARG "${options}" "${singleValueArgs}" "${multiValueArgs}") - find_program(CARGO_COMMAND cargo REQUIRED - PATHS $ENV{HOME}/.cargo/bin - DOC "Cargo executable" - ) - if(CMAKE_CROSSCOMPILING) if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|[Aa][Aa][Rr][Cc][Hh]64") set(arch aarch64) @@ -62,28 +127,14 @@ function(_add_cargo target is_exe) set(cargo_dir ${CMAKE_CURRENT_BINARY_DIR}/cargo) set(cargo_target_dir "${cargo_dir}/$<$:${triple}/>$>") - if(is_exe) - set(all ALL) - set(out_name ${target}${CMAKE_EXECUTABLE_SUFFIX}) - set(out_kind RUNTIME) - else() - set(all "") - set(out_name ${CMAKE_STATIC_LIBRARY_PREFIX}${ARG_PACKAGE}${CMAKE_STATIC_LIBRARY_SUFFIX}) - set(out_kind ARCHIVE) - endif() - - if(CMAKE_${out_kind}_OUTPUT_DIRECTORY) - set(out_dir ${CMAKE_${out_kind}_OUTPUT_DIRECTORY}) + set(out_name ${CMAKE_STATIC_LIBRARY_PREFIX}${package}${CMAKE_STATIC_LIBRARY_SUFFIX}) + if(CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set(out_dir ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}) else() set(out_dir ${CMAKE_CURRENT_BINARY_DIR}) endif() - if(ARG_PACKAGE) - set(extra_comment " with packages ${ARG_PACKAGE}") - endif() - add_custom_target(cargo-build_${target} - ${all} BYPRODUCTS "${out_dir}/${out_name}" COMMAND ${CMAKE_COMMAND} -E env MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} ${CARGO_COMMAND} build @@ -92,13 +143,13 @@ function(_add_cargo target is_exe) "$<$:--all-features>" "$<$:--no-default-features>" "$<$:--target;${triple}>" - "$<$:--package;${ARG_PACKAGE}>" + "$<$:--package;${package}>" "$<$:--verbose>" --profile "$,dev,$>>" --target-dir ${cargo_dir} COMMAND ${CMAKE_COMMAND} -E copy_if_different "${cargo_target_dir}/${out_name}" "${out_dir}/${out_name}" COMMAND_EXPAND_LISTS - COMMENT "Running cargo${extra_comment}" + COMMENT "Running cargo with package ${package}" VERBATIM ) @@ -106,43 +157,17 @@ function(_add_cargo target is_exe) # rules about finding package directories and it does not work if someone # does not specify packages at all but it works for how this function is # used today, so YAGNI - if(ARG_PACKAGE AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.20) - foreach(item IN LISTS ARG_PACKAGE) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.20) + foreach(item IN LISTS package) file(GLOB_RECURSE sources ${PROJECT_SOURCE_DIR}/src/${item}/*) target_sources(cargo-build_${target} PRIVATE ${sources}) source_group(TREE "${PROJECT_SOURCE_DIR}/src/${item}" PREFIX "Source Files" FILES ${sources}) endforeach() endif() - if(is_exe) - add_executable(${target} IMPORTED GLOBAL) - else() - add_library(${target} STATIC IMPORTED GLOBAL) - - # Adapted from corrosion. Required for successful linking of anything - # that uses the Rust Standard Library - set(libs "") - if(WIN32) - list(APPEND libs "advapi32" "userenv" "ws2_32" "bcrypt") - if(MSVC) - list(APPEND libs "$<$:msvcrtd>") - # CONFIG takes a comma seperated list starting with CMake 3.19, but we still need to - # support older CMake versions. - set(config_is_release "$,$,$>") - list(APPEND libs "$<${config_is_release}:msvcrt>") - else() - list(APPEND libs "gcc_eh" "pthread") - endif() - elseif(APPLE) - list(APPEND libs "System" "resolv" "c" "m") - else() - list(APPEND libs "dl" "rt" "pthread" "gcc_s" "c" "m" "util") - endif() + add_library(${target} STATIC IMPORTED GLOBAL) - set_target_properties(${target} PROPERTIES - INTERFACE_LINK_LIBRARIES "${libs}" - ) - endif() + _cargo_set_libs("${target}" "${triple}") add_dependencies(${target} cargo-build_${target}) set_target_properties(${target} PROPERTIES diff --git a/src/cmake-config/config.cpp.in b/src/cmake-config/config.cpp.in index 7e8f088158..3a862743d2 100644 --- a/src/cmake-config/config.cpp.in +++ b/src/cmake-config/config.cpp.in @@ -2,8 +2,4 @@ #include "cmake-config/config.h" -namespace cmake_config { - -const char *version() { return "@GIT_VERSION@"; } - -} // namespace config +extern "C" const char *DP_cmake_config_version() { return "@GIT_VERSION@"; } diff --git a/src/cmake-config/config.h.in b/src/cmake-config/config.h.in index 007380291d..1250d73f71 100644 --- a/src/cmake-config/config.h.in +++ b/src/cmake-config/config.h.in @@ -5,12 +5,15 @@ #include -namespace cmake_config { - // The version number goes to a separate TU because it will change after every // commit, and so if it were in this header that would a bunch of other TUs to -// invalidate on every new commit which would be bad for build speed -const char *version(); +// invalidate on every new commit which would be bad for build speed. It's +// extern "C" so that it can be called reasonably easily from Rust. +extern "C" const char *DP_cmake_config_version(); + +namespace cmake_config { + +inline const char *version() { return DP_cmake_config_version(); } constexpr const char *website() { return "@PROJECT_HOMEPAGE_URL@"; } constexpr auto locales() { return std::array{ @DRAWPILE_LOCALES@ }; } diff --git a/src/drawdance/libcommon/dpcommon/input.c b/src/drawdance/libcommon/dpcommon/input.c index 8118e323f1..90bde679eb 100644 --- a/src/drawdance/libcommon/dpcommon/input.c +++ b/src/drawdance/libcommon/dpcommon/input.c @@ -265,6 +265,11 @@ DP_Input *DP_file_input_new(FILE *fp, bool close) return DP_input_new(file_input_init, &state, sizeof(state)); } +DP_Input *DP_file_input_new_from_stdin(bool close) +{ + return DP_file_input_new(stdin, close); +} + DP_Input *DP_file_input_new_from_path(const char *path) { DP_ASSERT(path); diff --git a/src/drawdance/libcommon/dpcommon/input.h b/src/drawdance/libcommon/dpcommon/input.h index 004d5ffc9f..0601da5a2d 100644 --- a/src/drawdance/libcommon/dpcommon/input.h +++ b/src/drawdance/libcommon/dpcommon/input.h @@ -56,6 +56,8 @@ bool DP_input_seek(DP_Input *input, size_t offset); DP_Input *DP_file_input_new(FILE *fp, bool close); +DP_Input *DP_file_input_new_from_stdin(bool close); + DP_Input *DP_file_input_new_from_path(const char *path); diff --git a/src/drawdance/libcommon/dpcommon/output.c b/src/drawdance/libcommon/dpcommon/output.c index ca802ad92f..c8d1cd9efa 100644 --- a/src/drawdance/libcommon/dpcommon/output.c +++ b/src/drawdance/libcommon/dpcommon/output.c @@ -268,6 +268,11 @@ DP_Output *DP_file_output_new(FILE *fp, bool close) return DP_output_new(file_output_init, &state, sizeof(state)); } +DP_Output *DP_file_output_new_from_stdout(bool close) +{ + return DP_file_output_new(stdout, close); +} + DP_Output *DP_file_output_new_from_path(const char *path) { DP_ASSERT(path); diff --git a/src/drawdance/libcommon/dpcommon/output.h b/src/drawdance/libcommon/dpcommon/output.h index 4297389f87..6f86d96d39 100644 --- a/src/drawdance/libcommon/dpcommon/output.h +++ b/src/drawdance/libcommon/dpcommon/output.h @@ -65,6 +65,8 @@ bool DP_output_seek(DP_Output *output, size_t offset); DP_Output *DP_file_output_new(FILE *fp, bool close); +DP_Output *DP_file_output_new_from_stdout(bool close); + DP_Output *DP_file_output_new_from_path(const char *path); // With Qt file IO turned on, this writes to a temporary file and then renames diff --git a/src/drawdance/libengine/dpengine/canvas_history.c b/src/drawdance/libengine/dpengine/canvas_history.c index 4cddefc800..afd2e14886 100644 --- a/src/drawdance/libengine/dpengine/canvas_history.c +++ b/src/drawdance/libengine/dpengine/canvas_history.c @@ -1728,7 +1728,7 @@ DP_Recorder *DP_canvas_history_recorder_new( return params.r; } else { - DP_recorder_free_join(params.r); + DP_recorder_free_join(params.r, NULL); return NULL; } } diff --git a/src/drawdance/libengine/dpengine/paint_engine.c b/src/drawdance/libengine/dpengine/paint_engine.c index 163672ac3d..8368d2c170 100644 --- a/src/drawdance/libengine/dpengine/paint_engine.c +++ b/src/drawdance/libengine/dpengine/paint_engine.c @@ -919,7 +919,7 @@ static bool start_recording(DP_PaintEngine *pe, DP_RecorderType type, record_initial_message, r); if (!reset_image_ok) { DP_warn("Error build recorder reset image"); - DP_recorder_free_join(r); + DP_recorder_free_join(r, NULL); r = NULL; } } @@ -957,7 +957,7 @@ bool DP_paint_engine_recorder_stop(DP_PaintEngine *pe) if (pe->record.recorder) { // Need to take a lock due to cleanup handling, see explanation above. DP_MUTEX_MUST_LOCK(pe->queue_mutex); - DP_recorder_free_join(pe->record.recorder); + DP_recorder_free_join(pe->record.recorder, NULL); DP_free(pe->record.path); pe->record.path = NULL; pe->record.recorder = NULL; diff --git a/src/drawdance/libengine/dpengine/player.c b/src/drawdance/libengine/dpengine/player.c index 3badcedf9a..37902bfe68 100644 --- a/src/drawdance/libengine/dpengine/player.c +++ b/src/drawdance/libengine/dpengine/player.c @@ -91,6 +91,7 @@ struct DP_Player { JSON_Value *text_reader_header_value; long long position; bool compatible; + bool acl_override; bool input_error; bool end; DP_PlayerIndex index; @@ -261,6 +262,7 @@ static DP_Player *make_player(DP_PlayerType type, char *recording_path, compatible, false, false, + false, {DP_BUFFERED_INPUT_NULL, 0, NULL, 0}}; return player; } @@ -273,7 +275,8 @@ static DP_Player *new_binary_player(char *recording_path, char *index_path, return NULL; } - JSON_Object *header = DP_binary_reader_header(binary_reader); + JSON_Object *header = + json_value_get_object(DP_binary_reader_header(binary_reader)); char *version = DP_strdup(json_object_get_string(header, "version")); return make_player(DP_PLAYER_TYPE_BINARY, recording_path, index_path, (DP_PlayerReader){.binary = binary_reader}, version, @@ -342,6 +345,7 @@ static DP_Player *new_debug_dump_player(DP_Input *input) true, false, false, + false, {DP_BUFFERED_INPUT_NULL, 0, NULL, 0}}; return player; } @@ -421,6 +425,7 @@ void DP_player_free(DP_Player *player) break; case DP_PLAYER_TYPE_DEBUG_DUMP: DP_dump_reader_free(player->reader.dump); + break; default: break; } @@ -430,14 +435,20 @@ void DP_player_free(DP_Player *player) } } -JSON_Object *DP_player_header(DP_Player *player) +DP_PlayerType DP_player_type(DP_Player *player) +{ + DP_ASSERT(player); + return player->type; +} + +JSON_Value *DP_player_header(DP_Player *player) { DP_ASSERT(player); switch (player->type) { case DP_PLAYER_TYPE_BINARY: return DP_binary_reader_header(player->reader.binary); case DP_PLAYER_TYPE_TEXT: - return json_value_get_object(player->text_reader_header_value); + return player->text_reader_header_value; case DP_PLAYER_TYPE_DEBUG_DUMP: return NULL; default: @@ -451,6 +462,12 @@ bool DP_player_compatible(DP_Player *player) return player->compatible; } +void DP_player_acl_override_set(DP_Player *player, bool override) +{ + DP_ASSERT(player); + player->acl_override = override; +} + bool DP_player_index_loaded(DP_Player *player) { DP_ASSERT(player); @@ -596,8 +613,9 @@ static DP_PlayerResult step_valid_message(DP_Player *player, DP_Message *msg; DP_PlayerResult result = step_message(player, &msg); if (result == DP_PLAYER_SUCCESS) { - bool filtered = DP_acl_state_handle(player->acls, msg, false) - & DP_ACL_STATE_FILTERED_BIT; + bool filtered = + DP_acl_state_handle(player->acls, msg, player->acl_override) + & DP_ACL_STATE_FILTERED_BIT; if (filtered) { DP_debug( "ACL filtered recorded %s message from user %u", diff --git a/src/drawdance/libengine/dpengine/player.h b/src/drawdance/libengine/dpengine/player.h index 5b8cbd7131..f3c15f76a2 100644 --- a/src/drawdance/libengine/dpengine/player.h +++ b/src/drawdance/libengine/dpengine/player.h @@ -68,10 +68,14 @@ DP_Player *DP_player_new(DP_PlayerType type, const char *path_or_null, void DP_player_free(DP_Player *player); -JSON_Object *DP_player_header(DP_Player *player); +DP_PlayerType DP_player_type(DP_Player *player); + +JSON_Value *DP_player_header(DP_Player *player); bool DP_player_compatible(DP_Player *player); +void DP_player_acl_override_set(DP_Player *player, bool override); + bool DP_player_index_loaded(DP_Player *player); size_t DP_player_tell(DP_Player *player); diff --git a/src/drawdance/libengine/dpengine/recorder.c b/src/drawdance/libengine/dpengine/recorder.c index 2e47566a0b..cf44551b88 100644 --- a/src/drawdance/libengine/dpengine/recorder.c +++ b/src/drawdance/libengine/dpengine/recorder.c @@ -54,6 +54,7 @@ struct DP_Recorder { DP_Mutex *mutex; DP_Semaphore *sem; DP_Thread *thread; + char *error; }; struct DP_RecorderThreadArgs { @@ -161,6 +162,7 @@ static bool write_message_dec(DP_Recorder *r, DP_Message *msg) } else { DP_atomic_set(&r->running, 0); + r->error = DP_strdup(DP_error()); return false; } } @@ -243,6 +245,7 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, DP_ATOMIC_INIT(1), NULL, NULL, + NULL, NULL}; switch (type) { @@ -255,7 +258,7 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, default: DP_error_set("Unknown recorder type %d", (int)type); DP_output_free(output); - DP_recorder_free_join(r); + DP_recorder_free_join(r, NULL); return NULL; } @@ -263,13 +266,13 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, r->mutex = DP_mutex_new(); if (!r->mutex) { - DP_recorder_free_join(r); + DP_recorder_free_join(r, NULL); return NULL; } r->sem = DP_semaphore_new(0); if (!r->sem) { - DP_recorder_free_join(r); + DP_recorder_free_join(r, NULL); return NULL; } @@ -278,7 +281,7 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, r, DP_canvas_state_incref_nullable(cs_or_null)}; r->thread = DP_thread_new(run_recorder, args); if (!r->thread) { - DP_recorder_free_join(r); + DP_recorder_free_join(r, NULL); return NULL; } @@ -286,7 +289,7 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, return r; } -void DP_recorder_free_join(DP_Recorder *r) +void DP_recorder_free_join(DP_Recorder *r, char **out_error) { if (r) { if (r->thread) { @@ -307,7 +310,19 @@ void DP_recorder_free_join(DP_Recorder *r) default: break; } + char *error = r->error; DP_free(r); + + if (out_error) { + *out_error = error; + } + else { + DP_warn("Error in recorder: %s", error); + DP_free(error); + } + } + else if (out_error) { + *out_error = NULL; } } diff --git a/src/drawdance/libengine/dpengine/recorder.h b/src/drawdance/libengine/dpengine/recorder.h index 8836390ce4..f57d20924e 100644 --- a/src/drawdance/libengine/dpengine/recorder.h +++ b/src/drawdance/libengine/dpengine/recorder.h @@ -49,7 +49,10 @@ DP_Recorder *DP_recorder_new_inc(DP_RecorderType type, JSON_Value *header, DP_RecorderGetTimeMsFn get_time_fn, void *get_time_user, DP_Output *output); -void DP_recorder_free_join(DP_Recorder *r); +// If there was an error, the error message will be placed in the provided +// pointer, to be freed by the caller. If there was no error, it will be set to +// NULL. Providing that out pointer is optional. +void DP_recorder_free_join(DP_Recorder *r, char **out_error); DP_RecorderType DP_recorder_type(DP_Recorder *r); diff --git a/src/drawdance/libmsg/dpmsg/binary_reader.c b/src/drawdance/libmsg/dpmsg/binary_reader.c index 20172bf451..58be9d9a95 100644 --- a/src/drawdance/libmsg/dpmsg/binary_reader.c +++ b/src/drawdance/libmsg/dpmsg/binary_reader.c @@ -184,10 +184,10 @@ void DP_binary_reader_free(DP_BinaryReader *reader) } -JSON_Object *DP_binary_reader_header(DP_BinaryReader *reader) +JSON_Value *DP_binary_reader_header(DP_BinaryReader *reader) { DP_ASSERT(reader); - return json_value_get_object(reader->header); + return reader->header; } size_t DP_binary_reader_body_offset(DP_BinaryReader *reader) diff --git a/src/drawdance/libmsg/dpmsg/binary_reader.h b/src/drawdance/libmsg/dpmsg/binary_reader.h index c68d2e56ee..54eb00225a 100644 --- a/src/drawdance/libmsg/dpmsg/binary_reader.h +++ b/src/drawdance/libmsg/dpmsg/binary_reader.h @@ -25,7 +25,7 @@ typedef struct DP_Input DP_Input; typedef struct DP_Message DP_Message; -typedef struct json_object_t JSON_Object; +typedef struct json_value_t JSON_Value; #define DP_DPREC_MAGIC "DPREC" #define DP_DPREC_MAGIC_LENGTH 6 @@ -44,7 +44,7 @@ DP_BinaryReader *DP_binary_reader_new(DP_Input *input); void DP_binary_reader_free(DP_BinaryReader *reader); -JSON_Object *DP_binary_reader_header(DP_BinaryReader *reader); +JSON_Value *DP_binary_reader_header(DP_BinaryReader *reader); size_t DP_binary_reader_body_offset(DP_BinaryReader *reader); diff --git a/src/drawdance/libmsg/dpmsg/protocol.h b/src/drawdance/libmsg/dpmsg/protocol.h new file mode 100644 index 0000000000..31f3987881 --- /dev/null +++ b/src/drawdance/libmsg/dpmsg/protocol.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DPENGINE_PROTOCOL_H +#define DPENGINE_PROTOCOL_H +#include + + +const char *DP_protocol_version(void); + + +#endif diff --git a/src/drawdance/libmsg/test/read_write_roundtrip.c b/src/drawdance/libmsg/test/read_write_roundtrip.c index 9501ead865..df352e01e0 100644 --- a/src/drawdance/libmsg/test/read_write_roundtrip.c +++ b/src/drawdance/libmsg/test/read_write_roundtrip.c @@ -898,7 +898,8 @@ static void read_binary_messages(TEST_PARAMS) DP_TextWriter *tw = open_text_writer(TEST_ARGS, "test/tmp/roundtrip_from_dprec.dptxt"); - write_header(TEST_ARGS, bw, tw, DP_binary_reader_header(br)); + write_header(TEST_ARGS, bw, tw, + json_value_get_object(DP_binary_reader_header(br))); while (true) { DP_Message *msg; diff --git a/src/drawdance/rust/Cargo.toml b/src/drawdance/rust/Cargo.toml new file mode 100644 index 0000000000..63063c7467 --- /dev/null +++ b/src/drawdance/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "drawdance" +version = "2.2.0-pre" +edition = "2021" + +[lib] +path = "lib.rs" + +[build-dependencies] +bindgen = "0.66.1" diff --git a/src/drawdance/rust/build.rs b/src/drawdance/rust/build.rs new file mode 100644 index 0000000000..296bd80104 --- /dev/null +++ b/src/drawdance/rust/build.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=wrapper.h"); + + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .clang_arg("-I../bundled") + .clang_arg("-I../libcommon") + .clang_arg("-I../libmsg") + .clang_arg("-I../libengine") + .allowlist_function("(DP|json)_.*") + .allowlist_type("(DP|JSON)_.*") + .allowlist_var("DP_.*") + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .prepend_enum_name(false) + .generate() + .expect("Couldn't generate bindings for drawdance"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings for drawdance"); +} diff --git a/src/drawdance/rust/engine/mod.rs b/src/drawdance/rust/engine/mod.rs new file mode 100644 index 0000000000..55a7d53d8e --- /dev/null +++ b/src/drawdance/rust/engine/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +mod player; +mod recorder; + +pub use player::{Player, PlayerError}; +pub use recorder::{Recorder, RecorderError}; diff --git a/src/drawdance/rust/engine/player.rs b/src/drawdance/rust/engine/player.rs new file mode 100644 index 0000000000..c2f1ef44a9 --- /dev/null +++ b/src/drawdance/rust/engine/player.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::{ + dp_error, json_object_get_string, json_value_get_object, msg::Message, DP_Input, DP_LoadResult, + DP_Message, DP_Player, DP_PlayerResult, DP_PlayerType, DP_file_input_new_from_path, + DP_file_input_new_from_stdin, DP_player_acl_override_set, DP_player_compatible, DP_player_free, + DP_player_header, DP_player_new, DP_player_step, DP_player_type, JSON_Value, + DP_LOAD_RESULT_SUCCESS, DP_PLAYER_RECORDING_END, DP_PLAYER_SUCCESS, +}; +use std::{ + ffi::{c_char, CStr, CString, NulError}, + ptr, +}; + +pub struct Player { + player: *mut DP_Player, +} + +#[derive(Debug)] +pub enum PlayerError { + DpError(String), + LoadError(DP_LoadResult, String), + ResultError(DP_PlayerResult, String), + NulError(NulError), +} + +impl PlayerError { + fn from_dp_error() -> Self { + Self::DpError(dp_error()) + } + + fn from_load_result(result: DP_LoadResult) -> Self { + if result == DP_LOAD_RESULT_SUCCESS { + Self::from_dp_error() + } else { + Self::LoadError(result, dp_error()) + } + } + + fn from_player_result(result: DP_PlayerResult) -> Self { + if result == DP_PLAYER_SUCCESS { + Self::from_dp_error() + } else { + Self::ResultError(result, dp_error()) + } + } +} + +impl From for PlayerError { + fn from(err: NulError) -> Self { + Self::NulError(err) + } +} + +impl Player { + pub fn new_from_stdin(ptype: DP_PlayerType) -> Result { + let input = unsafe { DP_file_input_new_from_stdin(true) }; + if input.is_null() { + return Err(PlayerError::from_dp_error()); + } + Self::new(ptype, ptr::null(), input) + } + + pub fn new_from_path(ptype: DP_PlayerType, path: String) -> Result { + let cpath = CString::new(path)?; + let input = unsafe { DP_file_input_new_from_path(cpath.as_ptr()) }; + if input.is_null() { + return Err(PlayerError::from_dp_error()); + } + Self::new(ptype, cpath.as_ptr(), input) + } + + fn new( + ptype: DP_PlayerType, + path_or_null: *const c_char, + input: *mut DP_Input, + ) -> Result { + let mut result: DP_LoadResult = 0; + let player = unsafe { DP_player_new(ptype, path_or_null, input, &mut result) }; + if player.is_null() { + Err(PlayerError::from_load_result(result)) + } else { + Ok(Player { player }) + } + } + + pub fn player_type(&self) -> DP_PlayerType { + unsafe { DP_player_type(self.player) } + } + + pub fn header(&self) -> *mut JSON_Value { + unsafe { DP_player_header(self.player) } + } + + pub fn format_version(&self) -> Option { + self.get_string_from_header("version") + } + + pub fn writer_version(&self) -> Option { + self.get_string_from_header("writerversion") + } + + fn get_string_from_header(&self, key: &str) -> Option { + let ckey = match CString::new(key) { + Ok(s) => s, + Err(_) => return None, + }; + + let header = self.header(); + if header.is_null() { + return None; + } + + let value = unsafe { json_object_get_string(json_value_get_object(header), ckey.as_ptr()) }; + if value.is_null() { + return None; + } + + Some( + unsafe { CStr::from_ptr(value) } + .to_str() + .unwrap() + .to_owned(), + ) + } + + pub fn is_compatible(&self) -> bool { + unsafe { DP_player_compatible(self.player) } + } + + pub fn set_acl_override(&mut self, acl_override: bool) { + unsafe { DP_player_acl_override_set(self.player, acl_override) } + } + + pub fn step(&mut self) -> Result, PlayerError> { + let mut msg: *mut DP_Message = ptr::null_mut(); + let result = unsafe { DP_player_step(self.player, &mut msg) }; + if result == DP_PLAYER_SUCCESS { + Ok(Some(Message::new_noinc(msg))) + } else if result == DP_PLAYER_RECORDING_END { + Ok(None) + } else { + Err(PlayerError::from_player_result(result)) + } + } +} + +impl Drop for Player { + fn drop(&mut self) { + unsafe { DP_player_free(self.player) } + } +} diff --git a/src/drawdance/rust/engine/recorder.rs b/src/drawdance/rust/engine/recorder.rs new file mode 100644 index 0000000000..e5a5d37f1f --- /dev/null +++ b/src/drawdance/rust/engine/recorder.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::{ + dp_error, msg::Message, DP_Output, DP_Recorder, DP_RecorderType, DP_file_output_new_from_path, + DP_file_output_new_from_stdout, DP_free, DP_recorder_free_join, DP_recorder_header_clone, + DP_recorder_message_push_noinc, DP_recorder_new_inc, JSON_Value, +}; +use std::{ + ffi::{c_char, c_void, CStr, CString, NulError}, + ptr, +}; + +pub struct Recorder { + recorder: *mut DP_Recorder, +} + +#[derive(Debug)] +pub enum RecorderError { + DpError(String), + NulError(NulError), +} + +impl RecorderError { + fn from_dp_error() -> Self { + Self::DpError(dp_error()) + } +} + +impl From for RecorderError { + fn from(err: NulError) -> Self { + Self::NulError(err) + } +} + +impl Recorder { + pub fn new_from_stdout( + rtype: DP_RecorderType, + header: *mut JSON_Value, + ) -> Result { + let output = unsafe { DP_file_output_new_from_stdout(true) }; + if output.is_null() { + return Err(RecorderError::from_dp_error()); + } + Self::new(rtype, header, output) + } + + pub fn new_from_path( + rtype: DP_RecorderType, + header: *mut JSON_Value, + path: String, + ) -> Result { + let cpath = CString::new(path)?; + let output = unsafe { DP_file_output_new_from_path(cpath.as_ptr()) }; + if output.is_null() { + return Err(RecorderError::from_dp_error()); + } + Self::new(rtype, header, output) + } + + fn new( + rtype: DP_RecorderType, + header: *mut JSON_Value, + output: *mut DP_Output, + ) -> Result { + let header = unsafe { DP_recorder_header_clone(header) }; + if header.is_null() { + return Err(RecorderError::from_dp_error()); + } + + let recorder = unsafe { + DP_recorder_new_inc( + rtype, + header, + ptr::null_mut(), + None, + ptr::null_mut(), + output, + ) + }; + if recorder.is_null() { + Err(RecorderError::from_dp_error()) + } else { + Ok(Recorder { recorder }) + } + } + + pub fn push_noinc(&mut self, msg: Message) -> bool { + unsafe { DP_recorder_message_push_noinc(self.recorder, msg.move_to_ptr()) } + } + + pub fn dispose(mut self) -> Result<(), RecorderError> { + let mut error: *mut c_char = ptr::null_mut(); + unsafe { DP_recorder_free_join(self.recorder, &mut error) }; + self.recorder = ptr::null_mut(); + if error.is_null() { + Ok(()) + } else { + let message = unsafe { CStr::from_ptr(error) } + .to_str() + .unwrap() + .to_owned(); + unsafe { DP_free(error as *mut c_void) } + Err(RecorderError::DpError(message)) + } + } +} + +impl Drop for Recorder { + fn drop(&mut self) { + unsafe { DP_recorder_free_join(self.recorder, ptr::null_mut()) } + } +} diff --git a/src/drawdance/rust/lib.rs b/src/drawdance/rust/lib.rs new file mode 100644 index 0000000000..9e1c0e37d5 --- /dev/null +++ b/src/drawdance/rust/lib.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +extern "C" { + pub fn DP_cmake_config_version() -> *const ::std::os::raw::c_char; +} + +pub mod engine; +pub mod msg; + +fn dp_error() -> String { + unsafe { std::ffi::CStr::from_ptr(DP_error()) } + .to_str() + .unwrap() + .to_owned() +} + +pub fn dp_cmake_config_version() -> String { + unsafe { std::ffi::CStr::from_ptr(DP_cmake_config_version()) } + .to_str() + .unwrap() + .to_owned() +} diff --git a/src/drawdance/rust/msg/message.rs b/src/drawdance/rust/msg/message.rs new file mode 100644 index 0000000000..79e61c7366 --- /dev/null +++ b/src/drawdance/rust/msg/message.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use crate::{ + DP_Message, DP_MessageType, DP_message_decref_nullable, DP_message_length, DP_message_type, + DP_message_type_name, +}; +use std::{ffi::CStr, ptr}; + +pub struct Message { + msg: *mut DP_Message, +} + +impl Message { + pub fn message_type_name(message_type: DP_MessageType) -> String { + unsafe { CStr::from_ptr(DP_message_type_name(message_type)) } + .to_str() + .unwrap() + .to_owned() + } + + pub fn new_noinc(msg: *mut DP_Message) -> Self { + Message { msg } + } + + pub fn message_type(&self) -> DP_MessageType { + unsafe { DP_message_type(self.msg) } + } + + pub fn length(&self) -> usize { + unsafe { DP_message_length(self.msg) } + } + + pub fn move_to_ptr(mut self) -> *mut DP_Message { + let msg = self.msg; + self.msg = ptr::null_mut(); + msg + } +} + +impl Drop for Message { + fn drop(&mut self) { + unsafe { DP_message_decref_nullable(self.msg) } + } +} diff --git a/src/drawdance/rust/msg/mod.rs b/src/drawdance/rust/msg/mod.rs new file mode 100644 index 0000000000..ff4a1e685b --- /dev/null +++ b/src/drawdance/rust/msg/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +mod message; + +pub use message::Message; diff --git a/src/drawdance/rust/wrapper.h b/src/drawdance/rust/wrapper.h new file mode 100644 index 0000000000..591814315b --- /dev/null +++ b/src/drawdance/rust/wrapper.h @@ -0,0 +1,5 @@ +#include +#include +#include +#include +#include diff --git a/src/libclient/document.cpp b/src/libclient/document.cpp index 378a4aa2d5..00d06f1f79 100644 --- a/src/libclient/document.cpp +++ b/src/libclient/document.cpp @@ -165,8 +165,10 @@ DP_LoadResult Document::loadFile(const QString &path) static bool isSessionTemplate(DP_Player *player) { - JSON_Object *header = DP_player_header(player); - return header && DP_str_equal(json_object_get_string(header, "type"), "template"); + JSON_Value *header = DP_player_header(player); + return header && DP_str_equal( + json_object_get_string(json_value_get_object(header), "type"), + "template"); } DP_LoadResult Document::loadRecording( diff --git a/src/libclient/drawdance/paintengine.cpp b/src/libclient/drawdance/paintengine.cpp index 21185bca34..4fdb29c246 100644 --- a/src/libclient/drawdance/paintengine.cpp +++ b/src/libclient/drawdance/paintengine.cpp @@ -202,10 +202,20 @@ RecordStartResult PaintEngine::exportTemplate( } for(const drawdance::Message &msg : snapshot) { - DP_recorder_message_push_inc(r, msg.get()); + if(!DP_recorder_message_push_inc(r, msg.get())) { + break; + } + } + + char *error; + DP_recorder_free_join(r, &error); + if(error) { + DP_error_set("%s", error); + DP_free(error); + return RECORD_START_RECORDER_ERROR; + } else { + return RECORD_START_SUCCESS; } - DP_recorder_free_join(r); - return RECORD_START_SUCCESS; } bool PaintEngine::stopRecorder() diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt new file mode 100644 index 0000000000..5cc43064db --- /dev/null +++ b/src/tools/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: MIT + +set_property(SOURCE dprectool/main.c PROPERTY SKIP_AUTOGEN ON) + +add_cargo_library(dprectool_rust dprectool) +add_executable(dprectool dprectool/main.c) +target_link_libraries(dprectool PUBLIC dprectool_rust dpengine cmake-config) + +install(TARGETS dprectool) diff --git a/src/tools/dprectool/Cargo.toml b/src/tools/dprectool/Cargo.toml new file mode 100644 index 0000000000..0c6b646e9b --- /dev/null +++ b/src/tools/dprectool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dprectool" +version = "2.2.0-pre" +edition = "2021" + +[lib] +crate-type = ["staticlib"] +path = "lib.rs" + +[dependencies] +drawdance = { path = "../../drawdance/rust" } +xflags = "0.3.1" diff --git a/src/tools/dprectool/lib.rs b/src/tools/dprectool/lib.rs new file mode 100644 index 0000000000..4ff1acc37c --- /dev/null +++ b/src/tools/dprectool/lib.rs @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use drawdance::{ + dp_cmake_config_version, + engine::{Player, PlayerError, Recorder, RecorderError}, + msg::Message, + DP_MessageType, DP_PlayerType, DP_RecorderType, DP_PLAYER_TYPE_BINARY, DP_PLAYER_TYPE_GUESS, + DP_PLAYER_TYPE_TEXT, DP_RECORDER_TYPE_BINARY, DP_RECORDER_TYPE_TEXT, +}; +use std::{collections::HashMap, str::FromStr}; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub enum InputFormat { + #[default] + Guess, + Binary, + Text, +} + +impl InputFormat { + fn to_player_type(self) -> DP_PlayerType { + match self { + Self::Guess => DP_PLAYER_TYPE_GUESS, + Self::Binary => DP_PLAYER_TYPE_BINARY, + Self::Text => DP_PLAYER_TYPE_TEXT, + } + } +} + +impl std::str::FromStr for InputFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "guess" => Ok(Self::Guess), + "binary" => Ok(Self::Binary), + "text" => Ok(Self::Text), + _ => Err(format!( + "invalid input format '{s}', should be one of 'guess', \ + 'binary' or 'text'" + )), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub enum OutputFormat { + #[default] + Guess, + Binary, + Text, + Version, +} + +impl OutputFormat { + fn to_recorder_type( + self, + output_path: &String, + output_path_is_default: bool, + player: &Player, + ) -> DP_RecorderType { + match self { + Self::Binary => DP_RECORDER_TYPE_BINARY, + Self::Text => DP_RECORDER_TYPE_TEXT, + _ => self.guess_recorder_format(output_path, output_path_is_default, player), + } + } + + fn guess_recorder_format( + self, + output_path: &String, + output_path_is_default: bool, + player: &Player, + ) -> DP_RecorderType { + // No explicit output path means we'll write to text so that we don't + // end up spewing binary data into a terminal. + if output_path_is_default { + DP_RECORDER_TYPE_TEXT + } else { + // If the file extension is conclusive, use that format. + let folded_path = output_path.to_ascii_lowercase(); + if folded_path.ends_with(".dprec") { + DP_RECORDER_TYPE_BINARY + } else if folded_path.ends_with(".dptxt") { + DP_RECORDER_TYPE_TEXT + } else { + // Use the "opposite" format of what the input is. + if player.player_type() == DP_PLAYER_TYPE_TEXT { + DP_RECORDER_TYPE_BINARY + } else { + DP_RECORDER_TYPE_TEXT + } + } + } + } +} + +impl FromStr for OutputFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "guess" => Ok(Self::Guess), + "binary" => Ok(Self::Binary), + "text" => Ok(Self::Text), + "version" => Ok(Self::Version), + _ => Err(format!( + "invalid output format '{s}', should be one of 'guess', \ + 'binary', 'text' or 'version'" + )), + } + } +} + +#[derive(Debug)] +pub struct ConversionError { + message: String, +} + +impl From for ConversionError { + fn from(err: PlayerError) -> Self { + ConversionError { + message: format!( + "Input error: {}", + match &err { + PlayerError::DpError(s) => s, + PlayerError::LoadError(_, s) => s, + PlayerError::ResultError(_, s) => s, + PlayerError::NulError(_) => "Null path", + } + ), + } + } +} + +impl From for ConversionError { + fn from(err: RecorderError) -> Self { + ConversionError { + message: format!( + "Output error: {}", + match &err { + RecorderError::DpError(s) => s, + RecorderError::NulError(_) => "Null path", + } + ), + } + } +} + +#[no_mangle] +pub extern "C" fn dprectool_main() -> std::os::raw::c_int { + let flags = xflags::parse_or_exit! { + /// Displays version information and exits. + optional -v,--version + /// Output file. Use '-' for stdout, which is the default. + optional -o,--out output: String + /// Output format, one of 'guess' (the default), 'binary' (.dprec) or + /// 'text' (.dptxt). Alternatively, 'version' will print the recording + /// version and exit. + optional -f,--format format: OutputFormat + /// Input format, one of 'guess' (the default), 'binary' (.dprec) or + /// 'text' (.dptxt). + optional -e,--input-format input_format: InputFormat + /// Performs ACL filtering. This will filter out any commands that the + /// user wasn't allowed to actually perform, such as drawing on a layer + /// that they didn't have permission to draw on. The Drawpile client + /// would also filter these out when playing back a recording. + optional -A,--acl + /// Print message frequency table and exit. + optional --msg-freq + /// Input recording file. + optional input: String + }; + + if flags.version { + let protover = std::str::from_utf8(drawdance::DP_PROTOCOL_VERSION).unwrap(); + println!("dprectool {}", dp_cmake_config_version()); + println!("Protocol version: {}", protover); + return 0; + } + + let input_path = flags.input.unwrap_or_default(); + if input_path.is_empty() { + eprintln!("No input file given"); + return 2; + } + + let input_format = flags.input_format.unwrap_or_default(); + let output_format = flags.format.unwrap_or_default(); + if output_format == OutputFormat::Version { + return match print_version(input_format, input_path) { + Ok(_) => 0, + Err(e) => { + eprintln!("{}", e.message); + 1 + } + }; + } + + let acl_override = !flags.acl; + if flags.msg_freq { + return match print_message_frequency(input_format, input_path, acl_override) { + Ok(_) => 0, + Err(e) => { + eprintln!("{}", e.message); + 1 + } + }; + } + + let output_path_is_default = flags.out.is_none(); + let output_path = flags.out.unwrap_or("-".to_owned()); + if output_path.is_empty() { + eprintln!("No output file given"); + return 2; + } + + if input_path != "-" && output_path != "-" && output_path == input_path { + eprintln!("Input and output file can't be identical"); + return 2; + } + + match convert_recording( + input_format, + input_path, + output_format, + output_path, + output_path_is_default, + acl_override, + ) { + Ok(_) => 0, + Err(e) => { + eprintln!("{}", e.message); + 1 + } + } +} + +fn print_version(input_format: InputFormat, input_path: String) -> Result<(), ConversionError> { + let player = make_player(input_format, input_path)?; + + let compat_flag = if player.is_compatible() { "C" } else { "I" }; + let format_version = player.format_version().unwrap_or("(unknown)".to_owned()); + let writer_version = player + .writer_version() + .unwrap_or("(no writer version)".to_owned()); + + println!("{} {} {}", compat_flag, format_version, writer_version); + + Ok(()) +} + +#[derive(Default, Clone, Copy)] +struct MessageCount { + count: usize, + length: usize, +} + +impl MessageCount { + fn add(&mut self, length: usize) { + self.count += 1; + self.length += length; + } +} + +fn print_message_frequency( + input_format: InputFormat, + input_path: String, + acl_override: bool, +) -> Result<(), ConversionError> { + let mut player = make_player(input_format, input_path)?; + player.set_acl_override(acl_override); + + let mut total = MessageCount::default(); + let mut types: HashMap = HashMap::new(); + loop { + match player.step()? { + Some(msg) => { + let length = msg.length(); + total.add(length); + types.entry(msg.message_type()).or_default().add(length); + } + None => break, + }; + } + + let mut keys: Vec = types.keys().cloned().collect(); + keys.sort(); + for key in keys { + let value = types[&key]; + println!( + "{:02x} {:17} {:9} {:9} ({:.2}%)", + key, + Message::message_type_name(key), + value.count, + value.length, + value.length as f64 / total.length as f64 * 100.0f64 + ) + } + println!("Total count: {}", total.count); + println!( + "Total length: {} ({:.2} MB)", + total.length, + total.length as f64 / (1024.0f64 * 1024.0f64) + ); + + Ok(()) +} + +fn convert_recording( + input_format: InputFormat, + input_path: String, + output_format: OutputFormat, + output_path: String, + output_path_is_default: bool, + acl_override: bool, +) -> Result<(), ConversionError> { + let mut player = make_player(input_format, input_path)?; + + let rtype = output_format.to_recorder_type(&output_path, output_path_is_default, &player); + let mut recorder = if output_path == "-" { + Recorder::new_from_stdout(rtype, player.header()) + } else { + Recorder::new_from_path(rtype, player.header(), output_path) + }?; + + player.set_acl_override(acl_override); + loop { + match player.step()? { + Some(msg) => { + if !recorder.push_noinc(msg) { + break; + } + } + None => break, + }; + } + + recorder.dispose()?; + Ok(()) +} + +fn make_player(input_format: InputFormat, input_path: String) -> Result { + let ptype = input_format.to_player_type(); + if input_path == "-" { + Player::new_from_stdin(ptype) + } else { + Player::new_from_path(ptype, input_path) + } +} diff --git a/src/tools/dprectool/main.c b/src/tools/dprectool/main.c new file mode 100644 index 0000000000..b56f51acdb --- /dev/null +++ b/src/tools/dprectool/main.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +int dprectool_main(void); + +int main(void) +{ + return dprectool_main(); +}