diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/archive/src/archive.cpp | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/archive/src/archive.cpp')
| -rw-r--r-- | libs/archive/src/archive.cpp | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/libs/archive/src/archive.cpp b/libs/archive/src/archive.cpp new file mode 100644 index 0000000..6d35366 --- /dev/null +++ b/libs/archive/src/archive.cpp @@ -0,0 +1,609 @@ +/* +Mod Organizer archive handling + +Copyright (C) 2012 Sebastian Herbord, 2020 MO2 Team. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "archive.h" +#include "compat.h" + +#include "extractcallback.h" +#include "inputstream.h" +#include "library.h" +#include "opencallback.h" +#include "propertyvariant.h" + +#include <algorithm> +#include <map> +#include <sstream> +#include <stddef.h> +#include <string> +#include <unordered_map> +#include <vector> + +namespace PropID = NArchive::NHandlerPropID; + +class FileDataImpl : public FileData +{ + friend class Archive; + +public: + FileDataImpl(std::wstring const& fileName, UInt64 size, UInt64 crc, bool isDirectory) + : m_FileName(fileName), m_Size(size), m_CRC(crc), m_IsDirectory(isDirectory) + {} + + virtual std::wstring getArchiveFilePath() const override { return m_FileName; } + virtual uint64_t getSize() const override { return m_Size; } + + virtual void addOutputFilePath(std::wstring const& fileName) override + { + m_OutputFilePaths.push_back(fileName); + } + virtual const std::vector<std::wstring>& getOutputFilePaths() const override + { + return m_OutputFilePaths; + } + + virtual void clearOutputFilePaths() override { m_OutputFilePaths.clear(); } + + bool isEmpty() const { return m_OutputFilePaths.empty(); } + virtual bool isDirectory() const override { return m_IsDirectory; } + virtual uint64_t getCRC() const override { return m_CRC; } + +private: + std::wstring m_FileName; + UInt64 m_Size; + UInt64 m_CRC; + std::vector<std::wstring> m_OutputFilePaths; + bool m_IsDirectory; +}; + +/// represents the connection to one archive and provides common functionality +class ArchiveImpl : public Archive +{ + + // Callback that does nothing but avoid having to check if the callback is present + // everytime. + static LogCallback DefaultLogCallback; + +public: + ArchiveImpl(); + virtual ~ArchiveImpl(); + + virtual bool isValid() const { return m_Valid; } + + virtual Error getLastError() const { return m_LastError; } + virtual void setLogCallback(LogCallback logCallback) override + { + // Wrap the callback so that we do not have to check if it is set everywhere: + m_LogCallback = logCallback ? logCallback : DefaultLogCallback; + } + + virtual bool open(std::wstring const& archiveName, + PasswordCallback passwordCallback) override; + virtual void close() override; + const std::vector<FileData*>& getFileList() const override { return m_FileList; } + virtual bool extract(std::wstring const& outputDirectory, + ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback, + ErrorCallback errorCallback) override; + + virtual void cancel() override; + +private: + void clearFileList(); + void resetFileList(); + + HRESULT loadFormats(); + +private: + typedef UINT32(WINAPI* CreateObjectFunc)(const GUID* clsID, const GUID* interfaceID, + void** outObject); + CreateObjectFunc m_CreateObjectFunc; + + // A note: In 7zip source code this not is what this typedef is called, the old + // GetHandlerPropertyFunc appears to be deprecated. + typedef UInt32(WINAPI* GetPropertyFunc)(UInt32 index, PROPID propID, + PROPVARIANT* value); + GetPropertyFunc m_GetHandlerPropertyFunc; + + template <typename T> + T readHandlerProperty(UInt32 index, PROPID propID) const; + + template <typename T> + T readProperty(UInt32 index, PROPID propID) const; + + bool m_Valid; + Error m_LastError; + + ALibrary m_Library; + std::wstring m_ArchiveName; // TBH I don't think this is required + CComPtr<IInArchive> m_ArchivePtr; + CArchiveExtractCallback* m_ExtractCallback; + + LogCallback m_LogCallback; + PasswordCallback m_PasswordCallback; + + std::vector<FileData*> m_FileList; + + std::wstring m_Password; + + struct ArchiveFormatInfo + { + CLSID m_ClassID; + std::wstring m_Name; + std::vector<std::string> m_Signatures; + std::wstring m_Extensions; + std::wstring m_AdditionalExtensions; + UInt32 m_SignatureOffset; + }; + + typedef std::vector<ArchiveFormatInfo> Formats; + Formats m_Formats; + + typedef std::unordered_map<std::wstring, Formats> FormatMap; + FormatMap m_FormatMap; + + // I don't think one signature could possibly describe two formats. + typedef std::map<std::string, ArchiveFormatInfo> SignatureMap; + SignatureMap m_SignatureMap; + + std::size_t m_MaxSignatureLen = 0; +}; + +Archive::LogCallback ArchiveImpl::DefaultLogCallback([](LogLevel, std::wstring const&) { +}); + +template <typename T> +T ArchiveImpl::readHandlerProperty(UInt32 index, PROPID propID) const +{ + PropertyVariant prop; + if (m_GetHandlerPropertyFunc(index, propID, &prop) != S_OK) { + throw std::runtime_error("Failed to read property"); + } + return static_cast<T>(prop); +} + +template <typename T> +T ArchiveImpl::readProperty(UInt32 index, PROPID propID) const +{ + PropertyVariant prop; + if (m_ArchivePtr->GetProperty(index, propID, &prop) != S_OK) { + throw std::runtime_error("Failed to read property"); + } + return static_cast<T>(prop); +} + +// Seriously, there is one format returned in the list that has no registered +// extension and no signature. WTF? +HRESULT ArchiveImpl::loadFormats() +{ + typedef UInt32(WINAPI * GetNumberOfFormatsFunc)(UInt32 * numFormats); + GetNumberOfFormatsFunc getNumberOfFormats = + m_Library.resolve<GetNumberOfFormatsFunc>("GetNumberOfFormats"); + if (getNumberOfFormats == nullptr) { + return E_FAIL; + } + + UInt32 numFormats; + RINOK(getNumberOfFormats(&numFormats)); + + for (UInt32 i = 0; i < numFormats; ++i) { + ArchiveFormatInfo item; + + item.m_Name = readHandlerProperty<std::wstring>(i, PropID::kName); + + item.m_ClassID = readHandlerProperty<GUID>(i, PropID::kClassID); + + // Should split up the extensions and map extension to type, and see what we get + // from that for preference then try all extensions anyway... + item.m_Extensions = readHandlerProperty<std::wstring>(i, PropID::kExtension); + + // This is unnecessary currently for our purposes. Basically, for each + // extension, there's an 'addext' which, if set (to other than *) means that + // theres a double encoding going on. For instance, the bzip format is like this + // addext = "* * .tar .tar" + // ext = "bz2 bzip2 tbz2 tbz" + // which means that tbz2 and tbz should uncompress to a tar file which can be + // further processed as if it were a tar file. Having said which, we don't + // need to support this at all, so I'm storing it but ignoring it. + item.m_AdditionalExtensions = + readHandlerProperty<std::wstring>(i, PropID::kAddExtension); + + std::string signature = readHandlerProperty<std::string>(i, PropID::kSignature); + if (!signature.empty()) { + item.m_Signatures.push_back(signature); + if (m_MaxSignatureLen < signature.size()) { + m_MaxSignatureLen = signature.size(); + } + m_SignatureMap[signature] = item; + } + + std::string multiSig = readHandlerProperty<std::string>(i, PropID::kMultiSignature); + const char* multiSigBytes = multiSig.c_str(); + std::size_t size = multiSig.length(); + while (size > 0) { + unsigned len = *multiSigBytes++; + size--; + if (len > size) + break; + std::string sig(multiSigBytes, multiSigBytes + len); + multiSigBytes = multiSigBytes + len; + size -= len; + item.m_Signatures.push_back(sig); + if (m_MaxSignatureLen < sig.size()) { + m_MaxSignatureLen = sig.size(); + } + m_SignatureMap[sig] = item; + } + + UInt32 offset = readHandlerProperty<UInt32>(i, PropID::kSignatureOffset); + item.m_SignatureOffset = offset; + + // Now split the extension up from the space separated string and create + // a map from each extension to the possible formats + // We could make these pointers but it's not a massive overhead and nobody + // should be changing this + std::wistringstream s(item.m_Extensions); + std::wstring t; + while (s >> t) { + m_FormatMap[t].push_back(item); + } + m_Formats.push_back(item); + } + return S_OK; +} + +ArchiveImpl::ArchiveImpl() + : m_Valid(false), m_LastError(Error::ERROR_NONE), m_Library("dlls/7zip.dll"), + m_PasswordCallback{} +{ + // Reset the log callback: + setLogCallback({}); + + if (!m_Library) { + m_LastError = Error::ERROR_LIBRARY_NOT_FOUND; + return; + } + + m_CreateObjectFunc = m_Library.resolve<CreateObjectFunc>("CreateObject"); + if (m_CreateObjectFunc == nullptr) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + m_GetHandlerPropertyFunc = m_Library.resolve<GetPropertyFunc>("GetHandlerProperty2"); + if (m_GetHandlerPropertyFunc == nullptr) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + try { + if (loadFormats() != S_OK) { + m_LastError = Error::ERROR_LIBRARY_INVALID; + return; + } + + m_Valid = true; + return; + } catch (std::exception const& e) { + m_LogCallback(LogLevel::Error, std::format(L"Caught exception {}.", e)); + m_LastError = Error::ERROR_LIBRARY_INVALID; + } +} + +ArchiveImpl::~ArchiveImpl() +{ + close(); +} + +bool ArchiveImpl::open(std::wstring const& archiveName, + PasswordCallback passwordCallback) +{ + m_ArchiveName = archiveName; // Just for debugging, not actually used... + + Formats formatList = m_Formats; + + // Convert to long path if it's not already: + std::filesystem::path filepath = IO::make_path(archiveName); + + // If it doesn't exist or is a directory, error + if (!exists(filepath) || is_directory(filepath)) { + m_LastError = Error::ERROR_ARCHIVE_NOT_FOUND; + return false; + } + + // in rars the password seems to be requested during extraction, not on open, so we + // need to hold on to the callback for now + m_PasswordCallback = passwordCallback; + + CComPtr<InputStream> file(new InputStream); + + if (!file->Open(filepath)) { + m_LastError = Error::ERROR_FAILED_TO_OPEN_ARCHIVE; + return false; + } + + CComPtr<CArchiveOpenCallback> openCallbackPtr; + try { + openCallbackPtr = + new CArchiveOpenCallback(passwordCallback, m_LogCallback, filepath); + } catch (std::runtime_error const&) { + m_LastError = Error::ERROR_FAILED_TO_OPEN_ARCHIVE; + return false; + } + + // Try to open the archive + + bool sigMismatch = false; + + { + // Get the first iterator that is strictly > the signature we're looking for. + for (auto signatureInfo : m_SignatureMap) { + // Read the signature of the file and look that up. + std::vector<char> buff; + buff.reserve(m_MaxSignatureLen); + UInt32 act; + file->Seek(0, STREAM_SEEK_SET, nullptr); + file->Read(buff.data(), static_cast<UInt32>(m_MaxSignatureLen), &act); + file->Seek(0, STREAM_SEEK_SET, nullptr); + std::string signature = std::string(buff.data(), act); + if (signatureInfo.first == std::string(buff.data(), signatureInfo.first.size())) { + if (m_CreateObjectFunc(&signatureInfo.second.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) != S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Failed to open {} using {} (from signature).", + archiveName, signatureInfo.second.m_Name)); + m_ArchivePtr.Release(); + } else { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", + archiveName, signatureInfo.second.m_Name)); + + // Retrieve the extension (warning: .extension() contains the dot): + std::wstring ext = + ArchiveStrings::towlower(filepath.extension().wstring().substr(1)); + std::wistringstream s(signatureInfo.second.m_Extensions); + std::wstring t; + bool found = false; + while (s >> t) { + if (t == ext) { + found = true; + break; + } + } + if (!found) { + m_LogCallback(LogLevel::Warning, + L"The extension of this file did not match the expected " + L"extensions for this format."); + sigMismatch = true; + } + } + // Arguably we should give up here if it's not OK if 7zip can't even start + // to decode even though we've found the format from the signature. + // Sadly, the 7zip API documentation is pretty well non-existant. + break; + } + std::vector<ArchiveFormatInfo>::iterator iter = std::find_if( + formatList.begin(), formatList.end(), [=](ArchiveFormatInfo a) -> bool { + return a.m_Name == signatureInfo.second.m_Name; + }); + if (iter != formatList.end()) + formatList.erase(iter); + } + } + + { + // determine archive type based on extension + Formats const* formats = nullptr; + std::wstring ext = + ArchiveStrings::towlower(filepath.extension().wstring().substr(1)); + FormatMap::const_iterator map_iter = m_FormatMap.find(ext); + if (map_iter != m_FormatMap.end()) { + formats = &map_iter->second; + if (formats != nullptr) { + if (m_ArchivePtr == nullptr) { + // OK, we have some potential formats. If there is only one, try it now. If + // there are multiple formats, we'll try by signature lookup first. + for (ArchiveFormatInfo format : *formats) { + if (m_CreateObjectFunc(&format.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) != S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Failed to open {} using {} (from signature).", + archiveName, format.m_Name)); + m_ArchivePtr.Release(); + } else { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", + archiveName, format.m_Name)); + break; + } + + std::vector<ArchiveFormatInfo>::iterator iter = std::find_if( + formatList.begin(), formatList.end(), [=](ArchiveFormatInfo a) -> bool { + return a.m_Name == format.m_Name; + }); + if (iter != formatList.end()) + formatList.erase(iter); + } + } else if (sigMismatch) { + std::vector<std::wstring> vformats; + for (ArchiveFormatInfo format : *formats) { + vformats.push_back(format.m_Name); + } + m_LogCallback( + LogLevel::Warning, + std::format(L"The format(s) expected for this extension are: {}.", + ArchiveStrings::join(vformats, L", "))); + } + } + } + } + + if (m_ArchivePtr == nullptr) { + m_LogCallback(LogLevel::Warning, L"Trying to open an archive but could not " + L"recognize the extension or signature."); + m_LogCallback( + LogLevel::Debug, + L"Attempting to open the file with the remaining formats as a fallback..."); + for (auto format : formatList) { + if (m_CreateObjectFunc(&format.m_ClassID, &IID_IInArchive, + (void**)&m_ArchivePtr) != S_OK) { + m_LastError = Error::ERROR_LIBRARY_ERROR; + return false; + } + if (m_ArchivePtr->Open(file, 0, openCallbackPtr) == S_OK) { + m_LogCallback(LogLevel::Debug, + std::format(L"Opened {} using {} (from signature).", archiveName, + format.m_Name)); + m_LogCallback(LogLevel::Warning, + L"This archive likely has an incorrect extension."); + break; + } else + m_ArchivePtr.Release(); + } + } + + if (m_ArchivePtr == nullptr) { + m_LastError = Error::ERROR_INVALID_ARCHIVE_FORMAT; + return false; + } + + m_Password = openCallbackPtr->GetPassword(); + /* + UInt32 subFile = ULONG_MAX; + { + NCOM::CPropVariant prop; + if (m_ArchivePtr->GetArchiveProperty(kpidMainSubfile, &prop) != S_OK) { + throw std::runtime_error("failed to get property kpidMainSubfile"); + } + + if (prop.vt == VT_UI4) { + subFile = prop.ulVal; + } + } + + if (subFile != ULONG_MAX) { + std::wstring subPath = GetArchiveItemPath(m_ArchivePtr, subFile); + + CMyComPtr<IArchiveOpenSetSubArchiveName> setSubArchiveName; + openCallbackPtr.QueryInterface(IID_IArchiveOpenSetSubArchiveName, (void + **)&setSubArchiveName); if (setSubArchiveName) { + setSubArchiveName->SetSubArchiveName(subPath.c_str()); + } + }*/ + + m_LastError = Error::ERROR_NONE; + + resetFileList(); + return true; +} + +void ArchiveImpl::close() +{ + if (m_ArchivePtr != nullptr) { + m_ArchivePtr->Close(); + } + clearFileList(); + m_ArchivePtr.Release(); + m_PasswordCallback = {}; +} + +void ArchiveImpl::clearFileList() +{ + for (std::vector<FileData*>::iterator iter = m_FileList.begin(); + iter != m_FileList.end(); ++iter) { + delete *iter; + } + m_FileList.clear(); +} + +void ArchiveImpl::resetFileList() +{ + UInt32 numItems = 0; + clearFileList(); + + m_ArchivePtr->GetNumberOfItems(&numItems); + + for (UInt32 i = 0; i < numItems; ++i) { + m_FileList.push_back(new FileDataImpl( + readProperty<std::wstring>(i, kpidPath), readProperty<UInt64>(i, kpidSize), + readProperty<UInt64>(i, kpidCRC), readProperty<bool>(i, kpidIsDir))); + } +} + +bool ArchiveImpl::extract(std::wstring const& outputDirectory, + ProgressCallback progressCallback, + FileChangeCallback fileChangeCallback, + ErrorCallback errorCallback) + +{ + // Retrieve the list of indices we want to extract: + std::vector<UInt32> indices; + UInt64 totalSize = 0; + for (std::size_t i = 0; i < m_FileList.size(); ++i) { + FileDataImpl* fileData = static_cast<FileDataImpl*>(m_FileList[i]); + if (!fileData->isEmpty()) { + indices.push_back(static_cast<UInt32>(i)); + totalSize += fileData->getSize(); + } + } + + m_ExtractCallback = new CArchiveExtractCallback( + progressCallback, fileChangeCallback, errorCallback, m_PasswordCallback, + m_LogCallback, m_ArchivePtr, outputDirectory, &m_FileList[0], m_FileList.size(), + totalSize, &m_Password); + HRESULT result = m_ArchivePtr->Extract( + indices.data(), static_cast<UInt32>(indices.size()), false, m_ExtractCallback); + // Note: m_ExtractCallBack is deleted by Extract + switch (result) { + case S_OK: { + // nop + } break; + case E_ABORT: { + m_LastError = Error::ERROR_EXTRACT_CANCELLED; + } break; + case E_OUTOFMEMORY: { + m_LastError = Error::ERROR_OUT_OF_MEMORY; + } break; + default: { + m_LastError = Error::ERROR_LIBRARY_ERROR; + } break; + } + + return result == S_OK; +} + +void ArchiveImpl::cancel() +{ + m_ExtractCallback->SetCanceled(true); +} + +std::unique_ptr<Archive> CreateArchive() +{ + return std::make_unique<ArchiveImpl>(); +} |
