mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-09 10:06:08 +00:00
This PR introduces a pair of classes for managing VT parameters that automatically handle range checking and default fallback values, so the individual operations don't have to do that validation themselves. In addition to simplifying the code, this fixes a few cases where we were mishandling missing or extraneous parameters, and adds support for parameter sequences on commands that couldn't previously handle them. This PR also sets a limit on the number of parameters allowed, to help thwart DoS memory consumption attacks. ## References * The new parameter class also introduces the concept of an omitted/default parameter which is not necessarily zero, which is a prerequisite for addressing issue #4417. ## Detailed Description of the Pull Request / Additional comments There are two new classes provide by this PR: a `VTParameter` class, similar in function to a `std::optional<size_t>`, which holds an individual parameter (which may be an omitted/default value); and a `VTParameters` class, similar in function to `gsl:span<VTParameter>`, which holds a sequence of those parameters. Where `VTParameter` differs from `std::optional` is with the inclusion of two cast operators. There is a `size_t` cast that interprets omitted and zero values as 1 (the expected behaviour for most numeric parameters). And there is a generic cast, for use with the enum parameter types, which interprets omitted values as 0 (the expected behaviour for most selective parameters). The advantage of `VTParameters` class is that it has an `at` method that can never fail - out of range values simply return the a default `VTParameter` instance (this is standard behaviour in VT terminals). It also has a `size` method that will always return a minimum count of 1, since an empty parameter list is typically the equivalent of a single "default" parameter, so this guarantees you'll get at least one value when iterating over the list with `size()`. For cases where we just need to call the same dispatch method for every parameter, there is a helper `for_each` method, which repeatedly calls a given predicate function with each value in the sequence. It also collates the returned success values to determine the overall result of the sequence. As with the `size` method, this will always make at least one call, so it correctly handles empty sequences. With those two classes in place, we could get rid of all the parameter validation and default handling code in the `OutputStateMachineEngine`. We now just use the `VTParameters::at` method to grab a parameter and typically pass it straight to the appropriate dispatch method, letting the cast operators automatically handle the assignment of default values. Occasionally we might need a `value_or` call to specify a non-standard default value, but those cases are fairly rare. In some case the `OutputStateMachineEngine` was also checking whether parameters values were in range, but for the most part this shouldn't have been necessary, since that is something the dispatch classes would already have been doing themselves (in the few cases that they weren't, I've now updated them to do so). I've also updated the `InputStateMachineEngine` in a similar way to the `OutputStateMachineEngine`, getting rid of a few of the parameter extraction methods, and simplifying other parts of the implementation. It's not as clean a replacement as the output engine, but there are still benefits in using the new classes. ## Validation Steps Performed For the most part I haven't had to alter existing tests other than accounting for changes to the API. There were a couple of tests I needed to drop because they were checking for failure cases which shouldn't have been failing (unexpected parameters should never be an error), or testing output engine validation that is no longer handled at that level. I've added a few new tests to cover operations that take sequences of selective parameters (`ED`, `EL`, `TBC`, `SM`, and `RM`). And I've extended the cursor movement tests to make sure those operations can handle extraneous parameters that weren't expected. I've also added a test to verify that the state machine will correctly ignore parameters beyond the maximum 32 parameter count limit. I've also manual confirmed that the various test cases given in issues #2101 are now working as expected. Closes #2101
249 lines
8.6 KiB
C++
249 lines
8.6 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "WexTestClass.h"
|
|
#include "../../inc/consoletaeftemplates.hpp"
|
|
|
|
#include "stateMachine.hpp"
|
|
|
|
using namespace WEX::Common;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::TestExecution;
|
|
|
|
namespace Microsoft
|
|
{
|
|
namespace Console
|
|
{
|
|
namespace VirtualTerminal
|
|
{
|
|
class StateMachineTest;
|
|
class TestStateMachineEngine;
|
|
};
|
|
};
|
|
};
|
|
|
|
using namespace Microsoft::Console::VirtualTerminal;
|
|
|
|
class Microsoft::Console::VirtualTerminal::TestStateMachineEngine : public IStateMachineEngine
|
|
{
|
|
public:
|
|
void ResetTestState()
|
|
{
|
|
printed.clear();
|
|
passedThrough.clear();
|
|
csiParams.clear();
|
|
}
|
|
|
|
bool ActionExecute(const wchar_t /* wch */) override { return true; };
|
|
bool ActionExecuteFromEscape(const wchar_t /* wch */) override { return true; };
|
|
bool ActionPrint(const wchar_t /* wch */) override { return true; };
|
|
bool ActionPrintString(const std::wstring_view string) override
|
|
{
|
|
printed += string;
|
|
return true;
|
|
};
|
|
|
|
bool ActionPassThroughString(const std::wstring_view string) override
|
|
{
|
|
passedThrough += string;
|
|
return true;
|
|
};
|
|
|
|
bool ActionEscDispatch(const VTID /* id */) override { return true; };
|
|
|
|
bool ActionVt52EscDispatch(const VTID /*id*/, const VTParameters /*parameters*/) override { return true; };
|
|
|
|
bool ActionClear() override { return true; };
|
|
|
|
bool ActionIgnore() override { return true; };
|
|
|
|
bool ActionOscDispatch(const wchar_t /* wch */,
|
|
const size_t /* parameter */,
|
|
const std::wstring_view /* string */) override
|
|
{
|
|
if (pfnFlushToTerminal)
|
|
{
|
|
pfnFlushToTerminal();
|
|
return true;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
bool ActionSs3Dispatch(const wchar_t /* wch */, const VTParameters /* parameters */) override { return true; };
|
|
|
|
bool ParseControlSequenceAfterSs3() const override { return false; }
|
|
bool FlushAtEndOfString() const override { return false; };
|
|
bool DispatchControlCharsFromEscape() const override { return false; };
|
|
bool DispatchIntermediatesFromEscape() const override { return false; };
|
|
|
|
// ActionCsiDispatch is the only method that's actually implemented.
|
|
bool ActionCsiDispatch(const VTID /*id*/, const VTParameters parameters) override
|
|
{
|
|
// If flush to terminal is registered for a test, then use it.
|
|
if (pfnFlushToTerminal)
|
|
{
|
|
pfnFlushToTerminal();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < parameters.size(); i++)
|
|
{
|
|
csiParams.push_back(parameters.at(i).value_or(0));
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This will only be populated if ActionCsiDispatch is called.
|
|
std::vector<size_t> csiParams;
|
|
|
|
// Flush function for pass-through test.
|
|
std::function<bool()> pfnFlushToTerminal;
|
|
|
|
// Passed through string.
|
|
std::wstring passedThrough;
|
|
|
|
// Printed string.
|
|
std::wstring printed;
|
|
};
|
|
|
|
class Microsoft::Console::VirtualTerminal::StateMachineTest
|
|
{
|
|
TEST_CLASS(StateMachineTest);
|
|
|
|
TEST_CLASS_SETUP(ClassSetup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(ClassCleanup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD(TwoStateMachinesDoNotInterfereWithEachother);
|
|
|
|
TEST_METHOD(PassThroughUnhandled);
|
|
TEST_METHOD(RunStorageBeforeEscape);
|
|
TEST_METHOD(BulkTextPrint);
|
|
TEST_METHOD(PassThroughUnhandledSplitAcrossWrites);
|
|
};
|
|
|
|
void StateMachineTest::TwoStateMachinesDoNotInterfereWithEachother()
|
|
{
|
|
auto firstEnginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
// this dance is required because StateMachine presumes to take ownership of its engine.
|
|
const auto& firstEngine{ *firstEnginePtr.get() };
|
|
StateMachine firstStateMachine{ std::move(firstEnginePtr) };
|
|
|
|
auto secondEnginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
const auto& secondEngine{ *secondEnginePtr.get() };
|
|
StateMachine secondStateMachine{ std::move(secondEnginePtr) };
|
|
|
|
firstStateMachine.ProcessString(L"\x1b[12"); // partial sequence
|
|
secondStateMachine.ProcessString(L"\x1b[3C"); // full sequence on second parser
|
|
firstStateMachine.ProcessString(L";34m"); // completion to previous partial sequence on first parser
|
|
|
|
std::vector<size_t> expectedFirstCsi{ 12u, 34u };
|
|
std::vector<size_t> expectedSecondCsi{ 3u };
|
|
|
|
VERIFY_ARE_EQUAL(expectedFirstCsi, firstEngine.csiParams);
|
|
VERIFY_ARE_EQUAL(expectedSecondCsi, secondEngine.csiParams);
|
|
}
|
|
|
|
void StateMachineTest::PassThroughUnhandled()
|
|
{
|
|
auto enginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
// this dance is required because StateMachine presumes to take ownership of its engine.
|
|
auto& engine{ *enginePtr.get() };
|
|
StateMachine machine{ std::move(enginePtr) };
|
|
|
|
// Hook up the passthrough function.
|
|
engine.pfnFlushToTerminal = std::bind(&StateMachine::FlushToTerminal, &machine);
|
|
|
|
machine.ProcessString(L"\x1b[?999h 12345 Hello World");
|
|
|
|
VERIFY_ARE_EQUAL(String(L"\x1b[?999h"), String(engine.passedThrough.c_str()));
|
|
VERIFY_ARE_EQUAL(String(L" 12345 Hello World"), String(engine.printed.c_str()));
|
|
}
|
|
|
|
void StateMachineTest::RunStorageBeforeEscape()
|
|
{
|
|
auto enginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
// this dance is required because StateMachine presumes to take ownership of its engine.
|
|
auto& engine{ *enginePtr.get() };
|
|
StateMachine machine{ std::move(enginePtr) };
|
|
|
|
// Hook up the passthrough function.
|
|
engine.pfnFlushToTerminal = std::bind(&StateMachine::FlushToTerminal, &machine);
|
|
|
|
// Print a bunch of regular text to build up the run buffer before transitioning state.
|
|
machine.ProcessString(L"12345 Hello World\x1b[?999h");
|
|
|
|
// Then ensure the entire buffered run was printed all at once back to us.
|
|
VERIFY_ARE_EQUAL(String(L"12345 Hello World"), String(engine.printed.c_str()));
|
|
VERIFY_ARE_EQUAL(String(L"\x1b[?999h"), String(engine.passedThrough.c_str()));
|
|
}
|
|
|
|
void StateMachineTest::BulkTextPrint()
|
|
{
|
|
auto enginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
// this dance is required because StateMachine presumes to take ownership of its engine.
|
|
auto& engine{ *enginePtr.get() };
|
|
StateMachine machine{ std::move(enginePtr) };
|
|
|
|
// Print a bunch of regular text to build up the run buffer before transitioning state.
|
|
machine.ProcessString(L"12345 Hello World");
|
|
|
|
// Then ensure the entire buffered run was printed all at once back to us.
|
|
VERIFY_ARE_EQUAL(String(L"12345 Hello World"), String(engine.printed.c_str()));
|
|
}
|
|
|
|
void StateMachineTest::PassThroughUnhandledSplitAcrossWrites()
|
|
{
|
|
auto enginePtr{ std::make_unique<TestStateMachineEngine>() };
|
|
// this dance is required because StateMachine presumes to take ownership of its engine.
|
|
auto& engine{ *enginePtr.get() };
|
|
StateMachine machine{ std::move(enginePtr) };
|
|
|
|
// Hook up the passthrough function.
|
|
engine.pfnFlushToTerminal = std::bind(&StateMachine::FlushToTerminal, &machine);
|
|
|
|
// Broken in two pieces (test case from GH#3081)
|
|
machine.ProcessString(L"\x1b[?12");
|
|
VERIFY_ARE_EQUAL(L"", engine.passedThrough); // nothing out yet
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
machine.ProcessString(L"34h");
|
|
VERIFY_ARE_EQUAL(L"\x1b[?1234h", engine.passedThrough); // whole sequence out, no other output
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
engine.ResetTestState();
|
|
|
|
// Three pieces
|
|
machine.ProcessString(L"\x1b[?2");
|
|
VERIFY_ARE_EQUAL(L"", engine.passedThrough); // nothing out yet
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
machine.ProcessString(L"34");
|
|
VERIFY_ARE_EQUAL(L"", engine.passedThrough); // nothing out yet
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
machine.ProcessString(L"5h");
|
|
VERIFY_ARE_EQUAL(L"\x1b[?2345h", engine.passedThrough); // whole sequence out, no other output
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
engine.ResetTestState();
|
|
|
|
// Split during OSC terminator (test case from GH#3080)
|
|
machine.ProcessString(L"\x1b]99;foo\x1b");
|
|
VERIFY_ARE_EQUAL(L"", engine.passedThrough); // nothing out yet
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
|
|
machine.ProcessString(L"\\");
|
|
VERIFY_ARE_EQUAL(L"\x1b]99;foo\x1b\\", engine.passedThrough);
|
|
VERIFY_ARE_EQUAL(L"", engine.printed);
|
|
}
|