diff --git a/lsfg-vk-backend/src/modules/library/dll.cpp b/lsfg-vk-backend/src/modules/library/dll.cpp new file mode 100644 index 0000000..8e13f50 --- /dev/null +++ b/lsfg-vk-backend/src/modules/library/dll.cpp @@ -0,0 +1,212 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include "dll.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + /// DOS file header + struct DOSHeader { + uint16_t magic; // 0x5A4D + std::array pad; + int32_t pe_offset; // File offset + }; + + /// PE header + struct PEHeader { + uint32_t signature; // "PE\0\0" + std::array pad1; + uint16_t sect_count; + std::array pad2; + uint16_t opt_hdr_size; + std::array pad3; + }; + + /// (partial!) PE optional header + struct PEOptionalHeader { + uint16_t magic; // 0x20B + std::array pad4; + std::pair resource_table; // File offset/size + }; + + /// Section header + struct SectionHeader { + std::array pad1; + uint32_t vsize; // Virtual + uint32_t vaddress; + uint32_t fsize; // Raw + uint32_t foffset; + std::array pad2; + }; + + /// Resource directory + struct ResourceDirectory { + std::array pad; + uint16_t name_count; + uint16_t id_count; + }; + + /// Resource directory entry + struct ResourceDirectoryEntry { + uint32_t id; + uint32_t offset; // High bit = Directory + }; + + /// Resource data entry + struct ResourceDataEntry { + uint32_t offset; + uint32_t size; + std::array pad; + }; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-warning-option" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage-in-container" + /// Safely cast a vector to a pointer of type T + template + const T* safe_cast(const std::vector& data, size_t offset) { + const size_t end{offset + sizeof(T)}; + if (end > data.size() || end < offset) + throw std::runtime_error("Buffer overflow/underflow during safe cast"); + return reinterpret_cast(&data.at(offset)); // NOLINT (unsafe cast) + } + + /// Safely cast a vector to a span of T + template + std::span span_cast(const std::vector& data, size_t offset, size_t count) { + const size_t end{offset + (count * sizeof(T))}; + if (end > data.size() || end < offset) + throw std::runtime_error("Buffer overflow/underflow during safe cast"); + return{ reinterpret_cast(&data.at(offset)), count }; // NOLINT (unsafe cast) + } +#pragma clang diagnostic pop +} + +using namespace lsfgvk::library; + +std::unordered_map> priv::parseDll( + const std::filesystem::path& dll +) { + std::ifstream file(dll, std::ios::binary | std::ios::ate); + if (!file.is_open()) + throw std::runtime_error("Unable to open file"); + + const std::streamsize size{static_cast(file.tellg())}; + file.seekg(0, std::ios::beg); + + std::vector data(static_cast(size)); + if (!file.read(reinterpret_cast(data.data()), size)) // NOLINT (unsafe cast) + throw std::runtime_error("Unable to read file"); + + // Parse dos header + size_t fileOffset{0}; + const auto* dosHdr{safe_cast(data, 0)}; + if (dosHdr->magic != 0x5A4D) + throw std::runtime_error("Magic number in DOS header is incorrect"); + + // Parse pe header + fileOffset += static_cast(dosHdr->pe_offset); + const auto* peHdr{safe_cast(data, fileOffset)}; + if (peHdr->signature != 0x00004550) + throw std::runtime_error("Signature in PE header is incorrect"); + + // Parse optional pe header + fileOffset += sizeof(PEHeader); + const auto* peOptHdr{safe_cast(data, fileOffset)}; + if (peOptHdr->magic != 0x20B) + throw std::runtime_error("PE format is not PE32+"); + const auto& [rsrc_rva, rsrc_size] = peOptHdr->resource_table; + + // }Locate section containing resources + std::optional rsrc_offset; + fileOffset += peHdr->opt_hdr_size; + const auto sectHdrs{span_cast(data, fileOffset, peHdr->sect_count)}; + for (const auto& sectHdr : sectHdrs) { + if (rsrc_rva < sectHdr.vaddress || rsrc_rva > (sectHdr.vaddress + sectHdr.vsize)) + continue; + + rsrc_offset.emplace((rsrc_rva - sectHdr.vaddress) + sectHdr.foffset); + break; + } + if (!rsrc_offset) + throw std::runtime_error("Unable to locate resource section"); + + // Parse resource directory + fileOffset = rsrc_offset.value(); + const auto* rsrcDir{safe_cast(data, fileOffset)}; + if (rsrcDir->id_count < 3) + throw std::runtime_error("Resource directory does not have enough entries"); + + // Find resource table with data type + std::optional rsrc_tbl_offset; + fileOffset = rsrc_offset.value() + sizeof(ResourceDirectory); + const auto rsrcDirEntries{span_cast( + data, fileOffset, rsrcDir->name_count + rsrcDir->id_count)}; + for (const auto& rsrcDirEntry : rsrcDirEntries) { + if (rsrcDirEntry.id != 10) // RT_RCDATA + continue; + if ((rsrcDirEntry.offset & 0x80000000) == 0) + throw std::runtime_error("Expected resource directory, found data entry"); + + rsrc_tbl_offset.emplace(rsrcDirEntry.offset & 0x7FFFFFFF); + } + if (!rsrc_tbl_offset) + throw std::runtime_error("Unable to locate RT_RCDATA directory"); + + // Parse data type resource directory + fileOffset = rsrc_offset.value() + rsrc_tbl_offset.value(); + const auto* rsrcTbl{safe_cast(data, fileOffset)}; + if (rsrcTbl->id_count < 1) + throw std::runtime_error("RT_RCDATA directory does not have enough entries"); + + // Collect all resources + fileOffset += sizeof(ResourceDirectory); + const auto rsrcTblEntries{span_cast( + data, fileOffset, rsrcTbl->name_count + rsrcTbl->id_count)}; + + std::unordered_map> resources; + resources.reserve(rsrcTbl->id_count); + + for (const auto& rsrcTblEntry : rsrcTblEntries) { + if ((rsrcTblEntry.offset & 0x80000000) == 0) + throw std::runtime_error("Expected resource directory, found data entry"); + + // Skip over language directory + fileOffset = rsrc_offset.value() + (rsrcTblEntry.offset & 0x7FFFFFFF); + const auto* langDir = safe_cast(data, fileOffset); + if (langDir->id_count < 1) + throw std::runtime_error("Malformed language directory"); + + fileOffset += sizeof(ResourceDirectory); + const auto* langDirEntry{safe_cast(data, fileOffset)}; + if ((langDirEntry->offset & 0x80000000) != 0) + throw std::runtime_error("Expected resource data entry, found directory"); + + // Parse resource data entry + fileOffset = rsrc_offset.value() + (langDirEntry->offset & 0x7FFFFFFF); + const auto* entry{safe_cast(data, fileOffset)}; + if (entry->offset < rsrc_rva || entry->offset > (rsrc_rva + rsrc_size)) + throw std::runtime_error("Resource data entry points outside resource section"); + + // Extract resource + fileOffset = (entry->offset - rsrc_rva) + rsrc_offset.value(); + const auto rdata{span_cast( + data, fileOffset, entry->size / sizeof(uint32_t))}; + + resources.emplace(rsrcTblEntry.id, std::vector(rdata.begin(), rdata.end())); + } + + return resources; +} diff --git a/lsfg-vk-backend/src/modules/library/dll.hpp b/lsfg-vk-backend/src/modules/library/dll.hpp new file mode 100644 index 0000000..3a43642 --- /dev/null +++ b/lsfg-vk-backend/src/modules/library/dll.hpp @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#pragma once + +#include +#include +#include +#include + +namespace lsfgvk::library::priv { + + /// + /// Parse all resources from a DLL file + /// + /// @param dll File path + /// @returns Map of resource ID to data + /// @throws std::runtime_error if the file is invalid or cannot be read + /// + std::unordered_map> parseDll( + const std::filesystem::path& dll + ); + +}