summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--meson.build22
-rw-r--r--src/base64.cc142
-rw-r--r--src/base64.hh25
-rw-r--r--test/base64.cc70
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));
+}