mirror of
				https://github.com/alterware/aw-installer.git
				synced 2025-10-31 00:17:00 +00:00 
			
		
		
		
	init
This commit is contained in:
		
							
								
								
									
										248
									
								
								src/console.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								src/console.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										32
									
								
								src/console.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										53
									
								
								src/main.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								src/std_include.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| #include <std_include.hpp> | ||||
							
								
								
									
										74
									
								
								src/std_include.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/std_include.hpp
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										258
									
								
								src/updater/file_updater.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								src/updater/file_updater.cpp
									
									
									
									
									
										Normal 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()); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/updater/file_updater.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/updater/file_updater.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										37
									
								
								src/updater/updater.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										6
									
								
								src/updater/updater.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| namespace updater | ||||
| { | ||||
| 	int update_iw4x(); | ||||
| } | ||||
							
								
								
									
										280
									
								
								src/utils/compression.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								src/utils/compression.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										31
									
								
								src/utils/compression.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										65
									
								
								src/utils/http.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										14
									
								
								src/utils/http.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										136
									
								
								src/utils/io.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										21
									
								
								src/utils/io.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										109
									
								
								src/utils/memory.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										71
									
								
								src/utils/memory.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										94
									
								
								src/utils/properties.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										10
									
								
								src/utils/properties.hpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										102
									
								
								src/utils/string.cpp
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										97
									
								
								src/utils/string.hpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user