Skip to content

Commit cb62128

Browse files
committed
Merge branch 'tickets/DM-44281'
2 parents 5d27f05 + 5a6b6ba commit cb62128

12 files changed

+166
-8
lines changed

src/admin/python/lsst/qserv/admin/replicationInterface.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def __init__(
201201
self.repl_ctrl = urlparse(repl_ctrl_uri)
202202
self.auth_key = auth_key
203203
self.admin_auth_key = admin_auth_key
204-
self.repl_api_version = 34
204+
self.repl_api_version = 35
205205
_log.debug(f"ReplicationInterface %s", self.repl_ctrl)
206206

207207
def version(self) -> str:

src/czar/HttpCzarIngestModule.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ json HttpCzarIngestModule::executeImpl(string const& subModuleName) {
139139

140140
json HttpCzarIngestModule::_ingestData() {
141141
debug(__func__);
142-
checkApiVersion(__func__, 34);
142+
checkApiVersion(__func__, 35);
143143

144144
auto const databaseName = body().required<string>("database");
145145
auto const tableName = body().required<string>("table");

src/czar/HttpCzarQueryModule.cc

+5-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ json HttpCzarQueryModule::executeImpl(string const& subModuleName) {
8181

8282
json HttpCzarQueryModule::_submit() {
8383
debug(__func__);
84-
checkApiVersion(__func__, 33);
84+
checkApiVersion(__func__, 35);
8585

8686
string const binaryEncodingStr = body().optional<string>("binary_encoding", "hex");
8787
http::BinaryEncodingMode const binaryEncoding = http::parseBinaryEncoding(binaryEncodingStr);
@@ -138,7 +138,7 @@ json HttpCzarQueryModule::_status() {
138138

139139
json HttpCzarQueryModule::_result() {
140140
debug(__func__);
141-
checkApiVersion(__func__, 33);
141+
checkApiVersion(__func__, 35);
142142
string const binaryEncodingStr = query().optionalString("binary_encoding", "hex");
143143
http::BinaryEncodingMode const binaryEncoding = http::parseBinaryEncoding(binaryEncodingStr);
144144
debug(__func__, "binary_encoding=" + http::binaryEncoding2string(binaryEncoding));
@@ -302,6 +302,9 @@ json HttpCzarQueryModule::_rowsToJson(sql::SqlResults& results, json const& sche
302302
case http::BinaryEncodingMode::HEX:
303303
rowJson.push_back(util::String::toHex(row[i].first, row[i].second));
304304
break;
305+
case http::BinaryEncodingMode::B64:
306+
rowJson.push_back(util::String::toBase64(row[i].first, row[i].second));
307+
break;
305308
case http::BinaryEncodingMode::ARRAY:
306309
// Notes on the std::u8string type and constructor:
307310
// 1. This string type is required for encoding binary data which is only possible

src/http/BinaryEncoding.cc

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ namespace lsst::qserv::http {
3232
BinaryEncodingMode parseBinaryEncoding(string const& str) {
3333
if (str == "hex")
3434
return BinaryEncodingMode::HEX;
35+
else if (str == "b64")
36+
return BinaryEncodingMode::B64;
3537
else if (str == "array")
3638
return BinaryEncodingMode::ARRAY;
3739
throw invalid_argument("http::" + string(__func__) + " unsupported mode '" + str + "'");
@@ -41,6 +43,8 @@ string binaryEncoding2string(BinaryEncodingMode mode) {
4143
switch (mode) {
4244
case BinaryEncodingMode::HEX:
4345
return "hex";
46+
case BinaryEncodingMode::B64:
47+
return "b64";
4448
case BinaryEncodingMode::ARRAY:
4549
return "array";
4650
}

src/http/BinaryEncoding.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929
namespace lsst::qserv::http {
3030

3131
/// The names of the allowed modes.
32-
static std::vector<std::string> const allowedBinaryEncodingModes = {"hex", "array"};
32+
static std::vector<std::string> const allowedBinaryEncodingModes = {"hex", "b64", "array"};
3333

3434
/// Options for encoding data of the binary columns in the JSON result.
3535
enum class BinaryEncodingMode : int {
3636
HEX, ///< The hexadecimal representation stored as a string
37+
B64, ///< Data encoded using Base64 algorithm (with padding as needed)
3738
ARRAY ///< JSON array of 8-bit unsigned integers in a range of 0 .. 255.
3839
};
3940

src/http/MetaModule.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ string const adminAuthKey;
3737

3838
namespace lsst::qserv::http {
3939

40-
unsigned int const MetaModule::version = 34;
40+
unsigned int const MetaModule::version = 35;
4141

4242
void MetaModule::process(string const& context, nlohmann::json const& info, qhttp::Request::Ptr const& req,
4343
qhttp::Response::Ptr const& resp, string const& subModuleName) {

src/replica/ingest/IngestDataHttpSvcMod.cc

+26-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ json IngestDataHttpSvcMod::executeImpl(string const& subModuleName) {
9292

9393
json IngestDataHttpSvcMod::_syncProcessData() {
9494
debug(__func__);
95-
checkApiVersion(__func__, 34);
95+
checkApiVersion(__func__, 35);
9696

9797
auto const context_ = context() + __func__;
9898
auto const config = serviceProvider()->config();
@@ -258,6 +258,9 @@ json IngestDataHttpSvcMod::_syncProcessData() {
258258
case http::BinaryEncodingMode::HEX:
259259
row.append(_translateHexString(context_, jsonColumn, rowIdx, colIdx));
260260
break;
261+
case http::BinaryEncodingMode::B64:
262+
row.append(_translateBase64String(context_, jsonColumn, rowIdx, colIdx));
263+
break;
261264
case http::BinaryEncodingMode::ARRAY: {
262265
u8string const str = _translateByteArray(context_, jsonColumn, rowIdx, colIdx);
263266
row.append(reinterpret_cast<char const*>(str.data()), str.size());
@@ -333,6 +336,28 @@ string IngestDataHttpSvcMod::_translateHexString(string const& context_, json co
333336
throw http::Error(context_, _contrib.error);
334337
}
335338

339+
string IngestDataHttpSvcMod::_translateBase64String(string const& context_, json const& jsonColumn,
340+
size_t rowIdx, size_t colIdx) {
341+
if (jsonColumn.is_string()) {
342+
try {
343+
return util::String::fromBase64(jsonColumn.get<string>());
344+
} catch (exception const& ex) {
345+
_contrib.error = "failed to decode a value of the '" +
346+
http::binaryEncoding2string(http::BinaryEncodingMode::B64) +
347+
"' binary encoded column at row " + to_string(rowIdx) + " and column " +
348+
to_string(colIdx) + ", ex: " + string(ex.what());
349+
}
350+
} else {
351+
_contrib.error = "unsupported type name '" + string(jsonColumn.type_name()) + "' found at row " +
352+
to_string(rowIdx) + " and column " + to_string(colIdx) +
353+
" where the string type was expected";
354+
}
355+
bool const failed = true;
356+
_contrib = serviceProvider()->databaseServices()->startedTransactionContrib(_contrib, failed);
357+
_failed(context_);
358+
throw http::Error(context_, _contrib.error);
359+
}
360+
336361
u8string IngestDataHttpSvcMod::_translateByteArray(string const& context_, json const& jsonColumn,
337362
size_t rowIdx, size_t colIdx) {
338363
if (jsonColumn.is_array()) {

src/replica/ingest/IngestDataHttpSvcMod.h

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class IngestDataHttpSvcMod : public http::ModuleBase, public IngestFileSvc {
101101

102102
std::string _translateHexString(std::string const& context_, nlohmann::json const& jsonColumn,
103103
size_t rowIdx, size_t colIdx);
104+
std::string _translateBase64String(std::string const& context_, nlohmann::json const& jsonColumn,
105+
size_t rowIdx, size_t colIdx);
104106
std::u8string _translateByteArray(std::string const& context_, nlohmann::json const& jsonColumn,
105107
size_t rowIdx, size_t colIdx);
106108
std::string _translatePrimitiveType(std::string const& context_, nlohmann::json const& jsonColumn,

src/util/String.cc

+46
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,18 @@
3030
#include <functional>
3131
#include <stdexcept>
3232

33+
// Third party headers
34+
#include <boost/algorithm/string.hpp>
35+
#include <boost/archive/iterators/transform_width.hpp>
36+
#include <boost/archive/iterators/base64_from_binary.hpp>
37+
#include <boost/archive/iterators/binary_from_base64.hpp>
38+
3339
// LSST headers
3440
#include "lsst/log/Log.h"
3541

3642
using namespace std;
43+
using namespace boost::algorithm;
44+
using namespace boost::archive::iterators;
3745

3846
#define CONTEXT_(func) ("String::" + string(func) + " ")
3947

@@ -185,4 +193,42 @@ string String::toUpper(string const& str) {
185193
return result;
186194
}
187195

196+
string String::toBase64(char const* ptr, size_t length) {
197+
if (ptr == nullptr) {
198+
throw invalid_argument(CONTEXT_(__func__) + "sequnce pointer is nullptr");
199+
}
200+
if (length == 0) return string();
201+
202+
size_t const padding = (3 - length % 3) % 3; // calculate padding size
203+
size_t const encodedLength = (length + padding) * 4 / 3; // calculate encoded length
204+
205+
string encoded;
206+
encoded.reserve(encodedLength);
207+
208+
// Append base64 characters to result string.
209+
typedef base64_from_binary<transform_width<const char*, 6, 8>> base64_iterator;
210+
for (base64_iterator itr(ptr), end(ptr + length); itr != end; ++itr) {
211+
encoded.push_back(*itr);
212+
}
213+
214+
// Add padding characters if necessary.
215+
for (size_t i = 0; i < padding; ++i) {
216+
encoded.push_back('=');
217+
}
218+
return encoded;
219+
}
220+
221+
string String::fromBase64(string const& str) {
222+
if (str.empty()) return string();
223+
string decoded;
224+
try {
225+
typedef transform_width<binary_from_base64<string::const_iterator>, 8, 6> base64_decoder;
226+
decoded = trim_right_copy_if(string(base64_decoder(str.begin()), base64_decoder(str.end())),
227+
[](char c) { return c == '\0'; });
228+
} catch (exception const& ex) {
229+
throw range_error(CONTEXT_(__func__) + "failed to decode base64 string: " + ex.what());
230+
}
231+
return decoded;
232+
}
233+
188234
} // namespace lsst::qserv::util

src/util/String.h

+30
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,36 @@ class String {
158158
/// @param str A string to be translated
159159
/// @return The string with all characters converted to upper case.
160160
static std::string toUpper(std::string const& str);
161+
162+
/**
163+
* Encode the input sequence of bytes into the Base64 representation packaged
164+
* into a string with ('=') padding as needed.
165+
*
166+
* For example, the method will convert a sequence of characters as shown below:
167+
* @code
168+
* "0123456789" -> "MDEyMzQ1Njc4OQ=="
169+
* @endcode
170+
* @param ptr A pointer to the byte sequence.
171+
* @param length The number of bytes to translate.
172+
* @return The encoded sequence of bytes or the empty string if the length=0.
173+
* @throw std::invalid_argument If the pointer is nullptr.
174+
*/
175+
static std::string toBase64(char const* ptr, std::size_t length);
176+
static std::string toBase64(std::string const& str) { return toBase64(str.data(), str.size()); }
177+
178+
/**
179+
* Decode the Base64-encoded (padded with '=' as needed) string into the binary string.
180+
*
181+
* For example, the method will decode the encoded Base64 string as shown below:
182+
* @code
183+
* "MDEyMzQ1Njc4OQ==" -> "0123456789"
184+
* @endcode
185+
*
186+
* @param str The string to be decoded.
187+
* @return The decoded sequence of bytes or the empty string if the input is emoty.
188+
* @throw std::range_error For non-base64 characters in the input.
189+
*/
190+
static std::string fromBase64(std::string const& str);
161191
};
162192

163193
} // namespace lsst::qserv::util

src/util/testString.cc

+47
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <stdexcept>
2626
#include <string>
2727
#include <vector>
28+
#include <unordered_map>
2829

2930
// LSST headers
3031
#include "lsst/log/Log.h"
@@ -79,7 +80,22 @@ std::vector<std::string> const char2hex_lower = {
7980
"d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df",
8081
"e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef",
8182
"f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"};
83+
84+
std::unordered_map<std::string, std::string> const str2base64 = {
85+
{"0", "MA=="},
86+
{"01", "MDE="},
87+
{"012", "MDEy"},
88+
{"0123", "MDEyMw=="},
89+
{"01234", "MDEyMzQ="},
90+
{"012345", "MDEyMzQ1"},
91+
{"0123456", "MDEyMzQ1Ng=="},
92+
{"01234567", "MDEyMzQ1Njc="},
93+
{"012345678", "MDEyMzQ1Njc4"},
94+
{"0123456789", "MDEyMzQ1Njc4OQ=="},
95+
{"!@#$$\%\%^^&&**(())_)(**&&&", "IUAjJCQlJV5eJiYqKigoKSlfKSgqKiYmJg=="}};
96+
8297
} // namespace
98+
8399
BOOST_AUTO_TEST_SUITE(Suite)
84100

85101
BOOST_AUTO_TEST_CASE(SplitStringTest) {
@@ -425,4 +441,35 @@ BOOST_AUTO_TEST_CASE(StringCaseTranslationTest) {
425441
BOOST_CHECK_EQUAL(util::String::toUpper("Mixed_Case"), "MIXED_CASE");
426442
}
427443

444+
BOOST_AUTO_TEST_CASE(ToBase64Test) {
445+
LOGS_INFO("ToBase64Test test begins");
446+
447+
// Null pointer is treated as an illegal input.
448+
BOOST_CHECK_THROW(util::String::toBase64(nullptr, 0), std::invalid_argument);
449+
450+
// This test ensures that the empty string is always returned for the empty
451+
// input regardleass.
452+
char const empty[] = "";
453+
BOOST_CHECK_EQUAL(util::String::toBase64(empty, 0), std::string());
454+
455+
for (auto const& [str, b64] : ::str2base64) {
456+
BOOST_CHECK_EQUAL(util::String::toBase64(str), b64);
457+
}
458+
}
459+
460+
BOOST_AUTO_TEST_CASE(FromBase64Test) {
461+
LOGS_INFO("FromBase64Test test begins");
462+
463+
// Make sure the result is empty if no input beyond the optional
464+
// prefix is present.
465+
std::string const empty;
466+
BOOST_CHECK_EQUAL(util::String::fromBase64(empty), std::string());
467+
468+
for (auto const& [str, b64] : ::str2base64) {
469+
std::string const decoded = util::String::fromBase64(b64);
470+
BOOST_CHECK_EQUAL(decoded.size(), str.size());
471+
BOOST_CHECK_EQUAL(decoded, str);
472+
}
473+
}
474+
428475
BOOST_AUTO_TEST_SUITE_END()

src/www/qserv/js/Common.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function(sqlFormatter,
66
_) {
77

88
class Common {
9-
static RestAPIVersion = 34;
9+
static RestAPIVersion = 35;
1010
static query2text(query, expanded) {
1111
if (expanded) {
1212
return sqlFormatter.format(query, Common._sqlFormatterConfig);

0 commit comments

Comments
 (0)