-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Console input/output procedures. #55
Open
cheatfate
wants to merge
9
commits into
master
Choose a base branch
from
conio
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
70405da
Initial commit.
cheatfate cb44a0d
Fix error handler.
cheatfate 4363df1
User proper method of console identification (msvcrt).
cheatfate 6ee4974
Use proper method of console identification (glibc).
cheatfate cc001fa
Make isConsoleRedirected() public API.
cheatfate b0bbeb4
Add UTF-8 validation procedure.
cheatfate 39fb71b
Add UTF-8 length procedure.
cheatfate 1746bc0
Fix UTF-32 encoder/decoder.
cheatfate 8d87ba3
Fix *nix compilation problem.
cheatfate File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,386 @@ | ||
## Copyright (c) 2020 Status Research & Development GmbH | ||
## Licensed under either of | ||
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) | ||
## * MIT license ([LICENSE-MIT](LICENSE-MIT)) | ||
## at your option. | ||
## This file may not be copied, modified, or distributed except according to | ||
## those terms. | ||
|
||
## This module implements cross-platform console procedures. | ||
import io2, utf8 | ||
export io2 | ||
|
||
when defined(windows): | ||
proc setConsoleOutputCP(wCodePageID: cuint): int32 {. | ||
importc: "SetConsoleOutputCP", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc setConsoleCP(wCodePageID: cuint): int32 {. | ||
importc: "SetConsoleCP", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc getConsoleCP(): cuint {. | ||
importc: "GetConsoleCP", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc getConsoleOutputCP(): cuint {. | ||
importc: "GetConsoleOutputCP", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc setConsoleMode(hConsoleHandle: uint, dwMode: uint32): int32 {. | ||
importc: "SetConsoleMode", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc getConsoleMode(hConsoleHandle: uint, dwMode: var uint32): int32 {. | ||
importc: "GetConsoleMode", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc readConsole(hConsoleInput: uint, lpBuffer: pointer, | ||
nNumberOfCharsToRead: uint32, | ||
lpNumberOfCharsRead: var uint32, | ||
pInputControl: pointer): int32 {. | ||
importc: "ReadConsoleW", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc readFile(hFile: uint, lpBuffer: pointer, | ||
nNumberOfBytesToRead: uint32, | ||
lpNumberOfBytesRead: var uint32, | ||
lpOverlapped: pointer): int32 {. | ||
importc: "ReadFile", dynlib: "kernel32", stdcall, sideEffect.} | ||
proc writeConsole(hConsoleOutput: uint, lpBuffer: pointer, | ||
nNumberOfCharsToWrite: uint32, | ||
lpNumberOfCharsWritten: var uint32, | ||
lpReserved: pointer): int32 {. | ||
importc: "WriteConsoleW", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc writeFile(hFile: uint, lpBuffer: pointer, | ||
nNumberOfBytesToWrite: uint32, | ||
lpNumberOfBytesWritten: var uint32, | ||
lpOverlapped: pointer): int32 {. | ||
importc: "WriteFile", dynlib: "kernel32", stdcall, sideEffect.} | ||
proc getStdHandle(nStdHandle: uint32): uint {. | ||
importc: "GetStdHandle", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc wideCharToMultiByte(codePage: cuint, dwFlags: uint32, | ||
lpWideCharStr: ptr Utf16Char, cchWideChar: cint, | ||
lpMultiByteStr: ptr char, cbMultiByte: cint, | ||
lpDefaultChar: pointer, | ||
lpUsedDefaultChar: pointer): cint {. | ||
importc: "WideCharToMultiByte", stdcall, dynlib: "kernel32", sideEffect.} | ||
proc getFileType(hFile: uint): uint32 {. | ||
importc: "GetFileType", stdcall, dynlib: "kernel32", sideEffect.} | ||
|
||
const | ||
CP_UTF8 = 65001'u32 | ||
STD_INPUT_HANDLE = cast[uint32](-10) | ||
STD_OUTPUT_HANDLE = cast[uint32](-11) | ||
INVALID_HANDLE_VALUE = cast[uint](-1) | ||
ENABLE_PROCESSED_INPUT = 0x0001'u32 | ||
ENABLE_ECHO_INPUT = 0x0004'u32 | ||
FILE_TYPE_CHAR = 0x0002'u32 | ||
ERROR_NO_UNICODE_TRANSLATION = 1113'u32 | ||
|
||
proc isConsoleRedirected*(hConsole: uint): bool = | ||
## Returns ``true`` if console handle was redirected. | ||
let res = getFileType(hConsole) | ||
if res == FILE_TYPE_CHAR: | ||
# The specified handle is a character device, typically an LPT device or a | ||
# console. | ||
false | ||
else: | ||
true | ||
|
||
proc readConsoleInput(maxChars: int): IoResult[string] = | ||
let hConsoleInput = | ||
block: | ||
let res = getStdHandle(STD_INPUT_HANDLE) | ||
if res == INVALID_HANDLE_VALUE: | ||
return err(ioLastError()) | ||
res | ||
|
||
let prevInputCP = | ||
block: | ||
let res = getConsoleCP() | ||
if res == cuint(0): | ||
return err(ioLastError()) | ||
res | ||
|
||
if isConsoleRedirected(hConsoleInput): | ||
# Console STDIN is redirected, we should use ReadFile(), because | ||
# ReadConsole() is not working for such types of STDIN. | ||
if setConsoleCP(CP_UTF8) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
# Allocating buffer with size equal to `(maxChars + len(CRLF)) * 4`, | ||
# where 4 is maximum expected size of one character (UTF8 encoding). | ||
var buffer = newString((maxChars + 2) * 4) | ||
let bytesToRead = uint32(len(buffer)) | ||
var bytesRead: uint32 | ||
let rres = readFile(hConsoleInput, cast[pointer](addr buffer[0]), | ||
bytesToRead, bytesRead, nil) | ||
if rres == 0: | ||
let errCode = ioLastError() | ||
discard setConsoleCP(prevInputCP) | ||
return err(errCode) | ||
|
||
if setConsoleCP(prevInputCP) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
# Truncate additional bytes from buffer. | ||
buffer.setLen(int(bytesRead)) | ||
|
||
# Trim CR/CRLF from buffer. | ||
if len(buffer) > 0: | ||
if buffer[^1] == char(0x0A): | ||
if len(buffer) > 1: | ||
if buffer[^2] == char(0x0D): | ||
buffer.setLen(len(buffer) - 2) | ||
else: | ||
buffer.setLen(len(buffer) - 1) | ||
else: | ||
buffer.setLen(len(buffer) - 1) | ||
elif buffer[^1] == char(0x0D): | ||
buffer.setLen(len(buffer) - 1) | ||
|
||
# Check if buffer is valid UTF-8 encoded string. | ||
if utf8Validate(buffer): | ||
# Cut result buffer to `maxChars` characters. | ||
ok(utf8Substr(buffer, 0, maxChars - 1).get()) | ||
else: | ||
err(IoErrorCode(ERROR_NO_UNICODE_TRANSLATION)) | ||
else: | ||
let prevMode = | ||
block: | ||
var mode: uint32 | ||
let res = getConsoleMode(hConsoleInput, mode) | ||
if res == 0: | ||
return err(ioLastError()) | ||
mode | ||
|
||
var newMode = prevMode or ENABLE_PROCESSED_INPUT | ||
newMode = newMode and not(ENABLE_ECHO_INPUT) | ||
|
||
# Change console CodePage to allow UTF-8 strings input. | ||
if setConsoleCP(CP_UTF8) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
# Disable local echo output. | ||
let mres = setConsoleMode(hConsoleInput, newMode) | ||
if mres == 0: | ||
let errCode = ioLastError() | ||
discard setConsoleCP(prevInputCP) | ||
return err(errCode) | ||
|
||
# Allocating buffer with size equal to `maxChars` + len(CRLF). | ||
var buffer = newSeq[Utf16Char](maxChars + 2) | ||
let charsToRead = uint32(len(buffer)) | ||
var charsRead: uint32 | ||
let rres = readConsole(hConsoleInput, cast[pointer](addr buffer[0]), | ||
charsToRead, charsRead, nil) | ||
if rres == 0'i32: | ||
let errCode = ioLastError() | ||
discard setConsoleMode(hConsoleInput, prevMode) | ||
discard setConsoleCP(prevInputCP) | ||
return err(errCode) | ||
|
||
# Restore local echo output. | ||
if setConsoleMode(hConsoleInput, prevMode) == 0'i32: | ||
let errCode = ioLastError() | ||
discard setConsoleCP(prevInputCP) | ||
return err(errCode) | ||
|
||
# Restore previous console CodePage. | ||
if setConsoleCP(prevInputCP) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
# Truncate additional bytes from buffer. | ||
buffer.setLen(int(min(charsRead, uint32(maxChars)))) | ||
|
||
# Truncate CRLF in result wide string. | ||
if len(buffer) > 0: | ||
zah marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if int16(buffer[^1]) == int16(0x0A): | ||
if len(buffer) > 1: | ||
if int16(buffer[^2]) == int16(0x0D): | ||
buffer.setLen(len(buffer) - 2) | ||
else: | ||
buffer.setLen(len(buffer) - 1) | ||
else: | ||
buffer.setLen(len(buffer) - 1) | ||
elif int16(buffer[^1]) == int16(0x0D): | ||
buffer.setLen(len(buffer) - 1) | ||
|
||
# Convert Windows UCS-2 encoded string to UTF-8 encoded string. | ||
if len(buffer) > 0: | ||
var pwd = "" | ||
let bytesNeeded = wideCharToMultiByte(CP_UTF8, 0'u32, addr buffer[0], | ||
cint(len(buffer)), nil, | ||
cint(0), nil, nil) | ||
if bytesNeeded <= cint(0): | ||
return err(ioLastError()) | ||
pwd.setLen(bytesNeeded) | ||
let cres = wideCharToMultiByte(CP_UTF8, 0'u32, addr buffer[0], | ||
cint(len(buffer)), addr pwd[0], | ||
cint(len(pwd)), nil, nil) | ||
if cres == cint(0): | ||
return err(ioLastError()) | ||
ok(pwd) | ||
else: | ||
ok("") | ||
|
||
proc writeConsoleOutput(data: string): IoResult[void] = | ||
if len(data) == 0: | ||
return ok() | ||
|
||
let hConsoleOutput = | ||
block: | ||
let res = getStdHandle(STD_OUTPUT_HANDLE) | ||
if res == INVALID_HANDLE_VALUE: | ||
return err(ioLastError()) | ||
res | ||
|
||
let prevOutputCP = | ||
block: | ||
let res = getConsoleOutputCP() | ||
if res == cuint(0): | ||
return err(ioLastError()) | ||
res | ||
|
||
if isConsoleRedirected(hConsoleOutput): | ||
# If STDOUT is redirected we should use WriteFile() because WriteConsole() | ||
# is not working for such types of STDOUT. | ||
if setConsoleOutputCP(CP_UTF8) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
let bytesToWrite = uint32(len(data)) | ||
var bytesWritten: uint32 | ||
let wres = writeFile(hConsoleOutput, cast[pointer](unsafeAddr data[0]), | ||
bytesToWrite, bytesWritten, nil) | ||
if wres == 0'i32: | ||
let errCode = ioLastError() | ||
discard setConsoleOutputCP(prevOutputCP) | ||
return err(errCode) | ||
|
||
if setConsoleOutputCP(prevOutputCP) == 0'i32: | ||
return err(ioLastError()) | ||
else: | ||
if setConsoleOutputCP(CP_UTF8) == 0'i32: | ||
return err(ioLastError()) | ||
|
||
let widePrompt = newWideCString(data) | ||
var charsWritten: uint32 | ||
let wres = writeConsole(hConsoleOutput, cast[pointer](widePrompt), | ||
uint32(len(widePrompt)), charsWritten, nil) | ||
if wres == 0'i32: | ||
let errCode = ioLastError() | ||
discard setConsoleOutputCP(prevOutputCP) | ||
return err(errCode) | ||
|
||
if setConsoleOutputCP(prevOutputCP) == 0'i32: | ||
return err(ioLastError()) | ||
ok() | ||
|
||
elif defined(posix): | ||
import posix, termios | ||
|
||
proc isConsoleRedirected*(consoleFd: cint): bool = | ||
## Returns ``true`` if console handle was redirected. | ||
var mode: Termios | ||
# This is how `isatty()` checks for TTY. | ||
if tcGetAttr(consoleFd, addr mode) != cint(0): | ||
true | ||
else: | ||
false | ||
|
||
proc writeConsoleOutput(prompt: string): IoResult[void] = | ||
if len(prompt) == 0: | ||
ok() | ||
else: | ||
let res = posix.write(STDOUT_FILENO, cast[pointer](unsafeAddr prompt[0]), | ||
len(prompt)) | ||
if res != len(prompt): | ||
err(ioLastError()) | ||
else: | ||
ok() | ||
|
||
proc readConsoleInput(maxChars: int): IoResult[string] = | ||
# Allocating buffer with size equal to `(maxChars + len(LF)) * 4`, where | ||
# 4 is maximum expected size of one character (UTF8 encoding). | ||
var buffer = newString((maxChars + 1) * 4) | ||
|
||
if isConsoleRedirected(STDIN_FILENO): | ||
let bytesRead = | ||
block: | ||
let res = posix.read(STDIN_FILENO, cast[pointer](addr buffer[0]), | ||
len(buffer)) | ||
if res < 0: | ||
return err(ioLastError()) | ||
res | ||
|
||
# Truncate additional bytes from buffer. | ||
buffer.setLen(bytesRead) | ||
|
||
# Trim LF in result string | ||
if len(buffer) > 0: | ||
if buffer[^1] == char(0x0A): | ||
buffer.setLen(len(buffer) - 1) | ||
|
||
# Check if buffer is valid UTF-8 encoded string. | ||
if utf8Validate(buffer): | ||
# Cut result buffer to `maxChars` characters. | ||
ok(utf8Substr(buffer, 0, maxChars - 1).get()) | ||
else: | ||
err(IoErrorCode(EILSEQ)) | ||
else: | ||
let bytesRead = | ||
block: | ||
var cur, old: Termios | ||
if tcGetAttr(STDIN_FILENO, addr cur) != cint(0): | ||
return err(ioLastError()) | ||
|
||
old = cur | ||
cur.c_lflag = cur.c_lflag and not(Cflag(ECHO)) | ||
|
||
if tcSetAttr(STDIN_FILENO, TCSADRAIN, addr(cur)) != cint(0): | ||
return err(ioLastError()) | ||
|
||
let res = read(STDIN_FILENO, cast[pointer](addr buffer[0]), | ||
len(buffer)) | ||
if res < 0: | ||
let errCode = ioLastError() | ||
discard tcSetAttr(STDIN_FILENO, TCSADRAIN, addr(old)) | ||
return err(errCode) | ||
|
||
if tcSetAttr(STDIN_FILENO, TCSADRAIN, addr(old)) != cint(0): | ||
return err(ioLastError()) | ||
res | ||
|
||
# Truncate additional bytes from buffer. | ||
buffer.setLen(bytesRead) | ||
|
||
# Trim LF in result string | ||
if len(buffer) > 0: | ||
if buffer[^1] == char(0x0A): | ||
buffer.setLen(len(buffer) - 1) | ||
buffer.add(char(0x00)) | ||
|
||
# Conversion of console input into wide characters sequence. | ||
let wres = mbstowcs(uint32, buffer) | ||
if wres.isOk(): | ||
# Trim wide character sequence to `maxChars` number of characters. | ||
var wbuffer = wres.get() | ||
if maxChars < len(wbuffer): | ||
wbuffer.setLen(maxChars) | ||
# Conversion of wide characters sequence to UTF-8 encoded string. | ||
let ures = wbuffer.utf32toUtf8() | ||
if ures.isOk(): | ||
ok(ures.get()) | ||
else: | ||
err(IoErrorCode(EILSEQ)) | ||
else: | ||
err(IoErrorCode(EILSEQ)) | ||
|
||
proc readConsolePassword*(prompt: string, | ||
maxChars = 32768): IoResult[string] = | ||
## Reads a password from stdin without printing it with length in characters | ||
## up to ``maxChars``. | ||
## | ||
## This procedure supports reading of UTF-8 encoded passwords from console or | ||
## redirected pipe. | ||
## | ||
## Before reading password ``prompt`` will be printed. | ||
## | ||
## Please note that ``maxChars`` should be in range (0, 32768]. | ||
doAssert(maxChars > 0 and maxChars <= 32768, | ||
"maxChars should be integer in (0, 32768]") | ||
? writeConsoleOutput(prompt) | ||
let res = ? readConsoleInput(maxChars) | ||
# `\p` is platform specific newline: CRLF on Windows, LF on Unix | ||
? writeConsoleOutput("\p") | ||
ok(res) | ||
|
||
when isMainModule: | ||
echo readConsolePassword("Enter password: ", 4) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like duplicating
strutils.stripLineEnd
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem of
strutils.stripLineEnd
is that it thinks that\v
and\f
are line end, but this is not a proper markers of end of line.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These characters are quite obscure nowadays, but OK - if this is a general purpose procedure for reading a line of characters from the screen, why should it preserve a page break character at the end of the line, but not a new line character in the same place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zah we are supposed to read UTF-8 encoded strings are you sure
\v
and\f
could not be part of UTF-8 encoded symbol which was cut because of buffer limits?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in UTF-8, the first 128 characters in the ASCII table are encoded with a single byte that has a leading zero bit. All other characters are encoded with a sequence of bytes where each byte has a leading bit set to 1. More details here:
https://en.wikipedia.org/wiki/UTF-8#Encoding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zah you are right, but i still think
\v
and\f
is not a proper condition for line end which we are going to receive from console. When we reading from console the only proper line end is\n
and\r\n
and nothing else,strutils
procedure is incorrect.