From d15bd4771d4431d4498b0da696b63f9c49a92295 Mon Sep 17 00:00:00 2001 From: iTrooz Date: Wed, 4 Oct 2023 12:00:32 +0200 Subject: [PATCH] feat: Support for building ImHex for the web (#1328) Co-authored-by: WerWolv Co-authored-by: AnnsAnn --- .github/workflows/build_web.yml | 79 +++++++ CMakeLists.txt | 1 + README.md | 6 + cmake/build_helpers.cmake | 44 ++-- cmake/modules/ImHexPlugin.cmake | 18 +- dist/web/Dockerfile | 76 +++++++ dist/web/plugin-bundle.cpp.in | 11 + dist/web/serve.py | 14 ++ dist/web/source/enable-threads.js | 75 +++++++ dist/web/source/favicon.ico | Bin 0 -> 124397 bytes dist/web/source/index.html | 73 ++++++ dist/web/source/style.css | 28 +++ dist/web/source/wasm-config.js | 68 ++++++ lib/external/fmt | 2 +- lib/external/imgui/CMakeLists.txt | 3 +- lib/external/imgui/source/imgui_impl_glfw.cpp | 2 + lib/external/libromfs | 2 +- lib/external/libwolv | 2 +- lib/external/pattern_language | 2 +- lib/external/yara/CMakeLists.txt | 4 +- lib/libimhex/CMakeLists.txt | 21 +- .../include/hex/api/content_registry.hpp | 4 + .../include/hex/api/plugin_manager.hpp | 45 ++-- lib/libimhex/include/hex/api/task.hpp | 4 + lib/libimhex/include/hex/helpers/fs.hpp | 13 +- .../include/hex/helpers/http_requests.hpp | 208 +++--------------- .../hex/helpers/http_requests_emscripten.hpp | 72 ++++++ .../hex/helpers/http_requests_native.hpp | 140 ++++++++++++ lib/libimhex/include/hex/helpers/opengl.hpp | 9 +- lib/libimhex/include/hex/helpers/tar.hpp | 2 +- lib/libimhex/include/hex/helpers/types.hpp | 2 +- lib/libimhex/include/hex/plugin.hpp | 40 +++- .../include/hex/providers/provider_data.hpp | 10 +- .../include/hex/ui/imgui_imhex_extensions.h | 2 +- lib/libimhex/source/api/content_registry.cpp | 86 +++++--- lib/libimhex/source/api/imhex_api.cpp | 8 +- lib/libimhex/source/api/plugin_manager.cpp | 122 +++++----- lib/libimhex/source/api/task.cpp | 2 + lib/libimhex/source/helpers/crypto.cpp | 4 +- lib/libimhex/source/helpers/fs.cpp | 207 +++++++++++++---- lib/libimhex/source/helpers/http_requests.cpp | 91 +++----- .../helpers/http_requests_emscripten.cpp | 55 +++++ .../source/helpers/http_requests_native.cpp | 105 +++++++++ lib/libimhex/source/helpers/opengl.cpp | 7 + lib/libimhex/source/helpers/tar.cpp | 2 +- lib/libimhex/source/helpers/utils.cpp | 41 ++-- .../source/subcommands/subcommands.cpp | 22 +- .../source/ui/imgui_imhex_extensions.cpp | 2 - main/gui/CMakeLists.txt | 14 ++ main/gui/include/window.hpp | 2 + main/gui/source/init/splash_window.cpp | 13 +- main/gui/source/init/tasks.cpp | 29 ++- main/gui/source/main.cpp | 129 +++++++++-- main/gui/source/messaging/web.cpp | 24 ++ main/gui/source/messaging/win.cpp | 6 +- main/gui/source/window/linux_window.cpp | 2 +- main/gui/source/window/web_window.cpp | 71 ++++++ main/gui/source/window/window.cpp | 51 +++-- .../include/content/helpers/diagrams.hpp | 22 +- .../content/popups/popup_file_chooser.hpp | 4 +- .../content/providers/disk_provider.hpp | 4 +- .../content/views/view_pattern_data.hpp | 2 +- .../content/views/view_pattern_editor.hpp | 4 +- plugins/builtin/include/ui/pattern_drawer.hpp | 2 + .../builtin/source/content/data_inspector.cpp | 3 +- .../source/content/helpers/math_evaluator.cpp | 1 + .../builtin/source/content/pl_visualizers.cpp | 17 +- plugins/builtin/source/content/project.cpp | 7 +- plugins/builtin/source/content/providers.cpp | 8 +- .../content/providers/disk_provider.cpp | 6 +- plugins/builtin/source/content/recent.cpp | 8 +- .../source/content/settings_entries.cpp | 3 +- .../builtin/source/content/tools_entries.cpp | 81 ++++--- .../source/content/views/view_hex_editor.cpp | 2 +- .../source/content/views/view_information.cpp | 4 +- .../content/views/view_pattern_data.cpp | 16 +- .../content/views/view_pattern_editor.cpp | 24 +- .../source/content/views/view_store.cpp | 6 +- .../source/content/views/view_yara.cpp | 2 +- .../builtin/source/content/welcome_screen.cpp | 22 +- plugins/script_loader/CMakeLists.txt | 75 ++++--- plugins/windows/CMakeLists.txt | 1 - resources/dist/common/try_online_banner.png | Bin 0 -> 11279 bytes resources/projects/splash_wasm.xcf | Bin 0 -> 1878235 bytes 84 files changed, 1825 insertions(+), 676 deletions(-) create mode 100644 .github/workflows/build_web.yml create mode 100644 dist/web/Dockerfile create mode 100644 dist/web/plugin-bundle.cpp.in create mode 100644 dist/web/serve.py create mode 100644 dist/web/source/enable-threads.js create mode 100644 dist/web/source/favicon.ico create mode 100644 dist/web/source/index.html create mode 100644 dist/web/source/style.css create mode 100644 dist/web/source/wasm-config.js create mode 100644 lib/libimhex/include/hex/helpers/http_requests_emscripten.hpp create mode 100644 lib/libimhex/include/hex/helpers/http_requests_native.hpp create mode 100644 lib/libimhex/source/helpers/http_requests_emscripten.cpp create mode 100644 lib/libimhex/source/helpers/http_requests_native.cpp create mode 100644 main/gui/source/messaging/web.cpp create mode 100644 main/gui/source/window/web_window.cpp create mode 100644 resources/dist/common/try_online_banner.png create mode 100644 resources/projects/splash_wasm.xcf diff --git a/.github/workflows/build_web.yml b/.github/workflows/build_web.yml new file mode 100644 index 000000000..3ed5f748a --- /dev/null +++ b/.github/workflows/build_web.yml @@ -0,0 +1,79 @@ +name: Build for the web + +on: + push: + branches: ["*"] + pull_request: + workflow_dispatch: + +env: + BUILD_TYPE: Release + +permissions: + pages: write + id-token: write + actions: write + +jobs: + + build: + runs-on: ubuntu-22.04 + name: 🌍 WebAssembly + steps: + - name: 🧰 Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: 📁 Restore docker /cache + uses: actions/cache@v3 + with: + path: cache + key: build-web-cache-${{ secrets.CACHE_VERSION }} + + - name: 🐳 Inject /cache into docker + uses: reproducible-containers/buildkit-cache-dance@v2.1.2 + with: + cache-source: cache + cache-target: /cache + + - name: 🛠️ Build using docker + run: | + docker buildx build . -f dist/web/Dockerfile --progress=plain --build-arg 'JOBS=4' --output out + + - name: 🔨 Fix permissions + run: | + chmod -c -R +rX "out/" | while read line; do + echo "::warning title=Invalid file permissions automatically fixed::$line" + done + + - name: ⬆️ Upload artifacts + uses: actions/upload-pages-artifact@v2 + with: + path: out/ + + # See https://github.com/actions/cache/issues/342#issuecomment-1711054115 + - name: 🗑️ Delete old cache + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + gh extension install actions/gh-actions-cache + gh actions-cache delete "build-web-cache-${{ secrets.CACHE_VERSION }}" --confirm + + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + name: 📃 Deploy to GitHub Pages + runs-on: ubuntu-latest + + if: github.ref == 'refs/heads/master' + needs: build + + steps: + - name: 🌍 Deploy + id: deployment + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index bd95c0bec..6fd006dcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ option(IMHEX_BUNDLE_DOTNET "Bundle .NET runtime" ON) option(IMHEX_ENABLE_LTO "Enables Link Time Optimizations if possible" OFF) option(IMHEX_USE_DEFAULT_BUILD_SETTINGS "Use default build settings" OFF) option(IMHEX_STRICT_WARNINGS "Enable most available warnings and treat them as errors" ON) +option(IMHEX_STATIC_LINK_PLUGINS "Statically link all plugins into the main executable" OFF) # Basic compiler and cmake configurations set(CMAKE_CXX_STANDARD 23) diff --git a/README.md b/README.md index e79407da8..892e96874 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@

+

+ + Use the Web version of ImHex right in your browser! + +

+ ## Supporting If you like my work, please consider supporting me on GitHub Sponsors, Patreon or PayPal. Thanks a lot! diff --git a/cmake/build_helpers.cmake b/cmake/build_helpers.cmake index cbef0b1e5..d7785d1b7 100644 --- a/cmake/build_helpers.cmake +++ b/cmake/build_helpers.cmake @@ -30,6 +30,10 @@ macro(addDefines) set(IMHEX_VERSION_STRING ${IMHEX_VERSION_STRING}-MinSizeRel) add_compile_definitions(NDEBUG) endif () + + if (IMHEX_STATIC_LINK_PLUGINS) + add_compile_definitions(IMHEX_STATIC_LINK_PLUGINS) + endif () endmacro() function(addDefineToSource SOURCE DEFINE) @@ -54,6 +58,8 @@ macro(detectOS) set(PLUGINS_INSTALL_LOCATION "plugins") enable_language(OBJC) enable_language(OBJCXX) + elseif (EMSCRIPTEN) + add_compile_definitions(OS_WEB) elseif (UNIX AND NOT APPLE) add_compile_definitions(OS_LINUX) include(GNUInstallDirs) @@ -83,6 +89,8 @@ endmacro() macro(configurePackingResources) + set(IMHEX_FORCE_LINK_PLUGINS "") + option (CREATE_PACKAGE "Create a package with CPack" OFF) if (APPLE) @@ -351,8 +359,8 @@ endmacro() macro(setDefaultBuiltTypeIfUnset) if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Using Release build type as it was left unset" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release") + set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Using RelWithDebInfo build type as it was left unset" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "RelWithDebInfo") endif() endmacro() @@ -441,13 +449,17 @@ macro(setupCompilerFlags target) set(IMHEX_COMMON_FLAGS "-Wall -Wextra -Wpedantic -Werror") endif() - set(IMHEX_C_FLAGS "${IMHEX_COMMON_FLAGS} -Wno-array-bounds") + set(IMHEX_C_FLAGS "${IMHEX_COMMON_FLAGS} -Wno-array-bounds -Wno-deprecated-declarations") set(IMHEX_CXX_FLAGS "-fexceptions -frtti") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") set(IMHEX_C_FLAGS "${IMHEX_C_FLAGS} -Wno-restrict -Wno-stringop-overread -Wno-stringop-overflow -Wno-dangling-reference") endif() endif() + if (EMSCRIPTEN) + set(IMHEX_C_FLAGS "${IMHEX_C_FLAGS} -pthread -Wno-dollar-in-identifier-extension -Wno-pthreads-mem-growth") + endif () + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${IMHEX_C_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${IMHEX_CXX_FLAGS} ${IMHEX_C_FLAGS}") set(CMAKE_OBJC_FLAGS "${CMAKE_OBJC_FLAGS} ${IMHEX_COMMON_FLAGS}") @@ -489,6 +501,8 @@ macro(addBundledLibraries) set(XDGPP_INCLUDE_DIRS "${EXTERN_LIBS_FOLDER}/xdgpp") set(FPHSA_NAME_MISMATCHED ON CACHE BOOL "") + find_package(PkgConfig REQUIRED) + if(NOT USE_SYSTEM_FMT) add_subdirectory(${EXTERN_LIBS_FOLDER}/fmt EXCLUDE_FROM_ALL) set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) @@ -504,13 +518,20 @@ macro(addBundledLibraries) set(NFD_PORTAL ON CACHE BOOL "Use GTK for Linux file dialogs" FORCE) endif () - if (NOT USE_SYSTEM_NFD) - add_subdirectory(${EXTERN_LIBS_FOLDER}/nativefiledialog EXCLUDE_FROM_ALL) - set_target_properties(nfd PROPERTIES POSITION_INDEPENDENT_CODE ON) - set(NFD_LIBRARIES nfd) - else() - find_package(nfd) - set(NFD_LIBRARIES nfd) + if (NOT EMSCRIPTEN) + # curl + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBCURL REQUIRED IMPORTED_TARGET libcurl>=7.60.0) + + # nfd + if (NOT USE_SYSTEM_NFD) + add_subdirectory(${EXTERN_LIBS_FOLDER}/nativefiledialog EXCLUDE_FROM_ALL) + set_target_properties(nfd PROPERTIES POSITION_INDEPENDENT_CODE ON) + set(NFD_LIBRARIES nfd) + else() + find_package(nfd) + set(NFD_LIBRARIES nfd) + endif() endif() if(NOT USE_SYSTEM_NLOHMANN_JSON) @@ -521,9 +542,6 @@ macro(addBundledLibraries) set(NLOHMANN_JSON_LIBRARIES nlohmann_json::nlohmann_json) endif() - find_package(PkgConfig REQUIRED) - pkg_check_modules(LIBCURL REQUIRED IMPORTED_TARGET libcurl>=7.60.0) - if (NOT USE_SYSTEM_LLVM) add_subdirectory(${EXTERN_LIBS_FOLDER}/llvm-demangle EXCLUDE_FROM_ALL) set_target_properties(LLVMDemangle PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/cmake/modules/ImHexPlugin.cmake b/cmake/modules/ImHexPlugin.cmake index cfec63875..4e4051f2d 100644 --- a/cmake/modules/ImHexPlugin.cmake +++ b/cmake/modules/ImHexPlugin.cmake @@ -4,20 +4,32 @@ macro(add_imhex_plugin) set(oneValueArgs NAME) set(multiValueArgs SOURCES INCLUDES LIBRARIES) cmake_parse_arguments(IMHEX_PLUGIN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - + + if (IMHEX_STATIC_LINK_PLUGINS) + set(IMHEX_PLUGIN_LIBRARY_TYPE STATIC) + + target_link_libraries(libimhex PUBLIC ${IMHEX_PLUGIN_NAME}) + + configure_file(${CMAKE_SOURCE_DIR}/dist/web/plugin-bundle.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/plugin-bundle.cpp @ONLY) + target_sources(main PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/plugin-bundle.cpp) + else() + set(IMHEX_PLUGIN_LIBRARY_TYPE SHARED) + endif() + # Define new project for plugin project(${IMHEX_PLUGIN_NAME}) # Create a new shared library for the plugin source code - add_library(${IMHEX_PLUGIN_NAME} SHARED ${IMHEX_PLUGIN_SOURCES}) + add_library(${IMHEX_PLUGIN_NAME} ${IMHEX_PLUGIN_LIBRARY_TYPE} ${IMHEX_PLUGIN_SOURCES}) # Add include directories and link libraries - target_include_directories(${IMHEX_PLUGIN_NAME} PRIVATE ${IMHEX_PLUGIN_INCLUDES}) + target_include_directories(${IMHEX_PLUGIN_NAME} PUBLIC ${IMHEX_PLUGIN_INCLUDES}) target_link_libraries(${IMHEX_PLUGIN_NAME} PRIVATE libimhex ${FMT_LIBRARIES} ${IMHEX_PLUGIN_LIBRARIES}) # Add IMHEX_PROJECT_NAME and IMHEX_VERSION define target_compile_definitions(${IMHEX_PLUGIN_NAME} PRIVATE IMHEX_PROJECT_NAME="${IMHEX_PLUGIN_NAME}") target_compile_definitions(${IMHEX_PLUGIN_NAME} PRIVATE IMHEX_VERSION="${IMHEX_VERSION_STRING}") + target_compile_definitions(${IMHEX_PLUGIN_NAME} PRIVATE IMHEX_PLUGIN_NAME=${IMHEX_PLUGIN_NAME}) # Enable required compiler flags set_target_properties(${IMHEX_PLUGIN_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/dist/web/Dockerfile b/dist/web/Dockerfile new file mode 100644 index 000000000..bf772f8cf --- /dev/null +++ b/dist/web/Dockerfile @@ -0,0 +1,76 @@ +FROM emscripten/emsdk:latest as build + +# Used to invalidate layer cache but not mount cache +# See https://github.com/moby/moby/issues/41715#issuecomment-733976493 +ARG UNIQUEKEY 1 + +RUN apt update +RUN apt install -y git ccache autoconf automake libtool cmake pkg-config + +# Install vcpkg +# Note: we are using my fork of the repository with a libmagic patch +RUN git clone https://github.com/iTrooz/vcpkg --branch libmagic /vcpkg +RUN /vcpkg/bootstrap-vcpkg.sh + +# Patch vcpkg build instructions to add -pthread +RUN <> /emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake +EOF + +# Install dependencies with vcpkg +RUN /vcpkg/vcpkg install --triplet=wasm32-emscripten libmagic +RUN /vcpkg/vcpkg install --triplet=wasm32-emscripten freetype +RUN /vcpkg/vcpkg install --triplet=wasm32-emscripten josuttis-jthread +RUN /vcpkg/vcpkg install --triplet=wasm32-emscripten mbedtls + +# Build ImHex +ARG JOBS=4 +ENV CCACHE_DIR /cache/ccache + +RUN mkdir /build +WORKDIR /build +RUN --mount=type=cache,target=/cache \ + --mount=type=bind,source=.,target=/imhex < + +extern "C" void forceLinkPlugin_@IMHEX_PLUGIN_NAME@(); + +struct StaticLoad { + StaticLoad() { + forceLinkPlugin_@IMHEX_PLUGIN_NAME@(); + } +}; + +static StaticLoad staticLoad; \ No newline at end of file diff --git a/dist/web/serve.py b/dist/web/serve.py new file mode 100644 index 000000000..e0224f9a1 --- /dev/null +++ b/dist/web/serve.py @@ -0,0 +1,14 @@ +import http.server +import os + +class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler): + + def end_headers(self): + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + http.server.SimpleHTTPRequestHandler.end_headers(self) + +if __name__ == '__main__': + os.chdir(".") + httpd = http.server.HTTPServer(("", 9090), MyHttpRequestHandler) + httpd.serve_forever() \ No newline at end of file diff --git a/dist/web/source/enable-threads.js b/dist/web/source/enable-threads.js new file mode 100644 index 000000000..01d7d084c --- /dev/null +++ b/dist/web/source/enable-threads.js @@ -0,0 +1,75 @@ +// NOTE: This file creates a service worker that cross-origin-isolates the page (read more here: https://web.dev/coop-coep/) which allows us to use wasm threads. +// Normally you would set the COOP and COEP headers on the server to do this, but Github Pages doesn't allow this, so this is a hack to do that. + +/* Edited version of: coi-serviceworker v0.1.6 - Guido Zuidhof, licensed under MIT */ +// From here: https://github.com/gzuidhof/coi-serviceworker +if(typeof window === 'undefined') { + self.addEventListener("install", () => self.skipWaiting()); + self.addEventListener("activate", e => e.waitUntil(self.clients.claim())); + + async function handleFetch(request) { + if(request.cache === "only-if-cached" && request.mode !== "same-origin") { + return; + } + + if(request.mode === "no-cors") { // We need to set `credentials` to "omit" for no-cors requests, per this comment: https://bugs.chromium.org/p/chromium/issues/detail?id=1309901#c7 + request = new Request(request.url, { + cache: request.cache, + credentials: "omit", + headers: request.headers, + integrity: request.integrity, + destination: request.destination, + keepalive: request.keepalive, + method: request.method, + mode: request.mode, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + signal: request.signal, + }); + } + + let r = await fetch(request).catch(e => console.error(e)); + + if(r.status === 0) { + return r; + } + + const headers = new Headers(r.headers); + headers.set("Cross-Origin-Embedder-Policy", "require-corp"); // or: require-corp + headers.set("Cross-Origin-Opener-Policy", "same-origin"); + + return new Response(r.body, { status: r.status, statusText: r.statusText, headers }); + } + + self.addEventListener("fetch", function(e) { + e.respondWith(handleFetch(e.request)); // respondWith must be executed synchonously (but can be passed a Promise) + }); + +} else { + (async function() { + if(window.crossOriginIsolated !== false) return; + + let registration = await navigator.serviceWorker.register(window.document.currentScript.src).catch(e => console.error("COOP/COEP Service Worker failed to register:", e)); + if(registration) { + console.log("COOP/COEP Service Worker registered", registration.scope); + + registration.addEventListener("updatefound", () => { + console.log("Reloading page to make use of updated COOP/COEP Service Worker."); + window.location.reload(); + }); + + // If the registration is active, but it's not controlling the page + if(registration.active && !navigator.serviceWorker.controller) { + console.log("Reloading page to make use of COOP/COEP Service Worker."); + window.location.reload(); + } + } + })(); +} + +// Code to deregister: +// let registrations = await navigator.serviceWorker.getRegistrations(); +// for(let registration of registrations) { +// await registration.unregister(); +// } \ No newline at end of file diff --git a/dist/web/source/favicon.ico b/dist/web/source/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a72e2ab21acae1da8316c50f30aae5683c1f3a41 GIT binary patch literal 124397 zcmeFa2UHwKwm;m$0Xd+IB9h3NAu}+9A?KVCh#Z9i5)wig<$!WP8RbDaAR!6mWSnhm z%igs&$ZMST`|It~+WYXn$3yk~ZuK-8qHLL6cF)tCbEmqiyQ{0}R#o47>jpvSC%h~S z8YCdq7Ybh#gnU5|^!3H_qsadNd3JW{a}z-*(-j17Z}HsRR}jXz3qnPO`n-QXA#ec- zz>msXlqm>Pm-iFQagL&pBjzI!1e81UH4*svlfQmAe4!hlYu)~8BF%*DF%Bu)!mST( z4YS%W!8ROk3ANh4CeXrjQ>YbxKHp}TP-H(!k!v}4RfgHXi3uhHU-)Z@t8i+fV)K~^ z&g^uGQyZMY@yTRIwmZtU;Ao2FrBCDf8XztBxjsPHln~R{f+A9D8~0c8#G2hJvVI&G-fEDKVKE=G;nvg)q6XFOxPM9!@Mm4#{Bui zF*d@H1c$Q2QD|$JMg1~w}+lLW%=B6eG(! zoruAR-;GQ+;YO}!*^LZ2tDE3dzdhWh|93@B!p&T-O}8>#S#63VfBs5gz`%R?Uhmz> za${#xobs-w%lY%~6e@*xiv7yoDez>sv)t+r#5wf;`%Dkv{VD#NJ}B~Hx3Uzx|KFJ& zKJb%i{_i$UQnKq=ioCb;J^Ay$DG3mMQyNnCn`wUR{UT-k$rP9Ve=GGAuH|`ezLw?A zaDHKJh>iKKSSROeIbNS$&vs|0C&=^HMLEB|HNwtpU5JfweU7s7cCHt@lIdQ5IML-5 z_DQMm-qfINA5ZmTpG@&<{%wi>*MM*DPxbqs4~u=-r3^*>Z)Sz(eLg+#+uxQ1eEr!p z|G$1b#gDyNsJyx_&gm6)d6Mweyy*3Ro)_^y-_H$i2K*iH)1Pttb$QrNZ%^{g{ANMy z)bGXX{2H$R_ZOwX=Z~hizVi1K3BorO2}a*7it`32HKA!itg^8*e8hK)6UThFC|>ir z_ZQ_+Hs^DFwRp$;ioc3QSW8Ntz$zSf2NigaCNL)Obw(fg-T+#E_zw$DH|vae{a@ps z*0o;(zq;0Q17yNxbbJk25|%k`6fE)>(fuKIEh{h|H54Tg29N1n63FhyzAFH1IZ zbeh>9vsBZ8W+^5EO_NOq4oEf`(02c!R1aZyl0tqU(dGZ`i?;pm>S)>Dp9cGI57yQH z-56q3o8vTIyU<0ytvu;qnwz08-NR&Hk;CXqITnNdzijh?-({K&{4?rNI?;51Fu@E1 z0>3j^-ooKDPxsRk-F`fo${HSY?Ur zP$AC-A8`CmXZr|yQ`|kzWqJL0E?vPS!1wAWe7AnA%X$LH``=pJa~^PJq8r;C>v%2B zM(>prJMml6_HrC7_A?ij_*?2O^fb6X*LftH=RAVZ_iU>D@X9hrU15^l2!8uwe+yx* zx0zzGxABjQ+{ZG4xo%@0FZVZFe0@@o{BnW6q8@NA$Jc93kk!4VUIuK5=Xh4(F`kvW z=+y@q4DKIkJXG`5{CGv6m9R3{Mt7Ny@%;+7F>JBhXhz@InNFiB=evv)ra6x0_sv(D z2xT6|igH)IAE=LJ0jAmxeVlDMWI~~}SZ=Xku{P93SQl=myT;%2{tA4DS9y-b_jxRv<2t5tsbY*UQ!e6cDf(%? zkC~#vQ~!qr@{x?7%xT2qLYZ!AsiW9OnU3RyY&Wx)m-(93E=a+TLOVXi{N+3FxG!jod9kI@gy z-1XQp_c3gN>*&W*9Y&_km5Y6p>tZ0}dsw_&8E8?v8fC2V8p~FoJ#*w^>SIiYX(w5T zV@b0TJ7OG!opH{(+rzEzZw)YJ+X9T(CO<>A%uBy=y|00=$kTw|zA;=TtPZwTtPe2# zp%SzS!BUU0k7qiMNv&`n%kL?2H4-LyS-reD)VdZl3ft&wz}BHXXiI&9#c=I3E3uz6 z-(MZ?EbL2^>+X%Vy}v8OjO`9FW!s5%@inU42HIt*w-LX+D#~8i5N@m37Hav!W`9Gr z1%Q5fJjZQp>Pn?Pzh|nui7?IE=H;D{wzWGzCv68^L~UBG)UQvm8mXNr8>RVbetam| zRX99Bp?fgS;r{*z3w8joH_V)E3^1+S6JiSb%#`1^H_l1e7UiJW6KV6qPL#bX*o1BJ zH+sCtLqBz$pIFBkUgkonpY6;0VjXJtMOd=xFtjJsj8*y?)u+iuYvGb|%Y3{=D zbWh!*Nly1`VyxLw0MTRHLd+`cFJ$FbT|T*hm@njhEYcncSEeRbNmVG$PH_3*4C+X5B--ZjrXcgw15wueo<)H&VNs~l%UJ(vFHCS` z=acQ(*+e^59cf)Z$$7l?3^#EsYgXcFzMpWdFhKWew&(pT6P?*rz(uUzsyMsK3&{>b zb+j$N{SxYVI^9!oDZ}lDixV7h557SOc8@`Sr`E)Y_bdys6P8A}ynH3cs}^)CyPD?2 zE~PrL!?CvYQ(O(S%kZt~_`5wRK)5|QNcVQW_x)R0uIvurTDl86ndne?CCynl6z9Mn zxnAHWT*&rT+{p3x;X1ApfM$M-@tBHlAHQc+xP!1V+V$mI1wOTRvK726qPI^ZIMh#9 z7-`S-GU@nwpZIty!4!%_>&h@iAO|%#J zn00#pd(*>&_hv-szCShK{`-YqjA;3{@;w-Mh{`u}6#{5*{>(emLWEnB0u}ET`~C24 zfd?bFmF4#MXrg24l?+#Y&!!j`VMDyf%kNJMs%9)%vQJRvjVwj|LSJ+3RanD2{yv`*Eqp#V zR`-ipVfVi%@n^rE?vFb7vb(vSm7h%Z7V0y_vKvbygg2&yC_bDT_~QqYycxlrT#v^! ziB1z6^1XPmOG5001)+tccDBB;J9Rtua`{SL~VoehC2EHO_X=w>>mV^n%Gkts-iUa=kW`UA50Nwz+Rp|Yf z_a^x?N$_rw&o^%tdjCZC5P+9t=hED6gqe+c^=fg*VD!zE_lkYLmF}nX-AO**qMZMc zp1G6f#V({P*0c4A!fP+T$lL$b!g%3kaj@^#^P>LiyYdkB?QDRQ(s5US>tD?ZMjezl zLrh1$`rgc_VPDUW`Q+R3(5~-Sm-)xiKx*51cA}u|{Y`lP-68A#(kZic`!4G`gc832AIZL-_cH+ZHO+}&?@kV=FLE>1{(N4-Q1GH3e?LE}`{(h@gV|y1?a2Xa zS!I$i{Pq5PxPHAjS@^0VdDyp$ld`|9NSLICqVFmax&-N-uNK4=H_nUj{Tx_gfDAIy&{e!nzA{^f!=A;h9H&8|6ro*)0?dw}8hNMP)ipnRcA_!9Zg z3Xomi>H_GU(vK)GJ3uC&O-=!^+P{IE0yX|W{p*L;zW{i#m*7{+%zgnj`k4+NILLIE zj*-!j*Gzsj41LYSY}o6ghrRmpiw1*V{RP$d<+p}gj}tsh^ki`cp1I+8oJdI6O2Z=K2r`4>`dAv~MqCY;zcGU88^FdqEOb2dO!ujmSLac(PCdS`Ihj`bXccKAY-blzA(+r zAajz#sK>pDr2U?K$zujm^{#HK*LHW`p>IjA+m zd~iyJd1sl-1V}#n1mD+UdV~|{s zLF750>p9C1&y?GJO5CSIxpaZl@MEtv0REI@9SJI8Gto9Up-WoBiR0;bpw zYsH>gN&)uOI{qSU#tVU#V_!Ry?Q`W)j*``7x-;BA(F% zl1Cm)bpBz1pQTq#id=Z%1zMJS{#E#x3Kc$PxeFjeoCCQe$@At?3Ycy`ymgZ8@RVZP z;hhSJv>q$?nU3neJJI#xLGU35z^h0AzJl#faBi;6R^Gat?|Tt&Ndqok%JaQ+e4@u! zhwB_spd8bMsV?&q? zK$g2M(2UW3@EYYfE_bD4$Xr_?3s0Hj+L=Fzu^J;_4rnj&HLF{UxkK-lU@q!0&3^cU zREuE-d9wC%XQ;aV%L6Q`R(KmS!ZiWp;H(0&Y>e4Qxi-V)UE#c|`frJ_6}Cj$7?%P(cX~A5Mo);g8K=D>z@l!s68i}1I#4}Go?hxS@BHGak%*Mx5ZF4LEfepF~TQeGzSjB}|B&2&3t&fBBybD@vG zHbb6GGUu&4HDarj2CWqy`YG5C(0qg+C(6bOv9{y2*92SDL5|&4587K;3fVp8#DgrG z5e8En+Rq)S{_8?*sx}0gvbB)muk{|!VKs0LeSk;B4x{DRcj@%bt}=GVItbYN$=wxU z!)59u4=23?{+VfPJEPJALg9p*D3J0&uUo4wSC)8qemr={?A^ z9cfVF+`b>B`frY~t=bxF&i4^E`57P;alY7N+@onuW8{n6JIjH)s{g)tCt+WLb1r1C zT#inA1A8G)Cz<+2f8*8_K1M0n59#!1n!|V@$=*O4^q&dQe>e*mGb@z4RYU zcGNQ{cWd8|lB{G`tYg*QNE@~b{k02w6>WPeK8BCV+{Ve#Z=K%RRYpyUt57q+Ef;zt zd{5$VlqKI&ID~XZh<4ay%AQt9#jW{DvZ5V-i8mR zxr{ZK@1fuBwNm}DCsalBGWC-Ldw}!xe#VdHxa-T&Z|$Dz?&L(8LO7o0o(mZ^tHFK< z?XA#$1npn!4zp<89AuudC0N{F(JUj!#YmXwY^)7Bq3%GmtgQ}&zunlQTI*;0V1|60 z!6HvX&BufhI-;!VV3NE_6>pC|vt);{|3dvndp6jId9=XOP>z0U_gr@;XVN{e=irfh zdII(X;%)hU$|*|YQKv|&R_ukPR6#$f<1gRUSjdo@Xdh0J*I^$+tOwNvK;sa5TMuTr z=^HHdh7d#JH$vwXeg>RZ9ZzyZf2sS7_L8D8A41F@Emj)IyXrHIV%iSRXL<>>Szfu& z8)9dnBXkaXF||o{d{5?3v`y>IaI2JE;hkmTMG6xk+tpP2=mfXABq`6m}NpWV>R|Ka4$7xS1%<|DP9~1fZF8Zvk(i#V#eFg0M zt#GHOm;caZN zKB)cN8KtiOg)FbC`V2RA343r7oQF;Z^uZpj4K$ZSFQn5uyUMs(=qKDL^vk^govZ8E z$GU+%ubW6|@9RvGL+hbfyA<%|ogSU;Wg$%RwA8+s?NwKw?%Gy|OR0{m7JGu&TYRw8 z*VJHhNc*`ndY~v5xt!}=MfzV?C%Uj}*bgMQjD5VCIJ-w1f-U8EZ>M*5m2n4qdv}Th za_{6TNoS4k$KA#L-fh772~Mpw@eV0R!OM00m3mtW#a@=$SMt2;pu^Ty2dW3{O&*E0 zeXzpc+@LC~{Wut-uK%?H-zwU}AicUcFgZ|Ym=cuR zP~^kj$W^cg?B!ukj_>JRN^@yFk>r$eQoTQ@SwNYOl`z%YTKh(UZ{6)&?9-|1a2xfZ zK0Ow1|6p~Xg#q+pG#?YB`rj(@uWBgt=I?nU#|^0)@3XVXPLHa>ZR97C9NRtD-N`%C zf(7gw=0b0f(SGDR*jJ=|!?%%M&2Vi!o#LEwc7nKfsaeJxKO3RM$42|kB>y_T&!nyc z?PcCX8%`xTK3ErQWw1B8{W#DY^cpYb-sHflcPIJs_q>zu&fiPVVlF*`Zh{;&fr8$+!P_Q%?{dseFd+f#$9KAhsu_hM;3mfrgg`tNeO>!ZCf_VV*x zyt7?F>XVOWh6^9hjL7|@BnW$v;-2Y!q#tAN^iGa@>&0}}luPMuogQ5fXe-R}v(WgRS8*gChy;$H7>2|Gz&ywCa9IAogDIP5{00L+t5Z%XE7L zy;1q)jLz@uR0(AKrYutUtSl-QdZX+&*vlpO?KEHPk@~WRe9zYUOhwAoOhu1 z*k^u(Hp`)}+UcELW&93%%U{fi&i&o2F!np_E7RWc9{|6@J~Q-bTd!w(q}<5%==5j> z>R%pcuZ=zLx=+!M_pv7~0d!{>*7h&AgxeZkOmE+I44*oGMaI7a=8yjySNc&2n!)-$=g6SRvu(g^*Bb!ckn3NShqEuSAO36M_ZYX( zG5B#~xUJux%0dLTJ&*R&bHAJ$#lD1&z%MDE^aDO9_IZppq<>Q$+Nu6DejtAy@Z0j} z>$KPY9rOjdh7gT>=~znJKSO#9(5?7!6YAeY_1|3}d@(mR_xpu0>^tZakWR#tcSy%l z+Pp_2gpl*gAd-P7?*r~1d(>I(rDV+VZRq5h=vK<|)JdZ&0y?|A~!bQ2j|C;z?Qbj-JcXtUq))I@KTZN{F>HeC_vhW3KT2pQElfrGW3%=TG`e zye^%$)m!7auh3`ERrz6ejHB|Ssr~}nnj`#vUR?I~3uD#wZ(l}FWql1j6YBpf*&Y-B zI5W6Y{Z~df3l(8b+Fvb7sQdGxIQIR5XaICJG=X$Dx&zJo)W=P8BiV=3f*+mD^pZn| zNBFNb6NNu5O3eJeB0((w7nJo4bWiTj2zh+F$S<{VdPt}G-@w{do#xx`nmA58`jCf;OnDG6w3BuQlQyjiqlDri*7Cn^J@mgZl z*NYOW?$3_i(l9N|_MI6Ko$CKQ<9QD}?}2~69uW0~1lq&GPY@o#9FWp41wkpLf7cez zpD7)9OUa-6mrpCidXn0d*xp~2Qg2fKJuM|^lsBLP(59z_jOTh<=#b~X=RMGK4-hX) zN*NOG`j1SBXQuc4EB&bLuLEdPU-usfRQ><}_3yvLkL@Xfyxd~^`~aJ>nVzOAX1E)! zn4#!>n2s_DO5KeXLNCvrFY3?G1L+Rqgd7JwA=h!74)kDb(#!|j!Ctftpc7}80!TI; zIAVg)03q3QP>*l$o+vbU|Ep9!H{XWt)93k8GOzc&+p2fcFY3%hfo9}HVK#r|5UuoO`9XL_X5p5>j zRt}E3&=1vRMY3TJ^KEox`7-*Pcl~?u#TU{3uZ5k7irkC2KCPtddLDhy6Ij)j@#>(LpN5^k0l-1nb7^5 zY^VFM$ac64&~@XH%klr-Dq(lBd*;Og|5oU~vP-$%>|%~LPwBV=JjQt&V6Xgf7r>uE zKI;e=V@#Fi&elgd()jQ9Eck~Vys#v|N(XwD4U3@PIG^iRj$rT?!0qzMHlKVLOtT;H zu-I;dY;qSmwo-@C`h)##p-gF(wkp`Fb)~;4BYjr_=t*%{hO|Oy#FqJ+H=ND!&490i zwDu67J`YH%%lA*KigiA|HpH5(2{2=%d#VY`eT~_CPeWGZIA$edr=Ds3Uk0BE%Y$rm zs6I=eTTD8Uq*qK(fn$Ps@P7cm0}tUJKn9SS+ht1o>F~pmTIM#s6?)ZdD*3@sucx%8 zlFmNaAHYAzL80G^TE>IG)OYiPJ+Ls_)3^+JT5Oi%2v!Px-%?tacnUpcQFoqYSPWUg zws}3%`oA*RT38h#(}8Ys!!qcpE>`GqUkjRgqTGjs^Qea~NZdzNiIYy~?I&|Q^!%wH2K6Dq@Ob)a9`unPL6q}xh9AxOWObejpNPiDJ} zd06T^MpoK|jy{;NuJJHyYHx z+w$1XQ~~RA`Ep+qwwU~+w9QMJo1?fLz)Z}GLfhdh*x8tF%jxOD#t1uMW2A%5rZ8Cp z{Bf{K=n0cQ4${wE$90DF*-Gf?E>MhpSnfJj1|Jzc6(`)x@gSU~!f#0{^vdCXMO;gy zx|8h!)fwLr-hSi3J+yycCiy8TU*&HG9q(~mcbxRLB_JO!vtiSKb$JE5oY-T%y1Tdq zdf8i|9d))w*fzlb1B0IuwgG-dNVlAPju4O!l?wN958WuFQZ@7kSUH~h7* zsScx7up6me74g)(s%QrRe%^GpN7*%O4S`-Mbb>d-XA9}4OF(+gOFZ=-F7(is!AD7t zWxhShM_3$SnF_x#t?;G7*Aa0oq4h+M*Lfc5Jk4yDH+Td3&I9u_r;Uvp3eU0lJ`!eBF>< z|4#U%A$@NG`28b2>xWB}hO(u~_Ik=4U-0SlKw(v|O)7K)Th~*aF?Te9>P%}Hcnz`r zLwjid*~#RmryP2Er~`cDsP+G8d=ilD$5PC(GWpmQ?0#{_s{E{T_zM#DCAsSCi+66Q zjK%tPitI-5F)uS_ONF zt*A5Ex$qRnQk}7u^7dN}d#z&;ZRhNAsjw~Dp&YuAY*(ncI4)^ys>dhIRrsZ3^WDd< zU|*NDJ@-ovj!bY9j-=851peETpLvJ4$#fF=5`q>=;%v8 z{-?GCnLUKhQQ7)HvmQ(Qc3vd;AWPjB=g>-ZmTWaB-GgyNb*6Qp!qb4a-+Fit?Y}cu z*d6a&ekdM%exy}9+X)(%)E67EE-dykT*3afq{n)7cM)<*@|mRr`;La=iLg_EPQ7R& zU;{mW8*bxq5buV)!o!V0<}%O--4(_2|KGxBp(@5H^;6b<`0c`6l0aSOO{g>0 z(Wkfn%>_brqD#37a@51oHf`fk8kb~$0oxz8%-d)M`=6CP)~maVuqzf$W-4_~r@J?t zN_Ju=VBc^8zP8BUmS`VBzOk&?&M?b|TS6>kTRZ5vcP#hOl34nlrmB*hTMxn3gXWPm zpXiwCybX0;;bVLlBC8i=BXl|z(RR+>s~|sN<;PQ8SPg70r14mTu~~yUkv}r{m1Qe^ zO;)h~UfuTGFFAmZS@{fksyCx1V{1iJrPz9tHP{gfF6tg z+p>6kPhC=vrN~=h*THRF$lgV&JJlKXPQ=Hf{rz6F?Wz5Z%Y?%zZsn&^UEz1jj*mwg zlM;|$UHD65SeGFGUDsp1y1RHj+gqs3@zH^uMFVuPVbkEi?P|zA;{xm($nP1A3D}D~ z+!1aq+xZlGk)Kv1()TnKe%o4)qt0X_bqwQxr#PlM!(NK5^*811x9h3>pRR(RHurMq zwzAWZbxQU(kI6Kq=1{tewVvB&(2IX>hwP6zfm4W!Ro2RjqVHizs@E&@(- zA9AvXyCZF6@R8PIc`d7xg@eg%sb?oDTB*)tYjp}XRw9W0@Ty_Qwjt1rkAJ(7w0+n0 zkA1#YI5E+q{6dC1`DbJ2VDCggc25M&WH%cahyt&5`S>*)b$OQLP_|0prB|in~IYjna1XSmP zsPpDv3*LVFQ9ZQ(59@@}8J^{rVRLjb)tTdka3cZPfgO&Ku`MCA|FOHrdUbd4T7f_L z?9;hk;M+iY!B=71LbfrIUpz`_Ou$~};rodb9h3ukks_ksZ-iF2m{CGb+7;7&(6xZI)qhq;j zQ@U^>-7^(_3tOqqWG8i51E|hNu`XgQ;q7-C)BS!L`(}%9A;-J?W*&U#pv`U-uo2-tAjuCTfn@sydpWi{!iJB3=bqaCaEAaHdik9K z_|Svx7UAU)ukJ41of0hEof@L^#^k^TO*<*cK8w=p8Q5z` zc6tc^h%)%>?5-%D&vxbr=d!(1Zx{Hq-h|&s@)^m;lRBk3!`_ZnN69$;du;!I?jk>t z($*w_^T8eO2TyBni|vq*#&%7dT_s$illX|bnuk|kM)yM~@CQx6{x(Hs2w_iTC z`~6h*W3_O-FrfUcVt@7~Y^EAuFGn_W5|Cf}3)l;TO*qcr!x8Dbf9g9^ z0$ZuhG>$ZZ>U~_Ph1e{{K}8cP9sze>g3O``+X}P06pR8p!tW3ibhO5**R~ zTRl~)+iUP=D1108O6TL!@P?141#)}3kBgPuCn?#OkTD5VLgEzJs|>P+K@>Ma4`5cZbPe{kmQcOT!qJ^^DVg||yW z%Reg(WuLHalAQv^+-Vx3eP~L_0yUu_HS{KlVk5 zXm3aUZ*S#zJiMHtkikx|yQ0MQA1fi>qN$&jhP8eQzqOLBBBh@KsLrt0gkFHl&uITC z`11`f|Dr6MeGZ#Fv0kDLBOQ}J+<8R#)jX|3T@~?G?qT2u>@OBvR9?= z0_x29e{aLC_tSY<>~jI@)12SUj$mKR2;}%6oRDu{0`l$qMxH0x5~BTodZztP_zOn+ z<9Pf3P!`61kN5lm_GJWrRHtNbc`wiF;k9f}+4UamExG>7rMaLdgVH|0_-NC@Bc-IxRS>B9%o{{eh z5kQZmfhR_JN0VF_e4~)?qo0QU54HV801217D_hEc4gN_+p3U}o8$LF8oQLz^x8bXr z)#05NCSt9^d*Dm?;n8Hd>?G_xyZedyFRj9+Sb5IgB)8U05q4~2n5-SZ9}!y~XvIq0 zO>QX-bc5ha*}XgP)F0+u=3{+eOSBV%tsUDOZrcvv&x&mbm7zRq5`Mfu?H|ye`_0ly zv^N+RYcq~)Si3Es-WzH*Qju&sE?|A6(ECIc9J>CIZ%l}vxYHME& z-~HYF#P%~`N1}V-jiSKE*K(EYD)E%yUo`=KxEbKTU_06bd(=MxzSIQpVP6v9`k&xS z{;z;8%Yn@!`IcteqZ~G)PA^gWw5vMa{X>2AJvcg+{5{q#GP?OXIkdDLUX7UH(nWN+OS@HHK4z+W`2RzW43?Xa<|!U-f?S`AvNTKj?o!KRf@S3mZ?VFC_e5EC?t<{HVu> zHN|ae$p%#uB)eNN?}?9b&3sLpQ_W|kW4ivQSwVb#KAzIP{hQ<52EfPqz3&%@_PEkB zQu=e=Bi%#3*ncP5TmS6#U(#s5)DEdU>742!9&6e)Kd}{lVmB<^r)jG!=5=g8>Aw;F zpR^ygwr%#*?cYPb-JiTevVCs<4yt3j_9s6b|MVS39SS7;Yqs%8o2BFN87Z_*}=!569hS0@G28aMzx?(a(Ym)a>kN9W>k*YDB9k!Jfb&zwHT z{%&_auK&p17GTqx4d3*wR zW^vN(Ka)N9uYCV^sPi8{f4@65=s)W7eeK{QqTBWp&Hivjn($>svi-M9QZB+?{PwpM ziML??-2oDBeTV!mVKMRv=(;-X>Uqk;8h`uavZ$LFZ&AOWAJ6H3#6=M9&xwEOn^eu6$7;n(lEp9rxdp8r4Zf#*H&ya%55!1Eq>-UH8j;CT-` zTMy8F?!P-cTg(4%J|*E=(qACQ7XR)*ZIRmbe-o=euhrB?)K|2bPRb10fd2pHKy9G5 zP@AN2fWzm%E_#6QPyIwVCmkI!5FjOa4%lPrblfpd^PHZe^Nx8^c~Yu*UAkX0)htKz z-mdbc>zXN!${pG`7*LLd!uXV_i&MDPAre~$|uFBEO z(|lGlU-MY=9?fz)=1b@5bfivy=P?8NTfX|z3*@tsz6a0o-yZjLMt$;Renh8UMC zYa<*!SRG>XA#_DQTp4KbA^be5fsQrLrTk}05t~(AURPyye+@CTHR{yy_(@sI{mnmw zf2j|UzBAw3bROc5(>JZpz5%%|`a+J~X!7|vEZc6hXNL9gfHaFC{;6hzL@-4vfjSR< zAN?gr!7;!l*=*2DDQ2`x_u7x}|MIp3#kRf43bs4Jh3!O~6vX#|pGzB76>j}+2Z&S7 zw!p8-rcg_^J_LSTf-L`#0@DqHY{yoKH;P>zeTbgV(_=TtWg#87hgZe}p{`0C2rFyV~$*yca zd>>TD*uln4#(iCqzf0KRilBK+d79@P@7FxC` zGtMaP?Iz%VZLnoip3`{CzQX?$_n&nvQvMh_iECYUCGt35b z3+bi<82sTf_`_uw1Au`|SO?4jzW~KO-b(oIzdzNp`dEe+JDTdo;75T`%v8iC{I>)0 zr$Rn@X?&2sVp<2V1~lb4_Z|LA6vje{hq)Gf6dsu(AI}g2nH9=(SswiU=EC1^uDKfM zSOVnb0I~s@7#}naXbem+8Q6?(g4ff00{4jV_7^$+4^Q-}KAojxi22D*!1jXTryhfy zFKn>8grnW$OZPwRx^$mr`Yg|AK2OI~2Kj!bF|Y?~0e!zVgjhA@_aXjgxSI$wyezb4 zxEraayN+j5okp`_Tljai9>NOYmy)1BofcXS)W{S4LDQJX!Z=9BI7l-a*bJIM322YS zXFuckSEYMZpUw7WXEHq4sZ=@ao8XrPHq-xVKz^;es*~jJ8Zkooc%`+A)&TNXzA@D5 z>wIT@a-u?Q{ipwC!r${O9}BIS9!9ECH+?qUc?_Fs2miJ*_<6P-#3sSlwFJdDraTEK zzYvgXLH@8Y2FwRFPc$E_1oT!rs83$0N%z`!4){L{{GW!+*D2s%>eGK#%6)bNzt5bK zzi&KGF_&o#AV1AJK*Medwf-9Gzct#~C-na;Pg9}H+e!<5&s8%O`tZXy2EKjaM-})N z{hf=x&!^Z7;=XFhU+q)^*AJcX4x168RmnV;d@pI=q>zHpS+6szhA=t8IFHv z?(15@{WIZ&@SzEWTlh{8LGxJ3>p7Ktu=kWw*|a{?AO-;8pJ;$hVb))R{wF6oeTM&8 zN-M20;C~kI58sL0hb;Lx=Xh4*n(!_8f#<&9)jo`;z!!WG#sK`m6V2ERpYTdRZ{q*; zBNM$2z$X(sm+1i?->$IpMEr4%tEc0sCw%nkHML)R?%$q{I{^8$r}xpAB|3n73sM{m z;QyP6_G9hPVYHw1PyfyKG8Ja~SZcwqv}(4y0h=k;W78erM;LyYC483v*P`!#(f|K+ zj12{znjpgA!B^xvWLz<(|9e-{4X&j9~Y z9D!eiLp4q`V+>F^95-ED)4cClk11Ax=Cd?z3IFir4gAa4X5c@?R?h+LBpL8O{fAfw z!aP4~tvOyMs&WrQR^~dEmBLRq`S>UNO1Pe`j)5S?LLk56qVI0HhiHWY(2KcNLz@v3 zK?&%UcEEm==ER^wX`V;mLx!Etgl#|Uu+Jin#u?B6n*I7qkEIwAn$JmjU8nHf1zbJ> zI`1w|y4E=*{1fkjIQKkO3Gn}Ig7p}3qSGfaiwOU7e66(*_d+!nzVl}T|FaPHqZIfj z-|U+Brq~ynzT4?|I_@F=J?*DG{5I;kT#UZ5>Q-hA57JuWHT&MMhXh4z$5Dy6ae-~{z3M$up zAMlrSPp{m|@$X};1v#{8o|h5%&Shor$4xOVG;uu(xSr)eppFBAl#XdkOv4zMgf$@F zW>_T_s38Rzl`V9F;9L;-*S!T$n3b76jfmDVC3bJap`6Y?F+=E%pea>QvM{A%L+Nvw?# zJZ=NUfhog1Gw`jLLNX&;-DbpIPy&86{$GFnb@HFV*Z)Hky=o8>261nE*u`uwMsb8L zWWW~-kNcyJ{{tU7Jdg7EF`aAXQGVA@hkOFME*<})((|Y@$uN#4!|x4Z6>X1p_%7C3 zkDO@r$@hN|@V_X)Qmew(TvY-5!w)x`hqzC3DK-c2OL0{wrMO2D@Yo&d^Vzsoif_eK ztO3P#!<&ojMkpuQksF_1_Zv8HAmN|r|3Nhw-bd;S1KH&QKXy6Khru^39|trRB)EiQ z0y@`BrTk}3;cJvXE2UIE9it9nDxG_=%RImRIps+}_t)Wh;$x2^PSc?jH?}L@<=Y7k z21Nh$3H`q~z(QCYY^}A}&qB4>$CP}h^B5BI5SOGKzB|O4S)e|r_$?HRh~mynwj0r0 zY&TK~_;tvC@-*T9^+OYt2d_*DVpjoIiUQb`LVpIIf~+3nL4qqd)&$Cba<0A~-;m24 z0RA)?0p&}_KPzolKD|qPN2e5Zy{ZPfM(??h>&s5ThZW>nY-gg|H!1e|_Go9H@c&B! zEIIy{_*inF%FN9 zqaSlr2_vNfC_$TRF~U6ZS7pef15^POWA0wrF2`FE>wlKv01&8q?UJfxIZ3vn9$B2_D%FG-`ayob#=wKc* z6LJwTrE@7ywxhC`)vOQzUL6{PNs!a1$`v86tTq?dyHcgON#I- z;hLw2)wEaxD0UOYvzm)|cr&pElsJxTMjRt0;1{5{$6Go65!1MuWK**sn?T$>bw59e zOUmVxh)p_;;*+Xnm6SKRO*W1FDT~1^amIrt;BCDjXV-!sJw3=wKAMm95;4HqcxEX^ zsFZ5*&~!WlK(bX)W;T>%TMcar(jVAo`X4KTY=jjdc3LZgtW?l@Lu^K49z%#?+!1~? z@x26NLW0$uIMQk%l(NLBSMjes)5ExWuBQ>34cWv@yodM=l1H}5 zCP_|7=Ncf%rYTk}$)*usuqEDPaIfJ%9QcRKq;UacCWzf5^2}HRiD#CeEoLh6q~n>q z?!$PT!a~T*AS(m@2lN^KSB3!p;dWZ9f~{4E8_QN8hUPLaeYONKsWfpd#h9ZQYZMQW zU@6APVvGr*6=qXROs7%JhzF^BT1?0uXMFpMeEuVTV)cA4V>So<{8PIq_vgHCV_h{ zpH!zb&u9$LJ|@Ygu{Y6@WICkR=6?k6k64tAi+x0%c|Pc+jy$u-KZ9@9;GaomLUBr` zt7T@8l{JML4(hY{kJxO&ng~0s%1|3sWv~@njTpTvy$#q3#O2e(HQ{@OMyy4Ow@Bwp zk-rG>`{qJsgcx?sGhOtQfM>^e`zJa65kItg5y_??o1pj12CY#BS){b4O2<#+lj1dL zo)v>$glxJc*{t{Q51E}_g`auj67b9wp8A|;=CP8o21xP|;*B~2@zf+UnL)8?F$N$r zZ3^o{{KJQ@ur}O5Yh9R3wJyYpA)XssjTmw(5tmRC->baFA{ML&QtU>G4Z9p;fM|tz z82_{7W145l^^}12SbX*~j{mvdX4Mtw=Y^0>%)?llgMQ_)f2H-cLmXg|S1fZ^r(y67cL8Z~r9WUmNjrt3}xaWK-y8P1%$rqtZ-i{_%N+KIXDXl1VQ*Wq{##WY6SY5kk$Hnti2$4bDn zW4!&79RG{_EUG2hL^&$Q@Bi{F>SbME4 zk@l)B5i;1MSg;NLCW!lO$m1&$j@PSU1JVt?Phx>@;A2FLhrSpx!v%_Q&ESQVfc98? z_A`$E3gBOoO)NpI;0mlYG>@b;wVjMgEwiF)ZL&$sGsq2FGOT(J|M50@df=HGVZX&z zfoEQUd9chwj6qJkvcw~c>63HfmuURX#rP$e*)+$IP0=QOH~%*y-ucEjC#|hf_A2Z} zQ|v&t332Z?B9=16d?$QMao>5&W_8?Viru^sK(Wv%7W)#9@ob^{xMuLeO2D&Yy#13L z|A={Cy(-WWvI!HlY(kVfXv(HK9@Cmj^Nh7}ug(7i*;qaB%#E<$Vr#%N zcgr&o-=qOF9*AC^kGheb&2*n3sz@#y2nU&{tv)xjh!2{fy&(X@FI=rfgy<%`?og zo`BX|nrEVHlFO!BvTTO;D*nMUH?9ZIgxIZ|XIh1C0P)2gVHNTu;5;+NfFv_P-20|j z(_wvvf9ypG+hQHHut%oa5oJfQGZ91CjK{d&3_KG^_}+?RDRw!S$t3*F@*#zbp%_B)Br75S7=H7Dkx0vSH9LOe1okzFi$VT+q{7uub&_#K~F*-n3FfHX+HQI;Z3K8cg$yXhpSbvL(-UWUt{r*>0R3c&A3_Z?a9n z=A36DT4*iC0P#su%6TSr9v#ydq3^^J)Qw~&v*mhC34MtF-Eq#sjs&^Zo;YXK9@3|P zO~OvZBi|8h%C-}Z0fcJ_cH($vunF6R>pPIrIo&G2$zph)t^d^tF2bH9SFL>sE~ zjwW7;z7s2nUV;Riux+k9?~{TCu%h<{a0W_$Itp zhY^@_*o)&`SSPmP8?YWS<5fOJ&5(sD0nd){_D^#BLq=U)6={cgX2s{(M$nv+Y(h+* z%sFZ9N%L$apJ!Y)*)qvtbg#|-G~ge+Q{%2^JJ8HltSXrL!C2&vP(O)J+Dd$qntrBq z3$HtP1&mMPnHRW^Z%VQ1L;NQL{}U8i2a@Efg9$u#I@=dz&8ovK)OZ&0y$?9&5N^)) zt6?9~-53)@D{jCyV70Gt^GaV6C7?YPpZ$#EA9PkVpJ(7rh&SDgc}DZ-iG0FhHL(pSA2)m^H@KLl6-vGidfrM{L4@6kB!G0Xm zIg$1L`e-kwnc>qZ^a;%@@<$?%B+<=MO5@-O&ul<4lcuyj z#Q)(5Zo;87cdf%IZmJ`QoqQ zKn~Wt2Ka}ZrdN=ULOA}nL^@P+-W2xo+tJUQH^n@XWD;GawARuZK>Ju+HsvwCWvX-U z;XlL4Ko2}q<37^&R@2P%E!+uuNTQja(9I&h#CayH5u9fRtw=Is=ngcc+4SA~Ka#2d z{ynr*6Wmm)WI6Y(btp#04n|o4*Ou+@eJIM39o7Ikr!hjbBCX5o0!*7L{mqnsUd8{` zD2M93(3@|^n_>;1xg^OXI;M1u)(DbKaazk$zh#>9*j}6ena+lKd*fXi$sgT5(9CRN8 zW1iWz@unnukk(W#lOVZ-1nO%v&o*G5sby0wB`&>(|197iJX7OA(9Gn^MWUG{-l$Uw zTAA{8V~yC3HG*iEH9kfpGigef^&S3I6WxWw8J=24)7(|ZCcrK+(TS;Go34tnX@_gV zc?}0^Ruf~*jsjGCjDS}Ftyl#a*v3G!X6TP70lkX-b*7f3}N}-oYf-Mou$YROx3ap z^f9do#4FI=%t{}lmYHt7hyPr;k=~&c*Tx#yaUTKA{Dfu#jSM_Xv@+4lBF#*xI6n4p zo(ViNcqZtMG-cV2>9hHNJl#vE$?(xSneM4NHBkZIdM=Fo6dsSaWygVI5#KV7Z^HRW zf_M?=yar>0XvN(`1B6&KLq@0E9MZe^-xKdreI!ZFd6NV1n@HnLb4{h=wmGLhrgd0T zHUZgG%dB3-zp>tt6t|C#rpQ?h{LxA@6Q_?+KTSGW^O(i}$yRn?eIh*@=#DgH+w~p( zPi82EV_AM$r!%}%XVTr_TTKo>h453BU?<{Qjqg*y`6-Nv(*y}3oW$`ld?yZJ4cLP{ zgQ`%=X6O$oArJ4>`M)>OrCNo4J_4T^(ma#a)Q&05JIbTAmSht<)Ut^+zQ!$O3jJPN z|MOgp^;8oSjmJSV9Rtl=gY{CPnTSr7XylG5eJ`nQyTB`K0nZG5`=%Uwy*``&r!u`c z{?BH5sm{T_=2`exJp&(ArxWctzB$g-_&$S_;B2BD2O0;o1{}e+uNrjO_Atw4=np9Y zy^8;RiSp{B=x2#Hq46flAfCt~B$vC)dKLfslU%Ehp`VW? ziM+`XHQh<;s>qj0Dfm;&FM{^62~)Pt-?U{;FXF$z%|!25n)^qmKr@|8mS`rCCeoyl ziFT6cCOW1uaR~L3c%}`3W=;7HWBY9W!>_V%68Jx#?X9{1|9a=)UyXcQor7=Gj`*&{ zm^e?6ECQXM#u%Y-Py7$$;LY2^t(DLr>Q(&1?|=1)H1HtWby^S@AGs&`_d$46&CGo1#_bP_bv3Csmr4|@U{1GEMZ&$Jyp z)5ak4rUJ*l!+&kIk8lR~zX1H#!M|D^{1csrAG7oDQ`ZsS7l8K*DGsbo4KxnUU=1K% zf%u=@z&~OeC;`2S|3fKm)kK?~KtGGTiH!3m(weH7(%d7yMVe>Tpi6i^2b#6a_v~H# zpGx=q=p1M!@<(}^{09@wgfSq}N<=S7KzSn2*dRJ#KdqPGnV>t=ROmFW&*uMyY+vDA zmcQ1;93R!CZ1`0oA6W2}c42}e$2H+x!Z)QC2__IY^89mH15V)EcZlL&MA|g(im*`v zdKLdiCMc>;qn}TKHl=ySc@wMw(z@DJD$TR~m}fgdYe6>AvcS`**XDl_=Ktvo&&Knh zna;tl+gZ>|XFxMaw6R1hbxdh|(0X|g>*ek+OVS-`D(XZ0Uj+WoXZmZwx0>oQd|F+G zZ?#MCS9dYhiPaIF)%d;yd`oaS)sYd<7^uZ}f_R0)paH6*WX*dbWlBJ=;$M~OUQNE= z&ww^1|EcsoX^rLlDDfyAfOuEpO=%x%7rp_IO|&fZ>OK4yyPN5OW^SzG-%MA0GvOx` zb3vkw+rFI=&7@B0dr5UWjP-I4^ai$tS~gAU!}t%EbNz&iIRRSrdA_PEx!$Zk%bi_L zcV(B;oY^Hco+W(O!+I6}$0vGJOSCEZW<87d5#1s3Cn6sr%{wX8 zT%wtYhTR8Q_Kq;C zrpbMX|Eu`{LVbR)*0lmZ)%AQIb}h$~UCnf3SJPeC6~Z;v0mAt;3DTX}bzCE${7d*w z5U)V|@4*}1)|I0 z)4UnDS03nP{GZD3tZqk};(Zbi+A*cMM|sjblXz3eCR&#H_9p%<^guH;Ud1=_3ci{3 z>GHO3CecaKH?w0(>m|_%$7#NRX5JlP(=@e@@qY#Qzft6`x>e}MZssZ3P2m4VrmKi+ zHNJ0RJhVZkoZSN4K;9L6CoX{YIRX45u2D1M8Ylt1GWk&mpZ{kvysGPSyg6-pQKC&T zk2Lv`j>i|UMw|s*a{LKz+OpiQ_wZlhWubRD&-bJ2_-0;He>15sB>IR_>6@vU(t1gB zft6eLuR1Z{~G;Gq0(?nVR&GbS!;4>72$z9o8?RVQJ3@ zv~p94n_-{f|7LN3aJ@K4>vnOV>dqv8cBjCb-Olk~x3k>Z@O(SVmEF;RyErDG{Oec) zE`jzvjXk59c!%c0@eWGthxRJ|YqPzpuc4o>W_yA*b>p<@WsEsmTRBg{Q}89oj2$IPh;k>C$%PN2KUc>(kZ%e(a1%8dU3Vqp4d^2xg&7{82{8nln(|423X$%l= zEz!)Nm7AvbA^vYq4i;`s3DvqYIY@P{IDp+N^kH{%J=vXX1;@FB>$}-*>|O_;JX$9% zW8EY9$I%3bW>tcN640yohitl3)dTvcL#002HN*L`0^7;PR-CCR31xo>Q($B_DeOf~5w7*-)DYEMQ-auAll<751xog2o(J&l z&T&q-ek)Iew~Q(9lu zG-^jWRhnnCk3qZ%`nhFoQ19V?7Vr<6sj*>l0BgXS`9>FOX2~ zpW}b`cmL~pp0P5M%#tzQjFFMd%%lk%Luvp7Z3NWEeDKcbkuLXgh7Pv~pkInV*pDF| z`X*IC&d-5w@(k2DbRXnT;a8s&${5`@fxZ^(o1~oLSPPS7`DOZl0pMR{Ez@3a18k;+ z*rv^_1^TevW}@54?cpCj6Ob32XOpVIIRWkMNz*V?CbbOkExkFo*D$;t%%Ch_3-{Qw@-FHSo2n zK%Ikf{KGc|cyGpG+E;>g0NMc9S9}cmCZKP+2KL(iW%#>F9H*|fk!^=M(*WAc`af*u zzqXTT+X?L@tRK*4g5&FOo(-5gIO%$l;UB}l9^hXM@NaTZNNur~Lt1R4kY+0}q}f8` zPk6VOpF|+FnhPVX7Fz`CLp^9GpgzL!FR-`O@*_PS5kS8Z{~e{~Qt@?A&$js{(Dy{! z*KfWl^ic%>ZPf$lk8jF(t8cOf*6R6X_`6B6QrFtbbu`(@AWgt#HUfElZ!^*DB-(bu zIP{^QpHKqi1?Sm-xq}m6@6CS<|3*heR4uUoEshGQtq$@?tF08$YAuemSc(9=0nXbn zZv(hPY6p3cU_EFA?F7`vV(`u|N1u23u^z7om_ztW@h`KGj&A^ZRuANi_D!JgxoKBn z2xz-+3XfqufMXb&at3+^T?Cq-0)vodC@lVWPSqdd}XCylh*h$C%) zpEgjZH}U?8c?ZB8QYR#Xwy6=cO%0%J zst0urZBw_0XnjNL8EgwS<-FN9Ik9$I;n(5sA;n7FU?<Hlh}^0cDUML%wA&e^dm}uf!j((n==28ORyxS(ByMpL`Rv zZT&t(>lyS-^WdI~I$Uevimbm(|GlKysGIB++B-INMha;IHnZ)kO@ww4Z7aVIVf}!8 zE40r9eC7!+QO18v|Jz(tP>mPVF*}@Wq{~(k>9Q946W-kbZ#4CQ;~pC^ zaE>Tw13+I9=p!8eEHL6*&Nbu{$u)%A8UB*r=KG)L*0S;Fwh8JP^i81M`Q9Jh9*6Y- z`r{SQHwE*vfSlJNm3}S$Ee?w9osLjvq=C+eZ|V%XZA9zM_aUqs&`*Fq6P#xQeC7$T zkH|lUe}{`Is>S6LW|xa{Y7fA_$59sPwv$A<0Y87>ZL@_p#JqPKb%Wz}&`v;p;5(y2 zWBz3@7e)loF99FiWD|ejXUDe#Im5OI`X;SF&!F8w+tuwp>ElUy46v!z1vv<>2{C-IwOH}e$^S+HvY+0qHQJ!Vf}!0w+f6G!g)4N^m!+I z#E$-B_;&&P+g#K!yDzAu_PQt`eNJ*nue}t~3;6j1Z(A{>Z|34PNo|MYv`6!cBj zqE&t!{yqTzb|=eT*l3#hLyw z{Ch5_qB?>7?{!s4eQ`km5A;5O<9>Ci?%zqcBU%axuHTrN5- zC<5r0>VJd1LVORfO+Aisply;uxVIKziOnF9p6JYP%e+>UVcU4rkhdO4z zyISg?n+h^`K>-r2d5W5U3AB4qG$~LP&!ktrzeF`-*VscaxuUk%!m4= z_%}Kz#G~~LwoN@><=C^$FM)F1POvV(HUi2S&S%O8_=B~&ekuMvt}5+=Zpz4@i##&m zDE(DuB!J!kUD>8HXx)J!w5hOeHh?x0`b-5z{1adwp??hj7akCQHOv7IwbUVZRb<#z z5gBrpMTQ)u{sr$5khYy*eSmeo9r)yR;GJPPz`x{-kVxqnp%fg zG%~ym?@@3(>I6yp4}x_8wiB>DhU4FrCMTE6&Ylzj^lR~Vg!roiBRx*?NDt`G!v0JT>;nSo1sJ1gvtI!6`o89O04(79!slRa zPU+c`%V3V42%!HdAOG$=C=>kWHgX9q4hl#Ecn4Gq-vNU8Hk)&l;5+y42>KzA;2btM zS7Zx&uttYA_ZrZ%v*3ONfM6YfcHloDY;fI9ofC(SJhPVTYOs?-8o(TYdiV|yyf?20 zdJS#iCc)#c)bKTgb8Fx`Y~U+Hn}lQ=@K1V(G5t^We}~vXc|iJ4%Kg)8)Y#~7YV8yr zG}y}{=s5t5aGVvs2mVS;AVgC$$b$s)px!?N{FDHG3XBCo|6fp~;Pijr{^Q`_05&&y zB`h|VPOP?;MaoRYk)qA-Z8rTT=sW$GfWAZ7!nF**`)DK?tkHK@jdRWU8Ssmio%=QRAIKWYgecoYBB{_+ z5&_=)9@scsSnn)0PC$}0)93Les>Mm1^R+-BfudV-bG| zP&NrB1o$IJkj(KJB_=8Xz~>G?P|v^r!89C7qMamS4EtCF=*aoGs7XaY`M#C)8!tag z`ynq|{1JynIcEF@D&>=c~>wLlfJY&H@@T~EtMuMwPjRnCo(e&r%Z!Zh_7?uZ? zlRqr)_hp80bQ(HD z^MlTVVXm>z;+9R*h+)>X%d_B0N`AJO#*#_rtfKc-uC0o^S=4AXrJ~!3%@=e-hcb^zmLPbuW4}p z3Y^1K1^R!Lpw9&So~ z7dd?^_n-Lt$}9B$VV?)~$DYA&GfYmdBxrEU|AY^a{P%vixVRAi{lykCSufmFkv{ah zHaG^nt$&K{r~YpbyFp*N6O0SAg7H!~#~RiFpo??btOq0jpF2T)#{fRh_dsc zay)oP^S??4UGQBj=>NmIR1NyIVEhN-uK>#Xqk8;3U;j3a#=p`=E^E~56fz3NT1UZH z>j)U19s%Q!|24sJtpDUb|KW9TOcjn1!?Bb$IA#v)0O&KK@y7?g`H$qbiGP=?YW%;- zK;~QdzmCcMZGQd>Uf?@|J}|}!$AF+60N=r_fd0%c!yo9vrvBI1DP&FgY9N!|YRIIA z5;Ex~k4#*U10?$|H2H%x^m%`k_jk@gpZlW_J_AiZ${%yydQSj#2et)ZtZYt)jRxvJ z99Q@${(bK1@sod32LBU&e^kcpX@AAbxQh%J69(h)z^*lc@z&?(MOMHbLcbjUI(x;e zS${2L)<+$g@l-};+!c@+PzPq*pfQ*KYCk?R>OM{*3WATeFpqmMQvq@Sf;r2RcM!fR^?5Av|yETquuk_;0^{>l$?4H>|(O7@hY` zrdxUFdw&bRJ^x!h*p5H!Q-Sri(E9(E{lA~%zr8$LW%~Bq?d9Ko{OuYvU*BZ2Rd3KT zL5F|7=hiv@r}4+e#@^I_H2$++ZQo|ww`u>;IBt~(mJ!Xz_vJ>XZ}G96|8LiRp9ky6 z)_u_DqtAu$Z_h&GzjZzOoF9kZ;}81GbGmFa|F-}CL;OJ*(DiqU= z(8pV4{dUb(9L?i5xqM59Y5%A32mL=d{=2WiK{4~kZQm9SXt~4kqY0e`<$_Mz;t$O; zy6$|#@*AJu(zdSIk{?>e|9a1@d!e86?YwVtML+vb_`~+!aOI8fFAHMgXXmV6ahxq@i z|A)rqo6Ut~LlYWLbbKpBU-Ku-*{Kq^#9R)KyJ&`^^Ua zSqA7bpu^2_!)I^f_51SvRo=hjWy|htxBpw;KZ12oR?>8MWf27V5g&gmZO4us5Pvur zO4RA9nml_^8|eQjWX@X^nT2Bju>ZS_V4o1iH~WU0$6$=$2l?NpZNKhkc{3jHUf=GA ze#R_F+kQPd9etnearAkUZVJdKm|xioz8Pz_25Z`wicUKTGr|Rvu&Ai0ekLRA5pT`d zO&#z%h0JcsKb2;hf5%rPIWnC&vytONhB45mDkk#RT0&F>%Dz_)p|mXa$^&j?E)m7YIj z;y+Bp9`Vt-J9|kNnS(X}jDgJo9e}>T-=)p|1MEZmfHr-Luj%Mx^!@)hAB;bukI{L5 zCxq8dfidhc4;ADE_-+V((+bvIn~2jEID#Nq(BAz_L>MpqbfW)|0nme4U<=V@f#afo z9D`?|%Ma6jcD((XzrzFiTzKzkz{|L&Dl!1(48yep>TG3~!CEciNSNWz`1o7tii(OT z*p>j^17`sKkpJ0>I>=@_2*-o}V*>JnW2u|;-`wZ#T?@x=;TZm?_bCLv&xG%^YwZ;} zADc+*dw<2?r?NptqEQq6x;MVbK>Iu1eKmjQP*q#f`A`squR z)^dX2UDZ!{`MVh#H_xF)e6$Zv`s)TvUet}A@Yji+@Y4pQ^*^RbKdq=qKkcX~|9?a9 zoWDBnyF7SLc<=vlS$wsk$9y!S2fa>3ce|-YH#@6D)YvO2I*YSmJhN5$sc06i7@=PJ z>z)Ao-j>;FF*(&U=b5Q;%FTXQ> zOW?NzeoNrD1b$24w*-Dm;I{;ROW?NzeoNrD1b$24w*-Dm;I{;ROW?NzeoNrD1b$24 zw*-Dm;I{;ROW?Nz{_m3jc(4C!e;Zpe`s1c!TX9-+LgiL`_rHnnK!tzR0qQK>cKh`XCU3KM zV}CD>Rr%-he;fzXzWm^MU{Y-5X7G>2cYOE!t@t+nH}ki}$6$GX5Z^oxh1!kI-wZ(< zEzj@c=yvG;((tOPvdk`gDmdYFmz=C5STG7s^~FHp!PV^y9MeousMFGNl42Te1M_8u znKn(1G#@mt7xj+J%zQR7uDeP;%E#*yZ%@FeIUDKwRgjF zg3MuRA4!r+(&TC4nu+bpxG_c&iK~|nBYJMSB+0ks&2rhacY5aGU;Xl5X5aNk|i=fe19OW{P-P}R`+Ugt0>g+=`--ibaMF3XcztB=|#69pbr z@Ud`VQY}ocx%$qlAVEm|sF4EhMv?E#pxa8B@n`24+lrLRx+z2XrCoaV?T>QoODmSt za*yu$(ySmW8#u^e92jxUvHZg|yiljlU5A{@*(w&C%aJ>ezf>0ZOwDXG-mAWcobvUG zST2ge{M5L6BmdnRQh#s#5~=M5rQay?!TUu$-V??f>AP*r+4!Oi4pDd+pG}M}Bns}; ztb3rHv9h=xBjBOVsayZjE1=fXT!_X+cq7GqzVp_mAt0em=34b3T^ep6vKmDG=rmF z*D0^d^sM@ZEDh7H4(#@RJ%e2Ho!}LEXoG)1i>ccbpe!9?#5xdrGJH}75u0t> zkXk3Zp`t@nSh*~xHGa&Oc95r&R;!PM!VrURmFYaWwyk}vz#Y;jQ+$29^aM^idWJ82 zvG{yuJ*SDoainQalBrnNTvdtP zDBYc$Pok}wpDBC}y%VWUQmjtubWVANgAsqWYij-?P8G+i`YPHiLxJwuaG8jjPTOv(;BH!6X)Zwm2g__PYqUwY@0;BymSMehK7dfhldRdE7sniG?{Yr znG<3dhG+rCKOsp&$J~Zc@$kZ zuZcsPw^5U(yEAu!Rv5|KkD?Dq;HZ@$(7U@pK=Wwc@Fwn!4!lc9%7uOJogW$swM7j* z6x;XUIW|p$OU{wyn^Cz0OdA6-XRkI)=QDA<#A{wUhI}&FZz7s;$Zu_J4K-%}8m#I>mC%Z~`1g0L$T{H;Mo6=BdxU-}XJi)Pku0!#KHi?I+HZKiM14738 z+;Egw)b_TuVS38dV9KPSGNg5v-`w=Hw20TI?0_QWn^*8Q8bm((;+1&fc2e;lJ$m}; zLi`Nlnaaz3_cA&=1t*t}vQmdqy4c2Av@&3I3`pye%v`vdnK`2?8H|*OVH1nraF5?z zSK=CUE_+WG^9PA^4f5yeP9Y@`cQfyowH4^{ds@t<#_9Xes@~bynBt6LPOS3mS72Qi zOV4>Tdb^29Uh~bFB$14Sn)}iFHn6BP-->u$K-u%rTt7jhtl4QI)T?dbi?dVSMd5|c zu^`I^LrS9u(}lzpG`v>`v}q2s;b-PwJJomq>=oG@p>=5g?Bfs=sngjI>M5@r(n4uM zI#l%ZM3BTjF*`kDCY01J6+(>%JT|wTZodema!QocZo7)_ThP{h*>=9w9x;CyWP9)Jm9nyQlUt>^_7@iJNl7&ZaRht*`Az-4^mWri!9#ts{M@LL1z@akR}+ z{!tB<^dWkWMC;thjy)?zVRU*|1qw~OS@X+K28%SQ*K?PzBxfeRBCkv(_EYTA7;E5a zqa|8C;KY7|K0#|;CW`2E@y<8)ep95MHdI#+fVGBn_$Zw_PHuS56{Xq|^6#i2T0=zN zSytXU##(Yt^W81!-n9C8Uiuo0$(s%+wYZ(td12ib4F?1^+HEElGIiKgA61!ud6>V? zb>O`Fjhn}*956082fx+STFg3rpZOz+<%tYbykf>dlxBo`JeD65%|hzM8=dXaPZoDZ zzjwg3!X&7?_C~&^5T8jxk8z|X>gl^$=aWg8g%f2p#6D&{<7Ii+J=51++bGsTr0f!V zl7dsyvmUUT?>O#XWp3~ow~mRQCGNIcX)G#$GU-DR6~#>yu?s88GIntJY1Q+j)`eG- z1C=yQqbx#ZV5QgjZP?*w3EyagvZweUnO%2x{_0JbAS;)o=>1EHwKfB znkYI|?(N&Rn0|6D@p_l_YxIfJE9w3B;X97LOjy%xYaKlmhoAhG<3d!i^g*`^=?!b7 zkpdN-R_Z=Y%jUXr_N(QdchyDmlb6e>39)0;kBYqX@rZ~0rfL$-DTJRhob$@={Y$P4Cl2~z}ttR)mO(!9}hgfU9*=SOLD@1L=Rw;f_V*>7SPn{hHeMkWg1 zlPoJ0r9mE4B}Noe;bpp3{TG&ex{z4TpqXUX(mdfr7i8Eyd&J(kJ@IMtWAd`-3_y@kJ%=`KWJX zNujJZ2quF*nmvg;6vsl`cezW=S7dNxq+&luxyS`0Gq#}6&vrchDHxUa-+1J4c|2Vv zB%*Q0sV3RwS$DSPi{Ld}I))3TDI3K1%U>U~?;J>4ICrS^jGND#|Cd<$(!va{zBegJ z`>@%(wdTU^iHKk5rJpiRBHjDCte-BS)N|$05vFs-=O2=;hD8#dyU6>9Wv8;P)M(}d zzxy^k1VhS_98nWdLBRFrQC;WPB_71P>JZE|(Xa4N*(u!*j%I|qy&gguF zM|Y&2(Jwe_EaCou74l@)X}03^KHiH#B?{EJan);|y+T<_J02cNXJQFd|d`AXO4YSWV<8j$=8XI^Js zT6@EN(pE4?Unr7KA?ZSYIsvgsK>A(^(FEr8jOB%)wJB=4r!TEzmG?I8KhX2GfSh%I zKFes0VT~BC?*bmSk=|!JGMN0tyUv;T=HnBw zHq;BRvijO)CKX@`CwKEEuavzY4Obos`Y4}fW}tpbcY@AD@T`Onq0f1`k&R)Quq$g|8$(Tw4$Q!)Lz_u?$4*H9}-O)ckeLw95K1Ma6H+w;c4xx1=;oY=_WgC zK1DLoKY8jes>{P>)$t17FXQFO6R&G5HjrC*3r8Dm`^^@F$rCJSVnldDqIb*h>rY#b zG^%6k|FnE}im^e^X(&i=!55u!ZrkoC)?s;m( zRDRmO*zElE-Q6z3*SvGPEGyvuB49(0>Xm^Qh}eFio6wqQ+79b2#r-`=eHz#w%%InlZ(rz zdCIx?IJX#cHcQHGJXXY{>v*ewF3FxRln#567=jXp7pj7kCf06V6c%+Gs59YM@;@v{ z#+5T@@YrE;aH&_XQ?eVciZDE1J(%jszvw5rNYQh|W%m`Sk^!qB`|%UPoe5y}o{uM&Z?imksYSPU#%O@CwZvq2_&u zA#S{pX7g$11?+l}wxv?nRj+;Po+07t*Ll46xJ=s`?mqpo-%lCiT;MzF%&GJ{PG#fm zgcuc#--?vj77Mq(8IwBf6rT10BBAqnIfr*RNlwb)i#9G+ z-6tsR>X4b}x*u(oe>{`Qx<5`}EO4gh{_zr}{d-4Z^Ama4lXea9Tr#S7!INUwts)i6 zBo{32Usq5at)+?T;+i@ia`us8z`^d|XcyGAW5x0oMjRTAoxLe_R$WDwdV>x+)+}MY z7j^V>SkimxOf7Z`hTgMQa@UIN9Oxsz@E~5@^uy=lY>}$3=SF&1PfnkSBW0bzE#_pVYAI5)lgW)N>n&#Pf^Ivp z(xCzmZZ38%yhGhX&3h~sNvc%7(3F4L*+le&ts!F%lfeU;Qo-h_^^EuU!Y(%(-V17x z7EaM6`_5)x8S3XI3J4Vt*_W3}Ov`8&bFZ(QK(&|lgQNXLJ50}V<5kZ)JDO~)nid8F z;~CU5yB4toe4PWBRPvN`*I2Yth60|_7jRo}kOC%>eA=lnRtbHrC!1j!(gh5s?SB*VdPRmmX8&Q{f<{& z3lVDiJfFgeDg2*#tlu~8{#4a=&Zn=g^TA0o`kHj#C5^m$w@-7*|Uw_<%r9!(Ow~a(>A86x5JqXWp1y^J1!GN+_NrsV(C9t zo>U;o7}I&Ep8jQ}dwRPLL3fB~e*kA|u3}W%lWK_*7)mN#d>#W??AA8hAXPdp!HTu>hl?S$5j>jzy>W zr`>$3&K2gDmY(nKAsc&%XU_G)>zT&0p$Q5MCe^39cP^r)_wFBac`NyDd0yXZ!E@B} z?rC$$5E&GP1;xQE`2=fe(j~$D6TQzc(ml`LwxcMY38>-5(6^=V5FcO3zRD1lm}>Y+ z(BTfjy!zn9bNR*1E;G~1{5q;;`W$}l@%(lM`c(xf_mT$ZlCc8{+v0Pk*{-ubZlSC; z)_aRpsO7atMR=?PyD#rS(>=_}_ioi@_q>Ss!bPO>asM8EO^Ljgy-7+I`wq^>HCa3{HZx&y3)%Rf$pBS6^na z-tK$0u@t{PF?y}Zw|UKIgO$60BH{FD39Qz}NiDBe>{Q zSv(N>tnkT!e|KBY##svM_b06fDRB;*s_e}oDyGU3CXBu$(jWnA& z>$*%@D~W7>To>0k=pm381c@hE3Rw=nyJaB8EjGTC^{VV*ijMNNKJFD!wx0AMR>$d7 zQVZ(m>+iD!g!;aWk-p2Amx#NUUKX=gM+t*WtYN?G?E2b5_3nj=(7^&@n+aqee)mF^ zdBZKDHfpWWYqXZ5XW~7X6Euc}ibdD?X=;)jqk2n@VxGoSyC~-6H;UOy`*Fs3PE27H zbxsM}W&MlLyU>z0B#dUEd_P7t<%9{(j^n#;wE51@_>OlyMy4w;_`j6N#|bz1$7qj( zQ7_SZZ6_>c!bV~*U}QS76>A#yqioh(@7q|O#k?on!TNEA?yaQFDXl}3BnLxX3q4md zGQ|Bg4gDtGr_p1_&G6S!&a_pm7dWiEEy3$LK21r|Wc&mhC-efL0IxGAS5oNBy!Ot| z_^1lPofUMPEeFp;66c&^@EFL9N5yiB&R$nAK0x3i$&JsYoX(L}8%zA0R|3x%BZfWf z!^@*2_SF&RD{?JZQWmDdR^OI9JjtL=0`efO{z;q%(y=8Q=5yNj#F-L<63r+^ap4m`h~*_gjd-s_*_ZlWpLmZoJ#q4-!TngJ*ZoY- zI=L6-Qf=c(pAC>o%5~U?zTYVEeT{sXs@Ry>VA(l-;^thOY~iUfnu8?F*9JewMciQs z>RNezK`}u9<9z~0xqt2T=Pj`Sh|!G+-%raEkC6)rqGy@P+Kwm~o<6Xu+319mKtx-T zDROfs>5+gr+%v@;_HHK*5eyufK;etdtnwi$BI|4${nsm&Ly>v=wf7I+yR96lbxWe! zjcV?62pOPQm8&Zmy4Ue(WNN;XgQs?FmDl_Ay-(ivyb#`vqr~bj>5`rFW2otbZZNR;`4JQ01q~+SJ#QgAO+;Vq z9#7ju)6(R8c1Q%=bvN^#K;wMC*v-dlmPT#7+$wzd_VqeDLU_z{$3rvpNS zLT*uZG1iA(B2HP-mJ;n#z{Bg>C3$35*F1?R*-aT&pTlCIvIj(-s?8j8JwI_%?P-Bx zXk@TKi3ZcrD2suFcfR&sOl+^a$COLG*YbAr+6iY#A7eQ`UL;tFzc*kJ(VsT9r67^z zI~`-2NS~fwdrSf}fisxrtVkZj7A!ftUFuen&_vyuKv7NT&Ysh^NT3Q9Zy|XTM^-b}AAhoMYy zxDIqj-pJ3n#QNi5?^(WAoi>h~0E zPMKa$i8^(Yj-EO}b={7(pY9OwG*M|{mF&aS_Jx<^eMUA5>G5= zjQsVXL-gUv4w!zrl4lzk?~MSq;&&I&CVjf2=JYC4<&KZ4E7@8Pfvy8qW660kI?T9K z#?=@?za&B7YxQJsJQ8OYcRyyDL=Z81+leGUkCDe()ko=`>Yr%KGZ?hf>tYE|TxyXq zK1pLM5_quX9AQZ7QO_q8!#mjL1kLV$POwfcB}}@DL$8R^K$=ywjcgflMME@D0takO zqHRehh!Z$d>T0f}k&y29vg;UK8m~CxXh$+lgpn*lcUGV@j{XxWxuR;^{&D)!d$$mj zePgHlHBuAWfGLxtrPV@IC6`iSB@xDRu3;~}As=c`9~wSO(52%sn^p%TwMX&$mF-Um zh~(RsHlj8_M}5%mQ!+j38fN1{Lb}t4^oBJzXS#vVg8RI2)S&G{L*;g_rRgsB;#E9` z+iKh;hODx&%&v`Zqu5@yW@Fm#i{aN{nQA4bQ(d3B=lSJeR{Wzx@J3j^Q0*p`IPvUZ z)x$Um4uO(VgcFpN(dXV}@Kv>>YmSPvs@_SsMSi^%FQ~w2d2K;*aw9sQQ}`@{(r462 zxJ?(ndi3-0Wm|#L9(!4QQ976M0J3YBI8B~7ygBolVo6XEKYF^2kpC2N9d8$g5ckXG zl_*Xe7TlaivPGNcrJV+fVh>w_5N;P1C3oNr@RmC-Hjr zjlVMuQ(KUlG?EPCsP*LX_o@{`)eE(`)E&Ti%P=&g!b@8-_wGoQuYb68(Lr6-z4qft z0!Etj1aj?%OB8S6s7PY$0ubLDSoNkZb^f%dzTtGuSqLB`5q6&8oL0D#xWz!;wG3r# zL0!IxKGUP}{JDPl=W?~g73xab@O|$of|2?!XQ_yIna6J=Si4}HrHZ*0lb0r}n8rV$ z&X8gmc_!!OTF^c!#%g!*8#l;qv__}$l2nqko9wI{CjhTd4+p3U)N?5ntaP;uMjzXi zb0e`>W+%?M_>+khWD2J;eY)`i`mq%l>eLUHppK7i99@*JwEA@1)dzJYXz=>|0R6+B zG^Chmdj=NMM5W9Kun075y

67CT-C+XJPV2!Q2ZcJN}m*QfgNHJA+?l{cBX3{E$ zA!%XRb=ddAB~eD&$Rx3=?wyo-8R$e4Zf!_nJ*Q_u;ka(>Ya3i`vQi*A(uxWT8Z4-r zdna8r&a?9t{$X>&CoIgwufjWmgi@{I@1BVi;Lo`sW=cnMs;)0O$t;NazB{L?MJL0enPhSQHd7zSP(|@qx-B6?;m3~h}VfyzZI?S zAxXTLob=qN2X)6*1AB=}>Al-sFk;|DaAtfD*JTg0oxD!bw%h@O&DR4^7tP;(p))e% z46tdyR@S5!kl>~}{peYy>;c0B(P+02`%r-;0=wb~iq9$=`{}4hP@H01j87wE11Ha; z91Ud5ZN&18&t4~9Ez}Z6-EWXhb-@)sA1Ns-j`4cY)`eoso^Q-eC_NwnkKUri@0K*i zRSZ(m3Ev{^6DR6VH8-fvm+UvZ8FPo^X@0YRj4I~&odODlSJQhIeQ)9?lop%f5Lb;W z5q4z-&fWOpOCFG;dgWF^9O2Cm!HIPPHEo2%aeGe8Ov;m2-{jkSv%A%U=>bdB(y(!e&kk^O4=l^)P?xuByI~nsRHObYSr{xQ-7EE7$QhkWBviqd&;nYuZ%?5;e?kS}H$7bp2 z{j`-<7AmlI?jhxs!>G0n&!tS@q&q>C)i@f+k6P7#f5~Z(uGP<6tGTF*IGOvC2N=4N zO|lYwb@diUN%CyYosx&Ct|FycrRU7+Llo`B7?k8Ysj>$IdRY7y*A z!6gBrEzNP|H42e+5y#oqP`TnHi{S(~uVxiV-{m}v7NsG!`m}hfKHzS#AE)0p!sSjjTs0=)>n6SsU^CtBXd>^Pa3zjK~QLuQ}lA_lIChoR)da?RI{FID*C<3`Am zZtcyuk3&SbCsfDEJ93UL&#<{|nTDmKhuANi%+U1A+=o2YBnG;vutbYnclR|WJlxys zUx54MBU$`{`bAlnwfmH$Bs9vu8)N%I@&L0CCUWD@uX|JU! z#OOIb7CMws(sRG3nYTq*ZSM2@py~V0r)pNaw93iq((4>m8F0)EE{>i~nebQ7UgNyM zal%|zd;UwMrU|O_RS2Jx^nFt<9F;Jg*_@5STY;H#HJ7chtP0f97qY5%oT#28Ms?ZS z7Y^y0glgT0mXRK7bYg9;{Y-kQ;9ShTTNaXfgPp;sUHBnX3U%{SyY@9b;y5?xjDc$> zWq<4Ru9*U-;m`-ptfMK5&vrUk@GRh47a~Dimn1cJPVS?PBTLaEsn4(DXxt$;D=4up zu&;?(RxiYHEoLdE`ZKRn%9|APP95BrQ#Y*+6dUOoOZ9(FV%iyLvY$}rvQ`^5S4En{ zfZtBnjU%?N=tFr+#T1O%qOE!r|Mad@3k|bmRME}y-_rLJ3F$68^1JiRFz6(u+WB)_m(H0AqOt8e&1uqTD{Qq z{wHH$#CB$HM6>UmIkt1|)o_#fIC1_f50k@rlR1 zjd^;X)a$1S$5PvA@`e+%0@nskSKLswU^^#G(3$HIb!3+d6Q&Wz^TpPLiO)FE?4Vyn2sYcAwNlt>o@C{UBpp*)`9VC+#>_d9T9W#?X zi9=PFgq``tkv!ed(jIHIu;y?dwHqW8&3D3Vdeg}mOdjNy+4fba`%j)VVc*-+nRoht zXk6*59ULo+b9*E1CiUZAwxQtai!?e*Ud)VX?sMMLjNw@-yI4kaKW3A5u1@Kqq0)l_ zEydx{K&Bo|W(ir82R3zz>Q;#flF{dnCE0Rmt7E1 zXN5YM#A|}&1PrrMBUwT^&HK7s7ch?T?!U-+bMKU>T*DgMu$a6h?#U*ce9^tR1sk!X zZ*r0xH|VPII}+=VrOyMBx_`A1beiySFCNiT$lu&a`^7#zvH_ZDYcn zyv5kTiwse0?(Q6ZVx%~`o!qeuwd5==UAkM}xuWk9SB|TOr)=kam`>LJmQ7|+cPkY( zX%r@#%!xL_9g&KH8|Uz+Fbtdsi|)l{`IBxz+-|7!__=h z*|ii`M3n2}hTVH4Fly*cr0%L{AVTY{)9DPDpDx~gnrbMJEoPeSXoVH<+Hxji1|yuv zI78!-e#^=1+LO9j`K39m?{nY9XZlX4uuTr8l#XL5&M*|}4h|Q|1rx};UKH>RTrw=J zz8-BIYt|-yBwKM`jCqd1FjiQ<-vjKUp~%O+j;aC10-T~EwniLtv4@dmTE0$>1m_E$ zSPA3r4rw@qnz;Q8?^7Q0nq)AC0E1S#bSa(i&3NaD<|O*(9!xrg#jAThHfA}_U87Tv zeQ0xx5xPL0M}k$%{W$!YS1 z^-MbZnPh72sI#Zs=eZAN8d$#!f5Lu*DErKd4~pH(hcp=BymRNptDh4Mtc(Nz^qwE;?bk!jzNbFNiN zQf^j4sdxkNJ6{l|hHC2K*gVT(EXl;5aSOS3iHk*XY_P#R#-jj>dx_xTEM+G4V>&nnXQY)m#ASa)@WTcjE&hYX&W2>N9Pf;3~nhwVe30{Sy z{dGL&Gu*SX)FkDw9fN~E<>lT?XZ@0$xJOUE6X%@nKAlk~8Pox%JSnR$mkBuUrm&qq z8?iP(r%m&ov)1zN3+XzeTK0sGY$%fW@+(-`%QHu4=3v>3SRdqR?+cK=JYBTr!|Ye+|Rd3t!SYjyS;2^ zn>`$hi)9s?#a5#&jWT#LeJ$3qd-6dFomSnzhuGtXnD*23E;bq3L|Q(Q+rx94@VSu7 zIPsto6RjaRGvWGG?Y*Xjl=6(GkF|EbNM@qMHocWQtHbalu2jx}wWt-xe9yBRhL2N2 zIs}9djgae4QsL9#U<&%%Mm*e`brEwfAHKQ8+K2b#*Y4^>7rp;Y&(02OP>IvUlHyrYmI(HG;$RXw{#`(7k-h{m&a^B~E=df{26qg`%!1u0dtioU1S+G|`4 zrb@F+#dRZHolC9rA67l4ce#WZ4@y1NO|lJ7)-bR-jM92Xi9swvkf9P~*wz=Z zR(8YrkE+JU`^O7B+C_a%s7v%6#WsD+?x*fmZE(%8y1u++$5ERjEKQ3pj93oXx3QS3 z#{yC2z1?SP!#yv`pm+*ZN6$>4P^6vFftMr0vrnmNWPL~wClGo~jW_DbLN%YhJ4osu>}o40REOPM@28ogbHkx2 z2lo+GNNBhF6^H#nxuu7r59B5gitbN6WQ4c&hUfZy-&;}&)w7qS#PXYR%@TyCO(UFI zMlUYk{t!Y)po?5k#@)@;*G=g^$HMj;XUyoHlfhAxRCfBllk<08%HV|9;dGW;&l@r` zn5o-@=Oz!QzaD$Ts%fN$n^zpVH|B$oM)AerBO8Y0Z(04uO&P@_uBk`aOk94Gk{e80 zCml;EIrXmon(?Dha{p3dY?PJel{@FWrK@7FXi7$HaPQI;bDr_;zv-JVhELk=<&fxV zryJ5TY>_$k{-)GJdve<2!u=9Tmkf;`lcw2PoiWjWbTW>yB5vdOgEu5k1=h^#n+tRA z5>6UiqVPvaPShh8^lYkAZl?^KVSD=}>5Rz4(yI&FIINKck9MGJKh!1!^X=&MnSAP` zQzP8;OkP>f9plQ}>Xq|Xp0dQ8ee__}DHrc3<=EB9bwBC>Cllmj!Ewu$$f6vAc1klW zy=wWM8vLt>R-kUN3`x)zAfi%Umd@vQO!k(A^7~ZM^E^j zW}5_M66d2<7Uwf!j>x9vwydE#gGGikOF3`c9V^>w=+3EXnvfr63y`m<{><8?G&IG1 zrI5HIb)}!#LNSB!s(*MjZTBPH{_|RDuDN%w)s|+zt0=av4KA~NDazDzR;}4ch9};& zD0%`rKG-}fJS@_>plaZCM^7I%m-s6I^NcsLZ{1w!v@-=y-(Ts!2(lLK-_B;=Owx_u?Kgdwvv~ZRqixZm+3Oo31A#GrTJu_;f~6C?lp1tSq?m z*%$a;dpxYPUVq&u=6H?3D(!uoa>MDmhzbU!k>Fd$q#;c?`)(qlVy)G?Wc~!gVGT?&Y5Nvk_wDrcaYlDW8w;NcgOD zJ~JkdYUc-8&%KsQKB2)6n~$*665RZJ60_j$4%H}ZE+MASws%PGhTYPo(h$Ca`a*?P z@)vxMNpqkOf0ZC)JL>9|LVdEvh?$9h9)nEM1!&0fO zF`MgY{pk%oZNmIJC(dEUDx6-=w7C_zKB?r_;J0#NXjotS-En5gD1%SWu3RN44$~eq z9f)XH;od{S?vUw~DK~Z0q~QDxEWh<};wV#h%E`RUH`oTQW^~8pLuzvkajATd^(d;) zI6Z$u-z5=5Re=iDrN=MJ=%2{0asGr}HjL(!!Jpj6sZnPi&wXjJGi|#U zUYUB8-j~1HC-I})@eJI4(=-R(=CZ45<;`d{QfsqVTYg+V@Uus4bQ zbM>)7ejV~MKW~iB6hY)Xov9u#_foXuO6jUjKV5t8V>Q2cBL7(TOK)$B$RcL{=i-bZ z3Q~>(x!0c9V_th^5)!?9M%4U$zxxvg+T53wS^Hy1uDx$Mr>Jl%4ufjvBE^#o(@1|~ z|3|U-gf94@Wk)@IqE{Bu)4h@(MT}Mru(qe*f7owmEq#crJXr{pPEn*>JIclGtV&Md zm)~}gspM2Vw$%8tyPM7b)7e?DMZvUfcz0px?w0QEl~xnBp(TBc=`Uq_Zw#BK4z{sN6#AI#%Sk^rh#}Ixw`H|1uU>$&k6l(1LRY4CLEmf z1g^)D6RyAigaQ6{+ffF0sDTF>ZX~{zUjY>`UD)_KjQ=hFS|iizsQly`p{UI6|Az#w zM?x^tYm5KhVoB;MwyY#&OY5EC_Qc=h;jhF`+(<5`MVaw3L0uk@N^HCCHY~G{kN+8C zpOhQ?i!=54(6M#hujgjvLqBo=uNCd995|`{-0F0<0|$E43|&sh$~~ML6l>SY>L@ho zVc1zS&1rPB7t`lwJ5hbMMLbk96l`sL_U*1f^tq~bIMTXm9(tes)6f%W9tHYued4ZV zn3CB8xh~)D$d|iu5PEL>CNO(%jlerjF|l{+7E)I^urMT#N1&Io5oB`H6*c#FUf2of4rFAM+%LgqN|YuA|di^GE4#VW_vR06nB#8cAVN*+66O zFEOPTgkIV>MMRi2(kmv|00xeB?nF_h7<8?zcbBOxER%bLp1GQ|u)pOlrspL1pat2g^A>Kd8E=_L6tNv&r}q+>$(J*QrJdUmX2OnihX-GCEG_n zdXsY=Is`6iq=LqHd$I}}(VCmGCx7R+IhM1(S3{cbEt^2jSbH9yXwYF{AxYm|z&uIA z2O3U#=Tj82-L`fkKJ=65H-tP|d*StqvL1clpiE8gRs{t1fFWUjrY;S$HzMjr)OLYI z)3{>mQo7GgR?c!5u9H&_bhsh|O)wo-E8Xy-XXf=p@MXYLq?ZKG5h+8jC`*D7P8?a$ zz;3tWR8L*R)9p@N({|qGd7)%aghrQwZfYw@aTC)}fogwwuoOkLS`%3v7J}0#)$gB! z4=NhgzDlq{*H2@N%1wY(!ca176IXUnfw=c1R6~oj%9EgaG-WaTh`gqbiq6ONL9H=IsV_f-w|x? zd0xaPlT0Q%5690sL=TOkl-lguM1##se% zV}Am>9D}WivKNu;=gg&WaJC_pgi?fqWh5G3=d|LM)p_nkwmKxmY!JrUGmX1nX8T!sJmRfcI{WpSJ> z8N~CgrT|CdsUEKtnopmI!a|qyb&vsQrmio&q>A+bx2`7BoxTbE@G$)j6tK^2zBsp6 z%>zI;pM;-a$3R(-H9wJLrN)$w@wjS+5^zdS!zYroLHVf*zi!(FXU=!h$#$K!J($6m z>I6w8PR={Qhg3p;lKC~ah7Q?!YH|PA&^^_OKOZ-_Hnz4JAt5Xr?M9^Yt5b-z10sFn6 z(CRfOqy1rN$pm17wvX%H*U{K$pweex3CdJbN~qv}^WAgQ!royBOfK?5oKy2A0l}xf66HvF6EECU^*PH#4Cu>-fS+}-rOcn{`+V? zJ!<{*dh?s~Cyys#`Unc7n&m|32h?%++s7Nma*YDXO(GT8=6I|QHzsbuDX=C0;gpM% z@MN=JkmAUbFDKimYfX?S)w~9WN^o|LgBshUdRQbAZoQO29_CSy$2)k9c$+E7cjcfF zEN_#s18`T*^{A@k0Y82T|3F zksR2uv#Q7+!#+W3B~BLIbo{{}^4d7NdQsA`DTq+RGWa*a9NHC~gHa?B zTTsyORB>5=FvN%XtTE8(S2;3)7CLao&Yqh&4Hg*B#2a{N(2{%|IsBWsp^ zQIfir%j-wv#0Q-A3iYv!XTLKg611tPMs^j%Z?Mh_{cSck%a_36JE}QqNbEcAz0sq^ zB>!`6;DaL2cx!P<6B$%iOulttuFRav=Lx_s-3qEX2w-uZv9no^pq31+Vh-2cYF02< z{ERdyCzN3HmvR}F8!(3Orz!v-DCpE`oUOm~s3nU_5T;H|GNm=e5{l&7c$SxcufL4I zjl#0>a~7RM=2`WJ>?ugAd&qK=D~kalERZSxr}UsiyBxWqc4BMUK8=$^btR2F-ixt) zcI>kA%0J=v(AQk)ehHk2C>2S&Z!cV%jO}aQBj}P8vqGa>yHoF zOFVj%gmW48;YI>#Nxg{$DeZQhXEXU}X-#k;5zODK1pu^v*vm*a*e3+V(suqpZ>GFh zFPyms_FM;*MT+8Jcf$&Ta_O7L+L1OK8TAjiU5*=xz{NlPzJ{@dlKSbM`1%2JWa*kW zVo(p{+_;Mt+v?w+*_ZsLirB|pjnN!@ybcX?td1mijh_7`p8^@H#*O9D@Qi9Q0;g-5!;Vq)K_UdSRxD7UV-+!}}^7dko_) zJgGiJ;fQ|-1t%i@)wS@`hT6IqV4khM#mlwWy35n}sPh(8X zJDjLL-G(v-E>j_@*(fAoK*nhomj1|gT07qRW+5bLij?<{HO>Ng0?63rpX}>j5aKjejv#te5&oe+#SU7-A--<9dD=hNg zE8f~Rz+%1Qn!>9ui-N`|Q>oD=X{EfrwyyWgVJ19ETMEJQoo|9+Mc8C-T(rwa|KR0O zhHA!63l7UX1}+N%Dcn*xaFqlws+xKaouR;xc$Z+2GFMnr9ktB)O#(it3azXIP_<6v ze&>J>G`XNVoad?{98_dbPEU+FQzzlK@rz-f{M2tpcIU#jcNO+8VuEJj&>ofbJV+`3 zVcz_uIAQ5sq9ghqS&0;@>1L+C2`#sv|I-c#CYz}r>f=1CD|UuLox(HZaW18gNWe1o zC34sK&tq(S@mWaZ9ZC95103D~NYe!!pL6zyHdCgUSqRwTBMjtjK00hD0@B~iaJw9w zdAY)9S)C+0lSK3XR_Fh#K=`4Ch1F~2RMG1(B5WsbWB|{tfS?-YlJjUl{kmQ|g^6M) zCOa{`d%)6*(kcqTIDmrijoy6l;kohBshGNR+}XKLN;}g2(yvXeEh+R5BmW@80V79Y z_Xmjr@FIJa7cbphY31C`4y*7dylJhSn2PLsgoCa6tssC*CDJDm5yS7V5LaoO2JLFA zElf}T&vqZ;Yl(6-5Bqgd_Mb}F!g5}#s(|4#;`9bMzE2@j0^zv(cs9XfhuD# z*eXpM39XfUm;13q)=tLDF&{}PwJn;h41jd}rz#r}G6}|MyBs;Ran?OHy9=Pb+`)Df z=*6t<^+s;ncvg+9d_TlTrEWP8pfuZ(ZQ5MQr4kA5F>N**x;8y9sIt0cYgZMRP#st+ z+#=On$Lqtk`2f=K8SZFNO6F=ahlq z$jCA9>VM2I0X5QkA^ypi`05N|s|5qI<=JGdt{H0uB*BY`bPRDB`4L+g_%}an%-c_u zvJ4Y3Wq#Qc9;w}zZuw(z4hEKpzHLDHyqGWeVXPf4vAPEf|MJiHV=y_2CXG3ij=gAf`jA%$3i7zyL{4 zf4fnX?rY8vm=-Ae90nnR7K~IGiin-1XA|labo&u?&<(g#Y?znet4EUtVZT@2hoKWi z9pj~MZ`C2(^>Cj3mg}!Yf1d`Kuo?5#mC;WO@o;e7QH+QSc&!%Dj|eNO9}{hs^1xvDsZBFI$a3@jQa(+@^ zs#qlL;ud$+bmJK2!ulw3=8@4nz5irf7iZ=#gNw*!%VHAPx(T_!syi)QK!H1IpyhoL z8sYaa0tnQ-|Dn@V`LpJ_TnmC=an|uX5Pe*&2^U#UP~FAaRIRufv> z)egC8*3x)Ebk}bNhyB~p4>>heH}{xNdY!-1Hi7|&ofIGk$@MT@^8`#1=iHvUpTZ~` z9_+DSko7ivEX~%iJ6@J8N>dERP|*tu6#msXs4n$%Gr`vC#_#H;Ts{U(K0}T>b)6#P za8iDedplT;`P^Y#$IQn~Ns$-rDfpMRv^drZFt-&8DoN_hyZ+ST>{R;BV-6EbuU{e# z1E?W(@m9&dh@gx~v!97Ck6fQy+!rPowbJRgSx=HtgE#rR!j>=EN$o$eTN|>SFwD`! z#KOR9X!&OS53Rb<4XeLaIk9$WdLYA2EGdwRt7c;!%08bGL2Dn5&dV4n<58Jf+hQI! zJL8wpdb-VwG4-&4YiWU^X;2@0s;%NDigYcy;we-*I=AWrv7V*5@MPrj{Z`G7Gv4## zp*klKtDi<`9aR;jNH=8TmlW?y9TKq3bt$A;*fHDOKAQRwz&>k_p!*H>liNM&cxPN3 zAl+?+`d9pwSMoq*x-XQ{u{ivo3F6Msq=CL~NZPfE;8&`E=3{2~%TTt{0QOaEd)APf zhif0KD<9C1Xf}QV>JdAxp+^tW{b+A0ld_SXcy}SpNE`LHzpuYK;;^r;`>*xSWgWwzKJV`Ph7?Hqlm?TH!DobN7CBZg`rd0 z0D=5k{IcU-5J`u;-0)t9cFHd@w6g>Qa>UahFQvp8f|2WXl#q53y4I+ap{$?BaG}0% z?76yC+LdffC9=3ee9zR3-N2scqC0$@e2a;{nXddp!_|0h>3ZXuEk819_@^_x;nKus zCjLlM<}oiK0)W+Zy9M?xQT+o}{j#dqMGWKfE>~C^zxFqD{=5r~J$Fna*^ma;CaY>w zS3wJFlB;twW7-pS5LmMu} zj7AsjNgQQL{$_Hdd?9X~j@|IWpkDe8m68k_P;x4Hf&S|1`K$oyl%m^FKZHmGgeT!9UF6m=eI-<1|E%QqRky=>gJMJtR>5}u5|f}>ir#M@(k=2l6q z9qoZJ5ISQHR@`vr68P{J5sfmn36?TmfnK!NHr^O&6uV10i{sY`ae6 z=w|b<{6Iack6(>~*O!IQk2gI~AZtN3@2=ZT+Nq^MRn zl6q{=PgnLAHKgx=;Zq5dh)i5J=<-=!JUp2b${(MWbkiVuwl0^9V>;RF>JyK@e95H4 z1S>a+_I@g<^QE2qgtjBs=l5`-dsCUU3MUBg%Kj0?&gV{12(EGasHQ6L$M+lJV%4r` z=mfo)oz5*vKb!Rn!EbnV+jFFgT8yHDah~d7R%uS%>F4{Br5Dr!xw>uPbf##}h2>&4 zBp}bSgI0k?@HlY-i8tuy`Y`wE%HiVlrHOW1@3zz71ziuf%5)8S|0ylwxDq1&q-A4mN8Vo*2GU3Ny?y>uBhHvpFHC1 zF5F_21{Krvgk5)SAV$m-0z}XszdxUZw!!C|e3t(7r|GxyaxYKWqTX$2F6wRhy<2sv z`@%oMLHJ*!?il)5yT;nF%_b?D=n{~oDmx#~qC4`kOV!FTx9?1&WYr#l$Q)Rs>96Mdw?BY*gUV0~k`$W^srS7#nsR|cqB(yv^v#daaIWYlYD7&GU*|tlSilgjpq0HNYi7pQ*RXwij&lp#jH{L}mwkKrdB~B<2?tZZ|n#u9k=TeCe zGtVM3r79gf8{{>`k2F_dEHE0kQ(iTd!jaM_3sxdDQEV8H#QPl-q}!8MQ6><3^-8Ed zM$xG(l!Yz4l$5CI9S+#Imk1A;VRA}tH>uw3&z)IpBi?0iYz}6XIiL<8G z{3mP6i2S`A=mK4TOOJCvD3l6-xVew6Q-1-2xHNz(qOY~LeW7*5S&NRNaSW3?pNQFP zKHn2yQ&(;k^#%!6$ui0E8qF3A=8kY9?Rm#BClC(&Qm&Zm-FK{SA6)Tb$6`#D zG5mU4;rBl$IG`0|5?Nj03E6W2+IKRTv47U8qpX&J<4kdVkkRr?er+-Tp;2`RYfbN8Dwgv zVjiJ84gR|4%MumX)32G}RU%pto{U?;0!}EVH8ll`ku-I2o}}xJA!z%=pA8~0*cj{} z0Zp<=Z(RtD&44z*H2-emqc|b=zOZbecXXrz>TJ6TErn7F=Mu;q(Cx5Eh=0rZ5gGG4O@q}WJjMr0>=WX&ZLPGdOwAchpaOa-(lUp zd;fU7WxwGD^&&)pB)^gkizhpCA9`X2GWX;x{}xq`RBU%=?;S|5hUdU zhaN$QDv2A9i%9*Hf7+08+&K{u)b6Y|SQu4@5jj`N!UR`#1Nn^W3jv6ZAn4sOb`S#O zwQ%S2pa05u3S~;2BltDuF9^pZU6N5k2ZjPw;YsW{U!>Wc&W%{r0b@RZG80sx{N3Nh z7o($)$Im{w-^?ZlQ09xQjQvCD+eO%P@KwTR=7eQ$P~!Qv`yM%i{~o?V?o0a~UlHrx z=e1S}%E()=q>S3A`xdLi1vd<3UX4pFJVN7tX5co1Fq3YRAc@N#0A9ir9^zE+>b`UG zh1=K9J27unw>np@I=bgE+?8LQL`)$T`CB<)H)6N2t-4e9Y*kNmJ4i7rv7d>8UM62}ukX2NHn+DYo}Ztq z;}8~Y2UvI^V7rXb?OgDnn(4o#1HyQW$?>gc&nf(RaRwu2{&4g+kB9veMo<4Il9Qr zWAaC-0ZIN}9EC87MD2Lt?t2n<$p;@0j(HC{0Cb?jYdYJH=rHM8y9So3S*`&$Zy@Fe z;j4?eoGs)jIG(Z1A9KQ@5cM&6W(xH??v=*-Tt|6d>lEqKdAlnxusKCm&9`vQac-k< z1>X@&79vY{vMq?*hB#i5o;0^Xjma&eeg+XQ2?J4h6awOx`*fylE0L>i-0RpUc`A|l z_ExSMbZZ6BYlcC_o8@HfXPp4|o!+$lzsx)%1%HM0gW)spRR61|C6C-E^+uQ_4u3zY zE8$r?>T^{7hiGfC+CSs(ZmAPwEIii8ws=w**ixvlgd!)Oqrdkz7=$Ay=jQJ2>F4Js z|KgtNRYvvtPlS;pNkYc-{UbIBQ!{^nV=;hw$c{EC+YE%D z0qrYtWT|G&&2!aQCFJJ#{+anhy{sjH%;~)5ce3*>QkpoyuP-3uoM)=G-tK z_sP%;)9Ym*hoGav$@9dVl61{R&TW2re2ageQu|*%#|~@6OZ{{{i+1hJ*VQVt-5+1D zGs;*1NhF6_qRuBr{5AT7(w6_mCh!!O23I|u;nmPhh0*-*WK7K_qD97%JH9SX4UyMPM-y^T&U(0`{cG-hy@|7wSAH z08|N_h}DDGfus&BSPHt1J};8zp*+n z0_TmBA>ulvTBHB{%nbg80!N~KN(I-QqQ;t5p^CJKd-RS>guJ?x9==2WMIQq=?}Vq zH)7Sle?&Bh@jMFDa{Qw^-pf)eqYdL7<` zpOs{ zoZE-0cwn~|JKyTH=~o*(iJAWdt~G=u);4^wI1S0aFvLdXxD&_aEZ#ttQKa*=;qn?< z>#n@=>TWN|8K@y~sXfQnR0tZC`e2Jx5TJJmb%G@i*d~2HE8x_8=gJ)IkOU|>LyWi$ z=l;XR?q#m@2u@jwMZCNX52lwc!8KB`6rZyf))_(I6QBViL&TFi{OP>K#ep3lC)jKmGHGH2P#ojxm!W`Be3Xj}n z;n#2PPYK*PEAQ^I;KI>)cV=L98a&{U_^kid!ILL6g!Hyr)|^)hNnMdYpiwna*9{oM zY_bw?W|m#oNF4==Vx135r{g;)*D}`l*&Rk@^+;3SGP6!P>_5Q`8C*CQ$eFdPTNr>I z(WU(8*y;aSf5@f(G8r=>YZOvOfyEpD_o$jy%pH6zQc?EIW<_n+D)D^$)934d60HDO z8U_t46{!ZEu;kzh)}kFDqnC50=z)7e6|lkxN20ru+|ql89;%5SOG^TBM1Dh-XK6cA z>B27pkDoF$(~MMMMb&$|##{~Xd~HTMFQB<)xjk!OB8?dK&on5gTgn@C@kVuu>kHG*4>k`mPz z%SRlHr&qj>I5ku#rndhw0ym-qm&gcIZo=OYUr5;b0(0z0_c#42etdHgV`;U6P;y1Po7xZ8Wf<8dJU+ z$eiZA#f270D+T!kMzN0q7?-7xn%pqkyjc5>Rl}Cr>ni*6Xqc$aW%$IOHW@-2zcRLl z_RRDcgw#rEQ2LM(`pOQgb`=V&3LiDde0?#^(B?;i7vNXW_+>DV0;9&N)e*_C_Nt&m zYIfAnmmHLAvB2*w7RovpCCqZDUn@EK>NGoeYvTbWhH=SgL0N?+#ke35zcblcO!i}~ z#sjLFV)RKHyl;<^M{8Tz{G;#aB%9x0ci#FCI?T7s(*O(1wm;Vw&D<-ZIv?lwF5l{? zBDT$APkWis1Pi3&B^PbSXL?g$VHWaxD%+Br^~f}m3%bX}GPyI7($ImyWb)9*3NQ$P_0;*+wP-?-iyVmqfW?5;2l3hNC%0Jk)Ez3gD5_8>>#;O+N}qh^ z%S;=~rg~{BN-5=g@Q?dT2G{;u%sr55LvP4q=y}mYz6-ETYT3BdH+l|(&-Fjl4nvkt zYn2aM^P&ehBDfbSb)$H#J-a8kF=#fjq|JNr0iknBT9#(?A6uuTbr+l11Hk{?v2cku zPO(dP));XXqi`}8Lq2NFY~HT=_cP3j+}2?SY=c+yXgD{*IAo`h#gA0g8fllplF(k~ zeVxR~Jbu}jQI2t&&@Po^XbY}Pr}vf8)E^5$Tt9lm4|ghU?bo3h`=~}hrMa;J<1(VC z%q7tbAWBc&qfd1G`&AEBk5TZ`6zzdl@ru zo2a`6dKh8U(A3{U@09HYN*8k72nf&dcY8%`!(6ZY4pbm*pV3UlTWN7O_&>OlI#X!d z;iq;q*V+@3m-mQA=AgmZ^jVrnFz*`1yy;NIv%t+4$#WS*(awvh$CYXp3hpRa3G|D4 z&+1;e>moVrn1^>}e0>Z(STzJU=3|8h=8N;TRx1SZ_pjavv{t6l^UEUhMd97Bz=^*E zxBzX5RJ+s_N-022VMW0nST&F&jpo|g6f@vcJsD2-5P@Iz(S~^B;_u?<9ltgSNSC_-^RU-^q8JcDS`|WA)u&sfpP+QIJ(}8` za&O|f{AX7^zr$!6RmED_GoiE&y%=Y=9^tNUxn_Tf0Dz+pRfw^5cr{Q2q}HUsQ|{m6 zJ;@7x>)(Yv8wv}tBj>bdVt0Bf2dA${TT3iMJY@W!<3GyWeCnidjC4LT9_ewYLJ^$i z+6cTOTbhX#W8QdEPlMBEXPVOcn!txorB}rMoDDxVdi})8`P-Hw+J4cNG00 zZ+Mi~&A)EOwV&U5qF2vd7Y^dViULofsjlT!!#|zAOR2zU{HK#`IPWd8!h0E;FZEn% z#rKmNuaD0E9)<>oBm#ZwMQwg%1{duxmk*3k5LKWHgGyvA2}5ro+P$w6f6tr!4o&VS z)kDs>o|J>G{GKfxhC)ysV8@!EhhmrpG?4S^>$iUs^yl7kcD)6;<46ELxnQih5bUJa z)4N{M1Vq4rN>KmB)cx4v^_{rc2Li1qHZnGBjGa3)1gha<4w(I49) zP|nCAv2r!j^JAVKp`D)GkyHQq>F$@<>%ZBT?~;Gt4d0)?;Z^7fZ)r^&F&}|vl};aA9$#oT2S=Kn^nt%Dtaw>(_U3H)2iNL zcbyGD_>~lIn{p@QFyv)8|9&ClcQfRE4FdK+-iX*kk7DOK^0(v;FpNs3#!OKL>U%jaa>{NxhI*i0ih#^ zN>!VH29br_vf+|M-1=J0T--WJ<7lh28`7(&aCQDIHFu}hufI=sp?J5IN|8RK z&*ug7=y&qUQ1%wPFPWv)8*`qVrRA-3k~%|3Q$wbu9JodkwG>v;sS~Ba?x_|)Q-(XOx&zI3AhS4FBu-JW|cuXuD=@V_rrKG7hN4Iy7V(X zH&r1mT}ew&N#EHNQ^&W8i{iQTK5;LXBY++F@jy>V#E&*j6Ke`Y;VFa)J%WXBDeIbs7W z=@0lf^ICP3m8*@kb34;#HXk|rwnWzylsYp{+kax2B(6S|bFoPH*>8%y`Z``*|3CTr dT;VSvQsJ+khJJnWdHYxZpe(N;S0`f;_CKOr-{t@S literal 0 HcmV?d00001 diff --git a/dist/web/source/index.html b/dist/web/source/index.html new file mode 100644 index 000000000..d33a2459c --- /dev/null +++ b/dist/web/source/index.html @@ -0,0 +1,73 @@ + + + + + + + + + ImHex - Hex Editor + + + + + + + + + + + + + + + + + + + + + + + + + + + + ImHex Web + + + + +

ImHex is loading...

+ + + + + + \ No newline at end of file diff --git a/dist/web/source/style.css b/dist/web/source/style.css new file mode 100644 index 000000000..4b9c77497 --- /dev/null +++ b/dist/web/source/style.css @@ -0,0 +1,28 @@ +html, body { + height: 100%; + margin: 0px; + user-select: none; +} + +body { + display: flex; + align-items: center; + background-color: #121212; + overflow: hidden; +} + +.emscripten { + padding-right: 0; + margin-left: auto; + margin-right: auto; + display: block; + border: 0px none; +} + +#loading_text { + color: #F0F0F0; + font-size: 30px; + font-family: monospace; + width: 100%; + text-align: center; +} \ No newline at end of file diff --git a/dist/web/source/wasm-config.js b/dist/web/source/wasm-config.js new file mode 100644 index 000000000..45995aa59 --- /dev/null +++ b/dist/web/source/wasm-config.js @@ -0,0 +1,68 @@ +function glfwSetCursorCustom(wnd, shape) { + let body = document.getElementsByTagName("body")[0] + switch (shape) { + case 0x00036001: // GLFW_ARROW_CURSOR + body.style.cursor = "default"; + break; + case 0x00036002: // GLFW_IBEAM_CURSOR + body.style.cursor = "text"; + break; + case 0x00036003: // GLFW_CROSSHAIR_CURSOR + body.style.cursor = "crosshair"; + break; + case 0x00036004: // GLFW_HAND_CURSOR + body.style.cursor = "pointer"; + break; + case 0x00036005: // GLFW_HRESIZE_CURSOR + body.style.cursor = "ew-resize"; + break; + case 0x00036006: // GLFW_VRESIZE_CURSOR + body.style.cursor = "ns-resize"; + break; + default: + body.style.cursor = "default"; + break; + } + +} + +function glfwCreateStandardCursorCustom(shape) { + return shape +} + +var Module = { + preRun: [], + postRun: [], + onRuntimeInitialized: function() { + // Triggered when the wasm module is loaded and ready to use. + document.getElementById("loading_text").style.display = "none" + document.getElementById("canvas").style.display = "initial" + }, + print: (function() { })(), + printErr: function(text) { }, + canvas: (function() { + let canvas = document.getElementById('canvas'); + // As a default initial behavior, pop up an alert when webgl context is lost. To make your + // application robust, you may want to override this behavior before shipping! + // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 + canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); + + return canvas; + })(), + setStatus: function(text) { }, + totalDependencies: 0, + monitorRunDependencies: function(left) { }, + instantiateWasm: function(imports, successCallback) { + imports.env.glfwSetCursor = glfwSetCursorCustom + imports.env.glfwCreateStandardCursor = glfwCreateStandardCursorCustom + instantiateAsync(wasmBinary, wasmBinaryFile, imports, (result) => successCallback(result.instance, result.module)); + } +}; + + +window.addEventListener('resize', js_resizeCanvas, false); +function js_resizeCanvas() { + let canvas = document.getElementById('canvas'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} \ No newline at end of file diff --git a/lib/external/fmt b/lib/external/fmt index f5e54359d..f91828936 160000 --- a/lib/external/fmt +++ b/lib/external/fmt @@ -1 +1 @@ -Subproject commit f5e54359df4c26b6230fc61d38aa294581393084 +Subproject commit f9182893631e4a93e8a2d947b726f95a5367fce9 diff --git a/lib/external/imgui/CMakeLists.txt b/lib/external/imgui/CMakeLists.txt index f81b03ccc..6502bdccc 100644 --- a/lib/external/imgui/CMakeLists.txt +++ b/lib/external/imgui/CMakeLists.txt @@ -32,9 +32,8 @@ add_library(imgui OBJECT ) target_compile_definitions(imgui PUBLIC IMGUI_IMPL_OPENGL_LOADER_GLAD) -target_compile_options(imgui PRIVATE -Wno-stringop-overflow) - target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG="imgui_config.h") +target_compile_options(imgui PRIVATE -Wno-unknown-warning-option) target_include_directories(imgui PUBLIC include ${FREETYPE_INCLUDE_DIRS} ${GLFW_INCLUDE_DIRS} ${OpenGL_INCLUDE_DIRS}) target_link_directories(imgui PUBLIC ${GLFW_LIBRARY_DIRS} ${OpenGL_LIBRARY_DIRS}) diff --git a/lib/external/imgui/source/imgui_impl_glfw.cpp b/lib/external/imgui/source/imgui_impl_glfw.cpp index 4be45aa8d..e2728c7a1 100644 --- a/lib/external/imgui/source/imgui_impl_glfw.cpp +++ b/lib/external/imgui/source/imgui_impl_glfw.cpp @@ -126,6 +126,8 @@ #define GLFW_HAS_GETKEYNAME (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwGetKeyName() #define GLFW_HAS_GETERROR (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetError() +#undef GLFW_HAS_VULKAN + // GLFW data enum GlfwClientApi { diff --git a/lib/external/libromfs b/lib/external/libromfs index 31b331c02..03365d8c5 160000 --- a/lib/external/libromfs +++ b/lib/external/libromfs @@ -1 +1 @@ -Subproject commit 31b331c02af7464e436f38e7aed27646e097b1bd +Subproject commit 03365d8c5a43baa22cc6bbd5725db15a3038d035 diff --git a/lib/external/libwolv b/lib/external/libwolv index 094d87ca3..f5f081c28 160000 --- a/lib/external/libwolv +++ b/lib/external/libwolv @@ -1 +1 @@ -Subproject commit 094d87ca30fb3f03485fac1c8c11f53baad52210 +Subproject commit f5f081c28efb9c06e14fa3b4a6d85866d75f7350 diff --git a/lib/external/pattern_language b/lib/external/pattern_language index ff92cf631..a3574fe6c 160000 --- a/lib/external/pattern_language +++ b/lib/external/pattern_language @@ -1 +1 @@ -Subproject commit ff92cf631a7483faee461947538479919e736114 +Subproject commit a3574fe6c2dcca40cac5ac61dc95a063b6799249 diff --git a/lib/external/yara/CMakeLists.txt b/lib/external/yara/CMakeLists.txt index 11b19423b..1a61f9622 100644 --- a/lib/external/yara/CMakeLists.txt +++ b/lib/external/yara/CMakeLists.txt @@ -116,11 +116,13 @@ target_compile_definitions(libyara PRIVATE if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(libyara PRIVATE -Wno-shift-count-overflow -Wno-stringop-overflow) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(libyara PRIVATE -Wno-pointer-sign -Wno-tautological-constant-out-of-range-compare) endif () target_include_directories( libyara - PUBLIC $ $ $ + PUBLIC $ $ PRIVATE ${LIBYARA_SOURCE_PATH} ${MBEDTLS_INCLUDE_DIR} ) diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 117239a36..49592b2c7 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -27,6 +27,8 @@ set(LIBIMHEX_SOURCES source/helpers/magic.cpp source/helpers/crypto.cpp source/helpers/http_requests.cpp + source/helpers/http_requests_native.cpp + source/helpers/http_requests_emscripten.cpp source/helpers/opengl.cpp source/helpers/patches.cpp source/helpers/encoding_file.cpp @@ -58,7 +60,12 @@ endif () add_compile_definitions(IMHEX_PROJECT_NAME="${PROJECT_NAME}") -add_library(libimhex SHARED ${LIBIMHEX_SOURCES}) +if (IMHEX_STATIC_LINK_PLUGINS) + add_library(libimhex STATIC ${LIBIMHEX_SOURCES}) +else() + add_library(libimhex SHARED ${LIBIMHEX_SOURCES}) +endif() + set_target_properties(libimhex PROPERTIES POSITION_INDEPENDENT_CODE ON) setupCompilerFlags(libimhex) @@ -68,6 +75,16 @@ generate_export_header(libimhex) target_include_directories(libimhex PUBLIC include ${XDGPP_INCLUDE_DIRS} ${MBEDTLS_INCLUDE_DIR} ${CAPSTONE_INCLUDE_DIRS} ${MAGIC_INCLUDE_DIRS} ${LLVM_INCLUDE_DIRS} ${FMT_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} ${YARA_INCLUDE_DIRS} ${LIBBACKTRACE_INCLUDE_DIRS}) target_link_directories(libimhex PUBLIC ${MBEDTLS_LIBRARY_DIR} ${CAPSTONE_LIBRARY_DIRS} ${MAGIC_LIBRARY_DIRS}) +if (EMSCRIPTEN) + find_path(JOSUTTIS_JTHREAD_INCLUDE_DIRS "condition_variable_any2.hpp") + target_include_directories(libimhex PRIVATE ${JOSUTTIS_JTHREAD_INCLUDE_DIRS}) +else() + # curl is only used in non-emscripten builds + target_include_directories(libimhex PUBLIC ${CURL_INCLUDE_DIRS}) + target_link_libraries(libimhex PUBLIC ${LIBCURL_LIBRARIES}) +endif() + + if (WIN32) set_target_properties(libimhex PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE) target_link_options(libimhex PRIVATE -Wl,--export-all-symbols) @@ -77,7 +94,7 @@ elseif (APPLE) endif () target_link_libraries(libimhex PRIVATE ${FMT_LIBRARIES}) -target_link_libraries(libimhex PUBLIC dl imgui ${NFD_LIBRARIES} magic ${CAPSTONE_LIBRARIES} LLVMDemangle microtar ${NLOHMANN_JSON_LIBRARIES} ${YARA_LIBRARIES} ${LIBCURL_LIBRARIES} ${MBEDTLS_LIBRARIES} ${LIBBACKTRACE_LIBRARIES} plcli libpl libpl-gen ${MINIAUDIO_LIBRARIES} libwolv-utils libwolv-io libwolv-hash libwolv-net libwolv-containers) +target_link_libraries(libimhex PUBLIC dl imgui ${NFD_LIBRARIES} magic ${CAPSTONE_LIBRARIES} LLVMDemangle microtar ${NLOHMANN_JSON_LIBRARIES} ${YARA_LIBRARIES} ${MBEDTLS_LIBRARIES} ${LIBBACKTRACE_LIBRARIES} plcli libpl libpl-gen ${MINIAUDIO_LIBRARIES} libwolv-utils libwolv-io libwolv-hash libwolv-net libwolv-containers) set_property(TARGET libimhex PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE) diff --git a/lib/libimhex/include/hex/api/content_registry.hpp b/lib/libimhex/include/hex/api/content_registry.hpp index c5b06c965..b676f82bc 100644 --- a/lib/libimhex/include/hex/api/content_registry.hpp +++ b/lib/libimhex/include/hex/api/content_registry.hpp @@ -16,6 +16,10 @@ #include #include +#if defined(OS_WEB) +#include +#endif + #include using ImGuiDataType = int; diff --git a/lib/libimhex/include/hex/api/plugin_manager.hpp b/lib/libimhex/include/hex/api/plugin_manager.hpp index e15f1adf5..f3c75b265 100644 --- a/lib/libimhex/include/hex/api/plugin_manager.hpp +++ b/lib/libimhex/include/hex/api/plugin_manager.hpp @@ -24,9 +24,31 @@ namespace hex { std::function&)> callback; }; + struct PluginFunctions { + using InitializePluginFunc = void (*)(); + using GetPluginNameFunc = const char *(*)(); + using GetPluginAuthorFunc = const char *(*)(); + using GetPluginDescriptionFunc = const char *(*)(); + using GetCompatibleVersionFunc = const char *(*)(); + using SetImGuiContextFunc = void (*)(ImGuiContext *); + using IsBuiltinPluginFunc = bool (*)(); + using GetSubCommandsFunc = void* (*)(); + + InitializePluginFunc initializePluginFunction = nullptr; + GetPluginNameFunc getPluginNameFunction = nullptr; + GetPluginAuthorFunc getPluginAuthorFunction = nullptr; + GetPluginDescriptionFunc getPluginDescriptionFunction = nullptr; + GetCompatibleVersionFunc getCompatibleVersionFunction = nullptr; + SetImGuiContextFunc setImGuiContextFunction = nullptr; + IsBuiltinPluginFunc isBuiltinPluginFunction = nullptr; + GetSubCommandsFunc getSubCommandsFunction = nullptr; + }; + class Plugin { public: explicit Plugin(const std::fs::path &path); + explicit Plugin(PluginFunctions functions); + Plugin(const Plugin &) = delete; Plugin(Plugin &&other) noexcept; ~Plugin(); @@ -46,15 +68,6 @@ namespace hex { [[nodiscard]] std::span getSubCommands() const; private: - using InitializePluginFunc = void (*)(); - using GetPluginNameFunc = const char *(*)(); - using GetPluginAuthorFunc = const char *(*)(); - using GetPluginDescriptionFunc = const char *(*)(); - using GetCompatibleVersionFunc = const char *(*)(); - using SetImGuiContextFunc = void (*)(ImGuiContext *); - using IsBuiltinPluginFunc = bool (*)(); - using GetSubCommandsFunc = void* (*)(); - #if defined(OS_WINDOWS) HMODULE m_handle = nullptr; #else @@ -64,14 +77,7 @@ namespace hex { mutable bool m_initialized = false; - InitializePluginFunc m_initializePluginFunction = nullptr; - GetPluginNameFunc m_getPluginNameFunction = nullptr; - GetPluginAuthorFunc m_getPluginAuthorFunction = nullptr; - GetPluginDescriptionFunc m_getPluginDescriptionFunction = nullptr; - GetCompatibleVersionFunc m_getCompatibleVersionFunction = nullptr; - SetImGuiContextFunc m_setImGuiContextFunction = nullptr; - IsBuiltinPluginFunc m_isBuiltinPluginFunction = nullptr; - GetSubCommandsFunc m_getSubCommandsFunction = nullptr; + PluginFunctions m_functions = {}; template [[nodiscard]] auto getPluginFunction(const std::string &symbol) { @@ -90,7 +96,10 @@ namespace hex { static void unload(); static void reload(); - static const std::vector &getPlugins(); + static void addPlugin(PluginFunctions functions); + + static std::vector &getPlugins(); + static std::vector &getPluginPaths(); }; } \ No newline at end of file diff --git a/lib/libimhex/include/hex/api/task.hpp b/lib/libimhex/include/hex/api/task.hpp index a5fc2eca5..c3ecd9973 100644 --- a/lib/libimhex/include/hex/api/task.hpp +++ b/lib/libimhex/include/hex/api/task.hpp @@ -12,6 +12,10 @@ #include #include +#if defined(OS_WEB) +#include +#endif + namespace hex { class TaskHolder; diff --git a/lib/libimhex/include/hex/helpers/fs.hpp b/lib/libimhex/include/hex/helpers/fs.hpp index 8a04454ef..070297731 100644 --- a/lib/libimhex/include/hex/helpers/fs.hpp +++ b/lib/libimhex/include/hex/helpers/fs.hpp @@ -8,8 +8,6 @@ #include #include -#include - #include namespace hex::fs { @@ -20,8 +18,15 @@ namespace hex::fs { Folder }; + struct ItemFilter { + // Human-friendly name + std::string name; + // Extensions that constitute this filter + std::string spec; + }; + void setFileBrowserErrorCallback(const std::function &callback); - bool openFileBrowser(DialogMode mode, const std::vector &validExtensions, const std::function &callback, const std::string &defaultPath = {}, bool multiple = false); + bool openFileBrowser(DialogMode mode, const std::vector &validExtensions, const std::function &callback, const std::string &defaultPath = {}, bool multiple = false); void openFileExternal(const std::fs::path &filePath); void openFolderExternal(const std::fs::path &dirPath); @@ -53,7 +58,7 @@ namespace hex::fs { std::vector getDefaultPaths(ImHexPath path, bool listNonExisting = false); - // temporarily expose these for the migration function + // Temporarily expose these for the migration function std::vector getDataPaths(); std::vector appendPath(std::vector paths, const std::fs::path &folder); } \ No newline at end of file diff --git a/lib/libimhex/include/hex/helpers/http_requests.hpp b/lib/libimhex/include/hex/helpers/http_requests.hpp index 69b1a4293..609d17341 100644 --- a/lib/libimhex/include/hex/helpers/http_requests.hpp +++ b/lib/libimhex/include/hex/helpers/http_requests.hpp @@ -7,8 +7,6 @@ #include #include -#include - #include #include @@ -19,6 +17,14 @@ #include +#if defined(OS_WEB) + #include + + using curl_off_t = long; +#else + #include +#endif + namespace hex { class HttpRequest { @@ -67,27 +73,9 @@ namespace hex { HttpRequest(const HttpRequest&) = delete; HttpRequest& operator=(const HttpRequest&) = delete; - HttpRequest(HttpRequest &&other) noexcept { - this->m_curl = other.m_curl; - other.m_curl = nullptr; + HttpRequest(HttpRequest &&other) noexcept; - this->m_method = std::move(other.m_method); - this->m_url = std::move(other.m_url); - this->m_headers = std::move(other.m_headers); - this->m_body = std::move(other.m_body); - } - - HttpRequest& operator=(HttpRequest &&other) noexcept { - this->m_curl = other.m_curl; - other.m_curl = nullptr; - - this->m_method = std::move(other.m_method); - this->m_url = std::move(other.m_url); - this->m_headers = std::move(other.m_headers); - this->m_body = std::move(other.m_body); - - return *this; - } + HttpRequest& operator=(HttpRequest &&other) noexcept; static void setProxy(std::string proxy); @@ -120,173 +108,28 @@ namespace hex { } template - std::future> downloadFile(const std::fs::path &path) { - return std::async(std::launch::async, [this, path] { - std::vector response; + std::future> downloadFile(const std::fs::path &path); - wolv::io::File file(path, wolv::io::File::Mode::Create); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToFile); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &file); - - return this->executeImpl(response); - }); - } - - std::future>> downloadFile() { - return std::async(std::launch::async, [this] { - std::vector response; - - curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &response); - - return this->executeImpl>(response); - }); - } + std::future>> downloadFile(); template - std::future> uploadFile(const std::fs::path &path, const std::string &mimeName = "filename") { - return std::async(std::launch::async, [this, path, mimeName]{ - auto fileName = wolv::util::toUTF8String(path.filename()); - - curl_mime *mime = curl_mime_init(this->m_curl); - curl_mimepart *part = curl_mime_addpart(mime); - - wolv::io::File file(path, wolv::io::File::Mode::Read); - - curl_mime_data_cb(part, file.getSize(), - [](char *buffer, size_t size, size_t nitems, void *arg) -> size_t { - auto file = static_cast(arg); - - return fread(buffer, size, nitems, file); - }, - [](void *arg, curl_off_t offset, int origin) -> int { - auto file = static_cast(arg); - - if (fseek(file, offset, origin) != 0) - return CURL_SEEKFUNC_CANTSEEK; - else - return CURL_SEEKFUNC_OK; - }, - [](void *arg) { - auto file = static_cast(arg); - - fclose(file); - }, - file.getHandle()); - curl_mime_filename(part, fileName.c_str()); - curl_mime_name(part, mimeName.c_str()); - - curl_easy_setopt(this->m_curl, CURLOPT_MIMEPOST, mime); - - std::vector responseData; - curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); - - return this->executeImpl(responseData); - }); - } - template - std::future> uploadFile(std::vector data, const std::string &mimeName = "filename", const std::fs::path &fileName = "data.bin") { - return std::async(std::launch::async, [this, data = std::move(data), mimeName, fileName]{ - curl_mime *mime = curl_mime_init(this->m_curl); - curl_mimepart *part = curl_mime_addpart(mime); - - curl_mime_data(part, reinterpret_cast(data.data()), data.size()); - auto fileNameStr = wolv::util::toUTF8String(fileName.filename()); - curl_mime_filename(part, fileNameStr.c_str()); - curl_mime_name(part, mimeName.c_str()); - - curl_easy_setopt(this->m_curl, CURLOPT_MIMEPOST, mime); - - std::vector responseData; - curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); - - return this->executeImpl(responseData); - }); - } + std::future> uploadFile(const std::fs::path &path, const std::string &mimeName = "filename"); template - std::future> execute() { - return std::async(std::launch::async, [this] { + std::future> uploadFile(std::vector data, const std::string &mimeName = "filename", const std::fs::path &fileName = "data.bin"); - std::vector responseData; - curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); - curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); + template + std::future> execute(); - return this->executeImpl(responseData); - }); - } + std::string urlEncode(const std::string &input); - std::string urlEncode(const std::string &input) { - auto escapedString = curl_easy_escape(this->m_curl, input.c_str(), std::strlen(input.c_str())); - - if (escapedString != nullptr) { - std::string output = escapedString; - curl_free(escapedString); - - return output; - } - - return {}; - } - - std::string urlDecode(const std::string &input) { - auto unescapedString = curl_easy_unescape(this->m_curl, input.c_str(), std::strlen(input.c_str()), nullptr); - - if (unescapedString != nullptr) { - std::string output = unescapedString; - curl_free(unescapedString); - - return output; - } - - return {}; - } + std::string urlDecode(const std::string &input); protected: void setDefaultConfig(); template - Result executeImpl(std::vector &data) { - curl_easy_setopt(this->m_curl, CURLOPT_URL, this->m_url.c_str()); - curl_easy_setopt(this->m_curl, CURLOPT_CUSTOMREQUEST, this->m_method.c_str()); - - setDefaultConfig(); - - if (!this->m_body.empty()) { - curl_easy_setopt(this->m_curl, CURLOPT_POSTFIELDS, this->m_body.c_str()); - } - - curl_slist *headers = nullptr; - headers = curl_slist_append(headers, "Cache-Control: no-cache"); - ON_SCOPE_EXIT { curl_slist_free_all(headers); }; - - for (auto &[key, value] : this->m_headers) { - std::string header = hex::format("{}: {}", key, value); - headers = curl_slist_append(headers, header.c_str()); - } - curl_easy_setopt(this->m_curl, CURLOPT_HTTPHEADER, headers); - - { - std::scoped_lock lock(this->m_transmissionMutex); - - auto result = curl_easy_perform(this->m_curl); - if (result != CURLE_OK){ - char *url = nullptr; - curl_easy_getinfo(this->m_curl, CURLINFO_EFFECTIVE_URL, &url); - log::error("Http request '{0} {1}' failed with error {2}: '{3}'", this->m_method, url, u32(result), curl_easy_strerror(result)); - checkProxyErrors(); - - return { }; - } - } - - long statusCode = 0; - curl_easy_getinfo(this->m_curl, CURLINFO_RESPONSE_CODE, &statusCode); - - return Result(statusCode, { data.begin(), data.end() }); - } + Result executeImpl(std::vector &data); static size_t writeToVector(void *contents, size_t size, size_t nmemb, void *userdata); static size_t writeToFile(void *contents, size_t size, size_t nmemb, void *userdata); @@ -296,18 +139,29 @@ namespace hex { static void checkProxyErrors(); private: + #if defined(OS_WEB) + emscripten_fetch_attr_t m_attr; + #else CURL *m_curl; + #endif std::mutex m_transmissionMutex; std::string m_method; std::string m_url; std::string m_body; + std::promise> m_promise; std::map m_headers; u32 m_timeout = 1000; std::atomic m_progress = 0.0F; std::atomic m_canceled = false; }; +} -} \ No newline at end of file + +#if defined(OS_WEB) +#include +#else +#include +#endif diff --git a/lib/libimhex/include/hex/helpers/http_requests_emscripten.hpp b/lib/libimhex/include/hex/helpers/http_requests_emscripten.hpp new file mode 100644 index 000000000..f5a094ed9 --- /dev/null +++ b/lib/libimhex/include/hex/helpers/http_requests_emscripten.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include + +namespace hex { + template + std::future> HttpRequest::downloadFile(const std::fs::path &path) { + return std::async(std::launch::async, [this, path] { + std::vector response; + + // Execute the request + auto result = this->executeImpl(response); + + // Write the result to the file + wolv::io::File file(path, wolv::io::File::Mode::Create); + file.writeBuffer(reinterpret_cast(result.getData().data()), result.getData().size()); + + return result; + }); + } + + template + std::future> HttpRequest::uploadFile(const std::fs::path &path, const std::string &mimeName) { + hex::unused(path, mimeName); + throw std::logic_error("Not implemented"); + } + + template + std::future> HttpRequest::uploadFile(std::vector data, const std::string &mimeName, const std::fs::path &fileName) { + hex::unused(data, mimeName, fileName); + throw std::logic_error("Not implemented"); + } + + template + std::future> HttpRequest::execute() { + return std::async(std::launch::async, [this] { + std::vector responseData; + + return this->executeImpl(responseData); + }); + } + + template + HttpRequest::Result HttpRequest::executeImpl(std::vector &data) { + strcpy(this->m_attr.requestMethod, this->m_method.c_str()); + this->m_attr.attributes = EMSCRIPTEN_FETCH_SYNCHRONOUS | EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; + + if (!this->m_body.empty()) { + this->m_attr.requestData = this->m_body.c_str(); + this->m_attr.requestDataSize = this->m_body.size(); + } + + std::vector headers; + for (auto it = this->m_headers.begin(); it != this->m_headers.end(); it++) { + headers.push_back(it->first.c_str()); + headers.push_back(it->second.c_str()); + } + headers.push_back(nullptr); + this->m_attr.requestHeaders = headers.data(); + + // Send request + emscripten_fetch_t* fetch = emscripten_fetch(&this->m_attr, this->m_url.c_str()); + + data.resize(fetch->numBytes); + std::copy(fetch->data, fetch->data + fetch->numBytes, data.begin()); + + return Result(fetch->status, { data.begin(), data.end() }); + } + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/helpers/http_requests_native.hpp b/lib/libimhex/include/hex/helpers/http_requests_native.hpp new file mode 100644 index 000000000..7b5226940 --- /dev/null +++ b/lib/libimhex/include/hex/helpers/http_requests_native.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include + +#include + +namespace hex { + + template + std::future> HttpRequest::downloadFile(const std::fs::path &path) { + return std::async(std::launch::async, [this, path] { + std::vector response; + + wolv::io::File file(path, wolv::io::File::Mode::Create); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToFile); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &file); + + return this->executeImpl(response); + }); + } + + template + std::future> HttpRequest::uploadFile(const std::fs::path &path, const std::string &mimeName) { + return std::async(std::launch::async, [this, path, mimeName]{ + auto fileName = wolv::util::toUTF8String(path.filename()); + + curl_mime *mime = curl_mime_init(this->m_curl); + curl_mimepart *part = curl_mime_addpart(mime); + + wolv::io::File file(path, wolv::io::File::Mode::Read); + + curl_mime_data_cb(part, file.getSize(), + [](char *buffer, size_t size, size_t nitems, void *arg) -> size_t { + auto file = static_cast(arg); + + return fread(buffer, size, nitems, file); + }, + [](void *arg, curl_off_t offset, int origin) -> int { + auto file = static_cast(arg); + + if (fseek(file, offset, origin) != 0) + return CURL_SEEKFUNC_CANTSEEK; + else + return CURL_SEEKFUNC_OK; + }, + [](void *arg) { + auto file = static_cast(arg); + + fclose(file); + }, + file.getHandle()); + curl_mime_filename(part, fileName.c_str()); + curl_mime_name(part, mimeName.c_str()); + + curl_easy_setopt(this->m_curl, CURLOPT_MIMEPOST, mime); + + std::vector responseData; + curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); + + return this->executeImpl(responseData); + }); + } + + template + std::future> HttpRequest::uploadFile(std::vector data, const std::string &mimeName, const std::fs::path &fileName) { + return std::async(std::launch::async, [this, data = std::move(data), mimeName, fileName]{ + curl_mime *mime = curl_mime_init(this->m_curl); + curl_mimepart *part = curl_mime_addpart(mime); + + curl_mime_data(part, reinterpret_cast(data.data()), data.size()); + auto fileNameStr = wolv::util::toUTF8String(fileName.filename()); + curl_mime_filename(part, fileNameStr.c_str()); + curl_mime_name(part, mimeName.c_str()); + + curl_easy_setopt(this->m_curl, CURLOPT_MIMEPOST, mime); + + std::vector responseData; + curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); + + return this->executeImpl(responseData); + }); + } + + template + std::future> HttpRequest::execute() { + return std::async(std::launch::async, [this] { + + std::vector responseData; + curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &responseData); + + return this->executeImpl(responseData); + }); + } + + template + HttpRequest::Result HttpRequest::executeImpl(std::vector &data) { + curl_easy_setopt(this->m_curl, CURLOPT_URL, this->m_url.c_str()); + curl_easy_setopt(this->m_curl, CURLOPT_CUSTOMREQUEST, this->m_method.c_str()); + + setDefaultConfig(); + + if (!this->m_body.empty()) { + curl_easy_setopt(this->m_curl, CURLOPT_POSTFIELDS, this->m_body.c_str()); + } + + curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Cache-Control: no-cache"); + ON_SCOPE_EXIT { curl_slist_free_all(headers); }; + + for (auto &[key, value] : this->m_headers) { + std::string header = hex::format("{}: {}", key, value); + headers = curl_slist_append(headers, header.c_str()); + } + curl_easy_setopt(this->m_curl, CURLOPT_HTTPHEADER, headers); + + { + std::scoped_lock lock(this->m_transmissionMutex); + + auto result = curl_easy_perform(this->m_curl); + if (result != CURLE_OK){ + char *url = nullptr; + curl_easy_getinfo(this->m_curl, CURLINFO_EFFECTIVE_URL, &url); + log::error("Http request '{0} {1}' failed with error {2}: '{3}'", this->m_method, url, u32(result), curl_easy_strerror(result)); + checkProxyErrors(); + + return { }; + } + } + + long statusCode = 0; + curl_easy_getinfo(this->m_curl, CURLINFO_RESPONSE_CODE, &statusCode); + + return Result(statusCode, { data.begin(), data.end() }); + } + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/helpers/opengl.hpp b/lib/libimhex/include/hex/helpers/opengl.hpp index 1f8eb8c85..f0423041b 100644 --- a/lib/libimhex/include/hex/helpers/opengl.hpp +++ b/lib/libimhex/include/hex/helpers/opengl.hpp @@ -9,7 +9,14 @@ #include #include -#include +#if defined(OS_WEB) + #define GLFW_INCLUDE_ES3 + #include +#else + #include +#endif + +#include namespace hex::gl { diff --git a/lib/libimhex/include/hex/helpers/tar.hpp b/lib/libimhex/include/hex/helpers/tar.hpp index fee117da0..531c12901 100644 --- a/lib/libimhex/include/hex/helpers/tar.hpp +++ b/lib/libimhex/include/hex/helpers/tar.hpp @@ -52,7 +52,7 @@ namespace hex { bool m_valid = false; - // these will be updated when the constructor is called + // These will be updated when the constructor is called int m_tarOpenErrno = MTAR_ESUCCESS; int m_fileOpenErrno = 0; }; diff --git a/lib/libimhex/include/hex/helpers/types.hpp b/lib/libimhex/include/hex/helpers/types.hpp index 0df6343f2..983650941 100644 --- a/lib/libimhex/include/hex/helpers/types.hpp +++ b/lib/libimhex/include/hex/helpers/types.hpp @@ -21,7 +21,7 @@ namespace hex { struct Region { u64 address; - size_t size; + u64 size; [[nodiscard]] constexpr bool isWithin(const Region &other) const { if (*this == Invalid() || other == Invalid()) diff --git a/lib/libimhex/include/hex/plugin.hpp b/lib/libimhex/include/hex/plugin.hpp index 346498d1a..ac0c265a0 100644 --- a/lib/libimhex/include/hex/plugin.hpp +++ b/lib/libimhex/include/hex/plugin.hpp @@ -9,6 +9,13 @@ #include #include +#include + +#if defined (IMHEX_STATIC_LINK_PLUGINS) + #define IMHEX_PLUGIN_VISIBILITY_PREFIX static +#else + #define IMHEX_PLUGIN_VISIBILITY_PREFIX extern "C" [[gnu::visibility("default")]] +#endif /** * This macro is used to define all the required entry points for a plugin. @@ -16,16 +23,29 @@ */ #define IMHEX_PLUGIN_SETUP(name, author, description) IMHEX_PLUGIN_SETUP_IMPL(name, author, description) -#define IMHEX_PLUGIN_SETUP_IMPL(name, author, description) \ - extern "C" [[gnu::visibility("default")]] const char *getPluginName() { return name; } \ - extern "C" [[gnu::visibility("default")]] const char *getPluginAuthor() { return author; } \ - extern "C" [[gnu::visibility("default")]] const char *getPluginDescription() { return description; } \ - extern "C" [[gnu::visibility("default")]] const char *getCompatibleVersion() { return IMHEX_VERSION; } \ - extern "C" [[gnu::visibility("default")]] void setImGuiContext(ImGuiContext *ctx) { \ - ImGui::SetCurrentContext(ctx); \ - GImGui = ctx; \ - } \ - extern "C" [[gnu::visibility("default")]] void initializePlugin() +#define IMHEX_PLUGIN_SETUP_IMPL(name, author, description) \ + IMHEX_PLUGIN_VISIBILITY_PREFIX const char *getPluginName() { return name; } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX const char *getPluginAuthor() { return author; } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX const char *getPluginDescription() { return description; } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX const char *getCompatibleVersion() { return IMHEX_VERSION; } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX void setImGuiContext(ImGuiContext *ctx) { \ + ImGui::SetCurrentContext(ctx); \ + GImGui = ctx; \ + } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX void initializePlugin(); \ + extern "C" [[gnu::visibility("default")]] void WOLV_TOKEN_CONCAT(forceLinkPlugin_, IMHEX_PLUGIN_NAME)() { \ + hex::PluginManager::addPlugin(hex::PluginFunctions { \ + initializePlugin, \ + getPluginName, \ + getPluginAuthor, \ + getPluginDescription, \ + getCompatibleVersion, \ + setImGuiContext, \ + nullptr, \ + nullptr \ + }); \ + } \ + IMHEX_PLUGIN_VISIBILITY_PREFIX void initializePlugin() /** * This macro is used to define subcommands defined by the plugin diff --git a/lib/libimhex/include/hex/providers/provider_data.hpp b/lib/libimhex/include/hex/providers/provider_data.hpp index f80cec521..b36e0b1dd 100644 --- a/lib/libimhex/include/hex/providers/provider_data.hpp +++ b/lib/libimhex/include/hex/providers/provider_data.hpp @@ -83,18 +83,18 @@ namespace hex { this->m_data.clear(); }); - // moves the data of this PerProvider instance from one provider to another + // Moves the data of this PerProvider instance from one provider to another EventManager::subscribe(this, [this](prv::Provider *from, prv::Provider *to) { - // get the value from the old provider, (removes it from the map) + // Get the value from the old provider, (removes it from the map) auto node = m_data.extract(from); - // ensure the value existed + // Ensure the value existed if (node.empty()) return; - // delete the value from the new provider, that we want to replace + // Delete the value from the new provider, that we want to replace this->m_data.erase(to); - // re-insert it with the key of the new provider + // Re-insert it with the key of the new provider node.key() = to; this->m_data.insert(std::move(node)); }); diff --git a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h index 3dbf71b9d..756420772 100644 --- a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h +++ b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h @@ -177,7 +177,7 @@ namespace ImGui { } inline void TextFormattedWrappedSelectable(const std::string &fmt, auto &&...args) { - //Manually wrap text, using the letter M (generally the widest character in non-monospaced fonts) to calculate the character width to use. + // Manually wrap text, using the letter M (generally the widest character in non-monospaced fonts) to calculate the character width to use. auto text = wolv::util::wrapMonospacedString( hex::format(fmt, std::forward(args)...), ImGui::CalcTextSize("M").x, diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index 8242d4edc..62270ca2b 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -8,6 +8,10 @@ #include #include +#if defined(OS_WEB) +#include +#include +#endif #include @@ -18,7 +22,7 @@ namespace hex { namespace ContentRegistry::Settings { - constexpr auto SettingsFile = "settings.json"; + [[maybe_unused]] constexpr auto SettingsFile = "settings.json"; namespace impl { @@ -49,43 +53,71 @@ namespace hex { return settings; } - void load() { - bool loaded = false; - for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { - wolv::io::File file(dir / SettingsFile, wolv::io::File::Mode::Read); + #if defined(OS_WEB) + void load() { + char *data = (char *) MAIN_THREAD_EM_ASM_INT({ + let data = localStorage.getItem("config"); + return data ? stringToNewUTF8(data) : null; + }); - if (file.isValid()) { - getSettingsData() = nlohmann::json::parse(file.readString()); - loaded = true; - break; + if (data == nullptr) { + store(); + } else { + getSettingsData() = nlohmann::json::parse(data); } } - if (!loaded) - store(); - } - - void store() { - // During a crash settings can be empty, causing them to be overwritten. - if(getSettingsData().empty()) { - return; + void store() { + auto data = getSettingsData().dump(); + MAIN_THREAD_EM_ASM({ + localStorage.setItem("config", UTF8ToString($0)); + }, data.c_str()); } - for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { - wolv::io::File file(dir / SettingsFile, wolv::io::File::Mode::Create); + void clear() { + MAIN_THREAD_EM_ASM({ + localStorage.removeItem("config"); + }); + } + #else + void load() { + bool loaded = false; + for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { + wolv::io::File file(dir / SettingsFile, wolv::io::File::Mode::Read); - if (file.isValid()) { - file.writeString(getSettingsData().dump(4)); - break; + if (file.isValid()) { + getSettingsData() = nlohmann::json::parse(file.readString()); + loaded = true; + break; + } + } + + if (!loaded) + store(); + } + + void store() { + // During a crash settings can be empty, causing them to be overwritten. + if(getSettingsData().empty()) { + return; + } + + for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { + wolv::io::File file(dir / SettingsFile, wolv::io::File::Mode::Create); + + if (file.isValid()) { + file.writeString(getSettingsData().dump(4)); + break; + } } } - } - void clear() { - for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { - wolv::io::fs::remove(dir / SettingsFile); + void clear() { + for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { + wolv::io::fs::remove(dir / SettingsFile); + } } - } + #endif static auto getCategoryEntry(const std::string &unlocalizedCategory) { auto &entries = getEntries(); diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index 7eb451b89..0eca8739c 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -370,7 +370,7 @@ namespace hex { namespace impl { - // default to true means we forward to ourselves by default + // Default to true means we forward to ourselves by default static bool s_isMainInstance = true; void setMainInstanceStatus(bool status) { @@ -547,6 +547,8 @@ namespace hex { return "Linux"; #elif defined(OS_MACOS) return "macOS"; + #elif defined(OS_WEB) + return "Web"; #else return "Unknown"; #endif @@ -559,7 +561,7 @@ namespace hex { ::GetVersionExA(&info); return hex::format("{}.{}.{}", info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber); - #elif defined(OS_LINUX) || defined(OS_MACOS) + #elif defined(OS_LINUX) || defined(OS_MACOS) || defined(OS_WEB) struct utsname details; if (uname(&details) != 0) { @@ -591,7 +593,7 @@ namespace hex { default: return "Unknown"; } - #elif defined(OS_LINUX) || defined(OS_MACOS) + #elif defined(OS_LINUX) || defined(OS_MACOS) || defined(OS_WEB) struct utsname details; if (uname(&details) != 0) { diff --git a/lib/libimhex/source/api/plugin_manager.cpp b/lib/libimhex/source/api/plugin_manager.cpp index 47a438cdb..6000bc204 100644 --- a/lib/libimhex/source/api/plugin_manager.cpp +++ b/lib/libimhex/source/api/plugin_manager.cpp @@ -12,6 +12,7 @@ namespace hex { Plugin::Plugin(const std::fs::path &path) : m_path(path) { + #if defined(OS_WINDOWS) this->m_handle = LoadLibraryW(path.c_str()); @@ -28,38 +29,30 @@ namespace hex { } #endif - this->m_initializePluginFunction = getPluginFunction("initializePlugin"); - this->m_getPluginNameFunction = getPluginFunction("getPluginName"); - this->m_getPluginAuthorFunction = getPluginFunction("getPluginAuthor"); - this->m_getPluginDescriptionFunction = getPluginFunction("getPluginDescription"); - this->m_getCompatibleVersionFunction = getPluginFunction("getCompatibleVersion"); - this->m_setImGuiContextFunction = getPluginFunction("setImGuiContext"); - this->m_isBuiltinPluginFunction = getPluginFunction("isBuiltinPlugin"); - this->m_getSubCommandsFunction = getPluginFunction("getSubCommands"); + this->m_functions.initializePluginFunction = getPluginFunction("initializePlugin"); + this->m_functions.getPluginNameFunction = getPluginFunction("getPluginName"); + this->m_functions.getPluginAuthorFunction = getPluginFunction("getPluginAuthor"); + this->m_functions.getPluginDescriptionFunction = getPluginFunction("getPluginDescription"); + this->m_functions.getCompatibleVersionFunction = getPluginFunction("getCompatibleVersion"); + this->m_functions.setImGuiContextFunction = getPluginFunction("setImGuiContext"); + this->m_functions.isBuiltinPluginFunction = getPluginFunction("isBuiltinPlugin"); + this->m_functions.getSubCommandsFunction = getPluginFunction("getSubCommands"); } + Plugin::Plugin(hex::PluginFunctions functions) { + this->m_handle = nullptr; + this->m_functions = functions; + } + + Plugin::Plugin(Plugin &&other) noexcept { this->m_handle = other.m_handle; + other.m_handle = nullptr; + this->m_path = std::move(other.m_path); - this->m_initializePluginFunction = other.m_initializePluginFunction; - this->m_getPluginNameFunction = other.m_getPluginNameFunction; - this->m_getPluginAuthorFunction = other.m_getPluginAuthorFunction; - this->m_getPluginDescriptionFunction = other.m_getPluginDescriptionFunction; - this->m_getCompatibleVersionFunction = other.m_getCompatibleVersionFunction; - this->m_setImGuiContextFunction = other.m_setImGuiContextFunction; - this->m_isBuiltinPluginFunction = other.m_isBuiltinPluginFunction; - this->m_getSubCommandsFunction = other.m_getSubCommandsFunction; - - other.m_handle = nullptr; - other.m_initializePluginFunction = nullptr; - other.m_getPluginNameFunction = nullptr; - other.m_getPluginAuthorFunction = nullptr; - other.m_getPluginDescriptionFunction = nullptr; - other.m_getCompatibleVersionFunction = nullptr; - other.m_setImGuiContextFunction = nullptr; - other.m_isBuiltinPluginFunction = nullptr; - other.m_getSubCommandsFunction = nullptr; + this->m_functions = other.m_functions; + other.m_functions = {}; } Plugin::~Plugin() { @@ -67,15 +60,10 @@ namespace hex { if (this->m_handle != nullptr) FreeLibrary(this->m_handle); #else - if (this->m_handle != nullptr) - dlclose(this->m_handle); #endif } bool Plugin::initializePlugin() const { - if (this->m_handle == nullptr) - return false; - const auto pluginName = wolv::util::toUTF8String(this->m_path.filename()); const auto requestedVersion = getCompatibleVersion(); @@ -88,9 +76,9 @@ namespace hex { } } - if (this->m_initializePluginFunction != nullptr) { + if (this->m_functions.initializePluginFunction != nullptr) { try { - this->m_initializePluginFunction(); + this->m_functions.initializePluginFunction(); } catch (const std::exception &e) { log::error("Plugin '{}' threw an exception on init: {}", pluginName, e.what()); return false; @@ -99,6 +87,7 @@ namespace hex { return false; } } else { + log::error("Plugin '{}' does not have a proper entrypoint", pluginName); return false; } @@ -107,41 +96,41 @@ namespace hex { } std::string Plugin::getPluginName() const { - if (this->m_getPluginNameFunction != nullptr) - return this->m_getPluginNameFunction(); + if (this->m_functions.getPluginNameFunction != nullptr) + return this->m_functions.getPluginNameFunction(); else return hex::format("Unknown Plugin @ 0x{0:016X}", reinterpret_cast(this->m_handle)); } std::string Plugin::getPluginAuthor() const { - if (this->m_getPluginAuthorFunction != nullptr) - return this->m_getPluginAuthorFunction(); + if (this->m_functions.getPluginAuthorFunction != nullptr) + return this->m_functions.getPluginAuthorFunction(); else return "Unknown"; } std::string Plugin::getPluginDescription() const { - if (this->m_getPluginDescriptionFunction != nullptr) - return this->m_getPluginDescriptionFunction(); + if (this->m_functions.getPluginDescriptionFunction != nullptr) + return this->m_functions.getPluginDescriptionFunction(); else return ""; } std::string Plugin::getCompatibleVersion() const { - if (this->m_getCompatibleVersionFunction != nullptr) - return this->m_getCompatibleVersionFunction(); + if (this->m_functions.getCompatibleVersionFunction != nullptr) + return this->m_functions.getCompatibleVersionFunction(); else return ""; } void Plugin::setImGuiContext(ImGuiContext *ctx) const { - if (this->m_setImGuiContextFunction != nullptr) - this->m_setImGuiContextFunction(ctx); + if (this->m_functions.setImGuiContextFunction != nullptr) + this->m_functions.setImGuiContextFunction(ctx); } [[nodiscard]] bool Plugin::isBuiltinPlugin() const { - if (this->m_isBuiltinPluginFunction != nullptr) - return this->m_isBuiltinPluginFunction(); + if (this->m_functions.isBuiltinPluginFunction != nullptr) + return this->m_functions.isBuiltinPluginFunction(); else return false; } @@ -155,8 +144,8 @@ namespace hex { } std::span Plugin::getSubCommands() const { - if (this->m_getSubCommandsFunction != nullptr) { - auto result = this->m_getSubCommandsFunction(); + if (this->m_functions.getSubCommandsFunction != nullptr) { + auto result = this->m_functions.getSubCommandsFunction(); return *reinterpret_cast*>(result); } else return { }; @@ -171,43 +160,50 @@ namespace hex { #endif } - - namespace { - - std::fs::path s_pluginFolder; - std::vector s_plugins; - - } - bool PluginManager::load(const std::fs::path &pluginFolder) { if (!wolv::io::fs::exists(pluginFolder)) return false; - s_pluginFolder = pluginFolder; + getPluginPaths().push_back(pluginFolder); for (auto &pluginPath : std::fs::directory_iterator(pluginFolder)) { if (pluginPath.is_regular_file() && pluginPath.path().extension() == ".hexplug") - s_plugins.emplace_back(pluginPath.path()); + getPlugins().emplace_back(pluginPath.path()); } - if (s_plugins.empty()) + if (getPlugins().empty()) return false; return true; } void PluginManager::unload() { - s_plugins.clear(); - s_pluginFolder.clear(); + getPlugins().clear(); + getPluginPaths().clear(); } void PluginManager::reload() { + auto paths = getPluginPaths(); + PluginManager::unload(); - PluginManager::load(s_pluginFolder); + for (const auto &path : paths) + PluginManager::load(path); } - const std::vector &PluginManager::getPlugins() { - return s_plugins; + void PluginManager::addPlugin(hex::PluginFunctions functions) { + getPlugins().emplace_back(functions); + } + + std::vector &PluginManager::getPlugins() { + static std::vector plugins; + + return plugins; + } + + std::vector &PluginManager::getPluginPaths() { + static std::vector pluginPaths; + + return pluginPaths; } } diff --git a/lib/libimhex/source/api/task.cpp b/lib/libimhex/source/api/task.cpp index 14cc59149..98b7f16d9 100644 --- a/lib/libimhex/source/api/task.cpp +++ b/lib/libimhex/source/api/task.cpp @@ -49,6 +49,8 @@ namespace hex { RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); #elif defined(OS_LINUX) pthread_setname_np(pthread_self(), name.c_str()); + #elif defined(OS_WEB) + hex::unused(name); #elif defined(OS_MACOS) pthread_setname_np(name.c_str()); #endif diff --git a/lib/libimhex/source/helpers/crypto.cpp b/lib/libimhex/source/helpers/crypto.cpp index 7539d859d..7ce3b1d50 100644 --- a/lib/libimhex/source/helpers/crypto.cpp +++ b/lib/libimhex/source/helpers/crypto.cpp @@ -82,7 +82,7 @@ namespace hex::crypt { template requires (std::has_single_bit(NumBits)) class Crc { - // use reflected algorithm, so we reflect only if refin / refout is FALSE + // Use reflected algorithm, so we reflect only if refin / refout is FALSE // mask values, 0b1 << 64 is UB, so use 0b10 << 63 public: @@ -396,7 +396,7 @@ namespace hex::crypt { ON_SCOPE_EXIT { mbedtls_mpi_free(&ctx); }; - // read buffered + // Read buffered constexpr static auto BufferSize = 0x100; for (size_t offset = 0; offset < input.size(); offset += BufferSize) { std::string inputPart = input.substr(offset, std::min(BufferSize, input.size() - offset)); diff --git a/lib/libimhex/source/helpers/fs.cpp b/lib/libimhex/source/helpers/fs.cpp index 07d3beaa8..6ba9326d4 100644 --- a/lib/libimhex/source/helpers/fs.cpp +++ b/lib/libimhex/source/helpers/fs.cpp @@ -13,7 +13,13 @@ #include #elif defined(OS_LINUX) #include - #include + #include +#endif + +#if !defined(OS_WEB) +#include +#else +#include #endif #include @@ -84,66 +90,173 @@ namespace hex::fs { )); system(R"(osascript -e 'tell application "Finder" to activate')"); #elif defined(OS_LINUX) - // fallback to only opening the folder for now + // Fallback to only opening the folder for now // TODO actually select the file executeCmd({"xdg-open", wolv::util::toUTF8String(selectedFilePath.parent_path())}); #endif } - bool openFileBrowser(DialogMode mode, const std::vector &validExtensions, const std::function &callback, const std::string &defaultPath, bool multiple) { - NFD::ClearError(); + #if defined(OS_WEB) - if (NFD::Init() != NFD_OKAY) { - log::error("NFD init returned an error: {}", NFD::GetError()); - if (s_fileBrowserErrorCallback != nullptr) - s_fileBrowserErrorCallback(NFD::GetError() ? NFD::GetError() : "No details"); - return false; + std::function currentCallback; + + EMSCRIPTEN_KEEPALIVE + extern "C" void fileBrowserCallback(char* path) { + currentCallback(path); } - NFD::UniquePathU8 outPath; - NFD::UniquePathSet outPaths; - nfdresult_t result; - switch (mode) { - case DialogMode::Open: - if (multiple) - result = NFD::OpenDialogMultiple(outPaths, validExtensions.data(), validExtensions.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); - else - result = NFD::OpenDialog(outPath, validExtensions.data(), validExtensions.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); - break; - case DialogMode::Save: - result = NFD::SaveDialog(outPath, validExtensions.data(), validExtensions.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); - break; - case DialogMode::Folder: - result = NFD::PickFolder(outPath, defaultPath.empty() ? nullptr : defaultPath.c_str()); - break; - default: - std::unreachable(); - } + EM_JS(int, callJs_saveFile, (const char *rawFilename), { + let filename = UTF8ToString(rawFilename) || "file.bin"; + FS.createPath("/", "savedFiles"); - if (result == NFD_OKAY){ - if(outPath != nullptr) { - callback(reinterpret_cast(outPath.get())); + if (FS.analyzePath(filename).exists) { + FS.unlink(filename); } - if (outPaths != nullptr) { - nfdpathsetsize_t numPaths = 0; - if (NFD::PathSet::Count(outPaths, numPaths) == NFD_OKAY) { - for (size_t i = 0; i < numPaths; i++) { - NFD::UniquePathSetPath path; - if (NFD::PathSet::GetPath(outPaths, i, path) == NFD_OKAY) - callback(reinterpret_cast(path.get())); + + // Call callback that will write the file + Module._fileBrowserCallback(stringToNewUTF8("/savedFiles/" + filename)); + + let data = FS.readFile("/savedFiles/" + filename); + + const reader = Object.assign(new FileReader(), { + onload: () => { + + // Show popup to user to download + let saver = document.createElement('a'); + saver.href = reader.result; + saver.download = filename; + saver.style = "display: none"; + + saver.click(); + + }, + onerror: () => { + throw new Error(reader.error); + }, + }); + reader.readAsDataURL(new File([data], "", { type: "application/octet-stream" })); + + }); + + EM_JS(int, callJs_openFile, (bool multiple), { + let selector = document.createElement("input"); + selector.type = "file"; + selector.style = "display: none"; + if (multiple) { + selector.multiple = true; + } + selector.onchange = () => { + if (selector.files.length == 0) return; + + FS.createPath("/", "openedFiles"); + for (let file of selector.files) { + const fr = new FileReader(); + fr.onload = () => { + let path = "/openedFiles/"+file.name; + if (FS.analyzePath(path).exists) { + FS.unlink(path); + } + FS.createDataFile("/openedFiles/", file.name, fr.result, true, true); + Module._fileBrowserCallback(stringToNewUTF8(path)); + }; + + fr.readAsBinaryString(file); + } + }; + selector.click(); + }); + + bool openFileBrowser(DialogMode mode, const std::vector &validExtensions, const std::function &callback, const std::string &defaultPath, bool multiple) { + switch (mode) { + case DialogMode::Open: { + currentCallback = callback; + callJs_openFile(multiple); + break; + } + case DialogMode::Save: { + currentCallback = callback; + std::fs::path path; + + if (!defaultPath.empty()) + path = std::fs::path(defaultPath).filename(); + else if (!validExtensions.empty()) + path = "file." + validExtensions[0].spec; + + callJs_saveFile(path.filename().string().c_str()); + break; + } + case DialogMode::Folder: { + throw std::logic_error("Selecting a folder is not implemented"); + return false; + } + default: + std::unreachable(); + } + return true; + } + + #else + + bool openFileBrowser(DialogMode mode, const std::vector &validExtensions, const std::function &callback, const std::string &defaultPath, bool multiple) { + std::vector validExtensionsNfd; + for (auto ext : validExtensions) { + validExtensionsNfd.emplace_back(nfdfilteritem_t{ext.name.c_str(), ext.spec.c_str()}); + } + NFD::ClearError(); + + if (NFD::Init() != NFD_OKAY) { + log::error("NFD init returned an error: {}", NFD::GetError()); + if (s_fileBrowserErrorCallback != nullptr) + s_fileBrowserErrorCallback(NFD::GetError() ? NFD::GetError() : "No details"); + return false; + } + + NFD::UniquePathU8 outPath; + NFD::UniquePathSet outPaths; + nfdresult_t result; + switch (mode) { + case DialogMode::Open: + if (multiple) + result = NFD::OpenDialogMultiple(outPaths, validExtensionsNfd.data(), validExtensionsNfd.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); + else + result = NFD::OpenDialog(outPath, validExtensionsNfd.data(), validExtensionsNfd.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); + break; + case DialogMode::Save: + result = NFD::SaveDialog(outPath, validExtensionsNfd.data(), validExtensionsNfd.size(), defaultPath.empty() ? nullptr : defaultPath.c_str()); + break; + case DialogMode::Folder: + result = NFD::PickFolder(outPath, defaultPath.empty() ? nullptr : defaultPath.c_str()); + break; + default: + std::unreachable(); + } + + if (result == NFD_OKAY){ + if(outPath != nullptr) { + callback(reinterpret_cast(outPath.get())); + } + if (outPaths != nullptr) { + nfdpathsetsize_t numPaths = 0; + if (NFD::PathSet::Count(outPaths, numPaths) == NFD_OKAY) { + for (size_t i = 0; i < numPaths; i++) { + NFD::UniquePathSetPath path; + if (NFD::PathSet::GetPath(outPaths, i, path) == NFD_OKAY) + callback(reinterpret_cast(path.get())); + } } } + } else if (result == NFD_ERROR) { + log::error("Requested file dialog returned an error: {}", NFD::GetError()); + if (s_fileBrowserErrorCallback != nullptr) + s_fileBrowserErrorCallback(NFD::GetError() ? NFD::GetError() : "No details"); } - } else if (result == NFD_ERROR) { - log::error("Requested file dialog returned an error: {}", NFD::GetError()); - if (s_fileBrowserErrorCallback != nullptr) - s_fileBrowserErrorCallback(NFD::GetError() ? NFD::GetError() : "No details"); + + NFD::Quit(); + + return result == NFD_OKAY; } - NFD::Quit(); - - return result == NFD_OKAY; - } + #endif std::vector getDataPaths() { std::vector paths; @@ -202,7 +315,7 @@ namespace hex::fs { return getDataPaths(); #elif defined(OS_MACOS) return getDataPaths(); - #elif defined(OS_LINUX) + #elif defined(OS_LINUX) || defined(OS_WEB) return {xdg::ConfigHomeDir() / "imhex"}; #endif } diff --git a/lib/libimhex/source/helpers/http_requests.cpp b/lib/libimhex/source/helpers/http_requests.cpp index 88e0f1572..cfa4c762e 100644 --- a/lib/libimhex/source/helpers/http_requests.cpp +++ b/lib/libimhex/source/helpers/http_requests.cpp @@ -1,47 +1,9 @@ #include +#include + namespace hex { - namespace { - - std::string s_proxyUrl; - - } - - - HttpRequest::HttpRequest(std::string method, std::string url) : m_method(std::move(method)), m_url(std::move(url)) { - AT_FIRST_TIME { - curl_global_init(CURL_GLOBAL_ALL); - }; - - AT_FINAL_CLEANUP { - curl_global_cleanup(); - }; - - this->m_curl = curl_easy_init(); - } - - HttpRequest::~HttpRequest() { - curl_easy_cleanup(this->m_curl); - } - - void HttpRequest::setDefaultConfig() { - curl_easy_setopt(this->m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); - curl_easy_setopt(this->m_curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); - curl_easy_setopt(this->m_curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(this->m_curl, CURLOPT_USERAGENT, "ImHex/1.0"); - curl_easy_setopt(this->m_curl, CURLOPT_DEFAULT_PROTOCOL, "https"); - curl_easy_setopt(this->m_curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(this->m_curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(this->m_curl, CURLOPT_TIMEOUT_MS, 0L); - curl_easy_setopt(this->m_curl, CURLOPT_CONNECTTIMEOUT_MS, this->m_timeout); - curl_easy_setopt(this->m_curl, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(this->m_curl, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(this->m_curl, CURLOPT_XFERINFODATA, this); - curl_easy_setopt(this->m_curl, CURLOPT_XFERINFOFUNCTION, progressCallback); - curl_easy_setopt(this->m_curl, CURLOPT_PROXY, s_proxyUrl.c_str()); - } - size_t HttpRequest::writeToVector(void *contents, size_t size, size_t nmemb, void *userdata) { auto &response = *reinterpret_cast*>(userdata); auto startSize = response.size(); @@ -60,27 +22,36 @@ namespace hex { return size * nmemb; } - int HttpRequest::progressCallback(void *contents, curl_off_t dlTotal, curl_off_t dlNow, curl_off_t ulTotal, curl_off_t ulNow) { - auto &request = *static_cast(contents); - - if (dlTotal > 0) - request.m_progress = float(dlNow) / dlTotal; - else if (ulTotal > 0) - request.m_progress = float(ulNow) / ulTotal; - else - request.m_progress = 0.0F; - - return request.m_canceled ? CURLE_ABORTED_BY_CALLBACK : CURLE_OK; - } - - void HttpRequest::setProxy(std::string proxy) { - s_proxyUrl = std::move(proxy); - } - - void HttpRequest::checkProxyErrors() { - if (!s_proxyUrl.empty()){ - log::info("A custom proxy '{0}' is in use. Is it working correctly?", s_proxyUrl); + std::string HttpRequest::urlEncode(const std::string &input) { + std::string result; + for (char c : input){ + if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') + result += c; + else + result += hex::format("%02X", c); } + return result; + } + + std::string HttpRequest::urlDecode(const std::string &input) { + std::string result; + + for (u32 i = 0; i < input.size(); i++){ + if (input[i] != '%'){ + if (input[i] == '+') + result += ' '; + else + result += input[i]; + } else { + const auto hex = crypt::decode16(input.substr(i + 1, 2)); + if (hex.empty()) + return ""; + + result += char(hex[0]); + i += 2; + } + } + return input; } } \ No newline at end of file diff --git a/lib/libimhex/source/helpers/http_requests_emscripten.cpp b/lib/libimhex/source/helpers/http_requests_emscripten.cpp new file mode 100644 index 000000000..d13a0698c --- /dev/null +++ b/lib/libimhex/source/helpers/http_requests_emscripten.cpp @@ -0,0 +1,55 @@ +#if defined(OS_WEB) + +#include + +namespace hex { + + HttpRequest::HttpRequest(std::string method, std::string url) : m_method(std::move(method)), m_url(std::move(url)) { + emscripten_fetch_attr_init(&this->m_attr); + } + + HttpRequest::HttpRequest(HttpRequest &&other) noexcept { + this->m_attr = other.m_attr; + + this->m_method = std::move(other.m_method); + this->m_url = std::move(other.m_url); + this->m_headers = std::move(other.m_headers); + this->m_body = std::move(other.m_body); + } + + HttpRequest& HttpRequest::operator=(HttpRequest &&other) noexcept { + this->m_attr = other.m_attr; + + this->m_method = std::move(other.m_method); + this->m_url = std::move(other.m_url); + this->m_headers = std::move(other.m_headers); + this->m_body = std::move(other.m_body); + + return *this; + } + + HttpRequest::~HttpRequest() { } + + void HttpRequest::setDefaultConfig() { } + + std::future>> HttpRequest::downloadFile() { + return std::async(std::launch::async, [this] { + std::vector response; + + return this->executeImpl>(response); + }); + } + + void HttpRequest::setProxy(std::string proxy) { + hex::unused(proxy); + } + + void HttpRequest::checkProxyErrors() { } + + int HttpRequest::progressCallback(void *contents, curl_off_t dlTotal, curl_off_t dlNow, curl_off_t ulTotal, curl_off_t ulNow) { + hex::unused(contents, dlTotal, dlNow, ulTotal, ulNow); + return -1; + } +} + +#endif \ No newline at end of file diff --git a/lib/libimhex/source/helpers/http_requests_native.cpp b/lib/libimhex/source/helpers/http_requests_native.cpp new file mode 100644 index 000000000..cae9953df --- /dev/null +++ b/lib/libimhex/source/helpers/http_requests_native.cpp @@ -0,0 +1,105 @@ +#if !defined(OS_WEB) + +#include + +namespace hex { + + namespace { + std::string s_proxyUrl; + } + + HttpRequest::HttpRequest(std::string method, std::string url) : m_method(std::move(method)), m_url(std::move(url)) { + AT_FIRST_TIME { + curl_global_init(CURL_GLOBAL_ALL); + }; + + AT_FINAL_CLEANUP { + curl_global_cleanup(); + }; + + this->m_curl = curl_easy_init(); + } + + HttpRequest::~HttpRequest() { + curl_easy_cleanup(this->m_curl); + } + + HttpRequest::HttpRequest(HttpRequest &&other) noexcept { + this->m_curl = other.m_curl; + other.m_curl = nullptr; + + this->m_method = std::move(other.m_method); + this->m_url = std::move(other.m_url); + this->m_headers = std::move(other.m_headers); + this->m_body = std::move(other.m_body); + } + + HttpRequest& HttpRequest::operator=(HttpRequest &&other) noexcept { + this->m_curl = other.m_curl; + other.m_curl = nullptr; + + this->m_method = std::move(other.m_method); + this->m_url = std::move(other.m_url); + this->m_headers = std::move(other.m_headers); + this->m_body = std::move(other.m_body); + + return *this; + } + + void HttpRequest::setDefaultConfig() { + curl_easy_setopt(this->m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(this->m_curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_easy_setopt(this->m_curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(this->m_curl, CURLOPT_USERAGENT, "ImHex/1.0"); + curl_easy_setopt(this->m_curl, CURLOPT_DEFAULT_PROTOCOL, "https"); + curl_easy_setopt(this->m_curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(this->m_curl, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(this->m_curl, CURLOPT_TIMEOUT_MS, 0L); + curl_easy_setopt(this->m_curl, CURLOPT_CONNECTTIMEOUT_MS, this->m_timeout); + curl_easy_setopt(this->m_curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(this->m_curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(this->m_curl, CURLOPT_XFERINFODATA, this); + curl_easy_setopt(this->m_curl, CURLOPT_XFERINFOFUNCTION, progressCallback); + curl_easy_setopt(this->m_curl, CURLOPT_PROXY, s_proxyUrl.c_str()); + } + + std::future>> HttpRequest::downloadFile() { + return std::async(std::launch::async, [this] { + std::vector response; + + curl_easy_setopt(this->m_curl, CURLOPT_WRITEFUNCTION, writeToVector); + curl_easy_setopt(this->m_curl, CURLOPT_WRITEDATA, &response); + + return this->executeImpl>(response); + }); + } + + + + void HttpRequest::setProxy(std::string proxy) { + s_proxyUrl = std::move(proxy); + } + + void HttpRequest::checkProxyErrors() { + if (!s_proxyUrl.empty()){ + log::info("A custom proxy '{0}' is in use. Is it working correctly?", s_proxyUrl); + } + } + + int HttpRequest::progressCallback(void *contents, curl_off_t dlTotal, curl_off_t dlNow, curl_off_t ulTotal, curl_off_t ulNow) { + auto &request = *static_cast(contents); + + if (dlTotal > 0) + request.m_progress = float(dlNow) / dlTotal; + else if (ulTotal > 0) + request.m_progress = float(ulNow) / ulTotal; + else + request.m_progress = 0.0F; + + return request.m_canceled ? CURLE_ABORTED_BY_CALLBACK : CURLE_OK; + } + +} + + +#endif \ No newline at end of file diff --git a/lib/libimhex/source/helpers/opengl.cpp b/lib/libimhex/source/helpers/opengl.cpp index 81ac3c6bd..0a2d489ef 100644 --- a/lib/libimhex/source/helpers/opengl.cpp +++ b/lib/libimhex/source/helpers/opengl.cpp @@ -5,6 +5,13 @@ #include +#if defined(OS_WEB) + #define GLFW_INCLUDE_ES3 + #include +#else + #include +#endif + namespace hex::gl { Shader::Shader(std::string_view vertexSource, std::string_view fragmentSource) { diff --git a/lib/libimhex/source/helpers/tar.cpp b/lib/libimhex/source/helpers/tar.cpp index 75d75563b..69c76da08 100644 --- a/lib/libimhex/source/helpers/tar.cpp +++ b/lib/libimhex/source/helpers/tar.cpp @@ -35,7 +35,7 @@ namespace hex { if (!this->m_valid) { this->m_tarOpenErrno = tar_error; - // hopefully this errno corresponds to the file open call in mtar_open + // Hopefully this errno corresponds to the file open call in mtar_open this->m_fileOpenErrno = errno; } } diff --git a/lib/libimhex/source/helpers/utils.cpp b/lib/libimhex/source/helpers/utils.cpp index 4b93238e3..1231fcb0a 100644 --- a/lib/libimhex/source/helpers/utils.cpp +++ b/lib/libimhex/source/helpers/utils.cpp @@ -20,6 +20,8 @@ #elif defined(OS_MACOS) #include #include +#elif defined(OS_WEB) + #include "emscripten.h" #endif namespace hex { @@ -319,6 +321,8 @@ namespace hex { hex::unused(system(hex::format("open {0}", command).c_str())); #elif defined(OS_LINUX) executeCmd({"xdg-open", command}); + #elif defined(OS_WEB) + hex::unused(command); #endif } @@ -332,6 +336,10 @@ namespace hex { openWebpageMacos(url.c_str()); #elif defined(OS_LINUX) executeCmd({"xdg-open", url}); + #elif defined(OS_WEB) + EM_ASM({ + window.open(UTF8ToString($0), '_blank'); + }, url.c_str()); #else #warning "Unknown OS, can't open webpages" #endif @@ -497,26 +505,27 @@ namespace hex { } bool isProcessElevated() { -#if defined(OS_WINDOWS) - bool elevated = false; - HANDLE token = INVALID_HANDLE_VALUE; + #if defined(OS_WINDOWS) + bool elevated = false; + HANDLE token = INVALID_HANDLE_VALUE; - if (::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token)) { - TOKEN_ELEVATION elevation; - DWORD elevationSize = sizeof(TOKEN_ELEVATION); + if (::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token)) { + TOKEN_ELEVATION elevation; + DWORD elevationSize = sizeof(TOKEN_ELEVATION); - if (::GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &elevationSize)) - elevated = elevation.TokenIsElevated; - } + if (::GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &elevationSize)) + elevated = elevation.TokenIsElevated; + } - if (token != INVALID_HANDLE_VALUE) - ::CloseHandle(token); + if (token != INVALID_HANDLE_VALUE) + ::CloseHandle(token); - return elevated; - -#elif defined(OS_LINUX) || defined(OS_MACOS) - return getuid() == 0 || getuid() != geteuid(); -#endif + return elevated; + #elif defined(OS_LINUX) || defined(OS_MACOS) + return getuid() == 0 || getuid() != geteuid(); + #else + return false; + #endif } std::optional getEnvironmentVariable(const std::string &env) { diff --git a/lib/libimhex/source/subcommands/subcommands.cpp b/lib/libimhex/source/subcommands/subcommands.cpp index 8971d7ad5..10c82e67d 100644 --- a/lib/libimhex/source/subcommands/subcommands.cpp +++ b/lib/libimhex/source/subcommands/subcommands.cpp @@ -34,26 +34,26 @@ namespace hex::subcommands { auto argsIter = args.begin(); - // get subcommand associated with the first argument + // Get subcommand associated with the first argument std::optional currentSubCommand = findSubCommand(*argsIter); if (currentSubCommand) { argsIter++; - // if it is a valid subcommand, remove it from the argument list + // If it is a valid subcommand, remove it from the argument list } else { - // if no (valid) subcommand was provided, the default one is --open + // If no (valid) subcommand was provided, the default one is --open currentSubCommand = findSubCommand("--open"); } - // arguments of the current subcommand + // Arguments of the current subcommand std::vector currentSubCommandArgs; - // compute all subcommands to run + // Compute all subcommands to run while (argsIter != args.end()) { const std::string &arg = *argsIter; if (arg == "--othercmd") { - // save command to run + // Save command to run if (currentSubCommand) { subCommands.emplace_back(*currentSubCommand, currentSubCommandArgs); } @@ -62,10 +62,10 @@ namespace hex::subcommands { currentSubCommandArgs = { }; } else if (currentSubCommand) { - // add current argument to the current command + // Add current argument to the current command currentSubCommandArgs.push_back(arg); } else { - // get next subcommand from current argument + // Get next subcommand from current argument currentSubCommand = findSubCommand(arg); if (!currentSubCommand) { log::error("No subcommand named '{}' found", arg); @@ -76,17 +76,17 @@ namespace hex::subcommands { argsIter++; } - // save last command to run + // Save last command to run if (currentSubCommand) { subCommands.emplace_back(*currentSubCommand, currentSubCommandArgs); } - // run the subcommands + // Run the subcommands for (auto& subCommandPair : subCommands) { subCommandPair.first.callback(subCommandPair.second); } - // exit the process if its not the main instance (the commands have been forwarded to another instance) + // Exit the process if its not the main instance (the commands have been forwarded to another instance) if (!ImHexApi::System::isMainInstance()) { exit(0); } diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index 111091c4a..cb6b76ee9 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -9,8 +9,6 @@ #include -#include - #include #include diff --git a/main/gui/CMakeLists.txt b/main/gui/CMakeLists.txt index 69dbdf92f..b01a84aa4 100644 --- a/main/gui/CMakeLists.txt +++ b/main/gui/CMakeLists.txt @@ -8,11 +8,13 @@ add_executable(main ${APPLICATION_TYPE} source/window/win_window.cpp source/window/macos_window.cpp source/window/linux_window.cpp + source/window/web_window.cpp source/messaging/common.cpp source/messaging/linux.cpp source/messaging/macos.cpp source/messaging/win.cpp + source/messaging/web.cpp source/init/splash_window.cpp source/init/tasks.cpp @@ -29,6 +31,18 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/external/libromfs ${CMAKE set_target_properties(${LIBROMFS_LIBRARY} PROPERTIES POSITION_INDEPENDENT_CODE ON) add_dependencies(imhex_all main) +if (EMSCRIPTEN) + target_link_options(main PRIVATE -sUSE_GLFW=3 -sUSE_PTHREADS=1 -sALLOW_MEMORY_GROWTH=1) + target_link_options(main PRIVATE -sTOTAL_MEMORY=134217728) + target_link_options(main PRIVATE -sMAX_WEBGL_VERSION=2) + target_link_options(main PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall) + target_link_options(main PRIVATE -sFETCH) + target_link_options(main PRIVATE -sWASM_BIGINT) + target_link_options(main PRIVATE -O1) + target_link_options(main PRIVATE -sLEGACY_GL_EMULATION) + target_link_libraries(main PRIVATE idbfs.js) +endif () + set_target_properties(main PROPERTIES OUTPUT_NAME ${IMHEX_APPLICATION_NAME} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../.. diff --git a/main/gui/include/window.hpp b/main/gui/include/window.hpp index de061d56d..3110230b8 100644 --- a/main/gui/include/window.hpp +++ b/main/gui/include/window.hpp @@ -28,6 +28,8 @@ namespace hex { static void initNative(); + void resize(i32 width, i32 height); + private: void setupNativeWindow(); void beginNativeWindowFrame(); diff --git a/main/gui/source/init/splash_window.cpp b/main/gui/source/init/splash_window.cpp index 236499628..d78bde129 100644 --- a/main/gui/source/init/splash_window.cpp +++ b/main/gui/source/init/splash_window.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -29,6 +28,14 @@ #include #include +#if defined(OS_WEB) + #define GLFW_INCLUDE_ES3 + #include + #include +#else + #include +#endif + using namespace std::literals::chrono_literals; namespace hex::init { @@ -324,6 +331,8 @@ namespace hex::init { glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); glfwWindowHint(GLFW_FLOATING, GLFW_FALSE); + glfwWindowHint(GLFW_SAMPLES, 1); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); // Create the splash screen window this->m_window = glfwCreateWindow(1, 400, "Starting ImHex...", nullptr, nullptr); @@ -373,6 +382,8 @@ namespace hex::init { #if defined(OS_MACOS) ImGui_ImplOpenGL3_Init("#version 150"); + #elif defined(OS_WEB) + ImGui_ImplOpenGL3_Init(); #else ImGui_ImplOpenGL3_Init("#version 130"); #endif diff --git a/main/gui/source/init/tasks.cpp b/main/gui/source/init/tasks.cpp index 9bf3b9335..09832227e 100644 --- a/main/gui/source/init/tasks.cpp +++ b/main/gui/source/init/tasks.cpp @@ -80,6 +80,13 @@ namespace hex::init { } TaskManager::createBackgroundTask("Sending statistics...", [uuid, versionString](auto&) { + // To avoid potentially flooding our database with lots of dead users + // from people just visiting the website, don't send telemetry data from + // the web version + #if defined(OS_WEB) + return; + #endif + // Make telemetry request nlohmann::json telemetry = { { "uuid", uuid }, @@ -145,7 +152,7 @@ namespace hex::init { wolv::io::File newConfigFile(newConfigPath / "settings.json", wolv::io::File::Mode::Read); if (!newConfigFile.isValid()) { - // find an old config + // Find an old config std::fs::path oldConfigPath; for (const auto &dir : hex::fs::appendPath(hex::fs::getDataPaths(), "config")) { wolv::io::File oldConfigFile(dir / "settings.json", wolv::io::File::Mode::Read); @@ -493,15 +500,17 @@ namespace hex::init { // ImHex requires exactly one built-in plugin // If no built-in plugin or more than one was found, something's wrong and we can't continue - if (builtinPlugins == 0) { - log::error("Built-in plugin not found!"); - ImHexApi::System::impl::addInitArgument("no-builtin-plugin"); - return false; - } else if (builtinPlugins > 1) { - log::error("Found more than one built-in plugin!"); - ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins"); - return false; - } + #if !defined(EMSCRIPTEN) + if (builtinPlugins == 0) { + log::error("Built-in plugin not found!"); + ImHexApi::System::impl::addInitArgument("no-builtin-plugin"); + return false; + } else if (builtinPlugins > 1) { + log::error("Found more than one built-in plugin!"); + ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins"); + return false; + } + #endif return true; } diff --git a/main/gui/source/main.cpp b/main/gui/source/main.cpp index 3f32e35c2..d722853dc 100644 --- a/main/gui/source/main.cpp +++ b/main/gui/source/main.cpp @@ -19,6 +19,11 @@ #include #include +#if defined(OS_WEB) + #include + #include +#endif + using namespace hex; namespace { @@ -66,6 +71,7 @@ namespace { /** * @brief Displays ImHex's splash screen and runs all initialization tasks. The splash screen will be displayed until all tasks have finished. */ + [[maybe_unused]] void initializeImHex() { init::WindowSplash splashWindow; @@ -105,6 +111,103 @@ namespace { } } + + #if defined(OS_WEB) + using namespace hex::init; + + void saveFsData() { + EM_ASM({ + FS.syncfs(function (err) { + if (!err) + return; + alert("Failed to save permanent file system: "+err); + }); + }); + } + + int runImHex() { + auto splashWindow = new WindowSplash(); + + log::info("Using '{}' GPU", ImHexApi::System::getGPUVendor()); + + // Add initialization tasks to run + TaskManager::init(); + for (const auto &[name, task, async] : init::getInitTasks()) + splashWindow->addStartupTask(name, task, async); + + splashWindow->startStartupTasks(); + + // Draw the splash window while tasks are running + emscripten_set_main_loop_arg([](void *arg) { + auto splashWindow = reinterpret_cast(arg); + + FrameResult res = splashWindow->fullFrame(); + if (res == FrameResult::success) { + handleFileOpenRequest(); + + // Clean up everything after the main window is closed + emscripten_set_beforeunload_callback(nullptr, [](int eventType, const void *reserved, void *userData) { + hex::unused(eventType, reserved, userData); + + try { + saveFsData(); + deinitializeImHex(); + return ""; + } catch (const std::exception &ex) { + std::string *msg = new std::string("Failed to deinitialize ImHex. This is just a message warning you of this, the application has already closed, you probably can't do anything about it. Message: "); + msg->append(std::string(ex.what())); + log::fatal("{}", *msg); + return msg->c_str(); + } + }); + + // Delete splash window (do it before creating the main window so glfw destroys the window) + delete splashWindow; + + emscripten_cancel_main_loop(); + + // Main window + static Window window; + emscripten_set_main_loop([]() { + window.fullFrame(); + }, 60, 0); + } + }, splashWindow, 60, 0); + + return -1; + } + + #else + + int runImHex() { + + bool shouldRestart = false; + do { + // Register an event handler that will make ImHex restart when requested + shouldRestart = false; + EventManager::subscribe([&] { + shouldRestart = true; + }); + + initializeImHex(); + handleFileOpenRequest(); + + // Clean up everything after the main window is closed + ON_SCOPE_EXIT { + deinitializeImHex(); + }; + + // Main window + Window window; + window.loop(); + + } while (shouldRestart); + + return EXIT_SUCCESS; + } + + #endif + } /** @@ -127,27 +230,5 @@ int main(int argc, char **argv) { ImHexApi::System::impl::setPortableVersion(isPortableVersion()); - bool shouldRestart = false; - do { - // Register an event handler that will make ImHex restart when requested - shouldRestart = false; - EventManager::subscribe([&] { - shouldRestart = true; - }); - - initializeImHex(); - handleFileOpenRequest(); - - // Clean up everything after the main window is closed - ON_SCOPE_EXIT { - deinitializeImHex(); - }; - - // Main window - Window window; - window.loop(); - - } while (shouldRestart); - - return EXIT_SUCCESS; -} + return runImHex(); +}; diff --git a/main/gui/source/messaging/web.cpp b/main/gui/source/messaging/web.cpp new file mode 100644 index 000000000..5c6b9a4f5 --- /dev/null +++ b/main/gui/source/messaging/web.cpp @@ -0,0 +1,24 @@ +#if defined(OS_WEB) + +#include + +#include +#include + +#include "messaging.hpp" + +namespace hex::messaging { + + void sendToOtherInstance(const std::string &eventName, const std::vector &args) { + hex::unused(eventName); + hex::unused(args); + log::error("Unimplemented function 'sendToOtherInstance()' called"); + } + + // Not implemented, so lets say we are the main instance every time so events are forwarded to ourselves + bool setupNative() { + return true; + } +} + +#endif diff --git a/main/gui/source/messaging/win.cpp b/main/gui/source/messaging/win.cpp index aea59cb31..d77310882 100644 --- a/main/gui/source/messaging/win.cpp +++ b/main/gui/source/messaging/win.cpp @@ -22,13 +22,13 @@ namespace hex::messaging { // Check if the window is visible and if it's an ImHex window if (::IsWindowVisible(hWnd) == TRUE && length != 0) { if (windowName.starts_with("ImHex")) { - // it's our window, return it and stop iteration + // It's our window, return it and stop iteration *reinterpret_cast(ret) = hWnd; return FALSE; } } - // continue iteration + // Continue iteration return TRUE; }, reinterpret_cast(&imhexWindow)); @@ -69,7 +69,7 @@ namespace hex::messaging { constexpr static auto UniqueMutexId = "ImHex/a477ea68-e334-4d07-a439-4f159c683763"; - // check if an ImHex instance is already running by opening a global mutex + // Check if an ImHex instance is already running by opening a global mutex HANDLE globalMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, UniqueMutexId); if (globalMutex == nullptr) { // If no ImHex instance is running, create a new global mutex diff --git a/main/gui/source/window/linux_window.cpp b/main/gui/source/window/linux_window.cpp index 75b6ee868..457faa250 100644 --- a/main/gui/source/window/linux_window.cpp +++ b/main/gui/source/window/linux_window.cpp @@ -44,7 +44,7 @@ namespace hex { executeCmd({"zenity", "--error", "--text", message}); } else if(isFileInPath("notify-send")) { executeCmd({"notify-send", "-i", "script-error", "Error", message}); - } // hopefully one of these commands is installed + } // Hopefully one of these commands is installed } void Window::initNative() { diff --git a/main/gui/source/window/web_window.cpp b/main/gui/source/window/web_window.cpp new file mode 100644 index 000000000..0f05d3167 --- /dev/null +++ b/main/gui/source/window/web_window.cpp @@ -0,0 +1,71 @@ +#include "window.hpp" + +#if defined(OS_WEB) + +#include +#include + +// Function used by c++ to get the size of the html canvas +EM_JS(int, canvas_get_width, (), { + return Module.canvas.width; +}); + +// Function used by c++ to get the size of the html canvas +EM_JS(int, canvas_get_height, (), { + return Module.canvas.height; +}); + +// Function called by javascript +EM_JS(void, resizeCanvas, (), { + js_resizeCanvas(); +}); + +namespace hex { + + void nativeErrorMessage(const std::string &message) { + log::fatal(message); + EM_ASM({ + alert(UTF8ToString($0)); + }, message.c_str()); + } + + void Window::initNative() { + EM_ASM({ + // Save data directory + FS.mkdir("/home/web_user/.local"); + FS.mount(IDBFS, {}, '/home/web_user/.local'); + + FS.syncfs(true, function (err) { + if (!err) + return; + alert("Failed to load permanent file system: "+err); + }); + }); + } + + void Window::setupNativeWindow() { + resizeCanvas(); + } + + void Window::beginNativeWindowFrame() { + static i32 prevWidth = 0; + static i32 prevHeight = 0; + + auto width = canvas_get_width(); + auto height = canvas_get_height(); + + if (prevWidth != width || prevHeight != height) { + // Size has changed + + prevWidth = width; + prevHeight = height; + this->resize(width, height); + } + } + + void Window::endNativeWindowFrame() { + } + +} + +#endif \ No newline at end of file diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index 193c163f7..29a0c004c 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -156,6 +156,8 @@ namespace hex { } void Window::fullFrame() { + this->m_lastFrameTime = glfwGetTime(); + glfwPollEvents(); // Render frame @@ -255,7 +257,7 @@ namespace hex { ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabHovered)); - // custom titlebar buttons implementation for borderless window mode + // Custom titlebar buttons implementation for borderless window mode auto &titleBarButtons = ContentRegistry::Interface::impl::getTitleBarButtons(); // Draw custom title bar buttons @@ -812,6 +814,8 @@ namespace hex { glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_SAMPLES, 1); if (restoreWindowPos) { int maximized = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.window.maximized", GLFW_FALSE); @@ -924,30 +928,32 @@ namespace hex { win->processEvent(); }); - // Register key press callback - glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) { - hex::unused(mods); + #if !defined(OS_WEB) + // Register key press callback + glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) { + hex::unused(mods); - auto win = static_cast(glfwGetWindowUserPointer(window)); + auto win = static_cast(glfwGetWindowUserPointer(window)); - if (action == GLFW_RELEASE) { - win->m_buttonDown = false; - } else { - win->m_buttonDown = true; - } + if (action == GLFW_RELEASE) { + win->m_buttonDown = false; + } else { + win->m_buttonDown = true; + } - if (key == GLFW_KEY_UNKNOWN) return; + if (key == GLFW_KEY_UNKNOWN) return; - auto keyName = glfwGetKeyName(key, scancode); - if (keyName != nullptr) - key = std::toupper(keyName[0]); + auto keyName = glfwGetKeyName(key, scancode); + if (keyName != nullptr) + key = std::toupper(keyName[0]); - if (action == GLFW_PRESS || action == GLFW_REPEAT) { - win->m_pressedKeys.push_back(key); - } + if (action == GLFW_PRESS || action == GLFW_REPEAT) { + win->m_pressedKeys.push_back(key); + } - win->processEvent(); - }); + win->processEvent(); + }); + #endif // Register cursor position callback glfwSetCursorPosCallback(this->m_window, [](GLFWwindow *window, double x, double y) { @@ -995,6 +1001,10 @@ namespace hex { glfwShowWindow(this->m_window); } + void Window::resize(i32 width, i32 height) { + glfwSetWindowSize(this->m_window, width, height); + } + void Window::initImGui() { IMGUI_CHECKVERSION(); @@ -1094,10 +1104,13 @@ namespace hex { } } + ImGui_ImplGlfw_InitForOpenGL(this->m_window, true); #if defined(OS_MACOS) ImGui_ImplOpenGL3_Init("#version 150"); + #elif defined(OS_WEB) + ImGui_ImplOpenGL3_Init(); #else ImGui_ImplOpenGL3_Init("#version 130"); #endif diff --git a/plugins/builtin/include/content/helpers/diagrams.hpp b/plugins/builtin/include/content/helpers/diagrams.hpp index c6800feb3..43b6ca5f5 100644 --- a/plugins/builtin/include/content/helpers/diagrams.hpp +++ b/plugins/builtin/include/content/helpers/diagrams.hpp @@ -183,7 +183,7 @@ namespace hex { private: size_t m_sampleSize; - // The number of byte processed and the size of + // The number of bytes processed and the size of // the file to analyze (useful for iterative analysis) u64 m_byteCount; u64 m_fileSize; @@ -276,7 +276,7 @@ namespace hex { private: size_t m_sampleSize; - // The number of byte processed and the size of + // The number of bytes processed and the size of // the file to analyze (useful for iterative analysis) u64 m_byteCount; u64 m_fileSize; @@ -313,7 +313,7 @@ namespace hex { ImPlot::PlotLine("##ChunkBasedAnalysisLine", this->m_xBlockEntropy.data(), this->m_yBlockEntropySampled.data(), this->m_xBlockEntropy.size()); // The parameter updateHandle is used when using the pattern language since we don't have a provider - // but just a set of bytes we won't be able to use the drag bar correctly. + // but just a set of bytes, we won't be able to use the drag bar correctly. if (updateHandle) { // Set a draggable line on the plot if (ImPlot::DragLineX(1, &this->m_handlePosition, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { @@ -427,7 +427,7 @@ namespace hex { } // Method used to compute the entropy of a block of size `blockSize` - // using the bytes occurrences from `valueCounts` array. + // using the byte occurrences from `valueCounts` array. double calculateEntropy(std::array &valueCounts, size_t blockSize) { double entropy = 0; @@ -560,8 +560,8 @@ namespace hex { // Position of the handle inside the plot double m_handlePosition = 0.0; - // Hold the number of block that have been processed - // during the chunk based entropy analysis + // Hold the number of blocks that have been processed + // during the chunk-based entropy analysis u64 m_blockCount; // Hold the number of bytes that have been processed @@ -572,7 +572,7 @@ namespace hex { // (useful for the iterative analysis) std::array m_blockValueCounts; - // Variable to hold the result of the chunk based + // Variable to hold the result of the chunk-based // entropy analysis std::vector m_xBlockEntropy; std::vector m_yBlockEntropy, m_yBlockEntropySampled; @@ -708,7 +708,7 @@ namespace hex { } // The parameter updateHandle is used when using the pattern language since we don't have a provider - // but just a set of bytes we won't be able to use the drag bar correctly. + // but just a set of bytes, we won't be able to use the drag bar correctly. if (updateHandle) { // Set a draggable line on the plot if (ImPlot::DragLineX(1, &this->m_handlePosition, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { @@ -934,8 +934,8 @@ namespace hex { // Position of the handle inside the plot double m_handlePosition = 0.0; - // Hold the number of block that have been processed - // during the chunk based entropy analysis + // Hold the number of blocks that have been processed + // during the chunk-based entropy analysis u64 m_blockCount; // Hold the number of bytes that have been processed @@ -950,7 +950,7 @@ namespace hex { // (useful for the iterative analysis) std::array m_blockValueCounts; - // The m_xBlockTypeDistributions attributes is used to specify the position of + // The m_xBlockTypeDistributions attributes are used to specify the position of // the values in the plot when the Y axis doesn't start at 0 std::vector m_xBlockTypeDistributions; // Hold the result of the byte distribution analysis diff --git a/plugins/builtin/include/content/popups/popup_file_chooser.hpp b/plugins/builtin/include/content/popups/popup_file_chooser.hpp index 67b2121f5..e6254f2b5 100644 --- a/plugins/builtin/include/content/popups/popup_file_chooser.hpp +++ b/plugins/builtin/include/content/popups/popup_file_chooser.hpp @@ -11,7 +11,7 @@ namespace hex::plugin::builtin { class PopupFileChooser : public Popup { public: - PopupFileChooser(const std::vector &files, const std::vector &validExtensions, bool multiple, const std::function &callback) + PopupFileChooser(const std::vector &files, const std::vector &validExtensions, bool multiple, const std::function &callback) : hex::Popup("hex.builtin.common.choose_file"), m_indices({ }), m_files(files), m_openCallback(callback), @@ -80,7 +80,7 @@ namespace hex::plugin::builtin { std::set m_indices; std::vector m_files; std::function m_openCallback; - std::vector m_validExtensions; + std::vector m_validExtensions; bool m_multiple = false; }; diff --git a/plugins/builtin/include/content/providers/disk_provider.hpp b/plugins/builtin/include/content/providers/disk_provider.hpp index cd0c2acb5..dc0816953 100644 --- a/plugins/builtin/include/content/providers/disk_provider.hpp +++ b/plugins/builtin/include/content/providers/disk_provider.hpp @@ -1,4 +1,5 @@ #pragma once +#if !defined(OS_WEB) #include @@ -72,4 +73,5 @@ namespace hex::plugin::builtin { bool m_writable = false; }; -} \ No newline at end of file +} +#endif diff --git a/plugins/builtin/include/content/views/view_pattern_data.hpp b/plugins/builtin/include/content/views/view_pattern_data.hpp index c6c0a3947..d6d178550 100644 --- a/plugins/builtin/include/content/views/view_pattern_data.hpp +++ b/plugins/builtin/include/content/views/view_pattern_data.hpp @@ -19,7 +19,7 @@ namespace hex::plugin::builtin { void drawContent() override; private: - ui::PatternDrawer m_patternDrawer; + std::unique_ptr m_patternDrawer; }; } \ No newline at end of file diff --git a/plugins/builtin/include/content/views/view_pattern_editor.hpp b/plugins/builtin/include/content/views/view_pattern_editor.hpp index 24a21550b..6d584844f 100644 --- a/plugins/builtin/include/content/views/view_pattern_editor.hpp +++ b/plugins/builtin/include/content/views/view_pattern_editor.hpp @@ -156,7 +156,7 @@ namespace hex::plugin::builtin { bool m_syncPatternSourceCode = false; bool m_autoLoadPatterns = true; - std::map> m_sectionWindowDrawer; + std::map> m_sectionWindowDrawer; ui::HexEditor m_sectionHexEditor; @@ -175,7 +175,7 @@ namespace hex::plugin::builtin { PerProvider m_shouldAnalyze; PerProvider m_breakpointHit; - PerProvider m_debuggerDrawer; + PerProvider> m_debuggerDrawer; std::atomic m_resetDebuggerVariables; int m_debuggerScopeIndex = 0; diff --git a/plugins/builtin/include/ui/pattern_drawer.hpp b/plugins/builtin/include/ui/pattern_drawer.hpp index 7935f0937..f6ede7290 100644 --- a/plugins/builtin/include/ui/pattern_drawer.hpp +++ b/plugins/builtin/include/ui/pattern_drawer.hpp @@ -20,6 +20,8 @@ namespace hex::plugin::builtin::ui { this->m_formatters = pl::gen::fmt::createFormatters(); } + virtual ~PatternDrawer() = default; + void draw(const std::vector> &patterns, pl::PatternLanguage *runtime = nullptr, float height = 0.0F); enum class TreeStyle { diff --git a/plugins/builtin/source/content/data_inspector.cpp b/plugins/builtin/source/content/data_inspector.cpp index 863221aa4..a3e051408 100644 --- a/plugins/builtin/source/content/data_inspector.cpp +++ b/plugins/builtin/source/content/data_inspector.cpp @@ -275,9 +275,10 @@ namespace hex::plugin::builtin { auto format = (style == Style::Decimal) ? "{0}{1:d}" : ((style == Style::Hexadecimal) ? "{0}0x{1:X}" : "{0}0o{1:o}"); + auto number = hex::crypt::decodeSleb128(buffer); bool negative = number < 0; - auto value = hex::format(format, negative ? "-" : "", std::abs(number)); + auto value = hex::format(format, negative ? "-" : "", negative ? -number : number); return [value] { ImGui::TextUnformatted(value.c_str()); return value; }; }, diff --git a/plugins/builtin/source/content/helpers/math_evaluator.cpp b/plugins/builtin/source/content/helpers/math_evaluator.cpp index ec721ccb0..338bc6693 100644 --- a/plugins/builtin/source/content/helpers/math_evaluator.cpp +++ b/plugins/builtin/source/content/helpers/math_evaluator.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace hex { diff --git a/plugins/builtin/source/content/pl_visualizers.cpp b/plugins/builtin/source/content/pl_visualizers.cpp index 6bc3f855a..871fe2716 100644 --- a/plugins/builtin/source/content/pl_visualizers.cpp +++ b/plugins/builtin/source/content/pl_visualizers.cpp @@ -9,7 +9,16 @@ #include #include -#include + +#if defined(OS_WEB) + #define GLFW_INCLUDE_ES3 + #include +#else + #include +#endif + +#include + #include #include @@ -476,17 +485,17 @@ namespace hex::plugin::builtin { } void drawChunkBasedEntropyVisualizer(pl::ptrn::Pattern &, pl::ptrn::IIterable &, bool shouldReset, std::span arguments) { - // variable used to store the result to avoid having to recalculate the result at each frame + // Variable used to store the result to avoid having to recalculate the result at each frame static DiagramChunkBasedEntropyAnalysis analyzer; - // compute data + // Compute data if (shouldReset) { auto pattern = arguments[0].toPattern(); auto chunkSize = arguments[1].toUnsigned(); analyzer.process(pattern->getBytes(), chunkSize); } - // show results + // Show results analyzer.draw(ImVec2(400, 250), ImPlotFlags_NoChild | ImPlotFlags_CanvasOnly); } diff --git a/plugins/builtin/source/content/project.cpp b/plugins/builtin/source/content/project.cpp index a733f4d9d..7c27f3b6f 100644 --- a/plugins/builtin/source/content/project.cpp +++ b/plugins/builtin/source/content/project.cpp @@ -66,7 +66,7 @@ namespace hex::plugin::builtin { for (const auto &handler : ProjectFile::getHandlers()) { bool result = true; - // handlers are supposed to show the error/warning popup to the user themselves, so we don't show one here + // Handlers are supposed to show the error/warning popup to the user themselves, so we don't show one here try { if (!handler.load(handler.basePath, tar)) { log::warn("Project file handler for {} failed to load {}", filePath.string(), handler.basePath.string()); @@ -157,14 +157,15 @@ namespace hex::plugin::builtin { ImHexApi::Provider::resetDirty(); - // if saveLocation is false, reset the project path (do not release the lock) + // If saveLocation is false, reset the project path (do not release the lock) if (updateLocation) { resetPath.release(); } AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.save_project.name"); - EventManager::post(); // request, as this puts us into a project state + // Request, as this puts us into a project state + EventManager::post(); return result; } diff --git a/plugins/builtin/source/content/providers.cpp b/plugins/builtin/source/content/providers.cpp index f41b190cf..69b27fce1 100644 --- a/plugins/builtin/source/content/providers.cpp +++ b/plugins/builtin/source/content/providers.cpp @@ -25,7 +25,9 @@ namespace hex::plugin::builtin { ContentRegistry::Provider::add(false); ContentRegistry::Provider::add(false); + #if !defined(OS_WEB) ContentRegistry::Provider::add(); + #endif ContentRegistry::Provider::add(); ContentRegistry::Provider::add(); ContentRegistry::Provider::add(); @@ -59,7 +61,7 @@ namespace hex::plugin::builtin { }; if (provider == nullptr) { - // if a provider is not created, it will be overwritten when saving the project, + // If a provider is not created, it will be overwritten when saving the project, // so we should prevent the project from loading at all showError(hex::format("hex.builtin.popup.error.project.load"_lang, hex::format("hex.builtin.popup.error.project.load.create_provider"_lang, providerType) @@ -91,7 +93,7 @@ namespace hex::plugin::builtin { hex::format("\n - {} : {}", warning.first->getName(), warning.second)); } - // if no providers were opened, display an error with + // If no providers were opened, display an error with // the warnings that happened when opening them if (ImHexApi::Provider::getProviders().size() == 0) { showError(hex::format("hex.builtin.popup.error.project.load"_lang, @@ -100,7 +102,7 @@ namespace hex::plugin::builtin { return false; } else { - // else, if are warnings, still display them + // Else, if are warnings, still display them if (warningMsg.empty()) return true; else { showWarning( diff --git a/plugins/builtin/source/content/providers/disk_provider.cpp b/plugins/builtin/source/content/providers/disk_provider.cpp index 3168f51cb..f021442bf 100644 --- a/plugins/builtin/source/content/providers/disk_provider.cpp +++ b/plugins/builtin/source/content/providers/disk_provider.cpp @@ -1,3 +1,4 @@ +#if !defined(OS_WEB) #include #include "content/providers/disk_provider.hpp" @@ -105,7 +106,7 @@ namespace hex::plugin::builtin { return -1; if (st.st_size == 0) { - // try BLKGETSIZE + // Try BLKGETSIZE unsigned long long bytes64; if (ioctl(fd, BLKGETSIZE, &bytes64) >= 0) { *bytes = bytes64; @@ -453,4 +454,5 @@ namespace hex::plugin::builtin { return Provider::queryInformation(category, argument); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/plugins/builtin/source/content/recent.cpp b/plugins/builtin/source/content/recent.cpp index 04e04d310..597ac94e0 100644 --- a/plugins/builtin/source/content/recent.cpp +++ b/plugins/builtin/source/content/recent.cpp @@ -30,10 +30,10 @@ namespace hex::plugin::builtin::recent { if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.save_recent_providers", 1) == 1) { auto fileName = hex::format("{:%y%m%d_%H%M%S}.json", fmt::gmtime(std::chrono::system_clock::now())); - // do not save to recents if the provider is part of a project + // Do not save to recents if the provider is part of a project if (ProjectFile::hasPath()) return; - // do not save to recents if the provider doesnt want it + // Do not save to recents if the provider doesnt want it if (!provider->isSavableAsRecent()) return; // The recent provider is saved to every "recent" directory @@ -57,7 +57,7 @@ namespace hex::plugin::builtin::recent { updateRecentEntries(); }); - // save opened projects as a "recent" shortcut + // Save opened projects as a "recent" shortcut (void)EventManager::subscribe([] { if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.save_recent_providers", 1) == 1) { auto fileName = hex::format("{:%y%m%d_%H%M%S}.json", fmt::gmtime(std::chrono::system_clock::now())); @@ -208,7 +208,7 @@ namespace hex::plugin::builtin::recent { ImGui::EndPopup(); } - // handle deletion from vector and on disk + // Handle deletion from vector and on disk if (shouldRemove) { wolv::io::fs::remove(recentEntry.entryFilePath); it = s_recentEntries.erase(it); diff --git a/plugins/builtin/source/content/settings_entries.cpp b/plugins/builtin/source/content/settings_entries.cpp index ea5e6b9aa..0a914cbc8 100644 --- a/plugins/builtin/source/content/settings_entries.cpp +++ b/plugins/builtin/source/content/settings_entries.cpp @@ -211,7 +211,8 @@ namespace hex::plugin::builtin { static auto lang = std::string(setting); if (ImGui::InputText(name.data(), lang, ImGuiInputTextFlags_CharsNoBlank)) { - setting = std::string(lang.c_str()); // remove following zero bytes + // Remove trailing null bytes + setting = std::string(lang.c_str()); return true; } diff --git a/plugins/builtin/source/content/tools_entries.cpp b/plugins/builtin/source/content/tools_entries.cpp index 179c84a2f..5667e205f 100644 --- a/plugins/builtin/source/content/tools_entries.cpp +++ b/plugins/builtin/source/content/tools_entries.cpp @@ -1133,17 +1133,17 @@ namespace hex::plugin::builtin { } } - // Tool for converting between different number formats - // There are three places where input can be changed; the bit checkboxes, the hex input and the decimal input. + // Tool for converting between different number formats. + // There are three places where input can be changed; the bit checkboxes, the hex input, and the decimal input. // The bit checkboxes and the hex input are directly related and can be converted between each other easily. // The decimal input is a bit more complicated. IEEE 754 floating point numbers are represented as a sign bit, // an exponent and a mantissa. For details see https://en.wikipedia.org/wiki/IEEE_754. // Workflow is as follows: // From the bit checkboxes determine the integer hex value. This is straightforward. - // From the hex value determine the binary floating point value by extracting the sign, exponent and mantissa. - // From the binary floating point value determine the decimal floating point value using third party library. + // From the hex value determine the binary floating point value by extracting the sign, exponent, and mantissa. + // From the binary floating point value determine the decimal floating point value using a third party library. // From the decimal floating point we reconstruct the binary floating point value using internal hardware. - // If format is non-standard the reconstruction is done using properties of the format. + // If the format is non-standard, the reconstruction is done using properties of the format. void drawIEEE754Decoder() { constexpr static auto flags = ImGuiInputTextFlags_EnterReturnsTrue; @@ -1235,16 +1235,16 @@ namespace hex::plugin::builtin { const static auto BitsToFloat = [](IEEE754 &ieee754) { // Zero or denormal if (ieee754.exponentBits == 0) { - // result doesn't fit in 128 bits + // Result doesn't fit in 128 bits if ((ieee754.exponentBias - 1) > 128) ieee754.exponentValue = std::pow(2.0L, static_cast(-ieee754.exponentBias + 1)); else { if (ieee754.exponentBias == 0) { - // exponent is zero + // Exponent is zero if (ieee754.mantissaBits == 0) ieee754.exponentValue = 1.0; else - // exponent is one + // Exponent is one ieee754.exponentValue = 2.0; } else @@ -1253,18 +1253,18 @@ namespace hex::plugin::builtin { } // Normal else { - // result doesn't fit in 128 bits + // Result doesn't fit in 128 bits if (std::abs(ieee754.exponentBits - ieee754.exponentBias) > 128) ieee754.exponentValue = std::pow(2.0L, static_cast(ieee754.exponentBits - ieee754.exponentBias)); - //result fits in 128 bits + // Result fits in 128 bits else { - // exponent is positive + // Exponent is positive if (ieee754.exponentBits > ieee754.exponentBias) ieee754.exponentValue = static_cast(u128(1) << (ieee754.exponentBits - ieee754.exponentBias)); - // exponent is negative + // Exponent is negative else if (ieee754.exponentBits < ieee754.exponentBias) ieee754.exponentValue = 1.0 / static_cast(u128(1) << (ieee754.exponentBias - ieee754.exponentBits)); - // exponent is zero + // Exponent is zero else ieee754.exponentValue = 1.0; } } @@ -1275,7 +1275,7 @@ namespace hex::plugin::builtin { // Check if all exponent bits are set. if (std::popcount(static_cast(ieee754.exponentBits)) == static_cast(ieee754statics.exponentBitCount)) { - // if fraction is zero number is infinity. + // If fraction is zero number is infinity. if (ieee754.mantissaBits == 0) { if (ieee754.signBits == 0) { @@ -1290,7 +1290,7 @@ namespace hex::plugin::builtin { } ieee754.numberType = NumberType::Infinity; - // otherwise number is NaN. + // Otherwise number is NaN. } else { if (ieee754.mantissaBits & (u128(1) << (ieee754statics.mantissaBitCount - 1))) { @@ -1305,9 +1305,9 @@ namespace hex::plugin::builtin { } ieee754.numberType = NumberType::NaN; } - // if all exponent bits are zero, but we have a non-zero fraction - // then the number is denormal which are smaller than regular numbers - // but not as precise. + // If all exponent bits are zero, but we have a non-zero fraction, + // then the number is denormal. + // These are smaller than regular numbers but not as precise. } else if (ieee754.exponentBits == 0 && ieee754.mantissaBits != 0) { ieee754.numberType = NumberType::Denormal; @@ -1336,7 +1336,7 @@ namespace hex::plugin::builtin { // Sign ImGui::TableNextColumn(); - // this has the effect of dimming the color of the numbers so user doesn't try + // This has the effect of dimming the color of the numbers so user doesn't try // to interact with them. ImVec4 textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); ImGui::BeginDisabled(); @@ -1350,7 +1350,7 @@ namespace hex::plugin::builtin { ImGui::Text("+1"); ImGui::Unindent(20_scaled); - //times + // Times ImGui::TableNextColumn(); ImGui::Text("x"); ImGui::TableNextColumn(); @@ -1376,7 +1376,7 @@ namespace hex::plugin::builtin { ImGui::Unindent(20_scaled); - //times + // Times ImGui::TableNextColumn(); ImGui::Text("x"); ImGui::TableNextColumn(); @@ -1510,22 +1510,21 @@ namespace hex::plugin::builtin { ImGui::Unindent(indent); }; - const static auto FloatToBits = [&specialNumbers](IEEE754 &ieee754, std::string decimalFloatingPointNumberString - , std::string_view decimalStrView, std::from_chars_result &res,int totalBitCount) { + const static auto FloatToBits = [&specialNumbers](IEEE754 &ieee754, std::string decimalFloatingPointNumberString, int totalBitCount) { // Always obtain sign first. if (decimalFloatingPointNumberString[0] == '-') { - // and remove it from the string. + // And remove it from the string. ieee754.signBits = 1; decimalFloatingPointNumberString.erase(0, 1); } else - //important to switch from - to +. + // Important to switch from - to +. ieee754.signBits = 0; InputType inputType; bool matchFound = false; i32 i; - // detect and use special numbers. + // Detect and use special numbers. for (i = 0; i < 12; i++) { if (decimalFloatingPointNumberString == specialNumbers[i]) { inputType = InputType(i/3); @@ -1538,10 +1537,9 @@ namespace hex::plugin::builtin { inputType = InputType::regular; if (inputType == InputType::regular) { - decimalStrView = decimalFloatingPointNumberString; - res = std::from_chars(decimalStrView.data(), decimalStrView.data() + decimalStrView.size(), ieee754statics.resultFloat); - // this is why we use from_chars - if (res.ec != std::errc()) { + try { + ieee754statics.resultFloat = stod(decimalFloatingPointNumberString); + } catch(const std::invalid_argument& _) { inputType = InputType::invalid; } } else if (inputType == InputType::infinity) { @@ -1560,7 +1558,7 @@ namespace hex::plugin::builtin { long double log2Result; if (inputType != InputType::invalid) { - // deal with zero first so we can use log2. + // Deal with zero first so we can use log2. if (ieee754statics.resultFloat == 0.0) { if (ieee754.signBits == 1) ieee754statics.resultFloat = -0.0; @@ -1638,13 +1636,13 @@ namespace hex::plugin::builtin { }; const static auto ToolMenu = [](i64 &inputFieldWidth) { - // we are done. The rest selects the format if user interacts with the widgets. - // If precision and exponent match one of the IEEE 754 formats the format is highlighted - // and remains highlighted until user changes to a different format. Matching formats occur when + // We are done. The rest selects the format if user interacts with the widgets. + // If precision and exponent match one of the IEEE 754 formats, the format is highlighted + // and remains highlighted until the user changes to a different format. Matching formats occur when // the user clicks on one of the selections or if the slider values match the format in question. - // when a new format is selected it may have a smaller number of digits than + // When a new format is selected, it may have a smaller number of digits than // the previous selection. Since the largest of the hexadecimal and the decimal - // representation widths sets both field widths to the same value we need to + // representation widths set both field widths to the same value, we need to // reset it here when a new choice is set. auto exponentBitCount = ieee754statics.exponentBitCount; @@ -1705,7 +1703,7 @@ namespace hex::plugin::builtin { needsPop = false; if (ImGui::Button("hex.builtin.tools.ieee754.clear"_lang)) - //this will reset all interactive widgets to zero. + // This will reset all interactive widgets to zero. ieee754statics.value = 0; ImGui::Separator(); @@ -1789,11 +1787,11 @@ namespace hex::plugin::builtin { ieee754.precision = std::ceil(1+(ieee754statics.mantissaBitCount + 1) * std::log10(2.0L)); // For C++ from_chars is better than strtold. - // the main problem is that from_chars will not process special numbers + // The main problem is that from_chars will not process special numbers // like inf and nan, so we handle them manually static std::string decimalFloatingPointNumberString; - static std::string_view decimalStrView; - // use qnan for quiet NaN and snan for signaling NaN + + // Use qnan for quiet NaN and snan for signaling NaN if (ieee754.numberType == NumberType::NaN) { if (ieee754.valueType == ValueType::QuietNaN) decimalFloatingPointNumberString = "qnan"; @@ -1809,9 +1807,8 @@ namespace hex::plugin::builtin { // We allow any input in order to accept infinities and NaNs, all invalid entries // are detected by from_chars. You can also enter -0 or -inf. - std::from_chars_result res; if (ImGui::InputText("##resultFloat", decimalFloatingPointNumberString, flags)) { - FloatToBits(ieee754, decimalFloatingPointNumberString, decimalStrView, res, totalBitCount); + FloatToBits(ieee754, decimalFloatingPointNumberString, totalBitCount); } ImGui::PopItemWidth(); diff --git a/plugins/builtin/source/content/views/view_hex_editor.cpp b/plugins/builtin/source/content/views/view_hex_editor.cpp index 9194a91bd..45b5a3164 100644 --- a/plugins/builtin/source/content/views/view_hex_editor.cpp +++ b/plugins/builtin/source/content/views/view_hex_editor.cpp @@ -1066,7 +1066,7 @@ namespace hex::plugin::builtin { } } - PopupFileChooser::open(paths, std::vector{ {"Thingy Table File", "tbl"} }, false, + PopupFileChooser::open(paths, std::vector{ {"Thingy Table File", "tbl"} }, false, [this](const auto &path) { TaskManager::createTask("Loading encoding file", 0, [this, path](auto&) { auto encoding = EncodingFile(EncodingFile::Type::Thingy, path); diff --git a/plugins/builtin/source/content/views/view_information.cpp b/plugins/builtin/source/content/views/view_information.cpp index 4f579ce5d..b9029140d 100644 --- a/plugins/builtin/source/content/views/view_information.cpp +++ b/plugins/builtin/source/content/views/view_information.cpp @@ -112,8 +112,8 @@ namespace hex::plugin::builtin { u64 count = 0; - // Loop over each byte of the [part of the] file and update each analysis - // one byte at the time in order to process the file only once + // Loop over each byte of the selection and update each analysis + // one byte at a time in order to process the file only once for (u8 byte : reader) { this->m_byteDistribution.update(byte); this->m_byteTypesDistribution.update(byte); diff --git a/plugins/builtin/source/content/views/view_pattern_data.cpp b/plugins/builtin/source/content/views/view_pattern_data.cpp index e0d981e01..9e4a7b3c2 100644 --- a/plugins/builtin/source/content/views/view_pattern_data.cpp +++ b/plugins/builtin/source/content/views/view_pattern_data.cpp @@ -9,27 +9,29 @@ namespace hex::plugin::builtin { ViewPatternData::ViewPatternData() : View("hex.builtin.view.pattern_data.name") { + this->m_patternDrawer = std::make_unique(); + // Handle tree style setting changes EventManager::subscribe(this, [this]() { auto patternStyle = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.pattern_tree_style", 0); - this->m_patternDrawer.setTreeStyle(static_cast(patternStyle)); + this->m_patternDrawer->setTreeStyle(static_cast(patternStyle)); }); // Reset the pattern drawer when the provider changes EventManager::subscribe(this, [this](auto, auto) { - this->m_patternDrawer.reset(); + this->m_patternDrawer->reset(); }); EventManager::subscribe(this, [this]{ - this->m_patternDrawer.reset(); + this->m_patternDrawer->reset(); }); EventManager::subscribe(this, [this](auto){ - this->m_patternDrawer.reset(); + this->m_patternDrawer->reset(); }); // Handle jumping to a pattern's location when it is clicked - this->m_patternDrawer.setSelectionCallback([](Region region){ ImHexApi::HexEditor::setSelection(region); }); + this->m_patternDrawer->setSelectionCallback([](Region region){ ImHexApi::HexEditor::setSelection(region); }); } ViewPatternData::~ViewPatternData() { @@ -46,11 +48,11 @@ namespace hex::plugin::builtin { // Make sure the runtime has finished evaluating and produced valid patterns auto &runtime = ContentRegistry::PatternLanguage::getRuntime(); if (!runtime.arePatternsValid()) { - this->m_patternDrawer.draw({ }); + this->m_patternDrawer->draw({ }); } else { // If the runtime has finished evaluating, draw the patterns if (TRY_LOCK(ContentRegistry::PatternLanguage::getRuntimeLock())) { - this->m_patternDrawer.draw(runtime.getPatterns(), &runtime); + this->m_patternDrawer->draw(runtime.getPatterns(), &runtime); } } } diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index 155d3574c..4357d98a1 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -521,7 +521,7 @@ namespace hex::plugin::builtin { ImGui::TextFormatted("{} | 0x{:02X}", hex::toByteString(section.data.size()), section.data.size()); ImGui::TableNextColumn(); if (ImGui::IconButton(ICON_VS_OPEN_PREVIEW, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { - auto dataProvider = std::make_unique(); + auto dataProvider = std::make_shared(); dataProvider->resize(section.data.size()); dataProvider->writeRaw(0x00, section.data.data(), section.data.size()); dataProvider->setReadOnly(true); @@ -551,10 +551,10 @@ namespace hex::plugin::builtin { auto patternProvider = ImHexApi::Provider::get(); - this->m_sectionWindowDrawer[patternProvider] = [this, id, patternProvider, dataProvider = std::move(dataProvider), hexEditor, patternDrawer = ui::PatternDrawer(), &runtime] mutable { + this->m_sectionWindowDrawer[patternProvider] = [this, id, patternProvider, dataProvider, hexEditor, patternDrawer = std::make_shared(), &runtime] mutable { hexEditor.setProvider(dataProvider.get()); hexEditor.draw(480_scaled); - patternDrawer.setSelectionCallback([&](const auto ®ion) { + patternDrawer->setSelectionCallback([&](const auto ®ion) { hexEditor.setSelection(region); }); @@ -568,7 +568,7 @@ namespace hex::plugin::builtin { }(); if (*this->m_executionDone) - patternDrawer.draw(patterns, &runtime, 150_scaled); + patternDrawer->draw(patterns, &runtime, 150_scaled); }; } @@ -635,7 +635,7 @@ namespace hex::plugin::builtin { if (this->m_resetDebuggerVariables) { auto pauseLine = evaluator->getPauseLine(); - this->m_debuggerDrawer->reset(); + (*this->m_debuggerDrawer)->reset(); this->m_resetDebuggerVariables = false; this->m_textEditor.SetCursorPosition(TextEditor::Coordinates(pauseLine.value_or(0) - 1, 0)); @@ -644,7 +644,7 @@ namespace hex::plugin::builtin { } auto &currScope = evaluator->getScope(-this->m_debuggerScopeIndex); - this->m_debuggerDrawer->draw(*currScope.scope, &runtime, size.y - ImGui::GetTextLineHeightWithSpacing() * 4); + (*this->m_debuggerDrawer)->draw(*currScope.scope, &runtime, size.y - ImGui::GetTextLineHeightWithSpacing() * 4); } } ImGui::EndChild(); @@ -791,7 +791,11 @@ namespace hex::plugin::builtin { continue; try { - runtime.getInternals().preprocessor->preprocess(runtime, file.readString()); + auto &preprocessor = runtime.getInternals().preprocessor; + auto ret = preprocessor->preprocess(runtime, file.readString()); + if (!ret.has_value()) { + log::warn("Failed to preprocess file {} during MIME analysis: {}", entry.path().string(), (*preprocessor->getError()).what()); + } } catch (pl::core::err::PreprocessorError::Exception &e) { log::warn("Failed to preprocess file {} during MIME analysis: {}", entry.path().string(), e.what()); } @@ -1065,6 +1069,10 @@ namespace hex::plugin::builtin { } }); + EventManager::subscribe(this, [this](prv::Provider *provider) { + this->m_debuggerDrawer.get(provider) = std::make_unique(); + }); + EventManager::subscribe(this, [this](prv::Provider *) { if (this->m_syncPatternSourceCode && ImHexApi::Provider::getProviders().empty()) { this->m_textEditor.SetText(""); @@ -1124,7 +1132,7 @@ namespace hex::plugin::builtin { } } - PopupFileChooser::open(paths, std::vector{ { "Pattern File", "hexpat" } }, false, + PopupFileChooser::open(paths, std::vector{ { "Pattern File", "hexpat" } }, false, [this, provider](const std::fs::path &path) { this->loadPatternFile(path, provider); AchievementManager::unlockAchievement("hex.builtin.achievement.patterns", "hex.builtin.achievement.patterns.load_existing.name"); diff --git a/plugins/builtin/source/content/views/view_store.cpp b/plugins/builtin/source/content/views/view_store.cpp index 08edbd0be..eab4a0385 100644 --- a/plugins/builtin/source/content/views/view_store.cpp +++ b/plugins/builtin/source/content/views/view_store.cpp @@ -79,7 +79,7 @@ namespace hex::plugin::builtin { ImGui::EndTooltip(); } ImGui::TableNextColumn(); - // the space makes a padding in the UI + // The space makes a padding in the UI ImGui::Text("%s ", wolv::util::combineStrings(entry.authors, ", ").c_str()); ImGui::TableNextColumn(); @@ -181,7 +181,7 @@ namespace hex::plugin::builtin { } void ViewStore::refresh() { - // do not refresh if a refresh is already in progress + // Do not refresh if a refresh is already in progress if (this->m_requestStatus == RequestStatus::InProgress) return; this->m_requestStatus = RequestStatus::InProgress; @@ -275,7 +275,7 @@ namespace hex::plugin::builtin { if (!fs::isPathWritable(folderPath)) continue; - // verify that we write the file to the right folder + // Verify that we write the file to the right folder // this is to prevent the filename from having elements like ../ auto fullPath = std::fs::weakly_canonical(folderPath / std::fs::path(fileName)); auto [folderIter, pathIter] = std::mismatch(folderPath.begin(), folderPath.end(), fullPath.begin()); diff --git a/plugins/builtin/source/content/views/view_yara.cpp b/plugins/builtin/source/content/views/view_yara.cpp index 70b5ffc3f..2ceb2f8e0 100644 --- a/plugins/builtin/source/content/views/view_yara.cpp +++ b/plugins/builtin/source/content/views/view_yara.cpp @@ -126,7 +126,7 @@ namespace hex::plugin::builtin { } } - PopupFileChooser::open(paths, std::vector{ { "Yara File", "yara" }, { "Yara File", "yar" } }, true, + PopupFileChooser::open(paths, std::vector{ { "Yara File", "yara" }, { "Yara File", "yar" } }, true, [&](const auto &path) { this->m_rules->push_back({ path.filename(), path }); }); diff --git a/plugins/builtin/source/content/welcome_screen.cpp b/plugins/builtin/source/content/welcome_screen.cpp index dd46eccd2..26c393f07 100644 --- a/plugins/builtin/source/content/welcome_screen.cpp +++ b/plugins/builtin/source/content/welcome_screen.cpp @@ -202,7 +202,7 @@ namespace hex::plugin::builtin { ImGui::EndPopup(); } - // draw recent entries + // Draw recent entries recent::draw(); if (ImHexApi::System::getInitArguments().contains("update-available")) { @@ -440,11 +440,15 @@ namespace hex::plugin::builtin { }); EventManager::subscribe([] { - // documentation of the value above the setting definition + // Documentation of the value above the setting definition auto allowServerContact = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.server_contact", 2); if (allowServerContact == 2) { ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.server_contact", 0); - PopupTelemetryRequest::open(); + + // Open the telemetry popup but only on desktop versions + #if !defined(OS_WEB) + PopupTelemetryRequest::open(); + #endif } }); @@ -486,10 +490,11 @@ namespace hex::plugin::builtin { bool hasBackupFile = wolv::io::fs::exists(backupFilePath); PopupRestoreBackup::open( - // path of log file + // Path of log file crashFileData.value("logFile", ""), - // restore callback - [=]{ + + // Restore callback + [=] { if (hasBackupFile) { ProjectFile::load(backupFilePath); if (hasProject) { @@ -504,8 +509,9 @@ namespace hex::plugin::builtin { } } }, - // delete callback (also executed after restore) - [crashFilePath, backupFilePath]{ + + // Delete callback (also executed after restore) + [crashFilePath, backupFilePath] { wolv::io::fs::remove(crashFilePath); wolv::io::fs::remove(backupFilePath); } diff --git a/plugins/script_loader/CMakeLists.txt b/plugins/script_loader/CMakeLists.txt index f63488e6c..84c7e7be0 100644 --- a/plugins/script_loader/CMakeLists.txt +++ b/plugins/script_loader/CMakeLists.txt @@ -1,47 +1,52 @@ cmake_minimum_required(VERSION 3.16) -find_package(CoreClrEmbed) +if (NOT EMSCRIPTEN) -add_imhex_plugin( - NAME - script_loader + include(ImHexPlugin) + find_package(CoreClrEmbed) - SOURCES - source/plugin_script_loader.cpp + add_imhex_plugin( + NAME + script_loader - INCLUDES - include -) + SOURCES + source/plugin_script_loader.cpp -if (CoreClrEmbed_FOUND) - add_library(nethost SHARED IMPORTED) - target_include_directories(nethost INTERFACE "${CoreClrEmbed_INCLUDE_DIRS}") - get_filename_component(CoreClrEmbed_FOLDER ${CoreClrEmbed_SHARED_LIBRARIES} DIRECTORY) - set_target_properties(nethost - PROPERTIES - IMPORTED_IMPLIB ${CoreClrEmbed_SHARED_LIBRARIES} - IMPORTED_LOCATION ${CoreClrEmbed_LIBRARIES} - BUILD_RPATH ${CoreClrEmbed_FOLDER} - INSTALL_RPATH ${CoreClrEmbed_FOLDER}) - - target_link_directories(script_loader PRIVATE ${CoreClrEmbed_FOLDER}) - target_include_directories(script_loader PRIVATE ${CoreClrEmbed_INCLUDE_DIRS}) - target_compile_definitions(script_loader PRIVATE DOTNET_PLUGINS=1) - target_sources(script_loader PRIVATE - source/loaders/dotnet/dotnet_loader.cpp - - source/script_api/v1/mem.cpp - source/script_api/v1/bookmarks.cpp - source/script_api/v1/ui.cpp + INCLUDES + include ) - set(EXTRA_BUNDLE_LIBRARY_PATHS "${CoreClrEmbed_FOLDER}" PARENT_SCOPE) + if (CoreClrEmbed_FOUND) + add_library(nethost SHARED IMPORTED) + target_include_directories(nethost INTERFACE "${CoreClrEmbed_INCLUDE_DIRS}") + get_filename_component(CoreClrEmbed_FOLDER ${CoreClrEmbed_SHARED_LIBRARIES} DIRECTORY) + set_target_properties(nethost + PROPERTIES + IMPORTED_IMPLIB ${CoreClrEmbed_SHARED_LIBRARIES} + IMPORTED_LOCATION ${CoreClrEmbed_LIBRARIES} + BUILD_RPATH ${CoreClrEmbed_FOLDER} + INSTALL_RPATH ${CoreClrEmbed_FOLDER}) + + target_link_directories(script_loader PRIVATE ${CoreClrEmbed_FOLDER}) + target_include_directories(script_loader PRIVATE ${CoreClrEmbed_INCLUDE_DIRS}) + target_compile_definitions(script_loader PRIVATE DOTNET_PLUGINS=1) + target_sources(script_loader PRIVATE + source/loaders/dotnet/dotnet_loader.cpp + + source/script_api/v1/mem.cpp + source/script_api/v1/bookmarks.cpp + source/script_api/v1/ui.cpp + ) + + set(EXTRA_BUNDLE_LIBRARY_PATHS "${CoreClrEmbed_FOLDER}" PARENT_SCOPE) + + if (IMHEX_BUNDLE_DOTNET) + install(FILES ${CoreClrEmbed_SHARED_LIBRARIES} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + endif () + + add_subdirectory(dotnet) + add_dependencies(script_loader AssemblyLoader) - if (IMHEX_BUNDLE_DOTNET) - install(FILES ${CoreClrEmbed_SHARED_LIBRARIES} DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif () - add_subdirectory(dotnet) - add_dependencies(script_loader AssemblyLoader) - endif () \ No newline at end of file diff --git a/plugins/windows/CMakeLists.txt b/plugins/windows/CMakeLists.txt index 9d0253220..664263672 100644 --- a/plugins/windows/CMakeLists.txt +++ b/plugins/windows/CMakeLists.txt @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.16) if (WIN32) include(ImHexPlugin) - add_imhex_plugin( NAME windows diff --git a/resources/dist/common/try_online_banner.png b/resources/dist/common/try_online_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef4a483c3e6ea07189a2a217053bae98c645dff GIT binary patch literal 11279 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 z