summaryrefslogtreecommitdiff
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
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
-rw-r--r--examples/triage/headers.cpp13
-rw-r--r--examples/triage/resources.cpp515
-rw-r--r--examples/triage/resources.h102
-rw-r--r--examples/triage/view.cpp14
-rw-r--r--view/pe/peview.cpp604
5 files changed, 1170 insertions, 78 deletions
diff --git a/examples/triage/headers.cpp b/examples/triage/headers.cpp
index f2165de0..8aff9f59 100644
--- a/examples/triage/headers.cpp
+++ b/examples/triage/headers.cpp
@@ -306,8 +306,19 @@ PEHeaders::PEHeaders(BinaryViewRef data)
AddField("Compiler(s) Used", compilersUsed);
}
+ auto versionInfo = data->QueryMetadata("PEVersionInfo");
+ if (versionInfo && versionInfo->IsKeyValueStore())
+ {
+ for (const auto& [key, value] : versionInfo->GetKeyValueStore())
+ {
+ if (value->IsString() && !value->GetString().empty())
+ AddField(QString::fromStdString(key), QString::fromStdString(value->GetString()));
+ }
+ }
+
SetColumns(3);
- SetRowsPerColumn(9);
+ size_t numFields = GetFields().size();
+ SetRowsPerColumn((numFields + 2) / 3);
}
diff --git a/examples/triage/resources.cpp b/examples/triage/resources.cpp
new file mode 100644
index 00000000..71080e2c
--- /dev/null
+++ b/examples/triage/resources.cpp
@@ -0,0 +1,515 @@
+#include <cstring>
+#include <algorithm>
+#include <QtGui/QClipboard>
+#include <QtGui/QGuiApplication>
+#include <QtCore/QStringList>
+#include <QtWidgets/QMenu>
+#include <QtWidgets/QFileDialog>
+#include <QtWidgets/QVBoxLayout>
+#include <QtCore/QFile>
+#include "resources.h"
+#include "view.h"
+#include "fontsettings.h"
+
+
+static std::string GetResourceFileExtension(const std::string& type)
+{
+ if (type == "RT_ICON" || type == "RT_GROUP_ICON")
+ return ".ico";
+ if (type == "RT_CURSOR" || type == "RT_GROUP_CURSOR")
+ return ".cur";
+ if (type == "RT_BITMAP")
+ return ".bmp";
+ if (type == "RT_HTML")
+ return ".html";
+ if (type == "RT_MANIFEST")
+ return ".xml";
+ if (type == "RT_FONT")
+ return ".fnt";
+ if (type == "RT_VERSION" || type == "RT_STRING" || type == "RT_MESSAGETABLE"
+ || type == "RT_RCDATA" || type == "RT_DLGINCLUDE")
+ return ".bin";
+ return ".bin";
+}
+
+
+ResourcesModel::ResourcesModel(QWidget* parent, BinaryViewRef data) : QAbstractItemModel(parent)
+{
+ m_data = data;
+ m_sortCol = TypeCol;
+ m_sortOrder = Qt::AscendingOrder;
+
+ auto md = m_data->QueryMetadata("PEResources");
+ if (md && md->IsArray())
+ {
+ for (auto& item : md->GetArray())
+ {
+ if (!item->IsKeyValueStore())
+ continue;
+ auto kv = item->GetKeyValueStore();
+
+ ResourceEntry entry;
+ auto getStr = [&](const std::string& key) -> std::string {
+ auto it = kv.find(key);
+ if (it != kv.end() && it->second->IsString())
+ return it->second->GetString();
+ return "";
+ };
+ auto getUint = [&](const std::string& key) -> uint64_t {
+ auto it = kv.find(key);
+ if (it != kv.end() && it->second->IsUnsignedInteger())
+ return it->second->GetUnsignedInteger();
+ return 0;
+ };
+
+ entry.type = getStr("type");
+ entry.typeId = getUint("typeId");
+ entry.name = getStr("name");
+ entry.nameId = getUint("nameId");
+ entry.language = getStr("language");
+ entry.languageId = getUint("languageId");
+ entry.dataRva = getUint("dataRva");
+ entry.dataSize = getUint("dataSize");
+ entry.dataAddress = getUint("dataAddress");
+ entry.codepage = getUint("codepage");
+ entry.preview = getStr("preview");
+
+ m_allEntries.push_back(entry);
+ }
+ }
+ m_entries = m_allEntries;
+}
+
+
+int ResourcesModel::columnCount(const QModelIndex&) const
+{
+ return ColumnCount;
+}
+
+
+int ResourcesModel::rowCount(const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return 0;
+ return (int)m_entries.size();
+}
+
+
+QVariant ResourcesModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid() || index.row() >= (int)m_entries.size())
+ return QVariant();
+
+ const auto& entry = m_entries[index.row()];
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case TypeCol:
+ return QString::fromStdString(entry.type);
+ case NameCol:
+ return QString::fromStdString(entry.name);
+ case LanguageCol:
+ return QString::fromStdString(entry.language);
+ case SizeCol:
+ return QString("0x") + QString::number(entry.dataSize, 16);
+ case AddressCol:
+ return QString("0x") + QString::number(entry.dataAddress, 16);
+ case PreviewCol:
+ return QString::fromStdString(entry.preview);
+ }
+ break;
+ case Qt::ForegroundRole:
+ if (index.column() == AddressCol)
+ return getThemeColor(AddressColor);
+ break;
+ default:
+ break;
+ }
+
+ return QVariant();
+}
+
+
+QVariant ResourcesModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Vertical || role != Qt::DisplayRole)
+ return QVariant();
+
+ switch (section)
+ {
+ case TypeCol: return QString("Type");
+ case NameCol: return QString("Name");
+ case LanguageCol: return QString("Language");
+ case SizeCol: return QString("Size");
+ case AddressCol: return QString("Address");
+ case PreviewCol: return QString("Preview");
+ }
+ return QVariant();
+}
+
+
+QModelIndex ResourcesModel::index(int row, int col, const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return QModelIndex();
+ if (row >= (int)m_entries.size() || col >= ColumnCount)
+ return QModelIndex();
+ return createIndex(row, col);
+}
+
+
+QModelIndex ResourcesModel::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+
+const ResourceEntry* ResourcesModel::getEntry(const QModelIndex& index) const
+{
+ if (!index.isValid() || index.row() >= (int)m_entries.size())
+ return nullptr;
+ return &m_entries[index.row()];
+}
+
+
+void ResourcesModel::performSort(int col, Qt::SortOrder order)
+{
+ std::sort(m_entries.begin(), m_entries.end(), [&](const ResourceEntry& a, const ResourceEntry& b) {
+ bool less = false;
+ bool greater = false;
+ switch (col)
+ {
+ case TypeCol:
+ less = a.type < b.type;
+ greater = a.type > b.type;
+ break;
+ case NameCol:
+ // Sort numerically by ID when both names are numeric (e.g. "#303")
+ less = a.nameId < b.nameId;
+ greater = a.nameId > b.nameId;
+ if (a.nameId == b.nameId)
+ {
+ less = a.name < b.name;
+ greater = a.name > b.name;
+ }
+ break;
+ case LanguageCol:
+ less = a.language < b.language;
+ greater = a.language > b.language;
+ break;
+ case SizeCol:
+ less = a.dataSize < b.dataSize;
+ greater = a.dataSize > b.dataSize;
+ break;
+ case AddressCol:
+ less = a.dataAddress < b.dataAddress;
+ greater = a.dataAddress > b.dataAddress;
+ break;
+ case PreviewCol:
+ less = a.preview < b.preview;
+ greater = a.preview > b.preview;
+ break;
+ }
+ if (order == Qt::AscendingOrder)
+ return less;
+ return greater;
+ });
+}
+
+
+void ResourcesModel::sort(int col, Qt::SortOrder order)
+{
+ beginResetModel();
+ m_sortCol = col;
+ m_sortOrder = order;
+ performSort(col, order);
+ endResetModel();
+}
+
+
+void ResourcesModel::setFilter(const std::string& filterText, FilterOptions options)
+{
+ beginResetModel();
+ m_entries.clear();
+ bool caseSensitive = options.testFlag(FilterOption::CaseSensitiveOption);
+ for (auto& entry : m_allEntries)
+ {
+ if (FilteredView::match(entry.type, filterText, caseSensitive))
+ m_entries.push_back(entry);
+ else if (FilteredView::match(entry.name, filterText, caseSensitive))
+ m_entries.push_back(entry);
+ else if (FilteredView::match(entry.language, filterText, caseSensitive))
+ m_entries.push_back(entry);
+ else if (FilteredView::match(entry.preview, filterText, caseSensitive))
+ m_entries.push_back(entry);
+ }
+ performSort(m_sortCol, m_sortOrder);
+ endResetModel();
+}
+
+
+ResourcesTreeView::ResourcesTreeView(ResourcesWidget* parent, TriageView* view, BinaryViewRef data) : QTreeView(parent)
+{
+ setFont(getMonospaceFont(this));
+
+ m_data = data;
+ m_parent = parent;
+ m_view = view;
+
+ // Allow view-specific shortcuts when resources are focused
+ m_actionHandler.setupActionHandler(this);
+ m_actionHandler.setActionContext([=, this]() { return m_view->actionContext(); });
+
+ m_model = new ResourcesModel(this, m_data);
+ setModel(m_model);
+ setRootIsDecorated(false);
+ setUniformRowHeights(true);
+ setSortingEnabled(true);
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setAllColumnsShowFocus(true);
+ sortByColumn(ResourcesModel::TypeCol, Qt::AscendingOrder);
+ for (int i = 0; i < ResourcesModel::ColumnCount; i++)
+ resizeColumnToContents(i);
+
+ setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(this, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
+ const ResourceEntry* entry = m_model->getEntry(indexAt(pos));
+ if (!entry)
+ return;
+ QMenu menu(this);
+ menu.addAction("Save Resource to File...", this, &ResourcesTreeView::saveSelectedResource);
+ menu.addAction("Save All Resources to Folder...", this, &ResourcesTreeView::saveAllResources);
+ menu.addSeparator();
+ menu.addAction("Copy", this, &ResourcesTreeView::copySelection);
+ menu.exec(viewport()->mapToGlobal(pos));
+ });
+
+ connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &ResourcesTreeView::resourceSelected);
+ connect(this, &QTreeView::doubleClicked, this, &ResourcesTreeView::resourceDoubleClicked);
+
+ m_actionHandler.bindAction("Copy", UIAction([this]() { copySelection(); }, [this]() { return canCopySelection(); }));
+}
+
+
+void ResourcesTreeView::copySelection()
+{
+ if (!model() || !selectionModel())
+ return;
+
+ QModelIndexList rows = selectionModel()->selectedRows();
+ if (rows.isEmpty())
+ return;
+
+ std::sort(rows.begin(), rows.end(), [](const QModelIndex& a, const QModelIndex& b) { return a.row() < b.row(); });
+
+ QStringList lines;
+ for (const QModelIndex& rowIndex : rows)
+ {
+ QStringList cells;
+ for (int column = 0; column < m_model->columnCount(QModelIndex()); column++)
+ {
+ if (isColumnHidden(column))
+ continue;
+
+ QModelIndex idx = m_model->index(rowIndex.row(), column, QModelIndex());
+ cells << m_model->data(idx, Qt::DisplayRole).toString();
+ }
+ lines << cells.join("\t");
+ }
+
+ if (QClipboard* clipboard = QGuiApplication::clipboard())
+ clipboard->setText(lines.join("\n"));
+}
+
+
+bool ResourcesTreeView::canCopySelection() const
+{
+ return !selectionModel()->selectedRows().isEmpty();
+}
+
+
+static std::string SanitizeFilename(std::string name)
+{
+ for (char& c : name)
+ {
+ if (c == '#' || c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|')
+ c = '_';
+ }
+ return name;
+}
+
+
+void ResourcesTreeView::saveSelectedResource()
+{
+ QModelIndexList rows = selectionModel()->selectedRows();
+ if (rows.isEmpty())
+ return;
+
+ const ResourceEntry* entry = m_model->getEntry(rows.first());
+ if (!entry || entry->dataSize == 0)
+ return;
+
+ // Suggest a filename
+ std::string ext = GetResourceFileExtension(entry->type);
+ std::string suggestedName = SanitizeFilename(entry->type);
+ if (!entry->name.empty())
+ suggestedName += "_" + SanitizeFilename(entry->name);
+ if (!entry->language.empty())
+ suggestedName += "_" + SanitizeFilename(entry->language);
+ suggestedName += ext;
+
+ QString path = QFileDialog::getSaveFileName(this, "Save Resource", QString::fromStdString(suggestedName));
+ if (path.isEmpty())
+ return;
+
+ BinaryNinja::DataBuffer buf = m_data->ReadBuffer(entry->dataAddress, entry->dataSize);
+ QFile file(path);
+ if (file.open(QIODevice::WriteOnly))
+ {
+ file.write(reinterpret_cast<const char*>(buf.GetData()), buf.GetLength());
+ file.close();
+ }
+}
+
+
+void ResourcesTreeView::saveAllResources()
+{
+ QString dir = QFileDialog::getExistingDirectory(this, "Save All Resources to Folder");
+ if (dir.isEmpty())
+ return;
+
+ const auto& entries = m_model->getAllEntries();
+ for (const auto& entry : entries)
+ {
+ if (entry.dataSize == 0)
+ continue;
+
+ // Build filename: type/name_language.ext, organized into type subdirectories
+ std::string subdir = SanitizeFilename(entry.type.empty() ? "unknown" : entry.type);
+ QDir typeDir(dir + "/" + QString::fromStdString(subdir));
+ if (!typeDir.exists())
+ typeDir.mkpath(".");
+
+ std::string basename = entry.name.empty() ? "unnamed" : SanitizeFilename(entry.name);
+ if (!entry.language.empty())
+ basename += "_" + SanitizeFilename(entry.language);
+ basename += GetResourceFileExtension(entry.type);
+
+ QString path = typeDir.filePath(QString::fromStdString(basename));
+
+ BinaryNinja::DataBuffer buf = m_data->ReadBuffer(entry.dataAddress, entry.dataSize);
+ QFile file(path);
+ if (file.open(QIODevice::WriteOnly))
+ {
+ file.write(reinterpret_cast<const char*>(buf.GetData()), buf.GetLength());
+ file.close();
+ }
+ }
+}
+
+
+void ResourcesTreeView::resourceSelected(const QModelIndex& cur, const QModelIndex&)
+{
+ const ResourceEntry* entry = m_model->getEntry(cur);
+ if (entry)
+ m_view->setCurrentOffset(entry->dataAddress);
+}
+
+
+void ResourcesTreeView::resourceDoubleClicked(const QModelIndex& cur)
+{
+ const ResourceEntry* entry = m_model->getEntry(cur);
+ if (entry)
+ {
+ ViewFrame* viewFrame = ViewFrame::viewFrameForWidget(this);
+ if (viewFrame)
+ {
+ viewFrame->navigate("Linear:" + viewFrame->getCurrentDataType(), entry->dataAddress);
+ }
+ }
+}
+
+
+void ResourcesTreeView::setFilter(const std::string& filterText, FilterOptions options)
+{
+ m_model->setFilter(filterText, options);
+}
+
+
+void ResourcesTreeView::scrollToFirstItem()
+{
+ scrollToTop();
+}
+
+
+void ResourcesTreeView::scrollToCurrentItem()
+{
+ scrollTo(currentIndex());
+}
+
+
+void ResourcesTreeView::ensureSelection()
+{
+ if (auto current = currentIndex(); !current.isValid())
+ setCurrentIndex(m_model->index(0, 0, QModelIndex()));
+}
+
+
+void ResourcesTreeView::activateSelection()
+{
+ ensureSelection();
+ if (auto current = currentIndex(); current.isValid())
+ resourceDoubleClicked(current);
+}
+
+
+void ResourcesTreeView::closeFilter()
+{
+ setFocus(Qt::OtherFocusReason);
+}
+
+
+void ResourcesTreeView::keyPressEvent(QKeyEvent* event)
+{
+ if ((event->text().size() == 1) && (event->text()[0] > ' ') && (event->text()[0] <= '~'))
+ {
+ m_parent->showFilter(event->text());
+ event->accept();
+ }
+ else if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter))
+ {
+ QList<QModelIndex> sel = selectionModel()->selectedIndexes();
+ if (sel.size() != 0)
+ resourceDoubleClicked(sel[0]);
+ }
+ else if (event->matches(QKeySequence::Copy))
+ {
+ copySelection();
+ event->accept();
+ return;
+ }
+ QTreeView::keyPressEvent(event);
+}
+
+
+ResourcesWidget::ResourcesWidget(QWidget* parent, TriageView* view, BinaryViewRef data) : QWidget(parent)
+{
+ QVBoxLayout* layout = new QVBoxLayout();
+ layout->setContentsMargins(0, 0, 0, 0);
+ ResourcesTreeView* resources = new ResourcesTreeView(this, view, data);
+ m_filter = new FilteredView(this, resources, resources);
+ m_filter->setFilterPlaceholderText("Search resources");
+ layout->addWidget(m_filter, 1);
+ setLayout(layout);
+ setMinimumSize(UIContext::getScaledWindowSize(100, 196));
+}
+
+
+void ResourcesWidget::showFilter(const QString& filter)
+{
+ m_filter->showFilter(filter);
+}
diff --git a/examples/triage/resources.h b/examples/triage/resources.h
new file mode 100644
index 00000000..79869aea
--- /dev/null
+++ b/examples/triage/resources.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <QtCore/QAbstractItemModel>
+#include <QtWidgets/QTreeView>
+#include "filter.h"
+
+
+struct ResourceEntry
+{
+ std::string type;
+ uint64_t typeId;
+ std::string name;
+ uint64_t nameId;
+ std::string language;
+ uint64_t languageId;
+ uint64_t dataRva;
+ uint64_t dataSize;
+ uint64_t dataAddress;
+ uint64_t codepage;
+ std::string preview;
+};
+
+
+class ResourcesModel : public QAbstractItemModel
+{
+ BinaryViewRef m_data;
+ std::vector<ResourceEntry> m_allEntries, m_entries;
+ int m_sortCol;
+ Qt::SortOrder m_sortOrder;
+
+ void performSort(int col, Qt::SortOrder order);
+
+ public:
+ enum Column
+ {
+ TypeCol = 0,
+ NameCol = 1,
+ LanguageCol = 2,
+ SizeCol = 3,
+ AddressCol = 4,
+ PreviewCol = 5,
+ ColumnCount = 6
+ };
+
+ ResourcesModel(QWidget* parent, BinaryViewRef data);
+
+ virtual int columnCount(const QModelIndex& parent) const override;
+ virtual int rowCount(const QModelIndex& parent) const override;
+ virtual QVariant data(const QModelIndex& index, int role) const override;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ virtual QModelIndex index(int row, int col, const QModelIndex& parent) const override;
+ virtual QModelIndex parent(const QModelIndex& index) const override;
+ virtual void sort(int col, Qt::SortOrder order) override;
+ void setFilter(const std::string& filterText, FilterOptions options);
+
+ const ResourceEntry* getEntry(const QModelIndex& index) const;
+ const std::vector<ResourceEntry>& getAllEntries() const { return m_allEntries; }
+};
+
+
+class TriageView;
+class ResourcesWidget;
+
+class ResourcesTreeView : public QTreeView, public FilterTarget
+{
+ BinaryViewRef m_data;
+ ResourcesWidget* m_parent;
+ TriageView* m_view;
+ UIActionHandler m_actionHandler;
+ ResourcesModel* m_model;
+
+ public:
+ ResourcesTreeView(ResourcesWidget* parent, TriageView* view, BinaryViewRef data);
+ void copySelection();
+ bool canCopySelection() const;
+ void saveSelectedResource();
+ void saveAllResources();
+
+ virtual void setFilter(const std::string& filterText, FilterOptions options) override;
+ virtual void scrollToFirstItem() override;
+ virtual void scrollToCurrentItem() override;
+ virtual void ensureSelection() override;
+ virtual void activateSelection() override;
+ virtual void closeFilter() override;
+
+ protected:
+ virtual void keyPressEvent(QKeyEvent* event) override;
+
+ private Q_SLOTS:
+ void resourceSelected(const QModelIndex& cur, const QModelIndex& prev);
+ void resourceDoubleClicked(const QModelIndex& cur);
+};
+
+
+class ResourcesWidget : public QWidget
+{
+ FilteredView* m_filter;
+
+ public:
+ ResourcesWidget(QWidget* parent, TriageView* view, BinaryViewRef data);
+ void showFilter(const QString& filter);
+};
diff --git a/examples/triage/view.cpp b/examples/triage/view.cpp
index 0e995ae2..67eb27aa 100644
--- a/examples/triage/view.cpp
+++ b/examples/triage/view.cpp
@@ -6,6 +6,7 @@
#include "entry.h"
#include "imports.h"
#include "exports.h"
+#include "resources.h"
#include "sections.h"
#include "fileinfo.h"
#include "librariesinfo.h"
@@ -119,6 +120,19 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent
if (sectionsWidget->GetSections().size() == 0)
sectionsGroup->hide();
+ if (m_data->GetTypeName() == "PE")
+ {
+ auto resourcesMd = m_data->QueryMetadata("PEResources");
+ if (resourcesMd && resourcesMd->IsArray() && !resourcesMd->GetArray().empty())
+ {
+ QGroupBox* resourcesGroup = new QGroupBox("Resources", container);
+ QVBoxLayout* resourcesLayout = new QVBoxLayout();
+ resourcesLayout->addWidget(new ResourcesWidget(resourcesGroup, this, m_data));
+ resourcesGroup->setLayout(resourcesLayout);
+ layout->addWidget(resourcesGroup);
+ }
+ }
+
QGroupBox* analysisInfoGroup = new QGroupBox("Analysis Info", container);
QVBoxLayout* analysisInfoLayout = new QVBoxLayout();
analysisInfoLayout->addWidget(new AnalysisInfoWidget(analysisInfoGroup, m_data));
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)