Files
terminal-microsoft/src/terminal/parser/ut_parser/StateMachineTest.cpp
James Holderness 55151a4a04 Refactor VT parameter handling (#7799)
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
2020-10-15 16:12:52 +00:00

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);
}