1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-10-16 05:16:33 +00:00
openmw/apps/components_tests/bsa/testbsafile.cpp
elsid 7bf1ea32b0
Disable not working tests for MSVC
std::streambuf in MSVC does not support buffers larger than 2**31 - 1:
https://developercommunity.visualstudio.com/t/stdbasic-stringbuf-is-broken/290124

Simple test to check if it works:

TEST(IMemStreamTest, shouldRead)
{
    std::string src(std::numeric_limits<uint32_t>::max() / 2 + 1, '\0');
    Files::IMemStream stream(src.data(), src.size());
    std::string dst(src.size(), '\0');
    stream.read(dst.data(), src.size());
    EXPECT_FALSE(stream.fail()) << std::generic_category().message(errno);
}
2025-10-12 11:11:24 +02:00

356 lines
12 KiB
C++

#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
}
}