ObjFW  OFWin32ConsoleStdIOStream.m at [51dfaf9042]

File src/platform/Windows/OFWin32ConsoleStdIOStream.m artifact ab386bcc83 part of check-in 51dfaf9042


/*
 * Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

/*
 * This file tries to make writing UTF-8 strings to the console "just work" on
 * Windows.
 *
 * While Windows does provide a way to change the codepage of the console to
 * UTF-8, unfortunately, different Windows versions handle that differently.
 * For example, on Windows XP, when using Windows XP's console, changing the
 * codepage to UTF-8 mostly breaks write() and completely breaks read():
 * write() suddenly returns the number of characters - instead of bytes -
 * written and read() just returns 0 as soon as a Unicode character is being
 * read.
 *
 * Therefore, instead of just using the UTF-8 codepage, this captures all reads
 * and writes to OFStd{In,Out,Err} on the low level, interprets the buffer as
 * UTF-8 and converts to / from UTF-16 to use ReadConsoleW() / WriteConsoleW().
 * Doing so is safe, as the console only supports text anyway and thus it does
 * not matter if binary gets garbled by the conversion (e.g. because invalid
 * UTF-8 gets converted to U+FFFD).
 *
 * In order to not do this when redirecting input / output to a file (as the
 * file would then be read / written in the wrong encoding and break reading /
 * writing binary), it checks that the handle is indeed a console.
 */

#include "config.h"

#include <errno.h>
#include <io.h>

#import "OFWin32ConsoleStdIOStream.h"
#import "OFColor.h"
#import "OFData.h"
#import "OFStdIOStream+Private.h"
#import "OFString.h"
#import "OFSystemInfo.h"

#import "OFInvalidArgumentException.h"
#import "OFInvalidEncodingException.h"
#import "OFOutOfRangeException.h"
#import "OFReadFailedException.h"
#import "OFWriteFailedException.h"

#include <windows.h>

static OFStringEncoding
codepageToEncoding(UINT codepage)
{
	switch (codepage) {
	case 437:
		return OFStringEncodingCodepage437;
	case 850:
		return OFStringEncodingCodepage850;
	case 858:
		return OFStringEncodingCodepage858;
	case 1251:
		return OFStringEncodingWindows1251;
	case 1252:
		return OFStringEncodingWindows1252;
	default:
		@throw [OFInvalidEncodingException exception];
	}
}

@implementation OFWin32ConsoleStdIOStream
+ (void)load
{
	int fd;

	if (self != [OFWin32ConsoleStdIOStream class])
		return;

	if ((fd = _fileno(stdin)) >= 0)
		OFStdIn = [[OFWin32ConsoleStdIOStream alloc]
		    of_initWithFileDescriptor: fd];
	if ((fd = _fileno(stdout)) >= 0)
		OFStdOut = [[OFWin32ConsoleStdIOStream alloc]
		    of_initWithFileDescriptor: fd];
	if ((fd = _fileno(stderr)) >= 0)
		OFStdErr = [[OFWin32ConsoleStdIOStream alloc]
		    of_initWithFileDescriptor: fd];
}

- (instancetype)of_initWithFileDescriptor: (int)fd
{
	self = [super of_initWithFileDescriptor: fd];

	@try {
		DWORD mode;
		CONSOLE_SCREEN_BUFFER_INFO csbi;

		_handle = (HANDLE)_get_osfhandle(fd);
		if (_handle == INVALID_HANDLE_VALUE)
			@throw [OFInvalidArgumentException exception];

		/* Not a console: Treat it as a regular OFStdIOStream */
		if (!GetConsoleMode(_handle, &mode))
			object_setClass(self, [OFStdIOStream class]);

		if (GetConsoleScreenBufferInfo(_handle, &csbi))
			_attributes = csbi.wAttributes;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (size_t)lowlevelReadIntoBuffer: (void *)buffer_ length: (size_t)length
{
	void *pool = objc_autoreleasePoolPush();
	char *buffer = buffer_;
	OFChar16 *UTF16;
	size_t j = 0;

	if (length > UINT32_MAX)
		@throw [OFOutOfRangeException exception];

	UTF16 = OFAllocMemory(length, sizeof(OFChar16));
	@try {
		DWORD UTF16Len;
		OFMutableData *rest = nil;
		size_t i = 0;

		if ([OFSystemInfo isWindowsNT]) {
			if (!ReadConsoleW(_handle, UTF16, (DWORD)length,
			    &UTF16Len, NULL))
				@throw [OFReadFailedException
				    exceptionWithObject: self
					requestedLength: length * 2
						  errNo: EIO];
		} else {
			OFStringEncoding encoding;
			OFString *string;
			size_t stringLen;

			if (!ReadConsoleA(_handle, (char *)UTF16, (DWORD)length,
			    &UTF16Len, NULL))
				@throw [OFReadFailedException
				    exceptionWithObject: self
					requestedLength: length
						  errNo: EIO];

			encoding = codepageToEncoding(GetConsoleCP());
			string = [OFString stringWithCString: (char *)UTF16
						    encoding: encoding
						      length: UTF16Len];
			stringLen = string.UTF16StringLength;

			if (stringLen > length)
				@throw [OFOutOfRangeException exception];

			UTF16Len = (DWORD)stringLen;
			memcpy(UTF16, string.UTF16String, stringLen);
		}

		if (UTF16Len > 0 && _incompleteUTF16Surrogate != 0) {
			OFUnichar c =
			    (((_incompleteUTF16Surrogate & 0x3FF) << 10) |
			    (UTF16[0] & 0x3FF)) + 0x10000;
			char UTF8[4];
			size_t UTF8Len;

			if ((UTF8Len = OFUTF8StringEncode(c, UTF8)) == 0)
				@throw [OFInvalidEncodingException exception];

			if (UTF8Len <= length) {
				memcpy(buffer, UTF8, UTF8Len);
				j += UTF8Len;
			} else {
				if (rest == nil)
					rest = [OFMutableData data];

				[rest addItems: UTF8 count: UTF8Len];
			}

			_incompleteUTF16Surrogate = 0;
			i++;
		}

		for (; i < UTF16Len; i++) {
			OFUnichar c = UTF16[i];
			char UTF8[4];
			size_t UTF8Len;

			/* Missing high surrogate */
			if ((c & 0xFC00) == 0xDC00)
				@throw [OFInvalidEncodingException exception];

			if ((c & 0xFC00) == 0xD800) {
				OFChar16 next;

				if (UTF16Len <= i + 1) {
					_incompleteUTF16Surrogate = c;

					if (rest != nil) {
						const char *items = rest.items;
						size_t count = rest.count;

						[self unreadFromBuffer: items
								length: count];
					}

					objc_autoreleasePoolPop(pool);

					return j;
				}

				next = UTF16[i + 1];

				if ((next & 0xFC00) != 0xDC00)
					@throw [OFInvalidEncodingException
					    exception];

				c = (((c & 0x3FF) << 10) | (next & 0x3FF)) +
				    0x10000;

				i++;
			}

			if ((UTF8Len = OFUTF8StringEncode(c, UTF8)) == 0)
				@throw [OFInvalidEncodingException exception];

			if (j + UTF8Len <= length) {
				memcpy(buffer + j, UTF8, UTF8Len);
				j += UTF8Len;
			} else {
				if (rest == nil)
					rest = [OFMutableData data];

				[rest addItems: UTF8 count: UTF8Len];
			}
		}

		if (rest != nil)
			[self unreadFromBuffer: rest.items length: rest.count];
	} @finally {
		OFFreeMemory(UTF16);
	}

	objc_autoreleasePoolPop(pool);

	return j;
}

- (size_t)lowlevelWriteBuffer: (const void *)buffer_ length: (size_t)length
{
	const char *buffer = buffer_;
	OFChar16 *tmp;
	size_t i = 0, j = 0;

	if (length > SIZE_MAX / 2)
		@throw [OFOutOfRangeException exception];

	if (_incompleteUTF8SurrogateLen > 0) {
		OFUnichar c;
		OFChar16 UTF16[2];
		ssize_t UTF8Len;
		size_t toCopy;
		DWORD UTF16Len, bytesWritten;

		UTF8Len = -OFUTF8StringDecode(
		    _incompleteUTF8Surrogate, _incompleteUTF8SurrogateLen, &c);

		OFEnsure(UTF8Len > 0);

		toCopy = UTF8Len - _incompleteUTF8SurrogateLen;
		if (toCopy > length)
			toCopy = length;

		memcpy(_incompleteUTF8Surrogate + _incompleteUTF8SurrogateLen,
		    buffer, toCopy);
		_incompleteUTF8SurrogateLen += toCopy;

		if (_incompleteUTF8SurrogateLen < (size_t)UTF8Len)
			return 0;

		UTF8Len = OFUTF8StringDecode(
		    _incompleteUTF8Surrogate, _incompleteUTF8SurrogateLen, &c);

		if (UTF8Len <= 0 || c > 0x10FFFF) {
			OFAssert(UTF8Len == 0 || UTF8Len < -4);

			UTF16[0] = 0xFFFD;
			UTF16Len = 1;
		} else {
			if (c > 0xFFFF) {
				c -= 0x10000;
				UTF16[0] = 0xD800 | (c >> 10);
				UTF16[1] = 0xDC00 | (c & 0x3FF);
				UTF16Len = 2;
			} else {
				UTF16[0] = c;
				UTF16Len = 1;
			}
		}

		if ([OFSystemInfo isWindowsNT]) {
			if (!WriteConsoleW(_handle, UTF16, UTF16Len,
			    &bytesWritten, NULL))
				@throw [OFWriteFailedException
				    exceptionWithObject: self
					requestedLength: UTF16Len * 2
					   bytesWritten: bytesWritten * 2
						  errNo: EIO];
		} else {
			void *pool = objc_autoreleasePoolPush();
			OFString *string = [OFString
			    stringWithUTF16String: UTF16
					   length: UTF16Len];
			OFStringEncoding encoding =
			    codepageToEncoding(GetConsoleOutputCP());
			size_t nativeLen = [string
			    cStringLengthWithEncoding: encoding];

			if (nativeLen > UINT32_MAX)
				@throw [OFOutOfRangeException exception];

			if (!WriteConsoleA(_handle,
			    [string cStringWithEncoding: encoding],
			    (DWORD)nativeLen, &bytesWritten, NULL))
				@throw [OFWriteFailedException
				    exceptionWithObject: self
					requestedLength: nativeLen
					   bytesWritten: bytesWritten
						  errNo: EIO];

			objc_autoreleasePoolPop(pool);
		}

		if (bytesWritten != UTF16Len)
			@throw [OFWriteFailedException
			    exceptionWithObject: self
				requestedLength: UTF16Len * 2
				   bytesWritten: bytesWritten * 2
					  errNo: 0];

		_incompleteUTF8SurrogateLen = 0;
		i += toCopy;
	}

	tmp = OFAllocMemory(length * 2, sizeof(OFChar16));
	@try {
		DWORD bytesWritten;

		while (i < length) {
			OFUnichar c;
			ssize_t UTF8Len;

			UTF8Len = OFUTF8StringDecode(buffer + i, length - i,
			    &c);

			if (UTF8Len < 0 && UTF8Len >= -4) {
				OFEnsure(length - i < 4);

				memcpy(_incompleteUTF8Surrogate, buffer + i,
				    length - i);
				_incompleteUTF8SurrogateLen = length - i;

				break;
			}

			if (UTF8Len <= 0 || c > 0x10FFFF) {
				tmp[j++] = 0xFFFD;
				i++;
				continue;
			}

			if (c > 0xFFFF) {
				c -= 0x10000;
				tmp[j++] = 0xD800 | (c >> 10);
				tmp[j++] = 0xDC00 | (c & 0x3FF);
			} else
				tmp[j++] = c;

			i += UTF8Len;
		}

		if (j > UINT32_MAX)
			@throw [OFOutOfRangeException exception];

		if ([OFSystemInfo isWindowsNT]) {
			if (!WriteConsoleW(_handle, tmp, (DWORD)j,
			    &bytesWritten, NULL))
				@throw [OFWriteFailedException
				    exceptionWithObject: self
					requestedLength: j * 2
					   bytesWritten: bytesWritten * 2
						  errNo: EIO];
		} else {
			void *pool = objc_autoreleasePoolPush();
			OFString *string = [OFString stringWithUTF16String: tmp
								    length: j];
			OFStringEncoding encoding =
			    codepageToEncoding(GetConsoleOutputCP());
			size_t nativeLen = [string
			    cStringLengthWithEncoding: encoding];

			if (nativeLen > UINT32_MAX)
				@throw [OFOutOfRangeException exception];

			if (!WriteConsoleA(_handle,
			    [string cStringWithEncoding: encoding],
			    (DWORD)nativeLen, &bytesWritten, NULL))
				@throw [OFWriteFailedException
				    exceptionWithObject: self
					requestedLength: nativeLen
					   bytesWritten: bytesWritten
						  errNo: EIO];

			objc_autoreleasePoolPop(pool);
		}

		if (bytesWritten != j)
			@throw [OFWriteFailedException
			    exceptionWithObject: self
				requestedLength: j * 2
				   bytesWritten: bytesWritten * 2
					  errNo: 0];
	} @finally {
		OFFreeMemory(tmp);
	}

	/*
	 * We do not count in bytes when writing to the Win32 console. But
	 * since any incomplete write is an exception here anyway, we can just
	 * return length.
	 */
	return length;
}

- (bool)hasTerminal
{
	/*
	 * We can never get here if there is no terminal, as the initializer
	 * changes the class to OFStdIOStream in that case.
	 */
	return true;
}

- (int)columns
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return -1;

	return csbi.dwSize.X;
}

- (int)rows
{
	/*
	 * The buffer size returned is almost always larger than the window
	 * size, so this is useless.
	 */
	return -1;
}

- (void)setForegroundColor: (OFColor *)color
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	float red, green, blue;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	csbi.wAttributes &= ~(FOREGROUND_RED | FOREGROUND_GREEN |
	    FOREGROUND_BLUE | FOREGROUND_INTENSITY);

	[color getRed: &red green: &green blue: &blue alpha: NULL];

	if (red >= 0.25)
		csbi.wAttributes |= FOREGROUND_RED;
	if (green >= 0.25)
		csbi.wAttributes |= FOREGROUND_GREEN;
	if (blue >= 0.25)
		csbi.wAttributes |= FOREGROUND_BLUE;

	if (red >= 0.75 || green >= 0.75 || blue >= 0.75)
		csbi.wAttributes |= FOREGROUND_INTENSITY;

	SetConsoleTextAttribute(_handle, csbi.wAttributes);
}

- (void)setBackgroundColor: (OFColor *)color
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	float red, green, blue;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	csbi.wAttributes &= ~(BACKGROUND_RED | BACKGROUND_GREEN |
	    BACKGROUND_BLUE | BACKGROUND_INTENSITY);

	[color getRed: &red green: &green blue: &blue alpha: NULL];

	if (red >= 0.25)
		csbi.wAttributes |= BACKGROUND_RED;
	if (green >= 0.25)
		csbi.wAttributes |= BACKGROUND_GREEN;
	if (blue >= 0.25)
		csbi.wAttributes |= BACKGROUND_BLUE;

	if (red >= 0.75 || green >= 0.75 || blue >= 0.75)
		csbi.wAttributes |= BACKGROUND_INTENSITY;

	SetConsoleTextAttribute(_handle, csbi.wAttributes);
}

- (void)reset
{
	SetConsoleTextAttribute(_handle, _attributes);
}

- (void)clear
{
	static COORD zero = { 0, 0 };
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	DWORD bytesWritten;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	if (!FillConsoleOutputCharacter(_handle, ' ',
	    csbi.dwSize.X * csbi.dwSize.Y, zero, &bytesWritten))
		return;

	if (!FillConsoleOutputAttribute(_handle, csbi.wAttributes,
	    csbi.dwSize.X * csbi.dwSize.Y, zero, &bytesWritten))
		return;

	SetConsoleCursorPosition(_handle, zero);
}

- (void)eraseLine
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	DWORD bytesWritten;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	csbi.dwCursorPosition.X = 0;

	if (!FillConsoleOutputCharacter(_handle, ' ', csbi.dwSize.X,
		csbi.dwCursorPosition, &bytesWritten))
		return;

	FillConsoleOutputAttribute(_handle, csbi.wAttributes, csbi.dwSize.X,
	    csbi.dwCursorPosition, &bytesWritten);
}

- (void)setCursorColumn: (unsigned int)column
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	csbi.dwCursorPosition.X = column;

	SetConsoleCursorPosition(_handle, csbi.dwCursorPosition);
}

- (void)setCursorPosition: (OFPoint)position
{
	if (position.x < 0 || position.y < 0)
		@throw [OFInvalidArgumentException exception];

	SetConsoleCursorPosition(_handle, (COORD){ position.x, position.y });
}

- (void)setRelativeCursorPosition: (OFPoint)position
{
	CONSOLE_SCREEN_BUFFER_INFO csbi;

	if (!GetConsoleScreenBufferInfo(_handle, &csbi))
		return;

	csbi.dwCursorPosition.X += position.x;
	csbi.dwCursorPosition.Y += position.y;

	SetConsoleCursorPosition(_handle, csbi.dwCursorPosition);
}
@end