diff options
Diffstat (limited to 'libs/bsatk/src')
| -rw-r--r-- | libs/bsatk/src/CMakeLists.txt | 83 | ||||
| -rw-r--r-- | libs/bsatk/src/bsaarchive.cpp | 1237 | ||||
| -rw-r--r-- | libs/bsatk/src/bsaexception.cpp | 39 | ||||
| -rw-r--r-- | libs/bsatk/src/bsafile.cpp | 154 | ||||
| -rw-r--r-- | libs/bsatk/src/bsafolder.cpp | 342 | ||||
| -rw-r--r-- | libs/bsatk/src/bsatypes.cpp | 70 | ||||
| -rw-r--r-- | libs/bsatk/src/filehash.cpp | 100 |
7 files changed, 2025 insertions, 0 deletions
diff --git a/libs/bsatk/src/CMakeLists.txt b/libs/bsatk/src/CMakeLists.txt new file mode 100644 index 0000000..99a0f26 --- /dev/null +++ b/libs/bsatk/src/CMakeLists.txt @@ -0,0 +1,83 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Boost REQUIRED COMPONENTS thread) +find_package(ZLIB REQUIRED) +find_package(mo2-dds-header CONFIG REQUIRED) + +# lz4: try CONFIG first, fall back to pkg-config +find_package(lz4 CONFIG QUIET) +if (NOT lz4_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LZ4 REQUIRED IMPORTED_TARGET liblz4) +endif() +# lz4 1.10+ exports LZ4::lz4, older versions export lz4::lz4 +if(TARGET LZ4::lz4 AND NOT TARGET lz4::lz4) + add_library(lz4::lz4 ALIAS LZ4::lz4) +endif() + +add_library(bsatk STATIC) +target_link_libraries(bsatk + PUBLIC mo2::dds-header + PRIVATE ZLIB::ZLIB Boost::thread +) + +# Link lz4 based on how it was found +if (lz4_FOUND) + target_link_libraries(bsatk PRIVATE lz4::lz4) +else() + target_link_libraries(bsatk PRIVATE PkgConfig::LZ4) +endif() + +target_sources(bsatk + PRIVATE + bsaarchive.cpp + bsaexception.cpp + bsafile.cpp + bsafolder.cpp + bsatypes.cpp + filehash.cpp + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsaarchive.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsaexception.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsafile.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsafolder.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsatk.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/bsatypes.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/errorcodes.h + ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk/filehash.h + +) + +set_target_properties(bsatk PROPERTIES CXX_STANDARD 20) + +# for building +target_include_directories(bsatk PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include/bsatk) + +if (MSVC) + target_compile_options(bsatk + PRIVATE + "/MP" + "/W4" + "/external:anglebrackets" + "/external:W0" + ) + target_link_options(bsatk + PRIVATE + $<$<CONFIG:RelWithDebInfo>:/LTCG /INCREMENTAL:NO /OPT:REF /OPT:ICF> + ) + + set_target_properties(bsatk PROPERTIES VS_STARTUP_PROJECT bsatk) +endif() + +add_library(mo2::bsatk ALIAS bsatk) + +# install +install(TARGETS bsatk EXPORT bsatkTargets FILE_SET HEADERS) +install(EXPORT bsatkTargets + FILE mo2-bsatk-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-bsatk +) diff --git a/libs/bsatk/src/bsaarchive.cpp b/libs/bsatk/src/bsaarchive.cpp new file mode 100644 index 0000000..e1655c1 --- /dev/null +++ b/libs/bsatk/src/bsaarchive.cpp @@ -0,0 +1,1237 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 "bsaarchive.h" +#include "bsaexception.h" +#include "bsafile.h" +#include "bsafolder.h" +#include <algorithm> +#include <boost/interprocess/sync/interprocess_semaphore.hpp> +#include <boost/interprocess/sync/scoped_lock.hpp> +#include <boost/shared_array.hpp> +#include <boost/thread.hpp> +#include <cstring> +#include <fstream> +#include <iostream> +#include <lz4.h> +#include <lz4frame.h> +#include <memory> +#include <queue> +#include <sys/stat.h> +#include <zlib.h> +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> +#endif + +using std::fstream; + +using namespace boost::posix_time; + +namespace BSA +{ + +Archive::Archive() + : m_RootFolder(new Folder), m_ArchiveFlags(FLAG_HASDIRNAMES | FLAG_HASFILENAMES), + m_Type(TYPE_SKYRIM) +{} + +Archive::~Archive() +{ + if (m_File.is_open()) { + m_File.close(); + } + std::vector<Folder::Ptr> folders; + m_RootFolder->collectFolders(folders); + cleanFolder(m_RootFolder); + m_RootFolder.reset(); +} + +ArchiveType Archive::typeFromID(BSAULong typeID) +{ + switch (typeID) { + case 0x100: + return TYPE_MORROWIND; + case 0x67: + return TYPE_OBLIVION; + case 0x68: + return TYPE_FALLOUT3; + case 0x69: + return TYPE_SKYRIMSE; + case 0x01: + return TYPE_FALLOUT4; + case 0x02: + return TYPE_STARFIELD; + case 0x03: + return TYPE_STARFIELD_LZ4_TEXTURE; + case 0x07: + return TYPE_FALLOUT4NG_7; + case 0x08: + return TYPE_FALLOUT4NG_8; + default: + throw data_invalid_exception(makeString("invalid type %d", typeID)); + } +} + +BSAULong Archive::typeToID(ArchiveType type) +{ + switch (type) { + case TYPE_MORROWIND: + return 0x100; + case TYPE_OBLIVION: + return 0x67; + case TYPE_FALLOUT3: + return 0x68; + case TYPE_SKYRIMSE: + return 0x69; + case TYPE_FALLOUT4: + return 0x01; + case TYPE_STARFIELD: + return 0x02; + case TYPE_STARFIELD_LZ4_TEXTURE: + return 0x03; + case TYPE_FALLOUT4NG_7: + return 0x07; + case TYPE_FALLOUT4NG_8: + return 0x08; + default: + throw data_invalid_exception(makeString("invalid type %d", type)); + } +} + +Archive::Header Archive::readHeader(std::fstream& infile) +{ + Header result; + + result.fileIdentifier = readType<uint32_t>(infile); + if (result.fileIdentifier != 0x00415342 && result.fileIdentifier != 0x58445442 && + result.fileIdentifier != 0x00000100) { + throw data_invalid_exception(makeString("not a bsa or ba2 file")); + } + + if (result.fileIdentifier != 0x00000100) { + ArchiveType type = typeFromID(readType<BSAUInt>(infile)); + if (type == TYPE_FALLOUT4 || type == TYPE_STARFIELD || + type == TYPE_STARFIELD_LZ4_TEXTURE || type == TYPE_FALLOUT4NG_7 || + type == TYPE_FALLOUT4NG_8) { + result.type = type; + infile.read(result.archType, 4); + result.archType[4] = '\0'; + result.fileCount = readType<BSAUInt>(infile); + result.nameTableOffset = readType<BSAHash>(infile); + result.archiveFlags = FLAG_HASDIRNAMES | FLAG_HASFILENAMES; + } else { + result.type = type; + result.offset = readType<BSAUInt>(infile); + result.archiveFlags = readType<BSAUInt>(infile); + result.folderCount = readType<BSAUInt>(infile); + result.fileCount = readType<BSAUInt>(infile); + result.folderNameLength = readType<BSAUInt>(infile); + result.fileNameLength = readType<BSAUInt>(infile); + result.fileFlags = readType<BSAUInt>(infile); + } + } else { + result.type = TYPE_MORROWIND; + result.offset = readType<BSAUInt>(infile); + result.fileCount = readType<BSAUInt>(infile); + result.archiveFlags = FLAG_HASDIRNAMES | FLAG_HASFILENAMES; + } + + return result; +} + +EErrorCode Archive::read(const char* fileName, bool testHashes) +{ + m_File.open(fileName, fstream::in | fstream::binary); + if (!m_File.is_open()) { + return ERROR_FILENOTFOUND; + } + m_File.exceptions(std::ios_base::badbit); + try { + Header header; + try { + header = readHeader(m_File); + } catch (const data_invalid_exception& e) { + throw data_invalid_exception(makeString("%s (filename: %s)", e.what(), fileName)); + } + m_ArchiveFlags = header.archiveFlags; + m_Type = header.type; + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + + m_File.seekg(header.nameTableOffset); + + std::vector<std::string> fileNames; + for (unsigned int i = 0; i < header.fileCount; ++i) { + BSAUShort length = readType<BSAUShort>(m_File); + + char* file = new char[length + 1]; + m_File.read(file, length); + file[length] = '\0'; + + fileNames.push_back(file); + delete[] file; + } + std::streamoff offset; + switch (m_Type) { + case TYPE_STARFIELD: + offset = 32; + break; + case TYPE_STARFIELD_LZ4_TEXTURE: + offset = 36; + break; + default: + offset = 24; + } + if (strcmp(header.archType, "GNRL") == 0) { + m_File.seekg(offset, std::ios::beg); + for (unsigned int i = 0; i < header.fileCount; ++i) { + BSAUInt nameHash = readType<BSAUInt>(m_File); + char* extension = new char[4]; + m_File.read(extension, 4); + BSAUInt dirHash = readType<BSAUInt>(m_File); + m_File.seekg(4, std::ios::cur); + BSAHash offset = readType<BSAHash>(m_File); + BSAUInt packedSize = readType<BSAUInt>(m_File); + BSAUInt unpackedSize = readType<BSAUInt>(m_File); + m_File.seekg(4, std::ios::cur); + std::vector<FO4TextureChunk> dummy; + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + fileNames[i], packedSize, offset, unpackedSize, {}, dummy); + delete[] extension; + } + } else if (strcmp(header.archType, "DX10") == 0) { + m_File.seekg(offset, std::ios::beg); + for (unsigned int i = 0; i < header.fileCount; ++i) { + FO4TextureHeader texHeader; + texHeader.nameHash = readType<BSAUInt>(m_File); + m_File.read(texHeader.extension, 4); + texHeader.dirHash = readType<BSAUInt>(m_File); + texHeader.unknown1 = readType<BSAUChar>(m_File); + texHeader.chunkNumber = readType<BSAUChar>(m_File); + texHeader.chunkHeaderSize = readType<BSAUShort>(m_File); + texHeader.height = readType<BSAUShort>(m_File); + texHeader.width = readType<BSAUShort>(m_File); + texHeader.mipCount = readType<BSAUChar>(m_File); + texHeader.format = static_cast<DXGI_FORMAT>(readType<BSAUChar>(m_File)); + texHeader.isCubemap = readType<bool>(m_File); + texHeader.unknown2 = readType<BSAUChar>(m_File); + std::vector<FO4TextureChunk> chunks; + for (unsigned int j = 0; j < texHeader.chunkNumber; ++j) { + FO4TextureChunk chunk; + chunk.offset = readType<BSAHash>(m_File); + chunk.packedSize = readType<BSAUInt>(m_File); + chunk.unpackedSize = readType<BSAUInt>(m_File); + chunk.startMip = readType<BSAUShort>(m_File); + chunk.endMip = readType<BSAUShort>(m_File); + chunk.unknown = readType<BSAUInt>(m_File); + chunks.push_back(chunk); + } + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + fileNames[i], chunks[0].packedSize, chunks[0].offset, + chunks[0].unpackedSize, texHeader, chunks); + } + } + + return ERROR_NONE; + } else if (m_Type == TYPE_MORROWIND) { + BSAUInt dataOffset = 12 + header.offset + header.fileCount * 8; + + std::vector<MorrowindFileOffset> fileSizeOffset(header.fileCount); + m_File.read((char*)fileSizeOffset.data(), + header.fileCount * sizeof(MorrowindFileOffset)); + std::vector<BSAUInt> fileNameOffset(header.fileCount); + m_File.read((char*)fileNameOffset.data(), header.fileCount * sizeof(BSAUInt)); + BSAUInt last = header.offset - 12 * header.fileCount; + for (uint32_t i = 0; i < header.fileCount; ++i) { + uint32_t index = 0; + if (i + 1 == header.fileCount) + index = last; + else + index = fileNameOffset[i + 1] - fileNameOffset[i]; + char* filePath = new char[index + 1]; + m_File.read(filePath, index); + filePath[index] = '\0'; + + std::vector<FO4TextureChunk> dummy; + Folder::Ptr newDir = m_RootFolder->addFolderFromFile( + filePath, fileSizeOffset[i].size, dataOffset + fileSizeOffset[i].offset, 0, + {}, dummy); + delete[] filePath; + } + return ERROR_NONE; + } else { + // flat list of folders as they were stored in the archive + std::vector<Folder::Ptr> folders; + + for (unsigned long i = 0; i < header.folderCount; ++i) { + folders.push_back(m_RootFolder->addFolder(m_File, header.fileNameLength, + header.offset, header.type)); + } + + m_File.seekg(header.offset); + + bool hashesValid = true; + for (std::vector<Folder::Ptr>::iterator iter = folders.begin(); + iter != folders.end(); ++iter) { + if (!(*iter)->resolveFileNames(m_File, testHashes)) { + hashesValid = false; + } + } + return hashesValid ? ERROR_NONE : ERROR_INVALIDHASHES; + } + } catch (std::ios_base::failure&) { + return ERROR_INVALIDDATA; + } +} + +void Archive::close() +{ + m_File.close(); +} + +BSAULong Archive::countFiles() const +{ + return m_RootFolder->countFiles(); +} + +std::vector<std::string> Archive::collectFolderNames() const +{ + std::vector<std::string> result; + m_RootFolder->collectFolderNames(result); + return result; +} + +std::vector<std::string> Archive::collectFileNames() const +{ + std::vector<std::string> result; + m_RootFolder->collectFileNames(result); + return result; +} + +BSAULong Archive::countCharacters(const std::vector<std::string>& list) const +{ + size_t sum = 0; + for (std::vector<std::string>::const_iterator iter = list.begin(); iter != list.end(); + ++iter) { + sum += iter->length() + 1; + } + return static_cast<BSAULong>(sum); +} + +#ifndef WIN32 +#define _stricmp strcasecmp +#endif // WIN32 + +static bool endsWith(const std::string& fileName, const char* extension) +{ + size_t endLength = strlen(extension); + if (fileName.length() < endLength) { + return false; + } + return _stricmp(&fileName[fileName.length() - endLength], extension) == 0; +} + +BSAULong Archive::determineFileFlags(const std::vector<std::string>& fileList) const +{ + BSAULong result = 0; + + bool hasNIF = false; + bool hasDDS = false; + bool hasXML = false; + bool hasWAV = false; + bool hasMP3 = false; + bool hasTXT = false; + bool hasSPT = false; + bool hasTEX = false; + bool hasCTL = false; + + for (std::vector<std::string>::const_iterator iter = fileList.begin(); + iter != fileList.end(); ++iter) { + if (!hasNIF && endsWith(*iter, ".nif")) { + hasNIF = true; + result |= 1 << 0; + } else if (!hasDDS && endsWith(*iter, ".dds")) { + hasDDS = true; + result |= 1 << 1; + } else if (!hasXML && endsWith(*iter, ".xml")) { + hasXML = true; + result |= 1 << 2; + } else if (!hasWAV && endsWith(*iter, ".wav")) { + hasWAV = true; + result |= 1 << 3; + } else if (!hasMP3 && endsWith(*iter, ".mp3")) { + hasMP3 = true; + result |= 1 << 4; + } else if (!hasTXT && endsWith(*iter, ".txt")) { + hasTXT = true; + result |= 1 << 5; + } else if (!hasSPT && endsWith(*iter, ".spt")) { + hasSPT = true; + result |= 1 << 6; + } else if (!hasTEX && endsWith(*iter, ".tex")) { + hasTEX = true; + result |= 1 << 7; + } else if (!hasCTL && endsWith(*iter, ".ctl")) { + hasCTL = true; + result |= 1 << 8; + } + } + return result; +} + +void Archive::writeHeader(std::fstream& outfile, BSAULong fileFlags, + BSAULong numFolders, BSAULong folderNamesLength, + BSAULong fileNamesLength) +{ + outfile.write("BSA\0", 4); + writeType<BSAULong>(outfile, typeToID(m_Type)); + writeType<BSAULong>(outfile, 0x24); // header size is static + writeType<BSAULong>(outfile, m_ArchiveFlags); + writeType<BSAULong>(outfile, numFolders); + writeType<BSAULong>(outfile, countFiles()); + writeType<BSAULong>(outfile, folderNamesLength); + writeType<BSAULong>(outfile, fileNamesLength); + writeType<BSAULong>(outfile, fileFlags); +} + +EErrorCode Archive::write(const char* fileName) +{ + std::fstream outfile; + outfile.open(fileName, fstream::out | fstream::binary); + if (!outfile.is_open()) { + return ERROR_ACCESSFAILED; + } + outfile.exceptions(std::ios_base::badbit); + + std::vector<Folder::Ptr> folders; + m_RootFolder->collectFolders(folders); + + std::vector<std::string> folderNames; + std::vector<std::string> fileNames; + BSAULong folderNamesLength = 0; + BSAULong fileNamesLength = 0; + for (std::vector<Folder::Ptr>::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + std::string fullPath = (*folderIter)->getFullPath(); + folderNames.push_back(fullPath); + folderNamesLength += static_cast<BSAULong>(fullPath.length()); + for (std::vector<File::Ptr>::const_iterator fileIter = + (*folderIter)->m_Files.begin(); + fileIter != (*folderIter)->m_Files.end(); ++fileIter) { + fileNames.push_back((*fileIter)->m_Name); + fileNamesLength += static_cast<BSAULong>((*fileIter)->m_Name.length()); + } + } + + try { + writeHeader(outfile, determineFileFlags(fileNames), + static_cast<BSAULong>(folderNames.size()), folderNamesLength, + fileNamesLength); +#pragma message("folders (and files?) need to be sorted by hash!") + // dummy-pass: before we can store the actual folder data + + // prepare folder and file headers +#pragma message("it's unnecessary to write actual data, placeholders are sufficient") + for (std::vector<Folder::Ptr>::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeHeader(outfile); + } + + for (std::vector<Folder::Ptr>::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeData(outfile, fileNamesLength); + } + + // write file names + for (std::vector<std::string>::const_iterator folderIter = fileNames.begin(); + folderIter != fileNames.end(); ++folderIter) { + writeZString(outfile, *folderIter); + } + + // write file data + for (std::vector<Folder::Ptr>::iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeFileData(m_File, outfile); + } + + outfile.seekp(0x24, fstream::beg); + + // re-write folder and file structure, this time with the correct + // offsets + for (std::vector<Folder::Ptr>::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeHeader(outfile); + } + + for (std::vector<Folder::Ptr>::const_iterator folderIter = folders.begin(); + folderIter != folders.end(); ++folderIter) { + (*folderIter)->writeData(outfile, fileNamesLength); + } + + outfile.close(); + return ERROR_NONE; + } catch (std::ios_base::failure&) { + outfile.close(); + return ERROR_INVALIDDATA; + } +} + +DirectX::DDS_HEADER Archive::getDDSHeader(File::Ptr file, + DirectX::DDS_HEADER_DXT10& DX10Header, + bool& isDX10) const +{ + DirectX::DDS_HEADER DDSHeaderData = {}; + DDSHeaderData.size = sizeof(DDSHeaderData); + DDSHeaderData.flags = + DDS_HEADER_FLAGS_TEXTURE | DDS_HEADER_FLAGS_LINEARSIZE | DDS_HEADER_FLAGS_MIPMAP; + DDSHeaderData.height = file->m_TextureHeader.height; + DDSHeaderData.width = file->m_TextureHeader.width; + DDSHeaderData.mipMapCount = file->m_TextureHeader.mipCount; + DDSHeaderData.ddspf.size = sizeof(DirectX::DDS_PIXELFORMAT); + DDSHeaderData.caps = DDS_SURFACE_FLAGS_TEXTURE | DDS_SURFACE_FLAGS_MIPMAP; + + if (file->m_TextureHeader.isCubemap) + DDSHeaderData.caps2 = DDS_CUBEMAP_ALLFACES; + + bool supported = true; + + switch (file->m_TextureHeader.format) { + case DXGI_FORMAT_BC1_UNORM: + case DXGI_FORMAT_BC1_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT1; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height / 2; + break; + + case DXGI_FORMAT_BC2_UNORM: + case DXGI_FORMAT_BC2_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT3; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC3_UNORM: + case DXGI_FORMAT_BC3_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DXT5; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC4_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC4_UNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC4_SNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC4_SNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC5_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC5_UNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC5_SNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_BC5_SNORM; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + break; + + case DXGI_FORMAT_BC7_UNORM: + case DXGI_FORMAT_BC7_UNORM_SRGB: + DDSHeaderData.ddspf = DirectX::DDSPF_DX10; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; + + isDX10 = true; + DX10Header.dxgiFormat = file->m_TextureHeader.format; + break; + + case DXGI_FORMAT_R8G8B8A8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8B8G8R8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 4; // 32bpp + break; + + case DXGI_FORMAT_B8G8R8A8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8R8G8B8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 4; // 32bpp + break; + + case DXGI_FORMAT_B8G8R8X8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_X8R8G8B8; + break; + + case DXGI_FORMAT_R8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_L8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height; // 8bpp + break; + + case DXGI_FORMAT_R16_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_L16; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 2; // 16bpp + break; + + case DXGI_FORMAT_R8G8_UNORM: + DDSHeaderData.ddspf = DirectX::DDSPF_A8L8; + DDSHeaderData.pitchOrLinearSize = + file->m_TextureHeader.width * file->m_TextureHeader.height * 2; // 16bpp + break; + + default: + return {}; + break; + } + + return DDSHeaderData; +} + +void Archive::getDX10Header(DirectX::DDS_HEADER_DXT10& DX10Header, File::Ptr file, + DirectX::DDS_HEADER DDSHeader) const +{ + DX10Header.resourceDimension = DirectX::DDS_DIMENSION_TEXTURE2D; + DX10Header.miscFlag = 0; + DX10Header.arraySize = 1; + DX10Header.miscFlags2 = 0; +} + +static const unsigned long CHUNK_SIZE = 128 * 1024; + +EErrorCode Archive::extractDirect(File::Ptr file, std::ofstream& outFile) const +{ + EErrorCode result = ERROR_NONE; + if (file->m_FileSize == 0) { + // don't try to read empty file + return result; + } + + m_File.clear(); + m_File.seekg(static_cast<std::ifstream::pos_type>(file->m_DataOffset), std::ios::beg); + + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + if (!file->m_TextureChunks.size()) { + BSAULong size = file->m_UncompressedFileSize; + std::unique_ptr<char[]> buffer(new char[size]); + m_File.read(buffer.get(), size); + outFile.write(buffer.get(), size); + } else { + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = getDDSHeader(file, DX10HeaderData, isDX10); + + outFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + getDX10Header(DX10HeaderData, file, DDSHeaderData); + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + + for (BSAUInt i = 0; i < file->m_TextureChunks.size(); ++i) { + BSAULong length = file->m_TextureChunks[i].unpackedSize; + std::unique_ptr<char[]> chunk(new char[length]); + m_File.read(chunk.get(), length); + outFile.write(chunk.get(), length); + } + } + } else { + BSAULong size = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (size <= fullName.length()) { +#pragma message("report error!") + return result; + } + size -= fullName.length() + 1; + } + std::unique_ptr<unsigned char[]> buffer(new unsigned char[size]); + m_File.read(reinterpret_cast<char*>(buffer.get()), size); + if (result == ERROR_NONE) + outFile.write(reinterpret_cast<char*>(buffer.get()), size); + } + std::unique_ptr<char[]> inBuffer(new char[CHUNK_SIZE]); + + try { + unsigned long sizeLeft = file->m_FileSize; + + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + m_File.read(inBuffer.get(), chunkSize); + outFile.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + } + return result; +} + +std::shared_ptr<unsigned char[]> Archive::decompress(unsigned char* inBuffer, + BSAULong inSize, + EErrorCode& result, + BSAULong& outSize) +{ + if (outSize == 0) { + memcpy(&outSize, inBuffer, sizeof(BSAULong)); + inBuffer += sizeof(BSAULong); + inSize -= sizeof(BSAULong); + } + + if ((inSize == 0) || (outSize == 0)) { + return std::shared_ptr<unsigned char[]>(); + } + + std::shared_ptr<unsigned char[]> outBuffer(new unsigned char[outSize]); + + z_stream stream; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = inSize; + stream.next_in = static_cast<Bytef*>(inBuffer); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + result = ERROR_ZLIBINITFAILED; + return std::shared_ptr<unsigned char[]>(); + } + + do { + stream.avail_out = outSize; + stream.next_out = reinterpret_cast<Bytef*>(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && (zlibRet != Z_BUF_ERROR)) { +#pragma message("pass result code to caller") + throw std::runtime_error("invalid data"); + } + } while (stream.avail_out == 0); + inflateEnd(&stream); + return outBuffer; + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + inflateEnd(&stream); + return std::shared_ptr<unsigned char[]>(); + } +} + +EErrorCode Archive::extractCompressed(File::Ptr file, std::ofstream& outFile) const +{ + EErrorCode result = ERROR_NONE; + if (file->m_FileSize == 0) { + // don't try to read empty file + return result; + } + + m_File.clear(); + m_File.seekg(static_cast<std::ifstream::pos_type>(file->m_DataOffset), std::ios::beg); + + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_STARFIELD_LZ4_TEXTURE || m_Type == TYPE_FALLOUT4NG_7 || + m_Type == TYPE_FALLOUT4NG_8) { + if (!file->m_TextureChunks.size()) { + BSAULong inSize = file->m_FileSize; + std::unique_ptr<unsigned char[]> inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast<char*>(inBuffer.get()), inSize); + BSAULong length = file->m_UncompressedFileSize; + std::shared_ptr<unsigned char[]> buffer = + decompress(inBuffer.get(), inSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast<char*>(buffer.get()), length); + } + } else { + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = getDDSHeader(file, DX10HeaderData, isDX10); + + outFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + getDX10Header(DX10HeaderData, file, DDSHeaderData); + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + + for (BSAUInt i = 0; i < file->m_TextureChunks.size(); ++i) { + BSAULong length = file->m_TextureChunks[i].unpackedSize; + if (file->m_TextureChunks[i].packedSize > 0) { + unsigned char* chunk = new unsigned char[file->m_TextureChunks[i].packedSize]; + m_File.read(reinterpret_cast<char*>(chunk), + file->m_TextureChunks[i].packedSize); + if (m_Type == TYPE_STARFIELD_LZ4_TEXTURE) { + char* unpackedChunk = new char[length]; + LZ4_decompress_safe(reinterpret_cast<char*>(chunk), unpackedChunk, + file->m_TextureChunks[i].packedSize, length); + outFile.write(unpackedChunk, length); + delete[] unpackedChunk; + } else { + std::shared_ptr<unsigned char[]> unpackedChunk = + decompress(chunk, file->m_TextureChunks[i].packedSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast<char*>(unpackedChunk.get()), length); + } + } + delete[] chunk; + } else { + char* chunk = new char[length]; + m_File.read(chunk, length); + outFile.write(chunk, length); + delete[] chunk; + } + } + } + } else if (m_Type == TYPE_SKYRIMSE) { + BSAULong inSize = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (inSize <= fullName.length()) { +#pragma message("report error!") + return result; + } + inSize -= fullName.length() + 1; + } + BSAULong outSize = readType<BSAULong>(m_File); + inSize -= sizeof(BSAULong); + std::unique_ptr<unsigned char[]> inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast<char*>(inBuffer.get()), inSize); + + LZ4F_decompressionContext_t dcContext = nullptr; + LZ4F_decompressOptions_t options = {}; + LZ4F_createDecompressionContext(&dcContext, LZ4F_VERSION); + size_t lzOutSize = outSize; + size_t lzInSize = inSize; + + std::unique_ptr<unsigned char[]> outBuffer(new unsigned char[outSize]); + LZ4F_decompress(dcContext, outBuffer.get(), &lzOutSize, inBuffer.get(), &lzInSize, + &options); + + outFile.write(reinterpret_cast<char*>(outBuffer.get()), outSize); + } else { + BSAULong inSize = file->m_FileSize; + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (inSize <= fullName.length()) { +#pragma message("report error!") + return result; + } + inSize -= fullName.length() + 1; + } + std::unique_ptr<unsigned char[]> inBuffer(new unsigned char[inSize]); + m_File.read(reinterpret_cast<char*>(inBuffer.get()), inSize); + BSAULong length = 0UL; + std::shared_ptr<unsigned char[]> buffer = + decompress(inBuffer.get(), inSize, result, length); + if (result == ERROR_NONE) { + outFile.write(reinterpret_cast<char*>(buffer.get()), length); + } + } + + return result; +} + +EErrorCode Archive::extract(File::Ptr file, const char* outputDirectory) const +{ + std::string fileName = makeString("%s/%s", outputDirectory, file->getName().c_str()); + std::ofstream outputFile(fileName.c_str(), + fstream::out | fstream::binary | fstream::trunc); + if (!outputFile.is_open()) { + return ERROR_ACCESSFAILED; + } + + EErrorCode result = ERROR_NONE; + if (compressed(file)) { + result = extractCompressed(file, outputFile); + } else { + result = extractDirect(file, outputFile); + } + outputFile.close(); + return result; +} + +void Archive::readFiles(std::queue<FileInfo>& queue, boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + std::vector<File::Ptr>::iterator begin, + std::vector<File::Ptr>::iterator end) +{ + for (; begin != end && !boost::this_thread::interruption_requested(); ++begin) { + queueFree.wait(); + + FileInfo fileInfo; + fileInfo.file = *begin; + size_t size = static_cast<size_t>(fileInfo.file->m_FileSize); + + m_File.seekg(fileInfo.file->m_DataOffset); + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_STARFIELD && + m_Type != TYPE_STARFIELD_LZ4_TEXTURE && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) { + if (namePrefixed()) { + std::string fullName = readBString(m_File); + if (size <= fullName.length()) { +#pragma message("report error!") + continue; + } + size -= fullName.length() + 1; + } + if (m_Type == TYPE_SKYRIMSE && compressed(fileInfo.file)) { + fileInfo.file->m_UncompressedFileSize = readType<BSAULong>(m_File); + size -= 4; + } + if (!fileInfo.file->m_TextureChunks.size()) { + fileInfo.data = + std::make_pair(std::shared_ptr<unsigned char[]>(new unsigned char[size]), + static_cast<BSAULong>(size)); + m_File.read(reinterpret_cast<char*>(fileInfo.data.first.get()), size); + } + } else { + if (!fileInfo.file->m_TextureChunks.size()) { + if (size == 0) + size = fileInfo.file->m_UncompressedFileSize; + fileInfo.data = + std::make_pair(std::shared_ptr<unsigned char[]>(new unsigned char[size]), + static_cast<BSAULong>(size)); + m_File.read(reinterpret_cast<char*>(fileInfo.data.first.get()), size); + } else { + fileInfo.file->m_UncompressedFileSize = 0L; + BSAULong totalSize = 0U; + for (BSAUInt i = 0; i < fileInfo.file->m_TextureChunks.size(); ++i) { + totalSize += fileInfo.file->m_TextureChunks[i].unpackedSize; + } + char* chunkData = new char[totalSize]; + BSAULong currentPos = 0U; + for (BSAUInt i = 0; i < fileInfo.file->m_TextureChunks.size(); ++i) { + BSAULong length = fileInfo.file->m_TextureChunks[i].unpackedSize; + if (fileInfo.file->m_TextureChunks[i].packedSize > 0) { + char* chunk = new char[fileInfo.file->m_TextureChunks[i].packedSize]; + m_File.read(chunk, fileInfo.file->m_TextureChunks[i].packedSize); + if (m_Type == TYPE_FALLOUT4 || m_Type == TYPE_STARFIELD || + m_Type == TYPE_FALLOUT4NG_7 || m_Type == TYPE_FALLOUT4NG_8) { + EErrorCode result = ERROR_NONE; + try { + std::shared_ptr<unsigned char[]> unpackedChunk = decompress( + reinterpret_cast<unsigned char*>(chunk), + fileInfo.file->m_TextureChunks[i].packedSize, result, length); + memcpy(chunkData + currentPos, + reinterpret_cast<char*>(unpackedChunk.get()), length); + unpackedChunk.reset(); + } catch (const std::exception&) { +#pragma message("report error!") + continue; + } + } else { + char* unpackedChunk = new char[length]; + LZ4_decompress_safe(chunk, unpackedChunk, + fileInfo.file->m_TextureChunks[i].packedSize, length); + memcpy(chunkData + currentPos, unpackedChunk, length); + delete[] unpackedChunk; + } + delete[] chunk; + fileInfo.file->m_UncompressedFileSize += length; + } else { + char* chunk = new char[length]; + m_File.read(chunk, length); + memcpy(chunkData + currentPos, chunk, length); + delete[] chunk; + } + currentPos += length; + } + fileInfo.file->m_FileSize = 0; + fileInfo.data = std::make_pair(std::shared_ptr<unsigned char[]>( + reinterpret_cast<unsigned char*>(chunkData)), + static_cast<BSAULong>(totalSize)); + } + } + + { + boost::interprocess::scoped_lock<boost::mutex> lock(mutex); + queue.push(fileInfo); + } + bufferCount.post(); + } +} + +inline bool fileExists(const std::string& name) +{ + struct stat buffer; + return stat(name.c_str(), &buffer) != -1; +} + +void Archive::extractFiles(const std::string& targetDirectory, + std::queue<FileInfo>& queue, boost::mutex& mutex, + boost::interprocess::interprocess_semaphore& bufferCount, + boost::interprocess::interprocess_semaphore& queueFree, + int totalFiles, bool overwrite, int& filesDone) +{ + for (int i = 0; i < totalFiles; ++i) { + bufferCount.wait(); + if (boost::this_thread::interruption_requested()) { + break; + } + + FileInfo fileInfo; + + { + boost::interprocess::scoped_lock<boost::mutex> lock(mutex); + fileInfo = queue.front(); + ++filesDone; + queue.pop(); + } + queueFree.post(); + + DataBuffer dataBuffer = fileInfo.data; + + std::string fileName = makeString("%s\\%s", targetDirectory.c_str(), + fileInfo.file->getFilePath().c_str()); + if (!overwrite && fileExists(fileName)) { + continue; + } + + std::ofstream outputFile(fileName.c_str(), + fstream::out | fstream::binary | fstream::trunc); + + if (!outputFile.is_open()) { +#pragma message("report error!") + continue; + // return ERROR_ACCESSFAILED; + } + + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_STARFIELD && + m_Type != TYPE_STARFIELD_LZ4_TEXTURE && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) { + // BSA extraction + if (compressed(fileInfo.file)) { + // Decompress data + if (m_Type != TYPE_SKYRIMSE) { + // Oblivion - Skyrim LE use gzip compression + EErrorCode result = ERROR_NONE; + try { + BSAULong length = 0UL; + std::shared_ptr<unsigned char[]> buffer = + decompress(dataBuffer.first.get(), dataBuffer.second, result, length); + if (buffer.get() != nullptr) { + outputFile.write(reinterpret_cast<char*>(buffer.get()), length); + buffer.reset(); + } + } catch (const std::exception&) { +#pragma message("report error!") + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + continue; + } + } else { + // Skyrim SE uses LZ4 Frame compression + if (!fileInfo.file->m_TextureChunks.size()) { + char* outBuffer = new char[fileInfo.file->m_UncompressedFileSize]; + + LZ4F_decompressionContext_t dcContext = nullptr; + LZ4F_decompressOptions_t options = {}; + LZ4F_createDecompressionContext(&dcContext, LZ4F_VERSION); + size_t outSize = fileInfo.file->m_UncompressedFileSize; + size_t inSize = dataBuffer.second; + + LZ4F_decompress(dcContext, outBuffer, &outSize, dataBuffer.first.get(), + &inSize, &options); + outputFile.write(outBuffer, fileInfo.file->m_UncompressedFileSize); + LZ4F_freeDecompressionContext(dcContext); + delete[] outBuffer; + } + } + } else { + // No compression - just write the data. + outputFile.write(reinterpret_cast<char*>(dataBuffer.first.get()), + dataBuffer.second); + } + } else { + // BA2 format + if (fileInfo.file->m_TextureChunks.size()) { + // Texture stream format - requires building the header data for the DDS file + bool isDX10 = false; + DirectX::DDS_HEADER_DXT10 DX10HeaderData = {}; + DirectX::DDS_HEADER DDSHeaderData = + getDDSHeader(fileInfo.file, DX10HeaderData, isDX10); + + outputFile.write("DDS ", 4); + char* DDSHeader = new char[sizeof(DDSHeaderData)]; + memcpy(DDSHeader, &DDSHeaderData, sizeof(DDSHeaderData)); + outputFile.write(DDSHeader, sizeof(DDSHeaderData)); + delete[] DDSHeader; + + if (isDX10) { + // This format requires DX10 header info + getDX10Header(DX10HeaderData, fileInfo.file, DDSHeaderData); + + char* DX10Header = new char[sizeof(DX10HeaderData)]; + memcpy(DX10Header, &DX10HeaderData, sizeof(DX10HeaderData)); + outputFile.write(DX10Header, sizeof(DX10HeaderData)); + delete[] DX10Header; + } + } + + EErrorCode result = ERROR_NONE; + try { + if (fileInfo.file->m_FileSize > 0 && !fileInfo.file->m_TextureChunks.size()) { + BSAULong length = fileInfo.file->m_UncompressedFileSize; + std::shared_ptr<unsigned char[]> buffer = + decompress(dataBuffer.first.get(), dataBuffer.second, result, length); + if (buffer.get() != nullptr) { + outputFile.write(reinterpret_cast<char*>(buffer.get()), length); + buffer.reset(); + } + } else { + outputFile.write(reinterpret_cast<char*>(dataBuffer.first.get()), + dataBuffer.second); + } + } catch (const std::exception&) { +#pragma message("report error!") + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + continue; + } + } + dataBuffer.first.reset(); + fileInfo.data.first.reset(); + } +} + +void Archive::createFolders(const std::string& targetDirectory, Folder::Ptr folder) +{ + for (std::vector<Folder::Ptr>::iterator iter = folder->m_SubFolders.begin(); + iter != folder->m_SubFolders.end(); ++iter) { + std::string subDirName = targetDirectory + "/" + (*iter)->getName(); +#ifdef _WIN32 + ::CreateDirectoryA(subDirName.c_str(), nullptr); +#else + mkdir(subDirName.c_str(), 0755); +#endif + createFolders(subDirName, *iter); + } +} + +EErrorCode Archive::extractAll( + const char* outputDirectory, + const std::function<bool(int value, std::string fileName)>& progress, + bool overwrite) +{ +#pragma message("report errors") + createFolders(outputDirectory, m_RootFolder); + + std::vector<File::Ptr> fileList; + m_RootFolder->collectFiles(fileList); + std::sort(fileList.begin(), fileList.end(), ByOffset); + m_File.seekg((*(fileList.begin()))->m_DataOffset); + + std::queue<FileInfo> buffers; + boost::mutex queueMutex; + int filesDone = 0; + boost::interprocess::interprocess_semaphore bufferCount(0); + boost::interprocess::interprocess_semaphore queueFree(100); + + boost::thread readerThread(boost::bind(&Archive::readFiles, this, boost::ref(buffers), + boost::ref(queueMutex), + boost::ref(bufferCount), boost::ref(queueFree), + fileList.begin(), fileList.end())); + + boost::thread extractThread(boost::bind( + &Archive::extractFiles, this, outputDirectory, boost::ref(buffers), + boost::ref(queueMutex), boost::ref(bufferCount), boost::ref(queueFree), + static_cast<int>(fileList.size()), overwrite, boost::ref(filesDone))); + + bool readerDone = false; + bool extractDone = false; + bool canceled = false; + while (!readerDone || !extractDone) { + if (!readerDone) { + readerDone = readerThread.timed_join(boost::posix_time::millisec(100)); + } + if (readerDone) { + extractDone = extractThread.timed_join(boost::posix_time::millisec(100)); + // don't cancel extractor before reader is done or else reader may be stuck trying + // to write to a queue + if (canceled) { + // ensure the extract thread wakes up. + extractThread.interrupt(); + bufferCount.post(); + } + } + size_t index = (std::min)(static_cast<size_t>(filesDone), fileList.size() - 1); + if (!progress((filesDone * 100) / static_cast<int>(fileList.size()), + fileList[index]->getName()) && + !canceled) { + readerThread.interrupt(); + canceled = true; // don't interrupt repeatedly + } + } + + return ERROR_NONE; +} + +bool Archive::compressed(const File::Ptr& file) const +{ + if (m_Type != TYPE_FALLOUT4 && m_Type != TYPE_FALLOUT4NG_7 && + m_Type != TYPE_FALLOUT4NG_8) + return file->compressToggled() ^ defaultCompressed(); + return (file->m_FileSize > 0); +} + +File::Ptr Archive::createFile(const std::string& name, const std::string& sourceName, + bool compressed) +{ + return File::Ptr( + new File(name, sourceName, nullptr, defaultCompressed() != compressed)); +} + +void Archive::cleanFolder(Folder::Ptr folder) +{ + std::vector<Folder::Ptr> folders; + folder->collectFolders(folders); + for (Folder::Ptr subFolder : folders) { + cleanFolder(subFolder); + } + std::vector<File::Ptr> files; + folder->collectFiles(files); + for (File::Ptr file : files) { + file.reset(); + } + folder.reset(); +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsaexception.cpp b/libs/bsatk/src/bsaexception.cpp new file mode 100644 index 0000000..9422848 --- /dev/null +++ b/libs/bsatk/src/bsaexception.cpp @@ -0,0 +1,39 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 "bsaexception.h" + +#include <cstdarg> +#include <cstdio> + +#pragma warning(disable : 4996) + +std::string makeString(const char* format, ...) +{ + va_list argList; + va_start(argList, format); + char buffer[1024]; + vsnprintf(buffer, 1024, format, argList); + return std::string(buffer); +} + +data_invalid_exception::data_invalid_exception(const std::string& message) + : m_Message(message) +{} diff --git a/libs/bsatk/src/bsafile.cpp b/libs/bsatk/src/bsafile.cpp new file mode 100644 index 0000000..db4e4e6 --- /dev/null +++ b/libs/bsatk/src/bsafile.cpp @@ -0,0 +1,154 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 "bsafile.h" + +#include <algorithm> +#include <climits> +#include <cstring> +#include <memory> +#include <stdexcept> + +#include "bsaexception.h" +#include "bsafolder.h" +#include "filehash.h" + +using std::fstream; +using std::ifstream; +using std::ofstream; + +namespace BSA +{ + +bool ByOffset(const File::Ptr& LHS, const File::Ptr& RHS) +{ + return LHS->getDataOffset() < RHS->getDataOffset(); +} + +static const unsigned long CHUNK_SIZE = 128 * 1024; + +File::File(std::fstream& file, Folder* folder) + : m_Folder(folder), m_New(false), m_FileSize(0), m_UncompressedFileSize(0), + m_ToggleCompressedWrite(false), m_DataOffsetWrite(0) +{ + m_NameHash = readType<BSAHash>(file); + m_FileSize = readType<BSAULong>(file); + m_DataOffset = readType<BSAULong>(file); + m_ToggleCompressed = m_FileSize & COMPRESSMASK; + m_FileSize = m_FileSize & SIZEMASK; +} + +File::File(const std::string& name, Folder* folder, BSAULong fileSize, + BSAHash dataOffset, BSAULong uncompressedFileSize, FO4TextureHeader header, + std::vector<FO4TextureChunk>& texChunks) + : m_Folder(folder), m_New(false), m_Name(name), m_FileSize(fileSize), + m_UncompressedFileSize(uncompressedFileSize), m_DataOffset(dataOffset), + m_TextureHeader(header), m_ToggleCompressedWrite(false), + m_TextureChunks(texChunks), m_DataOffsetWrite(0) +{ + m_NameHash = calculateBSAHash(name); + m_ToggleCompressed = false; + if (m_FileSize > 0 && m_UncompressedFileSize > 0) + m_ToggleCompressed = true; +} + +File::File(const std::string& name, const std::string& sourceFile, Folder* folder, + bool toggleCompressed) + : m_Folder(folder), m_New(true), m_Name(name), m_FileSize(0), + m_UncompressedFileSize(0), m_DataOffset(0), m_ToggleCompressed(toggleCompressed), + m_SourceFile(sourceFile), m_ToggleCompressedWrite(toggleCompressed), + m_DataOffsetWrite(0) +{ + m_NameHash = calculateBSAHash(name); +} + +std::string File::getFilePath() const +{ + return m_Folder->getFullPath() + "/" + m_Name; +} + +void File::writeHeader(fstream& file) const +{ + writeType<BSAHash>(file, m_NameHash); + BSAULong size = m_FileSize; + if (m_ToggleCompressed) { + size |= (1 << 30); + } + writeType<BSAULong>(file, size); + writeType<BSAULong>(file, m_DataOffsetWrite); +} + +EErrorCode File::writeData(fstream& sourceArchive, fstream& targetArchive) const +{ + m_DataOffsetWrite = static_cast<BSAULong>(targetArchive.tellp()); + EErrorCode result = ERROR_NONE; + + std::unique_ptr<char[]> inBuffer(new char[CHUNK_SIZE]); + + if (m_SourceFile.length() == 0) { + // copy from source archive +#pragma message("we may have to compress/decompress!") + sourceArchive.seekg(m_DataOffset, fstream::beg); + + try { + unsigned long sizeLeft = m_FileSize; + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + sourceArchive.read(inBuffer.get(), chunkSize); + targetArchive.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } catch (const std::exception&) { + result = ERROR_INVALIDDATA; + } + } else { + // copy from file on disc + fstream sourceFile; + sourceFile.open(m_SourceFile.c_str()); + if (!sourceFile.is_open()) { + return ERROR_SOURCEFILEMISSING; + } + sourceFile.seekg(0, fstream::end); + m_FileSize = static_cast<BSAULong>(sourceFile.tellg()); + unsigned long sizeLeft = m_FileSize; + sourceFile.seekg(0, fstream::beg); + while (sizeLeft > 0) { + int chunkSize = (std::min)(sizeLeft, CHUNK_SIZE); + sourceFile.read(inBuffer.get(), chunkSize); + targetArchive.write(inBuffer.get(), chunkSize); + sizeLeft -= chunkSize; + } + } + return result; +} + +void File::readFileName(fstream& file, bool testHashes) +{ + m_Name = readZString(file); + if (testHashes) { + if (calculateBSAHash(m_Name) != m_NameHash) { + throw data_invalid_exception( + makeString("invalid name hash for \"%s\" (%lx vs %lx)", m_Name.c_str(), + calculateBSAHash(m_Name), m_NameHash)); + } + } +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsafolder.cpp b/libs/bsatk/src/bsafolder.cpp new file mode 100644 index 0000000..21e2611 --- /dev/null +++ b/libs/bsatk/src/bsafolder.cpp @@ -0,0 +1,342 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 <filesystem> +#include <limits.h> + +#include "bsaarchive.h" +#include "bsaexception.h" +#include "bsafile.h" +#include "bsafolder.h" + +using std::fstream; + +namespace BSA +{ + +Folder::Folder() : m_Parent(nullptr), m_Name() +{ + m_NameHash = calculateBSAHash(m_Name); + m_FileCount = 0; + m_Offset = ULONG_MAX; +} + +Folder::Ptr Folder::readFolder(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos) +{ + Folder::Ptr result(new Folder()); + result->m_NameHash = readType<BSAHash>(file); + result->m_FileCount = readType<unsigned long>(file); + result->m_Offset = readType<unsigned long>(file); + std::streamoff pos = file.tellg(); + + file.seekg(result->m_Offset - fileNamesLength, fstream::beg); + + result->m_Name = readBString(file); + + for (unsigned long i = 0UL; i < result->m_FileCount; ++i) { + result->m_Files.push_back(File::Ptr(new File(file, result.get()))); + } + + if (static_cast<unsigned long>(file.tellg()) > endPos) { + endPos = static_cast<BSAULong>(file.tellg()); + } + + file.seekg(pos); + + return result; +} + +Folder::Ptr Folder::readFolderSE(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos) +{ + Folder::Ptr result(new Folder()); + result->m_NameHash = readType<BSAHash>(file); + result->m_FileCount = readType<BSAUInt>(file); + readType<BSAUInt>(file); + result->m_Offset = readType<BSAHash>(file); + std::streamoff pos = file.tellg(); + + file.seekg(result->m_Offset - fileNamesLength, fstream::beg); + + result->m_Name = readBString(file); + + for (unsigned long i = 0UL; i < result->m_FileCount; ++i) { + result->m_Files.push_back(File::Ptr(new File(file, result.get()))); + } + + if (static_cast<unsigned long>(file.tellg()) > endPos) { + endPos = static_cast<BSAULong>(file.tellg()); + } + + file.seekg(pos); + + return result; +} + +void Folder::writeHeader(std::fstream& file) const +{ + writeType<BSAHash>(file, m_NameHash); + writeType<BSAULong>(file, static_cast<BSAULong>(m_Files.size())); + writeType<BSAULong>(file, m_OffsetWrite); +} + +void Folder::writeData(std::fstream& file, BSAULong fileNamesLength) const +{ + m_OffsetWrite = static_cast<BSAULong>(file.tellp()) + fileNamesLength; + writeBString(file, getFullPath()); + for (std::vector<File::Ptr>::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + (*iter)->writeHeader(file); + } +} + +EErrorCode Folder::writeFileData(std::fstream& sourceFile, + std::fstream& targetFile) const +{ + for (std::vector<File::Ptr>::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + EErrorCode error = (*iter)->writeData(sourceFile, targetFile); + if (error != ERROR_NONE) { + return error; + } + } + return ERROR_NONE; +} + +std::string Folder::getFullPath() const +{ + if (m_Parent != nullptr) { + std::string temp = m_Parent->getFullPath(); + if (temp.length() != 0) { + return temp.append("\\").append(m_Name); + } else { + return m_Name; + } + } else { + // root folder shouldn't have a name + return std::string(); + } +} + +void Folder::addFolderInt(Folder::Ptr folder) +{ + std::filesystem::path path(folder->m_Name); + auto it = path.begin(); + std::string firstStr = it->string(); + std::filesystem::path remaining; + for (++it; it != path.end(); ++it) { + remaining /= *it; + } + + if (m_SubFoldersByName.contains(firstStr)) { + // remove the matched part of the path and recurse + folder->m_Name = remaining.string(); + m_SubFoldersByName.at(firstStr)->addFolderInt(folder); + return; + } + + // no subfolder matches, create one + if (remaining.empty()) { + // no more path components, add the new folder right here + folder->m_Parent = this; + m_SubFolders.push_back(folder); + m_SubFoldersByName[firstStr] = folder; + } else { + // add dummy folder for the next path component + Folder::Ptr dummy(new Folder); + dummy->m_Parent = this; + dummy->m_Name = firstStr; + folder->m_Name = remaining.string(); + dummy->addFolderInt(folder); + m_SubFolders.push_back(dummy); + m_SubFoldersByName[firstStr] = dummy; + } +} + +Folder::Ptr Folder::addOrFindFolderInt(Folder* folder) +{ + std::filesystem::path path(folder->m_Name); + auto it = path.begin(); + std::string firstStr = it->string(); + std::filesystem::path remaining; + for (++it; it != path.end(); ++it) { + remaining /= *it; + } + + if (m_SubFoldersByName.contains(firstStr)) { + if (!remaining.empty()) { + // remove the matched part of the path and recurse + folder->m_Name = remaining.string(); + return m_SubFoldersByName.at(firstStr)->addOrFindFolderInt(folder); + } else { + return m_SubFoldersByName.at(firstStr); + } + } + + // no subfolder matches, create one + if (remaining.empty()) { + // no more path components, add the new folder right here + folder->m_Parent = this; + m_SubFolders.push_back(Folder::Ptr(folder)); + m_SubFoldersByName[firstStr] = m_SubFolders.back(); + return m_SubFolders.back(); + } else { + // add dummy folder for the next path component + Folder::Ptr dummy(new Folder); + dummy->m_Parent = this; + dummy->m_Name = firstStr; + folder->m_Name = remaining.string(); + Folder::Ptr result = dummy->addOrFindFolderInt(folder); + m_SubFolders.push_back(dummy); + m_SubFoldersByName[firstStr] = dummy; + return result; + } +} + +Folder::Ptr Folder::addFolder(std::fstream& file, BSAUInt fileNamesLength, + BSAUInt& endPos, ArchiveType type) +{ + Folder::Ptr temp; + if (type == ArchiveType::TYPE_SKYRIMSE) + temp = readFolderSE(file, fileNamesLength, endPos); + else + temp = readFolder(file, fileNamesLength, endPos); + addFolderInt(temp); + + return temp; +} + +Folder::Ptr Folder::addFolderFromFile(std::string filePath, BSAUInt size, + BSAHash offset, BSAUInt uncompressedSize, + FO4TextureHeader header, + std::vector<FO4TextureChunk>& texChunks) +{ + std::filesystem::path file(filePath); + + Folder* tempFolder = new Folder(); + + tempFolder->m_NameHash = calculateBSAHash(filePath); + tempFolder->m_Name = file.parent_path().string(); + Folder::Ptr result = addOrFindFolderInt(tempFolder); + + if (result.get() != tempFolder) { + delete tempFolder; + } + + std::string fileName = file.filename().string(); + + result->m_FileCount++; + result->m_Files.push_back(File::Ptr(new File(fileName, result.get(), size, offset, + uncompressedSize, header, texChunks))); + + return result; +} + +bool Folder::resolveFileNames(std::fstream& file, bool testHashes) +{ + bool hashesValid = true; + for (std::vector<File::Ptr>::iterator iter = m_Files.begin(); iter != m_Files.end(); + ++iter) { + try { + (*iter)->readFileName(file, testHashes); + } catch (const std::exception&) { + hashesValid = false; + } + } + return hashesValid; +} + +const Folder::Ptr Folder::getSubFolder(unsigned int index) const +{ + return m_SubFolders.at(index); +} + +unsigned int Folder::countFiles() const +{ + unsigned int result = 0; + for (std::vector<Folder::Ptr>::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + result += (*iter)->countFiles(); + } + return result + static_cast<unsigned int>(m_Files.size()); +} + +const File::Ptr Folder::getFile(unsigned int index) const +{ + return m_Files.at(index); +} + +Folder::Ptr Folder::addFolder(const std::string& folderName) +{ + Folder::Ptr newFolder(new Folder); + newFolder->m_Name = folderName; + newFolder->m_Parent = this; + m_SubFolders.push_back(newFolder); + return newFolder; +} + +void Folder::collectFolders(std::vector<Folder::Ptr>& folderList) const +{ + for (std::vector<Folder::Ptr>::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + if ((*iter)->m_Files.size() != 0) { + folderList.push_back(*iter); + } + (*iter)->collectFolders(folderList); + } +} + +void Folder::collectFiles(std::vector<File::Ptr>& fileList) const +{ + for (std::vector<File::Ptr>::const_iterator fileIter = m_Files.begin(); + fileIter != m_Files.end(); ++fileIter) { + fileList.push_back(*fileIter); + } + for (std::vector<Folder::Ptr>::const_iterator folderIter = m_SubFolders.begin(); + folderIter != m_SubFolders.end(); ++folderIter) { + (*folderIter)->collectFiles(fileList); + } +} + +void Folder::collectFileNames(std::vector<std::string>& nameList) const +{ + for (std::vector<File::Ptr>::const_iterator iter = m_Files.begin(); + iter != m_Files.end(); ++iter) { + nameList.push_back((*iter)->getName()); + } + for (std::vector<Folder::Ptr>::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + (*iter)->collectFileNames(nameList); + } +} + +void Folder::collectFolderNames(std::vector<std::string>& nameList) const +{ + if (m_Files.size() != 0) { + nameList.push_back(getFullPath()); + } + for (std::vector<Folder::Ptr>::const_iterator iter = m_SubFolders.begin(); + iter != m_SubFolders.end(); ++iter) { + (*iter)->collectFolderNames(nameList); + } +} + +} // namespace BSA diff --git a/libs/bsatk/src/bsatypes.cpp b/libs/bsatk/src/bsatypes.cpp new file mode 100644 index 0000000..eb33af3 --- /dev/null +++ b/libs/bsatk/src/bsatypes.cpp @@ -0,0 +1,70 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 "bsatypes.h" + +#include <algorithm> +#include <climits> +#include <cstring> +#include <fstream> +#include <string> + +using std::fstream; + +/*#ifndef MAX_PATH +#define MAX_PATH PATH_MAX +#endif // MAX_PATH +*/ + +std::string readBString(fstream& file) +{ + unsigned char length = readType<unsigned char>(file); + char buffer[256]; + if (length > 0) { + if (!file.read(buffer, length)) { + throw data_invalid_exception("can't read from bsa"); + } + } + buffer[length] = '\0'; + return std::string(buffer); +} + +void writeBString(fstream& file, const std::string& string) +{ + unsigned int length = + std::min<unsigned int>(static_cast<unsigned int>(string.length()), 255); + writeType<unsigned char>(file, length + 1); + file.write(string.c_str(), length + 1); +} + +std::string readZString(fstream& file) +{ + char buffer[FILENAME_MAX]; + memset(buffer, '\0', FILENAME_MAX); + if (!file.getline(buffer, FILENAME_MAX, '\0')) { + throw data_invalid_exception("can't read from bsa"); + } + return std::string(buffer); +} + +void writeZString(fstream& file, const std::string& string) +{ + file.write(string.c_str(), string.length() + 1); +} diff --git a/libs/bsatk/src/filehash.cpp b/libs/bsatk/src/filehash.cpp new file mode 100644 index 0000000..717a02c --- /dev/null +++ b/libs/bsatk/src/filehash.cpp @@ -0,0 +1,100 @@ +/* +Mod Organizer BSA handling + +Copyright (C) 2012 Sebastian Herbord. 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 "filehash.h" + +#include <algorithm> +#include <climits> +#include <cstdlib> +#include <cstring> + +#ifndef MAX_PATH +#define MAX_PATH PATH_MAX +#endif // MAX_PATH + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +/** + * @brief calculateBSAHash + * @param fileName + * @return + * @note the hash calculated for folders seem to be wrong + */ +BSAHash calculateBSAHash(const std::string& fileName) +{ + char fileNameLower[FILENAME_MAX + 1]; + int i = 0; + for (; i < FILENAME_MAX && fileName[i] != '\0'; ++i) { + fileNameLower[i] = tolower(fileName[i]); + if (fileNameLower[i] == '/') { + fileNameLower[i] = '\\'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast<unsigned char*>(fileNameLower); + + size_t length = strlen(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + length; + } + + size_t extLen = strlen(ext); + length -= extLen; + + unsigned char* extU = reinterpret_cast<unsigned char*>(ext); + + BSAHash hash1 = 0ULL; + + if (length > 0) { + hash1 = static_cast<BSAHash>(fileNameLowerU[length - 1] | + ((length > 2 ? fileNameLowerU[length - 2] : 0) << 8) | + (length << 16) | (fileNameLowerU[0] << 24)); + } + + if (extLen > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash1 |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash1 |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash1 |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash1 |= 0x80000000; + } + + BSAHash hash2 = static_cast<BSAHash>(genHashInt(fileNameLowerU + 1, extU - 2)) + + static_cast<BSAHash>(genHashInt(extU, extU + strlen(ext))); + + hash1 |= (hash2 & 0xFFFFFFFF) << 32; + } + + return hash1; +} |
