From a64758aa552c663f97cc336033dbf06069df225a Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 10 May 2026 13:59:49 -0700 Subject: [PATCH 1/2] sourcepp: add crypto::computeSHA1 --- include/sourcepp/crypto/SHA1.h | 13 +++++++++++++ src/sourcepp/String.cpp | 12 ++++++------ src/sourcepp/crypto/SHA1.cpp | 21 +++++++++++++++++++++ src/sourcepp/crypto/_crypto.cmake | 2 ++ 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 include/sourcepp/crypto/SHA1.h create mode 100644 src/sourcepp/crypto/SHA1.cpp diff --git a/include/sourcepp/crypto/SHA1.h b/include/sourcepp/crypto/SHA1.h new file mode 100644 index 00000000..70b11ee3 --- /dev/null +++ b/include/sourcepp/crypto/SHA1.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +#include + +namespace sourcepp::crypto { + +std::array computeSHA1(std::span buffer); + +} // namespace sourcepp::crypto diff --git a/src/sourcepp/String.cpp b/src/sourcepp/String.cpp index 884c6ff1..34cd56bf 100644 --- a/src/sourcepp/String.cpp +++ b/src/sourcepp/String.cpp @@ -20,7 +20,7 @@ std::mt19937& getRandomGenerator() { using namespace sourcepp; bool string::contains(std::string_view s, char c) { - return std::find(s.begin(), s.end(), c) != s.end(); + return std::ranges::find(s, c) != s.end(); } bool string::matches(std::string_view in, std::string_view search) { @@ -67,7 +67,7 @@ bool string::iequals(std::string_view s1, std::string_view s2) { // https://stackoverflow.com/a/217605 void string::ltrim(std::string& s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](char c) { return !std::isspace(c); })); + s.erase(s.begin(), std::ranges::find_if(s, [](char c) { return !std::isspace(c); })); } std::string_view string::ltrim(std::string_view s) { @@ -108,7 +108,7 @@ std::string string::trimInternal(std::string_view s) { } void string::ltrim(std::string& s, std::string_view chars) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [chars](char c) { + s.erase(s.begin(), std::ranges::find_if(s, [chars](char c) { return !contains(chars, c); })); } @@ -165,7 +165,7 @@ std::vector string::split(std::string_view s, char delim) { } void string::toLower(std::string& input) { - std::transform(input.begin(), input.end(), input.begin(), [](unsigned char c){ return std::tolower(c); }); + std::ranges::transform(input, input.begin(), [](unsigned char c){ return std::tolower(c); }); } std::string string::toLower(std::string_view input) { @@ -175,7 +175,7 @@ std::string string::toLower(std::string_view input) { } void string::toUpper(std::string& input) { - std::transform(input.begin(), input.end(), input.begin(), [](unsigned char c){ return std::toupper(c); }); + std::ranges::transform(input, input.begin(), [](unsigned char c){ return std::toupper(c); }); } std::string string::toUpper(std::string_view input) { @@ -235,7 +235,7 @@ void string::normalizeSlashes(std::string& path, bool stripSlashPrefix, bool str } void string::denormalizeSlashes(std::string& path, bool stripSlashPrefix, bool stripSlashSuffix) { - std::replace(path.begin(), path.end(), '/', '\\'); + std::ranges::replace(path, '/', '\\'); if (stripSlashPrefix && path.starts_with('\\')) { path = path.substr(1); } diff --git a/src/sourcepp/crypto/SHA1.cpp b/src/sourcepp/crypto/SHA1.cpp new file mode 100644 index 00000000..ca013274 --- /dev/null +++ b/src/sourcepp/crypto/SHA1.cpp @@ -0,0 +1,21 @@ +#include + +#include + +#include + +using namespace sourcepp; + +std::array crypto::computeSHA1(std::span buffer) { + if (!LTM_MATH || buffer.empty()) { + return {}; + } + + hash_state sha1; + sha1_init(&sha1); + sha1_process(&sha1, reinterpret_cast(buffer.data()), buffer.size()); + + std::array final{}; + sha1_done(&sha1, reinterpret_cast(final.data())); + return final; +} diff --git a/src/sourcepp/crypto/_crypto.cmake b/src/sourcepp/crypto/_crypto.cmake index 1b6714ca..e7c8988d 100644 --- a/src/sourcepp/crypto/_crypto.cmake +++ b/src/sourcepp/crypto/_crypto.cmake @@ -5,6 +5,7 @@ list(APPEND ${PROJECT_NAME}_crypto_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/crypto/Globals.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/crypto/MD5.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/crypto/RSA.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/crypto/SHA1.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/crypto/SHA256.h") add_library(${PROJECT_NAME}_crypto STATIC @@ -15,6 +16,7 @@ add_library(${PROJECT_NAME}_crypto STATIC "${CMAKE_CURRENT_LIST_DIR}/Globals.cpp" "${CMAKE_CURRENT_LIST_DIR}/MD5.cpp" "${CMAKE_CURRENT_LIST_DIR}/RSA.cpp" + "${CMAKE_CURRENT_LIST_DIR}/SHA1.cpp" "${CMAKE_CURRENT_LIST_DIR}/SHA256.cpp") target_precompile_headers(${PROJECT_NAME}_crypto PUBLIC ${${PROJECT_NAME}_crypto_HEADERS}) From 429c0b26b824a89a955afeec1d860c66e05a9b2f Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 10 May 2026 14:00:12 -0700 Subject: [PATCH 2/2] vpkpp: add support for TAB pack files from Just Cause 1 --- README.md | 10 +- docs/index.html | 7 +- include/vpkpp/format/TAB.h | 45 +++++++ include/vpkpp/vpkpp.h | 1 + lang/c/include/vpkppc/format/TAB.h | 21 ++++ lang/c/include/vpkppc/vpkpp.h | 1 + lang/c/src/vpkppc/_vpkppc.cmake | 2 + lang/c/src/vpkppc/format/TAB.cpp | 30 +++++ lang/csharp/src/vpkpp/DLL.cs | 10 +- lang/csharp/src/vpkpp/Format/TAB.cs | 28 +++++ lang/python/src/vpkpp.h | 10 ++ src/vpkpp/_vpkpp.cmake | 2 + src/vpkpp/format/TAB.cpp | 177 ++++++++++++++++++++++++++++ 13 files changed, 339 insertions(+), 5 deletions(-) create mode 100644 include/vpkpp/format/TAB.h create mode 100644 lang/c/include/vpkppc/format/TAB.h create mode 100644 lang/c/src/vpkppc/format/TAB.cpp create mode 100644 lang/csharp/src/vpkpp/Format/TAB.cs create mode 100644 src/vpkpp/format/TAB.cpp diff --git a/README.md b/README.md index cb0d4fdb..a69b9866 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,11 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj - vpkpp + vpkpp 007 v1.1, v1.3 (007 - Nightfire) ✅ ❌ - C
C#
Python + C
C#
Python @@ -259,6 +259,12 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj ✅ + + TAB v3 (Avalanche Engine) + ✅ + ❌ + + VPK pre-v1, v1-2, v54 diff --git a/docs/index.html b/docs/index.html index e9d6d5f7..23b0f8f3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -615,7 +615,7 @@

Supported Formats

✅ - + vpkpp
  • C
  • @@ -683,6 +683,11 @@

    Supported Formats

    ✅ ✅ + + TAB v3 (Avalanche Engine) + ✅ + ❌ + VPK pre-v1, v1-2, v54 diff --git a/include/vpkpp/format/TAB.h b/include/vpkpp/format/TAB.h new file mode 100644 index 00000000..926e22b3 --- /dev/null +++ b/include/vpkpp/format/TAB.h @@ -0,0 +1,45 @@ +// ReSharper disable CppRedundantQualifier + +#pragma once + +#include + +#include "../PackFile.h" + +namespace vpkpp { + +constexpr std::string_view TAB_EXTENSION = ".tab"; + +constexpr auto TAB_FILENAME_MAX_SIZE = 128; +constexpr std::string_view TAB_HASHED_FILEPATH_PREFIX = "__hashed__/"; + +constexpr std::string_view ARC_EXTENSION = ".arc"; + +/// Chunk size in bytes (1gb) +constexpr uint32_t ARC_CHUNK_SIZE = 1024 * 1024 * 1024; + +class TAB : public PackFileReadOnly { +public: + /// Open a TAB file + [[nodiscard]] static std::unique_ptr open(const std::string& path, const EntryCallback& callback = nullptr); + + [[nodiscard]] std::optional> readEntry(const std::string& path_) const override; + + [[nodiscard]] Attribute getSupportedEntryAttributes() const override; + + [[nodiscard]] explicit operator std::string() const override; + + [[nodiscard]] static uint32_t hashFilePath(const std::string& filepath); + +protected: + using PackFileReadOnly::PackFileReadOnly; + + uint32_t version = 0; + uint32_t sectorSize = 0; + uint32_t numArchives = 0; + +private: + VPKPP_REGISTER_PACKFILE_OPEN(TAB_EXTENSION, &TAB::open); +}; + +} // namespace vpkpp diff --git a/include/vpkpp/vpkpp.h b/include/vpkpp/vpkpp.h index 7fe67272..39d5c005 100644 --- a/include/vpkpp/vpkpp.h +++ b/include/vpkpp/vpkpp.h @@ -16,6 +16,7 @@ #include "format/ORE.h" #include "format/PAK.h" #include "format/PCK.h" +#include "format/TAB.h" #include "format/VPK.h" #include "format/VPK_VTMB.h" #include "format/VPP.h" diff --git a/lang/c/include/vpkppc/format/TAB.h b/lang/c/include/vpkppc/format/TAB.h new file mode 100644 index 00000000..401c9be1 --- /dev/null +++ b/lang/c/include/vpkppc/format/TAB.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../PackFile.h" + +VPKPP_EXTERNVAR const char* VPKPP_TAB_EXTENSION; + +VPKPP_EXTERNVAR const int VPKPP_TAB_FILENAME_MAX_SIZE; +VPKPP_EXTERNVAR const char* VPKPP_TAB_HASHED_FILEPATH_PREFIX; + +VPKPP_EXTERNVAR const char* VPKPP_ARC_EXTENSION; +VPKPP_EXTERNVAR const uint32_t VPKPP_ARC_CHUNK_SIZE; + +VPKPP_API vpkpp_pack_file_handle_t vpkpp_tab_open(const char* path, vpkpp_entry_callback_t callback); // REQUIRES MANUAL FREE: vpkpp_close +VPKPP_API uint32_t vpkpp_tab_hash_file_path(const char* filepath); + +// C++ conversion routines +#ifdef __cplusplus + +#include + +#endif diff --git a/lang/c/include/vpkppc/vpkpp.h b/lang/c/include/vpkppc/vpkpp.h index 80c2efdd..25854e8c 100644 --- a/lang/c/include/vpkppc/vpkpp.h +++ b/lang/c/include/vpkppc/vpkpp.h @@ -16,6 +16,7 @@ #include "format/ORE.h" #include "format/PAK.h" #include "format/PCK.h" +#include "format/TAB.h" #include "format/VPK.h" #include "format/VPK_VTMB.h" #include "format/WAD3.h" diff --git a/lang/c/src/vpkppc/_vpkppc.cmake b/lang/c/src/vpkppc/_vpkppc.cmake index a3a54d5e..76b2edf4 100644 --- a/lang/c/src/vpkppc/_vpkppc.cmake +++ b/lang/c/src/vpkppc/_vpkppc.cmake @@ -11,6 +11,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/ORE.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PCK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/TAB.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPK.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPK_VTMB.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPP.h" @@ -34,6 +35,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_LIST_DIR}/format/ORE.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/TAB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK_VTMB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPP.cpp" diff --git a/lang/c/src/vpkppc/format/TAB.cpp b/lang/c/src/vpkppc/format/TAB.cpp new file mode 100644 index 00000000..ebb5c1d7 --- /dev/null +++ b/lang/c/src/vpkppc/format/TAB.cpp @@ -0,0 +1,30 @@ +#include + +#include + +using namespace sourceppc; +using namespace vpkpp; + +const char* VPKPP_TAB_EXTENSION = TAB_EXTENSION.data(); + +const int VPKPP_TAB_FILENAME_MAX_SIZE = TAB_FILENAME_MAX_SIZE; +const char* VPKPP_TAB_HASHED_FILEPATH_PREFIX = TAB_HASHED_FILEPATH_PREFIX.data(); + +const char* VPKPP_ARC_EXTENSION = ARC_EXTENSION.data(); +const uint32_t VPKPP_ARC_CHUNK_SIZE = ARC_CHUNK_SIZE; + +VPKPP_API vpkpp_pack_file_handle_t vpkpp_tab_open(const char* path, vpkpp_entry_callback_t callback) { + SOURCEPP_EARLY_RETURN_VAL(path, nullptr); + + auto packFile = TAB::open(path, callback ? [callback](const std::string& entryPath, const Entry& entry) { + callback(entryPath.c_str(), const_cast(&entry)); + } : static_cast(nullptr)); + if (!packFile) { + return nullptr; + } + return packFile.release(); +} + +VPKPP_API uint32_t vpkpp_tab_hash_file_path(const char* filepath) { + return TAB::hashFilePath(filepath); +} diff --git a/lang/csharp/src/vpkpp/DLL.cs b/lang/csharp/src/vpkpp/DLL.cs index 9ad6159b..f09f66c7 100644 --- a/lang/csharp/src/vpkpp/DLL.cs +++ b/lang/csharp/src/vpkpp/DLL.cs @@ -94,10 +94,16 @@ internal static partial class DLL [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] public static partial nint vpkpp_pck_open(string path, EntryCallbackNative? callback); - + + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] + public static partial nint vpkpp_tab_open(string path, EntryCallbackNative? callback); + + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] + public static partial uint vpkpp_tab_hash_file_path(string path); + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] public static partial nint vpkpp_vpk_create(string path); - + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] public static partial nint vpkpp_vpk_create_with_options(string path, uint version); diff --git a/lang/csharp/src/vpkpp/Format/TAB.cs b/lang/csharp/src/vpkpp/Format/TAB.cs new file mode 100644 index 00000000..9d7fd3ea --- /dev/null +++ b/lang/csharp/src/vpkpp/Format/TAB.cs @@ -0,0 +1,28 @@ +using System; + +namespace sourcepp.vpkpp.Format; + +using OpenPropertyRequest = Func; + +using EntryCallback = Action; + +public class TAB : PackFile +{ + protected TAB(nint handle, bool managed = true) : base(handle, managed) + { + } + + public new static TAB? Open(string path, EntryCallback? callback = null, OpenPropertyRequest? _ = null) + { + var handle = DLL.vpkpp_tab_open(path, callback is not null ? (entryPath, entry) => + { + callback(entryPath, new Entry(entry, false)); + } : null); + return handle == nint.Zero ? null : new TAB(handle); + } + + public static uint HashFilepath(string path) + { + return DLL.vpkpp_tab_hash_file_path(path); + } +} diff --git a/lang/python/src/vpkpp.h b/lang/python/src/vpkpp.h index 79c3a4f0..15eb3f92 100644 --- a/lang/python/src/vpkpp.h +++ b/lang/python/src/vpkpp.h @@ -255,6 +255,16 @@ inline void register_python(py::module_& m) { .def("get_godot_version", &PCK::getGodotVersion) .def("set_godot_version", &PCK::setGodotVersion, "major"_a = 0, "minor"_a = 0, "patch"_a = 0); + vpkpp.attr("TAB_EXTENSION") = TAB_EXTENSION; + vpkpp.attr("TAB_FILENAME_MAX_SIZE") = TAB_FILENAME_MAX_SIZE; + vpkpp.attr("TAB_HASHED_FILEPATH_PREFIX") = TAB_HASHED_FILEPATH_PREFIX; + vpkpp.attr("ARC_EXTENSION") = ARC_EXTENSION; + vpkpp.attr("ARC_CHUNK_SIZE") = ARC_CHUNK_SIZE; + + py::class_(vpkpp, "TAB") + .def_static("open", &TAB::open, "path"_a, "callback"_a = nullptr) + .def_static("hash_filepath", &FGP::hashFilePath); + vpkpp.attr("VPK_SIGNATURE") = VPK_SIGNATURE; vpkpp.attr("VPK_DIR_INDEX") = VPK_DIR_INDEX; vpkpp.attr("VPK_ENTRY_TERM") = VPK_ENTRY_TERM; diff --git a/src/vpkpp/_vpkpp.cmake b/src/vpkpp/_vpkpp.cmake index 60d305af..ac059724 100644 --- a/src/vpkpp/_vpkpp.cmake +++ b/src/vpkpp/_vpkpp.cmake @@ -13,6 +13,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/ORE.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PCK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/TAB.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPK.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPK_VTMB.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPP.h" @@ -36,6 +37,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_LIST_DIR}/format/ORE.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/TAB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK_VTMB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPP.cpp" diff --git a/src/vpkpp/format/TAB.cpp b/src/vpkpp/format/TAB.cpp new file mode 100644 index 00000000..05376315 --- /dev/null +++ b/src/vpkpp/format/TAB.cpp @@ -0,0 +1,177 @@ +// ReSharper disable CppParameterMayBeConst +// ReSharper disable CppRedundantQualifier + +#include + +#include +#include +#include + +#include +#include +#include + +using namespace sourcepp; +using namespace vpkpp; + +namespace { + +constexpr std::string_view TAB_FILEPATH_LIST_STRIP_PATH_INDEX = "projects/justcause/data/"; + +[[nodiscard]] std::filesystem::path getArchivePath(const TAB& tab, uint32_t archiveIndex) { + return std::filesystem::path{tab.getFilepath()}.parent_path() / std::format("{}{}{}", tab.getFilestem(), archiveIndex, ARC_EXTENSION); +} + +} // namespace + +std::unique_ptr TAB::open(const std::string& path, const EntryCallback& callback) { + if (!std::filesystem::exists(path)) { + // File does not exist + return nullptr; + } + + auto* tab = new TAB{path}; + auto packFile = std::unique_ptr(tab); + + FileStream reader{tab->fullFilePath}; + reader.seek_in(0); + + reader >> tab->version; + if (tab->version != 3) { + BufferStream::swap_endian(&tab->version); + if (tab->version == 3) { + reader.set_big_endian(true); + } else { + return nullptr; + } + } + + reader >> tab->sectorSize >> tab->numArchives; + + std::vector> archives; + uint32_t alignment = 0; + for (uint32_t i = 0; i < tab->numArchives; i++) { + auto& [archivePath, archiveSize, archiveAlignment] = archives.emplace_back(); + archivePath = ::getArchivePath(*tab, i), + archiveSize = static_cast(std::filesystem::file_size(archivePath)); + alignment += (archiveSize + tab->sectorSize - 1) / tab->sectorSize; + archiveAlignment = alignment; + } + + // Here we load in the filepath list if it exists + std::unordered_map crackedHashes; + if (const std::filesystem::path mapPath{std::filesystem::path{tab->fullFilePath}.parent_path() / std::format("{}list.txt", tab->getFilestem())}; std::filesystem::exists(mapPath)) { + std::ifstream mapFile{mapPath}; + std::string filepath; + while (std::getline(mapFile, filepath)) { + if (filepath.empty() || filepath.starts_with(':')) { + continue; + } + string::trim(filepath); + string::normalizeSlashes(filepath); + string::toLower(filepath); + if (const auto index = filepath.rfind(TAB_FILEPATH_LIST_STRIP_PATH_INDEX); index != std::string::npos) { + filepath = filepath.substr(index + TAB_FILEPATH_LIST_STRIP_PATH_INDEX.size()); + } + crackedHashes[TAB::hashFilePath(filepath)] = filepath; + } + } + + const auto fileCount = (std::filesystem::file_size(tab->fullFilePath) - sizeof(uint32_t) * 3) / (sizeof(uint32_t) * 3); + for (uint32_t i = 0; i < fileCount; i++) { + Entry entry = createNewEntry(); + + std::string entryPath; + + // note: NOT a CRC32! check TAB::hashFilePath + entry.crc32 = reader.read(); + if (crackedHashes.contains(entry.crc32)) { + entryPath = tab->cleanEntryPath(crackedHashes[entry.crc32]); + } else { + entryPath = tab->cleanEntryPath(TAB_HASHED_FILEPATH_PREFIX.data() + string::encodeHex({reinterpret_cast(&entry.crc32), sizeof(entry.crc32)})); + } + + entry.offset = reader.read(); + entry.length = reader.read(); + entry.archiveIndex = 0; + + for (int j = 0; j < archives.size(); j++) { + if (entry.offset < std::get<2>(archives[j])) { + entry.archiveIndex = j; + break; + } + } + if (entry.archiveIndex == 0) { + entry.offset = (entry.offset * tab->sectorSize) % ARC_CHUNK_SIZE; + } else { + entry.offset = (entry.offset - std::get<2>(archives[entry.archiveIndex - 1])) * tab->sectorSize % ARC_CHUNK_SIZE; + } + + tab->entries.emplace(entryPath, entry); + + if (callback) { + callback(entryPath, entry); + } + } + + return packFile; +} + +std::optional> TAB::readEntry(const std::string& path_) const { + const auto path = this->cleanEntryPath(path_); + const auto entry = this->findEntry(path); + if (!entry) { + return std::nullopt; + } + if (entry->unbaked) { + return readUnbakedEntry(*entry); + } + + // It's baked into the file on disk + FileStream stream{::getArchivePath(*this, entry->archiveIndex)}; + if (!stream) { + return std::nullopt; + } + stream.seek_in_u(entry->offset); + return stream.read_bytes(entry->length); +} + +Attribute TAB::getSupportedEntryAttributes() const { + using enum Attribute; + return ARCHIVE_INDEX | LENGTH; +} + +TAB::operator std::string() const { + return PackFileReadOnly::operator std::string() + std::format(" | Version v{}", this->version); +} + +uint32_t TAB::hashFilePath(const std::string& filepath) { + auto cleanPath = filepath; + string::normalizeSlashes(cleanPath); + string::toLower(cleanPath); + cleanPath = string::trim(std::filesystem::path{cleanPath}.filename().string()); + + std::vector buffer; + buffer.resize(TAB_FILENAME_MAX_SIZE); + std::memset(buffer.data(), 0, TAB_FILENAME_MAX_SIZE); + std::memcpy(buffer.data(), cleanPath.c_str(), cleanPath.size()); + + buffer.push_back(std::byte{0x80}); + if (buffer.size() % 64 < 56) { + buffer.resize(buffer.size() + (56 - buffer.size() % 64)); + } else if (buffer.size() % 64 > 56) { + buffer.resize(buffer.size() + (64 - buffer.size() % 64) + 56); + } + for (int i = 7; i >= 0; i--) { + buffer.push_back(static_cast(static_cast(TAB_FILENAME_MAX_SIZE) * 8 >> i * 8 & 0xff)); + } + + for (std::span bufferU32{reinterpret_cast(buffer.data()), buffer.size() / sizeof(uint32_t)}; auto& uint : bufferU32) { + BufferStream::swap_endian(&uint); + } + hash_state sha1; + sha1_init(&sha1); + sha1_process(&sha1, reinterpret_cast(buffer.data()), buffer.size()); + BufferStream::swap_endian(&sha1.sha1.state[0]); + return sha1.sha1.state[0]; +}