Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ SUBDIRS = \
time \
debug \
base \
base64 \
anyp \
helper \
dns \
Expand Down Expand Up @@ -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
Expand Down
183 changes: 183 additions & 0 deletions src/base64/Base64Encoder.cc
Original file line number Diff line number Diff line change
@@ -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<const uint8_t*>(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<char*>(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<char>(ch);
if (inputBufferPos_ >= 4096)
encoder_.encodePending();
}
encoder_.encodePending(); // Sync after every append
return ch;
}

int
Base64Encoder::Base64StreamBuf::sync()
{
encoder_.encodePending();
encoder_.finalize();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sync() implementation essentially violates STL streambuf::sync() API. STL code that we do not control should be able to call this method multiple times, as needed, without being worried that the very first call effectively places (or should place) the stream into a "no more updates" state, a state that Base64Encoder enters when its finalize() method is called.

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<size_t>(static_cast<size_t>(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;
}
115 changes: 115 additions & 0 deletions src/base64/Base64Encoder.h
Original file line number Diff line number Diff line change
@@ -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 <ostream>
#include <limits>

/** 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

@rousskov rousskov Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed inheritance is an API mistake for several reasons, including:

  • std::ostream is meant for writing bytes to various destinations (as defined by derived classes). A base64 encoder should not know or care about the destination of the encoded bytes. I should be able to use the same encoder for writing into a file, a socket, or an SBuf. Changing high-level information into written bytes is the domain of formatting I/O functions (e.g., STL-provided << operators) and I/O manipulators, not std::ostream kids.

  • Base64 encoding requires a special action to finalize the result. std::ostream does not have a caller-convenient, safe, and efficient way for triggering that action. Neither class destructor nor buf() are it, for various reasons.

  • Formatted and locale-specific output (e.g., various STL-provided << operators and manipulators) is not fully compatible with many (all?) known base64 use cases in Squid code, where callers must base64-encode values while following strict syntax rules. An extra space character or the wrong number representation is likely to violate the protocol requirements but is not going to be caught by the compiler. In this context, we probably want to highlight value conversions rather than hide them behind various convenience APIs.

{
public:
/// Special value indicating no size limit
static constexpr size_t noLimit = std::numeric_limits<size_t>::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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burdening this SBuf-based class with explicitly maintaining output size restrictions is probably wrong. The underlying code should not care. Modern users should not care either. Legacy users, if any, would be better off with checking when converting from SBuf into their own storage.


/// 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 <typename... Args>
inline
SBuf ToBase64(Args&&... args)
{
Base64Encoder encoder;
(encoder << ... << args);
return encoder.buf();
}

#endif /* SQUID_SRC_BASE64_BASE64ENCODER_H */
20 changes: 20 additions & 0 deletions src/base64/Makefile.am
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading