quickmod/FuseMounter/fusearchive.cpp
Daniel O'Neill 191cd5362a Lots of fixes, VFS now seems to work reliably. Still a lot of cleanup and shoring to do.
Decided to leave the VFS tab for now. Filtering likely doesn't work, except Show Only Open. Sorting works.
Rewrites to replace dynamicRoles done for Mods and Plugins tables.
Database.qml now reads "moddir", though installing extracted is still unimplemented so far.
Launch still doesn't work.
Ignore the "file -> test extract". It's a test for file extraction.
2025-11-10 13:15:26 -08:00

465 lines
14 KiB
C++

#include "fusearchive.h"
#include "FuseMounter/fuseinterface.h"
#include "fusebase.h"
#include <QDebug>
#include <QDir>
#include <QSqlQuery>
#include <time.h>
FuseAccessorArchive::FuseAccessorArchive(FuseInterface *parent, QSqlDatabase &database, int modId, const QString &modPath)
: FuseAccessorBase(parent, modPath, FuseAccessorBase::FH_ARCHIVE),
m_valid{false},
m_stream{nullptr},
m_archive{nullptr},
m_database{database},
m_id{modId}
{
m_stream = ar_open_file(m_resource.toStdString().c_str());
if( !m_stream ) {
qDebug() << "Failed to open archive stream:" << m_resource;
//return false;
return;
}
if( m_resource.endsWith("7z", Qt::CaseInsensitive) )
m_archive = ar_open_7z_archive(m_stream);
else if( m_resource.endsWith("rar", Qt::CaseInsensitive) )
m_archive = ar_open_rar_archive(m_stream);
else if( m_resource.endsWith("zip", Qt::CaseInsensitive) )
m_archive = ar_open_zip_archive(m_stream, false);
else {
qDebug() << "Unsupported archive type:" << m_resource;
return;
}
if( !m_archive )
qDebug() << "Failed to open archive:" << m_resource;
m_entries = readEntries();
m_manifest = readManifest();
m_valid = true;
//return NULL != m_archive;
}
FuseAccessorArchive::~FuseAccessorArchive()
{
if( m_archive )
ar_close_archive(m_archive);
if( m_stream )
ar_close(m_stream);
m_archive = NULL;
m_stream = NULL;
}
QMap< QString, FuseArchiveEntry > FuseAccessorArchive::readEntries()
{
QMap< QString, FuseArchiveEntry > results;
qDebug() << "FuseAccessorArchive::readEntries:" << m_resource;
while( ar_parse_entry(m_archive) )
{
const char *entryname = ar_entry_get_name(m_archive);
if( !entryname || strlen(entryname) <= 0 )
continue;
/*
if( (st->st_mode & S_IFMT) == S_IFDIR )
continue;
*/
time64_t filetime = ar_entry_get_filetime(m_archive) / 100000000;
FuseArchiveEntry ent;
ent.m_actual = QString::fromUtf8(entryname);
QString lpath = ent.m_actual.toLower();
QStringList lparts = lpath.split(QDir::separator(), Qt::SkipEmptyParts);
ent.m_path = lparts.join(QDir::separator());
ent.m_offset = ar_entry_get_offset(m_archive);
memset( &ent.m_stat, 0, sizeof(struct stat));
ent.m_stat.st_size = ar_entry_get_size(m_archive);
ent.m_stat.st_mode = S_IFREG | 0755;
ent.m_stat.st_nlink = 1;
ent.m_stat.st_uid = geteuid();
ent.m_stat.st_gid = getegid();
ent.m_stat.st_atim.tv_sec = filetime;
ent.m_stat.st_mtim.tv_sec = filetime;
ent.m_stat.st_ctim.tv_sec = filetime;
results.insert( lpath, ent );
qDebug().nospace() << " + Adding " << lpath << "/" << ent.m_stat.st_size << " bytes at offset " << ent.m_offset;
}
return results;
}
QList< FuseArchiveMapping > FuseAccessorArchive::readManifest()
{
QList< FuseArchiveMapping > results;
QSqlQuery q = QSqlQuery(m_database);
q.prepare("SELECT dest, lower(source) FROM files WHERE modId=?");
q.bindValue(0, m_id);
if( !q.exec() )
return results;
while( q.next() )
{
QString dest = q.value(0).toString();
QString source = q.value(1).toString();
if( !m_entries.contains(source) )
{
qDebug() << "FuseAccessorArchive::readManifest: Missing entry" << source << "in archive!";
continue;
}
FuseArchiveMapping mapping;
mapping.m_entry = m_entries[source];
mapping.m_actual = dest;
mapping.m_path = dest.toLower();
results.append(mapping);
}
return results;
}
QList< QPair< int, QString > > FuseAccessorArchive::activeArchives(QSqlDatabase &database, int profileId)
{
QString qstr = QString("SELECT modId, filename FROM mods WHERE moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId);
qDebug() << qstr;
QSqlQuery q = QSqlQuery(qstr, database);
//QSqlQuery q = QSqlQuery("SELECT modId, filename FROM mods WHERE enabled != 0 ORDER BY idx DESC", m_database);
QList< QPair< int, QString > > results;
while( q.next() )
{
QPair< int, QString > pair( q.value(0).toInt(), q.value(1).toString() );
results << pair;
}
return results;
}
int FuseAccessorArchive::makeWritable(struct fuse_file_info *fi, const QString &origsource, const QString &path, const QByteArray &fullData, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset)
{
FuseFHBase *fh = m_parent->makeWritableFromData(fi, fullData, origsource, path, mode);
if( !fh )
return -1;
return fh->write(data, offset, fi);
}
/*******************/
int FuseAccessorArchive::getattr(const QString &path, struct stat *stbuf)
{
if( m_statCache.contains(path) )
{
if( NULL == m_statCache[path] )
return -ENOENT;
memcpy( stbuf, m_statCache[path], sizeof(struct stat) );
return 0;
}
QString lpath = path.toLower().mid(1).split( QDir::separator(), Qt::SkipEmptyParts ).join(QDir::separator());
int lplen = lpath.length();
if( lpath.isEmpty() )
{
memset( stbuf, 0, sizeof(struct stat) );
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
stbuf->st_uid = geteuid();
stbuf->st_gid = getegid();
time_t t = ::time(NULL);
stbuf->st_atim.tv_sec = t;
stbuf->st_ctim.tv_sec = t;
stbuf->st_mtim.tv_sec = t;
m_statCache[path] = new struct stat;
memcpy( m_statCache[path], stbuf, sizeof(struct stat) );
return 0;
}
for( const FuseArchiveMapping &mapping : std::as_const(m_manifest) )
{
int mplen = mapping.m_path.length();
if( mplen < lplen )
continue;
if( !mapping.m_path.startsWith(lpath) )
continue;
if( mplen > lplen && QDir::separator() == mapping.m_path.at(lplen) )
{
qDebug() << "FuseAccessorArchive::getattr: Found dir" << mapping.m_path << "in" << lpath;
memset( stbuf, 0, sizeof(struct stat) );
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
stbuf->st_uid = geteuid();
stbuf->st_gid = getegid();
time_t t = ::time(NULL);
stbuf->st_atim.tv_sec = t;
stbuf->st_ctim.tv_sec = t;
stbuf->st_mtim.tv_sec = t;
m_statCache[path] = new struct stat;
memcpy( m_statCache[path], stbuf, sizeof(struct stat) );
return 0;
}
if( mplen == lplen )
{
const FuseArchiveEntry &arcent = mapping.m_entry;
memcpy( stbuf, &arcent.m_stat, sizeof(struct stat) );
qDebug() << "FuseAccessorArchive::getattr: Found file" << mapping.m_path << "in" << lpath;
m_statCache[path] = new struct stat;
memcpy( m_statCache[path], stbuf, sizeof(struct stat) );
return 0;
}
}
//qDebug() << "Not found:" << path;
m_statCache[path] = NULL;
return -ENOENT;
}
QStringList FuseAccessorArchive::readdir(const QString &path)
{
if( m_dirCache.contains(path) )
return m_dirCache[path];
QStringList results;
QString lpath = path.toLower().mid(1).split( QDir::separator(), Qt::SkipEmptyParts ).join(QDir::separator());
int lplen = lpath.length();
//qDebug() << "FuseAccessorArchive::readdir:" << m_resource << " / " << lpath;
for( const FuseArchiveMapping &mapping : std::as_const(m_manifest) )
{
int mplen = mapping.m_path.length();
if( lplen >= mplen )
continue;
if( !mapping.m_path.startsWith(lpath) )
continue;
/* Avoid cases where a dir and filename share a starting:
* eg: Data/f4ee, Data/f4ee.ini, etc.
*/
if( QDir::separator() != mapping.m_path.at(lpath.length()) )
continue;
QStringList parts = mapping.m_path.mid(lplen).toLower().split( QDir::separator(), Qt::SkipEmptyParts );
QString nent = parts.at(0);
if( results.contains(nent) )
continue;
qDebug() << "FuseAccessorArchive::readdir:" << m_resource << "- Found" << nent << "inside" << lpath;
results.append(nent);
}
m_dirCache.insert(path, results);
return results;
}
#define BLOCKSIZE 8192
FuseFHBase *FuseAccessorArchive::open(const QString &path, QIODeviceBase::OpenMode mode)
{
QString lpath = path.toLower().mid(1).split( QDir::separator(), Qt::SkipEmptyParts ).join(QDir::separator());
int lplen = lpath.length();
QString npath;
for( const FuseArchiveMapping &mapping : std::as_const(m_manifest) )
{
int mplen = mapping.m_path.length();
if( lplen != mplen )
continue;
if( lpath != mapping.m_path )
continue;
npath = mapping.m_path; //mapping.m_actual;
break;
}
if( npath.isEmpty() )
return NULL;
qDebug() << "FuseAccessorArchive::open:" << path << mode << "->" << npath;
FuseFHArchive *arcent = new FuseFHArchive(this, path, npath);
if( !arcent->open(mode) )
{
arcent->deleteLater();
return NULL;
}
//m_open_handles.insert( path, arcent );
return arcent;
}
bool FuseAccessorArchive::slurpData(const QString &path, const QString &origpath)
{
/*
if( m_parent->cacheGrab(path) )
return true;
*/
int plen = path.length();
FuseArchiveEntry entry;
bool foundIt = false;
for( const FuseArchiveMapping &mapping : std::as_const(m_manifest) )
{
int mplen = mapping.m_path.length();
//qDebug() << "cmp:" << path << "vs." << mapping.m_path;
if( plen != mplen )
continue;
if( path != mapping.m_path )
continue;
entry = mapping.m_entry;
foundIt = true;
break;
}
if( !foundIt ) {
qDebug() << "FuseAccessorArchive::slurpData: Couldn't find" << path;
return false;
}
m_mutex.lock();
qDebug() << "Cheaty skip to" << entry.m_offset << "bytes...";
if( !ar_parse_entry_at(m_archive, entry.m_offset) ) {
qDebug() << "Archive::getEntry: ar_parse_entry_at failed!";
m_mutex.unlock();
deleteLater();
return false;
}
char buff[BLOCKSIZE * 256];
size_t len = sizeof(buff);
if( len > (size_t)entry.m_stat.st_size )
len = entry.m_stat.st_size;
QByteArray data;
data.reserve(len);
while( ar_entry_uncompress(m_archive, buff, len ) ) {
data.append(buff, len);
len = entry.m_stat.st_size - data.length();
if( len <= 0 )
break;
if( len > sizeof(buff) )
len = sizeof(buff);
}
m_mutex.unlock();
qDebug() << "Caching" << path << "which is" << data.length() << "bytes.";
m_parent->cacheInsert(this, origpath, data);
return true;
}
QSharedPointer<QByteArray> FuseAccessorArchive::cacheGrab(const QString &path)
{
if(QSharedPointer<QByteArray> sptr = m_parent->cacheGrab(path))
return sptr;
return {};
}
FuseFHArchive::FuseFHArchive(FuseAccessorBase *parent, const QString &path, const QString &newpath)
: FuseFHBase(parent, path),
m_accessCount{0},
m_newpath{newpath}
{
m_archive = qobject_cast<FuseAccessorArchive *>(parent);
}
FuseFHArchive::~FuseFHArchive()
{
FuseFHArchive::release();
}
bool FuseFHArchive::open(QIODeviceBase::OpenMode mode)
{
m_mode = mode;
qDebug() << "FuseFHArchive::open:" << m_path << "->" << m_newpath;
FuseAccessorArchive *accessor = qobject_cast<FuseAccessorArchive *>(m_parent);
//QString lpath = m_path.toLower();
//FuseArchiveEntry &entry = accessor->m_entries[lpath];
QSharedPointer<QByteArray> entry = m_archive->cacheGrab(m_path);
if( entry )
return true;
m_accessCount++;
m_lastAccess = time(NULL);
return accessor->slurpData(m_newpath, m_path);
}
QByteArray FuseFHArchive::read(size_t size, off_t offset)
{
m_lastAccess = time(NULL);
QSharedPointer<QByteArray> data = m_archive->cacheGrab(m_path);
if( !data )
{
if( !m_archive->slurpData(m_newpath, m_path) )
return QByteArray();
data = m_archive->cacheGrab(m_path);
if( !data )
return QByteArray();
}
return data->mid(offset, size);
}
int FuseFHArchive::write(const QByteArray &data, off_t offset, struct fuse_file_info *fi)
{
if( m_mode & QIODeviceBase::ReadOnly )
return -1;
//FuseArchiveEntry &entry = m_entries[lpath];
qDebug() << "FuseFHArchive::write: Upgrading" << m_newpath << "to writable" << m_path;
FuseAccessorArchive *accessor = qobject_cast<FuseAccessorArchive *>(m_parent);
QSharedPointer<QByteArray> entry = m_archive->cacheGrab(m_path);
if( !entry )
{
if( !m_archive->slurpData(m_newpath, m_path) )
return -EINVAL;
entry = m_archive->cacheGrab(m_path);
if( !entry )
return -EINVAL;
}
QByteArray dcopy(entry->constData(), entry->length());
int rval = accessor->makeWritable(fi, m_path, m_newpath, dcopy, m_mode, data, offset);
release();
qDebug() << "FuseFHArchive::write: is now writable. So long, pardner!" << rval;
deleteLater();
return rval;
}
off_t FuseFHArchive::lseek(off_t off, int whence)
{
Q_UNUSED(whence)
m_lastAccess = time(NULL);
QSharedPointer<QByteArray> data = m_archive->cacheGrab(m_path);
if( !data )
return -EINVAL;
return off > 0 && off < data->length() ? off : -EINVAL;
}
void FuseFHArchive::release()
{
/*
FuseAccessorArchive *accessor = qobject_cast<FuseAccessorArchive *>(m_parent);
accessor->m_open_handles.take(m_path)->deleteLater();
*/
}