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 <[email protected]>
This commit is contained in:
moandJan Laupetin authored and GitHub committed 2026-11-04 23:48:32 +01:00
1 parent 53e1b06cb3
commit 2346fa1077
2 files changed
+87 -2

No files matched your search

+33 -2
View File
@@ -8,6 +8,38 @@
#include <cstdlib>
#include <sstream>
namespace
{
/**
* \brief Escapes new lines in script whenever they are encountered in a script.
* e.g. "test<newline>" 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<std::string> 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;
@@ -0,0 +1,54 @@
#include "Menu/AbstractMenuWriter.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <sstream>
#include <string>
#include <vector>
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<std::string> expected{"exec", "first", ";", "exec", "second", ";"};
CHECK(tokens == expected);
}
} // namespace