diff options
| -rw-r--r-- | meson.build | 22 | ||||
| -rw-r--r-- | src/base64.cc | 142 | ||||
| -rw-r--r-- | src/base64.hh | 25 | ||||
| -rw-r--r-- | test/base64.cc | 70 |
4 files changed, 259 insertions, 0 deletions
diff --git a/meson.build b/meson.build index ae82f44..2b74a48 100644 --- a/meson.build +++ b/meson.build @@ -179,6 +179,18 @@ signals_dep = declare_dependency( dependencies: [io_dep, looper_dep], ) +base64_lib = library( + 'base64', + sources: [ + 'src/base64.cc', + 'src/base64.hh', + ], + include_directories: inc, +) +base64_dep = declare_dependency( + link_with: base64_lib, +) + bluetooth_jukebox = executable( 'bluetooth-jukebox', sources: [ @@ -311,6 +323,16 @@ test('cfg', executable( ], )) +test('base64', executable( + 'test_base64', + sources: ['test/base64.cc'], + include_directories: inc, + dependencies : [ + base64_dep, + test_dependencies, + ], +)) + run_clang_tidy = find_program('run-clang-tidy', required: false) if run_clang_tidy.found() diff --git a/src/base64.cc b/src/base64.cc new file mode 100644 index 0000000..ce7b41d --- /dev/null +++ b/src/base64.cc @@ -0,0 +1,142 @@ +#include "base64.hh" + +#include <cstddef> +#include <cstdint> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <vector> + +namespace base64 { + +namespace { + +std::string_view kAlphabet( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"); + +[[nodiscard]] +inline char encode(uint8_t c) { + return kAlphabet[c]; +} + +[[nodiscard]] +inline uint8_t decode(char c) { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return 26 + (c - 'a'); + } + if (c >= '0' && c <= '9') { + return 52 + (c - '0'); + } + if (c == '+') + return 62; + if (c == '/') + return 63; + return 255; +} + +} // namespace + +std::string encode(std::span<uint8_t const> data) { + std::string ret; + encode(data, ret); + return ret; +} + +void encode(std::span<uint8_t const> in, std::string& out) { + out.reserve((in.size() * 4 + 3) / 3); + size_t const leftover = in.size() % 3; + size_t const end = in.size() - leftover; + size_t i = 0; + for (; i < end; i += 3) { + out.push_back(encode(in[i] >> 2)); + out.push_back(encode(((in[i] & 0x03) << 4) | (in[i + 1] >> 4))); + out.push_back(encode(((in[i + 1] & 0x0f) << 2) | (in[i + 2] >> 6))); + out.push_back(encode(in[i + 2] & 0x3f)); + } + switch (leftover) { // NOLINT(bugprone-switch-missing-default-case) + case 0: + return; + case 1: + out.push_back(encode(in[i] >> 2)); + out.push_back(encode((in[i] & 0x03) << 4)); + out.push_back('='); + break; + case 2: + out.push_back(encode(in[i] >> 2)); + out.push_back(encode(((in[i] & 0x03) << 4) | (in[i + 1] >> 4))); + out.push_back(encode((in[i + 1] & 0x0f) << 2)); + break; + } + out.push_back('='); +} + +std::optional<std::vector<uint8_t>> decode(std::string_view value) { + std::vector<uint8_t> ret; + if (decode(value, ret)) + return ret; + return std::nullopt; +} + +bool decode(std::string_view in, std::vector<uint8_t>& out) { + if (in.size() % 4) + return false; + + if (in.empty()) + return true; + + size_t pad; + if (in.back() == '=') { + if (in[in.size() - 2] == '=') { + pad = 2; + } else { + pad = 1; + } + } else { + pad = 0; + } + + size_t const end = in.size() - (pad ? 4 : 0); + size_t i = 0; + for (; i < end; i += 4) { + auto v1 = decode(in[i]); + auto v2 = decode(in[i + 1]); + auto v3 = decode(in[i + 2]); + auto v4 = decode(in[i + 3]); + if (v1 == 255 || v2 == 255 || v3 == 255 || v4 == 255) + return false; + out.push_back((v1 << 2) | (v2 >> 4)); + out.push_back(((v2 & 0xf) << 4) | (v3 >> 2)); + out.push_back(((v3 & 0x3) << 6) | v4); + } + switch (pad) { // NOLINT(bugprone-switch-missing-default-case) + case 0: + break; + case 1: { + auto v1 = decode(in[i]); + auto v2 = decode(in[i + 1]); + auto v3 = decode(in[i + 2]); + if (v1 == 255 || v2 == 255 || v3 == 255) + return false; + out.push_back((v1 << 2) | (v2 >> 4)); + out.push_back(((v2 & 0xf) << 4) | (v3 >> 2)); + break; + } + case 2: { + auto v1 = decode(in[i]); + auto v2 = decode(in[i + 1]); + if (v1 == 255 || v2 == 255) + return false; + out.push_back((v1 << 2) | (v2 >> 4)); + break; + } + } + return true; +} + +} // namespace base64 diff --git a/src/base64.hh b/src/base64.hh new file mode 100644 index 0000000..491755c --- /dev/null +++ b/src/base64.hh @@ -0,0 +1,25 @@ +#ifndef BASE64_HH +#define BASE64_HH + +#include <cstdint> +#include <optional> +#include <span> +#include <string> +#include <vector> + +namespace base64 { + +[[nodiscard]] +std::string encode(std::span<uint8_t const> data); + +void encode(std::span<uint8_t const> in, std::string& out); + +[[nodiscard]] +std::optional<std::vector<uint8_t>> decode(std::string_view value); + +[[nodiscard]] +bool decode(std::string_view in, std::vector<uint8_t>& out); + +} // namespace base64 + +#endif // BASE64_HH diff --git a/test/base64.cc b/test/base64.cc new file mode 100644 index 0000000..47cdfa4 --- /dev/null +++ b/test/base64.cc @@ -0,0 +1,70 @@ +#include "base64.hh" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <gtest/gtest.h> +#include <iterator> +#include <vector> + +TEST(Base64, empty) { + auto str = base64::encode({}); + EXPECT_EQ("", str); + + auto data = base64::decode(""); + ASSERT_TRUE(data.has_value()); + EXPECT_TRUE(data->empty()); +} + +TEST(Base64, all_bytes) { + std::vector<uint8_t> in; + in.push_back(0); + for (uint8_t i = 0; i < 255; ++i) { + in.push_back(i + 1); + } + auto str = base64::encode(in); + EXPECT_EQ( + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1" + "Njc4" + "OTo7PD0+" + "P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3Bx" + "cnN0dXZ3eHl6e3x9fn+" + "AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmq" + "q6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/" + "g4eLj" + "5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==", + str); + + auto data = base64::decode(str); + ASSERT_TRUE(data.has_value()); + EXPECT_EQ(in, data.value()); +} + +TEST(Base64, rfc) { + std::string_view in = "foobar"; + std::vector<std::string_view> expected{ + "", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy", + }; + for (size_t i = 0; i <= in.size(); ++i) { + auto in_part = in.substr(0, i); + std::vector<uint8_t> in_bytes; + std::ranges::copy(in_part, std::back_inserter(in_bytes)); + auto str = base64::encode(in_bytes); + EXPECT_EQ(expected[i], str); + auto data = base64::decode(str); + ASSERT_TRUE(data.has_value()); + EXPECT_EQ(in_bytes, data.value()); + } +} + +TEST(Base64, invalid) { + std::vector<uint8_t> out; + EXPECT_FALSE(base64::decode("=", out)); + EXPECT_FALSE(base64::decode("==", out)); + EXPECT_FALSE(base64::decode("===", out)); + EXPECT_FALSE(base64::decode("====", out)); + EXPECT_FALSE(base64::decode("<>", out)); + EXPECT_FALSE(base64::decode("Z", out)); + EXPECT_FALSE(base64::decode("Zg", out)); + EXPECT_FALSE(base64::decode("Zg=", out)); +} |
