Add support for VT52 emulation (#4789)

## Summary of the Pull Request

This PR adds support for the core VT52 commands, and implements the `DECANM` private mode sequence, which switches the terminal between ANSI mode and VT52-compatible mode.

## References

PR #2017 defined the initial specification for VT52 support.
PR #4044 removed the original VT52 cursor ops that conflicted with VT100 sequences.

## PR Checklist
* [x] Closes #976
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [x] Tests added/passed
* [ ] Requires documentation to be updated
* [x] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #2017

## Detailed Description of the Pull Request / Additional comments

Most of the work involves updates to the parsing state machine, which behaves differently in VT52 mode. `CSI`, `OSC`, and `SS3` sequences are not applicable, and there is one special-case escape sequence (_Direct Cursor Address_), which requires an additional state to handle parameters that come _after_ the final character.

Once the parsing is handled though, it's mostly just a matter of dispatching the commands to existing methods in the `ITermDispatch` interface. Only one new method was required in the interface to handle the _Identify_ command.

The only real new functionality is in the `TerminalInput` class, which needs to generate different escape sequences for certain keys in VT52 mode. This does not yet support _all_ of the VT52 key sequences, because the VT100 support is itself not yet complete. But the basics are in place, and I think the rest is best left for a follow-up issue, and potentially a refactor of the `TerminalInput` class.

I should point out that the original spec called for a new _Graphic Mode_ character set, but I've since discovered that the VT terminals that _emulate_ VT52 just use the existing VT100 _Special Graphics_ set, so that is really what we should be doing too. We can always consider adding the VT52 graphic set as a option later, if there is demand for strict VT52 compatibility. 

## Validation Steps Performed

I've added state machine and adapter tests to confirm that the `DECANM` mode changing sequences are correctly dispatched and forwarded to the `ConGetSet` handler. I've also added state machine tests that confirm the VT52 escape sequences are dispatched correctly when the ANSI mode is reset.

For fuzzing support, I've extended the VT command fuzzer to generate the different kinds of VT52 sequences, as well as mode change sequences to switch between the ANSI and VT52 modes.

In terms of manual testing, I've confirmed that the _Test of VT52 mode_ in Vttest now works as expected.
This commit is contained in:
James Holderness 2020-06-01 22:20:40 +01:00 committed by GitHub
parent 44dcc861ad
commit d92c8293ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 592 additions and 21 deletions

View file

@ -239,6 +239,23 @@ bool ConhostInternalGetSet::PrivateSetKeypadMode(const bool fApplicationMode)
return NT_SUCCESS(DoSrvPrivateSetKeypadMode(fApplicationMode));
}
// Routine Description:
// - Sets the terminal emulation mode to either ANSI-compatible or VT52.
// PrivateSetAnsiMode is an internal-only "API" call that the vt commands can execute,
// but it is not represented as a function call on out public API surface.
// Arguments:
// - ansiMode - set to true to enable the ANSI mode, false for VT52 mode.
// Return Value:
// - true if successful. false otherwise.
bool ConhostInternalGetSet::PrivateSetAnsiMode(const bool ansiMode)
{
auto& stateMachine = _io.GetActiveOutputBuffer().GetStateMachine();
stateMachine.SetAnsiMode(ansiMode);
auto& terminalInput = _io.GetActiveInputBuffer()->GetTerminalInput();
terminalInput.ChangeAnsiMode(ansiMode);
return true;
}
// Routine Description:
// - Connects the PrivateSetScreenMode call directly into our Driver Message servicing call inside Conhost.exe
// PrivateSetScreenMode is an internal-only "API" call that the vt commands can execute,

View file

@ -74,6 +74,7 @@ public:
bool PrivateSetCursorKeysMode(const bool applicationMode) override;
bool PrivateSetKeypadMode(const bool applicationMode) override;
bool PrivateSetAnsiMode(const bool ansiMode) override;
bool PrivateSetScreenMode(const bool reverseMode) override;
bool PrivateSetAutoWrapMode(const bool wrapAtEOL) override;

View file

@ -82,6 +82,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
enum PrivateModeParams : unsigned short
{
DECCKM_CursorKeysMode = 1,
DECANM_AnsiMode = 2,
DECCOLM_SetNumberOfColumns = 3,
DECSCNM_ScreenMode = 5,
DECOM_OriginMode = 6,

View file

@ -54,6 +54,7 @@ public:
virtual bool SetCursorKeysMode(const bool applicationMode) = 0; // DECCKM
virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM
virtual bool EnableCursorBlinking(const bool enable) = 0; // ATT610
virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
virtual bool SetScreenMode(const bool reverseMode) = 0; // DECSCNM
virtual bool SetOriginMode(const bool relativeMode) = 0; // DECOM
virtual bool SetAutoWrapMode(const bool wrapAtEOL) = 0; // DECAWM
@ -92,6 +93,7 @@ public:
virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR
virtual bool DeviceAttributes() = 0; // DA1
virtual bool Vt52DeviceAttributes() = 0; // VT52 Identify
virtual bool DesignateCharset(const wchar_t wchCharset) = 0; // SCS

View file

@ -714,6 +714,19 @@ bool AdaptDispatch::DeviceAttributes()
return _WriteResponse(L"\x1b[?1;0c");
}
// Routine Description:
// - VT52 Identify - Reports the identity of the terminal in VT52 emulation mode.
// An actual VT52 terminal would typically identify itself with ESC / K.
// But for a terminal that is emulating a VT52, the sequence should be ESC / Z.
// Arguments:
// - <none>
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::Vt52DeviceAttributes()
{
return _WriteResponse(L"\x1b/Z");
}
// Routine Description:
// - DSR-OS - Reports the operating status back to the input channel
// Arguments:
@ -956,6 +969,9 @@ bool AdaptDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateModePar
// set - Enable Application Mode, reset - Normal mode
success = SetCursorKeysMode(enable);
break;
case DispatchTypes::PrivateModeParams::DECANM_AnsiMode:
success = SetAnsiMode(enable);
break;
case DispatchTypes::PrivateModeParams::DECCOLM_SetNumberOfColumns:
success = _DoDECCOLMHelper(enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns);
break;
@ -1129,6 +1145,20 @@ bool AdaptDispatch::DeleteLine(const size_t distance)
return _pConApi->DeleteLines(distance);
}
// - DECANM - Sets the terminal emulation mode to either ANSI-compatible or VT52.
// Arguments:
// - ansiMode - set to true to enable the ANSI mode, false for VT52 mode.
// Return Value:
// - True if handled successfully. False otherwise.
bool AdaptDispatch::SetAnsiMode(const bool ansiMode)
{
// When an attempt is made to update the mode, the designated character sets
// need to be reset to defaults, even if the mode doesn't actually change.
_termOutput = {};
return _pConApi->PrivateSetAnsiMode(ansiMode);
}
// Routine Description:
// - DECSCNM - Sets the screen mode to either normal or reverse.
// When in reverse screen mode, the background and foreground colors are switched.

View file

@ -58,6 +58,7 @@ namespace Microsoft::Console::VirtualTerminal
bool SetGraphicsRendition(const std::basic_string_view<DispatchTypes::GraphicsOptions> options) override; // SGR
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR
bool DeviceAttributes() override; // DA1
bool Vt52DeviceAttributes() override; // VT52 Identify
bool ScrollUp(const size_t distance) override; // SU
bool ScrollDown(const size_t distance) override; // SD
bool InsertLine(const size_t distance) override; // IL
@ -68,6 +69,7 @@ namespace Microsoft::Console::VirtualTerminal
bool SetCursorKeysMode(const bool applicationMode) override; // DECCKM
bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM
bool EnableCursorBlinking(const bool enable) override; // ATT610
bool SetAnsiMode(const bool ansiMode) override; // DECANM
bool SetScreenMode(const bool reverseMode) override; // DECSCNM
bool SetOriginMode(const bool relativeMode) noexcept override; // DECOM
bool SetAutoWrapMode(const bool wrapAtEOL) override; // DECAWM

View file

@ -46,6 +46,7 @@ namespace Microsoft::Console::VirtualTerminal
virtual bool PrivateSetCursorKeysMode(const bool applicationMode) = 0;
virtual bool PrivateSetKeypadMode(const bool applicationMode) = 0;
virtual bool PrivateSetAnsiMode(const bool ansiMode) = 0;
virtual bool PrivateSetScreenMode(const bool reverseMode) = 0;
virtual bool PrivateSetAutoWrapMode(const bool wrapAtEOL) = 0;

View file

@ -48,6 +48,7 @@ public:
bool SetCursorKeysMode(const bool /*applicationMode*/) noexcept override { return false; } // DECCKM
bool SetKeypadMode(const bool /*applicationMode*/) noexcept override { return false; } // DECKPAM, DECKPNM
bool EnableCursorBlinking(const bool /*enable*/) noexcept override { return false; } // ATT610
bool SetAnsiMode(const bool /*ansiMode*/) noexcept override { return false; } // DECANM
bool SetScreenMode(const bool /*reverseMode*/) noexcept override { return false; } // DECSCNM
bool SetOriginMode(const bool /*relativeMode*/) noexcept override { return false; }; // DECOM
bool SetAutoWrapMode(const bool /*wrapAtEOL*/) noexcept override { return false; }; // DECAWM
@ -86,6 +87,7 @@ public:
bool DeviceStatusReport(const DispatchTypes::AnsiStatusType /*statusType*/) noexcept override { return false; } // DSR, DSR-OS, DSR-CPR
bool DeviceAttributes() noexcept override { return false; } // DA1
bool Vt52DeviceAttributes() noexcept override { return false; } // VT52 Identify
bool DesignateCharset(const wchar_t /*wchCharset*/) noexcept override { return false; } // SCS

View file

@ -162,6 +162,18 @@ public:
return _privateSetKeypadModeResult;
}
bool PrivateSetAnsiMode(const bool ansiMode) override
{
Log::Comment(L"PrivateSetAnsiMode MOCK called...");
if (_privateSetAnsiModeResult)
{
VERIFY_ARE_EQUAL(_expectedAnsiMode, ansiMode);
}
return _privateSetAnsiModeResult;
}
bool PrivateSetScreenMode(const bool /*reverseMode*/) override
{
Log::Comment(L"PrivateSetScreenMode MOCK called...");
@ -744,6 +756,8 @@ public:
bool _privateSetKeypadModeResult = false;
bool _cursorKeysApplicationMode = false;
bool _keypadApplicationMode = false;
bool _privateSetAnsiModeResult = false;
bool _expectedAnsiMode = false;
bool _privateAllowCursorBlinkingResult = false;
bool _enable = false; // for cursor blinking
bool _privateSetScrollingRegionResult = false;
@ -1691,6 +1705,26 @@ public:
VERIFY_IS_TRUE(_pDispatch.get()->SetKeypadMode(true));
}
TEST_METHOD(AnsiModeTest)
{
Log::Comment(L"Starting test...");
// success cases
// set ansi mode = true
Log::Comment(L"Test 1: ansi mode = true");
_testGetSet->_privateSetAnsiModeResult = true;
_testGetSet->_expectedAnsiMode = true;
VERIFY_IS_TRUE(_pDispatch.get()->SetAnsiMode(true));
// set ansi mode = false
Log::Comment(L"Test 2: ansi mode = false.");
_testGetSet->_privateSetAnsiModeResult = true;
_testGetSet->_expectedAnsiMode = false;
VERIFY_IS_TRUE(_pDispatch.get()->SetAnsiMode(false));
}
TEST_METHOD(AllowBlinkingTest)
{
Log::Comment(L"Starting test...");

View file

@ -68,6 +68,15 @@ static constexpr std::array<TermKeyMap, 6> s_cursorKeysApplicationMapping{
TermKeyMap{ VK_END, L"\x1bOF" },
};
static constexpr std::array<TermKeyMap, 6> s_cursorKeysVt52Mapping{
TermKeyMap{ VK_UP, L"\033A" },
TermKeyMap{ VK_DOWN, L"\033B" },
TermKeyMap{ VK_RIGHT, L"\033C" },
TermKeyMap{ VK_LEFT, L"\033D" },
TermKeyMap{ VK_HOME, L"\033H" },
TermKeyMap{ VK_END, L"\033F" },
};
static constexpr std::array<TermKeyMap, 20> s_keypadNumericMapping{
TermKeyMap{ VK_TAB, L"\x09" },
TermKeyMap{ VK_BACK, L"\x7f" },
@ -150,6 +159,29 @@ static constexpr std::array<TermKeyMap, 20> s_keypadApplicationMapping{
// TermKeyMap{ VK_TAB, L"\x1bOI" }, // So I left them here as a reference just in case.
};
static constexpr std::array<TermKeyMap, 20> s_keypadVt52Mapping{
TermKeyMap{ VK_TAB, L"\x09" },
TermKeyMap{ VK_BACK, L"\x7f" },
TermKeyMap{ VK_PAUSE, L"\x1a" },
TermKeyMap{ VK_ESCAPE, L"\x1b" },
TermKeyMap{ VK_INSERT, L"\x1b[2~" },
TermKeyMap{ VK_DELETE, L"\x1b[3~" },
TermKeyMap{ VK_PRIOR, L"\x1b[5~" },
TermKeyMap{ VK_NEXT, L"\x1b[6~" },
TermKeyMap{ VK_F1, L"\x1bP" },
TermKeyMap{ VK_F2, L"\x1bQ" },
TermKeyMap{ VK_F3, L"\x1bR" },
TermKeyMap{ VK_F4, L"\x1bS" },
TermKeyMap{ VK_F5, L"\x1b[15~" },
TermKeyMap{ VK_F6, L"\x1b[17~" },
TermKeyMap{ VK_F7, L"\x1b[18~" },
TermKeyMap{ VK_F8, L"\x1b[19~" },
TermKeyMap{ VK_F9, L"\x1b[20~" },
TermKeyMap{ VK_F10, L"\x1b[21~" },
TermKeyMap{ VK_F11, L"\x1b[23~" },
TermKeyMap{ VK_F12, L"\x1b[24~" },
};
// Sequences to send when a modifier is pressed with any of these keys
// Basically, the 'm' will be replaced with a character indicating which
// modifier keys are pressed.
@ -220,6 +252,11 @@ const wchar_t* const CTRL_QUESTIONMARK_SEQUENCE = L"\x7F";
const wchar_t* const CTRL_ALT_SLASH_SEQUENCE = L"\x1b\x1f";
const wchar_t* const CTRL_ALT_QUESTIONMARK_SEQUENCE = L"\x1b\x7F";
void TerminalInput::ChangeAnsiMode(const bool ansiMode) noexcept
{
_ansiMode = ansiMode;
}
void TerminalInput::ChangeKeypadMode(const bool applicationMode) noexcept
{
_keypadApplicationMode = applicationMode;
@ -231,29 +268,44 @@ void TerminalInput::ChangeCursorKeysMode(const bool applicationMode) noexcept
}
static const std::basic_string_view<TermKeyMap> _getKeyMapping(const KeyEvent& keyEvent,
const bool ansiMode,
const bool cursorApplicationMode,
const bool keypadApplicationMode) noexcept
{
if (keyEvent.IsCursorKey())
if (ansiMode)
{
if (cursorApplicationMode)
if (keyEvent.IsCursorKey())
{
return { s_cursorKeysApplicationMapping.data(), s_cursorKeysApplicationMapping.size() };
if (cursorApplicationMode)
{
return { s_cursorKeysApplicationMapping.data(), s_cursorKeysApplicationMapping.size() };
}
else
{
return { s_cursorKeysNormalMapping.data(), s_cursorKeysNormalMapping.size() };
}
}
else
{
return { s_cursorKeysNormalMapping.data(), s_cursorKeysNormalMapping.size() };
if (keypadApplicationMode)
{
return { s_keypadApplicationMapping.data(), s_keypadApplicationMapping.size() };
}
else
{
return { s_keypadNumericMapping.data(), s_keypadNumericMapping.size() };
}
}
}
else
{
if (keypadApplicationMode)
if (keyEvent.IsCursorKey())
{
return { s_keypadApplicationMapping.data(), s_keypadApplicationMapping.size() };
return { s_cursorKeysVt52Mapping.data(), s_cursorKeysVt52Mapping.size() };
}
else
{
return { s_keypadNumericMapping.data(), s_keypadNumericMapping.size() };
return { s_keypadVt52Mapping.data(), s_keypadVt52Mapping.size() };
}
}
}
@ -560,7 +612,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
}
// Check any other key mappings (like those for the F1-F12 keys).
const auto mapping = _getKeyMapping(keyEvent, _cursorApplicationMode, _keypadApplicationMode);
const auto mapping = _getKeyMapping(keyEvent, _ansiMode, _cursorApplicationMode, _keypadApplicationMode);
if (_translateDefaultMapping(keyEvent, mapping, senderFunc))
{
return true;

View file

@ -34,6 +34,7 @@ namespace Microsoft::Console::VirtualTerminal
~TerminalInput() = default;
bool HandleKey(const IInputEvent* const pInEvent);
void ChangeAnsiMode(const bool ansiMode) noexcept;
void ChangeKeypadMode(const bool applicationMode) noexcept;
void ChangeCursorKeysMode(const bool applicationMode) noexcept;
@ -67,6 +68,7 @@ namespace Microsoft::Console::VirtualTerminal
// storage location for the leading surrogate of a utf-16 surrogate pair
std::optional<wchar_t> _leadingSurrogate;
bool _ansiMode = true;
bool _keypadApplicationMode = false;
bool _cursorApplicationMode = false;

View file

@ -32,6 +32,9 @@ namespace Microsoft::Console::VirtualTerminal
virtual bool ActionEscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates) = 0;
virtual bool ActionVt52EscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) = 0;
virtual bool ActionCsiDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) = 0;

View file

@ -333,6 +333,24 @@ bool InputStateMachineEngine::ActionEscDispatch(const wchar_t wch,
return success;
}
// Method Description:
// - Triggers the Vt52EscDispatch action to indicate that the listener should handle
// a VT52 escape sequence. These sequences start with ESC and a single letter,
// sometimes followed by parameters.
// Arguments:
// - wch - Character to dispatch.
// - intermediates - Intermediate characters in the sequence.
// - parameters - Set of parameters collected while parsing the sequence.
// Return Value:
// - true iff we successfully dispatched the sequence.
bool InputStateMachineEngine::ActionVt52EscDispatch(const wchar_t /*wch*/,
const std::basic_string_view<wchar_t> /*intermediates*/,
const std::basic_string_view<size_t> /*parameters*/) noexcept
{
// VT52 escape sequences are not used in the input state machine.
return false;
}
// Method Description:
// - Triggers the CsiDispatch action to indicate that the listener should handle
// a control sequence. These sequences perform various API-type commands

View file

@ -147,6 +147,10 @@ namespace Microsoft::Console::VirtualTerminal
bool ActionEscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates) override;
bool ActionVt52EscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) noexcept override;
bool ActionCsiDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) override;

View file

@ -286,6 +286,89 @@ bool OutputStateMachineEngine::ActionEscDispatch(const wchar_t wch,
return success;
}
// Method Description:
// - Triggers the Vt52EscDispatch action to indicate that the listener should handle
// a VT52 escape sequence. These sequences start with ESC and a single letter,
// sometimes followed by parameters.
// Arguments:
// - wch - Character to dispatch.
// - intermediates - Intermediate characters in the sequence.
// - parameters - Set of parameters collected while parsing the sequence.
// Return Value:
// - true iff we successfully dispatched the sequence.
bool OutputStateMachineEngine::ActionVt52EscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters)
{
bool success = false;
// no intermediates.
if (intermediates.empty())
{
switch (wch)
{
case Vt52ActionCodes::CursorUp:
success = _dispatch->CursorUp(1);
break;
case Vt52ActionCodes::CursorDown:
success = _dispatch->CursorDown(1);
break;
case Vt52ActionCodes::CursorRight:
success = _dispatch->CursorForward(1);
break;
case Vt52ActionCodes::CursorLeft:
success = _dispatch->CursorBackward(1);
break;
case Vt52ActionCodes::EnterGraphicsMode:
success = _dispatch->DesignateCharset(DispatchTypes::VTCharacterSets::DEC_LineDrawing);
break;
case Vt52ActionCodes::ExitGraphicsMode:
success = _dispatch->DesignateCharset(DispatchTypes::VTCharacterSets::USASCII);
break;
case Vt52ActionCodes::CursorToHome:
success = _dispatch->CursorPosition(1, 1);
break;
case Vt52ActionCodes::ReverseLineFeed:
success = _dispatch->ReverseLineFeed();
break;
case Vt52ActionCodes::EraseToEndOfScreen:
success = _dispatch->EraseInDisplay(DispatchTypes::EraseType::ToEnd);
break;
case Vt52ActionCodes::EraseToEndOfLine:
success = _dispatch->EraseInLine(DispatchTypes::EraseType::ToEnd);
break;
case Vt52ActionCodes::DirectCursorAddress:
// VT52 cursor addresses are provided as ASCII characters, with
// the lowest value being a space, representing an address of 1.
success = _dispatch->CursorPosition(parameters.at(0) - ' ' + 1, parameters.at(1) - ' ' + 1);
break;
case Vt52ActionCodes::Identify:
success = _dispatch->Vt52DeviceAttributes();
break;
case Vt52ActionCodes::EnterAlternateKeypadMode:
success = _dispatch->SetKeypadMode(true);
break;
case Vt52ActionCodes::ExitAlternateKeypadMode:
success = _dispatch->SetKeypadMode(false);
break;
case Vt52ActionCodes::ExitVt52Mode:
{
const DispatchTypes::PrivateModeParams mode[] = { DispatchTypes::PrivateModeParams::DECANM_AnsiMode };
success = _dispatch->SetPrivateModes(mode);
break;
}
default:
// If no functions to call, overall dispatch was a failure.
success = false;
break;
}
}
_ClearLastChar();
return success;
}
// Routine Description:
// - Triggers the CsiDispatch action to indicate that the listener should handle
// a control sequence. These sequences perform various API-type commands

View file

@ -36,6 +36,10 @@ namespace Microsoft::Console::VirtualTerminal
bool ActionEscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates) override;
bool ActionVt52EscDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) override;
bool ActionCsiDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters) override;
@ -126,6 +130,25 @@ namespace Microsoft::Console::VirtualTerminal
DECALN_ScreenAlignmentPattern = L'8'
};
enum Vt52ActionCodes : wchar_t
{
CursorUp = L'A',
CursorDown = L'B',
CursorRight = L'C',
CursorLeft = L'D',
EnterGraphicsMode = L'F',
ExitGraphicsMode = L'G',
CursorToHome = L'H',
ReverseLineFeed = L'I',
EraseToEndOfScreen = L'J',
EraseToEndOfLine = L'K',
DirectCursorAddress = L'Y',
Identify = L'Z',
EnterAlternateKeypadMode = L'=',
ExitAlternateKeypadMode = L'>',
ExitVt52Mode = L'<'
};
enum OscActionCodes : unsigned int
{
SetIconAndWindowTitle = 0,

View file

@ -38,6 +38,8 @@ static std::string GenerateOscTitleToken();
static std::string GenerateHardResetToken();
static std::string GenerateSoftResetToken();
static std::string GenerateOscColorTableToken();
static std::string GenerateVt52Token();
static std::string GenerateVt52CursorAddressToken();
const fuzz::_fuzz_type_entry<BYTE> g_repeatMap[] = {
{ 4, [](BYTE) { return CFuzzChance::GetRandom<BYTE>(2, 0xF); } },
@ -58,7 +60,9 @@ const std::function<std::string()> g_tokenGenerators[] = {
GenerateOscTitleToken,
GenerateHardResetToken,
GenerateSoftResetToken,
GenerateOscColorTableToken
GenerateOscColorTableToken,
GenerateVt52Token,
GenerateVt52CursorAddressToken
};
std::string GenerateTokenLowProbability()
@ -302,6 +306,7 @@ std::string GeneratePrivateModeParamToken()
const _fuzz_type_entry<std::string> map[] = {
{ 12, [](std::string) { std::string s; AppendFormat(s, "?%02d", CFuzzChance::GetRandom<BYTE>()); return s; } },
{ 8, [](std::string) { return std::string("?1"); } },
{ 8, [](std::string) { return std::string("?2"); } },
{ 8, [](std::string) { return std::string("?3"); } },
{ 8, [](std::string) { return std::string("?12"); } },
{ 8, [](std::string) { return std::string("?25"); } },
@ -505,6 +510,30 @@ std::string GenerateOscColorTableToken()
return GenerateFuzzedOscToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens));
}
// VT52 sequences without parameters.
std::string GenerateVt52Token()
{
const LPSTR tokens[] = { "A", "B", "C", "D", "F", "G", "H", "I", "J", "K", "Z", "<" };
std::string cux(ESC);
cux += GenerateTokenLowProbability();
cux += CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens));
return cux;
}
// VT52 direct cursor address sequence with parameters.
std::string GenerateVt52CursorAddressToken()
{
const LPSTR tokens[] = { "Y" };
std::string cux(ESC);
cux += GenerateTokenLowProbability();
cux += CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens));
cux += GenerateTokenLowProbability();
AppendFormat(cux, "%c", CFuzzChance::GetRandom<BYTE>(32, 255));
cux += GenerateTokenLowProbability();
AppendFormat(cux, "%c", CFuzzChance::GetRandom<BYTE>(32, 255));
return cux;
}
int __cdecl wmain(int argc, WCHAR* argv[])
{
if (argc != 3)

View file

@ -14,6 +14,7 @@ StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine) :
_engine(std::move(engine)),
_state(VTStates::Ground),
_trace(Microsoft::Console::VirtualTerminal::ParserTracing()),
_isInAnsiMode(true),
_intermediates{},
_parameters{},
_oscString{},
@ -23,6 +24,11 @@ StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine) :
_ActionClear();
}
void StateMachine::SetAnsiMode(bool ansiMode) noexcept
{
_isInAnsiMode = ansiMode;
}
const IStateMachineEngine& StateMachine::Engine() const noexcept
{
return *_engine;
@ -197,6 +203,19 @@ static constexpr bool _isSs3Indicator(const wchar_t wch) noexcept
return wch == L'O'; // 0x4F
}
// Routine Description:
// - Determines if a character is the VT52 "Direct Cursor Address" command.
// This immediately follows an escape and signifies the start of a multiple
// character command sequence.
// Arguments:
// - wch - Character to check.
// Return Value:
// - True if it is. False if it isn't.
static constexpr bool _isVt52CursorAddress(const wchar_t wch) noexcept
{
return wch == L'Y'; // 0x59
}
// Routine Description:
// - Determines if a character is a "Single Shift Select" indicator.
// This immediately follows an escape and signifies a varying length control string.
@ -346,6 +365,31 @@ void StateMachine::_ActionEscDispatch(const wchar_t wch)
}
}
// Routine Description:
// - Triggers the Vt52EscDispatch action to indicate that the listener should handle a VT52 escape sequence.
// These sequences start with ESC and a single letter, sometimes followed by parameters.
// Arguments:
// - wch - Character to dispatch.
// Return Value:
// - <none>
void StateMachine::_ActionVt52EscDispatch(const wchar_t wch)
{
_trace.TraceOnAction(L"Vt52EscDispatch");
const bool success = _engine->ActionVt52EscDispatch(wch,
{ _intermediates.data(), _intermediates.size() },
{ _parameters.data(), _parameters.size() });
// Trace the result.
_trace.DispatchSequenceTrace(success);
if (!success)
{
// Suppress it and log telemetry on failed cases
TermTelemetry::Instance().LogFailed(wch);
}
}
// Routine Description:
// - Triggers the CsiDispatch action to indicate that the listener should handle a control sequence.
// These sequences perform various API-type commands that can include many parameters.
@ -699,6 +743,20 @@ void StateMachine::_EnterSs3Param() noexcept
_trace.TraceStateChange(L"Ss3Param");
}
// Routine Description:
// - Moves the state machine into the VT52Param state.
// This state is entered:
// 1. When a VT52 Cursor Address escape is detected, so parameters are expected to follow.
// Arguments:
// - <none>
// Return Value:
// - <none>
void StateMachine::_EnterVt52Param() noexcept
{
_state = VTStates::Vt52Param;
_trace.TraceStateChange(L"Vt52Param");
}
// Routine Description:
// - Processes a character event into an Action that occurs while in the Ground state.
// Events in this state will:
@ -716,7 +774,7 @@ void StateMachine::_EventGround(const wchar_t wch)
{
_ActionExecute(wch);
}
else if (_isC1Csi(wch))
else if (_isC1Csi(wch) && _isInAnsiMode)
{
_EnterCsiEntry();
}
@ -770,21 +828,33 @@ void StateMachine::_EventEscape(const wchar_t wch)
_EnterEscapeIntermediate();
}
}
else if (_isCsiIndicator(wch))
else if (_isInAnsiMode)
{
_EnterCsiEntry();
if (_isCsiIndicator(wch))
{
_EnterCsiEntry();
}
else if (_isOscIndicator(wch))
{
_EnterOscParam();
}
else if (_isSs3Indicator(wch))
{
_EnterSs3Entry();
}
else
{
_ActionEscDispatch(wch);
_EnterGround();
}
}
else if (_isOscIndicator(wch))
else if (_isVt52CursorAddress(wch))
{
_EnterOscParam();
}
else if (_isSs3Indicator(wch))
{
_EnterSs3Entry();
_EnterVt52Param();
}
else
{
_ActionEscDispatch(wch);
_ActionVt52EscDispatch(wch);
_EnterGround();
}
}
@ -815,11 +885,20 @@ void StateMachine::_EventEscapeIntermediate(const wchar_t wch)
{
_ActionIgnore();
}
else
else if (_isInAnsiMode)
{
_ActionEscDispatch(wch);
_EnterGround();
}
else if (_isVt52CursorAddress(wch))
{
_EnterVt52Param();
}
else
{
_ActionVt52EscDispatch(wch);
_EnterGround();
}
}
// Routine Description:
@ -1156,6 +1235,41 @@ void StateMachine::_EventSs3Param(const wchar_t wch)
}
}
// Routine Description:
// - Processes a character event into an Action that occurs while in the Vt52Param state.
// Events in this state will:
// 1. Execute C0 control characters
// 2. Ignore Delete characters
// 3. Store exactly two parameter characters
// 4. Dispatch a control sequence with parameters for action (always Direct Cursor Address)
// Arguments:
// - wch - Character that triggered the event
// Return Value:
// - <none>
void StateMachine::_EventVt52Param(const wchar_t wch)
{
_trace.TraceOnEvent(L"Vt52Param");
if (_isC0Code(wch))
{
_ActionExecute(wch);
}
else if (_isDelete(wch))
{
_ActionIgnore();
}
else
{
_parameters.push_back(wch);
if (_parameters.size() == 2)
{
// The command character is processed before the parameter values,
// but it will always be 'Y', the Direct Cursor Address command.
_ActionVt52EscDispatch(L'Y');
_EnterGround();
}
}
}
// Routine Description:
// - Entry to the state machine. Takes characters one by one and processes them according to the state machine rules.
// Arguments:
@ -1213,6 +1327,8 @@ void StateMachine::ProcessCharacter(const wchar_t wch)
return _EventSs3Entry(wch);
case VTStates::Ss3Param:
return _EventSs3Param(wch);
case VTStates::Vt52Param:
return _EventVt52Param(wch);
default:
return;
}

View file

@ -37,6 +37,8 @@ namespace Microsoft::Console::VirtualTerminal
public:
StateMachine(std::unique_ptr<IStateMachineEngine> engine);
void SetAnsiMode(bool ansiMode) noexcept;
void ProcessCharacter(const wchar_t wch);
void ProcessString(const std::wstring_view string);
@ -52,6 +54,7 @@ namespace Microsoft::Console::VirtualTerminal
void _ActionExecuteFromEscape(const wchar_t wch);
void _ActionPrint(const wchar_t wch);
void _ActionEscDispatch(const wchar_t wch);
void _ActionVt52EscDispatch(const wchar_t wch);
void _ActionCollect(const wchar_t wch);
void _ActionParam(const wchar_t wch);
void _ActionCsiDispatch(const wchar_t wch);
@ -75,6 +78,7 @@ namespace Microsoft::Console::VirtualTerminal
void _EnterOscTermination() noexcept;
void _EnterSs3Entry();
void _EnterSs3Param() noexcept;
void _EnterVt52Param() noexcept;
void _EventGround(const wchar_t wch);
void _EventEscape(const wchar_t wch);
@ -88,6 +92,7 @@ namespace Microsoft::Console::VirtualTerminal
void _EventOscTermination(const wchar_t wch);
void _EventSs3Entry(const wchar_t wch);
void _EventSs3Param(const wchar_t wch);
void _EventVt52Param(const wchar_t wch);
void _AccumulateTo(const wchar_t wch, size_t& value) noexcept;
@ -104,7 +109,8 @@ namespace Microsoft::Console::VirtualTerminal
OscString,
OscTermination,
Ss3Entry,
Ss3Param
Ss3Param,
Vt52Param
};
Microsoft::Console::VirtualTerminal::ParserTracing _trace;
@ -113,6 +119,8 @@ namespace Microsoft::Console::VirtualTerminal
VTStates _state;
bool _isInAnsiMode;
std::wstring_view _run;
std::vector<wchar_t> _intermediates;

View file

@ -675,9 +675,11 @@ public:
_statusReportType{ (DispatchTypes::AnsiStatusType)-1 },
_deviceStatusReport{ false },
_deviceAttributes{ false },
_vt52DeviceAttributes{ false },
_isAltBuffer{ false },
_cursorKeysMode{ false },
_cursorBlinking{ true },
_isInAnsiMode{ true },
_isScreenModeReversed{ false },
_isOriginModeRelative{ false },
_isAutoWrapEnabled{ true },
@ -685,6 +687,7 @@ public:
_carriageReturn{ false },
_lineFeed{ false },
_lineFeedType{ (DispatchTypes::LineFeedType)-1 },
_reverseLineFeed{ false },
_forwardTab{ false },
_numTabs{ 0 },
_isDECCOLMAllowed{ false },
@ -847,6 +850,13 @@ public:
return true;
}
bool Vt52DeviceAttributes() noexcept override
{
_vt52DeviceAttributes = true;
return true;
}
bool _PrivateModeParamsHelper(_In_ DispatchTypes::PrivateModeParams const param, const bool fEnable)
{
bool fSuccess = false;
@ -856,6 +866,9 @@ public:
// set - Enable Application Mode, reset - Numeric/normal mode
fSuccess = SetVirtualTerminalInputMode(fEnable);
break;
case DispatchTypes::PrivateModeParams::DECANM_AnsiMode:
fSuccess = SetAnsiMode(fEnable);
break;
case DispatchTypes::PrivateModeParams::DECCOLM_SetNumberOfColumns:
fSuccess = SetColumns(static_cast<size_t>(fEnable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns));
break;
@ -928,6 +941,12 @@ public:
return true;
}
bool SetAnsiMode(const bool ansiMode) noexcept override
{
_isInAnsiMode = ansiMode;
return true;
}
bool SetScreenMode(const bool reverseMode) noexcept override
{
_isScreenModeReversed = reverseMode;
@ -965,6 +984,12 @@ public:
return true;
}
bool ReverseLineFeed() noexcept override
{
_reverseLineFeed = true;
return true;
}
bool ForwardTab(const size_t numTabs) noexcept override
{
_forwardTab = true;
@ -1016,9 +1041,11 @@ public:
DispatchTypes::AnsiStatusType _statusReportType;
bool _deviceStatusReport;
bool _deviceAttributes;
bool _vt52DeviceAttributes;
bool _isAltBuffer;
bool _cursorKeysMode;
bool _cursorBlinking;
bool _isInAnsiMode;
bool _isScreenModeReversed;
bool _isOriginModeRelative;
bool _isAutoWrapEnabled;
@ -1026,6 +1053,7 @@ public:
bool _carriageReturn;
bool _lineFeed;
DispatchTypes::LineFeedType _lineFeedType;
bool _reverseLineFeed;
bool _forwardTab;
size_t _numTabs;
bool _isDECCOLMAllowed;
@ -1299,6 +1327,26 @@ class StateMachineExternalTest final
pDispatch->ClearState();
}
TEST_METHOD(TestAnsiMode)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));
mach.ProcessString(L"\x1b[?2l");
VERIFY_IS_FALSE(pDispatch->_isInAnsiMode);
pDispatch->ClearState();
pDispatch->_isInAnsiMode = false;
mach.SetAnsiMode(false);
mach.ProcessString(L"\x1b<");
VERIFY_IS_TRUE(pDispatch->_isInAnsiMode);
pDispatch->ClearState();
}
TEST_METHOD(TestSetNumberOfColumns)
{
auto dispatch = std::make_unique<StatefulDispatch>();
@ -2011,4 +2059,95 @@ class StateMachineExternalTest final
pDispatch->ClearState();
}
TEST_METHOD(TestVt52Sequences)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));
// ANSI mode must be reset for VT52 sequences to be recognized.
mach.SetAnsiMode(false);
Log::Comment(L"Cursor Up");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'A');
VERIFY_IS_TRUE(pDispatch->_cursorUp);
VERIFY_ARE_EQUAL(1u, pDispatch->_cursorDistance);
pDispatch->ClearState();
Log::Comment(L"Cursor Down");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'B');
VERIFY_IS_TRUE(pDispatch->_cursorDown);
VERIFY_ARE_EQUAL(1u, pDispatch->_cursorDistance);
pDispatch->ClearState();
Log::Comment(L"Cursor Right");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'C');
VERIFY_IS_TRUE(pDispatch->_cursorForward);
VERIFY_ARE_EQUAL(1u, pDispatch->_cursorDistance);
pDispatch->ClearState();
Log::Comment(L"Cursor Left");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'D');
VERIFY_IS_TRUE(pDispatch->_cursorBackward);
VERIFY_ARE_EQUAL(1u, pDispatch->_cursorDistance);
pDispatch->ClearState();
Log::Comment(L"Cursor to Home");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'H');
VERIFY_IS_TRUE(pDispatch->_cursorPosition);
VERIFY_ARE_EQUAL(1u, pDispatch->_line);
VERIFY_ARE_EQUAL(1u, pDispatch->_column);
pDispatch->ClearState();
Log::Comment(L"Reverse Line Feed");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'I');
VERIFY_IS_TRUE(pDispatch->_reverseLineFeed);
pDispatch->ClearState();
Log::Comment(L"Erase to End of Screen");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'J');
VERIFY_IS_TRUE(pDispatch->_eraseDisplay);
VERIFY_ARE_EQUAL(DispatchTypes::EraseType::ToEnd, pDispatch->_eraseType);
pDispatch->ClearState();
Log::Comment(L"Erase to End of Line");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'K');
VERIFY_IS_TRUE(pDispatch->_eraseLine);
VERIFY_ARE_EQUAL(DispatchTypes::EraseType::ToEnd, pDispatch->_eraseType);
pDispatch->ClearState();
Log::Comment(L"Direct Cursor Address");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'Y');
mach.ProcessCharacter(L' ' + 3); // Coordinates must be printable ASCII values,
mach.ProcessCharacter(L' ' + 5); // so are relative to 0x20 (the space character).
VERIFY_IS_TRUE(pDispatch->_cursorPosition);
VERIFY_ARE_EQUAL(3u, pDispatch->_line - 1); // CursorPosition coordinates are 1-based,
VERIFY_ARE_EQUAL(5u, pDispatch->_column - 1); // so are 1 more than the expected values.
pDispatch->ClearState();
Log::Comment(L"Identify Device");
mach.ProcessCharacter(AsciiChars::ESC);
mach.ProcessCharacter(L'Z');
VERIFY_IS_TRUE(pDispatch->_vt52DeviceAttributes);
}
};

View file

@ -53,6 +53,10 @@ public:
bool ActionEscDispatch(const wchar_t /* wch */,
const std::basic_string_view<wchar_t> /* intermediates */) override { return true; };
bool ActionVt52EscDispatch(const wchar_t /*wch*/,
const std::basic_string_view<wchar_t> /*intermediates*/,
const std::basic_string_view<size_t> /*parameters*/) override { return true; };
bool ActionClear() override { return true; };
bool ActionIgnore() override { return true; };