diff --git a/TaikoSwitchDataTableDecryptor.sln b/TaikoSwitchDataTableDecryptor.sln
new file mode 100644
index 0000000..2f4ad3b
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor.sln
@@ -0,0 +1,36 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.28307.1525
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TaikoSwitchDataTableDecryptor", "TaikoSwitchDataTableDecryptor\TaikoSwitchDataTableDecryptor.vcxproj", "{2A0F9E61-C7B2-497B-B587-C580E0C17110}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zlib", "Dependencies\zlib\zlib.vcxproj", "{164A9D43-345C-407B-BF59-527386DED788}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{EE11261E-85ED-4445-AB6F-5C4F55B4E34B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Debug|x64.ActiveCfg = Debug|x64
+ {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Debug|x64.Build.0 = Debug|x64
+ {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Release|x64.ActiveCfg = Release|x64
+ {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Release|x64.Build.0 = Release|x64
+ {164A9D43-345C-407B-BF59-527386DED788}.Debug|x64.ActiveCfg = Debug|x64
+ {164A9D43-345C-407B-BF59-527386DED788}.Debug|x64.Build.0 = Debug|x64
+ {164A9D43-345C-407B-BF59-527386DED788}.Release|x64.ActiveCfg = Release|x64
+ {164A9D43-345C-407B-BF59-527386DED788}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {164A9D43-345C-407B-BF59-527386DED788} = {EE11261E-85ED-4445-AB6F-5C4F55B4E34B}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {32848A13-A06E-49EA-B024-22472419BF2F}
+ EndGlobalSection
+EndGlobal
diff --git a/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj
new file mode 100644
index 0000000..054a5cd
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj
@@ -0,0 +1,102 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 15.0
+ {2A0F9E61-C7B2-497B-B587-C580E0C17110}
+ TaikoSwitchDataTableDecryptor
+ 10.0.17763.0
+
+
+
+ Application
+ true
+ v141
+ MultiByte
+
+
+ Application
+ false
+ v141
+ true
+ MultiByte
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(ProjectDir)bin\$(Platform)-$(Configuration)\
+ $(ProjectDir)bin-int\$(Platform)-$(Configuration)\
+
+
+ $(ProjectDir)bin\$(Platform)-$(Configuration)\
+ $(ProjectDir)bin-int\$(Platform)-$(Configuration)\
+
+
+
+ Level3
+ Disabled
+ true
+ true
+ stdcpp17
+ false
+ MultiThreadedDebug
+ $(ProjectDir)src;$(SolutionDir)Dependencies\zlib\include
+
+
+ Console
+ zlib.lib;Bcrypt.lib;%(AdditionalDependencies)
+ $(SolutionDir)Dependencies\zlib\bin\$(Platform)-$(Configuration)
+
+
+
+
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ true
+ false
+ MultiThreaded
+ $(ProjectDir)src;$(SolutionDir)Dependencies\zlib\include
+ stdcpp17
+
+
+ Console
+ true
+ true
+ zlib.lib;Bcrypt.lib;%(AdditionalDependencies)
+ $(SolutionDir)Dependencies\zlib\bin\$(Platform)-$(Configuration)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters
new file mode 100644
index 0000000..1a1bb48
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters
@@ -0,0 +1,33 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp b/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp
new file mode 100644
index 0000000..9db51a3
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp
@@ -0,0 +1,107 @@
+#include "Types.h"
+#include "Utilities.h"
+
+namespace PeepoHappy
+{
+ bool HasValidGZipHeader(const u8* fileContent, size_t fileSize)
+ {
+ if (fileSize <= Crypto::AesKeySize)
+ return false;
+
+ constexpr std::array gzibMagic = { 0x1F, 0x8B };
+
+ const bool validMagic = (memcmp(fileContent, gzibMagic.data(), gzibMagic.size()) == 0);
+ const bool validCompressionMethod = (fileContent[2] == /*Z_DEFLATED*/ 0x08);
+
+ return (validMagic && validCompressionMethod);
+ }
+
+ bool DecompressAndWriteDataTableJsonFile(const u8* compressedData, size_t compressedDataSize, std::string_view jsonOutputFilePath)
+ {
+ auto decompressedBuffer = std::make_unique(IO::MaxDecompressedGameDataTableFileSize);
+ if (Compression::Inflate(compressedData, compressedDataSize, decompressedBuffer.get(), IO::MaxDecompressedGameDataTableFileSize))
+ {
+ const size_t jsonLength = strnlen(reinterpret_cast(decompressedBuffer.get()), IO::MaxDecompressedGameDataTableFileSize);
+ const auto jsonString = std::string_view(reinterpret_cast(decompressedBuffer.get()), jsonLength);
+
+ if (IO::WriteEntireFile(jsonOutputFilePath, reinterpret_cast(jsonString.data()), jsonString.size()))
+ return true;
+ }
+
+ return false;
+ }
+
+ int ReadAndWriteEncryptedAndOrCompressedBinToJsonFile(std::string_view encryptedAndOrCompressedInputFilePath, std::string_view jsonOutputFilePath)
+ {
+ const auto[fileContent, fileSize] = IO::ReadEntireFile(encryptedAndOrCompressedInputFilePath);
+ if (fileSize <= 8)
+ {
+ fprintf(stderr, "Bad file? :WidePeepoSad:");
+ return EXIT_WIDEPEEPOSAD;
+ }
+
+ if (HasValidGZipHeader(fileContent.get(), fileSize))
+ {
+ if (!DecompressAndWriteDataTableJsonFile(fileContent.get(), fileSize, jsonOutputFilePath))
+ return EXIT_WIDEPEEPOSAD;
+ }
+ else
+ {
+ std::array iv = {};
+ memcpy(iv.data(), fileContent.get(), iv.size());
+
+ const size_t fileSizeWithoutIV = (fileSize - iv.size());
+ const u8* fileContentWithoutIV = (fileContent.get() + iv.size());
+
+ auto decryptedBuffer = std::make_unique(fileSizeWithoutIV);
+ Crypto::DecryptAes128Cbc(fileContentWithoutIV, decryptedBuffer.get(), fileSizeWithoutIV, Crypto::DataTableAesKey, iv);
+
+ if (!DecompressAndWriteDataTableJsonFile(decryptedBuffer.get(), fileSizeWithoutIV, jsonOutputFilePath))
+ return EXIT_WIDEPEEPOSAD;
+ }
+
+ return EXIT_WIDEPEEPOHAPPY;
+ }
+
+ int ReadAndWriteJsonFileToCompressedBin(std::string_view jsonInputFilePath, std::string_view compressedOutputFilePath)
+ {
+ auto compressedBuffer = std::make_unique(IO::MaxDecompressedGameDataTableFileSize);
+
+ const auto[fileContent, fileSize] = IO::ReadEntireFile(jsonInputFilePath);
+ const auto compressedSize = Compression::Deflate(fileContent.get(), fileSize, compressedBuffer.get(), IO::MaxDecompressedGameDataTableFileSize);
+
+ if (compressedSize < 0)
+ return EXIT_WIDEPEEPOSAD;
+
+ if (!IO::WriteEntireFile(compressedOutputFilePath, compressedBuffer.get(), compressedSize))
+ return EXIT_WIDEPEEPOSAD;
+
+ return EXIT_WIDEPEEPOHAPPY;
+ }
+
+ int EntryPoint()
+ {
+ const auto[argc, argv] = UTF8::GetCommandLineArguments();
+
+ if (argc <= 1)
+ {
+ fprintf(stderr, "Insufficient arguments :WidePeepoSad:\n");
+ return EXIT_WIDEPEEPOSAD;
+ }
+
+ const auto inputPath = std::string_view(argv[1]);
+ if (IO::HasFileExtension(inputPath, ".bin"))
+ return ReadAndWriteEncryptedAndOrCompressedBinToJsonFile(inputPath, IO::ChangeFileExtension(inputPath, ".json"));
+
+ if (IO::HasFileExtension(inputPath, ".json"))
+ return ReadAndWriteJsonFileToCompressedBin(inputPath, IO::ChangeFileExtension(inputPath, ".bin"));
+
+ fprintf(stderr, "Unknown file extension\n");
+ return EXIT_WIDEPEEPOSAD;
+ }
+}
+
+int main()
+{
+ return PeepoHappy::EntryPoint();
+}
diff --git a/TaikoSwitchDataTableDecryptor/src/Types.h b/TaikoSwitchDataTableDecryptor/src/Types.h
new file mode 100644
index 0000000..cb87db8
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/src/Types.h
@@ -0,0 +1,29 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using i8 = int8_t;
+using u8 = uint8_t;
+using i16 = int16_t;
+using u16 = uint16_t;
+using i32 = int32_t;
+using u32 = uint32_t;
+using i64 = int64_t;
+using u64 = uint64_t;
+using f32 = float;
+using f64 = double;
+
+struct NonCopyable
+{
+ NonCopyable() = default;
+ ~NonCopyable() = default;
+
+ NonCopyable(const NonCopyable&) = delete;
+ NonCopyable& operator=(const NonCopyable&) = delete;
+};
diff --git a/TaikoSwitchDataTableDecryptor/src/Utilities.cpp b/TaikoSwitchDataTableDecryptor/src/Utilities.cpp
new file mode 100644
index 0000000..af62162
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/src/Utilities.cpp
@@ -0,0 +1,317 @@
+#include "Utilities.h"
+#include
+
+#define NOMINMAX
+#include
+#include
+
+#ifndef NT_SUCCESS
+#define NT_SUCCESS(Status) (((::NTSTATUS)(Status)) >= 0)
+#endif // ! NT_SUCCESS
+
+namespace PeepoHappy
+{
+ namespace UTF8
+ {
+ std::string Narrow(std::wstring_view inputString)
+ {
+ std::string utf8String;
+ const int utf8Length = ::WideCharToMultiByte(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0, nullptr, nullptr) - 1;
+
+ if (utf8Length > 0)
+ {
+ utf8String.resize(utf8Length);
+ ::WideCharToMultiByte(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), utf8String.data(), utf8Length, nullptr, nullptr);
+ }
+
+ return utf8String;
+ }
+
+ std::wstring Widen(std::string_view inputString)
+ {
+ std::wstring utf16String;
+ const int utf16Length = ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0) - 1;
+
+ if (utf16Length > 0)
+ {
+ utf16String.resize(utf16Length);
+ ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), utf16String.data(), utf16Length);
+ }
+
+ return utf16String;
+ }
+
+ std::pair GetCommandLineArguments()
+ {
+ static std::vector argvString;
+ static std::vector argvCStr;
+
+ if (!argvString.empty() || !argvCStr.empty())
+ return { static_cast(argvString.size()), argvCStr.data() };
+
+ int argc = 0;
+ auto argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+
+ argvString.reserve(argc);
+ argvCStr.reserve(argc);
+
+ for (auto i = 0; i < argc; i++)
+ argvCStr.emplace_back(argvString.emplace_back(UTF8::Narrow(argv[i])).c_str());
+
+ ::LocalFree(argv);
+ return { argc, argvCStr.data() };
+ }
+
+ std::vector GetArgV()
+ {
+ return std::vector();
+ }
+
+ WideArg::WideArg(std::string_view inputString)
+ {
+ // NOTE: Length **without** null terminator
+ convertedLength = ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0) - 1;
+
+ if (convertedLength <= 0)
+ {
+ stackBuffer[0] = L'\0';
+ return;
+ }
+
+ if (convertedLength < stackBuffer.size())
+ {
+ ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), stackBuffer.data(), convertedLength);
+ stackBuffer[convertedLength] = L'\0';
+ }
+ else
+ {
+ heapBuffer = std::make_unique(convertedLength + 1);
+ ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), heapBuffer.get(), convertedLength);
+ heapBuffer[convertedLength] = L'\0';
+ }
+ }
+
+ const wchar_t* WideArg::c_str() const
+ {
+ return (convertedLength < stackBuffer.size()) ? stackBuffer.data() : heapBuffer.get();
+ }
+ }
+
+ namespace IO
+ {
+ std::pair, size_t> ReadEntireFile(std::string_view filePath)
+ {
+ std::unique_ptr fileContent = nullptr;
+ size_t fileSize = 0;
+
+ ::HANDLE fileHandle = ::CreateFileW(UTF8::WideArg(filePath).c_str(), GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (fileHandle != INVALID_HANDLE_VALUE)
+ {
+ ::LARGE_INTEGER largeIntegerFileSize = {};
+ ::GetFileSizeEx(fileHandle, &largeIntegerFileSize);
+
+ if (fileSize = static_cast(largeIntegerFileSize.QuadPart); fileSize > 0)
+ {
+ if (fileContent = std::make_unique(fileSize); fileContent != nullptr)
+ {
+ assert(fileSize < std::numeric_limits::max() && "No way that's ever gonna happen, right?");
+
+ DWORD bytesRead = 0;
+ ::ReadFile(fileHandle, fileContent.get(), static_cast(fileSize), &bytesRead, nullptr);
+ }
+ }
+
+ ::CloseHandle(fileHandle);
+ }
+
+ return { std::move(fileContent), fileSize };
+ }
+
+ bool WriteEntireFile(std::string_view filePath, const u8* fileContent, size_t fileSize)
+ {
+ if (filePath.empty() || fileContent == nullptr || fileSize == 0)
+ return false;
+
+ ::HANDLE fileHandle = ::CreateFileW(UTF8::WideArg(filePath).c_str(), GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (fileHandle == INVALID_HANDLE_VALUE)
+ return false;
+
+ assert(fileSize < std::numeric_limits::max() && "No way that's ever gonna happen, right?");
+
+ DWORD bytesWritten = 0;
+ ::WriteFile(fileHandle, fileContent, static_cast(fileSize), &bytesWritten, nullptr);
+
+ ::CloseHandle(fileHandle);
+ return true;
+ }
+
+ bool HasFileExtension(std::string_view filePath, std::string_view extensionToCheckFor)
+ {
+ assert(!extensionToCheckFor.empty() && extensionToCheckFor[0] == '.');
+
+ if (extensionToCheckFor.size() >= filePath.size())
+ return false;
+
+ const auto stringA = filePath.substr(filePath.size() - extensionToCheckFor.size());
+ const auto stringB = extensionToCheckFor;
+ return std::equal(stringA.begin(), stringA.end(), stringB.begin(), stringB.end(), [](char a, char b) { return ::tolower(a) == ::tolower(b); });
+ }
+
+ std::string ChangeFileExtension(std::string_view filePath, std::string_view newExtension)
+ {
+ const size_t lastSeparator = filePath.find_last_of("./\\");
+ if (lastSeparator != std::string_view::npos)
+ {
+ if (filePath[lastSeparator] == '.')
+ return std::string(filePath.substr(0, lastSeparator)) + std::string(newExtension);
+ }
+
+ return std::string(filePath) + std::string(newExtension);
+ }
+ }
+
+ namespace Crypto
+ {
+ bool DecryptAes128Cbc(const u8* inEncryptedData, u8* outDecryptedData, size_t inOutDataSize, std::array key, std::array iv)
+ {
+ bool successful = false;
+ ::NTSTATUS status = {};
+ ::BCRYPT_ALG_HANDLE algorithmHandle = {};
+
+ status = ::BCryptOpenAlgorithmProvider(&algorithmHandle, BCRYPT_AES_ALGORITHM, nullptr, 0);
+ if (NT_SUCCESS(status))
+ {
+ status = ::BCryptSetProperty(algorithmHandle, BCRYPT_CHAINING_MODE, reinterpret_cast(const_cast(BCRYPT_CHAIN_MODE_CBC)), sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
+ if (NT_SUCCESS(status))
+ {
+ ULONG keyObjectSize = {};
+ ULONG copiedDataSize = {};
+
+ status = ::BCryptGetProperty(algorithmHandle, BCRYPT_OBJECT_LENGTH, reinterpret_cast(&keyObjectSize), sizeof(ULONG), &copiedDataSize, 0);
+ if (NT_SUCCESS(status))
+ {
+ ::BCRYPT_KEY_HANDLE symmetricKeyHandle = {};
+ auto keyObject = std::make_unique(keyObjectSize);
+
+ status = ::BCryptGenerateSymmetricKey(algorithmHandle, &symmetricKeyHandle, keyObject.get(), keyObjectSize, key.data(), static_cast(key.size()), 0);
+ if (NT_SUCCESS(status))
+ {
+ status = ::BCryptDecrypt(symmetricKeyHandle, const_cast(inEncryptedData), static_cast(inOutDataSize), nullptr, iv.data(), static_cast(iv.size()), outDecryptedData, static_cast(inOutDataSize), &copiedDataSize, 0);
+ if (NT_SUCCESS(status))
+ {
+ successful = true;
+ }
+ else
+ {
+ fprintf(stderr, __FUNCTION__"(): BCryptDecrypt() failed with 0x%X", status);
+ }
+
+ if (symmetricKeyHandle)
+ ::BCryptDestroyKey(symmetricKeyHandle);
+ }
+ else
+ {
+ fprintf(stderr, __FUNCTION__"(): BCryptGenerateSymmetricKey() failed with 0x%X", status);
+ }
+ }
+ else
+ {
+ fprintf(stderr, __FUNCTION__"(): BCryptGetProperty(BCRYPT_OBJECT_LENGTH) failed with 0x%X", status);
+ }
+ }
+ else
+ {
+ fprintf(stderr, __FUNCTION__"(): BCryptSetProperty(BCRYPT_CHAINING_MODE) failed with 0x%X", status);
+ }
+
+ if (algorithmHandle)
+ ::BCryptCloseAlgorithmProvider(algorithmHandle, 0);
+ }
+ else
+ {
+ fprintf(stderr, __FUNCTION__"(): BCryptOpenAlgorithmProvider(BCRYPT_AES_ALGORITHM) failed with 0x%X", status);
+ }
+
+ return successful;
+ }
+ }
+
+ namespace Compression
+ {
+ bool Inflate(const u8* inCompressedData, size_t inDataSize, u8* outDecompressedData, size_t outDataSize)
+ {
+ z_stream zStream = {};
+ zStream.zalloc = Z_NULL;
+ zStream.zfree = Z_NULL;
+ zStream.opaque = Z_NULL;
+ zStream.avail_in = static_cast(inDataSize);
+ zStream.next_in = static_cast(inCompressedData);
+ zStream.avail_out = static_cast(outDataSize);
+ zStream.next_out = static_cast(outDecompressedData);
+
+ const int initResult = inflateInit2(&zStream, 31);
+ if (initResult != Z_OK)
+ return false;
+
+ const int inflateResult = inflate(&zStream, Z_FINISH);
+ // assert(inflateResult == Z_STREAM_END && zStream.msg == nullptr);
+
+ const int endResult = inflateEnd(&zStream);
+ if (endResult != Z_OK)
+ return false;
+
+ return true;
+ }
+
+ size_t Deflate(const u8* inData, size_t inDataSize, u8* outCompressedData, size_t outDataSize)
+ {
+ constexpr size_t chunkStepSize = 0x4000;
+
+ z_stream zStream = {};
+ zStream.zalloc = Z_NULL;
+ zStream.zfree = Z_NULL;
+ zStream.opaque = Z_NULL;
+
+ int errorCode = deflateInit2(&zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY);
+ assert(errorCode == Z_OK);
+
+ const u8* inDataReadHeader = static_cast(inData);
+ size_t remainingSize = inDataSize;
+ size_t compressedSize = 0;
+
+ while (remainingSize > 0)
+ {
+ const size_t chunkSize = std::min(remainingSize, chunkStepSize);
+
+ zStream.avail_in = static_cast(chunkSize);
+ zStream.next_in = reinterpret_cast(inDataReadHeader);
+
+ inDataReadHeader += chunkSize;
+ remainingSize -= chunkSize;
+
+ do
+ {
+ std::array outputBuffer;
+
+ zStream.avail_out = chunkStepSize;
+ zStream.next_out = outputBuffer.data();
+
+ errorCode = deflate(&zStream, remainingSize == 0 ? Z_FINISH : Z_NO_FLUSH);
+ assert(errorCode != Z_STREAM_ERROR);
+
+ const auto compressedChunkSize = chunkStepSize - zStream.avail_out;
+ memcpy(&outCompressedData[compressedSize], outputBuffer.data(), compressedChunkSize);
+
+ compressedSize += compressedChunkSize;
+ }
+ while (zStream.avail_out == 0);
+ assert(zStream.avail_in == 0);
+ }
+
+ deflateEnd(&zStream);
+
+ assert(errorCode == Z_STREAM_END);
+ return compressedSize;
+ }
+ }
+}
diff --git a/TaikoSwitchDataTableDecryptor/src/Utilities.h b/TaikoSwitchDataTableDecryptor/src/Utilities.h
new file mode 100644
index 0000000..d3e3541
--- /dev/null
+++ b/TaikoSwitchDataTableDecryptor/src/Utilities.h
@@ -0,0 +1,64 @@
+#pragma once
+#include "Types.h"
+
+#define EXIT_WIDEPEEPOHAPPY EXIT_SUCCESS
+#define EXIT_WIDEPEEPOSAD EXIT_FAILURE
+
+namespace PeepoHappy
+{
+ // NOTE: Following the "UTF-8 Everywhere" guidelines
+ namespace UTF8
+ {
+ // NOTE: Convert UTF-16 to UTF-8
+ std::string Narrow(std::wstring_view);
+
+ // NOTE: Convert UTF-8 to UTF-16
+ std::wstring Widen(std::string_view);
+
+ // NOTE: To avoid needless heap allocations for temporary wchar_t C-API function arguments
+ // Example: DummyU16FuncW(UTF8::WideArg(stringU8).c_str(), ...)
+ class WideArg : NonCopyable
+ {
+ public:
+ WideArg(std::string_view);
+ const wchar_t* c_str() const;
+
+ private:
+ std::unique_ptr heapBuffer;
+ std::array stackBuffer;
+ int convertedLength;
+ };
+
+ // NOTE: Includes the program file path as first argument
+ std::pair GetCommandLineArguments();
+ }
+
+ namespace IO
+ {
+ // NOTE: Sucks for modders, makes sense for them to do it though...
+ constexpr size_t MaxDecompressedGameDataTableFileSize = 0x200000;
+
+ std::pair, size_t> ReadEntireFile(std::string_view filePath);
+ bool WriteEntireFile(std::string_view filePath, const u8* fileContent, size_t fileSize);
+
+ bool HasFileExtension(std::string_view filePath, std::string_view extensionToCheckFor);
+ std::string ChangeFileExtension(std::string_view filePath, std::string_view newExtension);
+ }
+
+ namespace Crypto
+ {
+ constexpr size_t AesKeySize = 16;
+
+ // NOTE: Literally loaded directly into X8 right before calling nn::crypto::DecryptAes128Cbc()
+ // they couldn't even bother trying to "hide" it by adding a few pointer indirection or scrambling first :KEKL:
+ constexpr std::array DataTableAesKey = { 0x57, 0x39, 0x73, 0x35, 0x38, 0x73, 0x68, 0x43, 0x54, 0x70, 0x76, 0x75, 0x6A, 0x6B, 0x4A, 0x74, };
+
+ bool DecryptAes128Cbc(const u8* inEncryptedData, u8* outDecryptedData, size_t inOutDataSize, std::array key, std::array iv);
+ }
+
+ namespace Compression
+ {
+ bool Inflate(const u8* inCompressedData, size_t inDataSize, u8* outDecompressedData, size_t outDataSize);
+ size_t Deflate(const u8* inData, size_t inDataSize, u8* outCompressedData, size_t outDataSize);
+ }
+}