Skip to content

Commit

Permalink
Clean up WriteConsoleInputA conversion (#15672)
Browse files Browse the repository at this point in the history
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 ✅
  • Loading branch information
lhecker authored Jul 18, 2023
1 parent 1c88629 commit 11a9808
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 112 deletions.
185 changes: 74 additions & 111 deletions src/host/directio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::unique_ptr<IInputEvent>>& inEvents,
_Out_ std::unique_ptr<IInputEvent>& partialEvent)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
std::deque<std::unique_ptr<IInputEvent>> 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<const KeyEvent* const>(currentEvent.get());

std::wstring outWChar;
auto hr = S_OK;

// convert char data to unicode
if (IsDBCSLeadByteConsole(static_cast<char>(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<const KeyEvent* const>(inEvents.front().get());
inEvents.pop_front();

char inBytes[] = {
static_cast<char>(keyEvent->GetCharData()),
static_cast<char>(keyEventEndByte->GetCharData())
};
try
{
outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) });
}
catch (...)
{
hr = wil::ResultFromCaughtException();
}
}
else
{
char inBytes[] = {
static_cast<char>(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<KeyEvent>(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
Expand Down Expand Up @@ -237,35 +138,97 @@ void EventsToUnicode(_Inout_ std::deque<std::unique_ptr<IInputEvent>>& inEvents,
const std::span<const INPUT_RECORD> 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<INPUT_RECORD, 16> 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<IInputEvent> 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
Expand Down
33 changes: 32 additions & 1 deletion src/host/ft_host/CJK_DbcsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// Licensed under the MIT license.

#include "precomp.h"

#include <io.h>
#include <fcntl.h>
#include <iostream>

#include "../../types/inc/IInputEvent.hpp"

#define JAPANESE_CP 932u

Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 11a9808

Please sign in to comment.