Skip to content

Commit 3d01d96

Browse files
Avoid reallocations in base64 encoding (#6951)
1 parent e8da3d9 commit 3d01d96

File tree

3 files changed

+111
-11
lines changed

3 files changed

+111
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- NodeJS:
2525
- CHANGED: Use node-api instead of NAN. [#6452](https://github.com/Project-OSRM/osrm-backend/pull/6452)
2626
- Misc:
27+
- CHANGED: Avoid reallocations in base64 encoding. [#6951](https://github.com/Project-OSRM/osrm-backend/pull/6951)
2728
- CHANGED: Get rid of unused Boost dependencies. [#6960](https://github.com/Project-OSRM/osrm-backend/pull/6960)
2829
- CHANGED: Apply micro-optimisation for Table & Trip APIs. [#6949](https://github.com/Project-OSRM/osrm-backend/pull/6949)
2930
- CHANGED: Apply micro-optimisation for Route API. [#6948](https://github.com/Project-OSRM/osrm-backend/pull/6948)

include/engine/base64.hpp

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,29 @@ namespace engine
4747
// Encodes a chunk of memory to Base64.
4848
inline std::string encodeBase64(const unsigned char *first, std::size_t size)
4949
{
50-
std::vector<unsigned char> bytes{first, first + size};
51-
BOOST_ASSERT(!bytes.empty());
50+
BOOST_ASSERT(size > 0);
5251

53-
std::size_t bytes_to_pad{0};
52+
std::string encoded;
53+
encoded.reserve(((size + 2) / 3) * 4);
5454

55-
while (bytes.size() % 3 != 0)
55+
auto padding = (3 - size % 3) % 3;
56+
57+
BOOST_ASSERT(padding == 0 || padding == 1 || padding == 2);
58+
59+
for (auto itr = detail::Base64FromBinary(first); itr != detail::Base64FromBinary(first + size);
60+
++itr)
5661
{
57-
bytes_to_pad += 1;
58-
bytes.push_back(0);
62+
encoded.push_back(*itr);
5963
}
6064

61-
BOOST_ASSERT(bytes_to_pad == 0 || bytes_to_pad == 1 || bytes_to_pad == 2);
62-
BOOST_ASSERT_MSG(0 == bytes.size() % 3, "base64 input data size is not a multiple of 3");
65+
for (size_t index = 0; index < padding; ++index)
66+
{
67+
encoded.push_back('=');
68+
}
6369

64-
std::string encoded{detail::Base64FromBinary{bytes.data()},
65-
detail::Base64FromBinary{bytes.data() + (bytes.size() - bytes_to_pad)}};
70+
BOOST_ASSERT(encoded.size() == (size + 2) / 3 * 4);
6671

67-
return encoded.append(bytes_to_pad, '=');
72+
return encoded;
6873
}
6974

7075
// C++11 standard 3.9.1/1: Plain char, signed char, and unsigned char are three distinct types

unit_tests/engine/base64.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,98 @@ BOOST_AUTO_TEST_CASE(hint_encoding_decoding_roundtrip_bytewise)
7474
reinterpret_cast<const unsigned char *>(&decoded)));
7575
}
7676

77+
BOOST_AUTO_TEST_CASE(long_string_encoding)
78+
{
79+
using namespace osrm::engine;
80+
std::string long_string(1000, 'A'); // String of 1000 'A's
81+
std::string encoded = encodeBase64(long_string);
82+
BOOST_CHECK_EQUAL(decodeBase64(encoded), long_string);
83+
}
84+
85+
BOOST_AUTO_TEST_CASE(invalid_base64_decoding)
86+
{
87+
using namespace osrm::engine;
88+
BOOST_CHECK_THROW(decodeBase64("Invalid!"), std::exception);
89+
}
90+
91+
BOOST_AUTO_TEST_CASE(hint_serialization_size)
92+
{
93+
using namespace osrm::engine;
94+
using namespace osrm::util;
95+
96+
const Coordinate coordinate;
97+
const PhantomNode phantom;
98+
const osrm::test::MockDataFacade<osrm::engine::routing_algorithms::ch::Algorithm> facade{};
99+
100+
const SegmentHint hint{phantom, facade.GetCheckSum()};
101+
const auto base64 = hint.ToBase64();
102+
103+
BOOST_CHECK_EQUAL(base64.size(), 112);
104+
}
105+
106+
BOOST_AUTO_TEST_CASE(extended_roundtrip_tests)
107+
{
108+
using namespace osrm::engine;
109+
110+
std::vector<std::string> test_strings = {
111+
"Hello, World!", // Simple ASCII string
112+
"1234567890", // Numeric string
113+
"!@#$%^&*()_+", // Special characters
114+
std::string(1000, 'A'), // Long repeating string
115+
"¡Hola, mundo!", // Non-ASCII characters
116+
"こんにちは、世界!", // Unicode characters
117+
std::string("\x00\x01\x02\x03", 4), // Binary data
118+
"a", // Single character
119+
"ab", // Two characters
120+
"abc", // Three characters (no padding in Base64)
121+
std::string(190, 'x') // String that doesn't align with Base64 padding
122+
};
123+
124+
for (const auto &test_str : test_strings)
125+
{
126+
std::string encoded = encodeBase64(test_str);
127+
std::string decoded = decodeBase64(encoded);
128+
BOOST_CHECK_EQUAL(decoded, test_str);
129+
130+
// Additional checks
131+
BOOST_CHECK(encoded.find_first_not_of(
132+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=") ==
133+
std::string::npos);
134+
if (test_str.length() % 3 != 0)
135+
{
136+
BOOST_CHECK(encoded.back() == '=');
137+
}
138+
}
139+
}
140+
141+
BOOST_AUTO_TEST_CASE(roundtrip_with_url_safe_chars)
142+
{
143+
using namespace osrm::engine;
144+
145+
std::string original = "Hello+World/Nothing?Is:Impossible";
146+
std::string encoded = encodeBase64(original);
147+
148+
// Replace '+' with '-' and '/' with '_'
149+
std::replace(encoded.begin(), encoded.end(), '+', '-');
150+
std::replace(encoded.begin(), encoded.end(), '/', '_');
151+
152+
std::string decoded = decodeBase64(encoded);
153+
BOOST_CHECK_EQUAL(decoded, original);
154+
}
155+
156+
BOOST_AUTO_TEST_CASE(roundtrip_stress_test)
157+
{
158+
using namespace osrm::engine;
159+
160+
std::string test_str;
161+
for (int i = 0; i < 1000; ++i)
162+
{
163+
test_str += static_cast<char>(i % 256);
164+
}
165+
166+
std::string encoded = encodeBase64(test_str);
167+
std::string decoded = decodeBase64(encoded);
168+
BOOST_CHECK_EQUAL(decoded, test_str);
169+
}
170+
77171
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)