diff options
Diffstat (limited to 'libs/installer_bsplugins/src/TESFile')
| -rw-r--r-- | libs/installer_bsplugins/src/TESFile/Reader.h | 67 | ||||
| -rw-r--r-- | libs/installer_bsplugins/src/TESFile/Reader.inl | 238 | ||||
| -rw-r--r-- | libs/installer_bsplugins/src/TESFile/Stream.h | 291 | ||||
| -rw-r--r-- | libs/installer_bsplugins/src/TESFile/Type.h | 78 |
4 files changed, 674 insertions, 0 deletions
diff --git a/libs/installer_bsplugins/src/TESFile/Reader.h b/libs/installer_bsplugins/src/TESFile/Reader.h new file mode 100644 index 0000000..0479842 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Reader.h @@ -0,0 +1,67 @@ +#ifndef TESFILE_READER_H +#define TESFILE_READER_H + +#include "Stream.h" + +#include <concepts> +#include <cstdint> +#include <filesystem> +#include <istream> +#include <utility> + +namespace TESFile +{ + +template <typename Handler> +concept ReaderHandler = requires(Handler& handler) { + { + handler.Group(std::declval<GroupData>()) + } -> std::convertible_to<bool>; + { + handler.Form(std::declval<FormData>()) + } -> std::convertible_to<bool>; + { + handler.Chunk(std::declval<Type>()) + } -> std::convertible_to<bool>; + { + handler.Data(std::declval<std::istream&>()) + }; +}; + +template <ReaderHandler Handler> +class Reader +{ +public: + void parse(const std::filesystem::path& path, Handler& handler); + + void parse(std::istream& stream, Handler& handler); + +private: + enum HeaderSize + { + HeaderSize_Standard = sizeof(RecordHeader), + HeaderSize_Oblivion = 20, + HeaderSize_Morrowind = 16, + }; + + std::uint32_t parsePluginInfo(std::istream& stream, Handler& handler); + + std::uint32_t parseRecord(std::istream& stream, Handler& handler); + + std::uint32_t handleForm(std::istream& stream, const RecordHeader& header, + Handler& handler); + + std::uint32_t handleGroup(std::istream& stream, const RecordHeader& header, + Handler& handler); + + std::uint32_t parseChunk(std::istream& stream, Handler& handler); + + TESFormat chunkFormat_; + int headerSize_; +}; + +} // namespace TESFile + +#include "Reader.inl" + +#endif // TESFILE_READER_H diff --git a/libs/installer_bsplugins/src/TESFile/Reader.inl b/libs/installer_bsplugins/src/TESFile/Reader.inl new file mode 100644 index 0000000..3324a15 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Reader.inl @@ -0,0 +1,238 @@ +#include "Reader.h" + +#include <zlib.h> + +#include <cstring> +#include <format> +#include <fstream> +#include <stdexcept> + +namespace TESFile +{ + +template <ReaderHandler Handler> +inline void Reader<Handler>::parse(const std::filesystem::path& path, Handler& handler) +{ + std::ifstream stream; + stream.open(path, std::ios_base::binary | std::ios_base::in); + if (!stream.good()) { + throw std::runtime_error(std::strerror(errno)); + } + parse(stream, handler); +} + +template <ReaderHandler Handler> +inline void Reader<Handler>::parse(std::istream& stream, Handler& handler) +{ + parsePluginInfo(stream, handler); + while (stream.good() && stream.peek() != std::char_traits<char>::eof()) { + parseRecord(stream, handler); + } +} + +template <ReaderHandler Handler> +inline std::uint32_t Reader<Handler>::parsePluginInfo(std::istream& stream, + Handler& handler) +{ + const RecordHeader header = readType<RecordHeader>(stream); + if (stream.fail()) { + throw std::runtime_error("record incomplete"); + } + + if (header.type == "TES4"_ts) { + if (header.old.firstChunk == "HEDR"_ts) { + chunkFormat_ = TESFormat::Oblivion; + headerSize_ = HeaderSize_Oblivion; + stream.seekg(HeaderSize_Oblivion - sizeof(RecordHeader), std::istream::cur); + } else { + chunkFormat_ = TESFormat::Standard; + headerSize_ = sizeof(RecordHeader); + } + } else if (header.type == "TES3"_ts) { + chunkFormat_ = TESFormat::Morrowind; + headerSize_ = HeaderSize_Morrowind; + stream.seekg(HeaderSize_Morrowind - sizeof(RecordHeader), std::istream::cur); + } else { + throw std::runtime_error( + std::format("Unrecognized plugin info type: '{}'", header.type.value)); + } + + return handleForm(stream, header, handler); +} + +template <ReaderHandler Handler> +inline std::uint32_t Reader<Handler>::parseRecord(std::istream& stream, + Handler& handler) +{ + RecordHeader header; + stream.read(reinterpret_cast<char*>(&header), headerSize_); + if (stream.fail()) { + throw std::runtime_error("record incomplete"); + } + + if (header.type == "GRUP"_ts) { + return handleGroup(stream, header, handler); + } else { + return handleForm(stream, header, handler); + } +} + +template <ReaderHandler Handler> +inline std::uint32_t Reader<Handler>::handleForm(std::istream& stream, + const RecordHeader& header, + Handler& handler) +{ + std::uint32_t dataSize = header.dataSize; + const bool compressed = header.formData.flags & RecordFlags::Compressed; + // Check for Oblivion plugin data + uint16_t version = 0; + if (header.old.firstChunk.string() != "HEDR") { + version = header.version; + } + if (handler.Form( + FormData(header.type, header.formData.flags, header.formData.formId, version))) { + + std::string data; + data.resize(dataSize); + stream.read(data.data(), dataSize); + if (stream.fail()) { + throw std::runtime_error("chunk incomplete"); + } + + std::istringstream chunkstream; + if (!compressed) { + chunkstream.str(std::move(data)); + } else { + if (data.size() < 4) { + throw std::runtime_error("chunk incomplete"); + } + + std::memcpy(&dataSize, data.data(), sizeof(dataSize)); + + std::string inflated; + inflated.resize(dataSize); + + int zret; + ::z_stream zstreambuf{ + .next_in = reinterpret_cast<z_const ::Bytef*>(data.data() + sizeof(dataSize)), + .avail_in = static_cast<::uInt>(data.size() - sizeof(dataSize)), + .next_out = reinterpret_cast<::Bytef*>(inflated.data()), + .avail_out = static_cast<::uInt>(inflated.size()), + .zalloc = Z_NULL, + .zfree = Z_NULL, + .opaque = Z_NULL, + }; + zret = ::inflateInit(&zstreambuf); + if (zret != Z_OK) { + throw std::runtime_error("zlib failed to init"); + } + + zret = ::inflate(&zstreambuf, Z_FINISH); + if (zret != Z_STREAM_END) { + throw std::runtime_error("zlib failed to read data"); + } + zret = ::inflateEnd(&zstreambuf); + + chunkstream.str(std::move(inflated)); + } + + while (dataSize != 0) { + const std::uint32_t fieldSize = parseChunk(chunkstream, handler); + + if (fieldSize > dataSize) { + throw std::runtime_error( + std::format("Subrecord exceeded record size ({}-{})", dataSize, fieldSize)); + } + + dataSize -= fieldSize; + } + + if constexpr (requires { handler.EndForm(); }) { + handler.EndForm(); + } + } else { + stream.seekg(dataSize, std::istream::cur); + if (stream.fail()) { + throw std::runtime_error("record incomplete"); + } + } + + return headerSize_ + header.dataSize; +} + +template <ReaderHandler Handler> +inline std::uint32_t Reader<Handler>::handleGroup(std::istream& stream, + const RecordHeader& header, + Handler& handler) +{ + std::uint32_t dataSize = header.dataSize - headerSize_; + + if (handler.Group(GroupData(header.groupData.label, header.groupData.groupType))) { + + while (dataSize != 0) { + const std::uint32_t recordSize = parseRecord(stream, handler); + + if (recordSize > dataSize) { + throw std::runtime_error( + std::format("Record exceeded group size ({}-{})", dataSize, recordSize)); + } + + dataSize -= recordSize; + } + + if constexpr (requires { handler.EndGroup(); }) { + handler.EndGroup(); + } + } else { + stream.seekg(dataSize, std::istream::cur); + if (stream.fail()) { + throw std::runtime_error("group incomplete"); + } + } + + return header.dataSize; +} + +template <ReaderHandler Handler> +inline std::uint32_t Reader<Handler>::parseChunk(std::istream& stream, Handler& handler) +{ + ChunkHeader header = readType<ChunkHeader>(stream); + if (stream.fail()) { + throw std::runtime_error("chunk header incomplete"); + } + std::uint32_t readSize = sizeof(ChunkHeader) + header.dataSize; + + std::uint32_t dataSize = header.dataSize; + if (header.type == "XXXX"_ts) { + if (dataSize != 4) { + throw std::runtime_error("XXXX field has invalid size"); + } + dataSize = readType<std::uint32_t>(stream); + header = readType<ChunkHeader>(stream); + if (stream.fail()) { + throw std::runtime_error("chunk size incomplete"); + } + readSize += sizeof(ChunkHeader) + dataSize; + } + + if (handler.Chunk(header.type)) { + std::string field; + field.resize(dataSize); + stream.read(field.data(), dataSize); + if (stream.fail()) { + throw std::runtime_error("chunk data incomplete"); + } + std::istringstream data(std::move(field)); + data.exceptions(std::ios_base::failbit); + handler.Data(data); + } else { + stream.seekg(dataSize, std::istream::cur); + if (stream.fail()) { + throw std::runtime_error("chunk incomplete"); + } + } + + return readSize; +} + +} // namespace TESFile diff --git a/libs/installer_bsplugins/src/TESFile/Stream.h b/libs/installer_bsplugins/src/TESFile/Stream.h new file mode 100644 index 0000000..db22580 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Stream.h @@ -0,0 +1,291 @@ +#ifndef TESFILE_STREAM_H +#define TESFILE_STREAM_H + +#include "Type.h" + +#include <algorithm> +#include <cctype> +#include <cstdint> +#include <cstring> +#include <istream> +#include <ranges> +#include <utility> + +namespace TESFile +{ + +template <typename T> +inline T readType(std::istream& stream) +{ + union + { + char buffer[sizeof(T)]; + T value; + }; + std::memset(buffer, 0x42, sizeof(T)); + stream.read(buffer, sizeof(T)); + return value; +} + +inline std::string readZstring(std::istream& stream) +{ + std::string value; + std::getline(stream, value, '\0'); + return value; +} + +inline int icompare(const std::string& a, const std::string& b) +{ + const std::size_t n = std::min(a.size(), b.size()); + for (std::size_t i = 0; i < n; ++i) { + const unsigned char ca = + static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(a[i]))); + const unsigned char cb = + static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(b[i]))); + if (ca != cb) + return ca < cb ? -1 : 1; + } + if (a.size() == b.size()) + return 0; + return a.size() < b.size() ? -1 : 1; +} + +inline bool iequals(const std::string& a, const std::string& b) +{ + return icompare(a, b) == 0; +} + +template <std::ranges::input_range R, typename Proj = std::identity> +inline auto find(R&& r, const std::string& value, Proj proj = {}) +{ + return std::ranges::find_if( + r, + [&](auto&& val) { + return iequals(val, value); + }, + proj); +} + +struct less +{ + bool operator()(const std::string& a, const std::string& b) const + { + return icompare(a, b) < 0; + } +}; + +enum class TESFormat +{ + Standard, + Oblivion, + Morrowind, +}; + +enum class GroupType : std::int32_t +{ + Top, + WorldChildren, + InteriorCellBlock, + InteriorCellSubBlock, + ExteriorCellBlock, + ExteriorCellSubBlock, + CellChildren, + TopicChildren, + CellPersistentChildren, + CellTemporaryChildren, + QuestChildren, + CellVisibleDistantChildren = 10, +}; + +struct RecordFlags +{ + enum TES4Flag : std::uint32_t + { + Master = 0x1, + Optimized = 0x10, + Localized = 0x80, + SmallNew = 0x100, + SmallOld = 0x200, + Update = 0x200, + Medium = 0x400, + Blueprint = 0x800, + }; + + enum Flag : std::uint32_t + { + Compressed = 0x40000, + }; +}; + +struct RecordHeader +{ + Type type; + std::uint32_t dataSize; + union + { + struct + { + std::uint32_t flags; + std::uint32_t formId; + } formData; + struct + { + std::uint32_t label; + GroupType groupType; + } groupData; + }; + union + { + struct + { + std::uint32_t revision; + Type firstChunk; + } old; + struct + { + std::uint16_t timestamp; + std::uint16_t revision; + std::uint16_t version; + std::uint16_t unknown1; + }; + }; +}; +static_assert(sizeof(RecordHeader) == 24); + +struct ChunkHeader +{ + Type type; + std::uint16_t dataSize; +}; +static_assert(sizeof(ChunkHeader) == 6); + +class GroupData final +{ +public: + constexpr GroupData() : formId_{0}, groupType_{GroupType::Top} {} + + constexpr GroupData(std::uint32_t label, GroupType type) + : formId_{label}, groupType_{type} + {} + + constexpr auto operator<=>(const GroupData& other) const + { + if (groupType_ != other.groupType_) { + return groupType_ <=> other.groupType_; + } else { + if (hasFormType()) { + return formType() <=> other.formType(); + } else if (hasBlock()) { + return block() <=> other.block(); + } else if (hasGridCell()) { + return gridCell() <=> other.gridCell(); + } else { + return formId_ <=> other.formId_; + } + } + } + + constexpr bool operator==(const GroupData& other) const + { + return groupType_ == other.groupType_ && formId_ == other.formId_; + } + + [[nodiscard]] constexpr GroupType type() const { return groupType_; } + + [[nodiscard]] constexpr bool hasFormType() const + { + return groupType_ == GroupType::Top; + } + + [[nodiscard]] constexpr bool hasParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::CellPersistentChildren || + groupType_ == GroupType::CellTemporaryChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasDirectParent() const + { + return groupType_ == GroupType::WorldChildren || + groupType_ == GroupType::CellChildren || + groupType_ == GroupType::TopicChildren || + groupType_ == GroupType::QuestChildren; + } + + [[nodiscard]] constexpr bool hasBlock() const + { + return groupType_ == GroupType::InteriorCellBlock || + groupType_ == GroupType::InteriorCellSubBlock; + } + + [[nodiscard]] constexpr bool hasGridCell() const + { + return groupType_ == GroupType::ExteriorCellBlock || + groupType_ == GroupType::ExteriorCellSubBlock; + } + + [[nodiscard]] constexpr Type formType() const { return formType_; } + + [[nodiscard]] constexpr std::uint32_t parent() const { return formId_; } + + [[nodiscard]] constexpr std::int32_t block() const { return number_; } + + [[nodiscard]] constexpr std::pair<std::int16_t, std::int16_t> gridCell() const + { + return {cell_.x, cell_.y}; + } + + constexpr void setLocalIndex(std::uint8_t index) + { + if (hasParent()) { + formId_ = (formId_ & 0xFFFFFFU) | (index << 24U); + } + } + +private: + GroupType groupType_; + union + { + Type formType_; + std::uint32_t formId_; + std::int32_t number_; + struct + { + std::int16_t y; + std::int16_t x; + } cell_; + }; +}; + +class FormData final +{ +public: + constexpr FormData(Type type, std::uint32_t flags, std::uint32_t formId, std::uint16_t formVersion) + : type_{type}, flags_{flags}, formId_{formId}, formVersion_{formVersion} + {} + + [[nodiscard]] constexpr Type type() const { return type_; } + + [[nodiscard]] constexpr std::uint32_t flags() const { return flags_; } + + [[nodiscard]] constexpr std::uint32_t formId() const { return formId_; } + + [[nodiscard]] constexpr std::uint16_t formVersion() const { return formVersion_; } + + [[nodiscard]] constexpr std::uint8_t localModIndex() const { return formId_ >> 24; } + +private: + Type type_; + std::uint32_t flags_; + std::uint32_t formId_; + std::uint16_t formVersion_; +}; + +} // namespace TESFile + +using namespace TESFile::literals; + +#endif // TESFILE_STREAM_H diff --git a/libs/installer_bsplugins/src/TESFile/Type.h b/libs/installer_bsplugins/src/TESFile/Type.h new file mode 100644 index 0000000..3b93b20 --- /dev/null +++ b/libs/installer_bsplugins/src/TESFile/Type.h @@ -0,0 +1,78 @@ +#ifndef TESFILE_TYPE_H +#define TESFILE_TYPE_H + +#include <bit> +#include <concepts> +#include <cstddef> +#include <cstdint> +#include <string> +#include <string_view> +#include <type_traits> +#include <utility> + +namespace TESFile +{ + +template <std::integral T> +constexpr std::make_unsigned_t<T> bswap(T i) +{ + return []<std::size_t... N>(std::make_unsigned_t<T> i, std::index_sequence<N...>) { + return ((((i >> (N * 8)) & 0xFF) << ((sizeof(T) - 1 - N) * 8)) | ...); + }(i, std::make_index_sequence<sizeof(T)>()); +} + +#pragma pack(push, 1) +struct Type +{ + std::uint32_t value; + + constexpr Type() = default; + + template <std::size_t N> + constexpr Type(const char (&str)[N]) + : value{[&]<std::size_t... Is>(std::index_sequence<Is...>) { + return ((static_cast<std::uint32_t>(str[Is]) << Is * 8U) | ...); + }(std::make_index_sequence<N - 1>())} + {} + + constexpr operator std::uint32_t() const { return value; } + + constexpr auto operator<=>(const Type& other) const + { + return bswap(value) <=> bswap(other.value); + } + + [[nodiscard]] std::string string() const { return std::string(view()); } + + [[nodiscard]] std::string_view view() const + { + return std::string_view(reinterpret_cast<const char*>(&value), sizeof(value)); + } + + [[nodiscard]] const char* data() const + { + return reinterpret_cast<const char*>(&value); + } + + [[nodiscard]] static constexpr std::size_t size() { return sizeof(value); } +}; +static_assert(sizeof(Type) == 4); +static_assert(alignof(Type) == 1); +static_assert(Type().value == 0); +#pragma pack(pop) + +namespace literals +{ + template <Type T> + consteval Type operator""_ts() + { + return T; + } + static_assert("TES4"_ts.value == '4SET'); + static_assert("AMMO"_ts < "BOOK"_ts); +} // namespace literals +} // namespace TESFile + +using namespace TESFile::literals; + +#endif // TESFILE_TYPE_H |
