From 2346fa1077d47d6a979de920febeda70a1290e59 Mon Sep 17 00:00:00 2001 From: mo Date: Wed, 4 Nov 2026 22:48:32 +0000 Subject: [PATCH] fix: handle multiline menu script arguments (#1052) * fix: handle multiline menu script arguments * chore: avoid string concatination via += and extract method --------- Co-authored-by: Jan Laupetin --- src/ObjWriting/Menu/AbstractMenuWriter.cpp | 35 +++++++++++- .../Menu/AbstractMenuWriterTest.cpp | 54 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 test/ObjWritingTests/Menu/AbstractMenuWriterTest.cpp diff --git a/src/ObjWriting/Menu/AbstractMenuWriter.cpp b/src/ObjWriting/Menu/AbstractMenuWriter.cpp index a272d4ebd..b6358b5df 100644 --- a/src/ObjWriting/Menu/AbstractMenuWriter.cpp +++ b/src/ObjWriting/Menu/AbstractMenuWriter.cpp @@ -8,6 +8,38 @@ #include #include +namespace +{ + /** + * \brief Escapes new lines in script whenever they are encountered in a script. + * e.g. "test" becomes "test\n". Occurrences outside of strings are not escaped. + * \param script The original script input. + * \return The script string with newlines in strings escaped. + */ + std::string EscapeStringNewLines(const char* script) + { + auto inString = false; + auto escaped = false; + std::ostringstream scriptString; + for (const auto* character = script; *character; ++character) + { + // Native script arguments can contain literal line breaks. + if (inString && *character == '\n') + scriptString << "\\n"; + else if (inString && *character == '\r') + scriptString << "\\r"; + else + scriptString << *character; + + if (*character == '"' && !escaped) + inString = !inString; + escaped = inString && *character == '\\' && !escaped; + } + + return std::move(scriptString).str(); + } +} // namespace + namespace menu { AbstractBaseWriter::AbstractBaseWriter(std::ostream& stream) @@ -66,8 +98,7 @@ namespace menu std::vector AbstractBaseWriter::CreateScriptTokenList(const char* script) { - const std::string scriptString(script); - std::istringstream stringStream(scriptString); + std::istringstream stringStream(EscapeStringNewLines(script)); ParserSingleInputStream inputStream(stringStream, "MenuScript"); SimpleLexer::Config lexerConfig; diff --git a/test/ObjWritingTests/Menu/AbstractMenuWriterTest.cpp b/test/ObjWritingTests/Menu/AbstractMenuWriterTest.cpp new file mode 100644 index 000000000..b69fb8f2f --- /dev/null +++ b/test/ObjWritingTests/Menu/AbstractMenuWriterTest.cpp @@ -0,0 +1,54 @@ +#include "Menu/AbstractMenuWriter.h" + +#include +#include +#include +#include +#include + +namespace +{ + class TestMenuWriter final : public menu::AbstractBaseWriter + { + public: + explicit TestMenuWriter(std::ostream& stream) + : AbstractBaseWriter(stream) + { + } + + using AbstractBaseWriter::CreateScriptTokenList; + using AbstractBaseWriter::WriteEscapedString; + }; + + TEST_CASE("AbstractMenuWriter: Preserves literal line breaks in script arguments", "[menu][writing]") + { + const std::string lineBreak = GENERATE("\n", "\r", "\r\n"); + const auto argument = std::format("onlinegame 0;\txstopprivateparty{}", lineBreak); + const auto script = std::format(R"("exec" "{}" ; )", argument); + const auto tokens = TestMenuWriter::CreateScriptTokenList(script.c_str()); + + REQUIRE(tokens.size() == 3); + CHECK(tokens[0] == "exec"); + CHECK(tokens[1] == argument); + CHECK(tokens[2] == ";"); + + std::ostringstream output; + TestMenuWriter writer(output); + writer.WriteEscapedString(tokens[1]); + const auto escapedArgument = output.str(); + + CHECK(escapedArgument.find('\n') == std::string::npos); + CHECK(escapedArgument.find('\r') == std::string::npos); + const auto reloadedTokens = TestMenuWriter::CreateScriptTokenList(escapedArgument.c_str()); + REQUIRE(reloadedTokens.size() == 1); + CHECK(reloadedTokens[0] == argument); + } + + TEST_CASE("AbstractMenuWriter: Preserves script command separators across lines", "[menu][writing]") + { + const auto tokens = TestMenuWriter::CreateScriptTokenList("\"exec\" \"first\" ;\r\n\"exec\" \"second\" ;"); + const std::vector expected{"exec", "first", ";", "exec", "second", ";"}; + + CHECK(tokens == expected); + } +} // namespace