Skip to content

Commit

Permalink
Switch to our own Base64 implementation
Browse files Browse the repository at this point in the history
Written in modern C++, it strictly adheres to the Base64 specification.

This means that the lack of padding is rejected when decoding.
The alternate alphabet that is used for URLs is handled just fine.

A benchmark would be nice to have, once we switch to a proper test framework.
  • Loading branch information
davidebeatrici committed Mar 24, 2023
1 parent 85f4b88 commit cceeee2
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 154 deletions.
151 changes: 140 additions & 11 deletions include/mumble/Base64.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,159 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

// Inspired by https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp.

#ifndef MUMBLE_BASE64_HPP
#define MUMBLE_BASE64_HPP

#include "Macros.hpp"
#include "NonCopyable.hpp"
#include "Types.hpp"

#include <memory>
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>

#include <gsl/span>

static constexpr std::byte operator+(const std::byte lhs, const std::byte rhs) {
const auto ret = std::to_integer< uint8_t >(lhs) + std::to_integer< uint8_t >(rhs);
return std::byte(ret);
}

// The standard doesn't overload this operator with both operands as std::byte...
static constexpr std::byte operator<<(const std::byte lhs, const std::byte rhs) {
return lhs << std::to_integer< uint8_t >(rhs);
}

namespace mumble {
class MUMBLE_EXPORT Base64 : NonCopyable {
class Base64 {
public:
class P;
using StrView = gsl::span< char >;
using StrViewConst = gsl::span< const char >;

static constexpr size_t decode(BufView out, StrViewConst in) {
if (in.empty()) {
return 0;
}

if (in.size() % 4) {
throw std::invalid_argument("Input is not a multiple of 4!");
}

const size_t size = decodedSize(in);
if (out.size() < size) {
throw std::invalid_argument("Insufficient output buffer size!");
}

for (; !in.empty(); out = out.subspan(3), in = in.subspan(4)) {
out[0] = (charToByte(in[0]) << std::byte(2)) + ((charToByte(in[1]) & std::byte(0x30)) >> 4);

if (in[2] == '=') {
break;
}

out[1] =
((charToByte(in[1]) & std::byte(0x0f)) << std::byte(4)) + ((charToByte(in[2]) & std::byte(0x3c)) >> 2);

if (in[3] == '=') {
break;
}

out[2] = ((charToByte(in[2]) & std::byte(0x03)) << std::byte(6)) + charToByte(in[3]);
}

return size;
}

Base64();
virtual ~Base64();
static constexpr size_t encode(StrView out, BufViewConst in) {
if (in.empty()) {
return 0;
}

virtual explicit operator bool();
const size_t size = encodedSize(in);
if (out.size() < size) {
throw std::invalid_argument("Insufficient output buffer size!");
}

virtual size_t decode(const BufView out, const BufViewConst in);
static size_t encode(const BufView out, const BufViewConst in);
for (; !in.empty(); out = out.subspan(4)) {
out[0] = byteToChar((in[0] & std::byte(0xfc)) >> 2);

if (in.size() >= 2) {
out[1] = byteToChar(((in[0] & std::byte(0x03)) << 4) + ((in[1] & std::byte(0xf0)) >> 4));

if (in.size() >= 3) {
out[2] = byteToChar(((in[1] & std::byte(0x0f)) << 2) + ((in[2] & std::byte(0xc0)) >> 6));
out[3] = byteToChar(in[2] & std::byte(0x3f));
} else {
out[2] = byteToChar((in[1] & std::byte(0x0f)) << 2);
out[3] = PAD_CHAR;
}
} else {
out[1] = byteToChar((in[0] & std::byte(0x03)) << 4);
out[2] = PAD_CHAR;
out[3] = PAD_CHAR;
}

// All data blocks are guaranteed to be 3 bytes except the last one.
in = in.subspan(std::min(size_t{ 3 }, in.size()));
}

return size;
}

static constexpr size_t decodedSize(const StrViewConst in) {
size_t size = (in.size() / 4) * 3;
// Base64 can have up to 2 pad characters, in order to make the string a multiple of 4.
// In order to calculate the exact output size we have to ignore padding.
if (auto iter = in.rbegin(); *iter == PAD_CHAR) {
size -= *++iter == PAD_CHAR ? 2U : 1U;
}

return size;
}

static constexpr size_t encodedSize(const BufViewConst in) {
// ceil((float) a / 3) == (a + 2) / 3, assuming a is an integer.
return ((in.size() + 2) / 3) * 4;
}

private:
std::unique_ptr< P > m_p;
using ByteView = gsl::span< uint8_t >;
using ByteViewConst = gsl::span< const uint8_t >;

static constexpr std::byte charToByte(const char ch) {
if (ch >= 'A' && ch <= 'Z')
return std::byte(ch - 'A');
if (ch >= 'a' && ch <= 'z')
return std::byte(ch - 'a' + ('Z' - 'A') + 1);
if (ch >= '0' && ch <= '9')
return std::byte(ch - '0' + ('Z' - 'A') + ('z' - 'a') + 2);
if (ch == '+' || ch == '-')
return std::byte(62);
if (ch == '/' || ch == '_')
return std::byte(63);

throw std::range_error("");
}

static constexpr char byteToChar(const std::byte byte) {
const auto num = std::to_integer< char >(byte);

if (num <= 25)
return num + 'A';
if (num >= 26 && num <= 51)
return num + 'a' - ('Z' - 'A') - 1;
if (num >= 52 && num <= 61)
return num + '0' - ('Z' - 'A') - ('z' - 'a') - 2;
if (num == 62)
return '+';
if (num == 63)
return '/';

throw std::range_error("");
}

static constexpr char PAD_CHAR = '=';
};
} // namespace mumble

Expand Down
98 changes: 0 additions & 98 deletions src/Base64.cpp

This file was deleted.

26 changes: 0 additions & 26 deletions src/Base64.hpp

This file was deleted.

2 changes: 0 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ target_include_directories(mumble_library

target_sources(mumble_library
PRIVATE
"Base64.cpp"
"Base64.hpp"
"Cert.cpp"
"Cert.hpp"
"Connection.cpp"
Expand Down
24 changes: 7 additions & 17 deletions tests/TestBase64/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,45 +23,35 @@ static constexpr size_t iterations = 1000000;

using namespace mumble;

static std::string decode(Base64 &base64, const Data::Entry &entry) {
const BufViewConst in(reinterpret_cast< const std::byte * >(entry.second.data()), entry.second.size());

static std::string decode(const Data::Entry &entry) {
std::string ret;
ret.resize(base64.decode({}, in));
ret.resize(Base64::decodedSize(entry.second));

const BufView out(reinterpret_cast< std::byte * >(ret.data()), ret.size());

const auto written = base64.decode(out, in);
const auto written = Base64::decode(out, entry.second);
if (!written) {
return {};
}

ret.resize(written);

return ret;
}

static std::string encode(const Data::Entry &entry) {
const BufViewConst in(reinterpret_cast< const std::byte * >(entry.first.data()), entry.first.size());

std::string ret;
ret.resize(Base64::encode({}, in) - 1);

const BufView out(reinterpret_cast< std::byte * >(ret.data()), ret.size());
ret.resize(Base64::encodedSize(in));

const auto written = Base64::encode(out, in);
const auto written = Base64::encode(ret, in);
if (!written) {
return {};
}

ret.resize(written - 1);

return ret;
}

static uint8_t thread() {
Base64 base64;

std::random_device device;
std::mt19937 algorithm(device());
std::uniform_int_distribution< size_t > gen(0, std::tuple_size< Data::Table >() - 1);
Expand All @@ -73,7 +63,7 @@ static uint8_t thread() {

auto entry = Data::ascii[gen(algorithm)];

auto decoded = decode(base64, entry);
auto decoded = decode(entry);
if (decoded != entry.first) {
return 1;
}
Expand All @@ -85,7 +75,7 @@ static uint8_t thread() {

entry = Data::unicode[gen(algorithm)];

decoded = decode(base64, entry);
decoded = decode(entry);
if (decoded != entry.first) {
return 3;
}
Expand Down

0 comments on commit cceeee2

Please sign in to comment.