From 4a79f260c53d9321940372c0e5d0f806d0552bdf Mon Sep 17 00:00:00 2001 From: Orange Date: Wed, 29 Jul 2026 02:58:25 +0300 Subject: [PATCH] added hashing/base64 functions --- include/omath/hashing.hpp | 130 ++++++++++++++++++++++++++++ include/omath/omath.hpp | 1 + tests/general/unit_test_hashing.cpp | 36 ++++++++ 3 files changed, 167 insertions(+) create mode 100644 include/omath/hashing.hpp create mode 100644 tests/general/unit_test_hashing.cpp diff --git a/include/omath/hashing.hpp b/include/omath/hashing.hpp new file mode 100644 index 0000000..2bc0377 --- /dev/null +++ b/include/omath/hashing.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include + +namespace omath::hashing +{ + enum class Base64Error + { + INVALID_LENGTH, + INVALID_CHARACTER, + INVALID_PADDING + }; + + [[nodiscard("FNV-1a hash result should not be discarded")]] + constexpr std::uint64_t fnv1a(const std::string_view value) noexcept + { + std::uint64_t hash = 14695981039346656037ULL; + + for (const char character : value) + { + hash ^= static_cast(character); + hash *= 1099511628211ULL; + } + + return hash; + } + + [[nodiscard("CRC-32 checksum result should not be discarded")]] + constexpr std::uint32_t crc32(const std::string_view value) noexcept + { + std::uint32_t crc = 0xFFFFFFFFU; + + for (const char character : value) + { + crc ^= static_cast(character); + + for (int bit = 0; bit < 8; bit++) + { + crc = crc & 1U ? (crc >> 1U) ^ 0xEDB88320U : crc >> 1U; + } + } + + return crc ^ 0xFFFFFFFFU; + } + + [[nodiscard("Base64 encoding result should not be discarded")]] + constexpr std::string base64_encode(const std::string_view value) + { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string result; + result.reserve((value.size() + 2) / 3 * 4); + + for (std::size_t index = 0; index < value.size(); index += 3) + { + const std::uint32_t first = static_cast(value[index]); + const std::uint32_t second = index + 1 < value.size() ? static_cast(value[index + 1]) : 0; + const std::uint32_t third = index + 2 < value.size() ? static_cast(value[index + 2]) : 0; + const std::uint32_t chunk = first << 16U | second << 8U | third; + + result += alphabet[(chunk >> 18U) & 0x3FU]; + result += alphabet[(chunk >> 12U) & 0x3FU]; + result += index + 1 < value.size() ? alphabet[(chunk >> 6U) & 0x3FU] : '='; + result += index + 2 < value.size() ? alphabet[chunk & 0x3FU] : '='; + } + + return result; + } + + [[nodiscard("Base64 decoding result should not be discarded")]] + constexpr std::expected base64_decode(const std::string_view value) + { + if (value.size() % 4 != 0) + return std::unexpected(Base64Error::INVALID_LENGTH); + + std::string result; + result.reserve(value.size() / 4 * 3); + + for (std::size_t index = 0; index < value.size(); index += 4) + { + const auto decode_character = [](const char character) constexpr -> int + { + if (character >= 'A' && character <= 'Z') + return character - 'A'; + if (character >= 'a' && character <= 'z') + return character - 'a' + 26; + if (character >= '0' && character <= '9') + return character - '0' + 52; + if (character == '+') + return 62; + if (character == '/') + return 63; + + return -1; + }; + + const bool is_last_chunk = index + 4 == value.size(); + const int first = decode_character(value[index]); + const int second = decode_character(value[index + 1]); + const int third = value[index + 2] == '=' ? -2 : decode_character(value[index + 2]); + const int fourth = value[index + 3] == '=' ? -2 : decode_character(value[index + 3]); + + if (first < 0 || second < 0 || third == -1 || fourth == -1) + return std::unexpected(Base64Error::INVALID_CHARACTER); + if (third == -2) + { + if (fourth != -2 || !is_last_chunk || (second & 0x0F) != 0) + return std::unexpected(Base64Error::INVALID_PADDING); + } + else if (fourth == -2 && (!is_last_chunk || (third & 0x03) != 0)) + return std::unexpected(Base64Error::INVALID_PADDING); + + const std::uint32_t chunk = static_cast(first) << 18U + | static_cast(second) << 12U + | static_cast(third < 0 ? 0 : third) << 6U + | static_cast(fourth < 0 ? 0 : fourth); + + result += static_cast(chunk >> 16U); + if (third >= 0) + result += static_cast(chunk >> 8U); + if (fourth >= 0) + result += static_cast(chunk); + } + + return result; + } +} // namespace omath::hashing diff --git a/include/omath/omath.hpp b/include/omath/omath.hpp index f249eac..3ddb233 100644 --- a/include/omath/omath.hpp +++ b/include/omath/omath.hpp @@ -6,6 +6,7 @@ #pragma once // Basic math utilities +#include "omath/hashing.hpp" #include "omath/trigonometry/angles.hpp" #include "omath/trigonometry/angle.hpp" diff --git a/tests/general/unit_test_hashing.cpp b/tests/general/unit_test_hashing.cpp new file mode 100644 index 0000000..c4386ef --- /dev/null +++ b/tests/general/unit_test_hashing.cpp @@ -0,0 +1,36 @@ +#include +#include + +TEST(unit_test_hashing, fnv1a) +{ + constexpr auto hash = omath::hashing::fnv1a("hello"); + + static_assert(hash == 0xA430D84680AABD0BULL); + EXPECT_EQ(hash, 0xA430D84680AABD0BULL); +} + +TEST(unit_test_hashing, crc32) +{ + constexpr auto crc = omath::hashing::crc32("123456789"); + + static_assert(crc == 0xCBF43926U); + EXPECT_EQ(crc, 0xCBF43926U); +} + +TEST(unit_test_hashing, base64_encode) +{ + static_assert(omath::hashing::base64_encode("foo") == "Zm9v"); + EXPECT_EQ(omath::hashing::base64_encode("foobar"), "Zm9vYmFy"); +} + +TEST(unit_test_hashing, base64_decode) +{ + constexpr auto decoded = omath::hashing::base64_decode("Zm9v"); + + static_assert(decoded.has_value()); + static_assert(decoded.value() == "foo"); + EXPECT_EQ(omath::hashing::base64_decode("Zm9vYmFy").value(), "foobar"); + EXPECT_EQ(omath::hashing::base64_decode("Zm9v=").error(), omath::hashing::Base64Error::INVALID_LENGTH); + EXPECT_EQ(omath::hashing::base64_decode("Zm?v").error(), omath::hashing::Base64Error::INVALID_CHARACTER); + EXPECT_EQ(omath::hashing::base64_decode("Zm9=").error(), omath::hashing::Base64Error::INVALID_PADDING); +}