ModOrganizer2 / modorganizer

Mod manager for various PC games. Discord Server: https://discord.gg/ewUVAqyrQX if you would like to be more involved
http://www.nexusmods.com/skyrimspecialedition/mods/6194
GNU General Public License v3.0
2.13k stars 158 forks source link

Add a check for bad BSAs on SSE #1147

Open Al12rs opened 4 years ago

Al12rs commented 4 years ago

Basically this: https://www.nexusmods.com/site/mods/126

AnyOldName3 commented 4 years ago

https://github.com/wrye-bash/wrye-bash/issues/489 has the compatibility table. The version is the second u32 in the file (with the first being the string BSA\0).

Ryan-rsm-McKenzie commented 4 years ago

Here's a simple standalone exe to implement this concept. I don't know how to make mo2 plugins, so 🤷

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <new>
#include <string>
#include <type_traits>

#include <boost/filesystem.hpp>
#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/outcome.hpp>

#define SPDLOG_COMPILED_LIB
#include <spdlog/spdlog.h>

namespace archive_checker
{
    namespace outcome = BOOST_OUTCOME_V2_NAMESPACE;

    enum class error_code : int
    {
        not_a_file,
        invalid_file_type,
        file_too_small,
        open_failure,
        unrecognized_format,
    };
}

#if 0  // boost
#include <boost/system/error_code.hpp>

namespace archive_checker
{
    namespace error = boost::system;
}

namespace boost
{
    namespace system
    {
        template <>
        struct is_error_code_enum<archive_checker::error_code> :
            public std::true_type
        {
        public:
        };
    }
}
#else  // std
#include <system_error>

namespace archive_checker
{
    namespace error = std;
}

namespace std
{
    template <>
    struct is_error_code_enum<archive_checker::error_code> :
        public std::true_type
    {
    public:
    };
}
#endif

namespace archive_checker
{
    class error_category :
        public error::error_category
    {
    private:
        using super = error::error_category;

    public:
        const char* name() const noexcept final { return "archive_checker::error_code"; }

        error::error_condition default_error_condition(int a_code) const noexcept final
        {
            switch (static_cast<error_code>(a_code)) {
            case error_code::not_a_file:
            case error_code::invalid_file_type:
                return make_error_condition(error::errc::invalid_argument);
            case error_code::file_too_small:
            case error_code::open_failure:
            case error_code::unrecognized_format:
                return make_error_condition(error::errc::io_error);
            default:
                return error::error_condition(a_code, *this);
            }
        }

        std::string message(int a_condition) const final
        {
            switch (static_cast<error_code>(a_condition)) {
            case error_code::not_a_file:
                return "file path does not indicate a regular file";
            case error_code::invalid_file_type:
                return "file path does not contain a known extension";
            case error_code::file_too_small:
                return "the file is too small to contain the necessary header";
            case error_code::open_failure:
                return "failed to map the file into memory";
            case error_code::unrecognized_format:
                return "the file contains an unrecognized format";
            default:
                return "unknown error code";
            }
        }

        using super::message;
    };

    inline error::error_code make_error_code(error_code a_error)
    {
        static const error_category e;
        return { static_cast<int>(a_error), e };
    }

    enum class archive_type
    {
        tes3,
        tes4,
        sse,
        fo4,
    };

    namespace tes3
    {
        struct header_t
        {
            std::uint32_t version;
        };
    }

    namespace tes4
    {
        constexpr inline std::array MAGIC{ 'B', 'S', 'A', '\0' };

        struct header_t
        {
            char magic[4];
            std::uint32_t version;
        };
    }

    namespace fo4
    {
        constexpr inline std::array MAGIC{ 'B', 'T', 'D', 'X' };

        struct header_t
        {
            char magic[4];
            std::uint32_t version;
        };
    }

    union header_t
    {
        tes3::header_t tes3;
        tes4::header_t tes4;
        fo4::header_t fo4;
    };

    namespace util
    {
        template <class T>
        [[nodiscard]] T start_lifetime_as(const std::byte* a_src) noexcept(std::is_nothrow_copy_constructible_v<T>)
        {
            std::array<std::byte, sizeof(T)> buf;
            return *std::launder(
                static_cast<T*>(
                    std::memcpy(
                        buf.data(), a_src, buf.size())));
        }

        template <std::size_t N>
        [[nodiscard]] bool equal(const char (&a_lhs)[N], const std::array<char, N>& a_rhs)
        {
            return std::equal(a_lhs, a_lhs + N, a_rhs.begin(), a_rhs.end());
        }
    }

    [[nodiscard]] inline outcome::result<archive_type, error::error_code> get_archive_type(const boost::filesystem::path& a_path)
    {
        if (!boost::filesystem::is_regular_file(a_path)) {
            return error_code::not_a_file;
        }

        if (const auto ext = a_path.extension();
            ext != ".bsa" && ext != ".ba2") {
            return error_code::invalid_file_type;
        }

        if (const auto size = boost::filesystem::file_size(a_path);
            size < sizeof(header_t)) {
            return error_code::file_too_small;
        }

        const boost::iostreams::mapped_file_source file(a_path, sizeof(header_t));
        if (!file.is_open()) {
            return error_code::open_failure;
        }

        const auto header =
            util::start_lifetime_as<header_t>(
                reinterpret_cast<const std::byte*>(file.data()));

        if (util::equal(header.fo4.magic, fo4::MAGIC)) {
            switch (header.fo4.version) {
            case 1:
                return archive_type::fo4;
            default:
                return error_code::unrecognized_format;
            }
        } else if (util::equal(header.tes4.magic, tes4::MAGIC)) {
            switch (header.tes4.version) {
            case 103:
            case 104:
                return archive_type::tes4;
            case 105:
                return archive_type::sse;
            default:
                return error_code::unrecognized_format;
            }
        } else {
            switch (header.tes3.version) {
            case 256:
                return archive_type::tes3;
            default:
                return error_code::unrecognized_format;
            }
        }
    }
}

int main(int a_argc, char* a_argv[])
{
    using namespace archive_checker;

    spdlog::set_pattern("[%^%l%$] %v");

    if (a_argc <= 1) {
        spdlog::info("please pass file paths as arguments");
        return EXIT_FAILURE;
    }

    boost::filesystem::path path;
    for (int i = 1; i < a_argc; ++i) {
        path = a_argv[i];
        const auto result = get_archive_type(path);
        if (result) {
            switch (result.value()) {
            case archive_type::tes3:
                spdlog::info("tes3");
                break;
            case archive_type::tes4:
                spdlog::info("tes4");
                break;
            case archive_type::sse:
                spdlog::info("sse");
                break;
            case archive_type::fo4:
                spdlog::info("fo4");
                break;
            default:
                spdlog::error("unhandled type");
                break;
            }
        } else {
            spdlog::error(result.error().message());
        }
    }

#ifdef NDEBUG
    std::cin.get();
#endif

    return EXIT_SUCCESS;
}