From 892070f3dec1dc690983fd862880e37e711c2516 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sun, 12 Apr 2026 17:55:56 -0500 Subject: Add BSA in-memory extraction and nested preview support bsatk: - Archive::extractToMemory() extracts a file directly into a byte buffer instead of a filesystem path, backed by a std::ostringstream sink. - Archive::findFile() locates a file by relative path, case-insensitive and separator-agnostic, walking the folder tree recursively. - extractDirect/extractCompressed now take std::ostream& so both the ofstream and ostringstream paths share the same implementation. libbsarch: shim rewrite on Linux to map the Windows API surface onto bsatk::Archive, enabling preview_bsa to open vanilla BSAs via the same interface as other plugins. preview_bsa: double-click on a file in the BSA tree now extracts the bytes via extractToMemory() and hands them to IOrganizer::previewFileData for nested preview rendering (e.g. NIF inside a BSA). uibase + organizerproxy + organizercore: new IOrganizer::previewFileData virtual that routes in-memory file bytes through the first registered IPluginPreview matching the extension. Uses heap + WA_DeleteOnClose + open() to avoid nested exec() softlock from mod info filetree -> preview_bsa tree -> nif preview. docker/build-inner.sh: stage preview_nif shaders into plugin_data/shaders so ShaderManager can load them via IOrganizer::getPluginDataPath(). Co-Authored-By: Claude Opus 4.6 (1M context) --- libs/preview_bsa/src/previewbsa.cpp | 79 ++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 14 deletions(-) (limited to 'libs/preview_bsa/src/previewbsa.cpp') diff --git a/libs/preview_bsa/src/previewbsa.cpp b/libs/preview_bsa/src/previewbsa.cpp index e21df2f..9941f06 100644 --- a/libs/preview_bsa/src/previewbsa.cpp +++ b/libs/preview_bsa/src/previewbsa.cpp @@ -19,6 +19,7 @@ along with Bsa Preview plugin. If not, see . #include "previewbsa.h" #include +#include #include #include #include @@ -26,6 +27,7 @@ along with Bsa Preview plugin. If not, see . #include #include #include +#include #include #include #include @@ -35,8 +37,14 @@ along with Bsa Preview plugin. If not, see . #include #include +#include "simplefiletreeitem.h" #include "simplefiletreemodel.h" +#include + +#include +#include + using namespace MOBase; PreviewBsa::PreviewBsa() : m_MOInfo(nullptr) {} @@ -132,29 +140,30 @@ QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&) QVBoxLayout* layout = new QVBoxLayout(); QLabel* infoLabel = new QLabel(); - BSA::Archive arch; // bs_archive_auto is easier to use, but is less performant when - // working with memory - BSA::EErrorCode res = arch.read(fileName.toLocal8Bit().constData(), true); + + // Kept alive via shared_ptr captured by the double-click lambda below so + // we can extract files from the still-open archive on demand without + // re-reading it each time. + auto archive = std::make_shared(); + BSA::EErrorCode res = + archive->read(fileName.toLocal8Bit().constData(), true); if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { log::error("invalid bsa '{}', error {}", fileName, res); infoLabel->setText("Unable to parse archive. Unrecognized format."); - arch.close(); + archive->close(); return wrapper; } - const BSA::Folder::Ptr archiveDir = arch.getRoot(); + const BSA::Folder::Ptr archiveDir = archive->getRoot(); readFiles(archiveDir); QString infoString = tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , " "Archive type: %5 , Archive flags: %6"); - // tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , " - // "Archive type: %5 , Archive flags: %6 , Contents flags: %7"); - infoString = infoString.arg(getFormat(arch.getType())) - .arg((arch.getFlags() & 0x00000004) ? tr("yes") : tr("no")) + infoString = infoString.arg(getFormat(archive->getType())) + .arg((archive->getFlags() & 0x00000004) ? tr("yes") : tr("no")) .arg(m_Files.size()) - .arg(getVersion(arch.getType())) - .arg(arch.getType()) - .arg("0x" + QString::number(arch.getFlags(), 16)); - //.arg("0x" + QString::number(arch.get_file_flags(), 16)); + .arg(getVersion(archive->getType())) + .arg(archive->getType()) + .arg("0x" + QString::number(archive->getFlags(), 16)); infoLabel->setText(infoString); layout->addWidget(infoLabel); @@ -173,8 +182,50 @@ QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&) view->setSortingEnabled(true); view->sortByColumn(0, Qt::SortOrder::AscendingOrder); + // Double-click: extract the selected file from the archive and hand the + // raw bytes to another preview plugin via IOrganizer::previewFileData. + // The view's model is wrapped in FilterWidgetProxyModel, so walk up via + // index.parent() + DisplayRole (proxy-transparent) instead of using + // index.internalPointer() directly. + IOrganizer* mo = m_MOInfo; + QObject::connect( + view, &QTreeView::doubleClicked, view, + [mo, archive, view](const QModelIndex& index) { + if (!index.isValid()) return; + + // Ignore folders — only files have no children. + if (index.model()->rowCount(index) > 0) { + return; + } + + QStringList parts; + QModelIndex cur = index; + while (cur.isValid()) { + parts.prepend(cur.data(Qt::DisplayRole).toString()); + cur = cur.parent(); + } + const QString path = parts.join('/'); + if (path.isEmpty()) return; + + BSA::File::Ptr file = archive->findFile(path.toStdString()); + if (!file) { + log::warn("preview_bsa: file not found in archive: {}", path); + return; + } + std::vector data; + if (archive->extractToMemory(file, data) != BSA::ERROR_NONE) { + log::warn("preview_bsa: extract failed for: {}", path); + return; + } + QByteArray bytes(reinterpret_cast(data.data()), + static_cast(data.size())); + + if (!mo->previewFileData(view->window(), path, bytes)) { + log::warn("preview_bsa: no preview plugin for: {}", path); + } + }); + wrapper->setLayout(layout); - arch.close(); return wrapper; } -- cgit v1.3.1