diff --git a/include/base64.h b/include/base64.h index 9def0dd6a9a..b7d8592e9d0 100644 --- a/include/base64.h +++ b/include/base64.h @@ -76,12 +76,6 @@ base64_encode_init(struct base64_encode_ctx *ctx); void base64url_encode_init(struct base64_encode_ctx *ctx); -/* Encodes a single byte. Returns amount of output (always 1 or 2). */ -size_t -base64_encode_single(struct base64_encode_ctx *ctx, - char *dst, - uint8_t src); - /* Returns the number of output characters. DST should point to an * area of size at least BASE64_ENCODE_LENGTH(length). */ size_t diff --git a/lib/base64.cc b/lib/base64.cc index eeeca44cae3..8754bdbfb5e 100644 --- a/lib/base64.cc +++ b/lib/base64.cc @@ -236,7 +236,7 @@ base64_encode_init(struct base64_encode_ctx *ctx) } /* Encodes a single byte. */ -size_t +static size_t base64_encode_single(struct base64_encode_ctx *ctx, char *dst, uint8_t src) diff --git a/src/HttpHeader.cc b/src/HttpHeader.cc index eb62a507ddd..cf4d6e13303 100644 --- a/src/HttpHeader.cc +++ b/src/HttpHeader.cc @@ -9,11 +9,11 @@ /* DEBUG: section 55 HTTP Header */ #include "squid.h" +#include "anyp/Base64.h" #include "base/Assure.h" #include "base/CharacterSet.h" #include "base/EnumIterator.h" #include "base/Raw.h" -#include "base64.h" #include "globals.h" #include "http/ContentLengthInterpreter.h" #include "HttpHdrCc.h" @@ -1430,18 +1430,11 @@ HttpHeader::getAuthToken(Http::HdrType id, const char *auth_scheme) const if (!*field) /* no authorization cookie */ return nil; - const auto fieldLen = strlen(field); - SBuf result; - char *decodedAuthToken = result.rawAppendStart(BASE64_DECODE_LENGTH(fieldLen)); - struct base64_decode_ctx ctx; - base64_decode_init(&ctx); - size_t decodedLen = 0; - if (!base64_decode_update(&ctx, &decodedLen, reinterpret_cast(decodedAuthToken), fieldLen, field) || - !base64_decode_final(&ctx)) { + try { + return Base64Decode(field, strlen(field)); + } catch (const DecodeException &) { return nil; } - result.rawAppendFinish(decodedAuthToken, decodedLen); - return result; } ETag diff --git a/src/Makefile.am b/src/Makefile.am index 4480764023d..f563cd2eb2b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -982,6 +982,23 @@ tests_testSBufList_LDADD = \ $(XTRA_LIBS) tests_testSBufList_LDFLAGS = $(LIBADD_DL) +check_PROGRAMS += tests/testBase64 +tests_testBase64_SOURCES = \ + tests/testBase64.cc +nodist_tests_testBase64_SOURCES = \ + tests/stub_debug.cc \ + tests/stub_libmem.cc +tests_testBase64_LDADD = \ + sbuf/libsbuf.la \ + base/libbase.la \ + anyp/libanyp.la \ + $(top_builddir)/lib/libmiscencoding.la \ + $(LIBCPPUNIT_LIBS) \ + $(COMPAT_LIB) \ + $(XTRA_LIBS) \ + $(LIBNETTLE_LIBS) +tests_testBase64_LDFLAGS = $(LIBADD_DL) + check_PROGRAMS += tests/testString tests_testString_SOURCES = \ tests/testString.cc diff --git a/src/adaptation/icap/ModXact.cc b/src/adaptation/icap/ModXact.cc index 3ad64b8f40e..172307806b5 100644 --- a/src/adaptation/icap/ModXact.cc +++ b/src/adaptation/icap/ModXact.cc @@ -19,6 +19,7 @@ #include "adaptation/icap/ModXact.h" #include "adaptation/icap/ServiceRep.h" #include "adaptation/Initiator.h" +#include "anyp/Base64.h" #include "auth/UserRequest.h" #include "base/TextException.h" #include "base64.h" @@ -32,6 +33,7 @@ #include "MasterXaction.h" #include "parser/Tokenizer.h" #include "sbuf/Stream.h" +#include "sbuf/StringConvert.h" // flow and terminology: // HTTP| --> receive --> encode --> write --> |network @@ -1399,20 +1401,10 @@ void Adaptation::Icap::ModXact::makeRequestHeaders(MemBuf &buf) String vh=virgin.header->header.getById(Http::HdrType::PROXY_AUTHORIZATION); buf.appendf("Proxy-Authorization: " SQUIDSTRINGPH "\r\n", SQUIDSTRINGPRINT(vh)); } else if (request->extacl_user.size() > 0 && request->extacl_passwd.size() > 0) { - const auto userLen = request->extacl_user.size(); - const auto passwdLen = request->extacl_passwd.size(); - // +1 for the ':' separator between user and passwd - const auto plainLen = userLen + 1 + passwdLen; - if (plainLen > MAX_LOGIN_SZ) - throw TextException("extacl credentials too long for Proxy-Authorization", Here()); - char base64buf[base64_encode_len(MAX_LOGIN_SZ)]; - struct base64_encode_ctx ctx; - base64_encode_init(&ctx); - auto resultLen = base64_encode_update(&ctx, base64buf, userLen, reinterpret_cast(request->extacl_user.rawBuf())); - resultLen += base64_encode_update(&ctx, base64buf+resultLen, 1, reinterpret_cast(":")); - resultLen += base64_encode_update(&ctx, base64buf+resultLen, passwdLen, reinterpret_cast(request->extacl_passwd.rawBuf())); - resultLen += base64_encode_final(&ctx, base64buf+resultLen); - buf.appendf("Proxy-Authorization: Basic %.*s\r\n", (int)resultLen, base64buf); + auto credentials=StringToSBuf(request->extacl_user); + credentials.append(':').append(request->extacl_passwd.rawBuf(), request->extacl_passwd.size()); + const auto encoded = Base64Encode(credentials); + buf.appendf("Proxy-Authorization: Basic " SQUIDSBUFPH "\r\n", SQUIDSBUFPRINT(encoded)); } // share the cross-transactional database records if needed @@ -1554,9 +1546,6 @@ void Adaptation::Icap::ModXact::makeAllowHeader(MemBuf &buf) void Adaptation::Icap::ModXact::makeUsernameHeader(const HttpRequest *request, MemBuf &buf) { #if USE_AUTH - struct base64_encode_ctx ctx; - base64_encode_init(&ctx); - const char *value = nullptr; if (request->auth_user_request != nullptr) { value = request->auth_user_request->username(); @@ -1566,10 +1555,8 @@ void Adaptation::Icap::ModXact::makeUsernameHeader(const HttpRequest *request, M if (value) { if (TheConfig.client_username_encode) { - char base64buf[base64_encode_len(MAX_LOGIN_SZ)]; - size_t resultLen = base64_encode_update(&ctx, base64buf, strlen(value), reinterpret_cast(value)); - resultLen += base64_encode_final(&ctx, base64buf+resultLen); - buf.appendf("%s: %.*s\r\n", TheConfig.client_username_header, (int)resultLen, base64buf); + const auto base64buf=Base64Encode(value,strlen(value)); + buf.appendf("%s: " SQUIDSBUFPH "\r\n", TheConfig.client_username_header, SQUIDSBUFPRINT(base64buf)); } else buf.appendf("%s: %s\r\n", TheConfig.client_username_header, value); } diff --git a/src/anyp/Base64.cc b/src/anyp/Base64.cc new file mode 100644 index 00000000000..99b34b559b8 --- /dev/null +++ b/src/anyp/Base64.cc @@ -0,0 +1,51 @@ +/* + * Copyright (C) 1996-2026 The Squid Software Foundation and contributors + * + * Squid software is distributed under GPLv2+ license and includes + * contributions from numerous individuals and organizations. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#include "squid.h" +#include "anyp/Base64.h" +#include "base64.h" +#include "sbuf/SBuf.h" + +SBuf Base64Encode(const char *input, size_t length) +{ + SBuf result; + const auto encodedLength = BASE64_ENCODE_RAW_LENGTH(length); + char *buf = result.rawAppendStart(encodedLength); + base64_encode_raw(buf, length, reinterpret_cast(input)); + result.rawAppendFinish(buf, encodedLength); + return result; +} + +SBuf Base64Encode(const SBuf &input) +{ + return Base64Encode(input.rawContent(), input.length()); +} + +SBuf Base64Decode(const char *input, size_t length) +{ + struct base64_decode_ctx ctx; + base64_decode_init(&ctx); + + SBuf result; + const auto maxDecodedLength = BASE64_DECODE_LENGTH(length); + uint8_t *buf = reinterpret_cast(result.rawAppendStart(maxDecodedLength)); + + size_t decodedLength = 0; + if (!base64_decode_update(&ctx, &decodedLength, buf, length, input)) + throw DecodeException("base64 decode error: invalid input", Here()); + if (!base64_decode_final(&ctx)) + throw DecodeException("base64 decode error: incomplete input", Here()); + + result.rawAppendFinish(reinterpret_cast(buf), decodedLength); + return result; +} + +SBuf Base64Decode(const SBuf &input) +{ + return Base64Decode(input.rawContent(), input.length()); +} diff --git a/src/anyp/Base64.h b/src/anyp/Base64.h new file mode 100644 index 00000000000..6885d36540a --- /dev/null +++ b/src/anyp/Base64.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 1996-2026 The Squid Software Foundation and contributors + * + * Squid software is distributed under GPLv2+ license and includes + * contributions from numerous individuals and organizations. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#ifndef SQUID_SRC_ANYP_BASE64_H +#define SQUID_SRC_ANYP_BASE64_H + +#include "base/TextException.h" +#include "sbuf/forward.h" + +/// Thrown by Base64Decode() when the input is not valid base64. +class DecodeException : public TextException +{ +public: + using TextException::TextException; +}; + +SBuf Base64Encode(const char *input, size_t length); +SBuf Base64Encode(const SBuf &input); + +/// Decodes a base64-encoded string. Throws DecodeException on invalid input. +SBuf Base64Decode(const char *input, size_t length); +SBuf Base64Decode(const SBuf &input); + +#endif /* SQUID_SRC_ANYP_BASE64_H */ diff --git a/src/anyp/Makefile.am b/src/anyp/Makefile.am index 12a7f170924..a9527e3b06c 100644 --- a/src/anyp/Makefile.am +++ b/src/anyp/Makefile.am @@ -10,6 +10,8 @@ include $(top_srcdir)/src/Common.am noinst_LTLIBRARIES = libanyp.la libanyp_la_SOURCES = \ + Base64.cc \ + Base64.h \ Host.cc \ Host.h \ PortCfg.cc \ diff --git a/src/auth/ntlm/fake/Makefile.am b/src/auth/ntlm/fake/Makefile.am index 93de71bf391..c7681a400ce 100644 --- a/src/auth/ntlm/fake/Makefile.am +++ b/src/auth/ntlm/fake/Makefile.am @@ -11,6 +11,14 @@ libexec_PROGRAMS= ntlm_fake_auth ntlm_fake_auth_SOURCES = ntlm_fake_auth.cc ntlm_fake_auth_LDADD= \ + $(top_builddir)/src/anyp/libanyp.la \ + $(top_builddir)/src/sbuf/libsbuf.la \ + $(top_builddir)/src/debug/libdebug.la \ + $(top_builddir)/src/error/liberror.la \ + $(top_builddir)/src/comm/libminimal.la \ + $(top_builddir)/src/mem/libminimal.la \ + $(top_builddir)/src/base/libbase.la \ + $(top_builddir)/src/time/libtime.la \ $(top_builddir)/lib/ntlmauth/libntlmauth.la \ $(top_builddir)/lib/libmiscencoding.la \ $(COMPAT_LIB) \ diff --git a/src/auth/ntlm/fake/ntlm_fake_auth.cc b/src/auth/ntlm/fake/ntlm_fake_auth.cc index bca930428db..360318f28ec 100644 --- a/src/auth/ntlm/fake/ntlm_fake_auth.cc +++ b/src/auth/ntlm/fake/ntlm_fake_auth.cc @@ -34,10 +34,12 @@ #define IGNORANCE_IS_BLISS #include "squid.h" +#include "anyp/Base64.h" #include "base64.h" #include "helper/protocol_defines.h" #include "ntlmauth/ntlmauth.h" #include "ntlmauth/support_bits.cci" +#include "sbuf/SBuf.h" #include #include @@ -201,18 +203,14 @@ main(int argc, char *argv[]) len = sizeof(chal) - sizeof(chal.payload) + le16toh(chal.target.maxlen); - struct base64_encode_ctx eCtx; - base64_encode_init(&eCtx); - char *data = static_cast(xcalloc(base64_encode_len(len), 1)); - size_t blen = base64_encode_update(&eCtx, data, len, reinterpret_cast(&chal)); - blen += base64_encode_final(&eCtx, data+blen); + const auto base64EncodedChallenge=Base64Encode(reinterpret_cast(&chal), len); + if (NTLM_packet_debug_enabled) { - printf("TT %.*s\n", (int)blen, data); + printf("TT " SQUIDSBUFPH "\n", SQUIDSBUFPRINT(base64EncodedChallenge)); debug("sending 'TT' to squid with data:\n"); hex_dump((unsigned char *)&chal, len); } else - SEND3("TT %.*s", (int)blen, data); - safe_free(data); + SEND3("TT " SQUIDSBUFPH, SQUIDSBUFPRINT(base64EncodedChallenge)); } else if (strncmp(buf, "KK ", 3) == 0) { if (!packet) { diff --git a/src/format/Format.cc b/src/format/Format.cc index 445a20712f3..c955b45eb1a 100644 --- a/src/format/Format.cc +++ b/src/format/Format.cc @@ -8,7 +8,7 @@ #include "squid.h" #include "AccessLogEntry.h" -#include "base64.h" +#include "anyp/Base64.h" #include "client_side.h" #include "comm/Connection.h" #include "error/Detail.h" @@ -556,16 +556,8 @@ Format::Format::assemble(MemBuf &mb, const AccessLogEntry::Pointer &al, int logS case LFT_CLIENT_HANDSHAKE: if (al->request && al->request->clientConnectionManager.valid()) { const auto &handshake = al->request->clientConnectionManager->preservedClientData; - if (const auto rawLength = handshake.length()) { - // add 1 byte to optimize the c_str() conversion below - char *buf = sb.rawAppendStart(base64_encode_len(rawLength) + 1); - - struct base64_encode_ctx ctx; - base64_encode_init(&ctx); - auto encLength = base64_encode_update(&ctx, buf, rawLength, reinterpret_cast(handshake.rawContent())); - encLength += base64_encode_final(&ctx, buf + encLength); - - sb.rawAppendFinish(buf, encLength); + if (!handshake.isEmpty()) { + sb = Base64Encode(handshake); out = sb.c_str(); } } diff --git a/src/http.cc b/src/http.cc index acb2299f2cf..719b83b30d5 100644 --- a/src/http.cc +++ b/src/http.cc @@ -15,11 +15,11 @@ #include "squid.h" #include "acl/FilledChecklist.h" +#include "anyp/Base64.h" #include "base/AsyncJobCalls.h" #include "base/DelayedAsyncCalls.h" #include "base/Raw.h" #include "base/TextException.h" -#include "base64.h" #include "CachePeer.h" #include "client_side.h" #include "comm/Connection.h" @@ -1834,11 +1834,6 @@ httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHe } } - char loginbuf[base64_encode_len(MAX_LOGIN_SZ)]; - size_t blen; - struct base64_encode_ctx ctx; - base64_encode_init(&ctx); - /* Special mode to pass the username to the upstream cache */ if (*request->peer_login == '*') { const char *username = "-"; @@ -1850,14 +1845,10 @@ httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHe username = request->auth_user_request->username(); #endif - const auto usernameLen = strlen(username); - const auto suffixLen = strlen(request->peer_login + 1); - if (usernameLen + suffixLen > MAX_LOGIN_SZ) - throw TextException("peer login credentials too long", Here()); - blen = base64_encode_update(&ctx, loginbuf, usernameLen, reinterpret_cast(username)); - blen += base64_encode_update(&ctx, loginbuf+blen, suffixLen, reinterpret_cast(request->peer_login +1)); - blen += base64_encode_final(&ctx, loginbuf+blen); - httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)blen, loginbuf); + SBuf toEncode(username); + toEncode.append(request->peer_login + 1); + SBuf encoded = Base64Encode(toEncode); + httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)encoded.length(), encoded.rawContent()); return; } @@ -1866,16 +1857,12 @@ httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHe (strcmp(request->peer_login, "PASS") == 0 || strcmp(request->peer_login, "PROXYPASS") == 0)) { - const auto userLen = request->extacl_user.size(); - const auto passwdLen = request->extacl_passwd.size(); - // +1 for the ':' separator between user and passwd - if (userLen + 1 + passwdLen > MAX_LOGIN_SZ) - throw TextException("extacl credentials too long for peer login", Here()); - blen = base64_encode_update(&ctx, loginbuf, userLen, reinterpret_cast(request->extacl_user.rawBuf())); - blen += base64_encode_update(&ctx, loginbuf+blen, 1, reinterpret_cast(":")); - blen += base64_encode_update(&ctx, loginbuf+blen, passwdLen, reinterpret_cast(request->extacl_passwd.rawBuf())); - blen += base64_encode_final(&ctx, loginbuf+blen); - httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)blen, loginbuf); + SBuf toEncode; + toEncode.append(request->extacl_user.rawBuf(), request->extacl_user.size()); + toEncode.append(':'); + toEncode.append(request->extacl_passwd.rawBuf(), request->extacl_passwd.size()); + SBuf encoded = Base64Encode(toEncode); + httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)encoded.length(), encoded.rawContent()); return; } // if no external user credentials are available to fake authentication with PASS acts like PASSTHRU @@ -1903,13 +1890,11 @@ httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHe } #endif /* HAVE_KRB5 && HAVE_GSSAPI */ - const auto loginLen = strlen(request->peer_login); - if (loginLen > MAX_LOGIN_SZ) - throw TextException("peer_login too long", Here()); - blen = base64_encode_update(&ctx, loginbuf, loginLen, reinterpret_cast(request->peer_login)); - blen += base64_encode_final(&ctx, loginbuf+blen); - httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)blen, loginbuf); - return; + { + SBuf encoded = Base64Encode(SBuf(request->peer_login)); + httpHeaderPutStrf(hdr_out, header, "Basic %.*s", (int)encoded.length(), encoded.rawContent()); + return; + } } /* @@ -2030,15 +2015,9 @@ HttpStateData::httpBuildRequestHeader(HttpRequest * request, /* append Authorization if known in URL, not in header and going direct */ if (!hdr_out->has(Http::HdrType::AUTHORIZATION)) { if (flags.toOrigin && !request->url.userInfo().isEmpty()) { - Assure(request->url.userInfo().length() < MAX_URL*2); - static char result[base64_encode_len(MAX_URL*2)]; // should be big enough for a single URI segment - struct base64_encode_ctx ctx; - base64_encode_init(&ctx); - size_t blen = base64_encode_update(&ctx, result, request->url.userInfo().length(), reinterpret_cast(request->url.userInfo().rawContent())); - blen += base64_encode_final(&ctx, result+blen); - result[blen] = '\0'; - if (blen) - httpHeaderPutStrf(hdr_out, Http::HdrType::AUTHORIZATION, "Basic %.*s", (int)blen, result); + auto encoded = Base64Encode(request->url.userInfo()); + if (!encoded.isEmpty()) + httpHeaderPutStrf(hdr_out, Http::HdrType::AUTHORIZATION, "Basic %.*s", (int)encoded.length(), encoded.rawContent()); } } diff --git a/src/tests/testBase64.cc b/src/tests/testBase64.cc new file mode 100644 index 00000000000..ae6f38cda54 --- /dev/null +++ b/src/tests/testBase64.cc @@ -0,0 +1,170 @@ +/* + * Copyright (C) 1996-2026 The Squid Software Foundation and contributors + * + * Squid software is distributed under GPLv2+ license and includes + * contributions from numerous individuals and organizations. + * Please see the COPYING and CONTRIBUTORS files for details. + */ + +#include "squid.h" +#include "anyp/Base64.h" +#include "compat/cppunit.h" +#include "sbuf/SBuf.h" +#include "unitTestMain.h" + +class TestBase64Encode : public CPPUNIT_NS::TestFixture +{ + CPPUNIT_TEST_SUITE(TestBase64Encode); + CPPUNIT_TEST(testBase64EncodeFunction); + CPPUNIT_TEST(testBase64EncodeFunctionEmpty); + CPPUNIT_TEST(testBase64EncodeFunctionLargeInput); + CPPUNIT_TEST_SUITE_END(); + +protected: + void testBase64EncodeFunction(); + void testBase64EncodeFunctionEmpty(); + void testBase64EncodeFunctionLargeInput(); +}; +CPPUNIT_TEST_SUITE_REGISTRATION( TestBase64Encode ); + +void +TestBase64Encode::testBase64EncodeFunction() +{ + SBuf input("Hello"); + SBuf result = Base64Encode(input); + CPPUNIT_ASSERT_EQUAL(SBuf("SGVsbG8="), result); +} + +void +TestBase64Encode::testBase64EncodeFunctionEmpty() +{ + SBuf input(""); + SBuf result = Base64Encode(input); + CPPUNIT_ASSERT_EQUAL(SBuf(""), result); +} + +void +TestBase64Encode::testBase64EncodeFunctionLargeInput() +{ + std::string largeInput(5000, 'A'); + SBuf input(largeInput.c_str(), largeInput.size()); + SBuf result = Base64Encode(input); + + CPPUNIT_ASSERT_EQUAL(static_cast(6668), result.length()); + CPPUNIT_ASSERT_EQUAL(static_cast('Q'), result[0]); + CPPUNIT_ASSERT_EQUAL(static_cast('U'), result[1]); + CPPUNIT_ASSERT_EQUAL(static_cast('F'), result[2]); + CPPUNIT_ASSERT_EQUAL(static_cast('B'), result[3]); + CPPUNIT_ASSERT_EQUAL(static_cast('='), result[result.length() - 1]); + CPPUNIT_ASSERT_EQUAL(static_cast('E'), result[result.length() - 2]); +} + +class TestBase64Decode : public CPPUNIT_NS::TestFixture +{ + CPPUNIT_TEST_SUITE(TestBase64Decode); + CPPUNIT_TEST(testDecodeSimple); + CPPUNIT_TEST(testDecodeEmpty); + CPPUNIT_TEST(testDecodeEncodeRoundtrip); + CPPUNIT_TEST(testEncodeDecodeRoundtrip); + CPPUNIT_TEST(testDecodeInvalidChars); + CPPUNIT_TEST(testDecodeInvalidPadding); + CPPUNIT_TEST(testDecodeIncomplete); + CPPUNIT_TEST_SUITE_END(); + +protected: + void testDecodeSimple(); + void testDecodeEmpty(); + void testDecodeEncodeRoundtrip(); + void testEncodeDecodeRoundtrip(); + void testDecodeInvalidChars(); + void testDecodeInvalidPadding(); + void testDecodeIncomplete(); +}; +CPPUNIT_TEST_SUITE_REGISTRATION(TestBase64Decode); + +static const struct { const char *encoded; const char *decoded; size_t decodedLen; } knownVectors[] = { + { "YQ==", "a", 1 }, + { "YWI=", "ab", 2 }, + { "YWJj", "abc", 3 }, + { "YWJjZA==", "abcd", 4 }, + { "SGVsbG8=", "Hello", 5 }, + { "dGVzdA==", "test", 4 }, + { "AQID", "\x01\x02\x03", 3 }, +}; + +void +TestBase64Decode::testDecodeSimple() +{ + for (const auto &v : knownVectors) { + SBuf result = Base64Decode(v.encoded, strlen(v.encoded)); + CPPUNIT_ASSERT_EQUAL(SBuf(v.decoded, v.decodedLen), result); + } +} + +void +TestBase64Decode::testDecodeEmpty() +{ + const auto result = Base64Decode("", 0); + CPPUNIT_ASSERT_EQUAL(SBuf(), result); +} + +void +TestBase64Decode::testDecodeEncodeRoundtrip() +{ + // decode a known base64 string, then re-encode and compare to original + for (const auto &v : knownVectors) { + const auto decoded = Base64Decode(v.encoded, strlen(v.encoded)); + const auto reencoded = Base64Encode(decoded); + CPPUNIT_ASSERT_EQUAL(SBuf(v.encoded), reencoded); + } +} + +void +TestBase64Decode::testEncodeDecodeRoundtrip() +{ + // encode a plaintext string, then decode and compare to original + static const char * const plainTexts[] = { + "Hello, World!", + "The quick brown fox jumps over the lazy dog", + "\x00\x01\x02\x03\xff\xfe\xfd", + "", + }; + static const size_t plainLens[] = { 13, 43, 7, 0 }; + + for (size_t i = 0; i < sizeof(plainTexts) / sizeof(*plainTexts); ++i) { + SBuf original(plainTexts[i], plainLens[i]); + const auto encoded = Base64Encode(original); + const auto decoded = Base64Decode(encoded); + CPPUNIT_ASSERT_EQUAL(original, decoded); + } +} + +void +TestBase64Decode::testDecodeInvalidChars() +{ + // characters outside the base64 alphabet + CPPUNIT_ASSERT_THROW(Base64Decode("!!!!", 4), DecodeException); + CPPUNIT_ASSERT_THROW(Base64Decode("SGVs!G8=", 8), DecodeException); + CPPUNIT_ASSERT_THROW(Base64Decode("abc$", 4), DecodeException); +} + +void +TestBase64Decode::testDecodeInvalidPadding() +{ + // padding in wrong position or wrong amount + CPPUNIT_ASSERT_THROW(Base64Decode("=YWJj", 5), DecodeException); + CPPUNIT_ASSERT_THROW(Base64Decode("YWJj====", 8), DecodeException); +} + +void +TestBase64Decode::testDecodeIncomplete() +{ + // a single base64 character cannot represent a complete byte + CPPUNIT_ASSERT_THROW(Base64Decode("A", 1), DecodeException); +} + +int +main(int argc, char *argv[]) +{ + return TestProgram().run(argc, argv); +} \ No newline at end of file