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/bsatk/src/bsaarchive.cpp | 102 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) (limited to 'libs/bsatk/src/bsaarchive.cpp') diff --git a/libs/bsatk/src/bsaarchive.cpp b/libs/bsatk/src/bsaarchive.cpp index 172a0b2..0ec9e20 100644 --- a/libs/bsatk/src/bsaarchive.cpp +++ b/libs/bsatk/src/bsaarchive.cpp @@ -27,6 +27,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#include #include #include #ifdef _WIN32 @@ -621,7 +623,7 @@ void Archive::getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr fil static const BSAULong CHUNK_SIZE = 128 * 1024; -EErrorCode Archive::extractDirect(File::Ptr file, std::ofstream& outFile) const +EErrorCode Archive::extractDirect(File::Ptr file, std::ostream& outFile) const { EErrorCode result = ERROR_NONE; if (file->m_FileSize == 0) { @@ -744,7 +746,7 @@ std::shared_ptr Archive::decompress(unsigned char* inBuffer, } } -EErrorCode Archive::extractCompressed(File::Ptr file, std::ofstream& outFile) const +EErrorCode Archive::extractCompressed(File::Ptr file, std::ostream& outFile) const { EErrorCode result = ERROR_NONE; if (file->m_FileSize == 0) { @@ -881,6 +883,102 @@ EErrorCode Archive::extract(File::Ptr file, const char* outputDirectory) const return result; } +EErrorCode Archive::extractToMemory(File::Ptr file, + std::vector& result) const +{ + result.clear(); + if (!file) { + return ERROR_INVALIDDATA; + } + + std::ostringstream stream(std::ios::out | std::ios::binary); + EErrorCode rc = ERROR_NONE; + if (compressed(file)) { + rc = extractCompressed(file, stream); + } else { + rc = extractDirect(file, stream); + } + + if (rc != ERROR_NONE) { + return rc; + } + + const std::string& data = stream.str(); + result.assign(reinterpret_cast(data.data()), + reinterpret_cast(data.data() + data.size())); + return ERROR_NONE; +} + +static bool iequals_bsa(const std::string& lhs, const std::string& rhs) +{ + if (lhs.size() != rhs.size()) { + return false; + } + for (std::size_t i = 0; i < lhs.size(); ++i) { + if (std::tolower(static_cast(lhs[i])) != + std::tolower(static_cast(rhs[i]))) { + return false; + } + } + return true; +} + +// Normalize a path for case-insensitive, separator-agnostic comparison: +// all forward slashes, all lowercase. Used by findFile only — doesn't mutate +// stored names. +static std::string normalize_bsa_path(const std::string& in) +{ + std::string out; + out.reserve(in.size()); + for (char ch : in) { + if (ch == '\\') { + ch = '/'; + } + out.push_back(static_cast( + std::tolower(static_cast(ch)))); + } + // strip any leading separator + std::size_t start = 0; + while (start < out.size() && out[start] == '/') { + ++start; + } + return out.substr(start); +} + +static void collect_files_recursive(Folder::Ptr folder, + std::vector& out) +{ + for (unsigned int i = 0; i < folder->getNumFiles(); ++i) { + out.push_back(folder->getFile(i)); + } + for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) { + collect_files_recursive(folder->getSubFolder(i), out); + } +} + +File::Ptr Archive::findFile(const std::string& path) const +{ + if (!m_RootFolder || path.empty()) { + return nullptr; + } + + const std::string want = normalize_bsa_path(path); + + // BSAs store flat folder names like "meshes\\actors\\character". On Linux + // std::filesystem::path doesn't treat '\\' as a separator, so bsatk builds + // a flat folder tree there. Rather than trying to reverse-engineer the + // structure, iterate every File in the archive and compare its full path + // (normalized) against the caller-supplied one. + std::vector allFiles; + collect_files_recursive(m_RootFolder, allFiles); + for (const File::Ptr& entry : allFiles) { + if (normalize_bsa_path(entry->getFilePath()) == want) { + return entry; + } + } + return nullptr; +} + void Archive::readFiles(std::queue& queue, boost::mutex& mutex, boost::interprocess::interprocess_semaphore& bufferCount, boost::interprocess::interprocess_semaphore& queueFree, -- cgit v1.3.1