#include "initfs.h" #include "native_db.h" #include "Engine/Core/Json/json.h" #include "Engine/Core/Platform/launcher_support.h" #include "Engine/Game/Build/supported_build.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/initfs.h" #include "Engine/Core/Platform/path_text.h" #include #include #include #include #include #include #include #include #include #include namespace dingosdk::initfs { namespace { namespace fs = std::filesystem; using Bytes = std::vector; constexpr std::size_t maximum_archive = 64 * 1024 * 1024; constexpr std::uintptr_t key_rva = addr::initfs::key; constexpr std::uintptr_t decrypt_reference_rva = addr::initfs::decrypt_reference; constexpr auto decrypt_reference = addr::initfs::decrypt_reference_prefix; [[noreturn]] void fail(const std::string& message) { throw std::runtime_error("InitFS: " + message); } void crypto_check(NTSTATUS status, const char* operation) { if (status < 0) fail(std::string(operation) + " failed (status " + std::to_string(status) + ")"); } std::string lower(std::string_view text) { std::string result(text); for (auto& ch : result) if (ch >= 'A' && ch <= 'Z') ch = static_cast(ch + ('a' - 'A')); return result; } Bytes read_file(const fs::path& path, std::size_t maximum) { std::ifstream input(path, std::ios::binary | std::ios::ate); if (!input) fail("Cannot open " + path_utf8(path)); const auto length = input.tellg(); if (length < 0 || static_cast(length) > maximum) fail("File exceeds size limit: " + path_utf8(path)); Bytes bytes(static_cast(length)); input.seekg(0); if (!bytes.empty() && !input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) fail("Cannot read " + path_utf8(path)); return bytes; } struct Secret { std::array bytes{}; ~Secret() { SecureZeroMemory(bytes.data(), bytes.size()); } Secret() = default; Secret(const Secret&) = delete; Secret& operator=(const Secret&) = delete; }; struct Aes { BCRYPT_ALG_HANDLE algorithm{}; BCRYPT_KEY_HANDLE key{}; Bytes object; ~Aes() { if (key) BCryptDestroyKey(key); if (!object.empty()) SecureZeroMemory(object.data(), object.size()); if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0); } }; void read_key(const fs::path& executable, Secret& key) { // Full build identity is checked before any fixed RVA is used. launcher::validate_game_file(executable); std::ifstream input(executable, std::ios::binary); IMAGE_DOS_HEADER dos{}; input.read(reinterpret_cast(&dos), sizeof(dos)); if (!input || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0 || dos.e_lfanew > 0x1000) fail("Invalid executable DOS header"); input.seekg(dos.e_lfanew); IMAGE_NT_HEADERS64 nt{}; input.read(reinterpret_cast(&nt), sizeof(nt)); if (!input || nt.Signature != IMAGE_NT_SIGNATURE || nt.FileHeader.NumberOfSections > 96) fail("Invalid executable PE header"); input.seekg(static_cast(dos.e_lfanew) + 24 + nt.FileHeader.SizeOfOptionalHeader); std::vector sections(nt.FileHeader.NumberOfSections); input.read(reinterpret_cast(sections.data()), static_cast(sections.size() * sizeof(IMAGE_SECTION_HEADER))); if (!input) fail("Truncated executable section table"); const auto read_rva = [&](std::uintptr_t rva, std::span output) { for (const auto& section : sections) { if (rva < section.VirtualAddress) continue; const auto offset = rva - section.VirtualAddress; if (offset > section.SizeOfRawData || output.size() > section.SizeOfRawData - offset) continue; input.seekg(static_cast(section.PointerToRawData) + static_cast(offset)); if (!input.read(reinterpret_cast(output.data()), static_cast(output.size()))) fail("Cannot read executable contract"); return; } fail("Executable contract is outside file-backed sections"); }; std::array actual{}; read_rva(decrypt_reference_rva, actual); if (actual != decrypt_reference) fail("InitFS key reference does not match the supported build"); read_rva(key_rva, key.bytes); } Bytes decrypt(std::span encrypted, const Secret& secret) { if (encrypted.empty() || encrypted.size() % 16 || encrypted.size() > maximum_archive) fail("Invalid AES payload length"); Aes aes; crypto_check(BCryptOpenAlgorithmProvider(&aes.algorithm, BCRYPT_AES_ALGORITHM, nullptr, 0), "Open AES"); crypto_check(BCryptSetProperty(aes.algorithm, BCRYPT_CHAINING_MODE, reinterpret_cast(const_cast(BCRYPT_CHAIN_MODE_CBC)), sizeof(BCRYPT_CHAIN_MODE_CBC), 0), "Set AES-CBC"); ULONG size{}, written{}; crypto_check(BCryptGetProperty(aes.algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast(&size), sizeof(size), &written, 0), "Get AES object size"); aes.object.resize(size); crypto_check(BCryptGenerateSymmetricKey(aes.algorithm, &aes.key, aes.object.data(), size, const_cast(secret.bytes.data()), static_cast(secret.bytes.size()), 0), "Create AES key"); Secret iv; std::copy(secret.bytes.begin(), secret.bytes.end(), iv.bytes.begin()); Bytes output(encrypted.size()); crypto_check(BCryptDecrypt(aes.key, const_cast(encrypted.data()), static_cast(encrypted.size()), nullptr, iv.bytes.data(), static_cast(iv.bytes.size()), output.data(), static_cast(output.size()), &written, BCRYPT_BLOCK_PADDING), "Decrypt InitFS"); output.resize(written); return output; } struct Entry { std::string name, source; Bytes payload; }; using Entries = std::map; void apply_archive(const fs::path& path, const Secret& key, Entries& effective, Json& sources) { if (!fs::is_regular_file(path)) return; auto input = read_file(path, maximum_archive); if (input.size() <= native_db::envelope_size || std::memcmp(input.data(), native_db::magic, sizeof(native_db::magic))) fail("Unrecognized native envelope in " + path_utf8(path)); const auto body = std::span(input).subspan(native_db::envelope_size); std::size_t consumed{}; auto root = native_db::read(body, "InitFS", &consumed); if (consumed != body.size()) fail("Trailing data in native envelope"); Bytes plaintext; if (const auto* encrypted = root.field("encrypted")) { if (encrypted->type != 19) fail("Encrypted payload has the wrong DB type"); plaintext = decrypt(encrypted->bytes, key); root = native_db::read(plaintext, "InitFS", &consumed); if (consumed != plaintext.size()) fail("Trailing data in decrypted InitFS"); } if (root.type != 1) fail("InitFS root must contain a file list"); Entries entries; bool inherit = false; for (const auto& stub : root.children) { const auto* file = stub.field("$file"); if (!file || file->type != 2) fail("Missing $file envelope"); const auto* name = file->field("name"); const auto* payload = file->field("payload"); if (!name || name->type != 7 || !payload || payload->type != 19) fail("Invalid InitFS file entry"); if (name->text == "__fsinternal__") { const auto metadata = native_db::read(payload->bytes, "InitFS"); const auto* field = metadata.field("inheritContent"); if (field && field->type != 6) fail("Invalid inheritContent metadata"); inherit = field && field->boolean; continue; } // Alternate filesystem destinations cannot be represented by this overlay. if (const auto* file_system = file->field("fs"); file_system && (file_system->type != 7 || (!file_system->text.empty() && file_system->text != "/data"))) fail("Unsupported InitFS filesystem destination for " + name->text); Entry entry{name->text, path_utf8(path), Bytes(payload->bytes.begin(), payload->bytes.end())}; if (!entries.emplace(lower(name->text), std::move(entry)).second) fail("Duplicate InitFS virtual path"); } if (!inherit) effective.clear(); for (auto& [name, entry] : entries) effective.insert_or_assign(name, std::move(entry)); sources.push_back(Json{{"path", path_utf8(path)}, {"sha256", launcher::sha256_file(path)}, {"inherit", inherit}}); } bool within(const fs::path& root, const fs::path& path) { auto parent = root.begin(), child = path.begin(); for (; parent != root.end(); ++parent, ++child) { if (child == path.end() || _wcsicmp(parent->c_str(), child->c_str())) return false; } return true; } bool write_atomic(const fs::path& root, const fs::path& path, std::span bytes, bool replace) { if (!within(root, fs::weakly_canonical(path))) fail("Output path escapes the export directory"); if (!replace && fs::exists(path)) { if (!fs::is_regular_file(path)) fail("Output path is not a regular file: " + path_utf8(path)); return false; } fs::create_directories(path.parent_path()); static std::atomic sequence{}; const auto temporary = fs::path(path.wstring() + L".reskate-" + std::to_wstring(GetCurrentProcessId()) + L"-" + std::to_wstring(GetTickCount64()) + L"-" + std::to_wstring(sequence.fetch_add(1)) + L".tmp"); const auto file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) fail("Cannot create output file: " + path_utf8(path)); DWORD written{}; const bool ready = WriteFile(file, bytes.data(), static_cast(bytes.size()), &written, nullptr) && written == bytes.size() && FlushFileBuffers(file); CloseHandle(file); if (!ready) { DeleteFileW(temporary.c_str()); fail("Cannot write output file: " + path_utf8(path)); } if (MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH | (replace ? MOVEFILE_REPLACE_EXISTING : 0))) return true; const auto error = GetLastError(); DeleteFileW(temporary.c_str()); if (!replace && (error == ERROR_ALREADY_EXISTS || error == ERROR_FILE_EXISTS) && fs::is_regular_file(path)) return false; fail("Cannot publish output file: " + path_utf8(path)); } Json read_preferences(const fs::path& root) { const auto path = root / L"ReSkate.settings.json"; if (!fs::exists(path)) return Json::object(); const auto bytes = read_file(path, 65536); auto settings = Json::parse(std::string_view(reinterpret_cast(bytes.data()), bytes.size())); if (!settings.is_object()) fail("ReSkate.settings.json must contain an object"); if (settings.contains("loose_files") && !settings["loose_files"].is_boolean()) fail("ReSkate.settings.json: loose_files must be true or false"); return settings; } } fs::path utf8_path(std::string_view text) { return fs::path(std::u8string(reinterpret_cast(text.data()), text.size())); } std::optional loose_relative_path(std::string_view input) { if (input.empty() || input.size() > 1024) return {}; std::string path(input); std::replace(path.begin(), path.end(), '\\', '/'); auto key = lower(path); for (const auto prefix : {"/data/", "/fs/", "/native_data/"}) { if (key.starts_with(prefix)) { path.erase(0, std::strlen(prefix)); key = lower(path); break; } } if (path.empty() || path.front() == '/' || path.back() == '/') return {}; for (const auto ch : path) if (static_cast(ch) < 32 || std::string_view(":<>\"|?*").find(ch) != std::string_view::npos) return {}; for (std::size_t begin = 0; begin < path.size();) { const auto end = path.find('/', begin); const auto component = std::string_view(key).substr(begin, (end == std::string::npos ? path.size() : end) - begin); if (component.empty() || component == "." || component == ".." || component.back() == '.' || component.back() == ' ') return {}; const auto stem = component.substr(0, component.find('.')); if (stem == "con" || stem == "prn" || stem == "aux" || stem == "nul" || (stem.size() == 4 && (stem.starts_with("com") || stem.starts_with("lpt")) && stem[3] >= '1' && stem[3] <= '9')) return {}; if (end == std::string::npos) break; begin = end + 1; } const bool scripts = key.starts_with("scripts/"); const auto relative = scripts ? path.substr(8) : path; if (scripts && key.ends_with(".lua")) return "scripts/" + relative; if (key.ends_with(".cfg") || key.ends_with(".yaml") || key.ends_with(".yml") || key.ends_with(".ini") || key.ends_with(".json")) return "config/" + relative; return {}; } fs::path export_manifest(const fs::path& root) { return root / L"scripts" / L".initfs-export.json"; } bool loose_files_preference(const fs::path& root) { const auto settings = read_preferences(root); return settings.contains("loose_files") ? settings["loose_files"].get() : true; } bool loose_files_session_enabled(const fs::path& root) { std::array value{}; if (GetEnvironmentVariableW(L"RESKATE_LOOSE_FILES", value.data(), static_cast(value.size())) == 1) { if (value[0] == L'0') return false; if (value[0] == L'1') return true; } return loose_files_preference(root); } fs::path data_directory(const fs::path& game_directory, const std::vector& arguments) { for (std::size_t index = 0; index < arguments.size(); ++index) { const auto equals = arguments[index].find(L'='); const auto name = arguments[index].substr(0, equals); if (_wcsicmp(name.c_str(), L"-dataPath")) continue; std::wstring value; if (equals != std::wstring::npos) value = arguments[index].substr(equals + 1); else if (++index < arguments.size()) value = arguments[index]; if (value.empty()) fail("-dataPath requires a directory"); return fs::weakly_canonical(game_directory / value); } return fs::is_directory(game_directory / L"ModData") ? fs::weakly_canonical(game_directory / L"ModData" / L"Default") : game_directory; } ExportReport export_files(const fs::path& executable, const fs::path& data_root, const fs::path& output_root, bool first_launch_only) { const auto output = fs::weakly_canonical(fs::absolute(output_root)); const auto marker = export_manifest(output); ExportReport report; if (first_launch_only && fs::is_regular_file(marker)) { report.already_exported = true; return report; } Secret key; read_key(executable, key); Entries entries; Json sources = Json::array(); const auto game = fs::canonical(executable).parent_path(); const auto data = fs::weakly_canonical(data_root); apply_archive(game / L"Data" / L"initfs_win32", key, entries, sources); apply_archive(game / L"Patch" / L"initfs_win32", key, entries, sources); if (_wcsicmp(game.c_str(), data.c_str())) { apply_archive(data / L"Data" / L"initfs_win32", key, entries, sources); apply_archive(data / L"Patch" / L"initfs_win32", key, entries, sources); } if (entries.empty()) fail("No InitFS files were found for this installation"); Json manifest = Json::object(); manifest["format"] = 1; manifest["game_sha256"] = supported_build::game_sha256; manifest["sources"] = std::move(sources); manifest["files"] = Json::array(); std::map destinations; for (const auto& [name, entry] : entries) { (void)name; const auto relative = loose_relative_path(entry.name); if (!relative) { ++report.skipped; continue; } if (!destinations.emplace(lower(*relative), entry.name).second) fail("Colliding loose file paths"); const bool script = relative->starts_with("scripts/"); script ? ++report.scripts : ++report.configs; const auto destination = output / utf8_path(*relative); const bool created = write_atomic(output, destination, entry.payload, false); created ? ++report.created : ++report.preserved; const bool bytecode = entry.payload.size() >= 4 && std::memcmp(entry.payload.data(), "\x1bLua", 4) == 0; manifest["files"].push_back(Json{{"virtual_path", entry.name}, {"loose_path", *relative}, {"source", entry.source}, {"bytes", entry.payload.size()}, {"bytecode", bytecode}}); } manifest["scripts"] = report.scripts; manifest["configs"] = report.configs; manifest["skipped"] = report.skipped; const auto text = manifest.dump(2) + "\n"; write_atomic(output, marker, std::span(reinterpret_cast(text.data()), text.size()), true); return report; } }