From 095e3e8b42f29bb7043a2d1cce07d1bff694264e Mon Sep 17 00:00:00 2001 From: FutureRave Date: Sat, 13 Jan 2024 22:57:08 +0100 Subject: [PATCH] init --- .github/dependabot.yml | 7 + .github/workflows/build.yml | 162 ++++++++++++++++++++ .gitignore | 2 + .gitmodules | 13 ++ LICENSE | 29 ++++ README.md | 24 +++ deps/GSL | 1 + deps/curl | 1 + deps/premake/curl.lua | 75 ++++++++++ deps/premake/gsl.lua | 19 +++ deps/premake/minizip.lua | 50 +++++++ deps/premake/rapidjson.lua | 20 +++ deps/premake/zlib.lua | 40 +++++ deps/rapidjson | 1 + deps/zlib | 1 + premake5.lua | 142 ++++++++++++++++++ src/console.cpp | 248 +++++++++++++++++++++++++++++++ src/console.hpp | 32 ++++ src/main.cpp | 53 +++++++ src/std_include.cpp | 1 + src/std_include.hpp | 74 +++++++++ src/updater/file_updater.cpp | 258 ++++++++++++++++++++++++++++++++ src/updater/file_updater.hpp | 46 ++++++ src/updater/updater.cpp | 37 +++++ src/updater/updater.hpp | 6 + src/utils/compression.cpp | 280 +++++++++++++++++++++++++++++++++++ src/utils/compression.hpp | 31 ++++ src/utils/http.cpp | 65 ++++++++ src/utils/http.hpp | 14 ++ src/utils/io.cpp | 136 +++++++++++++++++ src/utils/io.hpp | 21 +++ src/utils/memory.cpp | 109 ++++++++++++++ src/utils/memory.hpp | 71 +++++++++ src/utils/properties.cpp | 94 ++++++++++++ src/utils/properties.hpp | 10 ++ src/utils/string.cpp | 102 +++++++++++++ src/utils/string.hpp | 97 ++++++++++++ 37 files changed, 2372 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README.md create mode 160000 deps/GSL create mode 160000 deps/curl create mode 100644 deps/premake/curl.lua create mode 100644 deps/premake/gsl.lua create mode 100644 deps/premake/minizip.lua create mode 100644 deps/premake/rapidjson.lua create mode 100644 deps/premake/zlib.lua create mode 160000 deps/rapidjson create mode 160000 deps/zlib create mode 100644 premake5.lua create mode 100644 src/console.cpp create mode 100644 src/console.hpp create mode 100644 src/main.cpp create mode 100644 src/std_include.cpp create mode 100644 src/std_include.hpp create mode 100644 src/updater/file_updater.cpp create mode 100644 src/updater/file_updater.hpp create mode 100644 src/updater/updater.cpp create mode 100644 src/updater/updater.hpp create mode 100644 src/utils/compression.cpp create mode 100644 src/utils/compression.hpp create mode 100644 src/utils/http.cpp create mode 100644 src/utils/http.hpp create mode 100644 src/utils/io.cpp create mode 100644 src/utils/io.hpp create mode 100644 src/utils/memory.cpp create mode 100644 src/utils/memory.hpp create mode 100644 src/utils/properties.cpp create mode 100644 src/utils/properties.hpp create mode 100644 src/utils/string.cpp create mode 100644 src/utils/string.hpp diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4fcd556 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: gitsubmodule + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7520c7b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,162 @@ +name: Build + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + types: [opened, synchronize, reopened] + +env: + PREMAKE_VERSION: "5.0.0-beta2" + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + build-win: + name: Build Windows + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + configuration: + - debug + - release + arch: + - x64 + include: + - arch: x64 + platform: x64 + steps: + - name: Check out files + uses: actions/checkout@main + with: + submodules: true + fetch-depth: 0 + # NOTE - If LFS ever starts getting used during builds, switch this to true! + lfs: false + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@main + + - name: Install Premake5 + uses: abel0b/setup-premake@v2.3 + with: + version: ${{ env.PREMAKE_VERSION }} + + - name: Generate project files + run: premake5 vs2022 + + - name: Set up problem matching + uses: ammaraskar/msvc-problem-matcher@master + + - name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries + run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:Platform=${{matrix.platform}} build/aw-installer.sln + + - name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries + uses: actions/upload-artifact@main + with: + name: windows-${{matrix.arch}}-${{matrix.configuration}} + path: | + build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer.exe + build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer.pdb + + build-linux: + name: Build Linux + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + configuration: + - debug + - release + arch: + - x64 + steps: + - name: Check out files + uses: actions/checkout@main + with: + submodules: true + fetch-depth: 0 + # NOTE - If LFS ever starts getting used during builds, switch this to true! + lfs: false + + - name: Install dependencies (x64) + if: matrix.arch == 'x64' + run: | + sudo apt-get update + sudo apt-get install libcurl4-gnutls-dev -y + + - name: Install Premake5 + uses: abel0b/setup-premake@v2.3 + with: + version: ${{ env.PREMAKE_VERSION }} + + - name: Generate project files + run: premake5 --cc=clang gmake2 + + - name: Set up problem matching + uses: ammaraskar/gcc-problem-matcher@master + + - name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries + run: | + pushd build + make config=${{matrix.configuration}}_${{matrix.arch}} -j$(nproc) + env: + CC: clang + CXX: clang++ + + - name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries + uses: actions/upload-artifact@main + with: + name: linux-${{matrix.arch}}-${{matrix.configuration}} + path: | + build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer + + build-macos: + name: Build macOS + runs-on: macos-13 + strategy: + fail-fast: false + matrix: + configuration: + - debug + - release + arch: + - x64 + - arm64 + steps: + - name: Check out files + uses: actions/checkout@main + with: + submodules: true + fetch-depth: 0 + # NOTE - If LFS ever starts getting used during builds, switch this to true! + lfs: false + + - name: Install Premake5 + uses: abel0b/setup-premake@v2.3 + with: + version: ${{ env.PREMAKE_VERSION }} + + - name: Generate project files + run: premake5 gmake2 + + - name: Set up problem matching + uses: ammaraskar/gcc-problem-matcher@master + + - name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries + run: | + pushd build + make config=${{matrix.configuration}}_${{matrix.arch}} -j$(sysctl -n hw.logicalcpu) + + - name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries + uses: actions/upload-artifact@v3.1.3 + with: + name: macos-${{matrix.arch}}-${{matrix.configuration}} + path: | + build/bin/${{matrix.arch}}/${{matrix.configuration}}/aw-installer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9859769 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Build results +build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f0bb794 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,13 @@ +[submodule "deps/GSL"] + path = deps/GSL + url = https://github.com/microsoft/GSL.git +[submodule "deps/curl"] + path = deps/curl + url = https://github.com/curl/curl.git + branch = curl-8_5_0 +[submodule "deps/rapidjson"] + path = deps/rapidjson + url = https://github.com/Tencent/rapidjson.git +[submodule "deps/zlib"] + path = deps/zlib + url = https://github.com/madler/zlib.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..232bfc2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, AlterWare +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c61d5a0 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +[![build](https://github.com/alterware/aw-installer/workflows/Build/badge.svg)](https://github.com/alterware/aw-installer/actions) + + +# AlterWare: Installer +This is the tool we use to pull changes made from the release page of some of our clients and install it where we need to. + +## Build +- Install [Premake5](premake5-link) and add it to your system PATH +- Clone this repository using [Git][git-link] +- Update the submodules using ``git submodule update --init --recursive`` +- Run Premake with either of these two options ``premake5 vs2022`` (Windows) or ``premake5 gmake2`` (Linux/macOS) + +**IMPORTANT** +Requirements for Unix systems: +- Compilation: Please use Clang as the preferred compiler +- Dependencies: Ensure the LLVM C++ Standard library is installed +- Alternative compilers: If you opt for a different compiler such as GCC, use the [Mold][mold-link] linker +- Customization: Modifications to the Premake5.lua script may be required +- Platform support: Details regarding supported platforms are available in [build.yml][build-link] + +[premake5-link]: https://premake.github.io +[git-link]: https://git-scm.com +[mold-link]: https://github.com/rui314/mold +[build-link]: https://github.com/alterware/master-server/blob/master/.github/workflows/build.yml diff --git a/deps/GSL b/deps/GSL new file mode 160000 index 0000000..e64c97f --- /dev/null +++ b/deps/GSL @@ -0,0 +1 @@ +Subproject commit e64c97fc2cfc11992098bb38eda932de275e3f4d diff --git a/deps/curl b/deps/curl new file mode 160000 index 0000000..7161cb1 --- /dev/null +++ b/deps/curl @@ -0,0 +1 @@ +Subproject commit 7161cb17c01dcff1dc5bf89a18437d9d729f1ecd diff --git a/deps/premake/curl.lua b/deps/premake/curl.lua new file mode 100644 index 0000000..0f92f2c --- /dev/null +++ b/deps/premake/curl.lua @@ -0,0 +1,75 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_LDAP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter {} + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) diff --git a/deps/premake/gsl.lua b/deps/premake/gsl.lua new file mode 100644 index 0000000..7a2daf6 --- /dev/null +++ b/deps/premake/gsl.lua @@ -0,0 +1,19 @@ +gsl = { + source = path.join(dependencies.basePath, "GSL"), +} + +function gsl.import() + gsl.includes() +end + +function gsl.includes() + includedirs { + path.join(gsl.source, "include") + } +end + +function gsl.project() + +end + +table.insert(dependencies, gsl) diff --git a/deps/premake/minizip.lua b/deps/premake/minizip.lua new file mode 100644 index 0000000..07e5465 --- /dev/null +++ b/deps/premake/minizip.lua @@ -0,0 +1,50 @@ +minizip = { + source = path.join(dependencies.basePath, "zlib/contrib/minizip"), +} + +function minizip.import() + links { "minizip" } + zlib.import() + minizip.includes() +end + +function minizip.includes() + includedirs { + minizip.source + } + + zlib.includes() +end + +function minizip.project() + project "minizip" + language "C" + cdialect "C89" + + minizip.includes() + + files { + path.join(minizip.source, "*.h"), + path.join(minizip.source, "*.c"), + } + + filter "system:not windows" + removefiles { + path.join(minizip.source, "iowin32.c"), + } + filter {} + + removefiles { + path.join(minizip.source, "miniunz.c"), + path.join(minizip.source, "minizip.c"), + } + + filter { "system:windows" } + defines "_CRT_SECURE_NO_DEPRECATE" + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minizip) diff --git a/deps/premake/rapidjson.lua b/deps/premake/rapidjson.lua new file mode 100644 index 0000000..ee4929d --- /dev/null +++ b/deps/premake/rapidjson.lua @@ -0,0 +1,20 @@ +rapidjson = { + source = path.join(dependencies.basePath, "rapidjson"), +} + +function rapidjson.import() + defines{"RAPIDJSON_HAS_STDSTRING"} + rapidjson.includes() +end + +function rapidjson.includes() + includedirs { + path.join(rapidjson.source, "include"), + } +end + +function rapidjson.project() + +end + +table.insert(dependencies, rapidjson) diff --git a/deps/premake/zlib.lua b/deps/premake/zlib.lua new file mode 100644 index 0000000..89a2f7c --- /dev/null +++ b/deps/premake/zlib.lua @@ -0,0 +1,40 @@ +zlib = { + source = path.join(dependencies.basePath, "zlib"), +} + +function zlib.import() + links { "zlib" } + zlib.includes() +end + +function zlib.includes() + includedirs { + zlib.source + } + + defines { + "ZLIB_CONST", + } +end + +function zlib.project() + project "zlib" + language "C" + cdialect "C89" + + zlib.includes() + + files { + path.join(zlib.source, "*.h"), + path.join(zlib.source, "*.c"), + } + + filter { "system:windows" } + defines "_CRT_SECURE_NO_DEPRECATE" + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, zlib) diff --git a/deps/rapidjson b/deps/rapidjson new file mode 160000 index 0000000..6089180 --- /dev/null +++ b/deps/rapidjson @@ -0,0 +1 @@ +Subproject commit 6089180ecb704cb2b136777798fa1be303618975 diff --git a/deps/zlib b/deps/zlib new file mode 160000 index 0000000..643e17b --- /dev/null +++ b/deps/zlib @@ -0,0 +1 @@ +Subproject commit 643e17b7498d12ab8d15565662880579692f769d diff --git a/premake5.lua b/premake5.lua new file mode 100644 index 0000000..e4b9b85 --- /dev/null +++ b/premake5.lua @@ -0,0 +1,142 @@ +dependencies = { + basePath = "./deps" +} + +function dependencies.load() + dir = path.join(dependencies.basePath, "premake/*.lua") + deps = os.matchfiles(dir) + + for i, dep in pairs(deps) do + dep = dep:gsub(".lua", "") + require(dep) + end +end + +function dependencies.imports() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.import() + end + end +end + +function dependencies.projects() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.project() + end + end +end + +dependencies.load() + +workspace "aw-installer" +startproject "aw-installer" +location "./build" +objdir "%{wks.location}/obj" +targetdir "%{wks.location}/bin/%{cfg.platform}/%{cfg.buildcfg}" + +configurations {"debug", "release"} + +language "C++" +cppdialect "C++20" + +if os.istarget("darwin") then + platforms {"x64", "arm64"} +else + platforms {"x86", "x64"} +end + +filter "platforms:x86" +architecture "x86" +filter {} + +filter "platforms:x64" +architecture "x86_64" +filter {} + +filter "platforms:arm64" +architecture "ARM64" +filter {} + +symbols "On" +staticruntime "On" +editandcontinue "Off" +warnings "Extra" +characterset "ASCII" + +filter { "system:linux", "system:macosx" } + buildoptions "-pthread" + linkoptions "-pthread" +filter {} + +if os.istarget("linux") then + filter { "toolset:clang*" } + buildoptions "-stdlib=libc++" + linkoptions "-stdlib=libc++" + + -- always try to use lld. LD or Gold will not work + linkoptions "-fuse-ld=lld" + filter {} +end + +filter { "system:macosx", "platforms:arm64" } + buildoptions "-arch arm64" + linkoptions "-arch arm64" +filter {} + +if _OPTIONS["dev-build"] then + defines {"DEV_BUILD"} +end + +if os.getenv("CI") then + defines "CI" +end + +flags {"NoIncrementalLink", "NoMinimalRebuild", "MultiProcessorCompile", "No64BitChecks"} + +filter "configurations:Release" + optimize "Size" + defines "NDEBUG" + flags "FatalCompileWarnings" +filter {} + +filter "configurations:Debug" + optimize "Debug" + defines {"DEBUG", "_DEBUG"} +filter {} + +project "aw-installer" +kind "ConsoleApp" +language "C++" + +pchheader "std_include.hpp" +pchsource "src/std_include.cpp" + +files {"./src/**.rc", "./src/**.hpp", "./src/**.cpp"} + +includedirs {"./src", "%{prj.location}/src"} + +filter "system:windows" + files { + "./src/**.rc", + } +filter {} + +filter { "system:windows", "toolset:not msc*" } + resincludedirs { + "%{_MAIN_SCRIPT_DIR}/src" + } +filter {} + +filter { "system:windows", "toolset:msc*" } + resincludedirs { + "$(ProjectDir)src" + } +filter {} + +dependencies.imports() + + +group "Dependencies" +dependencies.projects() diff --git a/src/console.cpp b/src/console.cpp new file mode 100644 index 0000000..c5e0080 --- /dev/null +++ b/src/console.cpp @@ -0,0 +1,248 @@ +#include "std_include.hpp" +#include "console.hpp" + +#ifdef _WIN32 +#define COLOR_LOG_INFO 11//15 +#define COLOR_LOG_WARN 14 +#define COLOR_LOG_ERROR 12 +#define COLOR_LOG_DEBUG 15//7 +#else +#define COLOR_LOG_INFO "\033[0;36m" +#define COLOR_LOG_WARN "\033[0;33m" +#define COLOR_LOG_ERROR "\033[0;31m" +#define COLOR_LOG_DEBUG "\033[0m" +#endif + +namespace console +{ + namespace + { + std::mutex signal_mutex; + std::function signal_callback; + +#ifdef _WIN32 +#define COLOR(win, posix) win + using color_type = WORD; +#else +#define COLOR(win, posix) posix + using color_type = const char*; +#endif + + const color_type color_array[] = + { + COLOR(0x8, "\033[0;90m"), // 0 - black + COLOR(0xC, "\033[0;91m"), // 1 - red + COLOR(0xA, "\033[0;92m"), // 2 - green + COLOR(0xE, "\033[0;93m"), // 3 - yellow + COLOR(0x9, "\033[0;94m"), // 4 - blue + COLOR(0xB, "\033[0;96m"), // 5 - cyan + COLOR(0xD, "\033[0;95m"), // 6 - pink + COLOR(0xF, "\033[0;97m"), // 7 - white + }; + +#ifdef _WIN32 + BOOL WINAPI handler(const DWORD signal) + { + if (signal == CTRL_C_EVENT && signal_callback) + { + signal_callback(); + } + + return TRUE; + } + +#else + void handler(int signal) + { + if (signal == SIGINT && signal_callback) + { + signal_callback(); + } + } +#endif + + std::string format(va_list* ap, const char* message) + { + static thread_local char buffer[0x1000]; + +#ifdef _WIN32 + const int count = vsnprintf_s(buffer, _TRUNCATE, message, *ap); +#else + const int count = vsnprintf(buffer, sizeof(buffer), message, *ap); +#endif + + if (count < 0) return {}; + return {buffer, static_cast(count)}; + } + +#ifdef _WIN32 + HANDLE get_console_handle() + { + return GetStdHandle(STD_OUTPUT_HANDLE); + } +#endif + + void set_color(const color_type color) + { +#ifdef _WIN32 + SetConsoleTextAttribute(get_console_handle(), color); +#else + printf("%s", color); +#endif + } + + bool apply_color(const std::string& data, const size_t index, const color_type base_color) + { + if (data[index] != '^' || (index + 1) >= data.size()) + { + return false; + } + + auto code = data[index + 1] - '0'; + if (code < 0 || code > 11) + { + return false; + } + + code = std::min(code, 7); // Everything above white is white + if (code == 7) + { + set_color(base_color); + } + else + { + set_color(color_array[code]); + } + + return true; + } + + void print_colored(const std::string& line, const color_type base_color) + { + lock _{}; + set_color(base_color); + + for (size_t i = 0; i < line.size(); ++i) + { + if (apply_color(line, i, base_color)) + { + ++i; + continue; + } + + putchar(line[i]); + } + + reset_color(); + } + } + + lock::lock() + { +#ifdef _WIN32 + _lock_file(stdout); +#else + flockfile(stdout); +#endif + } + + lock::~lock() + { +#ifdef _WIN32 + _unlock_file(stdout); +#else + funlockfile(stdout); +#endif + } + + void reset_color() + { + lock _{}; +#ifdef _WIN32 + SetConsoleTextAttribute(get_console_handle(), 7); +#else + printf("\033[0m"); +#endif + + fflush(stdout); + } + + void info(const char* message, ...) + { + va_list ap; + va_start(ap, message); + + const auto data = format(&ap, message); + print_colored("[+] " + data + "\n", COLOR_LOG_INFO); + + va_end(ap); + } + + void warn(const char* message, ...) + { + va_list ap; + va_start(ap, message); + + const auto data = format(&ap, message); + print_colored("[!] " + data + "\n", COLOR_LOG_WARN); + + va_end(ap); + } + + void error(const char* message, ...) + { + va_list ap; + va_start(ap, message); + + const auto data = format(&ap, message); + print_colored("[-] " + data + "\n", COLOR_LOG_ERROR); + + va_end(ap); + } + + void log(const char* message, ...) + { + va_list ap; + va_start(ap, message); + + const auto data = format(&ap, message); + print_colored("[*] " + data + "\n", COLOR_LOG_DEBUG); + + va_end(ap); + } + + void set_title(const std::string& title) + { + lock _{}; + +#ifdef _WIN32 + SetConsoleTitleA(title.c_str()); +#else + printf("\033]0;%s\007", title.c_str()); + fflush(stdout); +#endif + } + + signal_handler::signal_handler(std::function callback) + : std::lock_guard(signal_mutex) + { + signal_callback = std::move(callback); + +#ifdef _WIN32 + SetConsoleCtrlHandler(handler, TRUE); +#else + signal(SIGINT, handler); +#endif + } + + signal_handler::~signal_handler() + { +#ifdef _WIN32 + SetConsoleCtrlHandler(handler, FALSE); +#else + signal(SIGINT, SIG_DFL); +#endif + + signal_callback = {}; + } +} diff --git a/src/console.hpp b/src/console.hpp new file mode 100644 index 0000000..f655dd0 --- /dev/null +++ b/src/console.hpp @@ -0,0 +1,32 @@ +#pragma once + +namespace console +{ + class lock + { + public: + lock(); + ~lock(); + + lock(lock&&) = delete; + lock(const lock&) = delete; + lock& operator=(lock&&) = delete; + lock& operator=(const lock&) = delete; + }; + + void reset_color(); + + void info(const char* message, ...); + void warn(const char* message, ...); + void error(const char* message, ...); + void log(const char* message, ...); + + void set_title(const std::string& title); + + class signal_handler : std::lock_guard + { + public: + signal_handler(std::function callback); + ~signal_handler(); + }; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4211f23 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,53 @@ +#include + +#include "console.hpp" + +#include "updater/updater.hpp" + +namespace +{ + int unsafe_main(std::string&& prog, std::vector&& args) + { + // Parse command-line flags (only increment i for matching flags) + for (auto i = args.begin(); i != args.end();) + { + if (*i == "-update-iw4x") + { + return updater::update_iw4x(); + } + else + { + console::info("AlterWare Installer\n" + "Usage: %s OPTIONS\n" + " -update-iw4x\n", + prog.data() + ); + + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; + } +} + +int main(const int argc, char* argv[]) +{ + console::set_title("AlterWare Installer"); + console::log("AlterWare Installer"); + + try + { + std::string prog(argv[0]); + std::vector args; + + args.reserve(argc - 1); + args.assign(argv + 1, argv + argc); + return unsafe_main(std::move(prog), std::move(args)); + } + catch (const std::exception& ex) + { + console::error("Fatal error: %s", ex.what()); + return EXIT_FAILURE; + } +} diff --git a/src/std_include.cpp b/src/std_include.cpp new file mode 100644 index 0000000..3306de2 --- /dev/null +++ b/src/std_include.cpp @@ -0,0 +1 @@ +#include diff --git a/src/std_include.hpp b/src/std_include.hpp new file mode 100644 index 0000000..c3f8696 --- /dev/null +++ b/src/std_include.hpp @@ -0,0 +1,74 @@ +#ifdef _WIN32 +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +#else + +#include +#include +#include +#include +#include +#include +#include + +#define ZeroMemory(x, y) std::memset(x, 0, y) + +#endif + +// min and max is required by gdi, therefore NOMINMAX won't work +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef _WIN32 + +#pragma comment(lib, "ws2_32.lib") + +#endif + +using namespace std::literals; diff --git a/src/updater/file_updater.cpp b/src/updater/file_updater.cpp new file mode 100644 index 0000000..df0360c --- /dev/null +++ b/src/updater/file_updater.cpp @@ -0,0 +1,258 @@ +#include + +#include + +#include "file_updater.hpp" + +#include +#include +#include + +namespace updater +{ + namespace + { + std::optional get_release_tag(const std::string& release_url) + { + const auto release_info = utils::http::get_data(release_url); + if (!release_info.has_value()) + { + console::warn("Could not reach remote URL \"%s\"", release_url.c_str()); + return {}; + } + + rapidjson::Document release_json{}; + + const rapidjson::ParseResult result = release_json.Parse(release_info.value()); + if (!result || !release_json.IsObject()) + { + console::error("Could not parse remote JSON response from \"%s\"", release_url.c_str()); + return {}; + } + + if (release_json.HasMember("tag_name") && release_json["tag_name"].IsString()) + { + const auto* tag_name = release_json["tag_name"].GetString(); + return tag_name; + } + + console::error("Remote JSON response from \"%s\" does not contain the data we expected", release_url.c_str()); + return {}; + } + } + + file_updater::file_updater(std::string name, std::filesystem::path base, std::filesystem::path out_name, + std::filesystem::path version_file, + std::string remote_tag, std::string remote_download) + : name_(std::move(name)) + , base_(std::move(base)) + , out_name_(std::move(out_name)) + , version_file_(std::move(version_file)) + , remote_tag_(std::move(remote_tag)) + , remote_download_(std::move(remote_download)) + { + } + + bool file_updater::update_if_necessary() const + { + update_state update_state; + + const auto local_version = this->read_local_revision_file(); + if (!this->does_require_update(update_state, local_version)) + { + console::log("%s does not require an update", this->name_.c_str()); + return true; + } + + console::info("Updating %s", this->name_.c_str()); + if (!this->update_file(this->remote_download_)) + { + console::error("Update failed"); + return false; + } + + this->cleanup_directories(); + + if (!this->deploy_files()) + { + console::error("Unable to deploy files"); + return false; + } + + // Do this last to make sure we don't ever create a version file when something failed + this->create_version_file(update_state.latest_tag); + + return true; + } + + void file_updater::add_dir_to_clean(const std::string& dir) + { + this->cleanup_directories_.emplace_back(this->base_ / dir); + } + + void file_updater::add_file_to_skip(const std::string& file) + { + this->skip_files_.emplace_back(file); + } + + std::string file_updater::read_local_revision_file() const + { + const std::filesystem::path revision_file_path = this->version_file_; + + std::string data; + if (!utils::io::read_file(revision_file_path.string(), &data) || data.empty()) + { + console::warn("Could not load \"%s\"", revision_file_path.string().c_str()); + return {}; + } + + rapidjson::Document doc{}; + const rapidjson::ParseResult result = doc.Parse(data); + if (!result || !doc.IsObject()) + { + console::error("Could not parse \"%s\"", revision_file_path.string().c_str()); + return {}; + } + + if (!doc.HasMember("version") || !doc["version"].IsString()) + { + console::error("\"%s\" contains invalid data", revision_file_path.string().c_str()); + return {}; + } + + return doc["version"].GetString(); + } + + bool file_updater::does_require_update(update_state& update_state, const std::string& local_version) const + { + console::info("Fetching tags from GitHub"); + + const auto raw_files_tag = get_release_tag(this->remote_tag_); + if (!raw_files_tag.has_value()) + { + console::warn("Failed to reach GitHub. Aborting the update"); + + update_state.requires_update = false; + return update_state.requires_update; + } + + update_state.requires_update = local_version != raw_files_tag.value(); + update_state.latest_tag = raw_files_tag.value(); + + console::info("Got release tag \"%s\". Requires updating: %s", raw_files_tag.value().c_str(), update_state.requires_update ? "Yes" : "No"); + return update_state.requires_update; + } + + void file_updater::create_version_file(const std::string& revision_version) const + { + console::info("Creating version file \"%s\". Revision is \"%s\"", this->version_file_.c_str(), revision_version.c_str()); + + rapidjson::Document doc{}; + doc.SetObject(); + + doc.AddMember("version", revision_version, doc.GetAllocator()); + + rapidjson::StringBuffer buffer{}; + rapidjson::Writer> + writer(buffer); + doc.Accept(writer); + + const std::string json(buffer.GetString(), buffer.GetLength()); + if (utils::io::write_file(this->version_file_.string(), json)) + { + console::info("File \"%s\" was created successfully", this->version_file_.string().c_str()); + return; + } + + console::error("Error while writing file \"%s\"", this->version_file_.string().c_str()); + } + + bool file_updater::update_file(const std::string& url) const + { + console::info("Downloading %s", url.c_str()); + const auto data = utils::http::get_data(url, {}); + if (!data) + { + console::error("Failed to download %s", url.c_str()); + return false; + } + + if (data.value().empty()) + { + console::error("The data buffer returned by Curl is empty"); + return false; + } + + // Download the files in the working directory, move them later. + const auto out_file = std::filesystem::current_path() / this->out_name_; + + console::info("Writing file to \"%s\"", out_file.string().c_str()); + + if (!utils::io::write_file(out_file.string(), data.value(), false)) + { + console::error("Error while writing file \"%s\"", out_file.string().c_str()); + return false; + } + + console::info("Done updating file \"%s\"", out_file.string().c_str()); + return true; + } + + // Not a fan of using exceptions here. Once C++23 is more widespread I'd like to use + bool file_updater::deploy_files() const + { + const auto out_dir = std::filesystem::current_path() / ".out"; + + assert(utils::io::file_exists(this->out_name_.string())); + + // Always try to cleanup + const auto _ = gsl::finally([this, &out_dir]() -> void + { + utils::io::remove_file(this->out_name_.string()); + + std::error_code ec; + std::filesystem::remove_all(out_dir, ec); + }); + + try + { + utils::io::create_directory(out_dir); + utils::compression::zip::archive::decompress(this->out_name_.string(), out_dir); + } + catch (const std::exception& ex) + { + console::error("Get error \"%s\" while decompressing \"%s\"", ex.what(), this->out_name_.string().c_str()); + return false; + } + + console::info("\"%s\" was decompressed. Removing files that must be skipped", this->out_name_.string().c_str()); + this->skip_files(out_dir); + + console::info("Deploying files to \"%s\"", this->base_.string().c_str()); + + utils::io::copy_folder(out_dir, this->base_); + return true; + } + + void file_updater::cleanup_directories() const + { + console::log("Cleaning up directories"); + std::for_each(this->cleanup_directories_.begin(), this->cleanup_directories_.end(), [](const auto& dir) + { + std::error_code ec; + std::filesystem::remove_all(dir, ec); + console::log("Removed directory \"%s\"", dir.string().c_str()); + }); + } + + void file_updater::skip_files(const std::filesystem::path& target_dir) const + { + console::log("Skipping files"); + std::for_each(this->skip_files_.begin(), this->skip_files_.end(), [&target_dir](const auto& file) + { + const auto target_file = target_dir / file; + utils::io::remove_file(target_file.string()); + console::log("Removed file \"%s\"", target_file.string().c_str()); + }); + } +} diff --git a/src/updater/file_updater.hpp b/src/updater/file_updater.hpp new file mode 100644 index 0000000..9b4196e --- /dev/null +++ b/src/updater/file_updater.hpp @@ -0,0 +1,46 @@ +#pragma once + +namespace updater +{ + class file_updater + { + public: + file_updater(std::string name, std::filesystem::path base, std::filesystem::path out_name, std::filesystem::path version_file, std::string remote_tag, std::string remote_download); + + [[nodiscard]] bool update_if_necessary() const; + + void add_dir_to_clean(const std::string& dir); + void add_file_to_skip(const std::string& file); + + private: + struct update_state + { + bool requires_update = false; + std::string latest_tag; + }; + + std::string name_; + + std::filesystem::path base_; + std::filesystem::path out_name_; + std::filesystem::path version_file_; + + std::string remote_tag_; + std::string remote_download_; + + // Directories to cleanup + std::vector cleanup_directories_; + + // Files to skip + std::vector skip_files_; + + [[nodiscard]] std::string read_local_revision_file() const; + [[nodiscard]] bool does_require_update(update_state& update_state, const std::string& local_version) const; + void create_version_file(const std::string& revision_version) const; + [[nodiscard]] bool update_file(const std::string& url) const; + [[nodiscard]] bool deploy_files() const; + + void cleanup_directories() const; + void skip_files(const std::filesystem::path& target_dir) const; + }; +} diff --git a/src/updater/updater.cpp b/src/updater/updater.cpp new file mode 100644 index 0000000..8ab6d22 --- /dev/null +++ b/src/updater/updater.cpp @@ -0,0 +1,37 @@ +#include + +#include + +#include "file_updater.hpp" +#include "updater.hpp" + +#include + +#define IW4X_VERSION_FILE "iw4x-version.json" +#define IW4X_RAW_FILES_UPDATE_FILE "release.zip" +#define IW4X_RAW_FILES_UPDATE_URL "https://github.com/iw4x/iw4x-rawfiles/releases/latest/download/" IW4X_RAW_FILES_UPDATE_FILE +#define IW4X_RAW_FILES_TAGS "https://api.github.com/repos/iw4x/iw4x-rawfiles/releases/latest" + +namespace updater +{ + int update_iw4x() + { + const auto iw4_install = utils::properties::load("iw4-install"); + if (!iw4_install) + { + console::error("Failed to load the properties file"); + return false; + } + + const auto& base = iw4_install.value(); + + file_updater file_updater{ "IW4x", base, IW4X_RAW_FILES_UPDATE_FILE, IW4X_VERSION_FILE, IW4X_RAW_FILES_TAGS, IW4X_RAW_FILES_UPDATE_URL }; + + file_updater.add_dir_to_clean("iw4x"); + file_updater.add_dir_to_clean("zone"); + + file_updater.add_file_to_skip("iw4sp.exe"); + + return file_updater.update_if_necessary(); + } +} diff --git a/src/updater/updater.hpp b/src/updater/updater.hpp new file mode 100644 index 0000000..28e29b9 --- /dev/null +++ b/src/updater/updater.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace updater +{ + int update_iw4x(); +} diff --git a/src/utils/compression.cpp b/src/utils/compression.cpp new file mode 100644 index 0000000..627527f --- /dev/null +++ b/src/utils/compression.cpp @@ -0,0 +1,280 @@ +#include + +#include "compression.hpp" + +#include + +#include +#include + +#include + +#include "io.hpp" +#include "string.hpp" + +#ifndef MAX_PATH +#define MAX_PATH 256 +#endif + +namespace utils::compression +{ + namespace zlib + { + namespace + { + class zlib_stream + { + public: + zlib_stream() + { + memset(&stream_, 0, sizeof(stream_)); + valid_ = inflateInit(&stream_) == Z_OK; + } + + zlib_stream(zlib_stream&&) = delete; + zlib_stream(const zlib_stream&) = delete; + zlib_stream& operator=(zlib_stream&&) = delete; + zlib_stream& operator=(const zlib_stream&) = delete; + + ~zlib_stream() + { + if (valid_) + { + inflateEnd(&stream_); + } + } + + z_stream& get() + { + return stream_; // + } + + bool is_valid() const + { + return valid_; + } + + private: + bool valid_{false}; + z_stream stream_{}; + }; + } + + std::string decompress(const std::string& data) + { + std::string buffer{}; + zlib_stream stream_container{}; + if (!stream_container.is_valid()) + { + return {}; + } + + int ret{}; + size_t offset = 0; + static thread_local uint8_t dest[CHUNK] = {0}; + auto& stream = stream_container.get(); + + do + { + const auto input_size = std::min(sizeof(dest), data.size() - offset); + stream.avail_in = static_cast(input_size); + stream.next_in = reinterpret_cast(data.data()) + offset; + offset += stream.avail_in; + + do + { + stream.avail_out = sizeof(dest); + stream.next_out = dest; + + ret = inflate(&stream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END) + { + return {}; + } + + buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out); + } + while (stream.avail_out == 0); + } + while (ret != Z_STREAM_END); + + return buffer; + } + + std::string compress(const std::string& data) + { + std::string result{}; + auto length = compressBound(static_cast(data.size())); + result.resize(length); + + if (compress2(reinterpret_cast(result.data()), &length, + reinterpret_cast(data.data()), static_cast(data.size()), + Z_BEST_COMPRESSION) != Z_OK) + { + return {}; + } + + result.resize(length); + return result; + } + } + + namespace zip + { + namespace + { + bool add_file(zipFile& zip_file, const std::string& filename, const std::string& data) + { + const auto zip_64 = data.size() > 0xffffffff ? 1 : 0; + if (ZIP_OK != zipOpenNewFileInZip64(zip_file, filename.c_str(), nullptr, nullptr, 0, nullptr, 0, nullptr, + Z_DEFLATED, Z_BEST_COMPRESSION, zip_64)) + { + return false; + } + + const auto _ = gsl::finally([&zip_file]() -> void + { + zipCloseFileInZip(zip_file); + }); + + return ZIP_OK == zipWriteInFileInZip(zip_file, data.c_str(), static_cast(data.size())); + } + } + + void archive::add(const std::string& filename, const std::string& data) + { + this->files_[filename] = data; + } + + bool archive::write(const std::string& filename, const std::string& comment) + { + // Hack to create the directory :3 + io::write_file(filename, {}); + io::remove_file(filename); + + auto* zip_file = zipOpen64(filename.c_str(), false); + if (!zip_file) + { + return false; + } + + const auto _ = gsl::finally([&zip_file, &comment]() -> void + { + zipClose(zip_file, comment.empty() ? nullptr : comment.c_str()); + }); + + for (const auto& file : this->files_) + { + if (!add_file(zip_file, file.first, file.second)) + { + return false; + } + } + + return true; + } + + // I apologize for writing such a huge function + void archive::decompress(const std::string& filename, const std::filesystem::path& out_dir) + { + unzFile file = unzOpen(filename.c_str()); + if (!file) + { + throw std::runtime_error(string::va("unzOpen failed on %s", filename.c_str())); + } + + unz_global_info global_info; + if (unzGetGlobalInfo(file, &global_info) != UNZ_OK) + { + unzClose(file); + throw std::runtime_error(string::va("unzGetGlobalInfo failed on %s", filename.c_str())); + } + + constexpr std::size_t READ_BUFFER_SIZE = 65336; + const auto read_buffer_large = std::make_unique(READ_BUFFER_SIZE); + // No need to memset this to 0 + auto* read_buffer = read_buffer_large.get(); + + // Loop to extract all the files + for (uLong i = 0; i < global_info.number_entry; ++i) + { + // Get info about the current file. + unz_file_info file_info; + char filename_buffer[MAX_PATH]{}; + + if (unzGetCurrentFileInfo(file, &file_info, filename_buffer, sizeof(filename_buffer) - 1, + nullptr, 0, nullptr, 0) != UNZ_OK) + { + continue; + } + + // Check if this entry is a directory or a file. + std::string out_file = filename_buffer; + // Fix for UNIX Systems + std::replace(out_file.begin(), out_file.end(), '\\', '/'); + + const auto filename_length = out_file.size(); + if (out_file[filename_length - 1] == '/') // ZIP is not directory-separator-agnostic + { + // Entry is a directory. Create it. + const auto dir = out_dir / out_file; + io::create_directory(dir); + } + else + { + // Entry is a file. Extract it. + if (unzOpenCurrentFile(file) != UNZ_OK) + { + // Could not read file from the ZIP + throw std::runtime_error(string::va("Failed to read file \"%s\" from \"%s\"", out_file.c_str(), filename.c_str())); + } + + const auto path = out_dir / out_file; + // Must create any directories before opening a stream + io::create_directory(path.parent_path()); + + // Open a stream to write out the data. + std::ofstream out(path.string(), std::ios::binary | std::ios::trunc); + if (!out.is_open()) + { + throw std::runtime_error("Failed to open stream"); + } + + auto read_bytes = UNZ_OK; + while (true) + { + read_bytes = unzReadCurrentFile(file, read_buffer, READ_BUFFER_SIZE); + if (read_bytes < 0) + { + throw std::runtime_error(string::va("Error while reading \"%s\" from the archive", out_file.c_str())); + } + + if (read_bytes > 0) + { + out.write(read_buffer, read_bytes); + } + else + { + // No more data to read, the loop will break + // This is normal behaviour + break; + } + } + + out.close(); + } + + // Go the the next entry listed in the ZIP file. + if ((i + 1) < global_info.number_entry) + { + if (unzGoToNextFile(file) != UNZ_OK) + { + break; + } + } + } + + unzClose(file); + } + } +} diff --git a/src/utils/compression.hpp b/src/utils/compression.hpp new file mode 100644 index 0000000..ab7408d --- /dev/null +++ b/src/utils/compression.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +#define CHUNK 16384u + +namespace utils::compression +{ + namespace zlib + { + std::string compress(const std::string& data); + std::string decompress(const std::string& data); + } + + namespace zip + { + class archive + { + public: + void add(const std::string& filename, const std::string& data); + [[nodiscard]] bool write(const std::string& filename, const std::string& comment = {}); + + static void decompress(const std::string& filename, const std::filesystem::path& out_dir); + + private: + std::unordered_map files_; + }; + } +}; diff --git a/src/utils/http.cpp b/src/utils/http.cpp new file mode 100644 index 0000000..0d4004c --- /dev/null +++ b/src/utils/http.cpp @@ -0,0 +1,65 @@ +#include + +#include "http.hpp" +#include + +namespace utils::http +{ + namespace + { + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) + { + static_cast(userp)->append(static_cast(contents), size * nmemb); + return size * nmemb; + } + } + + std::optional get_data(const std::string& url, const headers& headers) + { + curl_slist* header_list = nullptr; + auto* curl = curl_easy_init(); + if (!curl) + { + return {}; + } + + auto _ = gsl::finally([&]() + { + curl_slist_free_all(header_list); + curl_easy_cleanup(curl); + }); + + + for(const auto& header : headers) + { + auto data = header.first + ": "s + header.second; + header_list = curl_slist_append(header_list, data.c_str()); + } + + std::string buffer{}; + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "aw-installer/1.0"); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + + if (curl_easy_perform(curl) == CURLE_OK) + { + return {std::move(buffer)}; + } + + return {}; + } + + std::future> get_data_async(const std::string& url, const headers& headers) + { + return std::async(std::launch::async, [url, headers]() -> std::optional + { + return get_data(url, headers); + }); + } +} diff --git a/src/utils/http.hpp b/src/utils/http.hpp new file mode 100644 index 0000000..19c5674 --- /dev/null +++ b/src/utils/http.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include +#include + +namespace utils::http +{ + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const headers& headers = {}); + std::future> get_data_async(const std::string& url, const headers& headers = {}); +} diff --git a/src/utils/io.cpp b/src/utils/io.cpp new file mode 100644 index 0000000..2dacd20 --- /dev/null +++ b/src/utils/io.cpp @@ -0,0 +1,136 @@ +#include + +#include "io.hpp" +#include +#include + +namespace utils::io +{ + bool remove_file(const std::string& file) + { + return remove(file.c_str()) == 0; + } + + bool move_file(const std::string& src, const std::string& target) + { + return rename(src.c_str(), target.c_str()) == 0; + } + + bool file_exists(const std::string& file) + { + return std::ifstream(file).good(); + } + + bool write_file(const std::string& file, const std::string& data, const bool append) + { + const auto pos = file.find_last_of("/\\"); + if (pos != std::string::npos) + { + create_directory(file.substr(0, pos)); + } + + auto mode = std::ios::binary | std::ofstream::out; + if (append) + { + mode |= std::ofstream::app; + } + + std::ofstream stream(file, mode); + + if (stream.is_open()) + { + stream.write(data.data(), static_cast(data.size())); + stream.close(); + return true; + } + + return false; + } + + std::string read_file(const std::string& file) + { + std::string data; + read_file(file, &data); + return data; + } + + bool read_file(const std::string& file, std::string* data) + { + if (!data) return false; + data->clear(); + + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + if (!stream.is_open()) return false; + + stream.seekg(0, std::ios::end); + const std::streamsize size = stream.tellg(); + stream.seekg(0, std::ios::beg); + + if (size > -1) + { + data->resize(static_cast(size)); + stream.read(const_cast(data->data()), size); + stream.close(); + return true; + } + } + + return false; + } + + std::size_t file_size(const std::string& file) + { + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + + if (stream.good()) + { + stream.seekg(0, std::ios::end); + return static_cast(stream.tellg()); + } + } + + return 0; + } + + bool create_directory(const std::filesystem::path& directory) + { + std::error_code ec; + return std::filesystem::create_directories(directory, ec); + } + + bool directory_exists(const std::filesystem::path& directory) + { + std::error_code ec; + return std::filesystem::is_directory(directory, ec); + } + + bool directory_is_empty(const std::filesystem::path& directory) + { + std::error_code ec; + return std::filesystem::is_empty(directory, ec); + } + + std::vector list_files(const std::filesystem::path& directory) + { + std::vector files; + + for (auto& file : std::filesystem::directory_iterator(directory)) + { + files.push_back(file.path().generic_string()); + } + + return files; + } + + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target) + { + std::error_code ec; + std::filesystem::copy(src, target, + std::filesystem::copy_options::overwrite_existing | + std::filesystem::copy_options::recursive, ec); + } +} diff --git a/src/utils/io.hpp b/src/utils/io.hpp new file mode 100644 index 0000000..f68f86a --- /dev/null +++ b/src/utils/io.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace utils::io +{ + bool remove_file(const std::string& file); + bool move_file(const std::string& src, const std::string& target); + bool file_exists(const std::string& file); + bool write_file(const std::string& file, const std::string& data, bool append = false); + bool read_file(const std::string& file, std::string* data); + std::string read_file(const std::string& file); + std::size_t file_size(const std::string& file); + bool create_directory(const std::filesystem::path& directory); + bool directory_exists(const std::filesystem::path& directory); + bool directory_is_empty(const std::filesystem::path& directory); + std::vector list_files(const std::filesystem::path& directory); + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); +} diff --git a/src/utils/memory.cpp b/src/utils/memory.cpp new file mode 100644 index 0000000..f8cb9db --- /dev/null +++ b/src/utils/memory.cpp @@ -0,0 +1,109 @@ +#include + +#include "memory.hpp" + +namespace utils +{ + memory::allocator memory::mem_allocator_; + + memory::allocator::~allocator() + { + this->clear(); + } + + void memory::allocator::clear() + { + std::lock_guard _(this->mutex_); + + for (auto& data : this->pool_) + { + memory::free(data); + } + + this->pool_.clear(); + } + + void memory::allocator::free(void* data) + { + std::lock_guard _(this->mutex_); + + const auto j = std::find(this->pool_.begin(), this->pool_.end(), data); + if (j != this->pool_.end()) + { + memory::free(data); + this->pool_.erase(j); + } + } + + void memory::allocator::free(const void* data) + { + this->free(const_cast(data)); + } + + void* memory::allocator::allocate(const std::size_t length) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::allocate(length); + this->pool_.push_back(data); + return data; + } + + bool memory::allocator::empty() const + { + return this->pool_.empty(); + } + + char* memory::allocator::duplicate_string(const std::string& string) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::duplicate_string(string); + this->pool_.push_back(data); + return data; + } + + void* memory::allocate(const std::size_t length) + { + auto* buf = std::malloc(length); + std::memset(buf, 0, length); + return buf; + } + + char* memory::duplicate_string(const std::string& string) + { + const auto new_string = allocate_array(string.size() + 1); + std::memcpy(new_string, string.c_str(), string.size()); + return new_string; + } + + void memory::free(void* data) + { + ::free(data); + } + + void memory::free(const void* data) + { + free(const_cast(data)); + } + + bool memory::is_set(const void* mem, const char chr, const std::size_t length) + { + auto* const mem_arr = static_cast(mem); + + for (std::size_t i = 0; i < length; ++i) + { + if (mem_arr[i] != chr) + { + return false; + } + } + + return true; + } + + memory::allocator* memory::get_allocator() + { + return &memory::mem_allocator_; + } +} diff --git a/src/utils/memory.hpp b/src/utils/memory.hpp new file mode 100644 index 0000000..7d693c3 --- /dev/null +++ b/src/utils/memory.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +namespace utils +{ + class memory final + { + public: + class allocator final + { + public: + ~allocator(); + + void clear(); + + void free(void* data); + + void free(const void* data); + + void* allocate(std::size_t length); + + template + T* allocate() + { + return this->allocate_array(1); + } + + template + T* allocate_array(const std::size_t count = 1) + { + return static_cast(this->allocate(count * sizeof(T))); + } + + bool empty() const; + + char* duplicate_string(const std::string& string); + + private: + std::mutex mutex_; + std::vector pool_; + }; + + static void* allocate(std::size_t length); + + template + static T* allocate() + { + return allocate_array(1); + } + + template + static T* allocate_array(const std::size_t count = 1) + { + return static_cast(allocate(count * sizeof(T))); + } + + static char* duplicate_string(const std::string& string); + + static void free(void* data); + static void free(const void* data); + + static bool is_set(const void* mem, char chr, std::size_t length); + + static allocator* get_allocator(); + + private: + static allocator mem_allocator_; + }; +} diff --git a/src/utils/properties.cpp b/src/utils/properties.cpp new file mode 100644 index 0000000..90a4df8 --- /dev/null +++ b/src/utils/properties.cpp @@ -0,0 +1,94 @@ +#include + +#include "io.hpp" +#include "properties.hpp" + +#include +#include +#include + + +namespace utils::properties +{ + namespace + { + std::string get_properties_file() + { + return "properties.json"; + } + + rapidjson::Document load_properties() + { + rapidjson::Document default_doc{}; + default_doc.SetObject(); + + std::string data{}; + const auto& props = get_properties_file(); + if (!io::read_file(props, &data)) + { + return default_doc; + } + + rapidjson::Document doc{}; + const rapidjson::ParseResult result = doc.Parse(data); + + if (!result || !doc.IsObject()) + { + return default_doc; + } + + return doc; + } + + void store_properties(const rapidjson::Document& doc) + { + rapidjson::StringBuffer buffer{}; + rapidjson::Writer> + writer(buffer); + doc.Accept(writer); + + const std::string json{ buffer.GetString(), buffer.GetLength() }; + + const auto& props = get_properties_file(); + io::write_file(props, json); + } + } + + std::optional load(const std::string& name) + { + const auto doc = load_properties(); + + if (!doc.HasMember(name)) + { + return {}; + } + + const auto& value = doc[name]; + if (!value.IsString()) + { + return {}; + } + + return { std::string{ value.GetString() } }; + } + + void store(const std::string& name, const std::string& value) + { + auto doc = load_properties(); + + while (doc.HasMember(name)) + { + doc.RemoveMember(name); + } + + rapidjson::Value key{}; + key.SetString(name, doc.GetAllocator()); + + rapidjson::Value member{}; + member.SetString(value, doc.GetAllocator()); + + doc.AddMember(key, member, doc.GetAllocator()); + + store_properties(doc); + } +} diff --git a/src/utils/properties.hpp b/src/utils/properties.hpp new file mode 100644 index 0000000..dcfa1a4 --- /dev/null +++ b/src/utils/properties.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace utils::properties +{ + std::optional load(const std::string& name); + void store(const std::string& name, const std::string& value); +} diff --git a/src/utils/string.cpp b/src/utils/string.cpp new file mode 100644 index 0000000..fddf294 --- /dev/null +++ b/src/utils/string.cpp @@ -0,0 +1,102 @@ +#include + +#include "string.hpp" +#include +#include +#include + +namespace utils::string +{ + const char* va(const char* fmt, ...) + { + static thread_local va_provider<8, 256> provider; + + va_list ap; + va_start(ap, fmt); + + const char* result = provider.get(fmt, ap); + + va_end(ap); + return result; + } + + std::vector split(const std::string& s, const char delim) + { + std::stringstream ss(s); + std::string item; + std::vector elems; + + while (std::getline(ss, item, delim)) + { + elems.push_back(std::move(item)); + item = std::string{}; + } + + return elems; + } + + std::string to_lower(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(tolower(input)); + }); + + return text; + } + + std::string to_upper(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(toupper(input)); + }); + + return text; + } + + bool starts_with(const std::string& text, const std::string& substring) + { + return text.find(substring) == 0; + } + + bool ends_with(const std::string& text, const std::string& substring) + { + if (substring.size() > text.size()) return false; + return std::equal(substring.rbegin(), substring.rend(), text.rbegin()); + } + + std::string dump_hex(const std::string& data, const std::string& separator) + { + std::string result; + + for (unsigned int i = 0; i < data.size(); ++i) + { + if (i > 0) + { + result.append(separator); + } + + result.append(va("%02X", data[i] & 0xFF)); + } + + return result; + } + + std::string replace(std::string str, const std::string& from, const std::string& to) + { + if (from.empty()) + { + return str; + } + + std::size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) + { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; + } +} diff --git a/src/utils/string.hpp b/src/utils/string.hpp new file mode 100644 index 0000000..692a7f4 --- /dev/null +++ b/src/utils/string.hpp @@ -0,0 +1,97 @@ +#pragma once +#include "memory.hpp" +#include + +template +constexpr std::size_t ARRAY_COUNT(Type(&)[n]) { return n; } + +namespace utils::string +{ + template + class va_provider final + { + public: + static_assert(buffers != 0 && min_buffer_size != 0, "buffers and min_buffer_size mustn't be 0"); + + va_provider() : current_buffer_(0) + { + } + + char* get(const char* format, va_list ap) + { + ++this->current_buffer_ %= ARRAY_COUNT(this->string_pool_); + auto entry = &this->string_pool_[this->current_buffer_]; + + if (!entry->size_ || !entry->buffer_) + { + throw std::runtime_error("String pool not initialized"); + } + + while (true) + { +#ifdef _WIN32 + const auto res = vsnprintf_s(entry->buffer_, entry->size_, _TRUNCATE, format, ap); +#else + const auto res = vsnprintf(entry->buffer_, entry->size_, format, ap); +#endif + + if (res > 0) break; // Success + if (res == 0) return nullptr; // Error + + entry->double_size(); + } + + return entry->buffer_; + } + + private: + class entry final + { + public: + explicit entry(const std::size_t size = min_buffer_size) : size_(size), buffer_(nullptr) + { + if (this->size_ < min_buffer_size) this->size_ = min_buffer_size; + this->allocate(); + } + + ~entry() + { + if (this->buffer_) memory::get_allocator()->free(this->buffer_); + this->size_ = 0; + this->buffer_ = nullptr; + } + + void allocate() + { + if (this->buffer_) memory::get_allocator()->free(this->buffer_); + this->buffer_ = memory::get_allocator()->allocate_array(this->size_ + 1); + } + + void double_size() + { + this->size_ *= 2; + this->allocate(); + } + + std::size_t size_; + char* buffer_; + }; + + std::size_t current_buffer_; + entry string_pool_[buffers]; + }; + + const char* va(const char* fmt, ...); + + std::vector split(const std::string& s, char delim); + + std::string to_lower(std::string text); + std::string to_upper(std::string text); + + bool starts_with(const std::string& text, const std::string& substring); + bool ends_with(const std::string& text, const std::string& substring); + + std::string dump_hex(const std::string& data, const std::string& separator = " "); + + std::string replace(std::string str, const std::string& from, const std::string& to); +}