diff --git a/configure.ac b/configure.ac index 8fa781e4dbe..150c5301f9e 100644 --- a/configure.ac +++ b/configure.ac @@ -2573,6 +2573,7 @@ AC_CONFIG_FILES([ src/parser/Makefile src/proxyp/Makefile src/repl/Makefile + src/base64/Makefile src/sbuf/Makefile src/security/Makefile src/security/cert_generators/Makefile diff --git a/src/Makefile.am b/src/Makefile.am index 4480764023d..de1d907e57a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -22,6 +22,7 @@ SUBDIRS = \ time \ debug \ base \ + base64 \ anyp \ helper \ dns \ @@ -982,6 +983,22 @@ tests_testSBufList_LDADD = \ $(XTRA_LIBS) tests_testSBufList_LDFLAGS = $(LIBADD_DL) +check_PROGRAMS += tests/testBase64Encoder +tests_testBase64Encoder_SOURCES = \ + tests/testBase64Encoder.cc +nodist_tests_testBase64Encoder_SOURCES = \ + tests/stub_debug.cc \ + tests/stub_libmem.cc +tests_testBase64Encoder_LDADD = \ + sbuf/libsbuf.la \ + base/libbase.la \ + base64/libbase64.la \ + $(LIBCPPUNIT_LIBS) \ + $(COMPAT_LIB) \ + $(XTRA_LIBS) \ + $(LIBNETTLE_LIBS) +tests_testBase64Encoder_LDFLAGS = $(LIBADD_DL) + check_PROGRAMS += tests/testString tests_testString_SOURCES = \ tests/testString.cc diff --git a/src/base64/Base64Encoder.cc b/src/base64/Base64Encoder.cc new file mode 100644 index 00000000000..0a656048877 --- /dev/null +++ b/src/base64/Base64Encoder.cc @@ -0,0 +1,183 @@ +/* + * 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 "base/TextException.h" +#include "base64/Base64Encoder.h" + +Base64Encoder::Base64Encoder(size_t maxEncodedSize) + : std::ostream(nullptr), + maxEncodedSize_(maxEncodedSize), + streamBuffer_(*this) +{ + base64_encode_init(&ctx_); + rdbuf(&streamBuffer_); + clear(); +} + +Base64Encoder::Base64Encoder(const SBuf &input, size_t maxEncodedSize) + : Base64Encoder(maxEncodedSize) +{ + // Encode the input immediately - will throw if too large + *this << input; +} + +Base64Encoder::~Base64Encoder() +{ + // Ensure encoding is finalized; log but don't propagate exceptions to avoid terminate during unwinding + try { + streamBuffer_.pubsync(); + } catch (const std::exception &e) { + debugs(0, DBG_CRITICAL, "Base64Encoder dtor error: " << e.what()); + } catch (...) { + debugs(0, DBG_CRITICAL, "Base64Encoder dtor unknown error"); + } +} + +SBuf +Base64Encoder::buf() +{ + flush(); + return sink_; +} + +Base64Encoder& +Base64Encoder::clearBuf() +{ + flush(); + sink_.clear(); + base64_encode_init(&ctx_); + finalized_ = false; + clear(); // Clear stream error state (badbit, failbit, etc.) + return *this; +} + +std::ostream& +operator<<(std::ostream& os, Base64Encoder& encoder) +{ + encoder.flush(); + return encoder.sink_.print(os); +} + +// --- Base64Encoder encoding implementation --- + +void +Base64Encoder::checkSizeLimit(size_t newInputBytes) +{ + // Since we sync after every append, sink_.length() is always up to date + // The additional encoded size for newInputBytes raw bytes is BASE64_ENCODE_RAW_LENGTH + const size_t additionalEncoded = BASE64_ENCODE_RAW_LENGTH(newInputBytes); + if (sink_.length() + additionalEncoded > maxEncodedSize_) + throw TextException("Base64Encoder output size limit exceeded", Here()); +} + +void +Base64Encoder::encodePending() +{ + if (streamBuffer_.inputBufferPos_ == 0) + return; + + checkSizeLimit(0); // No additional new input, just check pending + + const size_t maxEncoded = BASE64_ENCODE_LENGTH(streamBuffer_.inputBufferPos_) + BASE64_ENCODE_FINAL_LENGTH; + sink_.reserveSpace(maxEncoded); + char *dst = sink_.rawAppendStart(maxEncoded); + size_t encoded = base64_encode_update(&ctx_, dst, streamBuffer_.inputBufferPos_, + reinterpret_cast(streamBuffer_.inputBuffer_)); + sink_.rawAppendFinish(dst, encoded); + streamBuffer_.inputBufferPos_ = 0; + streamBuffer_.setp(streamBuffer_.inputBuffer_, streamBuffer_.inputBuffer_ + 4096); +} + +void +Base64Encoder::finalize() +{ + if (finalized_) + return; + + encodePending(); + + const size_t maxFinal = BASE64_ENCODE_FINAL_LENGTH; + sink_.reserveSpace(maxFinal); + char *dst = sink_.rawAppendStart(maxFinal); + size_t encoded = base64_encode_final(&ctx_, dst); + sink_.rawAppendFinish(dst, encoded); + + finalized_ = true; +} + +// --- Base64StreamBuf implementation --- + +Base64Encoder::Base64StreamBuf::Base64StreamBuf(Base64Encoder &encoder) + : encoder_(encoder) +{ + inputBuffer_ = static_cast(memAllocate(MEM_4K_BUF)); + setp(inputBuffer_, inputBuffer_ + 4096); +} + +Base64Encoder::Base64StreamBuf::~Base64StreamBuf() +{ + memFree(inputBuffer_, MEM_4K_BUF); + inputBuffer_ = nullptr; + + if (!encoder_.finalized_) { + try { + encoder_.finalize(); + } catch (const std::exception &e) { + debugs(0, DBG_CRITICAL, "Base64StreamBuf dtor error: " << e.what()); + } catch (...) { + debugs(0, DBG_CRITICAL, "Base64StreamBuf dtor unknown error"); + } + } +} + +int +Base64Encoder::Base64StreamBuf::overflow(int_type ch) +{ + if (ch != traits_type::eof()) { + encoder_.checkSizeLimit(1); + inputBuffer_[inputBufferPos_++] = static_cast(ch); + if (inputBufferPos_ >= 4096) + encoder_.encodePending(); + } + encoder_.encodePending(); // Sync after every append + return ch; +} + +int +Base64Encoder::Base64StreamBuf::sync() +{ + encoder_.encodePending(); + encoder_.finalize(); + return 0; +} + +std::streamsize +Base64Encoder::Base64StreamBuf::xsputn(const char *s, std::streamsize n) +{ + std::streamsize written = 0; + + while (n > 0) { + const size_t space = 4096 - inputBufferPos_; + const size_t toCopy = std::min(static_cast(n), space); + + encoder_.checkSizeLimit(toCopy); + + memcpy(inputBuffer_ + inputBufferPos_, s, toCopy); + inputBufferPos_ += toCopy; + s += toCopy; + n -= toCopy; + written += toCopy; + + if (inputBufferPos_ >= 4096) + encoder_.encodePending(); + } + + encoder_.encodePending(); // Sync after every append + return written; +} diff --git a/src/base64/Base64Encoder.h b/src/base64/Base64Encoder.h new file mode 100644 index 00000000000..b2047c4bf19 --- /dev/null +++ b/src/base64/Base64Encoder.h @@ -0,0 +1,115 @@ +/* + * 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_BASE64_BASE64ENCODER_H +#define SQUID_SRC_BASE64_BASE64ENCODER_H + +#include "base/PackableStream.h" +#include "base64.h" +#include "mem/forward.h" +#include "sbuf/SBuf.h" + +#include +#include + +/** Stream interface to write to a Base64-encoded SBuf. + * + * Data is appended using standard operator << semantics. The data is + * base64-encoded on the fly as it is written. The encoded result can be + * retrieved using the buf() method. + * + * This class inherits from std::ostream to provide a familiar streaming + * interface, similar to SBufStream. + */ +class Base64Encoder : public std::ostream +{ +public: + /// Special value indicating no size limit + static constexpr size_t noLimit = std::numeric_limits::max(); + + /// Create a Base64Encoder with optional maximum encoded output size limit + /// \param maxEncodedSize maximum encoded output size (default: noLimit) + explicit Base64Encoder(size_t maxEncodedSize = noLimit); + + /// Create a Base64Encoder and immediately encode the contents of a SBuf + /// \param input SBuf to encode immediately + /// \param maxEncodedSize maximum encoded output size (default: noLimit) + explicit Base64Encoder(const SBuf &input, size_t maxEncodedSize = noLimit); + + /// Destructor finalizes the encoding + ~Base64Encoder() override; + + /// Non-copyable (std::ostream is non-copyable) + Base64Encoder(const Base64Encoder&) = delete; + Base64Encoder& operator=(const Base64Encoder&) = delete; + + /// Non-movable (std::ostream is non-movable) + Base64Encoder(Base64Encoder&&) = delete; + Base64Encoder& operator=(Base64Encoder&&) = delete; + + /// Get the encoded result (finalizes encoding if not already done) + SBuf buf(); + + /// Clear the stream's backing store and reset encoder state + Base64Encoder& clearBuf(); + + /// Stream output operator for printing the encoded contents (finalizes encoding) + friend std::ostream& operator<<(std::ostream& os, Base64Encoder& encoder); + +private: + /** Custom streambuf that buffers input data and delegates encoding to Base64Encoder. + * + * Only manages the input buffer. All encoding logic, size checking, + * and state management lives in Base64Encoder. + */ + class Base64StreamBuf : public std::streambuf + { + public: + Base64StreamBuf(Base64Encoder &encoder); + ~Base64StreamBuf() override; + + protected: + int_type overflow(int_type ch = traits_type::eof()) override; + int sync() override; + std::streamsize xsputn(const char *s, std::streamsize n) override; + + private: + Base64Encoder &encoder_; + char *inputBuffer_ = nullptr; + size_t inputBufferPos_ = 0; + + // Base64Encoder needs access to these + friend class Base64Encoder; + }; + + // Encoding state (moved from Base64StreamBuf) + const size_t maxEncodedSize_ = noLimit; + SBuf sink_; + base64_encode_ctx ctx_; + bool finalized_ = false; + + // Encoding implementation (moved from Base64StreamBuf) + void checkSizeLimit(size_t newInputBytes); + void encodePending(); + void finalize(); + + Base64StreamBuf streamBuffer_; +}; + +/// Helper to encode multiple arguments and return the Base64-encoded result +/// Usage: SBuf result = ToBase64(arg1, arg2, ...); +template +inline +SBuf ToBase64(Args&&... args) +{ + Base64Encoder encoder; + (encoder << ... << args); + return encoder.buf(); +} + +#endif /* SQUID_SRC_BASE64_BASE64ENCODER_H */ diff --git a/src/base64/Makefile.am b/src/base64/Makefile.am new file mode 100644 index 00000000000..7a1662e3e4d --- /dev/null +++ b/src/base64/Makefile.am @@ -0,0 +1,20 @@ +## 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 $(top_srcdir)/src/Common.am + +noinst_LTLIBRARIES = libbase64.la + +libbase64_la_SOURCES = \ + Base64Encoder.cc \ + Base64Encoder.h + +libbase64_la_LIBADD = \ + $(top_builddir)/lib/libmiscencoding.la \ + $(COMPAT_LIB) \ + $(LIBNETTLE_LIBS) \ + $(XTRA_LIBS) diff --git a/src/tests/testBase64Encoder.cc b/src/tests/testBase64Encoder.cc new file mode 100644 index 00000000000..2848aaaa7c3 --- /dev/null +++ b/src/tests/testBase64Encoder.cc @@ -0,0 +1,244 @@ +/* + * 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 "base/TextException.h" +#include "base64/Base64Encoder.h" +#include "compat/cppunit.h" +#include "event.h" +#include "MemObject.h" +#include "unitTestMain.h" + +#include "sbuf/Stream.h" + +/* + * test the Base64Encoder functionalities + */ + +class TestBase64Encoder : public CPPUNIT_NS::TestFixture +{ + CPPUNIT_TEST_SUITE(TestBase64Encoder); + CPPUNIT_TEST(testBase64EncoderDefault); + CPPUNIT_TEST(testBase64EncoderWithMaxSize); + CPPUNIT_TEST(testBase64EncoderWithInput); + CPPUNIT_TEST(testBase64EncoderStreaming); + CPPUNIT_TEST(testBase64EncoderBufAndClear); + CPPUNIT_TEST(testBase64EncoderPrint); + CPPUNIT_TEST(testBase64EncoderToBase64); + CPPUNIT_TEST(testBase64EncoderLargeInput); + CPPUNIT_TEST(testBase64EncoderMaxSize); + CPPUNIT_TEST(testBase64EncoderMaxSizeExceeded); + CPPUNIT_TEST(testBase64EncoderMaxSizeSBuf); + CPPUNIT_TEST(testBase64EncoderMaxSizeBoundary); + CPPUNIT_TEST(testBase64EncoderMaxSizeClear); + CPPUNIT_TEST_SUITE_END(); + +protected: + void testBase64EncoderDefault(); + void testBase64EncoderWithMaxSize(); + void testBase64EncoderWithInput(); + void testBase64EncoderStreaming(); + void testBase64EncoderBufAndClear(); + void testBase64EncoderPrint(); + void testBase64EncoderToBase64(); + void testBase64EncoderLargeInput(); + void testBase64EncoderMaxSize(); + void testBase64EncoderMaxSizeExceeded(); + void testBase64EncoderMaxSizeSBuf(); + void testBase64EncoderMaxSizeBoundary(); + void testBase64EncoderMaxSizeClear(); +}; +CPPUNIT_TEST_SUITE_REGISTRATION( TestBase64Encoder ); + +/* let this test link sanely */ +void +eventAdd(const char *, EVH *, void *, double, int, bool) +{} +int64_t +MemObject::endOffset() const +{ return 0; } +/* end of stubs */ + +void +TestBase64Encoder::testBase64EncoderDefault() +{ + Base64Encoder encoder; + encoder << "Hello"; + auto result = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("SGVsbG8="), result); +} + +void +TestBase64Encoder::testBase64EncoderWithMaxSize() +{ + // Test encoder with max size limit that is not exceeded + Base64Encoder encoder(100); + encoder << "Test"; + auto result = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("VGVzdA=="), result); +} + +void +TestBase64Encoder::testBase64EncoderWithInput() +{ + SBuf input("Direct input"); + Base64Encoder encoder(input); + auto result = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("RGlyZWN0IGlucHV0"), result); +} + +void +TestBase64Encoder::testBase64EncoderStreaming() +{ + Base64Encoder encoder; + encoder << "Part1" << "Part2" << 123; + auto result = encoder.buf(); + // "Part1Part2123" base64 encoded + CPPUNIT_ASSERT_EQUAL(SBuf("UGFydDFQYXJ0MjEyMw=="), result); +} + +void +TestBase64Encoder::testBase64EncoderBufAndClear() +{ + Base64Encoder encoder; + encoder << "First"; + auto result1 = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("Rmlyc3Q="), result1); + + encoder.clearBuf(); + encoder << "Second"; + auto result2 = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("U2Vjb25k"), result2); + + CPPUNIT_ASSERT(result1 != result2); +} + +void +TestBase64Encoder::testBase64EncoderPrint() +{ + Base64Encoder encoder; + encoder << "Printable"; + SBufStream ssb; + ssb << encoder; + CPPUNIT_ASSERT_EQUAL(SBuf("UHJpbnRhYmxl"), ssb.buf()); +} + +void +TestBase64Encoder::testBase64EncoderToBase64() +{ + auto result = ToBase64("A", "B", "C"); + CPPUNIT_ASSERT_EQUAL(SBuf("QUJD"), result); +} + +void +TestBase64Encoder::testBase64EncoderLargeInput() +{ + // Create a string larger than the internal 4KB buffer (4096 bytes) + SBuf largeInput; + std::string as(5000, 'A'); + largeInput.append(as.c_str(), as.length()); + + Base64Encoder encoder; + encoder << largeInput; + auto result = encoder.buf(); + + // 5000 'A' chars = 5000 bytes = base64 encoded = 6668 chars (with padding) + CPPUNIT_ASSERT_EQUAL(static_cast(6668), result.length()); + + // Verify known prefix: 5000 'A's encoded starts with "QUFBQUFB..." (AAA->QUFB repeated) + 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]); + + // Verify padding at the end (5000 % 3 = 2, so 1 padding char '=') + CPPUNIT_ASSERT_EQUAL(static_cast('='), result[result.length() - 1]); + // Second to last is 'E' (from 'AA' -> 'QUE') + CPPUNIT_ASSERT_EQUAL(static_cast('E'), result[result.length() - 2]); +} + +void +TestBase64Encoder::testBase64EncoderMaxSize() +{ + // Test encoder with max size limit that is not exceeded + Base64Encoder encoder(50); + encoder << "Hello"; // "Hello" = 5 bytes -> base64 = 8 chars (with padding) + auto result = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("SGVsbG8="), result); +} + +void +TestBase64Encoder::testBase64EncoderMaxSizeExceeded() +{ + // Test that encoding over the limit throws TextException + Base64Encoder encoder(10); // Very small limit + encoder.exceptions(std::ios::badbit); // Enable exceptions on stream + CPPUNIT_ASSERT_THROW(encoder << "This is a long string that exceeds the limit", TextException); +} + +void +TestBase64Encoder::testBase64EncoderMaxSizeSBuf() +{ + // Test SBuf constructor with max size - input too large should throw + SBuf input("Direct input"); // 12 bytes -> base64 = 16 chars + bool threw = false; + // cannot use CPPUNIT_ASSERT_THROW here because the exception is thrown in the constructor + try { + Base64Encoder encoder(input, 10); // Limit too small - encoding happens in constructor + encoder.exceptions(std::ios::badbit); // Enable exceptions to catch stream errors + encoder.buf(); // This will throw if stream is in error state + } catch (const TextException &e) { + threw = true; + CPPUNIT_ASSERT(std::string(e.what()).find("size limit exceeded") != std::string::npos); + } catch (const std::ios::failure &e) { + // Stream throws ios::failure when exceptions enabled and badbit set + threw = true; + } + CPPUNIT_ASSERT(threw); +} + +void +TestBase64Encoder::testBase64EncoderMaxSizeBoundary() +{ + // Test exact boundary: "AB" = 2 bytes -> base64 = 4 chars + Base64Encoder encoder(4); + encoder << "AB"; + auto result = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("QUI="), result); + + // One more byte should exceed (limit 3 is too small for "AB" which needs 4) + Base64Encoder encoder2(3); + encoder2.exceptions(std::ios::badbit); + CPPUNIT_ASSERT_THROW(encoder2 << "AB", TextException); +} + +void +TestBase64Encoder::testBase64EncoderMaxSizeClear() +{ + // Test that clearBuf preserves the max size limit + Base64Encoder encoder(20); + encoder << "First"; + auto result1 = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("Rmlyc3Q="), result1); + + encoder.clearBuf(); + // Should still enforce limit + encoder.exceptions(std::ios::badbit); + CPPUNIT_ASSERT_THROW(encoder << "This is a very long string that exceeds limit", TextException); + // But encoding within limit should work after clear + encoder.clearBuf(); + encoder << "Short"; + auto result2 = encoder.buf(); + CPPUNIT_ASSERT_EQUAL(SBuf("U2hvcnQ="), result2); +} + +int +main(int argc, char *argv[]) +{ + return TestProgram().run(argc, argv); +} \ No newline at end of file