// 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()) { // Inside this block, we're shifting down at the bottom. // This means that we had something like this: // AAAA // BBBB // CCCC // DDDD // EEEE // // Our margins were set for lines A-D, but not on line E. // So we circled the whole buffer up by one: // BBBB // CCCC // DDDD // EEEE // // // Then we scrolled the contents of everything OUTSIDE the margin frame down. // BBBB // CCCC // DDDD // // EEEE // // And now we need to report that only the bottom line didn't "move" as we put the EEEE // back where it started, but everything else moved. // In this case, delta was 1. So the amount that moved is the entire viewport height minus the delta. Viewport invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), viewport.Height() - 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(coordScreenBufferSize.X) - CursorPosition.X) { i = gsl::narrow_cast(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(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 buffer; try { buffer = std::make_unique(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(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(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(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(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, bool requiresVtQuirk, std::unique_ptr& 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(screenInfo, pwchBuffer, *pcbBuffer, gci.OutputCP, requiresVtQuirk); } catch (...) { return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } return CONSOLE_STATUS_WAIT; } auto restoreVtQuirk{ wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); }) }; if (requiresVtQuirk) { screenInfo.SetIgnoreLegacyEquivalentVTAttributes(); } else { restoreVtQuirk.release(); } 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, bool requiresVtQuirk, std::unique_ptr& 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(buffer.data()), &cbTextBufferLength, context, requiresVtQuirk, 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, bool requiresVtQuirk, std::unique_ptr& 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(*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(*mbPtr); try { const auto wFromComplemented{ ConvertToW(codepage, { reinterpret_cast(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(mbPtr), mbPtrLength, &consoleInfo.OutputCPInfo)) { screenInfo.WriteConsoleDbcsLeadByte[0] = gsl::narrow_cast(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 writeDataWaiter{}; // Make the W version of the call size_t wcBufferWritten{}; const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, requiresVtQuirk, 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, bool requiresVtQuirk, std::unique_ptr& waiter) noexcept { try { LockConsole(); auto unlock = wil::scope_exit([&] { UnlockConsole(); }); std::unique_ptr writeDataWaiter; RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, requiresVtQuirk, writeDataWaiter)); // Transfer specific waiter pointer into the generic interface wrapper. waiter.reset(writeDataWaiter.release()); return S_OK; } CATCH_RETURN(); }