This commit is contained in:
6arelyFuture 2024-01-13 22:57:08 +01:00
commit 095e3e8b42
Signed by: Future
GPG Key ID: FA77F074E98D98A5
37 changed files with 2372 additions and 0 deletions

7
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: gitsubmodule
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

162
.github/workflows/build.yml vendored Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Build results
build

13
.gitmodules vendored Normal file
View File

@ -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

29
LICENSE Normal file
View File

@ -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.

24
README.md Normal file
View File

@ -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

1
deps/GSL vendored Submodule

@ -0,0 +1 @@
Subproject commit e64c97fc2cfc11992098bb38eda932de275e3f4d

1
deps/curl vendored Submodule

@ -0,0 +1 @@
Subproject commit 7161cb17c01dcff1dc5bf89a18437d9d729f1ecd

75
deps/premake/curl.lua vendored Normal file
View File

@ -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)

19
deps/premake/gsl.lua vendored Normal file
View File

@ -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)

50
deps/premake/minizip.lua vendored Normal file
View File

@ -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)

20
deps/premake/rapidjson.lua vendored Normal file
View File

@ -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)

40
deps/premake/zlib.lua vendored Normal file
View File

@ -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)

1
deps/rapidjson vendored Submodule

@ -0,0 +1 @@
Subproject commit 6089180ecb704cb2b136777798fa1be303618975

1
deps/zlib vendored Submodule

@ -0,0 +1 @@
Subproject commit 643e17b7498d12ab8d15565662880579692f769d

142
premake5.lua Normal file
View File

@ -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()

248
src/console.cpp Normal file
View File

@ -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<void()> 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<size_t>(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<void()> callback)
: std::lock_guard<std::mutex>(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 = {};
}
}

32
src/console.hpp Normal file
View File

@ -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<std::mutex>
{
public:
signal_handler(std::function<void()> callback);
~signal_handler();
};
}

53
src/main.cpp Normal file
View File

@ -0,0 +1,53 @@
#include <std_include.hpp>
#include "console.hpp"
#include "updater/updater.hpp"
namespace
{
int unsafe_main(std::string&& prog, std::vector<std::string>&& 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<std::string> 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;
}
}

1
src/std_include.cpp Normal file
View File

@ -0,0 +1 @@
#include <std_include.hpp>

74
src/std_include.hpp Normal file
View File

@ -0,0 +1,74 @@
#ifdef _WIN32
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#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 <cassert>
#include <cctype>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <optional>
#include <queue>
#include <ranges>
#include <regex>
#include <span>
#include <sstream>
#include <thread>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gsl/gsl>
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#ifdef _WIN32
#pragma comment(lib, "ws2_32.lib")
#endif
using namespace std::literals;

View File

@ -0,0 +1,258 @@
#include <std_include.hpp>
#include <console.hpp>
#include "file_updater.hpp"
#include <utils/compression.hpp>
#include <utils/http.hpp>
#include <utils/io.hpp>
namespace updater
{
namespace
{
std::optional<std::string> 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<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
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 <expected>
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());
});
}
}

View File

@ -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<std::filesystem::path> cleanup_directories_;
// Files to skip
std::vector<std::string> 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;
};
}

37
src/updater/updater.cpp Normal file
View File

@ -0,0 +1,37 @@
#include <std_include.hpp>
#include <console.hpp>
#include "file_updater.hpp"
#include "updater.hpp"
#include <utils/properties.hpp>
#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();
}
}

6
src/updater/updater.hpp Normal file
View File

@ -0,0 +1,6 @@
#pragma once
namespace updater
{
int update_iw4x();
}

280
src/utils/compression.cpp Normal file
View File

@ -0,0 +1,280 @@
#include <std_include.hpp>
#include "compression.hpp"
#include <unzip.h>
#include <zlib.h>
#include <zip.h>
#include <gsl/gsl>
#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<uInt>(input_size);
stream.next_in = reinterpret_cast<const Bytef*>(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<uLong>(data.size()));
result.resize(length);
if (compress2(reinterpret_cast<Bytef*>(result.data()), &length,
reinterpret_cast<const Bytef*>(data.data()), static_cast<uLong>(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<unsigned>(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<char[]>(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);
}
}
}

31
src/utils/compression.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <filesystem>
#include <string>
#include <unordered_map>
#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<std::string, std::string> files_;
};
}
};

65
src/utils/http.cpp Normal file
View File

@ -0,0 +1,65 @@
#include <std_include.hpp>
#include "http.hpp"
#include <curl/curl.h>
namespace utils::http
{
namespace
{
size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
}
std::optional<std::string> 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<std::optional<std::string>> get_data_async(const std::string& url, const headers& headers)
{
return std::async(std::launch::async, [url, headers]() -> std::optional<std::string>
{
return get_data(url, headers);
});
}
}

14
src/utils/http.hpp Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <future>
#include <optional>
#include <string>
#include <unordered_map>
namespace utils::http
{
using headers = std::unordered_map<std::string, std::string>;
std::optional<std::string> get_data(const std::string& url, const headers& headers = {});
std::future<std::optional<std::string>> get_data_async(const std::string& url, const headers& headers = {});
}

136
src/utils/io.cpp Normal file
View File

@ -0,0 +1,136 @@
#include <std_include.hpp>
#include "io.hpp"
#include <fstream>
#include <ios>
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<std::streamsize>(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<uint32_t>(size));
stream.read(const_cast<char*>(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<std::size_t>(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<std::string> list_files(const std::filesystem::path& directory)
{
std::vector<std::string> 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);
}
}

21
src/utils/io.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
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<std::string> list_files(const std::filesystem::path& directory);
void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target);
}

109
src/utils/memory.cpp Normal file
View File

@ -0,0 +1,109 @@
#include <std_include.hpp>
#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<void*>(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<char>(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<void*>(data));
}
bool memory::is_set(const void* mem, const char chr, const std::size_t length)
{
auto* const mem_arr = static_cast<const char*>(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_;
}
}

71
src/utils/memory.hpp Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include <mutex>
#include <vector>
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 <typename T>
T* allocate()
{
return this->allocate_array<T>(1);
}
template <typename T>
T* allocate_array(const std::size_t count = 1)
{
return static_cast<T*>(this->allocate(count * sizeof(T)));
}
bool empty() const;
char* duplicate_string(const std::string& string);
private:
std::mutex mutex_;
std::vector<void*> pool_;
};
static void* allocate(std::size_t length);
template <typename T>
static T* allocate()
{
return allocate_array<T>(1);
}
template <typename T>
static T* allocate_array(const std::size_t count = 1)
{
return static_cast<T*>(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_;
};
}

94
src/utils/properties.cpp Normal file
View File

@ -0,0 +1,94 @@
#include <std_include.hpp>
#include "io.hpp"
#include "properties.hpp"
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
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<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
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<std::string> 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);
}
}

10
src/utils/properties.hpp Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <filesystem>
#include <optional>
namespace utils::properties
{
std::optional<std::string> load(const std::string& name);
void store(const std::string& name, const std::string& value);
}

102
src/utils/string.cpp Normal file
View File

@ -0,0 +1,102 @@
#include <std_include.hpp>
#include "string.hpp"
#include <sstream>
#include <cstdarg>
#include <algorithm>
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<std::string> split(const std::string& s, const char delim)
{
std::stringstream ss(s);
std::string item;
std::vector<std::string> 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<char>(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<char>(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;
}
}

97
src/utils/string.hpp Normal file
View File

@ -0,0 +1,97 @@
#pragma once
#include "memory.hpp"
#include <cstdint>
template <class Type, std::size_t n>
constexpr std::size_t ARRAY_COUNT(Type(&)[n]) { return n; }
namespace utils::string
{
template <std::size_t buffers, std::size_t min_buffer_size>
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<char>(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<std::string> 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);
}