utils: cleanup Split and Join

This commit is contained in:
Peter Keresztes Schmidt 2021-04-04 00:30:18 +02:00
parent 39a896f5b6
commit e330f8553d
9 changed files with 116 additions and 153 deletions

View File

@ -213,7 +213,7 @@ void LibvlcCamera::Terminate() {
int LibvlcCamera::PrimeCapture() {
Debug(1, "Priming capture from %s, libvlc version %s", mPath.c_str(), (*libvlc_get_version_f)());
StringVector opVect = split(Options(), ",");
StringVector opVect = Split(Options(), ",");
// Set transport method as specified by method field, rtpUni is default
if ( Method() == "rtpMulti" )

View File

@ -166,7 +166,7 @@ void Logger::initialise(const std::string &id, const Options &options) {
tempSyslogLevel = atoi(envPtr);
if ( config.log_debug ) {
StringVector targets = split(config.log_debug_target, "|");
StringVector targets = Split(config.log_debug_target, "|");
for ( unsigned int i = 0; i < targets.size(); i++ ) {
const std::string &target = targets[i];
if ( target == mId || target == "_"+mId || target == "_"+mIdRoot || target == "" ) {

View File

@ -34,7 +34,7 @@ RtspThread::PortSet RtspThread::smAssignedPorts;
bool RtspThread::sendCommand(std::string message) {
if ( mNeedAuth ) {
StringVector parts = split(message, " ");
StringVector parts = Split(message, " ");
if ( parts.size() > 1 )
message += mAuthenticator->getAuthHeader(parts[0], parts[1]);
}
@ -172,7 +172,7 @@ RtspThread::RtspThread(
mHttpSession = stringtf("%d", rand());
mNeedAuth = false;
StringVector parts = split(auth, ":");
StringVector parts = Split(auth, ":");
Debug(2, "# of auth parts %d", parts.size());
if ( parts.size() > 1 )
mAuthenticator = new zm::Authenticator(parts[0], parts[1]);
@ -320,7 +320,7 @@ void RtspThread::Run() {
} // end if failed response maybe due to auth
char publicLine[256] = "";
StringVector lines = split(response, "\r\n");
StringVector lines = Split(response, "\r\n");
for ( size_t i = 0; i < lines.size(); i++ )
sscanf(lines[i].c_str(), "Public: %[^\r\n]\r\n", publicLine);
@ -352,7 +352,7 @@ void RtspThread::Run() {
std::string DescHeader = response.substr(0, sdpStart);
Debug(1, "Processing DESCRIBE response header '%s'", DescHeader.c_str());
lines = split(DescHeader, "\r\n");
lines = Split(DescHeader, "\r\n");
for ( size_t i = 0; i < lines.size(); i++ ) {
// If the device sends us a url value for Content-Base in the response header, we should use that instead
if ( ( lines[i].size() > 13 ) && ( lines[i].substr( 0, 13 ) == "Content-Base:" ) ) {
@ -453,14 +453,14 @@ void RtspThread::Run() {
if ( !recvResponse(response) )
return;
lines = split(response, "\r\n");
lines = Split(response, "\r\n");
std::string session;
int timeout = 0;
char transport[256] = "";
for ( size_t i = 0; i < lines.size(); i++ ) {
if ( ( lines[i].size() > 8 ) && ( lines[i].substr(0, 8) == "Session:" ) ) {
StringVector sessionLine = split(lines[i].substr(9), ";");
StringVector sessionLine = Split(lines[i].substr(9), ";");
session = TrimSpaces(sessionLine[0]);
if ( sessionLine.size() == 2 )
sscanf(TrimSpaces(sessionLine[1]).c_str(), "timeout=%d", &timeout);
@ -483,33 +483,33 @@ void RtspThread::Run() {
int remoteChannels[2] = { 0, 0 };
std::string distribution = "";
unsigned long ssrc = 0;
StringVector parts = split( transport, ";" );
StringVector parts = Split(transport, ";");
for ( size_t i = 0; i < parts.size(); i++ ) {
if ( parts[i] == "unicast" || parts[i] == "multicast" )
distribution = parts[i];
else if (StartsWith(parts[i], "server_port=") ) {
method = "RTP/UNICAST";
StringVector subparts = split( parts[i], "=" );
StringVector ports = split( subparts[1], "-" );
StringVector subparts = Split(parts[i], "=");
StringVector ports = Split(subparts[1], "-");
remotePorts[0] = strtol( ports[0].c_str(), nullptr, 10 );
remotePorts[1] = strtol( ports[1].c_str(), nullptr, 10 );
} else if (StartsWith(parts[i], "interleaved=") ) {
method = "RTP/RTSP";
StringVector subparts = split( parts[i], "=" );
StringVector channels = split( subparts[1], "-" );
StringVector subparts = Split(parts[i], "=");
StringVector channels = Split(subparts[1], "-");
remoteChannels[0] = strtol( channels[0].c_str(), nullptr, 10 );
remoteChannels[1] = strtol( channels[1].c_str(), nullptr, 10 );
} else if (StartsWith(parts[i], "port=") ) {
method = "RTP/MULTICAST";
StringVector subparts = split( parts[i], "=" );
StringVector ports = split( subparts[1], "-" );
StringVector subparts = Split(parts[i], "=");
StringVector ports = Split(subparts[1], "-");
localPorts[0] = strtol( ports[0].c_str(), nullptr, 10 );
localPorts[1] = strtol( ports[1].c_str(), nullptr, 10 );
} else if (StartsWith(parts[i], "destination=") ) {
StringVector subparts = split( parts[i], "=" );
StringVector subparts = Split(parts[i], "=");
localHost = subparts[1];
} else if (StartsWith(parts[i], "ssrc=") ) {
StringVector subparts = split( parts[i], "=" );
StringVector subparts = Split(parts[i], "=");
ssrc = strtoll( subparts[1].c_str(), nullptr, 16 );
}
}
@ -528,14 +528,14 @@ void RtspThread::Run() {
if ( !recvResponse(response) )
return;
lines = split(response, "\r\n");
lines = Split(response, "\r\n");
std::string rtpInfo;
for ( size_t i = 0; i < lines.size(); i++ ) {
if ( ( lines[i].size() > 9 ) && ( lines[i].substr(0, 9) == "RTP-Info:" ) )
rtpInfo = TrimSpaces(lines[i].substr(9));
// Check for a timeout again. Some rtsp devices don't send a timeout until after the PLAY command is sent
if ( ( lines[i].size() > 8 ) && ( lines[i].substr(0, 8) == "Session:" ) && ( timeout == 0 ) ) {
StringVector sessionLine = split(lines[i].substr(9), ";");
StringVector sessionLine = Split(lines[i].substr(9), ";");
if ( sessionLine.size() == 2 )
sscanf(TrimSpaces(sessionLine[1]).c_str(), "timeout=%d", &timeout);
if ( timeout > 0 )
@ -551,18 +551,18 @@ void RtspThread::Run() {
} else {
Debug( 2, "Got RTP Info %s", rtpInfo.c_str() );
// More than one stream can be included in the RTP Info
streams = split( rtpInfo.c_str(), "," );
streams = Split(rtpInfo.c_str(), ",");
for ( size_t i = 0; i < streams.size(); i++ ) {
// We want the stream that matches the trackUrl we are using
if ( streams[i].find(controlUrl.c_str()) != std::string::npos ) {
// Parse the sequence and rtptime values
parts = split( streams[i].c_str(), ";" );
parts = Split(streams[i].c_str(), ";");
for ( size_t j = 0; j < parts.size(); j++ ) {
if (StartsWith(parts[j], "seq=") ) {
StringVector subparts = split( parts[j], "=" );
StringVector subparts = Split(parts[j], "=");
seq = strtol( subparts[1].c_str(), nullptr, 10 );
} else if (StartsWith(parts[j], "rtptime=") ) {
StringVector subparts = split( parts[j], "=" );
StringVector subparts = Split(parts[j], "=");
rtpTime = strtol( subparts[1].c_str(), nullptr, 10 );
}
}

View File

@ -68,10 +68,10 @@ void Authenticator::authHandleHeader(std::string headerData) {
else if ( strncasecmp(headerData.c_str(), digest_match, digest_match_len) == 0) {
fAuthMethod = AUTH_DIGEST;
Debug(2, "Set authMethod to Digest");
StringVector subparts = split(headerData.substr(digest_match_len, headerData.length() - digest_match_len), ",");
StringVector subparts = Split(headerData.substr(digest_match_len, headerData.length() - digest_match_len), ",");
// subparts are key="value"
for ( size_t i = 0; i < subparts.size(); i++ ) {
StringVector kvPair = split(TrimSpaces(subparts[i]), "=");
StringVector kvPair = Split(TrimSpaces(subparts[i]), "=");
std::string key = TrimSpaces(kvPair[0]);
if ( key == "realm" ) {
fRealm = Trim(kvPair[1], "\"");
@ -194,7 +194,7 @@ std::string Authenticator::computeDigestResponse(std::string &method, std::strin
void Authenticator::checkAuthResponse(std::string &response) {
std::string authLine;
StringVector lines = split(response, "\r\n");
StringVector lines = Split(response, "\r\n");
const char* authenticate_match = "WWW-Authenticate:";
size_t authenticate_match_len = strlen(authenticate_match);

View File

@ -107,7 +107,7 @@ SessionDescriptor::ConnInfo::ConnInfo( const std::string &connInfo ) :
mTtl( 16 ),
mNoAddresses( 0 )
{
StringVector tokens = split(connInfo, " ");
StringVector tokens = Split(connInfo, " ");
if ( tokens.size() < 3 )
throw Exception( "Unable to parse SDP connection info from '"+connInfo+"'" );
mNetworkType = tokens[0];
@ -116,7 +116,7 @@ SessionDescriptor::ConnInfo::ConnInfo( const std::string &connInfo ) :
mAddressType = tokens[1];
if ( mAddressType != "IP4" && mAddressType != "IP6" )
throw Exception( "Invalid SDP address type '"+mAddressType+"' in connection info '"+connInfo+"'" );
StringVector addressTokens = split( tokens[2], "/" );
StringVector addressTokens = Split(tokens[2], "/");
if ( addressTokens.size() < 1 )
throw Exception( "Invalid SDP address '"+tokens[2]+"' in connection info '"+connInfo+"'" );
mAddress = addressTokens[0];
@ -129,7 +129,7 @@ SessionDescriptor::ConnInfo::ConnInfo( const std::string &connInfo ) :
SessionDescriptor::BandInfo::BandInfo( const std::string &bandInfo ) :
mValue( 0 )
{
StringVector tokens = split( bandInfo, ":" );
StringVector tokens = Split(bandInfo, ":");
if ( tokens.size() < 2 )
throw Exception( "Unable to parse SDP bandwidth info from '"+bandInfo+"'" );
mType = tokens[0];
@ -165,7 +165,7 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
{
MediaDescriptor *currMedia = nullptr;
StringVector lines = split( sdp, "\r\n" );
StringVector lines = Split(sdp, "\r\n");
for ( StringVector::const_iterator iter = lines.begin(); iter != lines.end(); ++iter ) {
std::string line = *iter;
if ( line.empty() )
@ -208,7 +208,7 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
case 'a' :
{
mAttributes.push_back( line );
StringVector tokens = split( line, ":", 2 );
StringVector tokens = Split(line, ":", 2);
std::string attrName = tokens[0];
if ( currMedia ) {
if ( attrName == "control" ) {
@ -220,14 +220,14 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
// a=rtpmap:96 MP4V-ES/90000
if ( tokens.size() < 2 )
throw Exception( "Unable to parse SDP rtpmap attribute '"+line+"' for media '"+currMedia->getType()+"'" );
StringVector attrTokens = split( tokens[1], " " );
StringVector attrTokens = Split(tokens[1], " ");
int payloadType = atoi(attrTokens[0].c_str());
if ( payloadType != currMedia->getPayloadType() )
throw Exception( stringtf( "Payload type mismatch, expected %d, got %d in '%s'", currMedia->getPayloadType(), payloadType, line.c_str() ) );
std::string payloadDesc = attrTokens[1];
//currMedia->setPayloadType( payloadType );
if ( attrTokens.size() > 1 ) {
StringVector payloadTokens = split( attrTokens[1], "/" );
StringVector payloadTokens = Split(attrTokens[1], "/");
std::string payloadDesc = payloadTokens[0];
int payloadClock = atoi(payloadTokens[1].c_str());
currMedia->setPayloadDesc( payloadDesc );
@ -237,13 +237,13 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
// a=framesize:96 320-240
if ( tokens.size() < 2 )
throw Exception("Unable to parse SDP framesize attribute '"+line+"' for media '"+currMedia->getType()+"'");
StringVector attrTokens = split(tokens[1], " ");
StringVector attrTokens = Split(tokens[1], " ");
int payloadType = atoi(attrTokens[0].c_str());
if ( payloadType != currMedia->getPayloadType() )
throw Exception( stringtf("Payload type mismatch, expected %d, got %d in '%s'",
currMedia->getPayloadType(), payloadType, line.c_str()));
//currMedia->setPayloadType( payloadType );
StringVector sizeTokens = split(attrTokens[1], "-");
StringVector sizeTokens = Split(attrTokens[1], "-");
int width = atoi(sizeTokens[0].c_str());
int height = atoi(sizeTokens[1].c_str());
currMedia->setFrameSize(width, height);
@ -257,16 +257,16 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
// a=fmtp:96 profile-level-id=247; config=000001B0F7000001B509000001000000012008D48D8803250F042D14440F
if ( tokens.size() < 2 )
throw Exception("Unable to parse SDP fmtp attribute '"+line+"' for media '"+currMedia->getType()+"'");
StringVector attrTokens = split(tokens[1], " ", 2);
StringVector attrTokens = Split(tokens[1], " ", 2);
int payloadType = atoi(attrTokens[0].c_str());
if ( payloadType != currMedia->getPayloadType() )
throw Exception(stringtf("Payload type mismatch, expected %d, got %d in '%s'",
currMedia->getPayloadType(), payloadType, line.c_str()));
//currMedia->setPayloadType( payloadType );
if ( attrTokens.size() > 1 ) {
StringVector attr2Tokens = split( attrTokens[1], "; " );
StringVector attr2Tokens = Split(attrTokens[1], "; ");
for ( unsigned int i = 0; i < attr2Tokens.size(); i++ ) {
StringVector attr3Tokens = split( attr2Tokens[i], "=" );
StringVector attr3Tokens = Split(attr2Tokens[i], "=");
//Info( "Name = %s, Value = %s", attr3Tokens[0].c_str(), attr3Tokens[1].c_str() );
if ( attr3Tokens[0] == "profile-level-id" ) {
} else if ( attr3Tokens[0] == "config" ) {
@ -294,13 +294,13 @@ SessionDescriptor::SessionDescriptor( const std::string &url, const std::string
}
case 'm' :
{
StringVector tokens = split(line, " ");
StringVector tokens = Split(line, " ");
if ( tokens.size() < 4 )
throw Exception("Can't parse SDP media description '"+line+"'");
std::string mediaType = tokens[0];
if ( mediaType != "audio" && mediaType != "video" && mediaType != "application" )
throw Exception("Unsupported media type '"+mediaType+"' in SDP media attribute '"+line+"'");
StringVector portTokens = split(tokens[1], "/");
StringVector portTokens = Split(tokens[1], "/");
int mediaPort = atoi(portTokens[0].c_str());
int mediaNumPorts = 1;
if ( portTokens.size() > 1 )

View File

@ -54,7 +54,7 @@ User::User(const MYSQL_ROW &dbrow) {
system = (Permission)atoi(dbrow[index++]);
char *monitor_ids_str = dbrow[index++];
if ( monitor_ids_str && *monitor_ids_str ) {
StringVector ids = split(monitor_ids_str, ",");
StringVector ids = Split(monitor_ids_str, ",");
for ( StringVector::iterator i = ids.begin(); i < ids.end(); ++i ) {
monitor_ids.push_back(atoi((*i).c_str()));
}

View File

@ -61,53 +61,62 @@ std::string ReplaceAll(std::string str, const std::string &old_value, const std:
return str;
}
std::vector<std::string> split(const std::string &s, char delim) {
std::vector<std::string> elems;
std::stringstream ss(s);
std::string item;
while(std::getline(ss, item, delim)) {
elems.push_back(TrimSpaces(item));
StringVector Split(const std::string &str, char delim) {
std::vector<std::string> tokens;
size_t start = 0;
for (size_t end = str.find(delim); end != std::string::npos; end = str.find(delim, start)) {
tokens.push_back(str.substr(start, end - start));
start = end + 1;
}
return elems;
tokens.push_back(str.substr(start));
return tokens;
}
StringVector split(const std::string &string, const std::string &chars, int limit) {
StringVector stringVector;
std::string tempString = string;
std::string::size_type startIndex = 0;
std::string::size_type endIndex = 0;
StringVector Split(const std::string &str, const std::string &delim, size_t limit) {
StringVector tokens;
size_t start = 0;
//Info( "Looking for '%s' in '%s', limit %d", chars.c_str(), string.c_str(), limit );
do {
// Find delimiters
endIndex = string.find_first_of( chars, startIndex );
//Info( "Got endIndex at %d", endIndex );
if ( endIndex > 0 ) {
//Info( "Adding '%s'", string.substr( startIndex, endIndex-startIndex ).c_str() );
stringVector.push_back( string.substr( startIndex, endIndex-startIndex ) );
size_t end = str.find_first_of(delim, start);
if (end > 0) {
tokens.push_back(str.substr(start, end - start));
}
if ( endIndex == std::string::npos )
if (end == std::string::npos) {
break;
}
// Find non-delimiters
startIndex = tempString.find_first_not_of( chars, endIndex );
if ( limit && (stringVector.size() == (unsigned int)(limit-1)) ) {
stringVector.push_back( string.substr( startIndex ) );
start = str.find_first_not_of(delim, end);
if (limit && (tokens.size() == limit - 1)) {
tokens.push_back(str.substr(start));
break;
}
//Info( "Got new startIndex at %d", startIndex );
} while ( startIndex != std::string::npos );
//Info( "Finished with %d strings", stringVector.size() );
} while (start != std::string::npos);
return stringVector;
return tokens;
}
const std::string join(const StringVector &v, const char * delim=",") {
std::pair<std::string, std::string> PairSplit(const std::string &str, char delim) {
if (str.empty())
return std::make_pair("", "");
size_t pos = str.find(delim);
if (pos == std::string::npos)
return std::make_pair("", "");
return std::make_pair(str.substr(0, pos), str.substr(pos + 1, std::string::npos));
}
std::string Join(const StringVector &values, const std::string &delim) {
std::stringstream ss;
for (size_t i = 0; i < v.size(); ++i) {
for (size_t i = 0; i < values.size(); ++i) {
if ( i != 0 )
ss << delim;
ss << v[i];
ss << values[i];
}
return ss.str();
}
@ -159,46 +168,6 @@ const std::string base64Encode(const std::string &inString) {
return outString;
}
int split(const char* string, const char delim, std::vector<std::string>& items) {
if ( string == nullptr )
return -1;
if ( string[0] == 0 )
return -2;
std::string str(string);
while ( true ) {
size_t pos = str.find(delim);
items.push_back(str.substr(0, pos));
str.erase(0, pos+1);
if ( pos == std::string::npos )
break;
}
return items.size();
}
int pairsplit(const char* string, const char delim, std::string& name, std::string& value) {
if ( string == nullptr )
return -1;
if ( string[0] == 0 )
return -2;
std::string str(string);
size_t pos = str.find(delim);
if ( pos == std::string::npos || pos == 0 || pos >= str.length() )
return -3;
name = str.substr(0, pos);
value = str.substr(pos+1, std::string::npos);
return 0;
}
/* Detect special hardware features, such as SIMD instruction sets */
void hwcaps_detect() {
neonversion = 0;

View File

@ -35,6 +35,12 @@ std::string Trim(const std::string &str, const std::string &char_set);
inline std::string TrimSpaces(const std::string &str) { return Trim(str, " \t"); }
std::string ReplaceAll(std::string str, const std::string& old_value, const std::string& new_value);
StringVector Split(const std::string &str, char delim);
StringVector Split(const std::string &str, const std::string &delim, size_t limit = 0);
std::pair<std::string, std::string> PairSplit(const std::string &str, char delim);
std::string Join(const StringVector &values, const std::string &delim = ",");
inline bool StartsWith(const std::string &haystack, const std::string &needle) {
return (haystack.substr(0, needle.length()) == needle);
}
@ -50,15 +56,9 @@ std::string stringtf(const std::string &format, Args... args) {
return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
}
StringVector split( const std::string &string, const std::string &chars, int limit=0 );
const std::string join( const StringVector &, const char * );
const std::string base64Encode( const std::string &inString );
void string_toupper(std::string& str);
int split(const char* string, const char delim, std::vector<std::string>& items);
int pairsplit(const char* string, const char delim, std::string& name, std::string& value);
void* sse2_aligned_memcpy(void* dest, const void* src, size_t bytes);
void timespec_diff(struct timespec *start, struct timespec *end, struct timespec *diff);

View File

@ -73,77 +73,71 @@ TEST_CASE("StartsWith") {
REQUIRE(StartsWith(" test=abc", "test") == false);
}
TEST_CASE("split (char delimiter)") {
std::vector<std::string> items;
int res;
TEST_CASE("Split (char delimiter)") {
std::vector<std::string> items = Split("", ' ');
REQUIRE(items == std::vector<std::string>{""});
res = split(nullptr, ' ', items);
REQUIRE(res == -1);
REQUIRE(items.size() == 0);
res = split("", ' ', items);
REQUIRE(res == -2);
REQUIRE(items.size() == 0);
res = split("abc def ghi", ' ', items);
REQUIRE(res == 3);
items = Split("abc def ghi", ' ');
REQUIRE(items == std::vector<std::string>{"abc", "def", "ghi"});
items = Split("abc,def,,ghi", ',');
REQUIRE(items == std::vector<std::string>{"abc", "def", "", "ghi"});
}
TEST_CASE("split (string delimiter)") {
TEST_CASE("Split (string delimiter)") {
std::vector<std::string> items;
items = split("", "");
items = Split("", "");
REQUIRE(items == std::vector<std::string>{""});
items = split("", " ");
items = Split("", " ");
REQUIRE(items == std::vector<std::string>{""});
items = split("", " \t");
items = Split("", " \t");
REQUIRE(items == std::vector<std::string>{""});
items = split("", " \t");
items = Split("", " \t");
REQUIRE(items == std::vector<std::string>{""});
items = split(" ", " ");
items = Split(" ", " ");
REQUIRE(items.size() == 0);
items = split(" ", " ");
items = Split(" ", " ");
REQUIRE(items.size() == 0);
items = split(" ", " \t");
items = Split(" ", " \t");
REQUIRE(items.size() == 0);
items = split("a b", "");
items = Split("a b", "");
REQUIRE(items == std::vector<std::string>{"a b"});
items = split("a b", " ");
items = Split("a b", " ");
REQUIRE(items == std::vector<std::string>{"a", "b"});
items = split("a \tb", " \t");
items = Split("a \tb", " \t");
REQUIRE(items == std::vector<std::string>{"a", "b"});
items = split(" a \tb ", " \t");
items = Split(" a \tb ", " \t");
REQUIRE(items == std::vector<std::string>{"a", "b"});
items = split(" a=b ", "=");
items = Split(" a=b ", "=");
REQUIRE(items == std::vector<std::string>{" a", "b "});
items = split(" a=b ", " =");
items = Split(" a=b ", " =");
REQUIRE(items == std::vector<std::string>{"a", "b"});
items = split("a b c", " ", 2);
items = Split("a b c", " ", 2);
REQUIRE(items == std::vector<std::string>{"a", "b c"});
}
TEST_CASE("join") {
REQUIRE(join({}, "") == "");
REQUIRE(join({}, " ") == "");
REQUIRE(join({""}, "") == "");
REQUIRE(join({"a"}, "") == "a");
REQUIRE(join({"a"}, ",") == "a");
REQUIRE(join({"a", "b"}, ",") == "a,b");
REQUIRE(join({"a", "b"}, "") == "ab");
TEST_CASE("Join") {
REQUIRE(Join({}, "") == "");
REQUIRE(Join({}, " ") == "");
REQUIRE(Join({""}, "") == "");
REQUIRE(Join({"a"}, "") == "a");
REQUIRE(Join({"a"}, ",") == "a");
REQUIRE(Join({"a", "b"}, ",") == "a,b");
REQUIRE(Join({"a", "b"}, "") == "ab");
}
TEST_CASE("base64Encode") {