From 0056038ff4bff45499d81eca32d0063d366db142 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Mon, 15 Apr 2024 10:58:11 -0500 Subject: [PATCH] Toponaming/Base: Add ASCII stream output class (#13209) * Toponaming/Base: Add ASCII stream output class * Remove the ref from std::string * Update based on review comments --- src/Base/Stream.h | 91 ++++++++++++++++++++++++ tests/src/Base/CMakeLists.txt | 1 + tests/src/Base/Stream.cpp | 127 ++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 tests/src/Base/Stream.cpp diff --git a/src/Base/Stream.h b/src/Base/Stream.h index e7bec16d54..8b816b2754 100644 --- a/src/Base/Stream.h +++ b/src/Base/Stream.h @@ -256,6 +256,97 @@ private: std::ostringstream _ss; }; +/** + * The TextOutputStream class provides writing of ASCII data to an ostream, with custom handling + * for std::string to make it easier to write a single multi-line (or multi-word) string. This is + * designed for easy compatibility with the LinkStage3 implementation of the OutputStream class, + * used to store StringHashers for the toponaming mitigation technique. + */ +class BaseExport TextOutputStream: public Stream +{ +public: + /** Constructor + * @param rout: upstream output + */ + explicit TextOutputStream(std::ostream& rout) + : _out(rout) + {} + + TextOutputStream(const TextOutputStream&) = delete; + + TextOutputStream(const TextOutputStream&&) noexcept = delete; + + void operator=(const TextOutputStream&) = delete; + + void operator=(const TextOutputStream&&) = delete; + + ~TextOutputStream() override = default; + + template + TextOutputStream& operator<<(T object) + { + _out << object << '\n'; + return *this; + } + + TextOutputStream& operator<<(const char* object) + { + std::string_view str(object); + + // Count the lines so that we can deal with potential EOL conversions by external software + uint32_t lineCount = 0; + for (const auto character : str) { + if (character == '\n') { + ++lineCount; + } + } + // Stores the line count followed by a colon as the delimiter. We don't use + // whitespace because the input stream may also start with whitespace. + _out << lineCount << ':'; + + // Store the text, and normalize the end of line to a single '\n'. + bool foundSlashR = false; + for (const auto character : str) { + if (character == '\r') { + foundSlashR = true; + continue; + } + if (foundSlashR) { + if (character != '\n') { + // We allow '\r' if it is not at the end of a line + _out.put('\r'); + } + foundSlashR = false; // Reset for next time + } + _out.put(character); + } + + // Add an extra newline as the delimiter for the following data. + _out.put('\n'); + return *this; + } + + TextOutputStream& operator<<(const std::string& object) + { + return (*this) << object.c_str(); + } + + TextOutputStream& operator<<(char object) + { + _out.put(object); + return *this; + } + + explicit operator bool() const + { + // test if _Ipfx succeeded + return !_out.eof(); + } + +private: + std::ostream& _out; +}; + // ---------------------------------------------------------------------------- /** diff --git a/tests/src/Base/CMakeLists.txt b/tests/src/Base/CMakeLists.txt index 9fe58910bd..2feb4d2eb0 100644 --- a/tests/src/Base/CMakeLists.txt +++ b/tests/src/Base/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/Quantity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Reader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Rotation.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Stream.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TimeInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Tools.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Tools2D.cpp diff --git a/tests/src/Base/Stream.cpp b/tests/src/Base/Stream.cpp new file mode 100644 index 0000000000..93354e5d31 --- /dev/null +++ b/tests/src/Base/Stream.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" + +#ifdef _MSC_VER +#pragma warning(disable : 4996) +#endif + +#include "Base/Stream.h" + + +class TextOutputStreamTest: public ::testing::Test +{ +protected: + void SetUp() override + {} + + void TearDown() override + {} +}; + +TEST_F(TextOutputStreamTest, singleLineCharStar) +{ + // Arrange + const std::string testString("Single line const char *"); + std::ostringstream ss; + Base::TextOutputStream tos(ss); + + // Act + tos << testString; + + // Assert - the number of newlines in the string, a colon, the string, a newline + EXPECT_EQ(std::string("0:") + testString + "\n", ss.str()); +} + +TEST_F(TextOutputStreamTest, multiLineCharStar) +{ + // Arrange + const std::string testString("Multi-line\nconst char *"); + std::ostringstream ss; + Base::TextOutputStream tos(ss); + + // Act + tos << testString; // Testing it with a string instead of a const char * -- failing right now + + // Assert - the number of newlines in the string, a colon, the string, a newline + EXPECT_EQ(std::string("1:") + testString + "\n", ss.str()); +} + +TEST_F(TextOutputStreamTest, singleLineCharStarWithCarriageReturns) +{ + // Arrange + const std::string testString("Single-line\rconst char *"); + std::ostringstream ss; + Base::TextOutputStream tos(ss); + + // Act + tos << testString; + + // Assert - the number of newlines in the string, a colon, the string, a newline. Carriage + // returns are left alone because they aren't followed by a newline + EXPECT_EQ(std::string("0:") + testString + "\n", ss.str()); +} + +TEST_F(TextOutputStreamTest, multiLineCharStarWithCarriageReturnsAndNewlines) +{ + // Arrange + const std::string testString("Multi-line\r\nconst char *"); + const std::string testStringWithoutCR("Multi-line\nconst char *"); + std::ostringstream ss; + Base::TextOutputStream tos(ss); + + // Act + tos << testString; + + // Assert - the number of newlines in the string, a colon, the string, a newline, but the string + // has been stripped of the carriage returns. + EXPECT_EQ(std::string("1:") + testStringWithoutCR + "\n", ss.str()); +} + + +class TextStreamIntegrationTest: public ::testing::Test +{ +protected: + void SetUp() override + {} + + void TearDown() override + {} +}; + +TEST_F(TextStreamIntegrationTest, OutputThenInputSimpleMultiLine) +{ + // Arrange + std::string multiLineString("One\nTwo\nThree"); + + // Act + std::ostringstream ssO; + Base::TextOutputStream tos(ssO); + tos << multiLineString; + std::istringstream ssI(ssO.str()); + Base::TextInputStream tis(ssI); + std::string result; + tis >> result; + + // Assert + EXPECT_EQ(multiLineString, result); +} + +TEST_F(TextStreamIntegrationTest, OutputThenInputMultiLineWithCarriageReturns) +{ + // Arrange + std::string multiLineString("One\r\nTwo\r\nThree"); + std::string multiLineStringResult("One\nTwo\nThree"); + + // Act + std::ostringstream ssO; + Base::TextOutputStream tos(ssO); + tos << multiLineString; + std::istringstream ssI(ssO.str()); + Base::TextInputStream tis(ssI); + std::string result; + tis >> result; + + // Assert + EXPECT_EQ(multiLineStringResult, result); +}