1248 lines
53 KiB
C++
1248 lines
53 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "ApiRoutines.h"
|
|
|
|
#include "_stream.h"
|
|
#include "stream.h"
|
|
#include "writeData.hpp"
|
|
|
|
#include "_output.h"
|
|
#include "output.h"
|
|
#include "dbcs.h"
|
|
#include "handle.h"
|
|
#include "misc.h"
|
|
|
|
#include "../types/inc/convert.hpp"
|
|
#include "../types/inc/GlyphWidth.hpp"
|
|
#include "../types/inc/Viewport.hpp"
|
|
|
|
#include "..\interactivity\inc\ServiceLocator.hpp"
|
|
|
|
#pragma hdrstop
|
|
using namespace Microsoft::Console::Types;
|
|
using Microsoft::Console::Interactivity::ServiceLocator;
|
|
using Microsoft::Console::VirtualTerminal::StateMachine;
|
|
// Used by WriteCharsLegacy.
|
|
#define IS_GLYPH_CHAR(wch) (((wch) >= L' ') && ((wch) != 0x007F))
|
|
|
|
constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
|
|
|
|
// Routine Description:
|
|
// - This routine updates the cursor position. Its input is the non-special
|
|
// cased new location of the cursor. For example, if the cursor were being
|
|
// moved one space backwards from the left edge of the screen, the X
|
|
// coordinate would be -1. This routine would set the X coordinate to
|
|
// the right edge of the screen and decrement the Y coordinate by one.
|
|
// Arguments:
|
|
// - screenInfo - reference to screen buffer information structure.
|
|
// - coordCursor - New location of cursor.
|
|
// - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge
|
|
// Return Value:
|
|
[[nodiscard]] NTSTATUS AdjustCursorPosition(SCREEN_INFORMATION& screenInfo,
|
|
_In_ COORD coordCursor,
|
|
const BOOL fKeepCursorVisible,
|
|
_Inout_opt_ PSHORT psScrollY)
|
|
{
|
|
const bool inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
|
const COORD bufferSize = screenInfo.GetBufferSize().Dimensions();
|
|
if (coordCursor.X < 0)
|
|
{
|
|
if (coordCursor.Y > 0)
|
|
{
|
|
coordCursor.X = (SHORT)(bufferSize.X + coordCursor.X);
|
|
coordCursor.Y = (SHORT)(coordCursor.Y - 1);
|
|
}
|
|
else
|
|
{
|
|
coordCursor.X = 0;
|
|
}
|
|
}
|
|
else if (coordCursor.X >= bufferSize.X)
|
|
{
|
|
// at end of line. if wrap mode, wrap cursor. otherwise leave it where it is.
|
|
if (screenInfo.OutputMode & ENABLE_WRAP_AT_EOL_OUTPUT)
|
|
{
|
|
coordCursor.Y += coordCursor.X / bufferSize.X;
|
|
coordCursor.X = coordCursor.X % bufferSize.X;
|
|
}
|
|
else
|
|
{
|
|
if (inVtMode)
|
|
{
|
|
// In VT mode, the cursor must be left in the last column.
|
|
coordCursor.X = bufferSize.X - 1;
|
|
}
|
|
else
|
|
{
|
|
// For legacy apps, it is left where it was at the start of the write.
|
|
coordCursor.X = screenInfo.GetTextBuffer().GetCursor().GetPosition().X;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The VT standard requires the lines revealed when scrolling are filled
|
|
// with the current background color, but with no meta attributes set.
|
|
auto fillAttributes = screenInfo.GetAttributes();
|
|
fillAttributes.SetStandardErase();
|
|
|
|
const auto relativeMargins = screenInfo.GetRelativeScrollMargins();
|
|
auto viewport = screenInfo.GetViewport();
|
|
SMALL_RECT srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive();
|
|
const bool fMarginsSet = srMargins.Bottom > srMargins.Top;
|
|
COORD currentCursor = screenInfo.GetTextBuffer().GetCursor().GetPosition();
|
|
const int iCurrentCursorY = currentCursor.Y;
|
|
|
|
const bool fCursorInMargins = iCurrentCursorY <= srMargins.Bottom && iCurrentCursorY >= srMargins.Top;
|
|
const bool cursorAboveViewport = coordCursor.Y < 0 && inVtMode;
|
|
const bool fScrollDown = fMarginsSet && fCursorInMargins && (coordCursor.Y > srMargins.Bottom);
|
|
bool fScrollUp = fMarginsSet && fCursorInMargins && (coordCursor.Y < srMargins.Top);
|
|
|
|
const bool fScrollUpWithoutMargins = (!fMarginsSet) && cursorAboveViewport;
|
|
// if we're in VT mode, AND MARGINS AREN'T SET and a Reverse Line Feed took the cursor up past the top of the viewport,
|
|
// VT style scroll the contents of the screen.
|
|
// This can happen in applications like `less`, that don't set margins, because they're going to
|
|
// scroll the entire screen anyways, so no need for them to ever set the margins.
|
|
if (fScrollUpWithoutMargins)
|
|
{
|
|
fScrollUp = true;
|
|
srMargins.Top = 0;
|
|
srMargins.Bottom = screenInfo.GetViewport().BottomInclusive();
|
|
}
|
|
|
|
const bool scrollDownAtTop = fScrollDown && relativeMargins.Top() == 0;
|
|
if (scrollDownAtTop)
|
|
{
|
|
// We're trying to scroll down, and the top margin is at the top of the viewport.
|
|
// In this case, we want the lines that are "scrolled off" to appear in
|
|
// the scrollback instead of being discarded.
|
|
// To do this, we're going to scroll everything starting at the bottom
|
|
// margin down, then move the viewport down.
|
|
|
|
const SHORT delta = coordCursor.Y - srMargins.Bottom;
|
|
SMALL_RECT scrollRect{ 0 };
|
|
scrollRect.Left = 0;
|
|
scrollRect.Top = srMargins.Bottom + 1; // One below margins
|
|
scrollRect.Bottom = bufferSize.Y - 1; // -1, otherwise this would be an exclusive rect.
|
|
scrollRect.Right = bufferSize.X - 1; // -1, otherwise this would be an exclusive rect.
|
|
|
|
// This is the Y position we're moving the contents below the bottom margin to.
|
|
SHORT moveToYPosition = scrollRect.Top + delta;
|
|
|
|
// This is where the viewport will need to be to give the effect of
|
|
// scrolling the contents in the margins.
|
|
SHORT newViewTop = viewport.Top() + delta;
|
|
|
|
// This is how many new lines need to be added to the buffer to support this operation.
|
|
const SHORT newRows = (viewport.BottomExclusive() + delta) - bufferSize.Y;
|
|
|
|
// If we're near the bottom of the buffer, we might need to insert some
|
|
// new rows at the bottom.
|
|
// If we do this, then the viewport is now one line higher than it used
|
|
// to be, so it needs to move down by one less line.
|
|
for (auto i = 0; i < newRows; i++)
|
|
{
|
|
screenInfo.GetTextBuffer().IncrementCircularBuffer();
|
|
moveToYPosition--;
|
|
newViewTop--;
|
|
scrollRect.Top--;
|
|
}
|
|
|
|
const COORD newPostMarginsOrigin = { 0, moveToYPosition };
|
|
const COORD newViewOrigin = { 0, newViewTop };
|
|
|
|
try
|
|
{
|
|
ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes);
|
|
}
|
|
CATCH_LOG();
|
|
|
|
// Move the viewport down
|
|
auto hr = screenInfo.SetViewportOrigin(true, newViewOrigin, true);
|
|
if (FAILED(hr))
|
|
{
|
|
return NTSTATUS_FROM_HRESULT(hr);
|
|
}
|
|
// If we didn't actually move the viewport, it's because we're at the
|
|
// bottom of the buffer, and the top lines of the viewport have
|
|
// changed. Manually invalidate here, to make sure the screen
|
|
// displays the correct text.
|
|
if (newViewOrigin == viewport.Origin())
|
|
{
|
|
Viewport invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), delta });
|
|
screenInfo.GetRenderTarget().TriggerRedraw(invalid);
|
|
}
|
|
|
|
// reset where our local viewport is, and recalculate the cursor and
|
|
// margin positions.
|
|
viewport = screenInfo.GetViewport();
|
|
if (newRows > 0)
|
|
{
|
|
currentCursor.Y -= newRows;
|
|
coordCursor.Y -= newRows;
|
|
}
|
|
srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive();
|
|
}
|
|
|
|
// If we did the above scrollDownAtTop case, then we've already scrolled
|
|
// the margins content, and we can skip this.
|
|
if (fScrollUp || (fScrollDown && !scrollDownAtTop))
|
|
{
|
|
SHORT diff = coordCursor.Y - (fScrollUp ? srMargins.Top : srMargins.Bottom);
|
|
|
|
SMALL_RECT scrollRect = { 0 };
|
|
scrollRect.Top = srMargins.Top;
|
|
scrollRect.Bottom = srMargins.Bottom;
|
|
scrollRect.Left = 0; // NOTE: Left/Right Scroll margins don't do anything currently.
|
|
scrollRect.Right = bufferSize.X - 1; // -1, otherwise this would be an exclusive rect.
|
|
|
|
COORD dest;
|
|
dest.X = scrollRect.Left;
|
|
dest.Y = scrollRect.Top - diff;
|
|
|
|
try
|
|
{
|
|
ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes);
|
|
}
|
|
CATCH_LOG();
|
|
|
|
coordCursor.Y -= diff;
|
|
}
|
|
|
|
// If the margins are set, then it shouldn't be possible for the cursor to
|
|
// move below the bottom of the viewport. Either it should be constrained
|
|
// inside the margins by one of the scrollDown cases handled above, or
|
|
// we'll need to clamp it inside the viewport here.
|
|
if (fMarginsSet && coordCursor.Y > viewport.BottomInclusive())
|
|
{
|
|
coordCursor.Y = viewport.BottomInclusive();
|
|
}
|
|
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
|
|
if (coordCursor.Y >= bufferSize.Y)
|
|
{
|
|
// At the end of the buffer. Scroll contents of screen buffer so new position is visible.
|
|
FAIL_FAST_IF(!(coordCursor.Y == bufferSize.Y));
|
|
if (!StreamScrollRegion(screenInfo))
|
|
{
|
|
Status = STATUS_NO_MEMORY;
|
|
}
|
|
|
|
if (nullptr != psScrollY)
|
|
{
|
|
*psScrollY += (SHORT)(bufferSize.Y - coordCursor.Y - 1);
|
|
}
|
|
coordCursor.Y += (SHORT)(bufferSize.Y - coordCursor.Y - 1);
|
|
}
|
|
|
|
const bool cursorMovedPastViewport = coordCursor.Y > screenInfo.GetViewport().BottomInclusive();
|
|
const bool cursorMovedPastVirtualViewport = coordCursor.Y > screenInfo.GetVirtualViewport().BottomInclusive();
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
// if at right or bottom edge of window, scroll right or down one char.
|
|
if (cursorMovedPastViewport)
|
|
{
|
|
COORD WindowOrigin;
|
|
WindowOrigin.X = 0;
|
|
WindowOrigin.Y = coordCursor.Y - screenInfo.GetViewport().BottomInclusive();
|
|
Status = screenInfo.SetViewportOrigin(false, WindowOrigin, true);
|
|
}
|
|
}
|
|
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
if (fKeepCursorVisible)
|
|
{
|
|
screenInfo.MakeCursorVisible(coordCursor);
|
|
}
|
|
Status = screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible);
|
|
|
|
// MSFT:19989333 - Only re-initialize the cursor row if the cursor moved
|
|
// below the terminal section of the buffer (the virtual viewport),
|
|
// and the visible part of the buffer (the actual viewport).
|
|
// If this is only cursorMovedPastViewport, and you scroll up, then type
|
|
// a character, we'll re-initialize the line the cursor is on.
|
|
// If this is only cursorMovedPastVirtualViewport and you scroll down,
|
|
// (with terminal scrolling disabled) then all lines newly exposed
|
|
// will get their attributes constantly cleared out.
|
|
// Both cursorMovedPastViewport and cursorMovedPastVirtualViewport works
|
|
if (inVtMode && cursorMovedPastViewport && cursorMovedPastVirtualViewport)
|
|
{
|
|
screenInfo.InitializeCursorRowAttributes();
|
|
}
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This routine writes a string to the screen, processing any embedded
|
|
// unicode characters. The string is also copied to the input buffer, if
|
|
// the output mode is line mode.
|
|
// Arguments:
|
|
// - screenInfo - reference to screen buffer information structure.
|
|
// - pwchBufferBackupLimit - Pointer to beginning of buffer.
|
|
// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode.
|
|
// This pointer is updated to point to the next position in the buffer.
|
|
// - pwchRealUnicode - Pointer to string to write.
|
|
// - pcb - On input, number of bytes to write. On output, number of bytes written.
|
|
// - pcSpaces - On output, the number of spaces consumed by the written characters.
|
|
// - dwFlags -
|
|
// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters.
|
|
// WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge
|
|
// WC_ECHO if called by Read (echoing characters)
|
|
// Return Value:
|
|
// Note:
|
|
// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services.
|
|
[[nodiscard]] NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo,
|
|
_In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit,
|
|
_In_ const wchar_t* pwchBuffer,
|
|
_In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode,
|
|
_Inout_ size_t* const pcb,
|
|
_Out_opt_ size_t* const pcSpaces,
|
|
const SHORT sOriginalXPosition,
|
|
const DWORD dwFlags,
|
|
_Inout_opt_ PSHORT const psScrollY)
|
|
{
|
|
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
TextBuffer& textBuffer = screenInfo.GetTextBuffer();
|
|
Cursor& cursor = textBuffer.GetCursor();
|
|
COORD CursorPosition = cursor.GetPosition();
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
SHORT XPosition;
|
|
WCHAR LocalBuffer[LOCAL_BUFFER_SIZE];
|
|
size_t TempNumSpaces = 0;
|
|
const bool fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT);
|
|
const bool fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
|
|
|
|
// Must not adjust cursor here. It has to stay on for many write scenarios. Consumers should call for the
|
|
// cursor to be turned off if they want that.
|
|
|
|
const TextAttribute Attributes = screenInfo.GetAttributes();
|
|
const size_t BufferSize = *pcb;
|
|
*pcb = 0;
|
|
|
|
const wchar_t* lpString = pwchRealUnicode;
|
|
|
|
const COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
|
|
|
|
while (*pcb < BufferSize)
|
|
{
|
|
// correct for delayed EOL
|
|
if (cursor.IsDelayedEOLWrap() && fWrapAtEOL)
|
|
{
|
|
const COORD coordDelayedAt = cursor.GetDelayedAtPosition();
|
|
cursor.ResetDelayEOLWrap();
|
|
// Only act on a delayed EOL if we didn't move the cursor to a different position from where the EOL was marked.
|
|
if (coordDelayedAt.X == CursorPosition.X && coordDelayedAt.Y == CursorPosition.Y)
|
|
{
|
|
CursorPosition.X = 0;
|
|
CursorPosition.Y++;
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
|
|
|
|
CursorPosition = cursor.GetPosition();
|
|
}
|
|
}
|
|
|
|
// As an optimization, collect characters in buffer and print out all at once.
|
|
XPosition = cursor.GetPosition().X;
|
|
size_t i = 0;
|
|
wchar_t* LocalBufPtr = LocalBuffer;
|
|
while (*pcb < BufferSize && i < LOCAL_BUFFER_SIZE && XPosition < coordScreenBufferSize.X)
|
|
{
|
|
#pragma prefast(suppress : 26019, "Buffer is taken in multiples of 2. Validation is ok.")
|
|
const wchar_t Char = *lpString;
|
|
const wchar_t RealUnicodeChar = *pwchRealUnicode;
|
|
if (IS_GLYPH_CHAR(RealUnicodeChar) || fUnprocessed)
|
|
{
|
|
if (IsGlyphFullWidth(Char))
|
|
{
|
|
if (i < (LOCAL_BUFFER_SIZE - 1) && XPosition < (coordScreenBufferSize.X - 1))
|
|
{
|
|
*LocalBufPtr++ = Char;
|
|
|
|
// cursor adjusted by 2 because the char is double width
|
|
XPosition += 2;
|
|
i += 1;
|
|
pwchBuffer++;
|
|
}
|
|
else
|
|
{
|
|
goto EndWhile;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*LocalBufPtr = Char;
|
|
LocalBufPtr++;
|
|
XPosition++;
|
|
i++;
|
|
pwchBuffer++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
|
|
switch (RealUnicodeChar)
|
|
{
|
|
case UNICODE_BELL:
|
|
if (dwFlags & WC_ECHO)
|
|
{
|
|
goto CtrlChar;
|
|
}
|
|
else
|
|
{
|
|
screenInfo.SendNotifyBeep();
|
|
}
|
|
break;
|
|
case UNICODE_BACKSPACE:
|
|
|
|
// automatically go to EndWhile. this is because
|
|
// backspace is not destructive, so "aBkSp" prints
|
|
// a with the cursor on the "a". we could achieve
|
|
// this behavior staying in this loop and figuring out
|
|
// the string that needs to be printed, but it would
|
|
// be expensive and it's the exceptional case.
|
|
|
|
goto EndWhile;
|
|
break;
|
|
case UNICODE_TAB:
|
|
{
|
|
const ULONG TabSize = NUMBER_OF_SPACES_IN_TAB(XPosition);
|
|
XPosition = (SHORT)(XPosition + TabSize);
|
|
if (XPosition >= coordScreenBufferSize.X)
|
|
{
|
|
goto EndWhile;
|
|
}
|
|
|
|
for (ULONG j = 0; j < TabSize && i < LOCAL_BUFFER_SIZE; j++, i++)
|
|
{
|
|
*LocalBufPtr = UNICODE_SPACE;
|
|
LocalBufPtr++;
|
|
}
|
|
|
|
pwchBuffer++;
|
|
break;
|
|
}
|
|
case UNICODE_LINEFEED:
|
|
case UNICODE_CARRIAGERETURN:
|
|
goto EndWhile;
|
|
default:
|
|
|
|
// if char is ctrl char, write ^char.
|
|
if ((dwFlags & WC_ECHO) && (IS_CONTROL_CHAR(RealUnicodeChar)))
|
|
{
|
|
CtrlChar:
|
|
if (i < (LOCAL_BUFFER_SIZE - 1))
|
|
{
|
|
*LocalBufPtr = (WCHAR)'^';
|
|
LocalBufPtr++;
|
|
XPosition++;
|
|
i++;
|
|
|
|
*LocalBufPtr = (WCHAR)(RealUnicodeChar + (WCHAR)'@');
|
|
LocalBufPtr++;
|
|
XPosition++;
|
|
i++;
|
|
|
|
pwchBuffer++;
|
|
}
|
|
else
|
|
{
|
|
goto EndWhile;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Char == UNICODE_NULL)
|
|
{
|
|
*LocalBufPtr = UNICODE_SPACE;
|
|
}
|
|
else
|
|
{
|
|
// As a special favor to incompetent apps that attempt to display control chars,
|
|
// convert to corresponding OEM Glyph Chars
|
|
WORD CharType;
|
|
|
|
GetStringTypeW(CT_CTYPE1, &RealUnicodeChar, 1, &CharType);
|
|
if (WI_IsFlagSet(CharType, C1_CNTRL))
|
|
{
|
|
ConvertOutputToUnicode(gci.OutputCP,
|
|
(LPSTR)&RealUnicodeChar,
|
|
1,
|
|
LocalBufPtr,
|
|
1);
|
|
}
|
|
else
|
|
{
|
|
*LocalBufPtr = Char;
|
|
}
|
|
}
|
|
|
|
LocalBufPtr++;
|
|
XPosition++;
|
|
i++;
|
|
pwchBuffer++;
|
|
}
|
|
}
|
|
}
|
|
lpString++;
|
|
pwchRealUnicode++;
|
|
*pcb += sizeof(WCHAR);
|
|
}
|
|
EndWhile:
|
|
if (i != 0)
|
|
{
|
|
CursorPosition = cursor.GetPosition();
|
|
|
|
// Make sure we don't write past the end of the buffer.
|
|
if (i > gsl::narrow_cast<size_t>(coordScreenBufferSize.X) - CursorPosition.X)
|
|
{
|
|
i = gsl::narrow_cast<size_t>(coordScreenBufferSize.X) - CursorPosition.X;
|
|
}
|
|
|
|
// line was wrapped if we're writing up to the end of the current row
|
|
OutputCellIterator it(std::wstring_view(LocalBuffer, i), Attributes);
|
|
const auto itEnd = screenInfo.Write(it);
|
|
|
|
// Notify accessibility
|
|
screenInfo.NotifyAccessibilityEventing(CursorPosition.X, CursorPosition.Y, CursorPosition.X + gsl::narrow<SHORT>(i - 1), CursorPosition.Y);
|
|
|
|
// The number of "spaces" or "cells" we have consumed needs to be reported and stored for later
|
|
// when/if we need to erase the command line.
|
|
TempNumSpaces += itEnd.GetCellDistance(it);
|
|
CursorPosition.X = XPosition;
|
|
|
|
// enforce a delayed newline if we're about to pass the end and the WC_DELAY_EOL_WRAP flag is set.
|
|
if (WI_IsFlagSet(dwFlags, WC_DELAY_EOL_WRAP) && CursorPosition.X >= coordScreenBufferSize.X && fWrapAtEOL)
|
|
{
|
|
// Our cursor position as of this time is going to remain on the last position in this column.
|
|
CursorPosition.X = coordScreenBufferSize.X - 1;
|
|
|
|
// Update in the structures that we're still pointing to the last character in the row
|
|
cursor.SetPosition(CursorPosition);
|
|
|
|
// Record for the delay comparison that we're delaying on the last character in the row
|
|
cursor.DelayEOLWrap(CursorPosition);
|
|
}
|
|
else
|
|
{
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
|
|
}
|
|
|
|
if (*pcb == BufferSize)
|
|
{
|
|
if (nullptr != pcSpaces)
|
|
{
|
|
*pcSpaces = TempNumSpaces;
|
|
}
|
|
return STATUS_SUCCESS;
|
|
}
|
|
continue;
|
|
}
|
|
else if (*pcb >= BufferSize)
|
|
{
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
|
|
|
|
// this catches the case where the number of backspaces == the number of characters.
|
|
if (nullptr != pcSpaces)
|
|
{
|
|
*pcSpaces = TempNumSpaces;
|
|
}
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
|
|
switch (*lpString)
|
|
{
|
|
case UNICODE_BACKSPACE:
|
|
{
|
|
// move cursor backwards one space. overwrite current char with blank.
|
|
// we get here because we have to backspace from the beginning of the line
|
|
TempNumSpaces -= 1;
|
|
if (pwchBuffer == pwchBufferBackupLimit)
|
|
{
|
|
CursorPosition.X -= 1;
|
|
}
|
|
else
|
|
{
|
|
const wchar_t* Tmp;
|
|
wchar_t* Tmp2 = nullptr;
|
|
WCHAR LastChar;
|
|
|
|
const size_t bufferSize = pwchBuffer - pwchBufferBackupLimit;
|
|
std::unique_ptr<wchar_t[]> buffer;
|
|
try
|
|
{
|
|
buffer = std::make_unique<wchar_t[]>(bufferSize);
|
|
std::fill_n(buffer.get(), bufferSize, UNICODE_NULL);
|
|
}
|
|
catch (...)
|
|
{
|
|
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
for (i = 0, Tmp2 = buffer.get(), Tmp = pwchBufferBackupLimit;
|
|
i < bufferSize;
|
|
i++, Tmp++)
|
|
{
|
|
// see 18120085, these two need to be separate if statements
|
|
if (*Tmp == UNICODE_BACKSPACE)
|
|
{
|
|
//it is important we do nothing in the else case for
|
|
// this one instead of falling through to the below else.
|
|
if (Tmp2 > buffer.get())
|
|
{
|
|
Tmp2--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FAIL_FAST_IF(!(Tmp2 >= buffer.get()));
|
|
*Tmp2++ = *Tmp;
|
|
}
|
|
}
|
|
if (Tmp2 == buffer.get())
|
|
{
|
|
LastChar = UNICODE_SPACE;
|
|
}
|
|
else
|
|
{
|
|
#pragma prefast(suppress : 26001, "This is fine. Tmp2 has to have advanced or it would equal pBuffer.")
|
|
LastChar = *(Tmp2 - 1);
|
|
}
|
|
|
|
if (LastChar == UNICODE_TAB)
|
|
{
|
|
CursorPosition.X -= (SHORT)(RetrieveNumberOfSpaces(sOriginalXPosition,
|
|
pwchBufferBackupLimit,
|
|
(ULONG)(pwchBuffer - pwchBufferBackupLimit - 1)));
|
|
if (CursorPosition.X < 0)
|
|
{
|
|
CursorPosition.X = (coordScreenBufferSize.X - 1) / TAB_SIZE;
|
|
CursorPosition.X *= TAB_SIZE;
|
|
CursorPosition.X += 1;
|
|
CursorPosition.Y -= 1;
|
|
|
|
// since you just backspaced yourself back up into the previous row, unset the wrap
|
|
// flag on the prev row if it was set
|
|
textBuffer.GetRowByOffset(CursorPosition.Y).GetCharRow().SetWrapForced(false);
|
|
}
|
|
}
|
|
else if (IS_CONTROL_CHAR(LastChar))
|
|
{
|
|
CursorPosition.X -= 1;
|
|
TempNumSpaces -= 1;
|
|
|
|
// overwrite second character of ^x sequence.
|
|
if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
|
|
{
|
|
try
|
|
{
|
|
screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
|
|
Status = STATUS_SUCCESS;
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
CursorPosition.X -= 1;
|
|
}
|
|
else if (IsGlyphFullWidth(LastChar))
|
|
{
|
|
CursorPosition.X -= 1;
|
|
TempNumSpaces -= 1;
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
|
|
if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
|
|
{
|
|
try
|
|
{
|
|
screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
|
|
Status = STATUS_SUCCESS;
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
CursorPosition.X -= 1;
|
|
}
|
|
else
|
|
{
|
|
CursorPosition.X--;
|
|
}
|
|
}
|
|
if ((dwFlags & WC_LIMIT_BACKSPACE) && (CursorPosition.X < 0))
|
|
{
|
|
CursorPosition.X = 0;
|
|
OutputDebugStringA(("CONSRV: Ignoring backspace to previous line\n"));
|
|
}
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
|
|
if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
|
|
{
|
|
try
|
|
{
|
|
screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), cursor.GetPosition());
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
if (cursor.GetPosition().X == 0 && fWrapAtEOL && pwchBuffer > pwchBufferBackupLimit)
|
|
{
|
|
if (CheckBisectProcessW(screenInfo,
|
|
pwchBufferBackupLimit,
|
|
pwchBuffer + 1 - pwchBufferBackupLimit,
|
|
gsl::narrow_cast<size_t>(coordScreenBufferSize.X) - sOriginalXPosition,
|
|
sOriginalXPosition,
|
|
dwFlags & WC_ECHO))
|
|
{
|
|
CursorPosition.X = coordScreenBufferSize.X - 1;
|
|
CursorPosition.Y = (SHORT)(cursor.GetPosition().Y - 1);
|
|
|
|
// since you just backspaced yourself back up into the previous row, unset the wrap flag
|
|
// on the prev row if it was set
|
|
textBuffer.GetRowByOffset(CursorPosition.Y).GetCharRow().SetWrapForced(false);
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UNICODE_TAB:
|
|
{
|
|
const size_t TabSize = gsl::narrow_cast<size_t>(NUMBER_OF_SPACES_IN_TAB(cursor.GetPosition().X));
|
|
CursorPosition.X = (SHORT)(cursor.GetPosition().X + TabSize);
|
|
|
|
// move cursor forward to next tab stop. fill space with blanks.
|
|
// we get here when the tab extends beyond the right edge of the
|
|
// window. if the tab goes wraps the line, set the cursor to the first
|
|
// position in the next line.
|
|
pwchBuffer++;
|
|
|
|
TempNumSpaces += TabSize;
|
|
size_t NumChars = 0;
|
|
if (CursorPosition.X >= coordScreenBufferSize.X)
|
|
{
|
|
NumChars = gsl::narrow<size_t>(coordScreenBufferSize.X - cursor.GetPosition().X);
|
|
CursorPosition.X = 0;
|
|
CursorPosition.Y = cursor.GetPosition().Y + 1;
|
|
|
|
// since you just tabbed yourself past the end of the row, set the wrap
|
|
textBuffer.GetRowByOffset(cursor.GetPosition().Y).GetCharRow().SetWrapForced(true);
|
|
}
|
|
else
|
|
{
|
|
NumChars = gsl::narrow<size_t>(CursorPosition.X - cursor.GetPosition().X);
|
|
CursorPosition.Y = cursor.GetPosition().Y;
|
|
}
|
|
|
|
try
|
|
{
|
|
const OutputCellIterator it(UNICODE_SPACE, Attributes, NumChars);
|
|
const auto done = screenInfo.Write(it, cursor.GetPosition());
|
|
NumChars = done.GetCellDistance(it);
|
|
}
|
|
CATCH_LOG();
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
|
|
break;
|
|
}
|
|
case UNICODE_CARRIAGERETURN:
|
|
{
|
|
// Carriage return moves the cursor to the beginning of the line.
|
|
// We don't need to worry about handling cr or lf for
|
|
// backspace because input is sent to the user on cr or lf.
|
|
pwchBuffer++;
|
|
CursorPosition.X = 0;
|
|
CursorPosition.Y = cursor.GetPosition().Y;
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
|
|
break;
|
|
}
|
|
case UNICODE_LINEFEED:
|
|
{
|
|
// move cursor to the next line.
|
|
pwchBuffer++;
|
|
|
|
if (gci.IsReturnOnNewlineAutomatic())
|
|
{
|
|
// Traditionally, we reset the X position to 0 with a newline automatically.
|
|
// Some things might not want this automatic "ONLCR line discipline" (for example, things that are expecting a *NIX behavior.)
|
|
// They will turn it off with an output mode flag.
|
|
CursorPosition.X = 0;
|
|
}
|
|
|
|
CursorPosition.Y = (SHORT)(cursor.GetPosition().Y + 1);
|
|
|
|
{
|
|
// since we explicitly just moved down a row, clear the wrap status on the row we just came from
|
|
textBuffer.GetRowByOffset(cursor.GetPosition().Y).GetCharRow().SetWrapForced(false);
|
|
}
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
const wchar_t Char = *lpString;
|
|
if (Char >= UNICODE_SPACE &&
|
|
IsGlyphFullWidth(Char) &&
|
|
XPosition >= (coordScreenBufferSize.X - 1) &&
|
|
fWrapAtEOL)
|
|
{
|
|
const COORD TargetPoint = cursor.GetPosition();
|
|
ROW& Row = textBuffer.GetRowByOffset(TargetPoint.Y);
|
|
CharRow& charRow = Row.GetCharRow();
|
|
|
|
try
|
|
{
|
|
// If we're on top of a trailing cell, clear it and the previous cell.
|
|
if (charRow.DbcsAttrAt(TargetPoint.X).IsTrailing())
|
|
{
|
|
// Space to clear for 2 cells.
|
|
OutputCellIterator it(UNICODE_SPACE, 2);
|
|
|
|
// Back target point up one.
|
|
auto writeTarget = TargetPoint;
|
|
writeTarget.X--;
|
|
|
|
// Write 2 clear cells.
|
|
screenInfo.Write(it, writeTarget);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
CursorPosition.X = 0;
|
|
CursorPosition.Y = (SHORT)(TargetPoint.Y + 1);
|
|
|
|
// since you just moved yourself down onto the next row with 1 character, that sounds like a
|
|
// forced wrap so set the flag
|
|
charRow.SetWrapForced(true);
|
|
|
|
// Additionally, this padding is only called for IsConsoleFullWidth (a.k.a. when a character
|
|
// is too wide to fit on the current line).
|
|
charRow.SetDoubleBytePadded(true);
|
|
|
|
Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
return Status;
|
|
}
|
|
|
|
*pcb += sizeof(WCHAR);
|
|
lpString++;
|
|
pwchRealUnicode++;
|
|
}
|
|
|
|
if (nullptr != pcSpaces)
|
|
{
|
|
*pcSpaces = TempNumSpaces;
|
|
}
|
|
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This routine writes a string to the screen, processing any embedded
|
|
// unicode characters. The string is also copied to the input buffer, if
|
|
// the output mode is line mode.
|
|
// Arguments:
|
|
// - screenInfo - reference to screen buffer information structure.
|
|
// - pwchBufferBackupLimit - Pointer to beginning of buffer.
|
|
// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode.
|
|
// This pointer is updated to point to the next position in the buffer.
|
|
// - pwchRealUnicode - Pointer to string to write.
|
|
// - pcb - On input, number of bytes to write. On output, number of bytes written.
|
|
// - pcSpaces - On output, the number of spaces consumed by the written characters.
|
|
// - dwFlags -
|
|
// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters.
|
|
// WC_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge
|
|
// WC_ECHO if called by Read (echoing characters)
|
|
// Return Value:
|
|
// Note:
|
|
// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services.
|
|
[[nodiscard]] NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo,
|
|
_In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit,
|
|
_In_ const wchar_t* pwchBuffer,
|
|
_In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode,
|
|
_Inout_ size_t* const pcb,
|
|
_Out_opt_ size_t* const pcSpaces,
|
|
const SHORT sOriginalXPosition,
|
|
const DWORD dwFlags,
|
|
_Inout_opt_ PSHORT const psScrollY)
|
|
{
|
|
if (!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) ||
|
|
!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
|
|
{
|
|
return WriteCharsLegacy(screenInfo,
|
|
pwchBufferBackupLimit,
|
|
pwchBuffer,
|
|
pwchRealUnicode,
|
|
pcb,
|
|
pcSpaces,
|
|
sOriginalXPosition,
|
|
dwFlags,
|
|
psScrollY);
|
|
}
|
|
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
|
|
size_t const BufferSize = *pcb;
|
|
*pcb = 0;
|
|
|
|
{
|
|
size_t TempNumSpaces = 0;
|
|
|
|
{
|
|
if (NT_SUCCESS(Status))
|
|
{
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)));
|
|
|
|
// defined down in the WriteBuffer default case hiding on the other end of the state machine. See outputStream.cpp
|
|
// This is the only mode used by DoWriteConsole.
|
|
FAIL_FAST_IF(!(WI_IsFlagSet(dwFlags, WC_LIMIT_BACKSPACE)));
|
|
|
|
StateMachine& machine = screenInfo.GetStateMachine();
|
|
size_t const cch = BufferSize / sizeof(WCHAR);
|
|
|
|
machine.ProcessString({ pwchRealUnicode, cch });
|
|
*pcb += BufferSize;
|
|
}
|
|
}
|
|
|
|
if (nullptr != pcSpaces)
|
|
{
|
|
*pcSpaces = TempNumSpaces;
|
|
}
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Takes the given text and inserts it into the given screen buffer.
|
|
// Note:
|
|
// - Console lock must be held when calling this routine
|
|
// - String has been translated to unicode at this point.
|
|
// Arguments:
|
|
// - pwchBuffer - wide character text to be inserted into buffer
|
|
// - pcbBuffer - byte count of pwchBuffer on the way in, number of bytes consumed on the way out.
|
|
// - screenInfo - Screen Information class to write the text into at the current cursor position
|
|
// - ppWaiter - If writing to the console is blocked for whatever reason, this will be filled with a pointer to context
|
|
// that can be used by the server to resume the call at a later time.
|
|
// Return Value:
|
|
// - STATUS_SUCCESS if OK.
|
|
// - CONSOLE_STATUS_WAIT if we couldn't finish now and need to be called back later (see ppWaiter).
|
|
// - Or a suitable NTSTATUS format error code for memory/string/math failures.
|
|
[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer,
|
|
_Inout_ size_t* const pcbBuffer,
|
|
SCREEN_INFORMATION& screenInfo,
|
|
std::unique_ptr<WriteData>& waiter)
|
|
{
|
|
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
if (WI_IsAnyFlagSet(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING)))
|
|
{
|
|
try
|
|
{
|
|
waiter = std::make_unique<WriteData>(screenInfo,
|
|
pwchBuffer,
|
|
*pcbBuffer,
|
|
gci.OutputCP);
|
|
}
|
|
catch (...)
|
|
{
|
|
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
return CONSOLE_STATUS_WAIT;
|
|
}
|
|
|
|
const auto& textBuffer = screenInfo.GetTextBuffer();
|
|
return WriteChars(screenInfo,
|
|
pwchBuffer,
|
|
pwchBuffer,
|
|
pwchBuffer,
|
|
pcbBuffer,
|
|
nullptr,
|
|
textBuffer.GetCursor().GetPosition().X,
|
|
WC_LIMIT_BACKSPACE,
|
|
nullptr);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This method performs the actual work of attempting to write to the console, converting data types as necessary
|
|
// to adapt from the server types to the legacy internal host types.
|
|
// - It operates on Unicode data only. It's assumed the text is translated by this point.
|
|
// Arguments:
|
|
// - OutContext - the console output object to write the new text into
|
|
// - pwsTextBuffer - wide character text buffer provided by client application to insert
|
|
// - cchTextBufferLength - text buffer counted in characters
|
|
// - pcchTextBufferRead - character count of the number of characters we were able to insert before returning
|
|
// - ppWaiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later
|
|
// Return Value:
|
|
// - S_OK if successful.
|
|
// - S_OK if we need to wait (check if ppWaiter is not nullptr).
|
|
// - Or a suitable HRESULT code for math/string/memory failures.
|
|
[[nodiscard]] HRESULT WriteConsoleWImplHelper(IConsoleOutputObject& context,
|
|
const std::wstring_view buffer,
|
|
size_t& read,
|
|
std::unique_ptr<WriteData>& waiter) noexcept
|
|
{
|
|
try
|
|
{
|
|
// Set out variables in case we exit early.
|
|
read = 0;
|
|
waiter.reset();
|
|
|
|
// Convert characters to bytes to give to DoWriteConsole.
|
|
size_t cbTextBufferLength;
|
|
RETURN_IF_FAILED(SizeTMult(buffer.size(), sizeof(wchar_t), &cbTextBufferLength));
|
|
|
|
NTSTATUS Status = DoWriteConsole(const_cast<wchar_t*>(buffer.data()), &cbTextBufferLength, context, waiter);
|
|
|
|
// Convert back from bytes to characters for the resulting string length written.
|
|
read = cbTextBufferLength / sizeof(wchar_t);
|
|
|
|
if (Status == CONSOLE_STATUS_WAIT)
|
|
{
|
|
FAIL_FAST_IF_NULL(waiter.get());
|
|
Status = STATUS_SUCCESS;
|
|
}
|
|
|
|
RETURN_NTSTATUS(Status);
|
|
}
|
|
CATCH_RETURN();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes non-Unicode formatted data into the given console output object.
|
|
// - This method will convert from the given input into wide characters before chain calling the wide character version of the function.
|
|
// It uses the current Output Codepage for conversions (set via SetConsoleOutputCP).
|
|
// - NOTE: This may be blocked for various console states and will return a wait context pointer if necessary.
|
|
// Arguments:
|
|
// - context - the console output object to write the new text into
|
|
// - buffer - char/byte text buffer provided by client application to insert
|
|
// - read - character count of the number of characters (also bytes because A version) we were able to insert before returning
|
|
// - waiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later
|
|
// Return Value:
|
|
// - S_OK if successful.
|
|
// - S_OK if we need to wait (check if ppWaiter is not nullptr).
|
|
// - Or a suitable HRESULT code for math/string/memory failures.
|
|
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleAImpl(IConsoleOutputObject& context,
|
|
const std::string_view buffer,
|
|
size_t& read,
|
|
std::unique_ptr<IWaitRoutine>& waiter) noexcept
|
|
{
|
|
try
|
|
{
|
|
// Ensure output variables are initialized.
|
|
read = 0;
|
|
waiter.reset();
|
|
|
|
if (buffer.empty())
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
LockConsole();
|
|
auto unlock{ wil::scope_exit([&] { UnlockConsole(); }) };
|
|
|
|
auto& screenInfo{ context.GetActiveBuffer() };
|
|
const auto& consoleInfo{ ServiceLocator::LocateGlobals().getConsoleInformation() };
|
|
const auto codepage{ consoleInfo.OutputCP };
|
|
auto leadByteCaptured{ false };
|
|
auto leadByteConsumed{ false };
|
|
std::wstring wstr{};
|
|
static til::u8state u8State{};
|
|
|
|
// Convert our input parameters to Unicode
|
|
if (codepage == CP_UTF8)
|
|
{
|
|
RETURN_IF_FAILED(til::u8u16(buffer, wstr, u8State));
|
|
read = buffer.size();
|
|
}
|
|
else
|
|
{
|
|
// In case the codepage changes from UTF-8 to another,
|
|
// we discard partials that might still be cached.
|
|
u8State.reset();
|
|
|
|
int mbPtrLength{};
|
|
RETURN_IF_FAILED(SizeTToInt(buffer.size(), &mbPtrLength));
|
|
|
|
// (buffer.size() + 2) I think because we might be shoving another unicode char
|
|
// from screenInfo->WriteConsoleDbcsLeadByte in front
|
|
// because we previously checked that buffer.size() fits into an int, +2 won't cause an overflow of size_t
|
|
wstr.resize(buffer.size() + 2);
|
|
|
|
wchar_t* wcPtr{ wstr.data() };
|
|
auto mbPtr{ buffer.data() };
|
|
size_t dbcsLength{};
|
|
if (screenInfo.WriteConsoleDbcsLeadByte[0] != 0 && gsl::narrow_cast<byte>(*mbPtr) >= byte{ ' ' })
|
|
{
|
|
// there was a portion of a dbcs character stored from a previous
|
|
// call so we take the 2nd half from mbPtr[0], put them together
|
|
// and write the wide char to wcPtr[0]
|
|
screenInfo.WriteConsoleDbcsLeadByte[1] = gsl::narrow_cast<byte>(*mbPtr);
|
|
|
|
try
|
|
{
|
|
const auto wFromComplemented{
|
|
ConvertToW(codepage, { reinterpret_cast<const char*>(screenInfo.WriteConsoleDbcsLeadByte), ARRAYSIZE(screenInfo.WriteConsoleDbcsLeadByte) })
|
|
};
|
|
|
|
FAIL_FAST_IF(wFromComplemented.size() != 1);
|
|
dbcsLength = sizeof(wchar_t);
|
|
wcPtr[0] = wFromComplemented.at(0);
|
|
mbPtr++;
|
|
}
|
|
catch (...)
|
|
{
|
|
dbcsLength = 0;
|
|
}
|
|
|
|
// this looks weird to be always incrementing even if the conversion failed, but this is the
|
|
// original behavior so it's left unchanged.
|
|
wcPtr++;
|
|
mbPtrLength--;
|
|
|
|
// Note that we used a stored lead byte from a previous call in order to complete this write
|
|
// Use this to offset the "number of bytes consumed" calculation at the end by -1 to account
|
|
// for using a byte we had internally, not off the stream.
|
|
leadByteConsumed = true;
|
|
}
|
|
|
|
screenInfo.WriteConsoleDbcsLeadByte[0] = 0;
|
|
|
|
// if the last byte in mbPtr is a lead byte for the current code page,
|
|
// save it for the next time this function is called and we can piece it
|
|
// back together then
|
|
if (mbPtrLength != 0 && CheckBisectStringA(const_cast<char*>(mbPtr), mbPtrLength, &consoleInfo.OutputCPInfo))
|
|
{
|
|
screenInfo.WriteConsoleDbcsLeadByte[0] = gsl::narrow_cast<byte>(mbPtr[mbPtrLength - 1]);
|
|
mbPtrLength--;
|
|
|
|
// Note that we captured a lead byte during this call, but won't actually draw it until later.
|
|
// Use this to offset the "number of bytes consumed" calculation at the end by +1 to account
|
|
// for taking a byte off the stream.
|
|
leadByteCaptured = true;
|
|
}
|
|
|
|
if (mbPtrLength != 0)
|
|
{
|
|
// convert the remaining bytes in mbPtr to wide chars
|
|
mbPtrLength = sizeof(wchar_t) * MultiByteToWideChar(codepage, 0, mbPtr, mbPtrLength, wcPtr, mbPtrLength);
|
|
}
|
|
|
|
wstr.resize((dbcsLength + mbPtrLength) / sizeof(wchar_t));
|
|
}
|
|
|
|
// Hold the specific version of the waiter locally so we can tinker with it if we must to store additional context.
|
|
std::unique_ptr<WriteData> writeDataWaiter{};
|
|
|
|
// Make the W version of the call
|
|
size_t wcBufferWritten{};
|
|
const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, writeDataWaiter) };
|
|
|
|
// If there is no waiter, process the byte count now.
|
|
if (nullptr == writeDataWaiter.get())
|
|
{
|
|
// Calculate how many bytes of the original A buffer were consumed in the W version of the call to satisfy mbBufferRead.
|
|
// For UTF-8 conversions, we've already returned this information above.
|
|
if (CP_UTF8 != codepage)
|
|
{
|
|
size_t mbBufferRead{};
|
|
|
|
// Start by counting the number of A bytes we used in printing our W string to the screen.
|
|
try
|
|
{
|
|
mbBufferRead = GetALengthFromW(codepage, { wstr.data(), wcBufferWritten });
|
|
}
|
|
CATCH_LOG();
|
|
|
|
// If we captured a byte off the string this time around up above, it means we didn't feed
|
|
// it into the WriteConsoleW above, and therefore its consumption isn't accounted for
|
|
// in the count we just made. Add +1 to compensate.
|
|
if (leadByteCaptured)
|
|
{
|
|
mbBufferRead++;
|
|
}
|
|
|
|
// If we consumed an internally-stored lead byte this time around up above, it means that we
|
|
// fed a byte into WriteConsoleW that wasn't a part of this particular call's request.
|
|
// We need to -1 to compensate and tell the caller the right number of bytes consumed this request.
|
|
if (leadByteConsumed)
|
|
{
|
|
mbBufferRead--;
|
|
}
|
|
|
|
read = mbBufferRead;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If there is a waiter, then we need to stow some additional information in the wait structure so
|
|
// we can synthesize the correct byte count later when the wait routine is triggered.
|
|
if (CP_UTF8 != codepage)
|
|
{
|
|
// For non-UTF8 codepages, save the lead byte captured/consumed data so we can +1 or -1 the final decoded count
|
|
// in the WaitData::Notify method later.
|
|
writeDataWaiter->SetLeadByteAdjustmentStatus(leadByteCaptured, leadByteConsumed);
|
|
}
|
|
else
|
|
{
|
|
// For UTF8 codepages, just remember the consumption count from the UTF-8 parser.
|
|
writeDataWaiter->SetUtf8ConsumedCharacters(read);
|
|
}
|
|
}
|
|
|
|
// Give back the waiter now that we're done with tinkering with it.
|
|
waiter.reset(writeDataWaiter.release());
|
|
|
|
return hr;
|
|
}
|
|
CATCH_RETURN();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes Unicode formatted data into the given console output object.
|
|
// - NOTE: This may be blocked for various console states and will return a wait context pointer if necessary.
|
|
// Arguments:
|
|
// - OutContext - the console output object to write the new text into
|
|
// - pwsTextBuffer - wide character text buffer provided by client application to insert
|
|
// - cchTextBufferLength - text buffer counted in characters
|
|
// - pcchTextBufferRead - character count of the number of characters we were able to insert before returning
|
|
// - ppWaiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later
|
|
// Return Value:
|
|
// - S_OK if successful.
|
|
// - S_OK if we need to wait (check if ppWaiter is not nullptr).
|
|
// - Or a suitable HRESULT code for math/string/memory failures.
|
|
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleWImpl(IConsoleOutputObject& context,
|
|
const std::wstring_view buffer,
|
|
size_t& read,
|
|
std::unique_ptr<IWaitRoutine>& waiter) noexcept
|
|
{
|
|
try
|
|
{
|
|
LockConsole();
|
|
auto unlock = wil::scope_exit([&] { UnlockConsole(); });
|
|
|
|
std::unique_ptr<WriteData> writeDataWaiter;
|
|
RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, writeDataWaiter));
|
|
|
|
// Transfer specific waiter pointer into the generic interface wrapper.
|
|
waiter.reset(writeDataWaiter.release());
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN();
|
|
}
|