diff --git a/.gitignore b/.gitignore index 83849ba8..2c1a0d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /bin /bin64 /_build* +/build_cmake temp # Emacs diff --git a/include/boost/http_proto/fields.hpp b/include/boost/http_proto/fields.hpp index 607e8e51..456d7034 100644 --- a/include/boost/http_proto/fields.hpp +++ b/include/boost/http_proto/fields.hpp @@ -343,6 +343,44 @@ class fields final } }; +/** Format the container to the output stream + + This function serializes the container to + the specified output stream. + + @par Example + @code + fields f; + f.set(field::host, "example.com"); + std::stringstream ss; + ss << f; + assert( ss.str() == "Host: example.com\n" ); + @endcode + + @par Effects + Each field is written to the output stream with + CRLF line endings converted to LF. The trailing + CRLF that indicates the end of headers is not + written. + + @par Complexity + Linear in `f.buffer().size()` + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream, for chaining + + @param os The output stream to write to. + + @param f The container to write. +*/ +BOOST_HTTP_PROTO_DECL +std::ostream& +operator<<( + std::ostream& os, + const fields& f); + } // http_proto } // boost diff --git a/include/boost/http_proto/request.hpp b/include/boost/http_proto/request.hpp index e9741b0f..7831a382 100644 --- a/include/boost/http_proto/request.hpp +++ b/include/boost/http_proto/request.hpp @@ -452,6 +452,42 @@ class request } }; +/** Format the container to the output stream + + This function serializes the container to + the specified output stream. + + @par Example + @code + request req(method::get, "/"); + std::stringstream ss; + ss << req; + @endcode + + @par Effects + Each field is written to the output stream with + CRLF line endings converted to LF. The trailing + CRLF that indicates the end of headers is not + written. + + @par Complexity + Linear in `req.buffer().size()` + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream, for chaining + + @param os The output stream to write to. + + @param req The container to write. +*/ +BOOST_HTTP_PROTO_DECL +std::ostream& +operator<<( + std::ostream& os, + const request& req); + } // http_proto } // boost diff --git a/include/boost/http_proto/request_base.hpp b/include/boost/http_proto/request_base.hpp index 450158d3..ded1972e 100644 --- a/include/boost/http_proto/request_base.hpp +++ b/include/boost/http_proto/request_base.hpp @@ -14,6 +14,8 @@ #include #include +#include + namespace boost { namespace http_proto { @@ -299,6 +301,42 @@ class request_base http_proto::version v); }; +/** Format the container to the output stream + + This function serializes the container to + the specified output stream. + + @par Example + @code + request_base req; + std::stringstream ss; + ss << req; + @endcode + + @par Effects + Each field is written to the output stream with + CRLF line endings converted to LF. The trailing + CRLF that indicates the end of headers is not + written. + + @par Complexity + Linear in `req.buffer().size()` + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream, for chaining + + @param os The output stream to write to. + + @param req The container to write. +*/ +BOOST_HTTP_PROTO_DECL +std::ostream& +operator<<( + std::ostream& os, + const request_base& req); + } // http_proto } // boost diff --git a/include/boost/http_proto/response.hpp b/include/boost/http_proto/response.hpp index 0650abb7..5b04a8a2 100644 --- a/include/boost/http_proto/response.hpp +++ b/include/boost/http_proto/response.hpp @@ -448,6 +448,42 @@ class response } }; +/** Format the container to the output stream + + This function serializes the container to + the specified output stream. + + @par Example + @code + response res(status::ok); + std::stringstream ss; + ss << res; + @endcode + + @par Effects + Each field is written to the output stream with + CRLF line endings converted to LF. The trailing + CRLF that indicates the end of headers is not + written. + + @par Complexity + Linear in `res.buffer().size()` + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream, for chaining + + @param os The output stream to write to. + + @param res The container to write. +*/ +BOOST_HTTP_PROTO_DECL +std::ostream& +operator<<( + std::ostream& os, + const response& res); + } // http_proto } // boost diff --git a/include/boost/http_proto/response_base.hpp b/include/boost/http_proto/response_base.hpp index 3708b0d9..8452049e 100644 --- a/include/boost/http_proto/response_base.hpp +++ b/include/boost/http_proto/response_base.hpp @@ -16,6 +16,8 @@ #include #include +#include + namespace boost { namespace http_proto { @@ -183,6 +185,42 @@ class response_base http_proto::version v); }; +/** Format the container to the output stream + + This function serializes the container to + the specified output stream. + + @par Example + @code + response_base res; + std::stringstream ss; + ss << res; + @endcode + + @par Effects + Each field is written to the output stream with + CRLF line endings converted to LF. The trailing + CRLF that indicates the end of headers is not + written. + + @par Complexity + Linear in `res.buffer().size()` + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream, for chaining + + @param os The output stream to write to. + + @param res The container to write. +*/ +BOOST_HTTP_PROTO_DECL +std::ostream& +operator<<( + std::ostream& os, + const response_base& res); + } // http_proto } // boost diff --git a/src/fields_base.cpp b/src/fields_base.cpp index a58e4a81..1b1ff5e5 100644 --- a/src/fields_base.cpp +++ b/src/fields_base.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -857,7 +858,38 @@ operator<<( std::ostream& os, const fields_base& f) { - return os << f.buffer(); + auto buf = f.buffer(); + std::size_t i = 0; + + while (i < buf.size()) { + if (i + 1 < buf.size() && + buf[i] == '\r' && + buf[i+1] == '\n') { + // Check if this is the trailing CRLF (at the end) + if (i + 2 == buf.size()) { + // This is the trailing CRLF, don't output it + break; + } + // Replace CRLF with LF + os << '\n'; + i += 2; + } else { + os << buf[i]; + i++; + } + } + + return os; +} + +//------------------------------------------------ + +std::ostream& +operator<<( + std::ostream& os, + const fields& f) +{ + return operator<<(os, static_cast(f)); } //------------------------------------------------ @@ -1496,3 +1528,4 @@ length( } // http_proto } // boost + diff --git a/src/request_base.cpp b/src/request_base.cpp index 85dc6fd2..ce714d70 100644 --- a/src/request_base.cpp +++ b/src/request_base.cpp @@ -10,8 +10,11 @@ // #include +#include +#include #include +#include namespace boost { namespace http_proto { @@ -122,5 +125,25 @@ set_start_line_impl( h_.on_start_line(); } +//------------------------------------------------ + +std::ostream& +operator<<( + std::ostream& os, + const request_base& req) +{ + return operator<<(os, static_cast(req)); +} + +//------------------------------------------------ + +std::ostream& +operator<<( + std::ostream& os, + const request& req) +{ + return operator<<(os, static_cast(req)); +} + } // http_proto } // boost diff --git a/src/response_base.cpp b/src/response_base.cpp index f8e4cdbe..31cb76cf 100644 --- a/src/response_base.cpp +++ b/src/response_base.cpp @@ -8,8 +8,11 @@ // #include +#include +#include #include +#include namespace boost { namespace http_proto { @@ -57,5 +60,25 @@ set_start_line_impl( h_.on_start_line(); } +//------------------------------------------------ + +std::ostream& +operator<<( + std::ostream& os, + const response_base& res) +{ + return operator<<(os, static_cast(res)); +} + +//------------------------------------------------ + +std::ostream& +operator<<( + std::ostream& os, + const response& res) +{ + return operator<<(os, static_cast(res)); +} + } // http_proto } // boost diff --git a/test/unit/ostream.cpp b/test/unit/ostream.cpp new file mode 100644 index 00000000..9172f8ed --- /dev/null +++ b/test/unit/ostream.cpp @@ -0,0 +1,205 @@ +// +// Copyright (c) 2025 GitHub Copilot +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_proto +// + +// Test that header file is self-contained. +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include + +namespace boost { +namespace http_proto { + +struct ostream_test +{ + void + testFields() + { + // Test fields + { + fields f; + f.set(field::host, "example.com"); + f.set(field::content_type, "text/html"); + + std::stringstream ss; + ss << f; + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should contain LF + BOOST_TEST(result.find("\n") != std::string::npos); + + // Should not end with newline (no trailing CRLF) + BOOST_TEST(!result.empty() && result.back() != '\n'); + + // Should contain the field names and values + BOOST_TEST(result.find("Host: example.com") != std::string::npos); + BOOST_TEST(result.find("Content-Type: text/html") != std::string::npos); + } + + // Test fields_base + { + fields f; + f.set(field::server, "test-server"); + + std::stringstream ss; + ss << static_cast(f); + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should contain LF + BOOST_TEST(result.find("\n") != std::string::npos); + + // Should not end with newline + BOOST_TEST(!result.empty() && result.back() != '\n'); + } + } + + void + testRequest() + { + // Test request + { + request req(method::get, "/"); + req.set(field::host, "example.com"); + req.set(field::user_agent, "test-agent"); + + std::stringstream ss; + ss << req; + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should contain LF + BOOST_TEST(result.find("\n") != std::string::npos); + + // Should not end with newline + BOOST_TEST(!result.empty() && result.back() != '\n'); + + // Should start with request line + BOOST_TEST(result.find("GET / HTTP/1.1") == 0); + + // Should contain headers + BOOST_TEST(result.find("Host: example.com") != std::string::npos); + BOOST_TEST(result.find("User-Agent: test-agent") != std::string::npos); + } + + // Test request_base + { + request req(method::post, "/data"); + + std::stringstream ss; + ss << static_cast(req); + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should start with request line + BOOST_TEST(result.find("POST /data HTTP/1.1") == 0); + } + } + + void + testResponse() + { + // Test response + { + response res(status::ok); + res.set(field::server, "test-server"); + res.set(field::content_type, "text/plain"); + + std::stringstream ss; + ss << res; + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should contain LF + BOOST_TEST(result.find("\n") != std::string::npos); + + // Should not end with newline + BOOST_TEST(!result.empty() && result.back() != '\n'); + + // Should start with status line + BOOST_TEST(result.find("HTTP/1.1 200 OK") == 0); + + // Should contain headers + BOOST_TEST(result.find("Server: test-server") != std::string::npos); + BOOST_TEST(result.find("Content-Type: text/plain") != std::string::npos); + } + + // Test response_base + { + response res(status::not_found); + + std::stringstream ss; + ss << static_cast(res); + + std::string result = ss.str(); + + // Should not contain CRLF + BOOST_TEST(result.find("\r\n") == std::string::npos); + + // Should start with status line + BOOST_TEST(result.find("HTTP/1.1 404 Not Found") == 0); + } + } + + void + testEmptyMessages() + { + // Test empty fields + { + fields f; + + std::stringstream ss; + ss << f; + + std::string result = ss.str(); + + // Empty fields should produce empty output (no trailing CRLF) + BOOST_TEST(result.empty()); + } + } + + void + run() + { + testFields(); + testRequest(); + testResponse(); + testEmptyMessages(); + } +}; + +TEST_SUITE( + ostream_test, + "boost.http_proto.ostream" +); + +} // http_proto +} // boost