summaryrefslogtreecommitdiff
path: root/view/pe
diff options
context:
space:
mode:
authorXusheng <xusheng@vector35.com>2026-03-04 17:04:53 +0800
committerXusheng <xusheng@vector35.com>2026-03-05 12:59:40 +0800
commit432e144beeccf1d9a2d9ed9e7d016384eed0bb32 (patch)
treef963730f2b0b18cbc0620a755e6aad415228c2dc /view/pe
parent3520262824637979418554656607433424896809 (diff)
Parse and display PE resource information in triage view. Fix https://github.com/Vector35/binaryninja-api/issues/4052, fix https://github.com/Vector35/binaryninja-api/issues/5607
Diffstat (limited to 'view/pe')
-rw-r--r--view/pe/peview.cpp604
1 files changed, 527 insertions, 77 deletions
diff --git a/view/pe/peview.cpp b/view/pe/peview.cpp
index ffe9c91a..d7e49dd3 100644
--- a/view/pe/peview.cpp
+++ b/view/pe/peview.cpp
@@ -22,6 +22,47 @@ using namespace std;
static PEViewType* g_peViewType = nullptr;
static const char* imageDirName[] = { "exportTable", "importTable", "resourceTable", "exceptionTable", "certificateTable", "baseRelocationTable", "debug", "architecture", "globalPtr", "tlsTable", "loadConfigTable", "boundImport", "iat", "delayImportDescriptor", "clrRuntimeHeader", "reserved"};
+
+static const char* GetResourceTypeName(uint32_t id)
+{
+ switch (id)
+ {
+ case 1: return "RT_CURSOR";
+ case 2: return "RT_BITMAP";
+ case 3: return "RT_ICON";
+ case 4: return "RT_MENU";
+ case 5: return "RT_DIALOG";
+ case 6: return "RT_STRING";
+ case 7: return "RT_FONTDIR";
+ case 8: return "RT_FONT";
+ case 9: return "RT_ACCELERATOR";
+ case 10: return "RT_RCDATA";
+ case 11: return "RT_MESSAGETABLE";
+ case 12: return "RT_GROUP_CURSOR";
+ case 14: return "RT_GROUP_ICON";
+ case 16: return "RT_VERSION";
+ case 17: return "RT_DLGINCLUDE";
+ case 19: return "RT_PLUGPLAY";
+ case 20: return "RT_VXD";
+ case 21: return "RT_ANICURSOR";
+ case 22: return "RT_ANIICON";
+ case 23: return "RT_HTML";
+ case 24: return "RT_MANIFEST";
+ default: return nullptr;
+ }
+}
+
+
+struct ResourceParseItem
+{
+ uint64_t tableAddr;
+ uint32_t depth; // 0=root, 1=type children, 2=name children
+ std::string typeName; // resolved at depth 0
+ std::string resourceName;// resolved at depth 1
+ uint32_t typeId; // raw type ID
+ uint32_t nameId; // raw name/ID
+};
+
void BinaryNinja::InitPEViewType()
{
static PEViewType type;
@@ -2657,8 +2698,6 @@ bool PEView::Init()
try
{
- //TODO: properly name tables, entries, data entries
-
PEDataDirectory dir;
// Read resource directory
if (m_dataDirs.size() > IMAGE_DIRECTORY_ENTRY_RESOURCE)
@@ -2707,7 +2746,51 @@ bool PEView::Init()
string resourceDataEntryTypeId = Type::GenerateAutoTypeId("pe", resourceDataEntryName);
QualifiedName resourceDataEntryTypeName = DefineType(resourceDataEntryTypeId, resourceDataEntryName, resourceDataEntryType);
- std::list<uint64_t> tableAddrsToParse = {dir.virtualAddress};
+ // Helper: convert UTF-16LE code units to UTF-8
+ auto utf16ToUtf8 = [](const std::vector<uint16_t>& codeUnits) -> std::string {
+ std::string result;
+ result.reserve(codeUnits.size());
+ for (uint16_t ch : codeUnits)
+ {
+ if (ch < 0x80)
+ result.push_back(static_cast<char>(ch));
+ else if (ch < 0x800)
+ {
+ result.push_back(static_cast<char>(0xC0 | (ch >> 6)));
+ result.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
+ }
+ else
+ {
+ result.push_back(static_cast<char>(0xE0 | (ch >> 12)));
+ result.push_back(static_cast<char>(0x80 | ((ch >> 6) & 0x3F)));
+ result.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
+ }
+ }
+ return result;
+ };
+
+ // Helper lambda to read a UTF-16LE resource name string and convert to UTF-8
+ auto readResourceName = [&](uint64_t nameRva) -> std::string {
+ if (nameRva < dir.virtualAddress || nameRva >= dir.virtualAddress + dir.size)
+ return "";
+ BinaryReader nameReader(GetParentView(), LittleEndian);
+ nameReader.Seek(RVAToFileOffset(nameRva));
+ uint16_t nameLen = nameReader.Read16();
+ if (nameLen == 0 || nameRva + 2 + (nameLen * 2) > dir.virtualAddress + dir.size)
+ return "";
+
+ // Define the wide char array data variable
+ DefineDataVariable(m_imageBase + nameRva + 2, Type::ArrayType(Type::WideCharType(2), nameLen));
+
+ std::vector<uint16_t> codeUnits(nameLen);
+ for (uint16_t i = 0; i < nameLen; i++)
+ codeUnits[i] = nameReader.Read16();
+ return utf16ToUtf8(codeUnits);
+ };
+
+ // Path-tracking BFS traversal of the resource directory tree
+ std::list<ResourceParseItem> itemsToParse;
+ itemsToParse.push_back({dir.virtualAddress, 0, "", "", 0, 0});
std::unordered_set<uint64_t> visitedTables;
uint64_t maxTableCount = 10000;
@@ -2715,7 +2798,24 @@ bool PEView::Init()
maxTableCount = settings->Get<uint64_t>("loader.pe.maxResourceDirectoryTableCount", this);
uint32_t resourceDirectoryTableNum = 0;
- while (!tableAddrsToParse.empty())
+
+ // Collect resource leaf entries for metadata
+ std::vector<std::map<std::string, Ref<Metadata>>> resourceEntries;
+ // Track used symbol names to handle duplicates
+ std::unordered_map<std::string, int> usedSymbolNames;
+
+ auto makeUniqueSymbolName = [&](const std::string& baseName) -> std::string {
+ auto it = usedSymbolNames.find(baseName);
+ if (it == usedSymbolNames.end())
+ {
+ usedSymbolNames[baseName] = 1;
+ return baseName;
+ }
+ int count = it->second++;
+ return fmt::format("{}_{}", baseName, count);
+ };
+
+ while (!itemsToParse.empty())
{
// Check for safety limit to prevent infinite parsing
if (resourceDirectoryTableNum >= maxTableCount)
@@ -2724,97 +2824,135 @@ bool PEView::Init()
break;
}
- uint64_t tableAddr = tableAddrsToParse.front();
- tableAddrsToParse.pop_front();
+ ResourceParseItem item = itemsToParse.front();
+ itemsToParse.pop_front();
// Cycle detection - skip if we've already processed this table
- if (visitedTables.count(tableAddr))
+ if (visitedTables.count(item.tableAddr))
{
- m_logger->LogWarn("Resource directory cycle detected at RVA 0x%" PRIx64 ", skipping", tableAddr);
+ m_logger->LogWarn("Resource directory cycle detected at RVA 0x%" PRIx64 ", skipping", item.tableAddr);
continue;
}
- visitedTables.insert(tableAddr);
+ visitedTables.insert(item.tableAddr);
// Bounds check - ensure table address is within resource section
- if (tableAddr < dir.virtualAddress || tableAddr >= dir.virtualAddress + dir.size)
+ if (item.tableAddr < dir.virtualAddress || item.tableAddr >= dir.virtualAddress + dir.size)
{
- m_logger->LogWarn("Resource directory table at RVA 0x%" PRIx64 " is outside resource section bounds", tableAddr);
+ m_logger->LogWarn("Resource directory table at RVA 0x%" PRIx64 " is outside resource section bounds", item.tableAddr);
continue;
}
- // Read in next directory entry
- reader.Seek(RVAToFileOffset(tableAddr));
- PEResourceDirectoryTable importDirTable;
- importDirTable.characteristics = reader.Read32();
- importDirTable.timeDateStamp = reader.Read32();
- importDirTable.majorVersion = reader.Read16();
- importDirTable.minorVersion = reader.Read16();
- importDirTable.numNameEntries = reader.Read16();
- importDirTable.numIdEntries = reader.Read16();
+ // Read in next directory table
+ reader.Seek(RVAToFileOffset(item.tableAddr));
+ PEResourceDirectoryTable dirTable;
+ dirTable.characteristics = reader.Read32();
+ dirTable.timeDateStamp = reader.Read32();
+ dirTable.majorVersion = reader.Read16();
+ dirTable.minorVersion = reader.Read16();
+ dirTable.numNameEntries = reader.Read16();
+ dirTable.numIdEntries = reader.Read16();
- DefineDataVariable(m_imageBase + tableAddr, Type::NamedType(this, resourceDirTableTypeName));
- DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}", resourceDirectoryTableNum), m_imageBase + tableAddr, NoBinding));
+ // Build a descriptive symbol name for this directory table
+ std::string tableSymName;
+ if (item.depth == 0)
+ tableSymName = "__pe_rsrc_root";
+ else if (item.depth == 1)
+ tableSymName = makeUniqueSymbolName(fmt::format("__pe_rsrc_{}", item.typeName));
+ else
+ tableSymName = makeUniqueSymbolName(fmt::format("__pe_rsrc_{}_{}", item.typeName, item.resourceName));
+
+ DefineDataVariable(m_imageBase + item.tableAddr, Type::NamedType(this, resourceDirTableTypeName));
+ DefineAutoSymbol(new Symbol(DataSymbol, tableSymName, m_imageBase + item.tableAddr, NoBinding));
// All the Name entries precede all the ID entries for the table but we treat them the same
- // All entries for the table are sorted in ascending order: the Name entries by case-sensitive string and the ID entries by numeric value.
// Offsets are relative to the address in the IMAGE_DIRECTORY_ENTRY_RESOURCE DataDirectory.
- // Offset value:
- // High bit 0. Address of a Resource Data entry (a leaf).
- // High bit 1. The lower 31 bits are the address of another resource directory table (the next level down).
+ struct PendingDataEntry {
+ size_t offset;
+ std::string typeName;
+ std::string resourceName;
+ std::string langName;
+ uint32_t typeId;
+ uint32_t nameId;
+ uint32_t langId;
+ };
+ std::vector<PendingDataEntry> pendingDataEntries;
- std::vector<size_t> dataEntryOffsets;
-
- size_t numTableEntries = importDirTable.numNameEntries + importDirTable.numIdEntries;
+ size_t numTableEntries = dirTable.numNameEntries + dirTable.numIdEntries;
if (numTableEntries > 0)
{
for (size_t entryNum = 0; entryNum < numTableEntries; entryNum++)
{
- PEResourceDirectoryEntry importDirEntry;
- importDirEntry.id = reader.Read32();
- importDirEntry.offset = reader.Read32();
-
- if (importDirEntry.id & 0x80000000)
- {
- // Name entry
- // First 2 bytes of name are length
+ PEResourceDirectoryEntry dirEntry;
+ dirEntry.id = reader.Read32();
+ dirEntry.offset = reader.Read32();
- size_t nameAddr = dir.virtualAddress + (importDirEntry.id ^ 0x80000000);
+ // Resolve the name/ID for this entry based on depth
+ std::string entryName;
+ uint32_t entryRawId = dirEntry.id & 0x7FFFFFFF;
- // Bounds check - ensure name address is within resource section
- if (nameAddr < dir.virtualAddress || nameAddr >= dir.virtualAddress + dir.size)
+ if (dirEntry.id & 0x80000000)
+ {
+ // Name entry — UTF-16LE string at the offset
+ uint64_t nameAddr = dir.virtualAddress + entryRawId;
+ std::string uniName = readResourceName(nameAddr);
+ if (!uniName.empty())
+ entryName = uniName;
+ else
+ entryName = fmt::format("name_{:x}", entryRawId);
+ }
+ else
+ {
+ // ID entry — resolve based on depth
+ if (item.depth == 0)
{
- m_logger->LogWarn("Resource name at RVA 0x%zx is outside resource section bounds, skipping", nameAddr);
- continue;
+ // Type ID
+ const char* typeName = GetResourceTypeName(entryRawId);
+ if (typeName)
+ entryName = typeName;
+ else
+ entryName = fmt::format("type_{}", entryRawId);
}
-
- BinaryReader nameReader(GetParentView(), LittleEndian);
- nameReader.Seek(RVAToFileOffset(nameAddr));
-
- uint16_t nameLen = nameReader.Read16();
-
- // Check that the full name fits within the resource section
- // nameLen is in UTF-16 code-units, each 2 bytes, plus 2 bytes for the length prefix
- if (nameAddr + 2 + (nameLen * 2) > dir.virtualAddress + dir.size)
+ else if (item.depth == 1)
{
- m_logger->LogWarn("Resource name extends beyond resource section bounds, skipping");
- continue;
+ // Resource name/ID
+ entryName = fmt::format("#{}", entryRawId);
+ }
+ else
+ {
+ // Language ID
+ entryName = fmt::format("lang{}", entryRawId);
}
+ }
+
+ // Build child item context
+ std::string childTypeName = item.typeName;
+ std::string childResourceName = item.resourceName;
+ uint32_t childTypeId = item.typeId;
+ uint32_t childNameId = item.nameId;
- // Plus 2 because it's length-prefixed
- DefineDataVariable(m_imageBase + nameAddr + 2, Type::ArrayType(Type::WideCharType(2), nameLen));
+ if (item.depth == 0)
+ {
+ childTypeName = entryName;
+ childTypeId = entryRawId;
+ }
+ else if (item.depth == 1)
+ {
+ childResourceName = entryName;
+ childNameId = entryRawId;
}
- if (importDirEntry.offset & 0x80000000)
+ if (dirEntry.offset & 0x80000000)
{
// Lower 31 bits are address of another table
- uint64_t nextTableAddr = dir.virtualAddress + (importDirEntry.offset ^ 0x80000000);
+ uint64_t nextTableAddr = dir.virtualAddress + (dirEntry.offset ^ 0x80000000);
// Bounds check before adding to queue to prevent invalid references
if (nextTableAddr >= dir.virtualAddress && nextTableAddr < dir.virtualAddress + dir.size)
{
- tableAddrsToParse.push_back(nextTableAddr);
+ itemsToParse.push_back({nextTableAddr, item.depth + 1,
+ childTypeName, childResourceName, childTypeId, childNameId});
}
else
{
@@ -2823,32 +2961,54 @@ bool PEView::Init()
}
else
{
- // Address of data entry
- // Validate offset is within resource section bounds
- uint64_t dataEntryAddr = dir.virtualAddress + importDirEntry.offset;
+ // Address of data entry (leaf)
+ uint64_t dataEntryAddr = dir.virtualAddress + dirEntry.offset;
if (dataEntryAddr >= dir.virtualAddress && dataEntryAddr + sizeof(PEResourceDataEntry) <= dir.virtualAddress + dir.size)
{
- dataEntryOffsets.push_back(importDirEntry.offset);
+ std::string langName;
+ uint32_t langId = 0;
+ if (item.depth >= 2)
+ {
+ // This shouldn't happen in a well-formed PE (depth 2 entries point to data),
+ // but handle gracefully
+ langName = entryName;
+ langId = entryRawId;
+ }
+ else if (item.depth == 1)
+ {
+ // We're at the name level, entry is language
+ langName = entryName;
+ langId = entryRawId;
+ }
+ else
+ {
+ langName = entryName;
+ langId = entryRawId;
+ }
+
+ pendingDataEntries.push_back({(size_t)dirEntry.offset,
+ childTypeName, childResourceName, langName,
+ childTypeId, childNameId, langId});
}
else
{
- m_logger->LogWarn("Resource data entry at offset 0x%" PRIx32 " is outside resource section bounds, skipping", importDirEntry.offset);
+ m_logger->LogWarn("Resource data entry at offset 0x%" PRIx32 " is outside resource section bounds, skipping", dirEntry.offset);
}
}
}
- size_t tableEntriesStart = m_imageBase + tableAddr + sizeof(PEResourceDirectoryTable);
+ size_t tableEntriesStart = m_imageBase + item.tableAddr + sizeof(PEResourceDirectoryTable);
DefineDataVariable(tableEntriesStart, Type::ArrayType(Type::NamedType(this, resourceDirEntryTypeName), numTableEntries));
- DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}_entries", resourceDirectoryTableNum), tableEntriesStart, NoBinding));
+ std::string entriesSymName = tableSymName + "_entries";
+ DefineAutoSymbol(new Symbol(DataSymbol, entriesSymName, tableEntriesStart, NoBinding));
}
- // Create BinaryReader once outside the loop for better performance
+ // Process data entries (leaves)
BinaryReader entryReader(GetParentView(), LittleEndian);
- for(size_t dataEntryNum = 0; dataEntryNum < dataEntryOffsets.size(); dataEntryNum++)
+ for (auto& pending : pendingDataEntries)
{
- size_t entryOffset = dataEntryOffsets[dataEntryNum];
- entryReader.Seek(RVAToFileOffset(dir.virtualAddress + entryOffset));
+ entryReader.Seek(RVAToFileOffset(dir.virtualAddress + pending.offset));
PEResourceDataEntry dataEntry;
dataEntry.dataRva = entryReader.Read32();
dataEntry.dataSize = entryReader.Read32();
@@ -2856,12 +3016,8 @@ bool PEView::Init()
dataEntry.reserved = entryReader.Read32();
if (dataEntry.reserved != 0)
- {
- // Invalid entry, this needs to be 0
continue;
- }
- // Limit excessively large data sizes
const uint32_t MAX_REASONABLE_RESOURCE_SIZE = 100 * 1024 * 1024; // 100 MB
if (dataEntry.dataSize > MAX_REASONABLE_RESOURCE_SIZE)
{
@@ -2869,7 +3025,6 @@ bool PEView::Init()
continue;
}
- // Check for overflow when adding dataRva + dataSize
if (dataEntry.dataSize > 0)
{
uint64_t dataEnd = (uint64_t)dataEntry.dataRva + dataEntry.dataSize;
@@ -2880,20 +3035,315 @@ bool PEView::Init()
}
}
- size_t entryAddr = m_imageBase + dir.virtualAddress + entryOffset;
+ // Build descriptive path-based symbol names
+ std::string pathPrefix;
+ if (!pending.typeName.empty() && !pending.resourceName.empty() && !pending.langName.empty())
+ pathPrefix = fmt::format("__pe_rsrc_{}_{}_{}", pending.typeName, pending.resourceName, pending.langName);
+ else if (!pending.typeName.empty() && !pending.resourceName.empty())
+ pathPrefix = fmt::format("__pe_rsrc_{}_{}", pending.typeName, pending.resourceName);
+ else if (!pending.typeName.empty())
+ pathPrefix = fmt::format("__pe_rsrc_{}", pending.typeName);
+ else
+ pathPrefix = fmt::format("__pe_rsrc_entry_{}", resourceDirectoryTableNum);
+ size_t entryAddr = m_imageBase + dir.virtualAddress + pending.offset;
DefineDataVariable(entryAddr, Type::NamedType(this, resourceDataEntryTypeName));
- DefineAutoSymbol(new Symbol(DataSymbol, fmt::format("__resource_directory_table_{}_data_entry_{}", resourceDirectoryTableNum, dataEntryNum), entryAddr, NoBinding));
+ DefineAutoSymbol(new Symbol(DataSymbol, makeUniqueSymbolName(pathPrefix + "_data_entry"), entryAddr, NoBinding));
- //TODO: properly name based on path taken to get here
if (dataEntry.dataSize > 0)
{
DefineDataVariable(m_imageBase + dataEntry.dataRva, Type::ArrayType(Type::IntegerType(1, true), dataEntry.dataSize));
+ DefineAutoSymbol(new Symbol(DataSymbol, makeUniqueSymbolName(pathPrefix + "_data"), m_imageBase + dataEntry.dataRva, NoBinding));
}
+
+ // Generate preview for parseable resource types
+ std::string preview;
+ if (pending.typeId == 6 && dataEntry.dataSize > 0) // RT_STRING
+ {
+ try
+ {
+ BinaryReader strReader(GetParentView(), LittleEndian);
+ strReader.Seek(RVAToFileOffset(dataEntry.dataRva));
+ size_t bytesRead = 0;
+ std::vector<std::string> strings;
+ for (int strIdx = 0; strIdx < 16 && bytesRead < dataEntry.dataSize; strIdx++)
+ {
+ uint16_t strLen = strReader.Read16();
+ bytesRead += 2;
+ if (strLen == 0)
+ continue;
+ if (bytesRead + strLen * 2 > dataEntry.dataSize)
+ break;
+ std::vector<uint16_t> codeUnits(strLen);
+ for (uint16_t i = 0; i < strLen; i++)
+ codeUnits[i] = strReader.Read16();
+ bytesRead += strLen * 2;
+ std::string s = utf16ToUtf8(codeUnits);
+ if (!s.empty())
+ strings.push_back(s);
+ }
+ for (size_t i = 0; i < strings.size(); i++)
+ {
+ if (i > 0)
+ preview += ", ";
+ if (preview.size() + strings[i].size() > 200)
+ {
+ preview += "...";
+ break;
+ }
+ preview += strings[i];
+ }
+ }
+ catch (...) {}
+ }
+
+ // Collect metadata for this resource leaf
+ std::map<std::string, Ref<Metadata>> entry;
+ entry["type"] = new Metadata(pending.typeName);
+ entry["typeId"] = new Metadata((uint64_t)pending.typeId);
+ entry["name"] = new Metadata(pending.resourceName);
+ entry["nameId"] = new Metadata((uint64_t)pending.nameId);
+ entry["language"] = new Metadata(pending.langName);
+ entry["languageId"] = new Metadata((uint64_t)pending.langId);
+ entry["dataRva"] = new Metadata((uint64_t)dataEntry.dataRva);
+ entry["dataSize"] = new Metadata((uint64_t)dataEntry.dataSize);
+ entry["dataAddress"] = new Metadata((uint64_t)(m_imageBase + dataEntry.dataRva));
+ entry["codepage"] = new Metadata((uint64_t)dataEntry.dataCodePage);
+ entry["preview"] = new Metadata(preview);
+ resourceEntries.push_back(entry);
}
resourceDirectoryTableNum++;
}
+
+ // Parse RT_VERSION resource and store version info metadata
+ for (auto& resEntry : resourceEntries)
+ {
+ uint64_t typeId = resEntry["typeId"]->GetUnsignedInteger();
+ if (typeId != 16) // RT_VERSION
+ continue;
+ uint64_t dataAddr = resEntry["dataAddress"]->GetUnsignedInteger();
+ uint64_t dataSize = resEntry["dataSize"]->GetUnsignedInteger();
+ if (dataSize < 6 || dataSize > 65536)
+ break;
+
+ try
+ {
+ BinaryReader vr(GetParentView(), LittleEndian);
+ vr.Seek(RVAToFileOffset(resEntry["dataRva"]->GetUnsignedInteger()));
+ size_t baseOffset = vr.GetOffset();
+
+ uint16_t viLength = vr.Read16();
+ uint16_t viValueLength = vr.Read16();
+ uint16_t viType = vr.Read16();
+ (void)viType;
+
+ // Read and verify "VS_VERSION_INFO" key
+ std::vector<uint16_t> keyChars;
+ for (int i = 0; i < 20; i++)
+ {
+ uint16_t ch = vr.Read16();
+ if (ch == 0) break;
+ keyChars.push_back(ch);
+ }
+ std::string keyStr = utf16ToUtf8(keyChars);
+ if (keyStr != "VS_VERSION_INFO")
+ break;
+
+ // Align to DWORD boundary
+ size_t pos = vr.GetOffset() - baseOffset;
+ if (pos % 4 != 0)
+ vr.SeekRelative(4 - (pos % 4));
+
+ // Parse VS_FIXEDFILEINFO
+ std::map<std::string, Ref<Metadata>> versionInfo;
+ if (viValueLength >= 52)
+ {
+ uint32_t sig = vr.Read32();
+ if (sig == 0xFEEF04BD)
+ {
+ vr.Read32(); // dwStrucVersion
+ uint32_t fileVerMS = vr.Read32();
+ uint32_t fileVerLS = vr.Read32();
+ uint32_t prodVerMS = vr.Read32();
+ uint32_t prodVerLS = vr.Read32();
+
+ std::string fileVer = fmt::format("{}.{}.{}.{}",
+ (fileVerMS >> 16) & 0xFFFF, fileVerMS & 0xFFFF,
+ (fileVerLS >> 16) & 0xFFFF, fileVerLS & 0xFFFF);
+ std::string prodVer = fmt::format("{}.{}.{}.{}",
+ (prodVerMS >> 16) & 0xFFFF, prodVerMS & 0xFFFF,
+ (prodVerLS >> 16) & 0xFFFF, prodVerLS & 0xFFFF);
+
+ versionInfo["FileVersion"] = new Metadata(fileVer);
+ versionInfo["ProductVersion"] = new Metadata(prodVer);
+
+ // Skip rest of VS_FIXEDFILEINFO (7 more DWORDs):
+ // fileFlagsMask, fileFlags, fileOS, fileType, fileSubtype, fileDateMS, fileDateLS
+ for (int i = 0; i < 7; i++)
+ vr.Read32();
+ }
+ }
+
+ // Align after VS_FIXEDFILEINFO
+ pos = vr.GetOffset() - baseOffset;
+ if (pos % 4 != 0)
+ vr.SeekRelative(4 - (pos % 4));
+
+ // Parse StringFileInfo / VarFileInfo children
+ size_t endOffset = baseOffset + std::min((size_t)viLength, (size_t)dataSize);
+ while ((size_t)vr.GetOffset() + 6 < endOffset)
+ {
+ size_t childBase = vr.GetOffset();
+ uint16_t childLength = vr.Read16();
+ uint16_t childValueLength = vr.Read16();
+ uint16_t childType = vr.Read16();
+ (void)childValueLength;
+ (void)childType;
+
+ if (childLength < 6 || childBase + childLength > baseOffset + dataSize)
+ break;
+
+ // Read child key
+ std::vector<uint16_t> childKeyChars;
+ for (int i = 0; i < 30; i++)
+ {
+ if ((size_t)vr.GetOffset() >= childBase + childLength)
+ break;
+ uint16_t ch = vr.Read16();
+ if (ch == 0) break;
+ childKeyChars.push_back(ch);
+ }
+ std::string childKey = utf16ToUtf8(childKeyChars);
+
+ if (childKey == "StringFileInfo")
+ {
+ // Align
+ pos = vr.GetOffset() - baseOffset;
+ if (pos % 4 != 0)
+ vr.SeekRelative(4 - (pos % 4));
+
+ size_t sfiEnd = childBase + childLength;
+ // Parse StringTable(s)
+ while ((size_t)vr.GetOffset() + 6 < sfiEnd)
+ {
+ size_t stBase = vr.GetOffset();
+ uint16_t stLength = vr.Read16();
+ vr.Read16(); // stValueLength
+ vr.Read16(); // stType
+
+ if (stLength < 6 || stBase + stLength > sfiEnd)
+ break;
+
+ // Skip StringTable key (language+codepage like "040904b0")
+ for (int i = 0; i < 12; i++)
+ {
+ if ((size_t)vr.GetOffset() >= stBase + stLength)
+ break;
+ uint16_t ch = vr.Read16();
+ if (ch == 0) break;
+ }
+
+ // Align
+ pos = vr.GetOffset() - baseOffset;
+ if (pos % 4 != 0)
+ vr.SeekRelative(4 - (pos % 4));
+
+ size_t stEnd = stBase + stLength;
+ // Parse individual String entries
+ while ((size_t)vr.GetOffset() + 6 < stEnd)
+ {
+ size_t strBase = vr.GetOffset();
+ uint16_t strLength = vr.Read16();
+ uint16_t strValueLength = vr.Read16();
+ uint16_t strType = vr.Read16();
+ (void)strType;
+
+ if (strLength < 6 || strBase + strLength > stEnd)
+ break;
+
+ // Read key
+ std::vector<uint16_t> strKeyChars;
+ for (int i = 0; i < 80; i++)
+ {
+ if ((size_t)vr.GetOffset() >= strBase + strLength)
+ break;
+ uint16_t ch = vr.Read16();
+ if (ch == 0) break;
+ strKeyChars.push_back(ch);
+ }
+ std::string strKey = utf16ToUtf8(strKeyChars);
+
+ // Align
+ pos = vr.GetOffset() - baseOffset;
+ if (pos % 4 != 0)
+ vr.SeekRelative(4 - (pos % 4));
+
+ // Read value
+ std::string strValue;
+ if (strValueLength > 0 && (size_t)vr.GetOffset() < strBase + strLength)
+ {
+ std::vector<uint16_t> valChars;
+ uint16_t charsToRead = strValueLength;
+ for (uint16_t i = 0; i < charsToRead; i++)
+ {
+ if ((size_t)vr.GetOffset() >= strBase + strLength)
+ break;
+ uint16_t ch = vr.Read16();
+ if (ch == 0) break;
+ valChars.push_back(ch);
+ }
+ strValue = utf16ToUtf8(valChars);
+ }
+
+ if (!strKey.empty() && !strValue.empty())
+ versionInfo[strKey] = new Metadata(strValue);
+
+ // Advance to next String entry
+ vr.Seek(strBase + ((strLength + 3) & ~3));
+ }
+
+ // Advance to next StringTable
+ vr.Seek(stBase + ((stLength + 3) & ~3));
+ }
+ }
+
+ // Advance to next child
+ vr.Seek(childBase + ((childLength + 3) & ~3));
+ }
+
+ if (!versionInfo.empty())
+ {
+ StoreMetadata("PEVersionInfo", new Metadata(versionInfo), true);
+
+ // Set preview on this resource entry
+ std::string verPreview;
+ auto fv = versionInfo.find("FileVersion");
+ if (fv != versionInfo.end())
+ verPreview = fv->second->GetString();
+ auto cn = versionInfo.find("CompanyName");
+ if (cn != versionInfo.end())
+ {
+ if (!verPreview.empty())
+ verPreview += " - ";
+ verPreview += cn->second->GetString();
+ }
+ resEntry["preview"] = new Metadata(verPreview);
+ }
+ }
+ catch (...) {}
+ break; // Only parse first RT_VERSION
+ }
+
+ // Store resource tree as metadata for programmatic access
+ if (!resourceEntries.empty())
+ {
+ std::vector<Ref<Metadata>> metadataArray;
+ metadataArray.reserve(resourceEntries.size());
+ for (auto& entry : resourceEntries)
+ metadataArray.push_back(new Metadata(entry));
+ StoreMetadata("PEResources", new Metadata(metadataArray), true);
+ }
}
}
catch (std::exception& e)