Skip to content

Commit 5116cca

Browse files
committed
Fix various Read/WriteConsoleOutput bugs
1 parent de50310 commit 5116cca

File tree

5 files changed

+88
-171
lines changed

5 files changed

+88
-171
lines changed

src/host/directio.cpp

Lines changed: 66 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -463,91 +463,53 @@ CATCH_RETURN();
463463
return result;
464464
}
465465

466-
[[nodiscard]] static HRESULT _ReadConsoleOutputWImplHelper(const SCREEN_INFORMATION& context,
467-
std::span<CHAR_INFO> targetBuffer,
468-
const Microsoft::Console::Types::Viewport& requestRectangle,
469-
Microsoft::Console::Types::Viewport& readRectangle) noexcept
466+
[[nodiscard]] HRESULT ReadConsoleOutputWImplHelper(const SCREEN_INFORMATION& context,
467+
std::span<CHAR_INFO> targetBuffer,
468+
const Viewport& requestRectangle,
469+
Viewport& readRectangle) noexcept
470470
{
471471
try
472472
{
473473
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
474-
const auto& storageBuffer = context.GetActiveBuffer().GetTextBuffer();
475-
const auto storageSize = storageBuffer.GetSize().Dimensions();
476-
477-
const auto targetSize = requestRectangle.Dimensions();
474+
auto& storageBuffer = context.GetActiveBuffer();
475+
const auto storageRectangle = storageBuffer.GetBufferSize();
476+
const auto clippedRectangle = storageRectangle.Clamp(requestRectangle);
478477

479-
// If either dimension of the request is too small, return an empty rectangle as read and exit early.
480-
if (targetSize.width <= 0 || targetSize.height <= 0)
478+
if (!clippedRectangle.IsValid())
481479
{
482480
readRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 });
483481
return S_OK;
484482
}
485483

486-
// The buffer given should be big enough to hold the dimensions of the request.
487-
const auto targetArea = targetSize.area<size_t>();
488-
RETURN_HR_IF(E_INVALIDARG, targetArea < targetBuffer.size());
489-
490-
// Clip the request rectangle to the size of the storage buffer
491-
auto clip = requestRectangle.ToExclusive();
492-
clip.right = std::min(clip.right, storageSize.width);
493-
clip.bottom = std::min(clip.bottom, storageSize.height);
494-
495-
// Find the target point (where to write the user's buffer)
496-
// It will either be 0,0 or offset into the buffer by the inverse of the negative values.
497-
til::point targetPoint;
498-
targetPoint.x = clip.left < 0 ? -clip.left : 0;
499-
targetPoint.y = clip.top < 0 ? -clip.top : 0;
500-
501-
// The clipped rect must be inside the buffer size, so it has a minimum value of 0. (max of itself and 0)
502-
clip.left = std::max(clip.left, 0);
503-
clip.top = std::max(clip.top, 0);
504-
505-
// The final "request rectangle" or the area inside the buffer we want to read, is the clipped dimensions.
506-
const auto clippedRequestRectangle = Viewport::FromExclusive(clip);
507-
508-
// We will start reading the buffer at the point of the top left corner (origin) of the (potentially adjusted) request
509-
const auto sourcePoint = clippedRequestRectangle.Origin();
510-
511-
// Get an iterator to the beginning of the return buffer
512-
// We might have to seek this forward or skip around if we clipped the request.
513-
auto targetIter = targetBuffer.begin();
514-
til::point targetPos;
515-
const auto targetLimit = Viewport::FromDimensions(targetPoint, clippedRequestRectangle.Dimensions());
516-
517-
// Get an iterator to the beginning of the request inside the screen buffer
518-
// This should walk exactly along every cell of the clipped request.
519-
auto sourceIter = storageBuffer.GetCellDataAt(sourcePoint, clippedRequestRectangle);
520-
521-
// Walk through every cell of the target, advancing the buffer.
522-
// Validate that we always still have a valid iterator to the backing store,
523-
// that we always are writing inside the user's buffer (before the end)
524-
// and we're always targeting the user's buffer inside its original bounds.
525-
while (sourceIter && targetIter < targetBuffer.end())
484+
const auto bufferStride = gsl::narrow_cast<size_t>(std::max(0, requestRectangle.Width()));
485+
const auto width = gsl::narrow_cast<size_t>(clippedRectangle.Width());
486+
const auto offsetY = clippedRectangle.Top() - requestRectangle.Top();
487+
const auto offsetX = clippedRectangle.Left() - requestRectangle.Left();
488+
// We always write the intersection between the valid `storageRectangle` and the given `requestRectangle`.
489+
// This means that if the `requestRectangle` is -3 rows above the top of the buffer, we'll start
490+
// reading from `buffer` at row offset 3, because the first 3 are outside the valid range.
491+
// clippedRectangle.Top/Left() cannot be negative due to the previous Clamp() call.
492+
auto totalOffset = offsetY * bufferStride + offsetX;
493+
494+
if (bufferStride <= 0 || targetBuffer.size() < gsl::narrow_cast<size_t>(clippedRectangle.Height() * bufferStride))
526495
{
527-
// If the point we're trying to write is inside the limited buffer write zone...
528-
if (targetLimit.IsInBounds(targetPos))
529-
{
530-
// Copy the data into position...
531-
*targetIter = gci.AsCharInfo(*sourceIter);
532-
// ... and advance the read iterator.
533-
++sourceIter;
534-
}
496+
return E_INVALIDARG;
497+
}
535498

536-
// Always advance the write iterator, we might have skipped it due to clipping.
537-
++targetIter;
499+
for (til::CoordType y = clippedRectangle.Top(); y <= clippedRectangle.BottomInclusive(); y++)
500+
{
501+
auto it = storageBuffer.GetCellDataAt({ clippedRectangle.Left(), y });
538502

539-
// Increment the target
540-
targetPos.x++;
541-
if (targetPos.x >= targetSize.width)
503+
for (size_t i = 0; i < width; i++)
542504
{
543-
targetPos.x = 0;
544-
targetPos.y++;
505+
targetBuffer[totalOffset + i] = gci.AsCharInfo(*it);
506+
++it;
545507
}
546-
}
547508

548-
// Reply with the region we read out of the backing buffer (potentially clipped)
549-
readRectangle = clippedRequestRectangle;
509+
totalOffset += bufferStride;
510+
}
550511

512+
readRectangle = clippedRectangle;
551513
return S_OK;
552514
}
553515
CATCH_RETURN();
@@ -566,7 +528,7 @@ CATCH_RETURN();
566528
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
567529
const auto codepage = gci.OutputCP;
568530

569-
RETURN_IF_FAILED(_ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle));
531+
RETURN_IF_FAILED(ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle));
570532

571533
LOG_IF_FAILED(_ConvertCellsToAInplace(codepage, buffer, readRectangle));
572534

@@ -585,7 +547,7 @@ CATCH_RETURN();
585547

586548
try
587549
{
588-
RETURN_IF_FAILED(_ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle));
550+
RETURN_IF_FAILED(ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle));
589551

590552
if (!context.GetActiveBuffer().GetCurrentFont().IsTrueTypeFont())
591553
{
@@ -599,103 +561,59 @@ CATCH_RETURN();
599561
CATCH_RETURN();
600562
}
601563

602-
[[nodiscard]] static HRESULT _WriteConsoleOutputWImplHelper(SCREEN_INFORMATION& context,
603-
std::span<CHAR_INFO> buffer,
604-
const Viewport& requestRectangle,
605-
Viewport& writtenRectangle) noexcept
564+
[[nodiscard]] HRESULT WriteConsoleOutputWImplHelper(SCREEN_INFORMATION& context,
565+
std::span<const CHAR_INFO> buffer,
566+
til::CoordType bufferStride,
567+
const Viewport& requestRectangle,
568+
Viewport& writtenRectangle) noexcept
606569
{
607570
try
608571
{
572+
if (bufferStride <= 0)
573+
{
574+
return E_INVALIDARG;
575+
}
576+
609577
auto& storageBuffer = context.GetActiveBuffer();
610578
const auto storageRectangle = storageBuffer.GetBufferSize();
611-
const auto storageSize = storageRectangle.Dimensions();
579+
const auto clippedRectangle = storageRectangle.Clamp(requestRectangle);
612580

613-
const auto sourceSize = requestRectangle.Dimensions();
614-
615-
// If either dimension of the request is too small, return an empty rectangle as the read and exit early.
616-
if (sourceSize.width <= 0 || sourceSize.height <= 0)
581+
if (!clippedRectangle.IsValid())
617582
{
618583
writtenRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 });
619584
return S_OK;
620585
}
621586

622-
// If the top and left of the destination we're trying to write it outside the buffer,
623-
// give the original request rectangle back and exit early OK.
624-
if (requestRectangle.Left() >= storageSize.width || requestRectangle.Top() >= storageSize.height)
625-
{
626-
writtenRectangle = requestRectangle;
627-
return S_OK;
628-
}
629-
630-
// Do clipping according to the legacy patterns.
631-
auto writeRegion = requestRectangle.ToInclusive();
632-
til::inclusive_rect sourceRect;
633-
if (writeRegion.right > storageSize.width - 1)
634-
{
635-
writeRegion.right = storageSize.width - 1;
636-
}
637-
sourceRect.right = writeRegion.right - writeRegion.left;
638-
if (writeRegion.bottom > storageSize.height - 1)
639-
{
640-
writeRegion.bottom = storageSize.height - 1;
641-
}
642-
sourceRect.bottom = writeRegion.bottom - writeRegion.top;
643-
644-
if (writeRegion.left < 0)
645-
{
646-
sourceRect.left = -writeRegion.left;
647-
writeRegion.left = 0;
648-
}
649-
else
650-
{
651-
sourceRect.left = 0;
652-
}
587+
const auto width = clippedRectangle.Width();
588+
// We always write the intersection between the valid `storageRectangle` and the given `requestRectangle`.
589+
// This means that if the `requestRectangle` is -3 rows above the top of the buffer, we'll start
590+
// reading from `buffer` at row offset 3, because the first 3 are outside the valid range.
591+
// clippedRectangle.Top/Left() cannot be negative due to the previous Clamp() call.
592+
const auto offsetY = clippedRectangle.Top() - requestRectangle.Top();
593+
const auto offsetX = clippedRectangle.Left() - requestRectangle.Left();
594+
auto totalOffset = offsetY * bufferStride + offsetX;
653595

654-
if (writeRegion.top < 0)
655-
{
656-
sourceRect.top = -writeRegion.top;
657-
writeRegion.top = 0;
658-
}
659-
else
660-
{
661-
sourceRect.top = 0;
662-
}
663-
664-
if (sourceRect.left > sourceRect.right || sourceRect.top > sourceRect.bottom)
596+
if (bufferStride <= 0 || buffer.size() < gsl::narrow_cast<size_t>(clippedRectangle.Height() * bufferStride))
665597
{
666598
return E_INVALIDARG;
667599
}
668600

669-
const auto writeRectangle = Viewport::FromInclusive(writeRegion);
670-
671-
auto target = writeRectangle.Origin();
672-
673-
// For every row in the request, create a view into the clamped portion of just the one line to write.
674-
// This allows us to restrict the width of the call without allocating/copying any memory by just making
675-
// a smaller view over the existing big blob of data from the original call.
676-
for (; target.y < writeRectangle.BottomExclusive(); target.y++)
601+
for (til::CoordType y = clippedRectangle.Top(); y <= clippedRectangle.BottomInclusive(); y++)
677602
{
678-
// We find the offset into the original buffer by the dimensions of the original request rectangle.
679-
const auto rowOffset = (target.y - requestRectangle.Top()) * requestRectangle.Width();
680-
const auto colOffset = target.x - requestRectangle.Left();
681-
const auto totalOffset = rowOffset + colOffset;
682-
683-
// Now we make a subspan starting from that offset for as much of the original request as would fit
684-
const auto subspan = buffer.subspan(totalOffset, writeRectangle.Width());
685-
686-
// Convert to a CHAR_INFO view to fit into the iterator
687-
const auto charInfos = std::span<const CHAR_INFO>(subspan.data(), subspan.size());
603+
const auto charInfos = buffer.subspan(totalOffset, width);
604+
const til::point target{ clippedRectangle.Left(), y };
688605

689606
// Make the iterator and write to the target position.
690-
OutputCellIterator it(charInfos);
691-
storageBuffer.Write(it, target);
607+
storageBuffer.Write(OutputCellIterator(charInfos), target);
608+
609+
totalOffset += bufferStride;
692610
}
693611

694612
// If we've overwritten image content, it needs to be erased.
695-
ImageSlice::EraseBlock(storageBuffer.GetTextBuffer(), writeRectangle.ToExclusive());
613+
ImageSlice::EraseBlock(storageBuffer.GetTextBuffer(), clippedRectangle.ToExclusive());
696614

697615
// Since we've managed to write part of the request, return the clamped part that we actually used.
698-
writtenRectangle = writeRectangle;
616+
writtenRectangle = clippedRectangle;
699617

700618
return S_OK;
701619
}
@@ -712,11 +630,12 @@ CATCH_RETURN();
712630

713631
try
714632
{
715-
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
633+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
634+
716635
const auto codepage = gci.OutputCP;
717636
LOG_IF_FAILED(_ConvertCellsToWInplace(codepage, buffer, requestRectangle));
718637

719-
RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, buffer, requestRectangle, writtenRectangle));
638+
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, buffer, requestRectangle.Width(), requestRectangle, writtenRectangle));
720639

721640
return S_OK;
722641
}
@@ -738,11 +657,11 @@ CATCH_RETURN();
738657
// For compatibility reasons, we must maintain the behavior that munges the data if we are writing while a raster font is enabled.
739658
// This can be removed when raster font support is removed.
740659
auto translated = _ConvertCellsToMungedW(buffer, requestRectangle);
741-
RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, translated, requestRectangle, writtenRectangle));
660+
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, translated, requestRectangle.Width(), requestRectangle, writtenRectangle));
742661
}
743662
else
744663
{
745-
RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, buffer, requestRectangle, writtenRectangle));
664+
RETURN_IF_FAILED(WriteConsoleOutputWImplHelper(context, buffer, requestRectangle.Width(), requestRectangle, writtenRectangle));
746665
}
747666

748667
return S_OK;

src/host/directio.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@ Revision History:
1717
#pragma once
1818

1919
#include "conapi.h"
20-
#include "inputBuffer.hpp"
2120

2221
class SCREEN_INFORMATION;
2322

23+
[[nodiscard]] HRESULT ReadConsoleOutputWImplHelper(const SCREEN_INFORMATION& context,
24+
std::span<CHAR_INFO> targetBuffer,
25+
const Microsoft::Console::Types::Viewport& requestRectangle,
26+
Microsoft::Console::Types::Viewport& readRectangle) noexcept;
27+
28+
[[nodiscard]] HRESULT WriteConsoleOutputWImplHelper(SCREEN_INFORMATION& context,
29+
std::span<const CHAR_INFO> buffer,
30+
til::CoordType bufferStride,
31+
const Microsoft::Console::Types::Viewport& requestRectangle,
32+
Microsoft::Console::Types::Viewport& writtenRectangle) noexcept;
33+
2434
[[nodiscard]] NTSTATUS ConsoleCreateScreenBuffer(std::unique_ptr<ConsoleHandleData>& handle,
2535
_In_ PCONSOLE_API_MSG Message,
2636
_In_ PCD_CREATE_OBJECT_INFORMATION Information,

src/host/ft_host/API_OutputTests.cpp

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,16 @@ void OutputTests::WriteConsoleOutputWOutsideBuffer()
159159
// Call the API and confirm results.
160160

161161
// move outside in X and Y directions
162-
auto shiftedRegion = region;
163-
shiftedRegion.Left += bufferSize.X;
164-
shiftedRegion.Right += bufferSize.X;
165-
shiftedRegion.Top += bufferSize.Y;
166-
shiftedRegion.Bottom += bufferSize.Y;
167-
168-
auto affected = shiftedRegion;
162+
auto affected = region;
163+
affected.Left += bufferSize.X;
164+
affected.Right += bufferSize.X;
165+
affected.Top += bufferSize.Y;
166+
affected.Bottom += bufferSize.Y;
167+
auto expected = affected;
168+
expected.Right = expected.Left - 1;
169+
expected.Bottom = expected.Top - 1;
169170
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected));
170-
VERIFY_ARE_EQUAL(shiftedRegion, affected);
171+
VERIFY_ARE_EQUAL(expected, affected);
171172

172173
// Read the entire buffer back and validate that we didn't write anything anywhere
173174
const auto readBack = std::make_unique<CHAR_INFO[]>(sbiex.dwSize.X * sbiex.dwSize.Y);
@@ -363,12 +364,6 @@ void OutputTests::WriteConsoleOutputWNegativePositions()
363364
VERIFY_ARE_EQUAL(expectedItem, readItem);
364365
}
365366
}
366-
367-
// Set the region so the left will end up past the right
368-
adjustedRegion = region;
369-
adjustedRegion.Left = -(adjustedRegion.Right + 1);
370-
affected = adjustedRegion;
371-
VERIFY_WIN32_BOOL_FAILED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected));
372367
}
373368

374369
void OutputTests::WriteConsoleOutputCharacterWRunoff()

src/types/viewport.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,7 @@ void Viewport::Clamp(til::point& pos) const
198198
// - Clamped viewport
199199
Viewport Viewport::Clamp(const Viewport& other) const noexcept
200200
{
201-
auto clampMe = other.ToInclusive();
202-
203-
clampMe.left = std::clamp(clampMe.left, Left(), RightInclusive());
204-
clampMe.right = std::clamp(clampMe.right, Left(), RightInclusive());
205-
clampMe.top = std::clamp(clampMe.top, Top(), BottomInclusive());
206-
clampMe.bottom = std::clamp(clampMe.bottom, Top(), BottomInclusive());
207-
208-
return Viewport::FromInclusive(clampMe);
201+
return Viewport::FromExclusive(ToExclusive() & other.ToExclusive());
209202
}
210203

211204
// Method Description:

tools/OpenConsole.psm1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ function Invoke-OpenConsoleTests()
197197
{
198198
$OpenConsolePlatform = 'Win32'
199199
}
200-
$OpenConsolePath = "$env:OpenConsoleroot\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe"
200+
$OpenConsolePath = "$root\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe"
201201
$TaefExePath = "$root\packages\Microsoft.Taef.10.60.210621002\build\Binaries\$Platform\te.exe"
202202
$BinDir = "$root\bin\$OpenConsolePlatform\$Configuration"
203203

0 commit comments

Comments
 (0)