From 2eefad415208bf6d2bba52ad45ee2eb1bb20a783 Mon Sep 17 00:00:00 2001 From: TennesseeTrash Date: Tue, 10 Jun 2025 00:31:58 +0200 Subject: [PATCH] [PercentEncoding] Add minimal percent encoding/decoding functions --- Garbage/Include/Garbage/PercentEncoding.hpp | 91 +++++++++++++++++++++ Tests/CMakeLists.txt | 1 + Tests/PercentEncoding/CMakeLists.txt | 17 ++++ Tests/PercentEncoding/Tests.cpp | 71 ++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 Garbage/Include/Garbage/PercentEncoding.hpp create mode 100644 Tests/PercentEncoding/CMakeLists.txt create mode 100644 Tests/PercentEncoding/Tests.cpp diff --git a/Garbage/Include/Garbage/PercentEncoding.hpp b/Garbage/Include/Garbage/PercentEncoding.hpp new file mode 100644 index 0000000..f77ecbf --- /dev/null +++ b/Garbage/Include/Garbage/PercentEncoding.hpp @@ -0,0 +1,91 @@ +#ifndef GARBAGE_PERCENT_ENCODING_HPP +#define GARBAGE_PERCENT_ENCODING_HPP + +#include +#include +#include +#include +#include + +namespace Garbage::Percent +{ + namespace Internal + { + static constexpr auto EncodeLUT = std::array { + '0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F', + }; + + [[nodiscard]] constexpr + std::uint8_t DecodeNibble(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + return 0; + } + + [[nodiscard]] constexpr + std::uint8_t DecodeChar(char upper, char lower) { + return (DecodeNibble(upper) << 4) | DecodeNibble(lower); + } + + [[nodiscard]] constexpr + bool IsHex(char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); + } + } + + [[nodiscard]] constexpr + std::string Encode(std::span data) + { + std::string result; + result.reserve(data.size() * 3); + + for (std::uint8_t byte : data) { + result.push_back('%'); + result.push_back(Internal::EncodeLUT[byte >> 4]); + result.push_back(Internal::EncodeLUT[byte & 15]); + } + + return result; + } + + [[nodiscard]] constexpr + std::vector Decode(std::string_view data) + { + std::vector result; + result.reserve(data.size() / 3); + + for (std::size_t i = 0; i < data.size(); i += 3) { + result.push_back(Internal::DecodeChar(data[i + 1], data[i + 2])); + } + + return result; + } + + [[nodiscard]] constexpr + bool Validate(std::string_view data) + { + if (data.size() % 3) { + return false; + } + + for (std::size_t i = 0; i < data.size(); i += 3) { + if (data[i] != '%' || !Internal::IsHex(data[i + 1]) || !Internal::IsHex(data[i + 2])) { + return false; + } + } + + return true; + } +} + +#endif // GARBAGE_PERCENT_ENCODING_HPP diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 475c6f3..2b12d66 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -1,5 +1,6 @@ include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake) add_subdirectory(Base64) +add_subdirectory(PercentEncoding) add_subdirectory(SQLite) add_subdirectory(SimpleConf) diff --git a/Tests/PercentEncoding/CMakeLists.txt b/Tests/PercentEncoding/CMakeLists.txt new file mode 100644 index 0000000..f090fb1 --- /dev/null +++ b/Tests/PercentEncoding/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(TestPercentEncoding) + +target_compile_features(TestPercentEncoding + PRIVATE + cxx_std_20 +) + +target_link_libraries(TestPercentEncoding + PRIVATE + Garbage + Catch2::Catch2WithMain +) + +target_sources(TestPercentEncoding + PRIVATE + Tests.cpp +) diff --git a/Tests/PercentEncoding/Tests.cpp b/Tests/PercentEncoding/Tests.cpp new file mode 100644 index 0000000..2231c2e --- /dev/null +++ b/Tests/PercentEncoding/Tests.cpp @@ -0,0 +1,71 @@ +#include +#include + +TEST_CASE("Percent encoding/decoding tests") +{ + SECTION("Empty span") + { + std::vector data; + + std::string encoded = Garbage::Percent::Encode(data); + REQUIRE(encoded.size() == 0); + + REQUIRE(Garbage::Percent::Validate(encoded)); + + std::vector decoded = Garbage::Percent::Decode(encoded); + REQUIRE(encoded.size() == 0); + } + + SECTION("Validation rejections") + { + constexpr std::string_view invalid1 = "Random chars"; + REQUIRE_FALSE(Garbage::Percent::Validate(invalid1)); + + constexpr std::string_view invalid2 = "%AFFAF%AF"; + REQUIRE_FALSE(Garbage::Percent::Validate(invalid2)); + } + + SECTION("Encoding short data") + { + constexpr std::string_view source("日"); + std::vector data(source.begin(), source.end()); + + std::string encoded = Garbage::Percent::Encode(data); + + REQUIRE(encoded == "%E6%97%A5"); + REQUIRE(Garbage::Percent::Validate(encoded)); + } + + SECTION("Decoding short data") + { + std::string data("%E6%97%A5"); + + REQUIRE(Garbage::Percent::Validate(data)); + + std::vector decoded = Garbage::Percent::Decode(data); + std::string destination(decoded.begin(), decoded.end()); + REQUIRE(destination == "日"); + } + + SECTION("Encoding longer data") + { + constexpr std::string_view source("日男昼持賃竹田"); + std::vector data(source.begin(), source.end()); + + std::string encoded = Garbage::Percent::Encode(data); + + REQUIRE(encoded == "%E6%97%A5%E7%94%B7%E6%98%BC%E6%8C%81%E8%B3%83%E7%AB%B9%E7%94%B0"); + REQUIRE(Garbage::Percent::Validate(encoded)); + } + + SECTION("Decoding longer data") + { + std::string data("%E6%97%A5%E7%94%B7%E6%98%BC%E6%8C%81%E8%B3%83%E7%AB%B9%E7%94%B0"); + + REQUIRE(Garbage::Percent::Validate(data)); + + std::vector decoded = Garbage::Percent::Decode(data); + std::string destination(decoded.begin(), decoded.end()); + REQUIRE(destination == "日男昼持賃竹田"); + } +}