Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
12 kB · 356 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357#include "operators.hpp"
#include <components/bsa/compressedbsafile.hpp>#include <components/files/memorystream.hpp>#include <components/testing/util.hpp>
#include <gmock/gmock.h>#include <gtest/gtest.h>
#include <cstdint>#include <cstdlib>#include <cstring>#include <filesystem>#include <format>#include <fstream>#include <memory>#include <sstream>#include <vector>
namespace Bsa{ namespace { using namespace ::testing;
struct Free { void operator()(void* ptr) const { std::free(ptr); } };
struct Buffer { std::unique_ptr<char, Free> mData; std::size_t mCapacity; };
struct Header { uint32_t mFormat; uint32_t mDirSize; uint32_t mFileCount; };
struct Archive { Header mHeader; std::vector<std::uint32_t> mOffsets; std::vector<char> mStringBuffer; std::vector<BSAFile::Hash> mHashes; std::size_t mTailSize; };
struct TestBSAFile final : public BSAFile { void readHeader(std::istream& input) override { BSAFile::readHeader(input); }
void writeHeader() override { throw std::logic_error("TestBSAFile::writeHeader is not implemented"); } };
void writeArchive(const Archive& value, std::ostream& stream) { stream.write(reinterpret_cast<const char*>(&value.mHeader), sizeof(value.mHeader));
if (!value.mOffsets.empty()) stream.write(reinterpret_cast<const char*>(value.mOffsets.data()), value.mOffsets.size() * sizeof(std::uint32_t));
if (!value.mStringBuffer.empty()) stream.write(reinterpret_cast<const char*>(value.mStringBuffer.data()), value.mStringBuffer.size());
for (const BSAFile::Hash& hash : value.mHashes) stream.write(reinterpret_cast<const char*>(&hash), sizeof(BSAFile::Hash));
const std::size_t chunkSize = 4096; std::vector<char> chunk(chunkSize); for (std::size_t i = 0; i < value.mTailSize; i += chunkSize) stream.write(reinterpret_cast<const char*>(chunk.data()), std::min(chunk.size(), value.mTailSize - i)); }
std::filesystem::path makeOutputPath() { const auto testInfo = UnitTest::GetInstance()->current_test_info(); return TestingOpenMW::outputFilePath( std::format("{}.{}.bsa", testInfo->test_suite_name(), testInfo->name())); }
Buffer makeBsaBuffer(std::uint32_t fileSize, std::uint32_t fileOffset) { std::ostringstream stream;
const Header header{ .mFormat = static_cast<std::uint32_t>(BsaVersion::Uncompressed), .mDirSize = 14, .mFileCount = 1, };
const BSAFile::Hash hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd, };
const Archive archive{ .mHeader = header, .mOffsets = { fileSize, fileOffset, 0 }, .mStringBuffer = { 'a', '\0' }, .mHashes = { hash }, .mTailSize = 0, };
writeArchive(archive, stream);
const std::string data = std::move(stream).str();
const std::size_t capacity = static_cast<std::size_t>(fileSize) + static_cast<std::size_t>(fileOffset) + 34; std::unique_ptr<char, Free> buffer(reinterpret_cast<char*>(std::malloc(capacity)));
if (buffer == nullptr) throw std::bad_alloc();
std::memcpy(buffer.get(), data.data(), data.size());
return Buffer{ .mData = std::move(buffer), .mCapacity = capacity }; }
TEST(BSAFileTest, shouldHandleEmpty) { const std::filesystem::path path = makeOutputPath();
{ std::ofstream stream; stream.exceptions(std::ifstream::failbit | std::ifstream::badbit); stream.open(path, std::ios::binary); }
BSAFile file; EXPECT_THROW(file.open(path), std::runtime_error);
EXPECT_THAT(file.getList(), IsEmpty()); }
TEST(BSAFileTest, shouldHandleZeroFiles) { const std::filesystem::path path = makeOutputPath();
{ std::ofstream stream; stream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
stream.open(path, std::ios::binary);
const Header header{ .mFormat = static_cast<std::uint32_t>(BsaVersion::Uncompressed), .mDirSize = 0, .mFileCount = 0, };
const Archive archive{ .mHeader = header, .mOffsets = {}, .mStringBuffer = {}, .mHashes = {}, .mTailSize = 0, };
writeArchive(archive, stream); }
BSAFile file; file.open(path);
EXPECT_THAT(file.getList(), IsEmpty()); }
TEST(BSAFileTest, shouldHandleSingleFile) { const std::filesystem::path path = makeOutputPath();
{ std::ofstream stream; stream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
stream.open(path, std::ios::binary);
const Header header{ .mFormat = static_cast<std::uint32_t>(BsaVersion::Uncompressed), .mDirSize = 14, .mFileCount = 1, };
const BSAFile::Hash hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd, };
const Archive archive{ .mHeader = header, .mOffsets = { 42, 0, 0 }, .mStringBuffer = { 'a', '\0' }, .mHashes = { hash }, .mTailSize = 42, };
writeArchive(archive, stream); }
BSAFile file; file.open(path);
std::vector<char> namesBuffer = { 'a', '\0' };
EXPECT_THAT(file.getList(), ElementsAre(BSAFile::FileStruct{ .mFileSize = 42, .mOffset = 34, .mHash = BSAFile::Hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd }, .mNameOffset = 0, .mNameSize = 1, .mNamesBuffer = &namesBuffer, })); }
TEST(BSAFileTest, shouldHandleTwoFiles) { const std::filesystem::path path = makeOutputPath();
{ std::ofstream stream; stream.exceptions(std::ifstream::failbit | std::ifstream::badbit);
stream.open(path, std::ios::binary);
const std::uint32_t fileSize1 = 42; const std::uint32_t fileSize2 = 13;
const Header header{ .mFormat = static_cast<std::uint32_t>(BsaVersion::Uncompressed), .mDirSize = 28, .mFileCount = 2, };
const BSAFile::Hash hash1{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd, };
const BSAFile::Hash hash2{ .mLow = 0x11112222, .mHigh = 0x33334444, };
const Archive archive{ .mHeader = header, .mOffsets = { fileSize1, 0, fileSize2, fileSize1, 0, 2 }, .mStringBuffer = { 'a', '\0', 'b', '\0' }, .mHashes = { hash1, hash2 }, .mTailSize = fileSize1 + fileSize2, };
writeArchive(archive, stream); }
BSAFile file; file.open(path);
std::vector<char> namesBuffer = { 'a', '\0', 'b', '\0' };
EXPECT_THAT(file.getList(), ElementsAre( BSAFile::FileStruct{ .mFileSize = 42, .mOffset = 56, .mHash = BSAFile::Hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd }, .mNameOffset = 0, .mNameSize = 1, .mNamesBuffer = &namesBuffer, }, BSAFile::FileStruct{ .mFileSize = 13, .mOffset = 98, .mHash = BSAFile::Hash{ .mLow = 0x11112222, .mHigh = 0x33334444 }, .mNameOffset = 2, .mNameSize = 1, .mNamesBuffer = &namesBuffer, })); }
TEST(BSAFileTest, shouldHandleSomewhatLargeFiles) { constexpr std::uint32_t maxUInt32 = std::numeric_limits<uint32_t>::max(); constexpr std::uint32_t fileSize = maxUInt32 / 4; constexpr std::uint32_t fileOffset = maxUInt32 / 4 - 34; const Buffer buffer = makeBsaBuffer(fileSize, fileOffset);
TestBSAFile file; // Use capacity assuming we never read beyond small header. Files::IMemStream stream(buffer.mData.get(), buffer.mCapacity); file.readHeader(stream);
std::vector<char> namesBuffer = { 'a', '\0' };
EXPECT_THAT(file.getList(), ElementsAre(BSAFile::FileStruct{ .mFileSize = maxUInt32 / 4, .mOffset = maxUInt32 / 4, .mHash = BSAFile::Hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd }, .mNameOffset = 0, .mNameSize = 1, .mNamesBuffer = &namesBuffer, })); }
// std::streambuf in MSVC does not support buffers larger than 2**31 - 1:// https://developercommunity.visualstudio.com/t/stdbasic-stringbuf-is-broken/290124#ifndef _MSC_VER TEST(BSAFileTest, shouldHandleSingleFileAtTheEndOfLargeFile) { constexpr std::uint32_t maxUInt32 = std::numeric_limits<uint32_t>::max(); constexpr std::uint32_t fileSize = maxUInt32; constexpr std::uint32_t fileOffset = maxUInt32 - 34; const Buffer buffer = makeBsaBuffer(fileSize, fileOffset);
TestBSAFile file; // Use capacity assuming we never read beyond small header. Files::IMemStream stream(buffer.mData.get(), buffer.mCapacity); file.readHeader(stream);
std::vector<char> namesBuffer = { 'a', '\0' };
EXPECT_THAT(file.getList(), ElementsAre(BSAFile::FileStruct{ .mFileSize = maxUInt32, .mOffset = maxUInt32, .mHash = BSAFile::Hash{ .mLow = 0xaaaabbbb, .mHigh = 0xccccdddd }, .mNameOffset = 0, .mNameSize = 1, .mNamesBuffer = &namesBuffer, })); }
TEST(BSAFileTest, shouldThrowExceptionOnTooBigAbsoluteOffset) { constexpr std::uint32_t maxUInt32 = std::numeric_limits<uint32_t>::max(); constexpr std::uint32_t fileSize = maxUInt32; constexpr std::uint32_t fileOffset = maxUInt32 - 34 + 1; const Buffer buffer = makeBsaBuffer(fileSize, fileOffset);
TestBSAFile file; // Use capacity assuming we never read beyond small header. Files::IMemStream stream(buffer.mData.get(), buffer.mCapacity); EXPECT_THROW(file.readHeader(stream), std::runtime_error);
EXPECT_THAT(file.getList(), IsEmpty()); }#endif }}