diff --git a/.gitmodules b/.gitmodules index 84f5a89..dd3bf89 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "thirdparty/xxHash"] path = thirdparty/xxHash url = https://github.com/Cyan4973/xxHash.git +[submodule "thirdparty/miniz"] + path = thirdparty/miniz + url = https://github.com/richgel999/miniz diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index 7318386..ce96ced 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -15,6 +15,8 @@ add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/files.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/flash.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/math_routines.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mods.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/pi.cpp" @@ -46,4 +48,7 @@ if (WIN32) add_compile_definitions(NOMINMAX) endif() +add_subdirectory(${PROJECT_SOURCE_DIR}/../thirdparty/miniz ${CMAKE_BINARY_DIR}/miniz) + target_link_libraries(librecomp PRIVATE ultramodern) +target_link_libraries(librecomp PUBLIC miniz) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp new file mode 100644 index 0000000..7b0f792 --- /dev/null +++ b/librecomp/include/librecomp/mods.hpp @@ -0,0 +1,65 @@ +#ifndef __RECOMP_MODS_HPP__ +#define __RECOMP_MODS_HPP__ + +#include +#include +#include +#include +#include +#include + +#define MINIZ_NO_DEFLATE_APIS +#define MINIZ_NO_ARCHIVE_WRITING_APIS +#include "miniz.h" +#include "miniz_zip.h" + +namespace recomp { + namespace mods { + enum class ModLoadError { + Good, + DoesNotExist, + NotAFile, + FileError, + InvalidZip, + NoManifest, + InvalidManifest, + }; + + struct ZipModHandle { + FILE* file_handle = nullptr; + std::unique_ptr archive; + + ZipModHandle() = default; + ZipModHandle(const std::filesystem::path& mod_path, ModLoadError& error); + ZipModHandle(const ZipModHandle& rhs) = delete; + ZipModHandle& operator=(const ZipModHandle& rhs) = delete; + ZipModHandle(ZipModHandle&& rhs); + ZipModHandle& operator=(ZipModHandle&& rhs); + ~ZipModHandle(); + + std::vector read_file(const std::string& filename, bool& exists); + }; + + struct ModManifest { + std::filesystem::path mod_root_path; + + std::string mod_id; + + int major_version; + int minor_version; + int patch_version; + + // These are all relative to the base path for loose mods or inside the zip for zipped mods. + std::string binary_path; + std::string binary_syms_path; + std::string rom_patch_path; + std::string rom_patch_syms_path; + + ZipModHandle mod_handle; + }; + + ModManifest load_mod(const std::filesystem::path& mod_path, ModLoadError& error); + } +}; + +#endif diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp new file mode 100644 index 0000000..4d6050b --- /dev/null +++ b/librecomp/src/mod_manifest.cpp @@ -0,0 +1,235 @@ +#include + +#include "json/json.hpp" + +#include "librecomp/mods.hpp" + +recomp::mods::ZipModHandle::~ZipModHandle() { + if (file_handle) { + fclose(file_handle); + file_handle = nullptr; + } + + if (archive) { + mz_zip_reader_end(archive.get()); + } + archive = {}; +} + +recomp::mods::ZipModHandle::ZipModHandle(ZipModHandle&& rhs) { + *this = std::move(rhs); +} + +recomp::mods::ZipModHandle& recomp::mods::ZipModHandle::operator=(ZipModHandle&& rhs) { + if (file_handle) { + fclose(file_handle); + } + file_handle = rhs.file_handle; + rhs.file_handle = nullptr; + + mz_zip_reader_end(archive.get()); + archive = std::move(rhs.archive); + + return *this; +} + +recomp::mods::ZipModHandle::ZipModHandle(const std::filesystem::path& mod_path, ModLoadError& error) { +#ifdef _WIN32 + if (_wfopen_s(&file_handle, mod_path.c_str(), L"rb") != 0) { + error = ModLoadError::FileError; + return; + } +#else + file_handle = fopen(mod_path.c_str(), L"rb"); + if (!file_handle) { + error = ModLoadError::FileError; + return; + } +#endif + archive = std::make_unique(); + if (!mz_zip_reader_init_cfile(archive.get(), file_handle, 0, 0)) { + error = ModLoadError::InvalidZip; + return; + } + + error = ModLoadError::Good; +} + +std::vector recomp::mods::ZipModHandle::read_file(const std::string& filename, bool& exists) { + std::vector ret{}; + + mz_uint32 file_index; + if (!mz_zip_reader_locate_file_v2(archive.get(), filename.c_str(), nullptr, MZ_ZIP_FLAG_CASE_SENSITIVE, &file_index)) { + exists = false; + return ret; + } + + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(archive.get(), file_index, &stat)) { + exists = false; + return ret; + } + + ret.resize(stat.m_uncomp_size); + if (!mz_zip_reader_extract_to_mem(archive.get(), file_index, ret.data(), ret.size(), 0)) { + exists = false; + return {}; + } + + exists = true; + return ret; +} + +enum class ManifestField { + Id, + MajorVersion, + MinorVersion, + PatchVersion, + BinaryPath, + BinarySymsPath, + RomPatchPath, + RomPatchSymsPath, + Invalid, +}; + +std::unordered_map field_map { + { "id", ManifestField::Id }, + { "major_version", ManifestField::MajorVersion }, + { "minor_version", ManifestField::MinorVersion }, + { "patch_version", ManifestField::PatchVersion }, + { "binary", ManifestField::BinaryPath }, + { "binary_syms", ManifestField::BinarySymsPath }, + { "rom_patch", ManifestField::RomPatchPath }, + { "rom_patch_syms", ManifestField::RomPatchSymsPath }, +}; + +template +bool get_to(const nlohmann::json& val, T2& out) { + const T1* ptr = val.get_ptr(); + if (ptr == nullptr) { + return false; + } + + out = *ptr; + return true; +} + +bool parse_manifest(recomp::mods::ModManifest& ret, const std::vector& manifest_data) { + using json = nlohmann::json; + json manifest_json = json::parse(manifest_data.begin(), manifest_data.end(), false); + + if (manifest_json.is_discarded()) { + // Failed to parse + return false; + } + + if (!manifest_json.is_object()) { + // Invalid manifest + return false; + } + + for (const auto& [key, val] : manifest_json.items()) { + const auto find_key_it = field_map.find(key); + if (find_key_it == field_map.end()) { + // Unrecognized field + return false; + } + + ManifestField field = find_key_it->second; + switch (field) { + case ManifestField::Id: + if (!get_to(val, ret.mod_id)) { + // Invalid type + return false; + } + break; + case ManifestField::MajorVersion: + if (!get_to(val, ret.major_version)) { + // Invalid type + return false; + } + break; + case ManifestField::MinorVersion: + if (!get_to(val, ret.minor_version)) { + // Invalid type + return false; + } + break; + case ManifestField::PatchVersion: + if (!get_to(val, ret.patch_version)) { + // Invalid type + return false; + } + break; + case ManifestField::BinaryPath: + if (!get_to(val, ret.binary_path)) { + // Invalid type + return false; + } + break; + case ManifestField::BinarySymsPath: + if (!get_to(val, ret.binary_syms_path)) { + // Invalid type + return false; + } + break; + case ManifestField::RomPatchPath: + if (!get_to(val, ret.rom_patch_path)) { + // Invalid type + return false; + } + break; + case ManifestField::RomPatchSymsPath: + if (!get_to(val, ret.rom_patch_syms_path)) { + // Invalid type + return false; + } + break; + } + } + + return true; +} + +recomp::mods::ModManifest recomp::mods::load_mod(const std::filesystem::path& mod_path, ModLoadError& error) { + ModManifest ret{}; + std::error_code ec; + + if (!std::filesystem::exists(mod_path, ec) || ec) { + error = ModLoadError::DoesNotExist; + return {}; + } + + // TODO support symlinks? + if (!std::filesystem::is_regular_file(mod_path, ec) || ec) { + error = ModLoadError::NotAFile; + return {}; + } + + // Load the zip file. + ModLoadError zip_error; + ret.mod_handle = recomp::mods::ZipModHandle(mod_path, zip_error); + + if (zip_error != ModLoadError::Good) { + error = zip_error; + return {}; + } + + { + bool exists; + std::vector manifest_data = ret.mod_handle.read_file("manifest.json", exists); + if (!exists) { + error = ModLoadError::NoManifest; + return {}; + } + + if (!parse_manifest(ret, manifest_data)) { + error = ModLoadError::InvalidManifest; + return {}; + } + } + + // Return the loaded mod manifest + error = ModLoadError::Good; + return ret; +} diff --git a/thirdparty/miniz b/thirdparty/miniz new file mode 160000 index 0000000..8573fd7 --- /dev/null +++ b/thirdparty/miniz @@ -0,0 +1 @@ +Subproject commit 8573fd7cd6f49b262a0ccc447f3c6acfc415e556