From 11a9808d850feb6816d2d7364554b9f4ddd15350 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 18 Jul 2023 20:16:03 +0200 Subject: [PATCH] Clean up WriteConsoleInputA conversion (#15672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit inlines `EventsToUnicode` into `WriteConsoleInputAImpl` because soon we'll not use deques for events anymore and so the old code won't work. It cleans up the implementation because I intend to move all this code directly into `InputBuffer` to have a better and tighter control over how text gets converted. UTF-8 input for instance requires the storage of up to 3 input events and this code is not fit to handle that. It's also unmaintainable because our input handling code shouldn't be spread over a dozen files either. 😄 ## Validation Steps Performed * Unit and feature tests are ✅ --- src/host/directio.cpp | 185 ++++++++++++----------------- src/host/ft_host/CJK_DbcsTests.cpp | 33 ++++- 2 files changed, 106 insertions(+), 112 deletions(-) diff --git a/src/host/directio.cpp b/src/host/directio.cpp index 8e6456f0526..b5ababfe4b4 100644 --- a/src/host/directio.cpp +++ b/src/host/directio.cpp @@ -25,105 +25,6 @@ using namespace Microsoft::Console::Types; using Microsoft::Console::Interactivity::ServiceLocator; -class CONSOLE_INFORMATION; - -// Routine Description: -// - converts non-unicode InputEvents to unicode InputEvents -// Arguments: -// inEvents - InputEvents to convert -// partialEvent - on output, will contain a partial dbcs byte char -// data if the last event in inEvents is a dbcs lead byte -// Return Value: -// - inEvents will contain unicode InputEvents -// - partialEvent may contain a partial dbcs KeyEvent -void EventsToUnicode(_Inout_ std::deque>& inEvents, - _Out_ std::unique_ptr& partialEvent) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - std::deque> outEvents; - - while (!inEvents.empty()) - { - auto currentEvent = std::move(inEvents.front()); - inEvents.pop_front(); - - if (currentEvent->EventType() != InputEventType::KeyEvent) - { - outEvents.push_back(std::move(currentEvent)); - } - else - { - const auto keyEvent = static_cast(currentEvent.get()); - - std::wstring outWChar; - auto hr = S_OK; - - // convert char data to unicode - if (IsDBCSLeadByteConsole(static_cast(keyEvent->GetCharData()), &gci.CPInfo)) - { - if (inEvents.empty()) - { - // we ran out of data and have a partial byte leftover - partialEvent = std::move(currentEvent); - break; - } - - // get the 2nd byte and convert to unicode - const auto keyEventEndByte = static_cast(inEvents.front().get()); - inEvents.pop_front(); - - char inBytes[] = { - static_cast(keyEvent->GetCharData()), - static_cast(keyEventEndByte->GetCharData()) - }; - try - { - outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); - } - catch (...) - { - hr = wil::ResultFromCaughtException(); - } - } - else - { - char inBytes[] = { - static_cast(keyEvent->GetCharData()) - }; - try - { - outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); - } - catch (...) - { - hr = wil::ResultFromCaughtException(); - } - } - - // push unicode key events back out - if (SUCCEEDED(hr) && outWChar.size() > 0) - { - auto unicodeKeyEvent = *keyEvent; - for (const auto wch : outWChar) - { - try - { - unicodeKeyEvent.SetCharData(wch); - outEvents.push_back(std::make_unique(unicodeKeyEvent)); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } - } - } - } - } - - inEvents.swap(outEvents); - return; -} - // Routine Description: // - This routine reads or peeks input events. In both cases, the events // are copied to the user's buffer. In the read case they are removed @@ -237,35 +138,97 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents, const std::span buffer, size_t& written, const bool append) noexcept +try { written = 0; + if (buffer.empty()) + { + return S_OK; + } + LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - try + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + til::small_vector events; + + auto it = buffer.begin(); + const auto end = buffer.end(); + + // Check out the loop below. When a previous call ended on a leading DBCS we store it for + // the next call to WriteConsoleInputAImpl to join it with the now available trailing DBCS. + if (context.IsWritePartialByteSequenceAvailable()) { - auto events = IInputEvent::Create(buffer); + auto lead = context.FetchWritePartialByteSequence(false)->ToInputRecord(); + const auto& trail = *it; - // add partial byte event if necessary - if (context.IsWritePartialByteSequenceAvailable()) + if (trail.EventType == KEY_EVENT) { - events.push_front(context.FetchWritePartialByteSequence(false)); + const char narrow[2]{ + lead.Event.KeyEvent.uChar.AsciiChar, + trail.Event.KeyEvent.uChar.AsciiChar, + }; + wchar_t wide[2]; + const auto length = MultiByteToWideChar(gci.CP, 0, &narrow[0], 2, &wide[0], 2); + + for (int i = 0; i < length; i++) + { + lead.Event.KeyEvent.uChar.UnicodeChar = wide[i]; + events.push_back(lead); + } + + ++it; } + } - // convert to unicode if necessary - std::unique_ptr partialEvent; - EventsToUnicode(events, partialEvent); + for (; it != end; ++it) + { + if (it->EventType != KEY_EVENT) + { + events.push_back(*it); + continue; + } - if (partialEvent.get()) + auto lead = *it; + char narrow[2]{ lead.Event.KeyEvent.uChar.AsciiChar }; + int narrowLength = 1; + + if (IsDBCSLeadByteConsole(lead.Event.KeyEvent.uChar.AsciiChar, &gci.CPInfo)) { - context.StoreWritePartialByteSequence(std::move(partialEvent)); + ++it; + if (it == end) + { + // Missing trailing DBCS -> Store the lead for the next call to WriteConsoleInputAImpl. + context.StoreWritePartialByteSequence(IInputEvent::Create(lead)); + break; + } + + const auto& trail = *it; + if (trail.EventType != KEY_EVENT) + { + // Invalid input -> Skip. + continue; + } + + narrow[1] = trail.Event.KeyEvent.uChar.AsciiChar; + narrowLength = 2; } - return _WriteConsoleInputWImplHelper(context, events, written, append); + wchar_t wide[2]; + const auto length = MultiByteToWideChar(gci.CP, 0, &narrow[0], narrowLength, &wide[0], 2); + + for (int i = 0; i < length; i++) + { + lead.Event.KeyEvent.uChar.UnicodeChar = wide[i]; + events.push_back(lead); + } } - CATCH_RETURN(); + + auto result = IInputEvent::Create(std::span{ events.data(), events.size() }); + return _WriteConsoleInputWImplHelper(context, result, written, append); } +CATCH_RETURN(); // Routine Description: // - Writes events to the input buffer diff --git a/src/host/ft_host/CJK_DbcsTests.cpp b/src/host/ft_host/CJK_DbcsTests.cpp index 05664cbbe71..6e29b8a815c 100644 --- a/src/host/ft_host/CJK_DbcsTests.cpp +++ b/src/host/ft_host/CJK_DbcsTests.cpp @@ -2,9 +2,11 @@ // Licensed under the MIT license. #include "precomp.h" + #include #include -#include + +#include "../../types/inc/IInputEvent.hpp" #define JAPANESE_CP 932u @@ -115,6 +117,7 @@ class DbcsTests // This test must come before ones that launch another process as launching another process can tamper with the codepage // in ways that this test is not expecting. TEST_METHOD(TestMultibyteInputRetrieval); + TEST_METHOD(TestMultibyteInputCoalescing); BEGIN_TEST_METHOD(TestDbcsWriteRead) TEST_METHOD_PROPERTY(L"Data:fUseTrueTypeFont", L"{true, false}") @@ -1906,6 +1909,34 @@ void DbcsTests::TestMultibyteInputRetrieval() FlushConsoleInputBuffer(hIn); } +// This test ensures that two separate WriteConsoleInputA with trailing/leading DBCS are joined (coalesced) into a single wide character. +void DbcsTests::TestMultibyteInputCoalescing() +{ + SetConsoleCP(932); + + const auto in = GetStdHandle(STD_INPUT_HANDLE); + FlushConsoleInputBuffer(in); + + DWORD count; + { + const auto record = KeyEvent{ true, 1, 123, 456, 0x82, 789 }.ToInputRecord(); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); + } + { + const auto record = KeyEvent{ true, 1, 234, 567, 0xA2, 890 }.ToInputRecord(); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count)); + } + + // Asking for 2 records and asserting we only got 1 ensures + // that we receive the exact number of expected records. + INPUT_RECORD actual[2]; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(in, &actual[0], 2, &count)); + VERIFY_ARE_EQUAL(1u, count); + + const auto expected = KeyEvent{ true, 1, 123, 456, L'い', 789 }.ToInputRecord(); + VERIFY_ARE_EQUAL(expected, actual[0]); +} + void DbcsTests::TestDbcsOneByOne() { const auto hOut = GetStdOutputHandle();