QtDebugger/src/resourceexplorer.cpp

327 lines
9.2 KiB
C++

#include "resourceexplorer.h"
#include "ui_resourceexplorer.h"
#include <QFile>
#include <QFileInfo>
#include <QDateTime>
#include <QDebug>
#include <QFileDialog>
#include <endian.h>
#include <list>
#include "injector.h"
#pragma pack (push, 1)
struct ResourceLibraryEntry {
uint32_t nameOffset;
uint16_t flags; // use ResourceFlags
union Content {
struct Directory {
uint32_t childCount;
uint32_t childOffset;
} directory;
struct File {
uint16_t country;
uint16_t language;
uint32_t dataOffset;
} file;
} content;
};
struct ResourceLibraryEntryV2 : public ResourceLibraryEntry {
uint64_t lastModTimeMS;
};
struct ResouceNameEntry {
uint16_t length;
uint32_t hash;
// string content
char str[0];
};
struct ResouceDataEntry {
uint32_t length;
char data[0];
};
#pragma pack (pop)
ResourceExplorer::ResourceExplorer( QWidget* parent ) : QMainWindow( parent ), ui( new Ui::ResourceExplorer ) {
this->ui->setupUi(this);
this->rebuild();
}
ResourceExplorer::~ResourceExplorer() {
delete this->ui;
}
void ResourceExplorer::extractButtonPressed() {
QTreeWidgetItem* item = this->ui->treeWidget->currentItem();
if ( !item ) return;
ResourceNode* node = (ResourceNode*) item->data(0, Qt::UserRole).toULongLong();
if ( !node ) return;
QString saveFile = QFileDialog::getSaveFileName(this, "Extract File");
if ( saveFile.isEmpty() ) return;
QFile fileOut( saveFile );
if( !fileOut.open( QIODevice::WriteOnly | QIODevice::Truncate )) return;
QByteArray data = QByteArray::fromRawData( (const char*) node->start, node->size );
if ( node->isCompressed() ) {
data = qUncompress(data);
}
fileOut.write(data);
}
void ResourceExplorer::itemSelected(QTreeWidgetItem* newItem) {
if ( !newItem ) return;
ResourceNode* node = (ResourceNode*) newItem->data(0, Qt::UserRole).toULongLong();
if ( node ) {
if ( !node->isDir() ) {
this->ui->extractButton->setEnabled( true );
return;
}
}
this->ui->extractButton->setEnabled( false );
}
ResourceExplorer::ResourceNode::~ResourceNode() {
for ( ResourceNode* rn : this->children ) {
delete rn;
}
}
bool ResourceExplorer::ResourceNode::isDir() const {
return this->flags & ResourceFlags::Directory;
}
bool ResourceExplorer::ResourceNode::isCompressed() const {
return this->flags & ResourceFlags::Compressed;
}
uint64_t ResourceExplorer::ResourceNode::getByteSize() const {
if ( this->isDir() ) {
return std::accumulate( children.begin(), children.end(), 0, [](uint64_t v, const ResourceNode* b) { return v + b->getByteSize(); } );
}
return size;
}
void ResourceExplorer::rebuild() {
this->ui->treeWidget->clear();
this->ui->extractButton->setEnabled(false);
if ( !registeredResources || registeredResources->empty() ) {
QTreeWidgetItem* resItem = new QTreeWidgetItem();
resItem->setText( 0, "No Resources Captured" );
this->ui->treeWidget->addTopLevelItem( resItem );
return;
}
std::vector<MemoryMap> memMaps = readMemoryMaps();
for ( auto it : *registeredResources ) {
// find correct memory map
auto memMapIt = std::find_if(memMaps.cbegin(), memMaps.cend(), [c = it.second](const MemoryMap& mm) { return mm.begin < c->resourceStruct && mm.end > c->resourceStruct; } );
QString fileName( "<file unknown>" );
QString filePath( "" );
if ( memMapIt != memMaps.cend() ) {
fileName = QFileInfo( memMapIt->path ).fileName();
filePath = memMapIt->path;
}
QTreeWidgetItem* resItem = new QTreeWidgetItem( );
resItem->setText( 0, fileName );
resItem->setToolTip( 0, filePath );
std::shared_ptr<RegisteredResource> r = it.second;
std::vector<ResourceNode*> resources = parseResource(r->version, r->resourceStruct, r->resourceName, r->resourceData);
for ( ResourceNode* rnode : resources ) {
QTreeWidgetItem* item = this->resourceNodeToItem( rnode, "" );
resItem->addChild( item );
}
this->ui->treeWidget->addTopLevelItem( resItem );
}
}
std::vector<ResourceExplorer::MemoryMap> ResourceExplorer::readMemoryMaps() {
QFile maps("/proc/self/maps");
if ( !maps.open( QIODevice::ReadOnly | QIODevice::Text ) ) {
qWarning() << "failed to read MemoryMaps";
return {};
}
// QTextStream stream(&maps);
std::vector<MemoryMap> out;
out.reserve( maps.size() / 100 ); // approximation of row count
do {
QString line = QString::fromUtf8( maps.readLine() );
if( line.isEmpty() ) continue;
QVector<QStringRef> parts = line.splitRef(' ', QString::SplitBehavior::SkipEmptyParts );
if ( parts.size() != 6) {
continue;
}
QVector<QStringRef> borders = parts.at(0).split('-');
if ( borders.size() != 2) continue;
bool ok = true;
const unsigned char* begin = (const unsigned char*) borders.at( 0 ).toULongLong( &ok, 16);
if ( !ok ) continue;
const unsigned char* end = (const unsigned char*) borders.at( 1 ).toULongLong( &ok, 16);
if ( !ok ) continue;
if ( begin > end ) std::swap( begin, end );
out.push_back( MemoryMap{ begin, end, parts.at( 5 ).toString().simplified() } );
} while( !maps.atEnd() );
return out;
}
// reads an UTF-16 String into a QString
// the problem is that the input string is in bigendian.
// this function makes a copy and swaps every two bytes if required
static QString readUTF16String(const char16_t* ptr, uint32_t size) {
char16_t* newptr = new char16_t[size];
for ( uint32_t i = 0; i < size; ++i ) {
newptr[i] = be16toh(ptr[i]);
}
QString out = QString::fromUtf16(newptr, size);
delete[] newptr;
return out;
}
std::vector<ResourceExplorer::ResourceNode*> ResourceExplorer::parseResource(int version, ResourceExplorer::data libraryIndex, ResourceExplorer::data names, ResourceExplorer::data data ) {
std::vector<ResourceNode*> out;
if ( version > 2 ) {
qWarning() << "resource has unknown version: " << version;
return out;
}
const uint32_t libraryOffset = ( version == 1 ) ? sizeof(ResourceLibraryEntry) : sizeof(ResourceLibraryEntryV2);
using task_t = std::pair<uint32_t, std::vector<ResourceNode*>*>;
std::list<task_t> pendingOffsets;
pendingOffsets.push_back( {0, &out} );
while ( pendingOffsets.size() > 0) {
task_t task = pendingOffsets.front();
uint32_t offset = task.first;
pendingOffsets.pop_front();
const ResourceLibraryEntryV2* entry = (const ResourceLibraryEntryV2*) &libraryIndex[ offset * libraryOffset ];
ResourceNode* newNode = new ResourceNode();
newNode->flags = (ResourceFlags) be16toh( entry->flags );
if( offset == 0) {
newNode->name = ":";
newNode->hash = 0;
} else {
uint32_t nameOffset = be32toh(entry->nameOffset);
uint16_t nameLen = be16toh(*(uint16_t*) (names + nameOffset));
newNode->name = readUTF16String((const char16_t*) (names + nameOffset + 6), nameLen);
newNode->hash = *((uint16_t*) ( names + nameOffset + 2));
}
if ( newNode->flags & ResourceFlags::Directory ) {
// directory
newNode->size = 0;
newNode->country = 0;
newNode->language = 0;
uint32_t childCount = be32toh( entry->content.directory.childCount );
uint32_t childOffset = be32toh( entry->content.directory.childOffset );
for ( uint32_t i = 0; i < childCount; ++i ) {
pendingOffsets.push_back( { i + childOffset, &(newNode->children) } );
}
} else {
// file
newNode->country = be16toh(entry->content.file.country);
newNode->language = be16toh(entry->content.file.language);
uint32_t dataOffset = be32toh(entry->content.file.dataOffset);
const unsigned char* dataBegin = data + dataOffset;
newNode->size = be32toh(*((const uint32_t*) dataBegin));
newNode->start = dataBegin + 4;
}
if ( version >= 2 ) {
newNode->lastModTimeMS = be64toh( entry->lastModTimeMS );
}
task.second->push_back( newNode );
}
return out;
}
QTreeWidgetItem* ResourceExplorer::resourceNodeToItem( ResourceNode* node, const QString& path ) {
if ( ! node ) return nullptr;
QTreeWidgetItem* item = new QTreeWidgetItem();
const QString ownPath = path + node->name;
item->setText( 0, node->isDir() ? (node->name + "/" ) : node->name);
if ( node->isDir() ) {
std::size_t size = node->children.size();
item->setText(1, QString::number( size ));
uint64_t filesize = node->getByteSize();
item->setToolTip(1, QString("TotalSize: %1 (%2 Bytes)").arg(approxFileSize(filesize)).arg( filesize ));
item->setToolTip(0, ownPath);
} else {
std::size_t size = node->size;
item->setText( 1, approxFileSize( size ) );
item->setToolTip( 1, QString::number( size ) );
QString tooltip = ownPath;
QLocale::Language lang = (QLocale::Language) node->language;
QLocale::Country country = (QLocale::Country) node->country;
QLocale fileLocale(lang, country);
tooltip += (node->isCompressed() ? "\nCompressed" : "\nUncompressed")
+ ((fileLocale != QLocale()) ? ("\nLocale: " + fileLocale.name() ) : "" );
item->setToolTip(0, tooltip);
}
if( node->lastModTimeMS ) {
QDateTime lastModTime = QDateTime::fromMSecsSinceEpoch( node->lastModTimeMS );
item->setText( 2, lastModTime.toString( "yyyy-MM-dd HH:mm:ss" ) );
} else {
item->setText( 2, "-" );
}
for ( ResourceNode* subnode : node->children ) {
QTreeWidgetItem* sub = resourceNodeToItem( subnode, ownPath + "/" );
item->addChild( sub );
}
QVariant ptrVariant( (qulonglong) node );
item->setData(0, Qt::UserRole, ptrVariant);
return item;
}
QString ResourceExplorer::approxFileSize( uint64_t size ) {
return QLocale().formattedDataSize( size );
}