diff --git a/.gitmodules b/.gitmodules index 0e402752..ab4f4eaa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,10 @@ [submodule "deps/asmjit"] path = deps/asmjit url = https://github.com/asmjit/asmjit.git +[submodule "deps/curl"] + path = deps/curl + url = https://github.com/curl/curl.git +[submodule "deps/zlib"] + path = deps/zlib + url = https://github.com/madler/zlib.git + branch = develop diff --git a/deps/curl b/deps/curl new file mode 160000 index 00000000..a8a4abb2 --- /dev/null +++ b/deps/curl @@ -0,0 +1 @@ +Subproject commit a8a4abb2ae97c9a42db0cb82c7f1c508b01d75f3 diff --git a/deps/premake/curl.lua b/deps/premake/curl.lua new file mode 100644 index 00000000..4377e319 --- /dev/null +++ b/deps/premake/curl.lua @@ -0,0 +1,73 @@ +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 "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) diff --git a/deps/premake/minizip.lua b/deps/premake/minizip.lua new file mode 100644 index 00000000..4a5754bc --- /dev/null +++ b/deps/premake/minizip.lua @@ -0,0 +1,43 @@ +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" + + minizip.includes() + + files { + path.join(minizip.source, "*.h"), + path.join(minizip.source, "*.c"), + } + + removefiles { + path.join(minizip.source, "miniunz.c"), + path.join(minizip.source, "minizip.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minizip) diff --git a/deps/premake/zlib.lua b/deps/premake/zlib.lua new file mode 100644 index 00000000..566a707b --- /dev/null +++ b/deps/premake/zlib.lua @@ -0,0 +1,39 @@ +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" + + zlib.includes() + + files { + path.join(zlib.source, "*.h"), + path.join(zlib.source, "*.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, zlib) diff --git a/deps/zlib b/deps/zlib new file mode 160000 index 00000000..ec3df002 --- /dev/null +++ b/deps/zlib @@ -0,0 +1 @@ +Subproject commit ec3df00224d4b396e2ac6586ab5d25f673caa4c2 diff --git a/src/common/utils/com.cpp b/src/common/utils/com.cpp new file mode 100644 index 00000000..83cfc47b --- /dev/null +++ b/src/common/utils/com.cpp @@ -0,0 +1,127 @@ +#include "com.hpp" +#include "nt.hpp" +#include "string.hpp" +#include "finally.hpp" + +#include + +#include + + +namespace utils::com +{ + namespace + { + [[maybe_unused]] class _ + { + public: + _() + { + if(FAILED(CoInitialize(nullptr))) + { + throw std::runtime_error("Failed to initialize the component object model"); + } + } + + ~_() + { + CoUninitialize(); + } + } __; + } + + bool select_folder(std::string& out_folder, const std::string& title, const std::string& selected_folder) + { + CComPtr file_dialog{}; + if(FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&file_dialog)))) + { + throw std::runtime_error("Failed to create co instance"); + } + + DWORD dw_options; + if(FAILED(file_dialog->GetOptions(&dw_options))) + { + throw std::runtime_error("Failed to get options"); + } + + if(FAILED(file_dialog->SetOptions(dw_options | FOS_PICKFOLDERS))) + { + throw std::runtime_error("Failed to set options"); + } + + std::wstring wide_title(title.begin(), title.end()); + if(FAILED(file_dialog->SetTitle(wide_title.data()))) + { + throw std::runtime_error("Failed to set title"); + } + + if (!selected_folder.empty()) + { + file_dialog->ClearClientData(); + + std::wstring wide_selected_folder(selected_folder.begin(), selected_folder.end()); + for (auto& chr : wide_selected_folder) + { + if (chr == L'/') + { + chr = L'\\'; + } + } + + IShellItem* shell_item = nullptr; + if(FAILED(SHCreateItemFromParsingName(wide_selected_folder.data(), NULL, IID_PPV_ARGS(&shell_item)))) + { + throw std::runtime_error("Failed to create item from parsing name"); + } + + if (FAILED(file_dialog->SetDefaultFolder(shell_item))) + { + throw std::runtime_error("Failed to set default folder"); + } + } + + const auto result = file_dialog->Show(nullptr); + if(result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) + { + return false; + } + + if (FAILED(result)) + { + throw std::runtime_error("Failed to show dialog"); + } + + CComPtr result_item{}; + if(FAILED(file_dialog->GetResult(&result_item))) + { + throw std::runtime_error("Failed to get result"); + } + + PWSTR raw_path = nullptr; + if(FAILED(result_item->GetDisplayName(SIGDN_FILESYSPATH, &raw_path))) + { + throw std::runtime_error("Failed to get path display name"); + } + + const auto _ = finally([raw_path]() + { + CoTaskMemFree(raw_path); + }); + + const std::wstring result_path = raw_path; + out_folder = string::convert(result_path); + + return true; + } + + CComPtr create_progress_dialog() + { + CComPtr progress_dialog{}; + if(FAILED(CoCreateInstance(CLSID_ProgressDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&progress_dialog)))) + { + throw std::runtime_error("Failed to create co instance"); + } + + return progress_dialog; + } +} diff --git a/src/common/utils/com.hpp b/src/common/utils/com.hpp new file mode 100644 index 00000000..adafdb46 --- /dev/null +++ b/src/common/utils/com.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "nt.hpp" +#include +#include + +namespace utils::com +{ + bool select_folder(std::string& out_folder, const std::string& title = "Select a Folder", const std::string& selected_folder = {}); + CComPtr create_progress_dialog(); +} diff --git a/src/common/utils/compression.cpp b/src/common/utils/compression.cpp new file mode 100644 index 00000000..7354c6ae --- /dev/null +++ b/src/common/utils/compression.cpp @@ -0,0 +1,168 @@ +#include "memory.hpp" +#include "compression.hpp" + +#include +#include + +#include "io.hpp" +#include "finally.hpp" + +namespace utils::compression +{ + namespace zlib + { + namespace + { + class zlib_stream + { + public: + zlib_stream() + { + memset(&stream_, 0, sizeof(stream_)); + valid_ = inflateInit(&stream_) == Z_OK; + } + + zlib_stream(zlib_stream&&) = delete; + zlib_stream(const zlib_stream&) = delete; + zlib_stream& operator=(zlib_stream&&) = delete; + zlib_stream& operator=(const zlib_stream&) = delete; + + ~zlib_stream() + { + if (valid_) + { + inflateEnd(&stream_); + } + } + + z_stream& get() + { + return stream_; // + } + + bool is_valid() const + { + return valid_; + } + + private: + bool valid_{false}; + z_stream stream_{}; + }; + } + + std::string decompress(const std::string& data) + { + std::string buffer{}; + zlib_stream stream_container{}; + if (!stream_container.is_valid()) + { + return {}; + } + + int ret{}; + size_t offset = 0; + static thread_local uint8_t dest[CHUNK] = {0}; + auto& stream = stream_container.get(); + + do + { + const auto input_size = std::min(sizeof(dest), data.size() - offset); + stream.avail_in = static_cast(input_size); + stream.next_in = reinterpret_cast(data.data()) + offset; + offset += stream.avail_in; + + do + { + stream.avail_out = sizeof(dest); + stream.next_out = dest; + + ret = inflate(&stream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END) + { + return {}; + } + + buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out); + } + while (stream.avail_out == 0); + } + while (ret != Z_STREAM_END); + + return buffer; + } + + std::string compress(const std::string& data) + { + std::string result{}; + auto length = compressBound(static_cast(data.size())); + result.resize(length); + + if (compress2(reinterpret_cast(result.data()), &length, + reinterpret_cast(data.data()), static_cast(data.size()), + Z_BEST_COMPRESSION) != Z_OK) + { + return {}; + } + + result.resize(length); + return result; + } + } + + namespace zip + { + namespace + { + bool add_file(zipFile& zip_file, const std::string& filename, const std::string& data) + { + const auto zip_64 = data.size() > 0xffffffff ? 1 : 0; + if (ZIP_OK != zipOpenNewFileInZip64(zip_file, filename.data(), nullptr, nullptr, 0, nullptr, 0, nullptr, + Z_DEFLATED, Z_BEST_COMPRESSION, zip_64)) + { + return false; + } + + const auto _ = finally([&zip_file]() + { + zipCloseFileInZip(zip_file); + }); + + return ZIP_OK == zipWriteInFileInZip(zip_file, data.data(), static_cast(data.size())); + } + } + + void archive::add(std::string filename, std::string data) + { + this->files_[std::move(filename)] = std::move(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.data(), 0); + if (!zip_file) + { + return false; + } + + const auto _ = finally([&zip_file, &comment]() + { + zipClose(zip_file, comment.empty() ? nullptr : comment.data()); + }); + + for (const auto& file : this->files_) + { + if (!add_file(zip_file, file.first, file.second)) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/common/utils/compression.hpp b/src/common/utils/compression.hpp new file mode 100644 index 00000000..dfe36ada --- /dev/null +++ b/src/common/utils/compression.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#define CHUNK 16384u + +namespace utils::compression +{ + namespace zlib + { + std::string compress(const std::string& data); + std::string decompress(const std::string& data); + } + + namespace zip + { + class archive + { + public: + void add(std::string filename, std::string data); + bool write(const std::string& filename, const std::string& comment = {}); + + private: + std::unordered_map files_; + }; + } +}; diff --git a/src/common/utils/http.cpp b/src/common/utils/http.cpp index 3cb59991..40795e81 100644 --- a/src/common/utils/http.cpp +++ b/src/common/utils/http.cpp @@ -1,48 +1,112 @@ #include "http.hpp" -#include "nt.hpp" -#include +#include "finally.hpp" +#include + +#pragma comment(lib, "ws2_32.lib") namespace utils::http { - std::optional get_data(const std::string& url) + namespace { - CComPtr stream; - - if (FAILED(URLOpenBlockingStreamA(nullptr, url.data(), &stream, 0, nullptr))) + struct progress_helper { - return {}; - } + const std::function* callback{}; + std::exception_ptr exception{}; + }; - char buffer[0x1000]; - std::string result; - - HRESULT status{}; - - do + int progress_callback(void *clientp, const curl_off_t /*dltotal*/, const curl_off_t dlnow, const curl_off_t /*ultotal*/, const curl_off_t /*ulnow*/) { - DWORD bytes_read = 0; - status = stream->Read(buffer, sizeof(buffer), &bytes_read); + auto* helper = static_cast(clientp); - if (bytes_read > 0) + try { - result.append(buffer, bytes_read); + if (*helper->callback) + { + (*helper->callback)(dlnow); + } + } + catch(...) + { + helper->exception = std::current_exception(); + return -1; } - } - while (SUCCEEDED(status) && status != S_FALSE); - if (FAILED(status)) + return 0; + } + + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) { - return {}; - } + auto* buffer = static_cast(userp); - return {result}; + const auto total_size = size * nmemb; + buffer->append(static_cast(contents), total_size); + return total_size; + } } - std::future> get_data_async(const std::string& url) + std::optional get_data(const std::string& url, const headers& headers, const std::function& callback) { - return std::async(std::launch::async, [url]() + curl_slist* header_list = nullptr; + auto* curl = curl_easy_init(); + if (!curl) { - return get_data(url); + return {}; + } + + auto _ = finally([&]() + { + curl_slist_free_all(header_list); + curl_easy_cleanup(curl); + }); + + for(const auto& header : headers) + { + auto data = header.first + ": " + header.second; + header_list = curl_slist_append(header_list, data.data()); + } + + std::string buffer{}; + progress_helper helper{}; + helper.callback = &callback; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &helper); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "boiii/1.0"); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, true); + + // Due to CURLOPT_FAILONERROR, CURLE_OK will not be met when the server returns 400 or 500 + if (curl_easy_perform(curl) == CURLE_OK) + { + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200) + { + return { std::move(buffer) }; + } + + throw std::runtime_error("Bad status code " + std::to_string(http_code) + " met while trying to download file " + url); + } + + if (helper.exception) + { + std::rethrow_exception(helper.exception); + } + + return {}; + } + + std::future> get_data_async(const std::string& url, const headers& headers) + { + return std::async(std::launch::async, [url, headers]() + { + return get_data(url, headers); }); } } diff --git a/src/common/utils/http.hpp b/src/common/utils/http.hpp index 65628a9f..b5248bc9 100644 --- a/src/common/utils/http.hpp +++ b/src/common/utils/http.hpp @@ -6,6 +6,8 @@ namespace utils::http { - std::optional get_data(const std::string& url); - std::future> get_data_async(const std::string& url); + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const headers& headers = {}, const std::function& callback = {}); + std::future> get_data_async(const std::string& url, const headers& headers = {}); }