diff --git a/.gitignore b/.gitignore index e257658..408404e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ # ---> C++ +# Build dirs +build/ + # Prerequisites *.d @@ -31,4 +34,3 @@ *.exe *.out *.app - diff --git a/CMake/Catch2.cmake b/CMake/Catch2.cmake new file mode 100644 index 0000000..3159c30 --- /dev/null +++ b/CMake/Catch2.cmake @@ -0,0 +1,8 @@ +include(FetchContent) +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.0 +) + +FetchContent_MakeAvailable(Catch2) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c5e9bff --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.21) + +project(Garbage + LANGUAGES C CXX +) + +add_subdirectory(Garbage) + +if(PROJECT_IS_TOP_LEVEL) + add_subdirectory(Tests) +endif() diff --git a/Garbage/CMakeLists.txt b/Garbage/CMakeLists.txt new file mode 100644 index 0000000..8b3ff67 --- /dev/null +++ b/Garbage/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(Garbage INTERFACE) + +target_compile_features(Garbage + INTERFACE + cxx_std_20 +) + +target_include_directories(Garbage + INTERFACE + "Include" +) diff --git a/Garbage/Include/Garbage/Base64.hpp b/Garbage/Include/Garbage/Base64.hpp new file mode 100644 index 0000000..e7536e8 --- /dev/null +++ b/Garbage/Include/Garbage/Base64.hpp @@ -0,0 +1,143 @@ +#ifndef GARBAGE_BASE64_HPP +#define GARBAGE_BASE64_HPP + +#include +#include +#include +#include +#include +#include + +namespace Garbage::Base64 +{ + namespace Internal + { + struct SmallBuffer + { + public: + [[nodiscard]] constexpr + SmallBuffer() + : mStoredBits(0), mData(0) + {} + + constexpr + void Enqueue(std::uint8_t byte, std::size_t bits = 8) + { + std::uint8_t mask = (1u << bits) - 1; + mData = (mData << bits) | (byte & mask); + mStoredBits += bits; + } + + [[nodiscard]] constexpr + std::uint8_t Dequeue(std::size_t bits = 8) + { + std::uint64_t mask = (1u << bits) - 1; + mStoredBits -= bits; + std::uint64_t out = (mData >> mStoredBits) & mask; + return out; + } + + [[nodiscard]] constexpr + std::size_t Size() + { + return mStoredBits; + } + + [[nodiscard]] constexpr + std::size_t Capacity() + { + return 64; + } + private: + std::size_t mStoredBits; + std::uint64_t mData; + }; + + static constexpr auto EncodeLUT = std::array { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/', + }; + + static constexpr std::size_t DecodeOffset = '+'; + static constexpr auto DecodeLUT = std::array { + 0b111'110, /* + */ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ + 0b111'111, /* / */ 0b110'100, /* 0 */ 0b110'101, /* 1 */ 0b110'110, /* 2 */ + 0b110'111, /* 3 */ 0b111'000, /* 4 */ 0b111'001, /* 5 */ 0b111'010, /* 6 */ + 0b111'011, /* 7 */ 0b111'100, /* 8 */ 0b111'101, /* 9 */ 0b000'000, /*---*/ + 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ + 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /* A */ 0b000'001, /* B */ + 0b000'010, /* C */ 0b000'011, /* D */ 0b000'100, /* E */ 0b000'101, /* F */ + 0b000'110, /* G */ 0b000'111, /* H */ 0b001'000, /* I */ 0b001'001, /* J */ + 0b001'010, /* K */ 0b001'011, /* L */ 0b001'100, /* M */ 0b001'101, /* N */ + 0b001'110, /* O */ 0b001'111, /* P */ 0b010'000, /* Q */ 0b010'001, /* R */ + 0b010'010, /* S */ 0b010'011, /* T */ 0b010'100, /* U */ 0b010'101, /* V */ + 0b010'110, /* W */ 0b010'111, /* X */ 0b011'000, /* Y */ 0b011'001, /* Z */ + 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ + 0b000'000, /*---*/ 0b000'000, /*---*/ 0b011'010, /* a */ 0b011'011, /* b */ + 0b011'100, /* c */ 0b011'101, /* d */ 0b011'110, /* e */ 0b011'111, /* f */ + 0b100'000, /* g */ 0b100'001, /* h */ 0b100'010, /* i */ 0b100'011, /* j */ + 0b100'100, /* k */ 0b100'101, /* l */ 0b100'110, /* m */ 0b100'111, /* n */ + 0b101'000, /* o */ 0b101'001, /* p */ 0b101'010, /* q */ 0b101'011, /* r */ + 0b101'100, /* s */ 0b101'101, /* t */ 0b101'110, /* u */ 0b101'111, /* v */ + 0b110'000, /* w */ 0b110'001, /* x */ 0b110'010, /* y */ 0b110'011, /* z */ + }; + } + + [[nodiscard]] constexpr + std::string Encode(std::span data) { + std::string result; + result.reserve(data.size() + data.size() / 2); + + Internal::SmallBuffer buffer; + std::size_t i = 0; + while (i < data.size()) { + while (buffer.Capacity() - buffer.Size() >= 8 && i < data.size()) { + buffer.Enqueue(data[i++]); + } + while (buffer.Size() >= 6) { + result.push_back(Internal::EncodeLUT[buffer.Dequeue(6)]); + } + } + if (buffer.Size() > 0) { + std::size_t remainingBits = buffer.Size(); + std::uint8_t data = (buffer.Dequeue(remainingBits) << (6 - remainingBits)); + result.push_back(Internal::EncodeLUT[data]); + } + if (data.size() % 3) { + result.append(std::string(4 - (result.size() % 4), '=')); + } + + return result; + } + + [[nodiscard]] constexpr + std::vector Decode(std::string_view data) { + std::vector result; + result.reserve(data.size()); + + std::size_t end = data.size(); + end -= (data.size() > 0 && data[data.size() - 1] == '='); + end -= (data.size() > 1 && data[data.size() - 2] == '='); + + Internal::SmallBuffer buffer; + std::size_t i = 0; + while (i < end) { + while (buffer.Capacity() - buffer.Size() >= 6 && i < end) { + buffer.Enqueue(Internal::DecodeLUT[data[i++] - Internal::DecodeOffset], 6); + } + while (buffer.Size() >= 8) { + result.push_back(buffer.Dequeue(8)); + } + } + + return result; + } +} + +#endif // GARBAGE_BASE64_HPP diff --git a/Tests/Base64/CMakeLists.txt b/Tests/Base64/CMakeLists.txt new file mode 100644 index 0000000..16e03e9 --- /dev/null +++ b/Tests/Base64/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(TestBase64) + +target_compile_features(TestBase64 + PRIVATE + cxx_std_20 +) + +target_link_libraries(TestBase64 + PRIVATE + Garbage + Catch2::Catch2WithMain +) + +target_sources(TestBase64 + PRIVATE + Tests.cpp +) diff --git a/Tests/Base64/Tests.cpp b/Tests/Base64/Tests.cpp new file mode 100644 index 0000000..67a6784 --- /dev/null +++ b/Tests/Base64/Tests.cpp @@ -0,0 +1,70 @@ +#include +#include + +TEST_CASE("Base64 encoding/decoding tests") +{ + SECTION("Empty span") + { + std::vector data; + + std::string encoded = Garbage::Base64::Encode(data); + REQUIRE(encoded.size() == 0); + + std::vector decoded = Garbage::Base64::Decode(encoded); + REQUIRE(encoded.size() == 0); + } + + SECTION("Encoding short data") + { + std::string source("Many hands make light work."); + std::vector data(source.begin(), source.end()); + + std::string encoded = Garbage::Base64::Encode(data); + REQUIRE(encoded == "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu"); + } + + SECTION("Decoding short data") + { + std::string data("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu"); + + std::vector decoded = Garbage::Base64::Decode(data); + std::string destination(decoded.begin(), decoded.end()); + REQUIRE(destination == "Many hands make light work."); + } + + SECTION("Encoding with a padding char") + { + std::string source("With a padding"); + std::vector data(source.begin(), source.end()); + + std::string encoded = Garbage::Base64::Encode(data); + REQUIRE(encoded == "V2l0aCBhIHBhZGRpbmc="); + } + + SECTION("Decoding with a padding char") + { + std::string data("V2l0aCBhIHBhZGRpbmc="); + + std::vector decoded = Garbage::Base64::Decode(data); + std::string destination(decoded.begin(), decoded.end()); + REQUIRE(destination == "With a padding"); + } + + SECTION("Encoding with two padding chars") + { + std::string source("With two padding"); + std::vector data(source.begin(), source.end()); + + std::string encoded = Garbage::Base64::Encode(data); + REQUIRE(encoded == "V2l0aCB0d28gcGFkZGluZw=="); + } + + SECTION("Decoding with two padding chars") + { + std::string data("V2l0aCB0d28gcGFkZGluZw=="); + + std::vector decoded = Garbage::Base64::Decode(data); + std::string destination(decoded.begin(), decoded.end()); + REQUIRE(destination == "With two padding"); + } +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..8f0b7dd --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,3 @@ +include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake) + +add_subdirectory(Base64)