Skip to content

Commit c6ccd05

Browse files
perf(util): encode Base64 in three-byte blocks (#1045)
1 parent 8698e93 commit c6ccd05

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

‎src/iceberg/test/base64_test.cc‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
#include "iceberg/util/base64.h"
2121

22+
#include <cstddef>
23+
#include <string>
24+
2225
#include <gtest/gtest.h>
2326

2427
#include "iceberg/test/matchers.h"
@@ -50,6 +53,27 @@ TEST(Base64Test, Encode) {
5053
EXPECT_EQ("AA==", Base64::Encode({"\x00", 1}));
5154
}
5255

56+
TEST(Base64Test, EncodeBinaryAcrossBlockBoundaries) {
57+
for (size_t length = 0; length <= 256; ++length) {
58+
std::string data(length, '\0');
59+
for (size_t i = 0; i < length; ++i) {
60+
data[i] = static_cast<char>((i * 73 + length) & 0xFF);
61+
}
62+
63+
const auto encoded = Base64::Encode(data);
64+
EXPECT_EQ(((length + 2) / 3) * 4, encoded.size()) << length;
65+
ICEBERG_UNWRAP_OR_FAIL(auto decoded, Base64::Decode(encoded));
66+
EXPECT_EQ(data, decoded) << length;
67+
68+
const auto url_encoded = Base64::UrlEncode(data);
69+
EXPECT_EQ((length / 3) * 4 + (length % 3 == 0 ? 0 : length % 3 + 1),
70+
url_encoded.size())
71+
<< length;
72+
ICEBERG_UNWRAP_OR_FAIL(auto url_decoded, Base64::UrlDecode(url_encoded));
73+
EXPECT_EQ(data, url_decoded) << length;
74+
}
75+
}
76+
5377
TEST(Base64Test, Decode) {
5478
// Empty string
5579
ICEBERG_UNWRAP_OR_FAIL(auto empty, Base64::Decode(""));

‎src/iceberg/util/base64.cc‎

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,40 @@ namespace {
3030
// When `pad` is true, the output is padded with '=' to a multiple of 4 characters.
3131
std::string Base64EncodeWithAlphabet(std::string_view input, std::string_view alphabet,
3232
bool pad) {
33-
std::string output;
34-
output.reserve((input.size() + 2) / 3 * 4);
33+
const size_t full_blocks = input.size() / 3;
34+
const size_t remainder = input.size() % 3;
35+
const size_t output_size =
36+
full_blocks * 4 + (remainder == 0 ? 0 : (pad ? 4 : remainder + 1));
37+
std::string output(output_size, '\0');
38+
39+
const auto* bytes = reinterpret_cast<const uint8_t*>(input.data());
40+
size_t out = 0;
41+
for (size_t i = 0; i < full_blocks * 3; i += 3) {
42+
const uint32_t block = (static_cast<uint32_t>(bytes[i]) << 16) |
43+
(static_cast<uint32_t>(bytes[i + 1]) << 8) |
44+
static_cast<uint32_t>(bytes[i + 2]);
45+
output[out++] = alphabet[(block >> 18) & 0x3F];
46+
output[out++] = alphabet[(block >> 12) & 0x3F];
47+
output[out++] = alphabet[(block >> 6) & 0x3F];
48+
output[out++] = alphabet[block & 0x3F];
49+
}
3550

36-
uint32_t buffer = 0;
37-
int bits_collected = 0;
38-
for (unsigned char byte : input) {
39-
buffer = (buffer << 8) | byte;
40-
bits_collected += 8;
41-
while (bits_collected >= 6) {
42-
bits_collected -= 6;
43-
output.push_back(alphabet[(buffer >> bits_collected) & 0x3F]);
51+
if (remainder > 0) {
52+
const size_t i = full_blocks * 3;
53+
uint32_t block = static_cast<uint32_t>(bytes[i]) << 16;
54+
if (remainder == 2) {
55+
block |= static_cast<uint32_t>(bytes[i + 1]) << 8;
4456
}
45-
}
46-
if (bits_collected > 0) {
47-
// Pad the remaining bits on the right to form the final 6-bit group.
48-
output.push_back(alphabet[(buffer << (6 - bits_collected)) & 0x3F]);
49-
}
50-
if (pad) {
51-
while (output.size() % 4 != 0) {
52-
output.push_back('=');
57+
output[out++] = alphabet[(block >> 18) & 0x3F];
58+
output[out++] = alphabet[(block >> 12) & 0x3F];
59+
if (remainder == 2) {
60+
output[out++] = alphabet[(block >> 6) & 0x3F];
61+
}
62+
while (out < output_size) {
63+
output[out++] = '=';
5364
}
5465
}
66+
5567
return output;
5668
}
5769

0 commit comments

Comments
 (0)