Index: ObjFW.xcodeproj/project.pbxproj ================================================================== --- ObjFW.xcodeproj/project.pbxproj +++ ObjFW.xcodeproj/project.pbxproj @@ -435,10 +435,11 @@ 4BE52D2617B990DA005958D1 /* OFChecksumFailedException.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BE52D2417B990DA005958D1 /* OFChecksumFailedException.m */; }; 4BEAF52D19A811DA00B61868 /* module.map in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4BEAF52519A8107500B61868 /* module.map */; }; 4BEC83B919B7CB7100E4BB08 /* OFRIPEMD160Hash.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BEC83B719B7CB7100E4BB08 /* OFRIPEMD160Hash.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4BEC83BA19B7CB7100E4BB08 /* OFRIPEMD160Hash.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BEC83B819B7CB7100E4BB08 /* OFRIPEMD160Hash.m */; }; 4BEC83BC19B7CBDE00E4BB08 /* OFRIPEMD160HashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BEC83BB19B7CBDE00E4BB08 /* OFRIPEMD160HashTests.m */; }; + 4BF171B21C949A3300F5B47B /* OFStdIOStream+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BF171B11C9499F300F5B47B /* OFStdIOStream+Private.h */; }; 4BF33AFB133807590059CEF7 /* ObjFW.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B3D23761337FBC800DD29B8 /* ObjFW.framework */; }; 4BF33AFC133807A20059CEF7 /* OFArrayTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B6EF66E1235358D0076B512 /* OFArrayTests.m */; }; 4BF33AFD133807A20059CEF7 /* OFBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5F0E412DF4259005C7A0C /* OFBlockTests.m */; }; 4BF33AFE133807A20059CEF7 /* OFDataArrayTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B6EF66F1235358D0076B512 /* OFDataArrayTests.m */; }; 4BF33AFF133807A20059CEF7 /* OFDateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5F0E512DF4259005C7A0C /* OFDateTests.m */; }; @@ -963,10 +964,11 @@ 4BEAF52519A8107500B61868 /* module.map */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; name = module.map; path = misc/module.map; sourceTree = SOURCE_ROOT; }; 4BEC83B719B7CB7100E4BB08 /* OFRIPEMD160Hash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFRIPEMD160Hash.h; path = src/OFRIPEMD160Hash.h; sourceTree = ""; }; 4BEC83B819B7CB7100E4BB08 /* OFRIPEMD160Hash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFRIPEMD160Hash.m; path = src/OFRIPEMD160Hash.m; sourceTree = ""; }; 4BEC83BB19B7CBDE00E4BB08 /* OFRIPEMD160HashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFRIPEMD160HashTests.m; path = tests/OFRIPEMD160HashTests.m; sourceTree = ""; }; 4BF0749512DFAFCA00A4ADD1 /* OFURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFURLTests.m; path = tests/OFURLTests.m; sourceTree = SOURCE_ROOT; }; + 4BF171B11C9499F300F5B47B /* OFStdIOStream+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "OFStdIOStream+Private.h"; path = "src/OFStdIOStream+Private.h"; sourceTree = ""; }; 4BF1BCBF11C9663F0025511F /* objfw-defs.h.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "objfw-defs.h.in"; path = "src/objfw-defs.h.in"; sourceTree = ""; }; 4BF1BCC011C9663F0025511F /* OFHash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFHash.h; path = src/OFHash.h; sourceTree = ""; }; 4BF1BCC211C9663F0025511F /* OFMD5Hash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFMD5Hash.h; path = src/OFMD5Hash.h; sourceTree = ""; }; 4BF1BCC311C9663F0025511F /* OFMD5Hash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFMD5Hash.m; path = src/OFMD5Hash.m; sourceTree = ""; }; 4BF1BCC411C9663F0025511F /* OFSHA1Hash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFSHA1Hash.h; path = src/OFSHA1Hash.h; sourceTree = ""; }; @@ -1414,10 +1416,11 @@ 4B60259E19B76A5C00694BCC /* OFSHA512Hash.m */, 4B141BA215FCDF74000C21A8 /* OFSortedList.h */, 4B141BA315FCDF74000C21A8 /* OFSortedList.m */, 4B0256E2172B60400062B5F1 /* OFStdIOStream.h */, 4B0256E3172B60400062B5F1 /* OFStdIOStream.m */, + 4BF171B11C9499F300F5B47B /* OFStdIOStream+Private.h */, 4B67997D1099E7C50041064A /* OFStream.h */, 4B67997E1099E7C50041064A /* OFStream.m */, 4B6C8AD217BD5C2E00B194F2 /* OFStream+Private.h */, 4BAF5F49123460C900F4E111 /* OFStreamSocket.h */, 4BAF5F4A123460C900F4E111 /* OFStreamSocket.m */, @@ -1812,10 +1815,11 @@ 4BA85BCC140ECCE800E91D51 /* OFMutableSet_hashtable.h in Headers */, 4B552552147AA5DB0003BF47 /* OFMutableString_UTF8.h in Headers */, 4B6C8AD817BD5C2E00B194F2 /* OFRunLoop+Private.h in Headers */, 4BA85BCE140ECCE800E91D51 /* OFSet_hashtable.h in Headers */, 4B8385161951BF9500D5358A /* OFSettings_INIFile.h in Headers */, + 4BF171B21C949A3300F5B47B /* OFStdIOStream+Private.h in Headers */, 4B6C8AD917BD5C2E00B194F2 /* OFStream+Private.h in Headers */, 4B552554147AA5DB0003BF47 /* OFString_UTF8.h in Headers */, 4B6C8ADB17BD5C2E00B194F2 /* OFString_UTF8+Private.h in Headers */, 4BD653C5143B8489006182F0 /* OFTCPSocket+SOCKS5.h in Headers */, 4B6C8ADC17BD5C2E00B194F2 /* OFThread+Private.h in Headers */, Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -559,10 +559,16 @@ ]) AC_MSG_RESULT($ac_cv_snprintf_useful_ret) ]) test x"$have_asprintf" != x"yes" -a x"$ac_cv_snprintf_useful_ret" != x"yes" && \ AC_MSG_ERROR(No asprintf and no snprintf returning required space!) + +case "$host_os" in +mingw*) + AC_SUBST(OFSTDIOSTREAM_WIN32CONSOLE_M, "OFStdIOStream_Win32Console.m") + ;; +esac AC_CHECK_FUNCS(sigaction) AC_CHECK_FUNCS([arc4random random], break) Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -36,10 +36,11 @@ OFPROCESS_M = @OFPROCESS_M@ OFKERNELEVENTOBSERVER_EPOLL_M = @OFKERNELEVENTOBSERVER_EPOLL_M@ OFKERNELEVENTOBSERVER_KQUEUE_M = @OFKERNELEVENTOBSERVER_KQUEUE_M@ OFKERNELEVENTOBSERVER_POLL_M = @OFKERNELEVENTOBSERVER_POLL_M@ OFKERNELEVENTOBSERVER_SELECT_M = @OFKERNELEVENTOBSERVER_SELECT_M@ +OFSTDIOSTREAM_WIN32CONSOLE_M = @OFSTDIOSTREAM_WIN32CONSOLE_M@ OFZIP = @OFZIP@ PROPERTIESTESTS_M = @PROPERTIESTESTS_M@ REEXPORT_LIBOBJC = @REEXPORT_LIBOBJC@ RUNTIME = @RUNTIME@ RUNTIME_A = @RUNTIME_A@ Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -51,10 +51,11 @@ OFSHA384Hash.m \ OFSHA384Or512Hash.m \ OFSHA512Hash.m \ OFSortedList.m \ OFStdIOStream.m \ + ${OFSTDIOSTREAM_WIN32CONSOLE_M} \ OFStream.m \ OFString.m \ OFString+Hashing.m \ OFString+JSONValue.m \ OFString+Serialization.m \ ADDED src/OFStdIOStream+Private.h Index: src/OFStdIOStream+Private.h ================================================================== --- src/OFStdIOStream+Private.h +++ src/OFStdIOStream+Private.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFStdIOStream.h" + +OF_ASSUME_NONNULL_BEGIN + +@interface OFStdIOStream () +- (instancetype)OF_initWithFileDescriptor: (int)fd; +@end + +OF_ASSUME_NONNULL_END Index: src/OFStdIOStream.h ================================================================== --- src/OFStdIOStream.h +++ src/OFStdIOStream.h @@ -24,11 +24,13 @@ * @brief A class for providing standard input, output and error as OFStream. * * The global variables @ref of_stdin, @ref of_stdout and @ref of_stderr are * instances of this class and need no initialization. */ +#ifdef OF_STDIO_STREAM_WIN32_CONSOLE_H OF_SUBCLASSING_RESTRICTED +#endif @interface OFStdIOStream: OFStream { int _fd; bool _atEndOfStream; } Index: src/OFStdIOStream.m ================================================================== --- src/OFStdIOStream.m +++ src/OFStdIOStream.m @@ -24,41 +24,22 @@ #include #include #import "OFStdIOStream.h" +#import "OFStdIOStream+Private.h" #import "OFDate.h" #import "OFApplication.h" #import "OFOutOfRangeException.h" #import "OFReadFailedException.h" #import "OFWriteFailedException.h" -#ifdef OF_WINDOWS -# include -#endif - OFStdIOStream *of_stdin = nil; OFStdIOStream *of_stdout = nil; OFStdIOStream *of_stderr = nil; -#ifdef OF_WINDOWS -UINT originalConsoleCP; -UINT originalConsoleOutputCP; - -static void -restoreCodepage(void) -{ - SetConsoleCP(originalConsoleCP); - SetConsoleOutputCP(originalConsoleOutputCP); -} -#endif - -@interface OFStdIOStream () -- (instancetype)OF_initWithFileDescriptor: (int)fd; -@end - void of_log(OFConstantString *format, ...) { void *pool = objc_autoreleasePoolPush(); OFDate *date; @@ -79,27 +60,17 @@ objc_autoreleasePoolPop(pool); } @implementation OFStdIOStream +#ifndef OF_WINDOWS + (void)load { of_stdin = [[OFStdIOStream alloc] OF_initWithFileDescriptor: 0]; of_stdout = [[OFStdIOStream alloc] OF_initWithFileDescriptor: 1]; of_stderr = [[OFStdIOStream alloc] OF_initWithFileDescriptor: 2]; } - -#ifdef OF_WINDOWS -+ (void)initialize -{ - originalConsoleCP = GetConsoleCP(); - originalConsoleOutputCP = GetConsoleOutputCP(); - atexit(restoreCodepage); - - SetConsoleCP(CP_UTF8); - SetConsoleOutputCP(CP_UTF8); -} #endif - init { OF_INVALID_INIT_METHOD ADDED src/OFStdIOStream_Win32Console.h Index: src/OFStdIOStream_Win32Console.h ================================================================== --- src/OFStdIOStream_Win32Console.h +++ src/OFStdIOStream_Win32Console.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#define OF_STDIO_STREAM_WIN32CONSOLE_H + +#import "OFStdIOStream.h" + +OF_ASSUME_NONNULL_BEGIN + +@interface OFStdIOStream_Win32Console: OFStdIOStream +{ + HANDLE _handle; +} +@end + +OF_ASSUME_NONNULL_END ADDED src/OFStdIOStream_Win32Console.m Index: src/OFStdIOStream_Win32Console.m ================================================================== --- src/OFStdIOStream_Win32Console.m +++ src/OFStdIOStream_Win32Console.m @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +/* + * This file tries to make writing UTF-8 strings to the console "just work" on + * Windows. + * + * Windows does provide a way to change the codepage of the console to UTF-8, + * but 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. + * + * So instead of just using the UTF-8 codepage, this captures all reads and + * writes to of_std{in,err,out} on the lowlevel, interprets the buffer as UTF-8 + * and converts to / from UTF-16 to use ReadConsoleW() and WriteConsoleW(), as + * reading or writing binary from / to the console would not make any sense + * anyway and thus it's safe to assume it's text. + * + * In order to not do this when redirecting input / output to a file, it checks + * that the handle is indeed a console. + * + * TODO: Properly handle surrogates being cut in the middle + */ + +#define OF_STDIO_STREAM_WIN32_CONSOLE_M + +#include "config.h" + +#import "OFStdIOStream_Win32Console.h" +#import "OFStdIOStream+Private.h" +#import "OFDataArray.h" + +#import "OFInvalidArgumentException.h" +#import "OFInvalidEncodingException.h" +#import "OFOutOfRangeException.h" +#import "OFReadFailedException.h" +#import "OFWriteFailedException.h" + +#include + +@implementation OFStdIOStream_Win32Console ++ (void)load +{ + of_stdin = [[OFStdIOStream_Win32Console alloc] + OF_initWithFileDescriptor: 0]; + of_stdout = [[OFStdIOStream_Win32Console alloc] + OF_initWithFileDescriptor: 1]; + of_stderr = [[OFStdIOStream_Win32Console alloc] + OF_initWithFileDescriptor: 2]; +} + +- (instancetype)OF_initWithFileDescriptor: (int)fd +{ + self = [super OF_initWithFileDescriptor: fd]; + + @try { + DWORD mode; + + switch (fd) { + case 0: + _handle = GetStdHandle(STD_INPUT_HANDLE); + break; + case 1: + _handle = GetStdHandle(STD_OUTPUT_HANDLE); + break; + case 2: + _handle = GetStdHandle(STD_ERROR_HANDLE); + break; + default: + @throw [OFInvalidArgumentException exception]; + } + + /* Not a console: Treat it as a regular OFStdIOStream */ + if (!GetConsoleMode(_handle, &mode)) + object_setClass(self, [OFStdIOStream class]); + } @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_; + of_char16_t *UTF16; + size_t j = 0; + OFDataArray *rest = nil; + + UTF16 = [self allocMemoryWithSize: sizeof(of_char16_t) + count: length]; + @try { + DWORD UTF16Len; + + if (!ReadConsoleW(_handle, UTF16, length, &UTF16Len, NULL)) + @throw [OFReadFailedException + exceptionWithObject: self + requestedLength: length]; + + for (size_t i = 0; i < UTF16Len; i++) { + of_unichar_t c = UTF16[i]; + char UTF8[4]; + size_t UTF8Len; + + /* Missing high surrogate */ + if ((c & 0xFC00) == 0xDC00) + @throw [OFInvalidEncodingException exception]; + + if ((c & 0xFC00) == 0xD800) { + of_char16_t next; + + if (UTF16Len <= i + 1) + @throw [OFInvalidEncodingException + exception]; + + next = UTF16[i + 1]; + + if ((next & 0xFC00) != 0xDC00) + @throw [OFInvalidEncodingException + exception]; + + c = (((c & 0x3FF) << 10) | (next & 0x3FF)) + + 0x10000; + + i++; + } + + if ((UTF8Len = of_string_utf8_encode(c, UTF8)) == 0) + @throw [OFInvalidEncodingException exception]; + + if (j + UTF8Len <= length) { + memcpy(buffer + j, UTF8, UTF8Len); + j += UTF8Len; + } else { + if (rest == nil) + rest = [OFDataArray dataArray]; + + [rest addItems: UTF8 + count: UTF8Len]; + } + } + + if (rest != nil) + [self unreadFromBuffer: [rest items] + length: [rest count]]; + } @finally { + [self freeMemory: UTF16]; + } + + objc_autoreleasePoolPop(pool); + + return j; +} + +- (void)lowlevelWriteBuffer: (const void*)buffer_ + length: (size_t)length +{ + const char *buffer = buffer_; + of_char16_t *tmp; + + if (length > SIZE_MAX / 2) + @throw [OFOutOfRangeException exception]; + + tmp = [self allocMemoryWithSize: sizeof(of_char16_t) + count: length * 2]; + @try { + size_t i = 0, j = 0; + DWORD written; + + while (i < length) { + of_unichar_t c; + size_t cLen; + + cLen = of_string_utf8_decode(buffer + i, length - i, + &c); + + if (cLen == 0 || c > 0x10FFFF) + @throw [OFInvalidEncodingException exception]; + + if (c > 0xFFFF) { + c -= 0x10000; + tmp[j++] = 0xD800 | (c >> 10); + tmp[j++] = 0xDC00 | (c & 0x3FF); + } else + tmp[j++] = c; + + i += cLen; + } + + if (!WriteConsoleW(_handle, tmp, j, &written, NULL) || + written != j) + @throw [OFWriteFailedException + exceptionWithObject: self + requestedLength: j]; + } @finally { + [self freeMemory: tmp]; + } +} +@end