diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 17:55:56 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-12 17:55:56 -0500 |
| commit | 892070f3dec1dc690983fd862880e37e711c2516 (patch) | |
| tree | 423da9fca609a9239efbb6c74ab2e8657975c5a4 /libs/bsatk/src/bsaarchive.cpp | |
| parent | a0355c9b897281d0bfea48bec9d490724ecabd96 (diff) | |
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) <noreply@anthropic.com>
Diffstat (limited to 'libs/bsatk/src/bsaarchive.cpp')
| -rw-r--r-- | libs/bsatk/src/bsaarchive.cpp | 102 |
1 files changed, 100 insertions, 2 deletions
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 <boost/interprocess/sync/scoped_lock.hpp> #include <boost/shared_array.hpp> #include <boost/thread.hpp> +#include <cctype> #include <cstring> #include <fstream> #include <iostream> @@ -34,6 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include <lz4frame.h> #include <memory> #include <queue> +#include <sstream> #include <sys/stat.h> #include <zlib.h> #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<unsigned char[]> 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<unsigned char>& 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<const unsigned char*>(data.data()), + reinterpret_cast<const unsigned char*>(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<unsigned char>(lhs[i])) != + std::tolower(static_cast<unsigned char>(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<char>( + std::tolower(static_cast<unsigned char>(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<File::Ptr>& 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<File::Ptr> 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<FileInfo>& queue, boost::mutex& mutex, boost::interprocess::interprocess_semaphore& bufferCount, boost::interprocess::interprocess_semaphore& queueFree, |
