blob: 0f1686eb5a6036ed726b654de8568da18ceec5d0 [file] [log] [blame] [edit]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmStdIoConsole.h"
#ifdef _WIN32
# include <cstddef>
# include <cstdlib>
# include <ios>
# include <streambuf>
# include <utility>
# include <vector>
# include <cm/memory>
# include <windows.h>
# include <fcntl.h> // for _O_BINARY
# include <io.h> // for _setmode
# include "cm_utf8.h"
# include "cmStdIoStream.h"
#endif
namespace cm {
namespace StdIo {
namespace {
#ifdef _WIN32
// Base class for a streambuf that reads or writes a Windows Console.
class ConsoleBuf : public std::streambuf
{
public:
ConsoleBuf(HANDLE console)
: console_(console)
{
}
~ConsoleBuf() throw() override {}
protected:
HANDLE console_ = nullptr;
};
// A streambuf that reads from a Windows Console using wide-character
// encoding to avoid conversion through the console output code page.
class ConsoleBufRead : public ConsoleBuf
{
public:
ConsoleBufRead(HANDLE console, DWORD consoleMode)
: ConsoleBuf(console)
, ConsoleMode_(consoleMode)
{
}
~ConsoleBufRead() throw() override {}
protected:
// Called to read an input character when the input buffer may be empty.
int_type underflow() override
{
// If the input buffer is not empty, return the next input character.
if (this->gptr() < this->egptr()) {
return traits_type::to_int_type(*this->gptr());
}
// The input buffer is empty. Read more input from the console.
static constexpr std::size_t kBufSize = 4096;
this->TmpW_.resize(kBufSize);
DWORD wlen = 0;
if (!ReadConsoleW(this->console_, this->TmpW_.data(),
DWORD(this->TmpW_.size()), &wlen, nullptr)) {
// Failure. Nothing was read.
return traits_type::eof();
}
// Emulate ReadFile behavior when the console is in "cooked mode".
// Treat a leading Ctrl+Z as EOF.
static constexpr char ctrl_z = 26; // Ctrl+Z is Ctrl + 26th letter.
if ((this->ConsoleMode_ & ENABLE_LINE_INPUT) &&
(wlen > 0 && this->TmpW_.front() == ctrl_z)) {
wlen = 0;
}
// Convert the wide-character encoding from the console to our
// internal UTF-8 narrow encoding.
if (int nlen =
WideCharToMultiByte(CP_UTF8, 0, this->TmpW_.data(), int(wlen),
nullptr, 0, nullptr, nullptr)) {
this->Buf_.resize(nlen);
if (WideCharToMultiByte(CP_UTF8, 0, this->TmpW_.data(), int(wlen),
this->Buf_.data(), int(nlen), nullptr,
nullptr)) {
// The converted content is now in the input buffer.
this->setg_();
// Success. Return the next input character.
return traits_type::to_int_type(*this->gptr());
}
}
// Failure. Nothing was read.
return traits_type::eof();
}
private:
DWORD ConsoleMode_ = 0;
std::vector<char> Buf_;
std::vector<wchar_t> TmpW_;
// Set input buffer pointers.
void setg_()
{
this->setg(this->Buf_.data(), this->Buf_.data(),
this->Buf_.data() + this->Buf_.size());
}
};
// A streambuf that writes to a Windows Console using wide-character
// encoding to avoid conversion through the console output code page.
class ConsoleBufWrite : public ConsoleBuf
{
public:
ConsoleBufWrite(HANDLE console)
: ConsoleBuf(console)
{
this->setp_();
}
~ConsoleBufWrite() throw() override { sync(); }
protected:
// Called to sync input and output buffers with the underlying device.
int sync() override
{
// Flush buffered output, if any.
if (this->pptr() != this->pbase()) {
// Use overflow() to flush the entire output buffer.
// It returns eof on failure.
if (traits_type::eq_int_type(this->overflow(), traits_type::eof())) {
return -1;
}
}
return 0;
}
// Called to flush at least some content from the output buffer.
int_type overflow(int_type ch = traits_type::eof()) override
{
std::size_t nlen; // Number of chars to emit.
std::size_t rlen = 0; // Number of chars to roll over.
if (traits_type::eq_int_type(ch, traits_type::eof())) {
// Our caller wants to flush the entire buffer. If there is a
// trailing partial codepoint, it's the caller's fault.
nlen = this->pptr() - this->pbase();
// If the buffer is empty, trivially succeed.
if (nlen == 0) {
return traits_type::not_eof(ch);
}
} else {
// Our caller had no room for this character in the buffer.
// However, setp_() reserved one byte for us to store it.
*this->pptr() = traits_type::to_char_type(ch);
this->pbump(1);
// Flush all complete codepoints, of which we expect at least one.
// If there is a trailing partial codepoint, roll over those chars.
char const* p = this->pptr_();
nlen = p - this->pbase();
rlen = this->pptr() - p;
}
// Fail unless we emit at least one (wide) character.
int_type result = traits_type::eof();
// Convert our internal UTF-8 narrow encoding to wide-character
// encoding to write to the console.
if (int wlen = MultiByteToWideChar(CP_UTF8, 0, this->pbase(), int(nlen),
nullptr, 0)) {
this->TmpW_.resize(wlen);
if (MultiByteToWideChar(CP_UTF8, 0, this->pbase(), int(nlen),
this->TmpW_.data(), int(wlen)) &&
WriteConsoleW(this->console_, this->TmpW_.data(), wlen, nullptr,
nullptr)) {
result = traits_type::not_eof(ch);
}
}
// Remove emitted contents from the buffer.
this->Buf_.erase(this->Buf_.begin(), this->Buf_.begin() + nlen);
// Re-initialize the output buffer.
this->setp_();
// Move the put-pointer past the rollover content.
this->pbump(rlen);
return result;
}
private:
std::vector<char> Buf_;
std::vector<wchar_t> TmpW_;
// Initialize the output buffer and set its put-pointer.
void setp_()
{
// Allocate the output buffer.
static constexpr std::size_t kBufSize = 4096;
this->Buf_.resize(kBufSize);
// Reserve one byte for the overflow() character.
this->setp(this->Buf_.data(), this->Buf_.data() + this->Buf_.size() - 1);
}
// Return pptr() adjusted backward past a partial codepoint.
char const* pptr_() const
{
char const* p = this->pptr();
while (p != this->pbase()) {
--p;
switch (cm_utf8_ones[static_cast<unsigned char>(*p)]) {
case 0: // 0xxx xxxx: starts codepoint of size 1
return p + 1;
case 1: // 10xx xxxx: continues a codepoint
continue;
case 2: // 110x xxxx: starts codepoint of size 2
return ((p + 2) <= this->pptr()) ? (p + 2) : p;
case 3: // 1110 xxxx: starts codepoint of size 3
return ((p + 3) <= this->pptr()) ? (p + 3) : p;
case 4: // 1111 0xxx: starts codepoint of size 4
return ((p + 4) <= this->pptr()) ? (p + 4) : p;
default: // invalid byte
// Roll over the invalid byte.
// The next overflow() will fail to convert it.
return p;
}
}
// No complete codepoint found. This overflow() will fail.
return p;
}
};
#endif
} // anonymous namespace
#ifdef _WIN32
class Console::Impl
{
protected:
class RAII
{
std::ios* IOS_ = nullptr;
int FD_ = -1;
std::unique_ptr<ConsoleBuf> ConsoleBuf_;
std::streambuf* OldStreamBuf_ = nullptr;
int OldMode_ = 0;
RAII(Stream& s);
void Init();
public:
RAII(IStream& is);
RAII(OStream& os);
~RAII();
};
RAII In_;
RAII Out_;
RAII Err_;
public:
Impl();
~Impl();
};
Console::Impl::RAII::RAII(Stream& s)
: IOS_(&s.IOS())
, FD_(s.FD())
{
}
Console::Impl::RAII::RAII(IStream& is)
: RAII(static_cast<Stream&>(is))
{
DWORD mode;
if (is.Console() && GetConsoleMode(is.Console(), &mode) &&
GetConsoleCP() != CP_UTF8) {
// The input stream reads from a console whose input code page is not
// UTF-8. Use a ConsoleBufRead to read wide-character encoding.
this->ConsoleBuf_ = cm::make_unique<ConsoleBufRead>(is.Console(), mode);
}
this->Init();
}
Console::Impl::RAII::RAII(OStream& os)
: RAII(static_cast<Stream&>(os))
{
DWORD mode;
if (os.Console() && GetConsoleMode(os.Console(), &mode) &&
GetConsoleOutputCP() != CP_UTF8) {
// The output stream writes to a console whose output code page is not
// UTF-8. Use a ConsoleBufWrite to write wide-character encoding.
this->ConsoleBuf_ = cm::make_unique<ConsoleBufWrite>(os.Console());
}
this->Init();
}
void Console::Impl::RAII::Init()
{
if (this->ConsoleBuf_) {
this->OldStreamBuf_ = this->IOS_->rdbuf(this->ConsoleBuf_.get());
} else if (this->FD_ >= 0) {
// The stream reads/writes a pipe, a file, or a console whose code
// page is UTF-8. Read/write UTF-8 using the default streambuf,
// but disable newline conversion to match ConsoleBuf behavior.
this->OldMode_ = _setmode(this->FD_, _O_BINARY);
}
}
Console::Impl::RAII::~RAII()
{
if (this->ConsoleBuf_) {
this->IOS_->rdbuf(this->OldStreamBuf_);
this->OldStreamBuf_ = nullptr;
this->ConsoleBuf_.reset();
} else if (this->FD_ >= 0) {
this->IOS_->rdbuf()->pubsync();
_setmode(this->FD_, this->OldMode_);
this->OldMode_ = 0;
}
this->FD_ = -1;
this->IOS_ = nullptr;
}
Console::Impl::Impl()
: In_(In())
, Out_(Out())
, Err_(Err())
{
}
Console::Impl::~Impl() = default;
Console::Console()
: Impl_(cm::make_unique<Impl>())
{
}
#else
Console::Console() = default;
#endif
Console::~Console() = default;
Console::Console(Console&&) noexcept = default;
Console& Console::operator=(Console&&) noexcept = default;
}
}