Skip to content

Commit 3828dc9

Browse files
Switch to our own Base64 implementation
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.
1 parent 5fa94ba commit 3828dc9

5 files changed

Lines changed: 165 additions & 157 deletions

File tree

include/mumble/Base64.hpp

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,175 @@
33
// that can be found in the LICENSE file at the root of the
44
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
55

6+
// Inspired by https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp.
7+
68
#ifndef MUMBLE_BASE64_HPP
79
#define MUMBLE_BASE64_HPP
810

9-
#include "Macros.hpp"
10-
#include "NonCopyable.hpp"
1111
#include "Types.hpp"
1212

13-
#include <memory>
13+
#include <array>
14+
#include <cmath>
15+
#include <limits>
16+
#include <stdexcept>
17+
18+
#include <gsl/span>
1419

1520
namespace mumble {
16-
class MUMBLE_EXPORT Base64 : NonCopyable {
17-
public:
18-
class P;
21+
namespace base64 {
22+
using StrView = gsl::span< char >;
23+
using StrViewConst = gsl::span< const char >;
24+
25+
static constexpr char PAD_CHAR = '=';
26+
27+
static constexpr std::byte operator+(const std::byte lhs, const std::byte rhs) {
28+
const auto ret = std::to_integer< uint8_t >(lhs) + std::to_integer< uint8_t >(rhs);
29+
return std::byte(ret);
30+
}
31+
32+
// The standard doesn't overload this operator with both operands as std::byte...
33+
static constexpr std::byte operator<<(const std::byte lhs, const std::byte rhs) {
34+
return lhs << std::to_integer< uint8_t >(rhs);
35+
}
36+
37+
static constexpr std::byte charToByte(const char ch) {
38+
if (ch >= 'A' && ch <= 'Z')
39+
return std::byte(ch - 'A');
40+
if (ch >= 'a' && ch <= 'z')
41+
return std::byte(ch - 'a' + ('Z' - 'A') + 1);
42+
if (ch >= '0' && ch <= '9')
43+
return std::byte(ch - '0' + ('Z' - 'A') + ('z' - 'a') + 2);
44+
if (ch == '+' || ch == '-')
45+
return std::byte(62);
46+
if (ch == '/' || ch == '_')
47+
return std::byte(63);
48+
49+
throw std::range_error(std::string() + ch + " is not a valid character!");
50+
}
51+
52+
static constexpr char byteToChar(const std::byte byte) {
53+
const auto num = std::to_integer< char >(byte);
54+
55+
if (num <= 25)
56+
return num + 'A';
57+
if (num >= 26 && num <= 51)
58+
return num + 'a' - ('Z' - 'A') - 1;
59+
if (num >= 52 && num <= 61)
60+
return num + '0' - ('Z' - 'A') - ('z' - 'a') - 2;
61+
if (num == 62)
62+
return '+';
63+
if (num == 63)
64+
return '/';
65+
66+
throw std::range_error(std::string() + num + " is not a valid byte!");
67+
}
68+
69+
static inline bool isValid(const char ch) {
70+
try {
71+
charToByte(ch);
72+
} catch (const std::range_error &) {
73+
return false;
74+
}
75+
76+
return true;
77+
}
78+
79+
static inline bool isValid(const StrViewConst in) {
80+
for (const auto ch : in) {
81+
if (!isValid(ch)) {
82+
return false;
83+
}
84+
}
85+
86+
return true;
87+
}
88+
89+
static constexpr size_t decodedSize(const StrViewConst in) {
90+
size_t size = (in.size() / 4) * 3;
91+
// Base64 can have up to 2 pad characters, in order to make the string a multiple of 4.
92+
// In order to calculate the exact output size we have to ignore padding.
93+
if (auto iter = in.rbegin(); *iter == PAD_CHAR) {
94+
size -= *++iter == PAD_CHAR ? 2U : 1U;
95+
}
96+
97+
return size;
98+
}
99+
100+
static constexpr size_t encodedSize(const BufViewConst in) {
101+
// ceil((float) a / 3) == (a + 2) / 3, assuming a is an integer.
102+
return ((in.size() + 2) / 3) * 4;
103+
}
104+
105+
static constexpr size_t decode(BufView out, StrViewConst in) {
106+
if (in.empty()) {
107+
return 0;
108+
}
109+
110+
if (in.size() % 4) {
111+
throw std::invalid_argument("Input is not a multiple of 4!");
112+
}
113+
114+
const size_t size = decodedSize(in);
115+
if (out.size() < size) {
116+
throw std::invalid_argument("Insufficient output buffer size!");
117+
}
118+
119+
for (; !in.empty(); out = out.subspan(3), in = in.subspan(4)) {
120+
out[0] = (charToByte(in[0]) << std::byte(2)) + ((charToByte(in[1]) & std::byte(0x30)) >> 4);
121+
122+
if (in[2] == '=') {
123+
break;
124+
}
125+
126+
out[1] =
127+
((charToByte(in[1]) & std::byte(0x0f)) << std::byte(4)) + ((charToByte(in[2]) & std::byte(0x3c)) >> 2);
128+
129+
if (in[3] == '=') {
130+
break;
131+
}
132+
133+
out[2] = ((charToByte(in[2]) & std::byte(0x03)) << std::byte(6)) + charToByte(in[3]);
134+
}
135+
136+
return size;
137+
}
138+
139+
static constexpr size_t encode(StrView out, BufViewConst in) {
140+
if (in.empty()) {
141+
return 0;
142+
}
143+
144+
const size_t size = encodedSize(in);
145+
if (out.size() < size) {
146+
throw std::invalid_argument("Insufficient output buffer size!");
147+
}
148+
149+
for (; !in.empty(); out = out.subspan(4)) {
150+
out[0] = byteToChar((in[0] & std::byte(0xfc)) >> 2);
19151

20-
Base64();
21-
virtual ~Base64();
152+
if (in.size() >= 2) {
153+
out[1] = byteToChar(((in[0] & std::byte(0x03)) << 4) + ((in[1] & std::byte(0xf0)) >> 4));
22154

23-
virtual explicit operator bool();
155+
if (in.size() >= 3) {
156+
out[2] = byteToChar(((in[1] & std::byte(0x0f)) << 2) + ((in[2] & std::byte(0xc0)) >> 6));
157+
out[3] = byteToChar(in[2] & std::byte(0x3f));
158+
} else {
159+
out[2] = byteToChar((in[1] & std::byte(0x0f)) << 2);
160+
out[3] = PAD_CHAR;
161+
}
162+
} else {
163+
out[1] = byteToChar((in[0] & std::byte(0x03)) << 4);
164+
out[2] = PAD_CHAR;
165+
out[3] = PAD_CHAR;
166+
}
24167

25-
virtual size_t decode(const BufView out, const BufViewConst in);
26-
static size_t encode(const BufView out, const BufViewConst in);
168+
// All data blocks are guaranteed to be 3 bytes except the last one.
169+
in = in.subspan(std::min(size_t{ 3 }, in.size()));
170+
}
27171

28-
private:
29-
std::unique_ptr< P > m_p;
30-
};
172+
return size;
173+
}
174+
}; // namespace base64
31175
} // namespace mumble
32176

33177
#endif

src/Base64.cpp

Lines changed: 0 additions & 98 deletions
This file was deleted.

src/Base64.hpp

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ target_include_directories(mumble_library
5757

5858
target_sources(mumble_library
5959
PRIVATE
60-
"Base64.cpp"
61-
"Base64.hpp"
6260
"Cert.cpp"
6361
"Cert.hpp"
6462
"Connection.cpp"

tests/TestBase64/main.cpp

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,45 +23,35 @@ static constexpr size_t iterations = 1000000;
2323

2424
using namespace mumble;
2525

26-
static std::string decode(Base64 &base64, const Data::Entry &entry) {
27-
const BufViewConst in(reinterpret_cast< const std::byte * >(entry.second.data()), entry.second.size());
28-
26+
static std::string decode(const Data::Entry &entry) {
2927
std::string ret;
30-
ret.resize(base64.decode({}, in));
28+
ret.resize(base64::decodedSize(entry.second));
3129

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

34-
const auto written = base64.decode(out, in);
32+
const auto written = base64::decode(out, entry.second);
3533
if (!written) {
3634
return {};
3735
}
3836

39-
ret.resize(written);
40-
4137
return ret;
4238
}
4339

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

4743
std::string ret;
48-
ret.resize(Base64::encode({}, in) - 1);
49-
50-
const BufView out(reinterpret_cast< std::byte * >(ret.data()), ret.size());
44+
ret.resize(base64::encodedSize(in));
5145

52-
const auto written = Base64::encode(out, in);
46+
const auto written = base64::encode(ret, in);
5347
if (!written) {
5448
return {};
5549
}
5650

57-
ret.resize(written - 1);
58-
5951
return ret;
6052
}
6153

6254
static uint8_t thread() {
63-
Base64 base64;
64-
6555
std::random_device device;
6656
std::mt19937 algorithm(device());
6757
std::uniform_int_distribution< size_t > gen(0, std::tuple_size< Data::Table >() - 1);
@@ -73,7 +63,7 @@ static uint8_t thread() {
7363

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

76-
auto decoded = decode(base64, entry);
66+
auto decoded = decode(entry);
7767
if (decoded != entry.first) {
7868
return 1;
7969
}
@@ -85,7 +75,7 @@ static uint8_t thread() {
8575

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

88-
decoded = decode(base64, entry);
78+
decoded = decode(entry);
8979
if (decoded != entry.first) {
9080
return 3;
9181
}

0 commit comments

Comments
 (0)