diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index c18d880e4..bcb639072 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -508,6 +508,7 @@ dealloc Debian debolden debugtype +DECAC DECALN DECANM DECAUPSS diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index c12a02f6d..6f8d06453 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -61,6 +61,14 @@ Terminal::Terminal() : _stateMachine = std::make_unique(std::move(engine)); + // Until we have a true pass-through mode (GH#1173), the decision as to + // whether C1 controls are interpreted or not is made at the conhost level. + // If they are being filtered out, then we will simply never receive them. + // But if they are being accepted by conhost, there's a chance they may get + // passed through in some situations, so it's important that our state + // machine is always prepared to accept them. + _stateMachine->SetParserMode(StateMachine::Mode::AcceptC1, true); + auto passAlongInput = [&](std::deque>& inEventsToWrite) { if (!_pfnWriteInput) { diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index a67aec90d..acb85c492 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -271,7 +271,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlink() auto& stateMachine = *(term._stateMachine); // Process the opening osc 8 sequence - stateMachine.ProcessString(L"\x1b]8;;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); @@ -281,7 +281,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlink() VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); // Process the closing osc 8 sequences - stateMachine.ProcessString(L"\x1b]8;;\x9c"); + stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } @@ -297,7 +297,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomId() auto& stateMachine = *(term._stateMachine); // Process the opening osc 8 sequence - stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); @@ -309,7 +309,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomId() VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); // Process the closing osc 8 sequences - stateMachine.ProcessString(L"\x1b]8;;\x9c"); + stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } @@ -325,7 +325,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomIdDifferentUri() auto& stateMachine = *(term._stateMachine); // Process the opening osc 8 sequence - stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); @@ -333,7 +333,7 @@ void TerminalCoreUnitTests::TerminalApiTest::AddHyperlinkCustomIdDifferentUri() const auto oldAttributes{ tbi.GetCurrentAttributes() }; // Send any other text - stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"other.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"other.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); @@ -356,58 +356,58 @@ void TerminalCoreUnitTests::TerminalApiTest::SetTaskbarProgress() VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(0)); // Set some values for taskbar state and progress through state machine - stateMachine.ProcessString(L"\x1b]9;4;1;50\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;1;50\x1b\\"); VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(1)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(50)); // Reset to 0 - stateMachine.ProcessString(L"\x1b]9;4;0;0\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;0;0\x1b\\"); VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(0)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(0)); // Set an out of bounds value for state - stateMachine.ProcessString(L"\x1b]9;4;5;50\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;5;50\x1b\\"); // Nothing should have changed (dispatch should have returned false) VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(0)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(0)); // Set an out of bounds value for progress - stateMachine.ProcessString(L"\x1b]9;4;1;999\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;1;999\x1b\\"); // Progress should have been clamped to 100 VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(1)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(100)); // Don't specify any params - stateMachine.ProcessString(L"\x1b]9;4\x9c"); + stateMachine.ProcessString(L"\x1b]9;4\x1b\\"); // State and progress should both be reset to 0 VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(0)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(0)); // Specify additional params - stateMachine.ProcessString(L"\x1b]9;4;1;80;123\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;1;80;123\x1b\\"); // Additional params should be ignored, state and progress still set normally VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(1)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(80)); // Edge cases + trailing semicolon testing - stateMachine.ProcessString(L"\x1b]9;4;2;\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;2;\x1b\\"); // String should be processed correctly despite the trailing semicolon, // taskbar progress should remain unchanged from previous value VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(2)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(80)); - stateMachine.ProcessString(L"\x1b]9;4;3;75\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;3;75\x1b\\"); // Given progress value should be ignored because this is the indeterminate state, // so the progress value should remain unchanged VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(3)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(80)); - stateMachine.ProcessString(L"\x1b]9;4;0;50\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;0;50\x1b\\"); // Taskbar progress should be 0 (the given value should be ignored) VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(0)); VERIFY_ARE_EQUAL(term.GetTaskbarProgress(), gsl::narrow(0)); - stateMachine.ProcessString(L"\x1b]9;4;2;\x9c"); + stateMachine.ProcessString(L"\x1b]9;4;2;\x1b\\"); // String should be processed correctly despite the trailing semicolon, // taskbar progress should be set to a 'minimum', non-zero value VERIFY_ARE_EQUAL(term.GetTaskbarState(), gsl::narrow(2)); @@ -427,45 +427,45 @@ void TerminalCoreUnitTests::TerminalApiTest::SetWorkingDirectory() VERIFY_IS_TRUE(term.GetWorkingDirectory().empty()); // Invalid sequences should not change CWD - stateMachine.ProcessString(L"\x1b]9;9\x9c"); + stateMachine.ProcessString(L"\x1b]9;9\x1b\\"); VERIFY_IS_TRUE(term.GetWorkingDirectory().empty()); - stateMachine.ProcessString(L"\x1b]9;9\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9\"\x1b\\"); VERIFY_IS_TRUE(term.GetWorkingDirectory().empty()); - stateMachine.ProcessString(L"\x1b]9;9\"C:\\\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9\"C:\\\"\x1b\\"); VERIFY_IS_TRUE(term.GetWorkingDirectory().empty()); // Valid sequences should change CWD - stateMachine.ProcessString(L"\x1b]9;9;\"C:\\\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"C:\\\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"C:\\"); - stateMachine.ProcessString(L"\x1b]9;9;\"C:\\Program Files\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"C:\\Program Files\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"C:\\Program Files"); - stateMachine.ProcessString(L"\x1b]9;9;\"D:\\中文\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"D:\\中文\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"D:\\中文"); // Test OSC 9;9 sequences without quotation marks - stateMachine.ProcessString(L"\x1b]9;9;C:\\\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;C:\\\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"C:\\"); - stateMachine.ProcessString(L"\x1b]9;9;C:\\Program Files\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;C:\\Program Files\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"C:\\Program Files"); - stateMachine.ProcessString(L"\x1b]9;9;D:\\中文\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;D:\\中文\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"D:\\中文"); // These OSC 9;9 sequences will result in invalid CWD. We shouldn't crash on these. - stateMachine.ProcessString(L"\x1b]9;9;\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"\""); - stateMachine.ProcessString(L"\x1b]9;9;\"\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"\"\""); - stateMachine.ProcessString(L"\x1b]9;9;\"\"\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"\"\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"\""); - stateMachine.ProcessString(L"\x1b]9;9;\"\"\"\"\x9c"); + stateMachine.ProcessString(L"\x1b]9;9;\"\"\"\"\x1b\\"); VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"\"\""); } diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index c6c0f9868..8ef3f44b5 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -15,6 +15,7 @@ using namespace Microsoft::Console; using Microsoft::Console::Interactivity::ServiceLocator; +using Microsoft::Console::VirtualTerminal::StateMachine; using Microsoft::Console::VirtualTerminal::TerminalInput; WriteBuffer::WriteBuffer(_In_ Microsoft::Console::IIoProvider& io) : @@ -263,22 +264,35 @@ bool ConhostInternalGetSet::SetInputMode(const TerminalInput::Mode mode, const b } // 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, +// - Sets the various StateMachine parser modes. +// SetParserMode 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. +// - mode - the parser mode to change. +// - enabled - set to true to enable the mode, false to disable it. // Return Value: // - true if successful. false otherwise. -bool ConhostInternalGetSet::PrivateSetAnsiMode(const bool ansiMode) +bool ConhostInternalGetSet::SetParserMode(const StateMachine::Mode mode, const bool enabled) { auto& stateMachine = _io.GetActiveOutputBuffer().GetStateMachine(); - stateMachine.SetAnsiMode(ansiMode); - auto& terminalInput = _io.GetActiveInputBuffer()->GetTerminalInput(); - terminalInput.SetInputMode(TerminalInput::Mode::Ansi, ansiMode); + stateMachine.SetParserMode(mode, enabled); return true; } +// Routine Description: +// - Retrieves the various StateMachine parser modes. +// GetParserMode 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: +// - mode - the parser mode to query. +// Return Value: +// - true if the mode is enabled. false if disabled. +bool ConhostInternalGetSet::GetParserMode(const Microsoft::Console::VirtualTerminal::StateMachine::Mode mode) const +{ + auto& stateMachine = _io.GetActiveOutputBuffer().GetStateMachine(); + return stateMachine.GetParserMode(mode); +} + // 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, diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 51300f3a1..d019e3efa 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -73,8 +73,9 @@ public: const SMALL_RECT& window) override; bool SetInputMode(const Microsoft::Console::VirtualTerminal::TerminalInput::Mode mode, const bool enabled) override; + bool SetParserMode(const Microsoft::Console::VirtualTerminal::StateMachine::Mode mode, const bool enabled) override; + bool GetParserMode(const Microsoft::Console::VirtualTerminal::StateMachine::Mode mode) const override; - bool PrivateSetAnsiMode(const bool ansiMode) override; bool PrivateSetScreenMode(const bool reverseMode) override; bool PrivateSetAutoWrapMode(const bool wrapAtEOL) override; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 83ba0c502..1b65bfc2a 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -5978,7 +5978,7 @@ void ScreenBufferTests::TestAddHyperlink() auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with no custom id - stateMachine.ProcessString(L"\x1b]8;;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); @@ -5988,7 +5988,7 @@ void ScreenBufferTests::TestAddHyperlink() VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); // Process the closing osc 8 sequences - stateMachine.ProcessString(L"\x1b]8;;\x9c"); + stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } @@ -6001,7 +6001,7 @@ void ScreenBufferTests::TestAddHyperlinkCustomId() auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with a custom id - stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); @@ -6013,7 +6013,7 @@ void ScreenBufferTests::TestAddHyperlinkCustomId() VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); // Process the closing osc 8 sequences - stateMachine.ProcessString(L"\x1b]8;;\x9c"); + stateMachine.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(tbi.GetCurrentAttributes().IsHyperlink()); } @@ -6026,7 +6026,7 @@ void ScreenBufferTests::TestAddHyperlinkCustomIdDifferentUri() auto& stateMachine = si.GetStateMachine(); // Process the opening osc 8 sequence with a custom id - stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;test.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"test.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"test.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); @@ -6034,7 +6034,7 @@ void ScreenBufferTests::TestAddHyperlinkCustomIdDifferentUri() const auto oldAttributes{ tbi.GetCurrentAttributes() }; // Send any other text - stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x9c"); + stateMachine.ProcessString(L"\x1b]8;id=myId;other.url\x1b\\"); VERIFY_IS_TRUE(tbi.GetCurrentAttributes().IsHyperlink()); VERIFY_ARE_EQUAL(tbi.GetHyperlinkUriFromId(tbi.GetCurrentAttributes().GetHyperlinkId()), L"other.url"); VERIFY_ARE_EQUAL(tbi.GetHyperlinkId(L"other.url", L"myId"), tbi.GetCurrentAttributes().GetHyperlinkId()); diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index c95a18d70..e1c7bfdeb 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -113,6 +113,7 @@ public: virtual bool LockingShift(const size_t gsetNumber) = 0; // LS0, LS1, LS2, LS3 virtual bool LockingShiftRight(const size_t gsetNumber) = 0; // LS1R, LS2R, LS3R virtual bool SingleShift(const size_t gsetNumber) = 0; // SS2, SS3 + virtual bool AcceptC1Controls(const bool enabled) = 0; // DECAC1 virtual bool SoftReset() = 0; // DECSTR virtual bool HardReset() = 0; // RIS diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 0553cc538..eef56cb83 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -344,6 +344,7 @@ bool AdaptDispatch::CursorSaveState() savedCursorState.IsOriginModeRelative = _isOriginModeRelative; savedCursorState.Attributes = attributes; savedCursorState.TermOutput = _termOutput; + savedCursorState.C1ControlsAccepted = _pConApi->GetParserMode(StateMachine::Mode::AcceptC1); _pConApi->GetConsoleOutputCP(savedCursorState.CodePage); } @@ -386,6 +387,9 @@ bool AdaptDispatch::CursorRestoreState() // Restore designated character set. _termOutput = savedCursorState.TermOutput; + // Restore the parsing state of C1 control codes. + AcceptC1Controls(savedCursorState.C1ControlsAccepted); + // Restore the code page if it was previously saved. if (savedCursorState.CodePage != 0) { @@ -1243,7 +1247,12 @@ bool AdaptDispatch::SetAnsiMode(const bool ansiMode) // need to be reset to defaults, even if the mode doesn't actually change. _termOutput = {}; - return _pConApi->PrivateSetAnsiMode(ansiMode); + _pConApi->SetParserMode(StateMachine::Mode::Ansi, ansiMode); + _pConApi->SetInputMode(TerminalInput::Mode::Ansi, ansiMode); + + // We don't check the SetInputMode return value, because we'll never want + // to forward a DECANM mode change over conpty. + return true; } // Routine Description: @@ -1687,9 +1696,10 @@ void AdaptDispatch::_InitTabStopsForWidth(const size_t width) //Routine Description: // DOCS - Selects the coding system through which character sets are activated. -// When ISO2022 is selected, the code page is set to ISO-8859-1, and both -// GL and GR areas of the code table can be remapped. When UTF8 is selected, -// the code page is set to UTF-8, and only the GL area can be remapped. +// When ISO2022 is selected, the code page is set to ISO-8859-1, C1 control +// codes are accepted, and both GL and GR areas of the code table can be +// remapped. When UTF8 is selected, the code page is set to UTF-8, the C1 +// control codes are disabled, and only the GL area can be remapped. //Arguments: // - codingSystem - The coding system that will be selected. // Return value: @@ -1712,6 +1722,7 @@ bool AdaptDispatch::DesignateCodingSystem(const VTID codingSystem) success = _pConApi->SetConsoleOutputCP(28591); if (success) { + AcceptC1Controls(true); _termOutput.EnableGrTranslation(true); } break; @@ -1719,6 +1730,7 @@ bool AdaptDispatch::DesignateCodingSystem(const VTID codingSystem) success = _pConApi->SetConsoleOutputCP(CP_UTF8); if (success) { + AcceptC1Controls(false); _termOutput.EnableGrTranslation(false); } break; @@ -1792,6 +1804,17 @@ bool AdaptDispatch::SingleShift(const size_t gsetNumber) return _termOutput.SingleShift(gsetNumber); } +//Routine Description: +// DECAC1 - Enable or disable the reception of C1 control codes in the parser. +//Arguments: +// - enabled - true to allow C1 controls to be used, false to disallow. +// Return value: +// True if handled successfully. False otherwise. +bool AdaptDispatch::AcceptC1Controls(const bool enabled) +{ + return _pConApi->SetParserMode(StateMachine::Mode::AcceptC1, enabled); +} + //Routine Description: // Soft Reset - Perform a soft reset. See http://www.vt100.net/docs/vt510-rm/DECSTR.html // The following table lists everything that should be done, 'X's indicate the ones that @@ -1840,6 +1863,8 @@ bool AdaptDispatch::SoftReset() // Restore initial code page if previously changed by a DOCS sequence. success = _pConApi->SetConsoleOutputCP(_initialCodePage.value()) && success; } + // Disable parsing of C1 control codes. + success = AcceptC1Controls(false) && success; success = SetGraphicsRendition({}) && success; // Normal rendition. diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index da53109df..be8db832d 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -101,6 +101,7 @@ namespace Microsoft::Console::VirtualTerminal bool LockingShift(const size_t gsetNumber) override; // LS0, LS1, LS2, LS3 bool LockingShiftRight(const size_t gsetNumber) override; // LS1R, LS2R, LS3R bool SingleShift(const size_t gsetNumber) override; // SS2, SS3 + bool AcceptC1Controls(const bool enabled) override; // DECAC1 bool SoftReset() override; // DECSTR bool HardReset() override; // RIS bool ScreenAlignmentPattern() override; // DECALN @@ -155,6 +156,7 @@ namespace Microsoft::Console::VirtualTerminal bool IsOriginModeRelative = false; TextAttribute Attributes = {}; TerminalOutput TermOutput = {}; + bool C1ControlsAccepted = false; unsigned int CodePage = 0; }; struct Offset diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index db61551e3..833e84b17 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -16,6 +16,7 @@ Author(s): #pragma once #include "../input/terminalInput.hpp" +#include "../parser/stateMachine.hpp" #include "../../types/inc/IInputEvent.hpp" #include "../../buffer/out/LineRendition.hpp" #include "../../buffer/out/TextAttribute.hpp" @@ -49,8 +50,9 @@ namespace Microsoft::Console::VirtualTerminal const SMALL_RECT& window) = 0; virtual bool SetInputMode(const TerminalInput::Mode mode, const bool enabled) = 0; + virtual bool SetParserMode(const StateMachine::Mode mode, const bool enabled) = 0; + virtual bool GetParserMode(const StateMachine::Mode mode) const = 0; - virtual bool PrivateSetAnsiMode(const bool ansiMode) = 0; virtual bool PrivateSetScreenMode(const bool reverseMode) = 0; virtual bool PrivateSetAutoWrapMode(const bool wrapAtEOL) = 0; diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 16af1776e..a9e4d830e 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -104,6 +104,7 @@ public: bool LockingShift(const size_t /*gsetNumber*/) noexcept override { return false; } // LS0, LS1, LS2, LS3 bool LockingShiftRight(const size_t /*gsetNumber*/) noexcept override { return false; } // LS1R, LS2R, LS3R bool SingleShift(const size_t /*gsetNumber*/) noexcept override { return false; } // SS2, SS3 + bool AcceptC1Controls(const bool /*enabled*/) noexcept override { return false; } // DECAC1 bool SoftReset() noexcept override { return false; } // DECSTR bool HardReset() noexcept override { return false; } // RIS diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index d494b2deb..98a1ab2c0 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -125,16 +125,24 @@ public: return _setInputModeResult; } - bool PrivateSetAnsiMode(const bool ansiMode) override + bool SetParserMode(const StateMachine::Mode mode, const bool enabled) override { - Log::Comment(L"PrivateSetAnsiMode MOCK called..."); + Log::Comment(L"SetParserMode MOCK called..."); - if (_privateSetAnsiModeResult) + if (_setParserModeResult) { - VERIFY_ARE_EQUAL(_expectedAnsiMode, ansiMode); + VERIFY_ARE_EQUAL(_expectedParserMode, mode); + VERIFY_ARE_EQUAL(_expectedParserModeEnabled, enabled); } - return _privateSetAnsiModeResult; + return _setParserModeResult; + } + + bool GetParserMode(const StateMachine::Mode /*mode*/) const override + { + Log::Comment(L"GetParserMode MOCK called..."); + + return false; } bool PrivateSetScreenMode(const bool /*reverseMode*/) override @@ -386,10 +394,14 @@ public: return FALSE; } - bool SetConsoleOutputCP(const unsigned int /*codepage*/) override + bool SetConsoleOutputCP(const unsigned int codepage) override { Log::Comment(L"SetConsoleOutputCP MOCK called..."); - return TRUE; + if (_setConsoleOutputCPResult) + { + VERIFY_ARE_EQUAL(_expectedOutputCP, codepage); + } + return _setConsoleOutputCPResult; } bool GetConsoleOutputCP(unsigned int& codepage) override @@ -712,8 +724,9 @@ public: bool _setInputModeResult = false; TerminalInput::Mode _expectedInputMode; bool _expectedInputModeEnabled = false; - bool _privateSetAnsiModeResult = false; - bool _expectedAnsiMode = false; + bool _setParserModeResult = false; + StateMachine::Mode _expectedParserMode; + bool _expectedParserModeEnabled = false; bool _privateAllowCursorBlinkingResult = false; bool _enable = false; // for cursor blinking bool _privateSetScrollingRegionResult = false; @@ -728,6 +741,7 @@ public: CursorType _expectedCursorStyle; bool _setCursorColorResult = false; COLORREF _expectedCursorColor = 0; + bool _setConsoleOutputCPResult = false; bool _getConsoleOutputCPResult = false; bool _moveToBottomResult = false; @@ -2057,15 +2071,17 @@ public: // success cases // set ansi mode = true Log::Comment(L"Test 1: ansi mode = true"); - _testGetSet->_privateSetAnsiModeResult = true; - _testGetSet->_expectedAnsiMode = true; + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::Ansi; + _testGetSet->_expectedParserModeEnabled = 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; + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::Ansi; + _testGetSet->_expectedParserModeEnabled = false; VERIFY_IS_TRUE(_pDispatch.get()->SetAnsiMode(false)); } @@ -2623,6 +2639,39 @@ public: VERIFY_IS_TRUE(decdld(CellMatrix::Default, 0, FontSet::Size132x24, FontUsage::FullCell, bitmapOf6x18)); } + TEST_METHOD(TogglingC1ParserMode) + { + Log::Comment(L"1. Accept C1 controls"); + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::AcceptC1; + _testGetSet->_expectedParserModeEnabled = true; + VERIFY_IS_TRUE(_pDispatch.get()->AcceptC1Controls(true)); + + Log::Comment(L"2. Don't accept C1 controls"); + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::AcceptC1; + _testGetSet->_expectedParserModeEnabled = false; + VERIFY_IS_TRUE(_pDispatch.get()->AcceptC1Controls(false)); + + Log::Comment(L"3. Designate ISO-2022 coding system"); + // Code page should be set to ISO-8859-1 and C1 parsing enabled + _testGetSet->_setConsoleOutputCPResult = true; + _testGetSet->_expectedOutputCP = 28591; + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::AcceptC1; + _testGetSet->_expectedParserModeEnabled = true; + VERIFY_IS_TRUE(_pDispatch.get()->DesignateCodingSystem(DispatchTypes::CodingSystem::ISO2022)); + + Log::Comment(L"4. Designate UTF-8 coding system"); + // Code page should be set to UTF-8 and C1 parsing disabled + _testGetSet->_setConsoleOutputCPResult = true; + _testGetSet->_expectedOutputCP = CP_UTF8; + _testGetSet->_setParserModeResult = true; + _testGetSet->_expectedParserMode = StateMachine::Mode::AcceptC1; + _testGetSet->_expectedParserModeEnabled = false; + VERIFY_IS_TRUE(_pDispatch.get()->DesignateCodingSystem(DispatchTypes::CodingSystem::UTF8)); + } + private: TestGetSet* _testGetSet; // non-ownership pointer std::unique_ptr _pDispatch; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 948ffaa4e..0c4532ec5 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -265,6 +265,10 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) success = _dispatch->LockingShiftRight(3); TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3R); break; + case EscActionCodes::DECAC1_AcceptC1Controls: + success = _dispatch->AcceptC1Controls(true); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC1); + break; case EscActionCodes::DECDHL_DoubleHeightLineTop: _dispatch->SetLineRendition(LineRendition::DoubleHeightTop); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 6c6462491..0a9c2e6a9 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -88,6 +88,7 @@ namespace Microsoft::Console::VirtualTerminal LS1R_LockingShift = VTID("~"), LS2R_LockingShift = VTID("}"), LS3R_LockingShift = VTID("|"), + DECAC1_AcceptC1Controls = VTID(" 7"), DECDHL_DoubleHeightLineTop = VTID("#3"), DECDHL_DoubleHeightLineBottom = VTID("#4"), DECSWL_SingleWidthLine = VTID("#5"), diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp index 51392bed9..8516049af 100644 --- a/src/terminal/parser/stateMachine.cpp +++ b/src/terminal/parser/stateMachine.cpp @@ -14,7 +14,6 @@ StateMachine::StateMachine(std::unique_ptr engine) : _engine(std::move(engine)), _state(VTStates::Ground), _trace(Microsoft::Console::VirtualTerminal::ParserTracing()), - _isInAnsiMode(true), _parameters{}, _parameterLimitReached(false), _oscString{}, @@ -24,9 +23,14 @@ StateMachine::StateMachine(std::unique_ptr engine) : _ActionClear(); } -void StateMachine::SetAnsiMode(bool ansiMode) noexcept +void StateMachine::SetParserMode(const Mode mode, const bool enabled) { - _isInAnsiMode = ansiMode; + _parserMode.set(mode, enabled); +} + +bool StateMachine::GetParserMode(const Mode mode) const +{ + return _parserMode.test(mode); } const IStateMachineEngine& StateMachine::Engine() const noexcept @@ -1064,7 +1068,7 @@ void StateMachine::_EventEscape(const wchar_t wch) _EnterEscapeIntermediate(); } } - else if (_isInAnsiMode) + else if (_parserMode.test(Mode::Ansi)) { if (_isCsiIndicator(wch)) { @@ -1129,7 +1133,7 @@ void StateMachine::_EventEscapeIntermediate(const wchar_t wch) { _ActionIgnore(); } - else if (_isInAnsiMode) + else if (_parserMode.test(Mode::Ansi)) { _ActionEscDispatch(wch); _EnterGround(); @@ -1729,8 +1733,16 @@ void StateMachine::ProcessCharacter(const wchar_t wch) // Preprocess C1 control characters and treat them as ESC + their 7-bit equivalent. else if (_isC1ControlCharacter(wch)) { - ProcessCharacter(AsciiChars::ESC); - ProcessCharacter(_c1To7Bit(wch)); + // But note that we only do this if C1 control code parsing has been + // explicitly requested, since there are some code pages with "unmapped" + // code points that get translated as C1 controls when that is not their + // intended use. In order to avoid them triggering unintentional escape + // sequences, we ignore these characters by default. + if (_parserMode.test(Mode::AcceptC1)) + { + ProcessCharacter(AsciiChars::ESC); + ProcessCharacter(_c1To7Bit(wch)); + } } // Don't go to escape from the OSC string state - ESC can be used to terminate OSC strings. else if (_isEscape(wch) && _state != VTStates::OscString) diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp index 2b72c2307..56b9e3ecc 100644 --- a/src/terminal/parser/stateMachine.hpp +++ b/src/terminal/parser/stateMachine.hpp @@ -42,7 +42,14 @@ namespace Microsoft::Console::VirtualTerminal public: StateMachine(std::unique_ptr engine); - void SetAnsiMode(bool ansiMode) noexcept; + enum class Mode : size_t + { + AcceptC1, + Ansi, + }; + + void SetParserMode(const Mode mode, const bool enabled); + bool GetParserMode(const Mode mode) const; void ProcessCharacter(const wchar_t wch); void ProcessString(const std::wstring_view string); @@ -144,7 +151,7 @@ namespace Microsoft::Console::VirtualTerminal VTStates _state; - bool _isInAnsiMode; + til::enumset _parserMode{ Mode::Ansi }; std::wstring_view _currentString; size_t _runOffset; diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index 12a2235d4..8985fe782 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -273,6 +273,7 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[OSCBG], "OscBackgroundColor"), TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"), TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), + TraceLoggingUInt32(_uiTimesUsed[DECAC1], "DECAC1"), TraceLoggingUInt32(_uiTimesUsed[DECSWL], "DECSWL"), TraceLoggingUInt32(_uiTimesUsed[DECDWL], "DECDWL"), TraceLoggingUInt32(_uiTimesUsed[DECDHL], "DECDHL"), diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index ec79bf81c..2f59800ca 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -99,6 +99,7 @@ namespace Microsoft::Console::VirtualTerminal REP, OSCFG, OSCBG, + DECAC1, DECSWL, DECDWL, DECDHL, diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp index f9471f47f..d7c8d563c 100644 --- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -269,6 +269,9 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + // Enable the acceptance of C1 control codes in the state machine. + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); mach.ProcessCharacter(L'\x9b'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); @@ -447,6 +450,9 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + // Enable the acceptance of C1 control codes in the state machine. + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); mach.ProcessCharacter(L'\x9d'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); @@ -696,6 +702,9 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + // Enable the acceptance of C1 control codes in the state machine. + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); mach.ProcessCharacter(L'\x90'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::DcsEntry); @@ -913,6 +922,9 @@ class Microsoft::Console::VirtualTerminal::OutputEngineTest final auto engine = std::make_unique(std::move(dispatch)); StateMachine mach(std::move(engine)); + // Enable the acceptance of C1 control codes in the state machine. + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + // C1 ST should terminate OSC string. VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); mach.ProcessCharacter(AsciiChars::ESC); @@ -978,12 +990,14 @@ public: { } - virtual void Print(const wchar_t /*wchPrintable*/) override + virtual void Print(const wchar_t wchPrintable) override { + _printString += wchPrintable; } - virtual void PrintString(const std::wstring_view /*string*/) override + virtual void PrintString(const std::wstring_view string) override { + _printString += string; } StatefulDispatch() : @@ -1471,6 +1485,7 @@ public: return true; } + std::wstring _printString; size_t _cursorDistance; size_t _line; size_t _column; @@ -1828,7 +1843,7 @@ class StateMachineExternalTest final pDispatch->ClearState(); pDispatch->_isInAnsiMode = false; - mach.SetAnsiMode(false); + mach.SetParserMode(StateMachine::Mode::Ansi, false); mach.ProcessString(L"\x1b<"); VERIFY_IS_TRUE(pDispatch->_isInAnsiMode); @@ -2737,7 +2752,7 @@ class StateMachineExternalTest final StateMachine mach(std::move(engine)); // ANSI mode must be reset for VT52 sequences to be recognized. - mach.SetAnsiMode(false); + mach.SetParserMode(StateMachine::Mode::Ansi, false); Log::Comment(L"Cursor Up"); mach.ProcessCharacter(AsciiChars::ESC); @@ -2823,7 +2838,7 @@ class StateMachineExternalTest final StateMachine mach(std::move(engine)); Log::Comment(L"Identify Device in VT52 mode."); - mach.SetAnsiMode(false); + mach.SetParserMode(StateMachine::Mode::Ansi, false); mach.ProcessCharacter(AsciiChars::ESC); mach.ProcessCharacter(L'Z'); VERIFY_IS_TRUE(pDispatch->_vt52DeviceAttributes); @@ -2832,7 +2847,7 @@ class StateMachineExternalTest final pDispatch->ClearState(); Log::Comment(L"Identify Device in ANSI mode."); - mach.SetAnsiMode(true); + mach.SetParserMode(StateMachine::Mode::Ansi, true); mach.ProcessCharacter(AsciiChars::ESC); mach.ProcessCharacter(L'Z'); VERIFY_IS_TRUE(pDispatch->_deviceAttributes); @@ -3298,76 +3313,127 @@ class StateMachineExternalTest final // First we test with no custom id // Process the opening osc 8 sequence - mach.ProcessString(L"\x1b]8;;test.url\x9c"); + mach.ProcessString(L"\x1b]8;;test.url\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"test.url"); VERIFY_IS_TRUE(pDispatch->_customId.empty()); // Process the closing osc 8 sequences - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); // Next we test with a custom id // Process the opening osc 8 sequence - mach.ProcessString(L"\x1b]8;id=testId;test2.url\x9c"); + mach.ProcessString(L"\x1b]8;id=testId;test2.url\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"test2.url"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); // Process the closing osc 8 sequence - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); // Let's try more complicated params and URLs - mach.ProcessString(L"\x1b]8;id=testId;https://example.com\x9c"); + mach.ProcessString(L"\x1b]8;id=testId;https://example.com\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"https://example.com"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); // Multiple params - mach.ProcessString(L"\x1b]8;id=testId:foo=bar;https://example.com\x9c"); + mach.ProcessString(L"\x1b]8;id=testId:foo=bar;https://example.com\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"https://example.com"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); - mach.ProcessString(L"\x1b]8;foo=bar:id=testId;https://example.com\x9c"); + mach.ProcessString(L"\x1b]8;foo=bar:id=testId;https://example.com\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"https://example.com"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); // URIs with query strings - mach.ProcessString(L"\x1b]8;id=testId;https://example.com?query1=value1\x9c"); + mach.ProcessString(L"\x1b]8;id=testId;https://example.com?query1=value1\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"https://example.com?query1=value1"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); - mach.ProcessString(L"\x1b]8;id=testId;https://example.com?query1=value1;value2;value3\x9c"); + mach.ProcessString(L"\x1b]8;id=testId;https://example.com?query1=value1;value2;value3\x1b\\"); VERIFY_IS_TRUE(pDispatch->_hyperlinkMode); VERIFY_ARE_EQUAL(pDispatch->_uri, L"https://example.com?query1=value1;value2;value3"); VERIFY_ARE_EQUAL(pDispatch->_customId, L"testId"); - mach.ProcessString(L"\x1b]8;;\x9c"); + mach.ProcessString(L"\x1b]8;;\x1b\\"); VERIFY_IS_FALSE(pDispatch->_hyperlinkMode); VERIFY_IS_TRUE(pDispatch->_uri.empty()); pDispatch->ClearState(); } + + TEST_METHOD(TestC1ParserMode) + { + auto dispatch = std::make_unique(); + auto pDispatch = dispatch.get(); + auto engine = std::make_unique(std::move(dispatch)); + StateMachine mach(std::move(engine)); + + Log::Comment(L"C1 parsing disabled: CSI control ignored and rest of sequence printed"); + mach.SetParserMode(StateMachine::Mode::AcceptC1, false); + mach.ProcessString(L"\u009b" + L"123A"); + VERIFY_IS_FALSE(pDispatch->_cursorUp); + VERIFY_ARE_EQUAL(pDispatch->_printString, L"123A"); + + pDispatch->ClearState(); + + Log::Comment(L"C1 parsing enabled: CSI interpreted and CUP sequence executed"); + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + mach.ProcessString(L"\u009b" + L"123A"); + VERIFY_IS_TRUE(pDispatch->_cursorUp); + VERIFY_ARE_EQUAL(pDispatch->_cursorDistance, 123u); + + pDispatch->ClearState(); + + Log::Comment(L"C1 parsing disabled: NEL has no effect within a sequence"); + mach.SetParserMode(StateMachine::Mode::AcceptC1, false); + mach.ProcessString(L"\x1b[12" + L"\u0085" + L";34H"); + VERIFY_IS_FALSE(pDispatch->_lineFeed); + VERIFY_IS_TRUE(pDispatch->_cursorPosition); + VERIFY_ARE_EQUAL(pDispatch->_line, 12u); + VERIFY_ARE_EQUAL(pDispatch->_column, 34u); + VERIFY_ARE_EQUAL(pDispatch->_printString, L""); + + pDispatch->ClearState(); + + Log::Comment(L"C1 parsing enabled: NEL aborts sequence and executes line feed"); + mach.SetParserMode(StateMachine::Mode::AcceptC1, true); + mach.ProcessString(L"\x1b[12" + L"\u0085" + L";34H"); + VERIFY_IS_TRUE(pDispatch->_lineFeed); + VERIFY_ARE_EQUAL(DispatchTypes::LineFeedType::WithReturn, pDispatch->_lineFeedType); + VERIFY_IS_FALSE(pDispatch->_cursorPosition); + VERIFY_ARE_EQUAL(pDispatch->_printString, L";34H"); + + pDispatch->ClearState(); + } };