mirror of
https://github.com/OpenMW/openmw.git
synced 2025-10-14 13:56:42 +00:00
Add tests for BSAFile
This commit is contained in:
parent
d7f6d7c13c
commit
c87cc643d1
12 changed files with 362 additions and 59 deletions
|
@ -89,6 +89,7 @@ file(GLOB UNITTEST_SRC_FILES
|
|||
|
||||
sceneutil/osgacontroller.cpp
|
||||
|
||||
bsa/testbsafile.cpp
|
||||
bsa/testcompressedbsafile.cpp
|
||||
)
|
||||
|
||||
|
|
40
apps/components_tests/bsa/operators.hpp
Normal file
40
apps/components_tests/bsa/operators.hpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#ifndef COMPONETS_TESTS_BSA_OPERATORS_H
|
||||
#define COMPONETS_TESTS_BSA_OPERATORS_H
|
||||
|
||||
#include <components/bsa/bsafile.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include <tuple>
|
||||
|
||||
namespace Bsa
|
||||
{
|
||||
inline auto makeTuple(const BSAFile::Hash& value)
|
||||
{
|
||||
return std::make_tuple(value.mLow, value.mHigh);
|
||||
}
|
||||
|
||||
inline auto makeTuple(const BSAFile::FileStruct& value)
|
||||
{
|
||||
return std::make_tuple(
|
||||
value.mFileSize, value.mOffset, makeTuple(value.mHash), value.mNameOffset, value.mNameSize, value.name());
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& stream, const BSAFile::Hash& value)
|
||||
{
|
||||
return stream << "Hash { .mLow = " << value.mLow << ", .mHigh = " << value.mHigh << "}";
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& stream, const BSAFile::FileStruct& value)
|
||||
{
|
||||
return stream << "FileStruct { .mFileSize = " << value.mFileSize << ", .mOffset = " << value.mOffset
|
||||
<< ", .mHash = " << value.mHash << ", .mNameOffset = " << value.mNameOffset
|
||||
<< ", .mNameSize = " << value.mNameSize << ", .name() = " << value.name() << "}";
|
||||
}
|
||||
|
||||
inline bool operator==(const BSAFile::FileStruct& lhs, const BSAFile::FileStruct& rhs)
|
||||
{
|
||||
return makeTuple(lhs) == makeTuple(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
302
apps/components_tests/bsa/testbsafile.cpp
Normal file
302
apps/components_tests/bsa/testbsafile.cpp
Normal file
|
@ -0,0 +1,302 @@
|
|||
#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 <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Bsa
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using namespace ::testing;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
std::string makeBsaBuffer(std::uint32_t fileSize, std::uint32_t fileOffset)
|
||||
{
|
||||
std::string buffer;
|
||||
|
||||
buffer.reserve(static_cast<std::size_t>(fileSize) + static_cast<std::size_t>(fileOffset) + 34);
|
||||
|
||||
std::ostringstream stream(std::move(buffer));
|
||||
|
||||
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);
|
||||
|
||||
return std::move(stream).str();
|
||||
}
|
||||
|
||||
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, shouldHandleSingleFileAtTheEndOfLargeFile)
|
||||
{
|
||||
constexpr std::uint32_t maxUInt32 = std::numeric_limits<uint32_t>::max();
|
||||
const std::string buffer = makeBsaBuffer(maxUInt32, maxUInt32 - 34);
|
||||
|
||||
TestBSAFile file;
|
||||
// Use capacity assuming we never read beyond small header.
|
||||
Files::IMemStream stream(buffer.data(), buffer.capacity());
|
||||
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();
|
||||
const std::string buffer = makeBsaBuffer(maxUInt32, maxUInt32 - 34 + 1);
|
||||
|
||||
TestBSAFile file;
|
||||
// Use capacity assuming we never read beyond small header.
|
||||
Files::IMemStream stream(buffer.data(), buffer.capacity());
|
||||
EXPECT_THROW(file.readHeader(stream), std::runtime_error);
|
||||
|
||||
EXPECT_THAT(file.getList(), IsEmpty());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#include "operators.hpp"
|
||||
|
||||
#include <components/bsa/compressedbsafile.hpp>
|
||||
#include <components/testing/util.hpp>
|
||||
|
||||
|
@ -9,41 +11,9 @@
|
|||
#include <format>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace Bsa
|
||||
{
|
||||
namespace
|
||||
{
|
||||
auto makeTuple(const BSAFile::Hash& value)
|
||||
{
|
||||
return std::make_tuple(value.mLow, value.mHigh);
|
||||
}
|
||||
|
||||
auto makeTuple(const BSAFile::FileStruct& value)
|
||||
{
|
||||
return std::make_tuple(value.mFileSize, value.mOffset, makeTuple(value.mHash), value.mNameOffset,
|
||||
value.mNameSize, value.name());
|
||||
}
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& stream, const BSAFile::Hash& value)
|
||||
{
|
||||
return stream << "Hash { .mLow = " << value.mLow << ", .mHigh = " << value.mHigh << "}";
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& stream, const BSAFile::FileStruct& value)
|
||||
{
|
||||
return stream << "FileStruct { .mFileSize = " << value.mFileSize << ", .mOffset = " << value.mOffset
|
||||
<< ", .mHash = " << value.mHash << ", .mNameOffset = " << value.mNameOffset
|
||||
<< ", .mNameSize = " << value.mNameSize << ", .name() = " << value.name() << "}";
|
||||
}
|
||||
|
||||
inline bool operator==(const BSAFile::FileStruct& lhs, const BSAFile::FileStruct& rhs)
|
||||
{
|
||||
return makeTuple(lhs) == makeTuple(rhs);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace ::testing;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
|
@ -75,12 +75,10 @@ namespace Bsa
|
|||
}
|
||||
|
||||
/// Read header information from the input source
|
||||
void BA2DX10File::readHeader()
|
||||
void BA2DX10File::readHeader(std::istream& input)
|
||||
{
|
||||
assert(!mIsLoaded);
|
||||
|
||||
std::ifstream input(mFilepath, std::ios_base::binary);
|
||||
|
||||
const std::streamsize fsize = Files::getStreamSizeLeft(input);
|
||||
|
||||
if (fsize < 24) // header is 24 bytes
|
||||
|
@ -138,8 +136,6 @@ namespace Bsa
|
|||
mFiles[i].mNameSize = fileNameSize;
|
||||
mFiles[i].mNamesBuffer = &mFileNames.back();
|
||||
}
|
||||
|
||||
mIsLoaded = true;
|
||||
}
|
||||
|
||||
std::optional<BA2DX10File::FileRecord> BA2DX10File::getFileRecord(std::string_view str) const
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace Bsa
|
|||
virtual ~BA2DX10File();
|
||||
|
||||
/// Read header information from the input source
|
||||
void readHeader() override;
|
||||
void readHeader(std::istream& stream) override;
|
||||
|
||||
Files::IStreamPtr getFile(const char* filePath);
|
||||
Files::IStreamPtr getFile(const FileStruct* fileStruct);
|
||||
|
|
|
@ -70,12 +70,10 @@ namespace Bsa
|
|||
}
|
||||
|
||||
/// Read header information from the input source
|
||||
void BA2GNRLFile::readHeader()
|
||||
void BA2GNRLFile::readHeader(std::istream& input)
|
||||
{
|
||||
assert(!mIsLoaded);
|
||||
|
||||
std::ifstream input(mFilepath, std::ios_base::binary);
|
||||
|
||||
const std::streamsize fsize = Files::getStreamSizeLeft(input);
|
||||
|
||||
if (fsize < 24) // header is 24 bytes
|
||||
|
@ -129,8 +127,6 @@ namespace Bsa
|
|||
mFiles[i].mNameSize = fileNameSize;
|
||||
mFiles[i].mNamesBuffer = &mFileNames.back();
|
||||
}
|
||||
|
||||
mIsLoaded = true;
|
||||
}
|
||||
|
||||
BA2GNRLFile::FileRecord BA2GNRLFile::getFileRecord(std::string_view str) const
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Bsa
|
|||
virtual ~BA2GNRLFile();
|
||||
|
||||
/// Read header information from the input source
|
||||
void readHeader() override;
|
||||
void readHeader(std::istream& input) override;
|
||||
|
||||
Files::IStreamPtr getFile(const char* filePath);
|
||||
Files::IStreamPtr getFile(const FileStruct* fileStruct);
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <system_error>
|
||||
|
||||
#include <components/esm/fourcc.hpp>
|
||||
|
@ -73,7 +74,7 @@ BSAFile::Hash getHash(const std::string& name)
|
|||
}
|
||||
|
||||
/// Read header information from the input source
|
||||
void BSAFile::readHeader()
|
||||
void BSAFile::readHeader(std::istream& input)
|
||||
{
|
||||
/*
|
||||
* The layout of a BSA archive is as follows:
|
||||
|
@ -107,8 +108,6 @@ void BSAFile::readHeader()
|
|||
*/
|
||||
assert(!mIsLoaded);
|
||||
|
||||
std::ifstream input(mFilepath, std::ios_base::binary);
|
||||
|
||||
// Total archive size
|
||||
const std::streamsize fsize = Files::getStreamSizeLeft(input);
|
||||
|
||||
|
@ -211,8 +210,6 @@ void BSAFile::readHeader()
|
|||
|
||||
std::sort(mFiles.begin(), mFiles.end(),
|
||||
[](const FileStruct& left, const FileStruct& right) { return left.mOffset < right.mOffset; });
|
||||
|
||||
mIsLoaded = true;
|
||||
}
|
||||
|
||||
/// Write header information to the output sink
|
||||
|
@ -260,7 +257,11 @@ void BSAFile::open(const std::filesystem::path& file)
|
|||
|
||||
mFilepath = file;
|
||||
if (std::filesystem::exists(file))
|
||||
readHeader();
|
||||
{
|
||||
std::ifstream input(mFilepath, std::ios_base::binary);
|
||||
readHeader(input);
|
||||
mIsLoaded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -95,7 +96,7 @@ namespace Bsa
|
|||
[[noreturn]] void fail(const std::string& msg) const;
|
||||
|
||||
/// Read header information from the input source
|
||||
virtual void readHeader();
|
||||
virtual void readHeader(std::istream& input);
|
||||
virtual void writeHeader();
|
||||
|
||||
public:
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
#include <cerrno>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <system_error>
|
||||
|
||||
#include <lz4frame.h>
|
||||
|
@ -45,12 +45,10 @@
|
|||
namespace Bsa
|
||||
{
|
||||
/// Read header information from the input source
|
||||
void CompressedBSAFile::readHeader()
|
||||
void CompressedBSAFile::readHeader(std::istream& input)
|
||||
{
|
||||
assert(!mIsLoaded);
|
||||
|
||||
std::ifstream input(mFilepath, std::ios_base::binary);
|
||||
|
||||
const std::streamsize fsize = Files::getStreamSizeLeft(input);
|
||||
|
||||
if (fsize < 36) // Header is 36 bytes
|
||||
|
@ -208,8 +206,6 @@ namespace Bsa
|
|||
mFiles.push_back(fileStruct);
|
||||
}
|
||||
}
|
||||
|
||||
mIsLoaded = true;
|
||||
}
|
||||
|
||||
CompressedBSAFile::FileRecord CompressedBSAFile::getFileRecord(std::string_view str) const
|
||||
|
|
|
@ -125,7 +125,7 @@ namespace Bsa
|
|||
virtual ~CompressedBSAFile() = default;
|
||||
|
||||
/// Read header information from the input source
|
||||
void readHeader() override;
|
||||
void readHeader(std::istream& input) override;
|
||||
|
||||
Files::IStreamPtr getFile(const char* filePath);
|
||||
Files::IStreamPtr getFile(const FileStruct* fileStruct);
|
||||
|
|
Loading…
Reference in a new issue