mirror of
https://github.com/reactos/reactos.git
synced 2024-11-19 05:22:59 +00:00
9393fc320e
Excluded: 3rd-party code (incl. wine) and most of the win32ss.
1205 lines
37 KiB
C
1205 lines
37 KiB
C
/*
|
|
* PROJECT: ReactOS More Command
|
|
* LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
|
|
* PURPOSE: Displays text stream from STDIN or from an arbitrary number
|
|
* of files to STDOUT, with screen capabilities (more than CAT,
|
|
* but less than LESS ^^).
|
|
* COPYRIGHT: Copyright 1999 Paolo Pantaleo
|
|
* Copyright 2003 Timothy Schepens
|
|
* Copyright 2016-2021 Hermes Belusca-Maito
|
|
* Copyright 2021 Katayama Hirofumi MZ
|
|
*/
|
|
/*
|
|
* MORE.C - external command.
|
|
*
|
|
* clone from 4nt more command
|
|
*
|
|
* 26 Sep 1999 - Paolo Pantaleo <paolopan@freemail.it>
|
|
* started
|
|
*
|
|
* Oct 2003 - Timothy Schepens <tischepe at fastmail dot fm>
|
|
* use window size instead of buffer size.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <windef.h>
|
|
#include <winbase.h>
|
|
#include <winnt.h>
|
|
#include <winnls.h>
|
|
#include <winreg.h>
|
|
#include <winuser.h>
|
|
|
|
#include <conutils.h>
|
|
#include <strsafe.h>
|
|
|
|
#include "resource.h"
|
|
|
|
/* PagePrompt statistics for the current file */
|
|
DWORD dwFileSize; // In bytes
|
|
DWORD dwSumReadBytes, dwSumReadChars;
|
|
// The average number of bytes per character is equal to
|
|
// dwSumReadBytes / dwSumReadChars. Note that dwSumReadChars
|
|
// will never be == 0 when ConWritePaging (and possibly PagePrompt)
|
|
// is called.
|
|
|
|
/* Handles for file and console */
|
|
HANDLE hFile = INVALID_HANDLE_VALUE;
|
|
HANDLE hStdIn, hStdOut;
|
|
HANDLE hKeyboard;
|
|
|
|
/* Enable/Disable extensions */
|
|
BOOL bEnableExtensions = TRUE; // FIXME: By default, it should be FALSE.
|
|
|
|
/* Parser flags */
|
|
#define FLAG_HELP (1 << 0)
|
|
#define FLAG_E (1 << 1)
|
|
#define FLAG_C (1 << 2)
|
|
#define FLAG_P (1 << 3)
|
|
#define FLAG_S (1 << 4)
|
|
#define FLAG_Tn (1 << 5)
|
|
#define FLAG_PLUSn (1 << 6)
|
|
|
|
/* Prompt flags */
|
|
#define PROMPT_PERCENT (1 << 0)
|
|
#define PROMPT_LINE_AT (1 << 1)
|
|
#define PROMPT_OPTIONS (1 << 2)
|
|
#define PROMPT_LINES (1 << 3)
|
|
|
|
static DWORD s_dwFlags = 0;
|
|
static LONG s_nTabWidth = 8;
|
|
static DWORD s_nNextLineNo = 0;
|
|
static BOOL s_bPrevLineIsBlank = FALSE;
|
|
static WORD s_fPrompt = 0;
|
|
static BOOL s_bDoNextFile = FALSE;
|
|
|
|
static BOOL IsBlankLine(IN PCWCH line, IN DWORD cch)
|
|
{
|
|
DWORD ich;
|
|
WORD wType;
|
|
for (ich = 0; ich < cch; ++ich)
|
|
{
|
|
/*
|
|
* Explicitly exclude FORM-FEED from the check,
|
|
* so that the pager can handle it.
|
|
*/
|
|
if (line[ich] == L'\f')
|
|
return FALSE;
|
|
|
|
/*
|
|
* Otherwise do the extended blanks check.
|
|
* Note that MS MORE.COM only checks for spaces (\x20) and TABs (\x09).
|
|
* See http://archives.miloush.net/michkap/archive/2007/06/11/3230072.html
|
|
* for more information.
|
|
*/
|
|
wType = 0;
|
|
GetStringTypeW(CT_CTYPE1, &line[ich], 1, &wType);
|
|
if (!(wType & (C1_BLANK | C1_SPACE)))
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static BOOL
|
|
__stdcall
|
|
MorePagerLine(
|
|
IN OUT PCON_PAGER Pager,
|
|
IN PCWCH line,
|
|
IN DWORD cch)
|
|
{
|
|
if (s_dwFlags & FLAG_PLUSn) /* Skip lines */
|
|
{
|
|
if (Pager->lineno < s_nNextLineNo)
|
|
{
|
|
s_bPrevLineIsBlank = FALSE;
|
|
return TRUE; /* Handled */
|
|
}
|
|
s_dwFlags &= ~FLAG_PLUSn;
|
|
}
|
|
|
|
if (s_dwFlags & FLAG_S) /* Shrink blank lines */
|
|
{
|
|
if (IsBlankLine(line, cch))
|
|
{
|
|
if (s_bPrevLineIsBlank)
|
|
return TRUE; /* Handled */
|
|
|
|
/*
|
|
* Display a single blank line, independently of the actual size
|
|
* of the current line, by displaying just one space: this is
|
|
* especially needed in order to force line wrapping when the
|
|
* ENABLE_VIRTUAL_TERMINAL_PROCESSING or DISABLE_NEWLINE_AUTO_RETURN
|
|
* console modes are enabled.
|
|
* Then, reposition the cursor to the next line, first column.
|
|
*/
|
|
if (Pager->PageColumns > 0)
|
|
ConStreamWrite(Pager->Screen->Stream, TEXT(" "), 1);
|
|
ConStreamWrite(Pager->Screen->Stream, TEXT("\n"), 1);
|
|
Pager->iLine++;
|
|
Pager->iColumn = 0;
|
|
|
|
s_bPrevLineIsBlank = TRUE;
|
|
s_nNextLineNo = 0;
|
|
|
|
return TRUE; /* Handled */
|
|
}
|
|
else
|
|
{
|
|
s_bPrevLineIsBlank = FALSE;
|
|
}
|
|
}
|
|
|
|
s_nNextLineNo = 0;
|
|
/* Not handled, let the pager do the default action */
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL
|
|
__stdcall
|
|
PagePrompt(PCON_PAGER Pager, DWORD Done, DWORD Total)
|
|
{
|
|
HANDLE hInput = ConStreamGetOSHandle(StdIn);
|
|
HANDLE hOutput = ConStreamGetOSHandle(Pager->Screen->Stream);
|
|
CONSOLE_SCREEN_BUFFER_INFO csbi;
|
|
COORD orgCursorPosition;
|
|
DWORD dwMode;
|
|
|
|
KEY_EVENT_RECORD KeyEvent;
|
|
BOOL fCtrl;
|
|
DWORD nLines;
|
|
WCHAR chSubCommand = 0;
|
|
|
|
/* Prompt strings (small size since the prompt should
|
|
* hold ideally on one <= 80-character line) */
|
|
static WCHAR StrPercent[80] = L"";
|
|
static WCHAR StrLineAt[80] = L"";
|
|
static WCHAR StrOptions[80] = L"";
|
|
static WCHAR StrLines[80] = L"";
|
|
|
|
WCHAR szPercent[80] = L"";
|
|
WCHAR szLineAt[80] = L"";
|
|
|
|
/* Load the prompt strings */
|
|
if (!*StrPercent)
|
|
K32LoadStringW(NULL, IDS_CONTINUE_PERCENT, StrPercent, ARRAYSIZE(StrPercent));
|
|
if (!*StrLineAt)
|
|
K32LoadStringW(NULL, IDS_CONTINUE_LINE_AT, StrLineAt, ARRAYSIZE(StrLineAt));
|
|
if (!*StrOptions)
|
|
K32LoadStringW(NULL, IDS_CONTINUE_OPTIONS, StrOptions, ARRAYSIZE(StrOptions));
|
|
if (!*StrLines)
|
|
K32LoadStringW(NULL, IDS_CONTINUE_LINES, StrLines, ARRAYSIZE(StrLines));
|
|
|
|
/*
|
|
* Check whether the pager is prompting, but we have actually finished
|
|
* to display a given file, or no data is present in STDIN anymore.
|
|
* In this case, skip the prompt altogether. The only exception is when
|
|
* we are displaying other files.
|
|
*/
|
|
// TODO: Implement!
|
|
|
|
Restart:
|
|
nLines = 0;
|
|
|
|
/* Do not show the progress percentage when STDIN is being displayed */
|
|
if (s_fPrompt & PROMPT_PERCENT) // && (hFile != hStdIn)
|
|
{
|
|
/*
|
|
* The progress percentage is evaluated as follows.
|
|
* So far we have read a total of 'dwSumReadBytes' bytes from the file.
|
|
* Amongst those is the latest read chunk of 'dwReadBytes' bytes, to which
|
|
* correspond a number of 'dwReadChars' characters with which we have called
|
|
* ConWritePaging who called PagePrompt. We then have: Total == dwReadChars.
|
|
* During this ConWritePaging call the PagePrompt was called after 'Done'
|
|
* number of characters over 'Total'.
|
|
* It should be noted that for 'dwSumReadBytes' number of bytes read it
|
|
* *roughly* corresponds 'dwSumReadChars' number of characters. This is
|
|
* because there may be some failures happening during the conversion of
|
|
* the bytes read to the character string for a given encoding.
|
|
* Therefore the number of characters displayed on screen is equal to:
|
|
* dwSumReadChars - Total + Done ,
|
|
* but the best corresponding approximed number of bytes would be:
|
|
* dwSumReadBytes - (Total - Done) * (dwSumReadBytes / dwSumReadChars) ,
|
|
* where the ratio is the average number of bytes per character.
|
|
* The percentage is then computed relative to the total file size.
|
|
*/
|
|
DWORD dwPercent = (dwSumReadBytes - (Total - Done) *
|
|
(dwSumReadBytes / dwSumReadChars)) * 100 / dwFileSize;
|
|
StringCchPrintfW(szPercent, ARRAYSIZE(szPercent), StrPercent, dwPercent);
|
|
}
|
|
if (s_fPrompt & PROMPT_LINE_AT)
|
|
{
|
|
StringCchPrintfW(szLineAt, ARRAYSIZE(szLineAt), StrLineAt, Pager->lineno);
|
|
}
|
|
|
|
/* Suitably format and display the prompt */
|
|
ConResMsgPrintf(Pager->Screen->Stream, 0, IDS_CONTINUE_PROMPT,
|
|
(s_fPrompt & PROMPT_PERCENT ? szPercent : L""),
|
|
(s_fPrompt & PROMPT_LINE_AT ? szLineAt : L""),
|
|
(s_fPrompt & PROMPT_OPTIONS ? StrOptions : L""),
|
|
(s_fPrompt & PROMPT_LINES ? StrLines : L""));
|
|
|
|
/* Reset the prompt to a default state */
|
|
s_fPrompt &= ~(PROMPT_LINE_AT | PROMPT_OPTIONS | PROMPT_LINES);
|
|
|
|
/* RemoveBreakHandler */
|
|
SetConsoleCtrlHandler(NULL, TRUE);
|
|
/* ConInDisable */
|
|
GetConsoleMode(hInput, &dwMode);
|
|
dwMode &= ~ENABLE_PROCESSED_INPUT;
|
|
SetConsoleMode(hInput, dwMode);
|
|
|
|
// FIXME: Does not support TTY yet!
|
|
ConGetScreenInfo(Pager->Screen, &csbi);
|
|
orgCursorPosition = csbi.dwCursorPosition;
|
|
for (;;)
|
|
{
|
|
INPUT_RECORD ir = {0};
|
|
DWORD dwRead;
|
|
WCHAR ch;
|
|
|
|
do
|
|
{
|
|
ReadConsoleInput(hInput, &ir, 1, &dwRead);
|
|
}
|
|
while ((ir.EventType != KEY_EVENT) || (!ir.Event.KeyEvent.bKeyDown));
|
|
|
|
/* Got our key */
|
|
KeyEvent = ir.Event.KeyEvent;
|
|
|
|
/* Ignore any unsupported keyboard press */
|
|
if ((KeyEvent.wVirtualKeyCode == VK_SHIFT) ||
|
|
(KeyEvent.wVirtualKeyCode == VK_MENU) ||
|
|
(KeyEvent.wVirtualKeyCode == VK_CONTROL))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* Ctrl key is pressed? */
|
|
fCtrl = !!(KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED));
|
|
|
|
/* Ctrl+C or Ctrl+Esc? */
|
|
if (fCtrl && ((KeyEvent.wVirtualKeyCode == VK_ESCAPE) ||
|
|
(KeyEvent.wVirtualKeyCode == L'C')))
|
|
{
|
|
chSubCommand = 0;
|
|
break;
|
|
}
|
|
|
|
/* If extended features are unavailable, or no
|
|
* pending commands, don't do more processing. */
|
|
if (!(s_dwFlags & FLAG_E) || (chSubCommand == 0))
|
|
break;
|
|
|
|
ch = KeyEvent.uChar.UnicodeChar;
|
|
if (L'0' <= ch && ch <= L'9')
|
|
{
|
|
nLines *= 10;
|
|
nLines += ch - L'0';
|
|
ConStreamWrite(Pager->Screen->Stream, &ch, 1);
|
|
continue;
|
|
}
|
|
else if (KeyEvent.wVirtualKeyCode == VK_RETURN)
|
|
{
|
|
/* Validate the line number */
|
|
break;
|
|
}
|
|
else if (KeyEvent.wVirtualKeyCode == VK_ESCAPE)
|
|
{
|
|
/* Cancel the current command */
|
|
chSubCommand = 0;
|
|
break;
|
|
}
|
|
else if (KeyEvent.wVirtualKeyCode == VK_BACK)
|
|
{
|
|
if (nLines != 0)
|
|
nLines /= 10;
|
|
|
|
/* Erase the current character */
|
|
ConGetScreenInfo(Pager->Screen, &csbi);
|
|
if ( (csbi.dwCursorPosition.Y > orgCursorPosition.Y) ||
|
|
((csbi.dwCursorPosition.Y == orgCursorPosition.Y) &&
|
|
(csbi.dwCursorPosition.X > orgCursorPosition.X)) )
|
|
{
|
|
if (csbi.dwCursorPosition.X > 0)
|
|
{
|
|
csbi.dwCursorPosition.X = csbi.dwCursorPosition.X - 1;
|
|
}
|
|
else if (csbi.dwCursorPosition.Y > 0)
|
|
{
|
|
csbi.dwCursorPosition.Y = csbi.dwCursorPosition.Y - 1;
|
|
csbi.dwCursorPosition.X = (csbi.dwSize.X ? csbi.dwSize.X - 1 : 0);
|
|
}
|
|
|
|
SetConsoleCursorPosition(hOutput, csbi.dwCursorPosition);
|
|
|
|
ch = L' ';
|
|
ConStreamWrite(Pager->Screen->Stream, &ch, 1);
|
|
SetConsoleCursorPosition(hOutput, csbi.dwCursorPosition);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* AddBreakHandler */
|
|
SetConsoleCtrlHandler(NULL, FALSE);
|
|
/* ConInEnable */
|
|
GetConsoleMode(hInput, &dwMode);
|
|
dwMode |= ENABLE_PROCESSED_INPUT;
|
|
SetConsoleMode(hInput, dwMode);
|
|
|
|
/* Refresh the screen information, as the console may have been
|
|
* redimensioned. Update also the default number of lines to scroll. */
|
|
ConGetScreenInfo(Pager->Screen, &csbi);
|
|
Pager->ScrollRows = csbi.srWindow.Bottom - csbi.srWindow.Top;
|
|
|
|
/*
|
|
* Erase the full line where the cursor is, and move
|
|
* the cursor back to the beginning of the line.
|
|
*/
|
|
ConClearLine(Pager->Screen->Stream);
|
|
|
|
/* Ctrl+C or Ctrl+Esc: Control Break */
|
|
if (fCtrl && ((KeyEvent.wVirtualKeyCode == VK_ESCAPE) ||
|
|
(KeyEvent.wVirtualKeyCode == L'C')))
|
|
{
|
|
/* We break, output a newline */
|
|
WCHAR ch = L'\n';
|
|
ConStreamWrite(Pager->Screen->Stream, &ch, 1);
|
|
return FALSE;
|
|
}
|
|
|
|
switch (chSubCommand)
|
|
{
|
|
case L'P':
|
|
{
|
|
/* If we don't display other lines, just restart the prompt */
|
|
if (nLines == 0)
|
|
{
|
|
chSubCommand = 0;
|
|
goto Restart;
|
|
}
|
|
/* Otherwise tell the pager to display them */
|
|
Pager->ScrollRows = nLines;
|
|
return TRUE;
|
|
}
|
|
case L'S':
|
|
{
|
|
s_dwFlags |= FLAG_PLUSn;
|
|
s_nNextLineNo = Pager->lineno + nLines;
|
|
/* Use the default Pager->ScrollRows value */
|
|
return TRUE;
|
|
}
|
|
default:
|
|
chSubCommand = 0;
|
|
break;
|
|
}
|
|
|
|
/* If extended features are available */
|
|
if (s_dwFlags & FLAG_E)
|
|
{
|
|
/* Ignore any key presses if Ctrl is pressed */
|
|
if (fCtrl)
|
|
{
|
|
chSubCommand = 0;
|
|
goto Restart;
|
|
}
|
|
|
|
/* 'Q': Quit */
|
|
if (KeyEvent.wVirtualKeyCode == L'Q')
|
|
{
|
|
/* We break, output a newline */
|
|
WCHAR ch = L'\n';
|
|
ConStreamWrite(Pager->Screen->Stream, &ch, 1);
|
|
return FALSE;
|
|
}
|
|
|
|
/* 'F': Next file */
|
|
if (KeyEvent.wVirtualKeyCode == L'F')
|
|
{
|
|
s_bDoNextFile = TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
/* '?': Show Options */
|
|
if (KeyEvent.uChar.UnicodeChar == L'?')
|
|
{
|
|
s_fPrompt |= PROMPT_OPTIONS;
|
|
goto Restart;
|
|
}
|
|
|
|
/* [Enter] key: Display one line */
|
|
if (KeyEvent.wVirtualKeyCode == VK_RETURN)
|
|
{
|
|
Pager->ScrollRows = 1;
|
|
return TRUE;
|
|
}
|
|
|
|
/* [Space] key: Display one page */
|
|
if (KeyEvent.wVirtualKeyCode == VK_SPACE)
|
|
{
|
|
if (s_dwFlags & FLAG_C)
|
|
{
|
|
/* Clear the screen */
|
|
ConClearScreen(Pager->Screen);
|
|
}
|
|
/* Use the default Pager->ScrollRows value */
|
|
return TRUE;
|
|
}
|
|
|
|
/* 'P': Display n lines */
|
|
if (KeyEvent.wVirtualKeyCode == L'P')
|
|
{
|
|
s_fPrompt |= PROMPT_LINES;
|
|
chSubCommand = L'P';
|
|
goto Restart;
|
|
}
|
|
|
|
/* 'S': Skip n lines */
|
|
if (KeyEvent.wVirtualKeyCode == L'S')
|
|
{
|
|
s_fPrompt |= PROMPT_LINES;
|
|
chSubCommand = L'S';
|
|
goto Restart;
|
|
}
|
|
|
|
/* '=': Show current line number */
|
|
if (KeyEvent.uChar.UnicodeChar == L'=')
|
|
{
|
|
s_fPrompt |= PROMPT_LINE_AT;
|
|
goto Restart;
|
|
}
|
|
|
|
chSubCommand = 0;
|
|
goto Restart;
|
|
}
|
|
else
|
|
{
|
|
/* Extended features are unavailable: display one page */
|
|
/* Use the default Pager->ScrollRows value */
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* See base/applications/cmdutils/clip/clip.c!IsDataUnicode()
|
|
* and base/applications/notepad/text.c!ReadText() for more details.
|
|
* Also some good code example can be found at:
|
|
* https://github.com/AutoIt/text-encoding-detect
|
|
*/
|
|
typedef enum
|
|
{
|
|
ENCODING_ANSI = 0,
|
|
ENCODING_UTF16LE = 1,
|
|
ENCODING_UTF16BE = 2,
|
|
ENCODING_UTF8 = 3
|
|
} ENCODING;
|
|
|
|
static BOOL
|
|
IsDataUnicode(
|
|
IN PVOID Buffer,
|
|
IN DWORD BufferSize,
|
|
OUT ENCODING* Encoding OPTIONAL,
|
|
OUT PDWORD SkipBytes OPTIONAL)
|
|
{
|
|
PBYTE pBytes = Buffer;
|
|
ENCODING encFile = ENCODING_ANSI;
|
|
DWORD dwPos = 0;
|
|
|
|
/*
|
|
* See http://archives.miloush.net/michkap/archive/2007/04/22/2239345.html
|
|
* for more details about the algorithm and the pitfalls behind it.
|
|
* Of course it would be actually great to make a nice function that
|
|
* would work, once and for all, and put it into a library.
|
|
*/
|
|
|
|
/* Look for Byte Order Marks */
|
|
if ((BufferSize >= 2) && (pBytes[0] == 0xFF) && (pBytes[1] == 0xFE))
|
|
{
|
|
encFile = ENCODING_UTF16LE;
|
|
dwPos = 2;
|
|
}
|
|
else if ((BufferSize >= 2) && (pBytes[0] == 0xFE) && (pBytes[1] == 0xFF))
|
|
{
|
|
encFile = ENCODING_UTF16BE;
|
|
dwPos = 2;
|
|
}
|
|
else if ((BufferSize >= 3) && (pBytes[0] == 0xEF) && (pBytes[1] == 0xBB) && (pBytes[2] == 0xBF))
|
|
{
|
|
encFile = ENCODING_UTF8;
|
|
dwPos = 3;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Try using statistical analysis. Do not rely on the return value of
|
|
* IsTextUnicode as we can get FALSE even if the text is in UTF-16 BE
|
|
* (i.e. we have some of the IS_TEXT_UNICODE_REVERSE_MASK bits set).
|
|
* Instead, set all the tests we want to perform, then just check
|
|
* the passed tests and try to deduce the string properties.
|
|
*/
|
|
|
|
/*
|
|
* This mask contains the 3 highest bits from IS_TEXT_UNICODE_NOT_ASCII_MASK
|
|
* and the 1st highest bit from IS_TEXT_UNICODE_NOT_UNICODE_MASK.
|
|
*/
|
|
#define IS_TEXT_UNKNOWN_FLAGS_MASK ((7 << 13) | (1 << 11))
|
|
|
|
/* Flag out the unknown flags here, the passed tests will not have them either */
|
|
INT Tests = (IS_TEXT_UNICODE_NOT_ASCII_MASK |
|
|
IS_TEXT_UNICODE_NOT_UNICODE_MASK |
|
|
IS_TEXT_UNICODE_REVERSE_MASK | IS_TEXT_UNICODE_UNICODE_MASK)
|
|
& ~IS_TEXT_UNKNOWN_FLAGS_MASK;
|
|
INT Results;
|
|
|
|
IsTextUnicode(Buffer, BufferSize, &Tests);
|
|
Results = Tests;
|
|
|
|
/*
|
|
* As the IS_TEXT_UNICODE_NULL_BYTES or IS_TEXT_UNICODE_ILLEGAL_CHARS
|
|
* flags are expected to be potentially present in the result without
|
|
* modifying our expectations, filter them out now.
|
|
*/
|
|
Results &= ~(IS_TEXT_UNICODE_NULL_BYTES | IS_TEXT_UNICODE_ILLEGAL_CHARS);
|
|
|
|
/*
|
|
* NOTE: The flags IS_TEXT_UNICODE_ASCII16 and
|
|
* IS_TEXT_UNICODE_REVERSE_ASCII16 are not reliable.
|
|
*
|
|
* NOTE2: Check for potential "bush hid the facts" effect by also
|
|
* checking the original results (in 'Tests') for the absence of
|
|
* the IS_TEXT_UNICODE_NULL_BYTES flag, as we may presumably expect
|
|
* that in UTF-16 text there will be at some point some NULL bytes.
|
|
* If not, fall back to ANSI. This shows the limitations of using the
|
|
* IsTextUnicode API to perform such tests, and the usage of a more
|
|
* improved encoding detection algorithm would be really welcome.
|
|
*/
|
|
if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) &&
|
|
!(Results & IS_TEXT_UNICODE_REVERSE_MASK) &&
|
|
(Results & IS_TEXT_UNICODE_UNICODE_MASK) &&
|
|
(Tests & IS_TEXT_UNICODE_NULL_BYTES))
|
|
{
|
|
encFile = ENCODING_UTF16LE;
|
|
dwPos = (Results & IS_TEXT_UNICODE_SIGNATURE) ? 2 : 0;
|
|
}
|
|
else
|
|
if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) &&
|
|
!(Results & IS_TEXT_UNICODE_UNICODE_MASK) &&
|
|
(Results & IS_TEXT_UNICODE_REVERSE_MASK) &&
|
|
(Tests & IS_TEXT_UNICODE_NULL_BYTES))
|
|
{
|
|
encFile = ENCODING_UTF16BE;
|
|
dwPos = (Results & IS_TEXT_UNICODE_REVERSE_SIGNATURE) ? 2 : 0;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Either 'Results' has neither of those masks set, as it can be
|
|
* the case for UTF-8 text (or ANSI), or it has both as can be the
|
|
* case when analysing pure binary data chunk. This is therefore
|
|
* invalid and we fall back to ANSI encoding.
|
|
* FIXME: In case of failure, assume ANSI (as long as we do not have
|
|
* correct tests for UTF8, otherwise we should do them, and at the
|
|
* very end, assume ANSI).
|
|
*/
|
|
encFile = ENCODING_ANSI; // ENCODING_UTF8;
|
|
dwPos = 0;
|
|
}
|
|
}
|
|
|
|
if (Encoding)
|
|
*Encoding = encFile;
|
|
if (SkipBytes)
|
|
*SkipBytes = dwPos;
|
|
|
|
return (encFile != ENCODING_ANSI);
|
|
}
|
|
|
|
/*
|
|
* Adapted from base/shell/cmd/misc.c!FileGetString(), but with correct
|
|
* text encoding support. Also please note that similar code should be
|
|
* also used in the CMD.EXE 'TYPE' command.
|
|
* Contrary to CMD's FileGetString() we do not stop at new-lines.
|
|
*
|
|
* Read text data from a file and convert it from a given encoding to UTF-16.
|
|
*
|
|
* IN OUT PVOID pCacheBuffer and IN DWORD CacheBufferLength :
|
|
* Implementation detail so that the function uses an external user-provided
|
|
* buffer to store the data temporarily read from the file. The function
|
|
* could have used an internal buffer instead. The length is in number of bytes.
|
|
*
|
|
* IN OUT PWSTR* pBuffer and IN OUT PDWORD pnBufferLength :
|
|
* Reallocated buffer containing the string data converted to UTF-16.
|
|
* In input, contains a pointer to the original buffer and its length.
|
|
* In output, contains a pointer to the reallocated buffer and its length.
|
|
* The length is in number of characters.
|
|
*
|
|
* At first call to this function, pBuffer can be set to NULL, in which case
|
|
* when the function returns the pointer will point to a valid buffer.
|
|
* After the last call to this function, free the pBuffer pointer with:
|
|
* HeapFree(GetProcessHeap(), 0, *pBuffer);
|
|
*
|
|
* If Encoding is set to ENCODING_UTF16LE or ENCODING_UTF16BE, since we are
|
|
* compiled in UNICODE, no extra conversion is performed and therefore
|
|
* pBuffer is unused (remains unallocated) and one can directly use the
|
|
* contents of pCacheBuffer as it is expected to contain valid UTF-16 text.
|
|
*
|
|
* OUT PDWORD pdwReadBytes : Number of bytes read from the file (optional).
|
|
* OUT PDWORD pdwReadChars : Corresponding number of characters read (optional).
|
|
*/
|
|
static BOOL
|
|
FileGetString(
|
|
IN HANDLE hFile,
|
|
IN ENCODING Encoding,
|
|
IN OUT PVOID pCacheBuffer,
|
|
IN DWORD CacheBufferLength,
|
|
IN OUT PWCHAR* pBuffer,
|
|
IN OUT PDWORD pnBufferLength,
|
|
OUT PDWORD pdwReadBytes OPTIONAL,
|
|
OUT PDWORD pdwReadChars OPTIONAL)
|
|
{
|
|
BOOL Success;
|
|
UINT CodePage = (UINT)-1;
|
|
DWORD dwReadBytes;
|
|
INT len;
|
|
|
|
// ASSERT(pCacheBuffer && (CacheBufferLength > 0));
|
|
// ASSERT(CacheBufferLength % 2 == 0); // Cache buffer length MUST BE even!
|
|
// ASSERT(pBuffer && pnBufferLength);
|
|
|
|
/* Always reset the retrieved number of bytes/characters */
|
|
if (pdwReadBytes) *pdwReadBytes = 0;
|
|
if (pdwReadChars) *pdwReadChars = 0;
|
|
|
|
Success = ReadFile(hFile, pCacheBuffer, CacheBufferLength, &dwReadBytes, NULL);
|
|
if (!Success || dwReadBytes == 0)
|
|
return FALSE;
|
|
|
|
if (pdwReadBytes) *pdwReadBytes = dwReadBytes;
|
|
|
|
if ((Encoding == ENCODING_ANSI) || (Encoding == ENCODING_UTF8))
|
|
{
|
|
/* Conversion is needed */
|
|
|
|
if (Encoding == ENCODING_ANSI)
|
|
CodePage = GetConsoleCP(); // CP_ACP; // FIXME: Cache GetConsoleCP() value.
|
|
else // if (Encoding == ENCODING_UTF8)
|
|
CodePage = CP_UTF8;
|
|
|
|
/* Retrieve the needed buffer size */
|
|
len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes,
|
|
NULL, 0);
|
|
if (len == 0)
|
|
{
|
|
/* Failure, bail out */
|
|
return FALSE;
|
|
}
|
|
|
|
/* Initialize the conversion buffer if needed... */
|
|
if (*pBuffer == NULL)
|
|
{
|
|
*pnBufferLength = len;
|
|
*pBuffer = HeapAlloc(GetProcessHeap(), 0, *pnBufferLength * sizeof(WCHAR));
|
|
if (*pBuffer == NULL)
|
|
{
|
|
// *pBuffer = NULL;
|
|
*pnBufferLength = 0;
|
|
// WARN("DEBUG: Cannot allocate memory for *pBuffer!\n");
|
|
// ConErrFormatMessage(GetLastError());
|
|
return FALSE;
|
|
}
|
|
}
|
|
/* ... or reallocate only if the new length is greater than the old one */
|
|
else if (len > *pnBufferLength)
|
|
{
|
|
PWSTR OldBuffer = *pBuffer;
|
|
|
|
*pnBufferLength = len;
|
|
*pBuffer = HeapReAlloc(GetProcessHeap(), 0, *pBuffer, *pnBufferLength * sizeof(WCHAR));
|
|
if (*pBuffer == NULL)
|
|
{
|
|
/* Do not leak old buffer */
|
|
HeapFree(GetProcessHeap(), 0, OldBuffer);
|
|
// *pBuffer = NULL;
|
|
*pnBufferLength = 0;
|
|
// WARN("DEBUG: Cannot reallocate memory for *pBuffer!\n");
|
|
// ConErrFormatMessage(GetLastError());
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* Now perform the conversion proper */
|
|
len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes,
|
|
*pBuffer, len);
|
|
dwReadBytes = len;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* No conversion needed, just convert from big to little endian if needed.
|
|
* pBuffer and pnBufferLength are left untouched and pCacheBuffer can be
|
|
* directly used.
|
|
*/
|
|
PWCHAR pWChars = pCacheBuffer;
|
|
DWORD i;
|
|
|
|
dwReadBytes /= sizeof(WCHAR);
|
|
|
|
if (Encoding == ENCODING_UTF16BE)
|
|
{
|
|
for (i = 0; i < dwReadBytes; i++)
|
|
{
|
|
/* Equivalent to RtlUshortByteSwap: reverse high/low bytes */
|
|
pWChars[i] = MAKEWORD(HIBYTE(pWChars[i]), LOBYTE(pWChars[i]));
|
|
}
|
|
}
|
|
// else if (Encoding == ENCODING_UTF16LE), we are good, nothing to do.
|
|
}
|
|
|
|
/* Return the number of characters (dwReadBytes is converted) */
|
|
if (pdwReadChars) *pdwReadChars = dwReadBytes;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static VOID
|
|
LoadRegistrySettings(HKEY hKeyRoot)
|
|
{
|
|
LONG lRet;
|
|
HKEY hKey;
|
|
DWORD dwType, len;
|
|
/*
|
|
* Buffer big enough to hold the string L"4294967295",
|
|
* corresponding to the literal 0xFFFFFFFF (MAXULONG) in decimal.
|
|
*/
|
|
WCHAR Buffer[sizeof("4294967295")];
|
|
C_ASSERT(sizeof(Buffer) >= sizeof(DWORD));
|
|
|
|
lRet = RegOpenKeyExW(hKeyRoot,
|
|
L"Software\\Microsoft\\Command Processor",
|
|
0,
|
|
KEY_QUERY_VALUE,
|
|
&hKey);
|
|
if (lRet != ERROR_SUCCESS)
|
|
return;
|
|
|
|
len = sizeof(Buffer);
|
|
lRet = RegQueryValueExW(hKey,
|
|
L"EnableExtensions",
|
|
NULL,
|
|
&dwType,
|
|
(PBYTE)&Buffer,
|
|
&len);
|
|
if (lRet == ERROR_SUCCESS)
|
|
{
|
|
/* Overwrite the default setting */
|
|
if (dwType == REG_DWORD)
|
|
bEnableExtensions = !!*(PDWORD)Buffer;
|
|
else if (dwType == REG_SZ)
|
|
bEnableExtensions = (_wtol((PWSTR)Buffer) == 1);
|
|
}
|
|
// else, use the default setting set globally.
|
|
|
|
RegCloseKey(hKey);
|
|
}
|
|
|
|
static BOOL IsFlag(PCWSTR param)
|
|
{
|
|
PCWSTR pch;
|
|
PWCHAR endptr;
|
|
|
|
if (param[0] == L'/')
|
|
return TRUE;
|
|
|
|
if (param[0] == L'+')
|
|
{
|
|
pch = param + 1;
|
|
if (*pch)
|
|
{
|
|
(void)wcstol(pch, &endptr, 10);
|
|
return (*endptr == 0);
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL ParseArgument(PCWSTR arg, BOOL* pbHasFiles)
|
|
{
|
|
PWCHAR endptr;
|
|
|
|
if (arg[0] == L'/')
|
|
{
|
|
switch (towupper(arg[1]))
|
|
{
|
|
case L'?':
|
|
if (arg[2] == 0)
|
|
{
|
|
s_dwFlags |= FLAG_HELP;
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case L'E':
|
|
if (arg[2] == 0)
|
|
{
|
|
s_dwFlags |= FLAG_E;
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case L'C':
|
|
if (arg[2] == 0)
|
|
{
|
|
s_dwFlags |= FLAG_C;
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case L'P':
|
|
if (arg[2] == 0)
|
|
{
|
|
s_dwFlags |= FLAG_P;
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case L'S':
|
|
if (arg[2] == 0)
|
|
{
|
|
s_dwFlags |= FLAG_S;
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case L'T':
|
|
if (arg[2] != 0)
|
|
{
|
|
s_dwFlags |= FLAG_Tn;
|
|
s_nTabWidth = wcstol(&arg[2], &endptr, 10);
|
|
if (*endptr == 0)
|
|
return TRUE;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (arg[0] == L'+')
|
|
{
|
|
if (arg[1] != 0)
|
|
{
|
|
s_dwFlags |= FLAG_PLUSn;
|
|
s_nNextLineNo = wcstol(&arg[1], &endptr, 10) + 1;
|
|
if (*endptr == 0)
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
if (IsFlag(arg))
|
|
{
|
|
ConResPrintf(StdErr, IDS_BAD_FLAG, arg);
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
*pbHasFiles = TRUE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static BOOL ParseMoreVariable(BOOL* pbHasFiles)
|
|
{
|
|
BOOL ret = TRUE;
|
|
PWSTR psz;
|
|
PWCHAR pch;
|
|
DWORD cch;
|
|
|
|
cch = GetEnvironmentVariableW(L"MORE", NULL, 0);
|
|
if (cch == 0)
|
|
return TRUE;
|
|
|
|
psz = (PWSTR)malloc((cch + 1) * sizeof(WCHAR));
|
|
if (!psz)
|
|
return TRUE;
|
|
|
|
if (!GetEnvironmentVariableW(L"MORE", psz, cch + 1))
|
|
{
|
|
free(psz);
|
|
return TRUE;
|
|
}
|
|
|
|
for (pch = wcstok(psz, L" "); pch; pch = wcstok(NULL, L" "))
|
|
{
|
|
ret = ParseArgument(pch, pbHasFiles);
|
|
if (!ret)
|
|
break;
|
|
}
|
|
|
|
free(psz);
|
|
return ret;
|
|
}
|
|
|
|
// INT CommandMore(LPTSTR cmd, LPTSTR param)
|
|
int wmain(int argc, WCHAR* argv[])
|
|
{
|
|
// FIXME this stuff!
|
|
CON_SCREEN Screen = {StdOut};
|
|
CON_PAGER Pager = {&Screen, 0};
|
|
|
|
int i;
|
|
|
|
BOOL bRet, bContinue;
|
|
|
|
ENCODING Encoding;
|
|
DWORD SkipBytes = 0;
|
|
BOOL HasFiles;
|
|
|
|
#define FileCacheBufferSize 4096
|
|
PVOID FileCacheBuffer = NULL;
|
|
PWCHAR StringBuffer = NULL;
|
|
DWORD StringBufferLength = 0;
|
|
DWORD dwReadBytes = 0, dwReadChars = 0;
|
|
|
|
TCHAR szFullPath[MAX_PATH];
|
|
|
|
hStdIn = GetStdHandle(STD_INPUT_HANDLE);
|
|
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
/* Initialize the Console Standard Streams */
|
|
ConStreamInit(StdIn , GetStdHandle(STD_INPUT_HANDLE) , UTF8Text, INVALID_CP);
|
|
ConStreamInit(StdOut, GetStdHandle(STD_OUTPUT_HANDLE), UTF8Text, INVALID_CP);
|
|
ConStreamInit(StdErr, GetStdHandle(STD_ERROR_HANDLE) , UTF8Text, INVALID_CP);
|
|
|
|
/*
|
|
* Bad usage (too much options) or we use the /? switch.
|
|
* Display help for the MORE command.
|
|
*/
|
|
if (argc > 1 && wcscmp(argv[1], L"/?") == 0)
|
|
{
|
|
ConResPuts(StdOut, IDS_USAGE);
|
|
return 0;
|
|
}
|
|
|
|
/* Load the registry settings */
|
|
LoadRegistrySettings(HKEY_LOCAL_MACHINE);
|
|
LoadRegistrySettings(HKEY_CURRENT_USER);
|
|
if (bEnableExtensions)
|
|
s_dwFlags |= FLAG_E;
|
|
|
|
// NOTE: We might try to duplicate the ConOut for read access... ?
|
|
hKeyboard = CreateFileW(L"CONIN$", GENERIC_READ|GENERIC_WRITE,
|
|
FILE_SHARE_READ|FILE_SHARE_WRITE, NULL,
|
|
OPEN_EXISTING, 0, NULL);
|
|
FlushConsoleInputBuffer(hKeyboard);
|
|
ConStreamSetOSHandle(StdIn, hKeyboard);
|
|
|
|
FileCacheBuffer = HeapAlloc(GetProcessHeap(), 0, FileCacheBufferSize);
|
|
if (!FileCacheBuffer)
|
|
{
|
|
ConPuts(StdErr, L"Error: no memory\n");
|
|
CloseHandle(hKeyboard);
|
|
return 1;
|
|
}
|
|
|
|
/* First, load the "MORE" environment variable and parse it,
|
|
* then parse the command-line parameters. */
|
|
HasFiles = FALSE;
|
|
if (!ParseMoreVariable(&HasFiles))
|
|
return 1;
|
|
for (i = 1; i < argc; i++)
|
|
{
|
|
if (!ParseArgument(argv[i], &HasFiles))
|
|
return 1;
|
|
}
|
|
|
|
if (s_dwFlags & FLAG_HELP)
|
|
{
|
|
ConResPuts(StdOut, IDS_USAGE);
|
|
return 0;
|
|
}
|
|
|
|
Pager.PagerLine = MorePagerLine;
|
|
Pager.dwFlags |= CON_PAGER_EXPAND_TABS | CON_PAGER_CACHE_INCOMPLETE_LINE;
|
|
if (s_dwFlags & FLAG_P)
|
|
Pager.dwFlags |= CON_PAGER_EXPAND_FF;
|
|
Pager.nTabWidth = s_nTabWidth;
|
|
|
|
/* Special case where we run 'MORE' without any argument: we use STDIN */
|
|
if (!HasFiles)
|
|
{
|
|
/*
|
|
* Assign STDIN handle to hFile so that the page prompt function will
|
|
* know the data comes from STDIN, and will take different actions.
|
|
*/
|
|
hFile = hStdIn;
|
|
|
|
/* Update the statistics for PagePrompt */
|
|
dwFileSize = 0;
|
|
dwSumReadBytes = dwSumReadChars = 0;
|
|
|
|
/* We suppose we read text from the file */
|
|
|
|
/* For STDIN we always suppose we are in ANSI mode */
|
|
// SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
|
|
Encoding = ENCODING_ANSI; // ENCODING_UTF8;
|
|
|
|
/* Start paging */
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0);
|
|
if (!bContinue)
|
|
goto Quit;
|
|
|
|
do
|
|
{
|
|
bRet = FileGetString(hFile, Encoding,
|
|
FileCacheBuffer, FileCacheBufferSize,
|
|
&StringBuffer, &StringBufferLength,
|
|
&dwReadBytes, &dwReadChars);
|
|
if (!bRet || dwReadBytes == 0 || dwReadChars == 0)
|
|
{
|
|
/* We failed at reading the file, bail out */
|
|
break;
|
|
}
|
|
|
|
/* Update the statistics for PagePrompt */
|
|
dwSumReadBytes += dwReadBytes;
|
|
dwSumReadChars += dwReadChars;
|
|
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
|
|
StringBuffer, dwReadChars);
|
|
/* If we Ctrl-C/Ctrl-Break, stop everything */
|
|
if (!bContinue)
|
|
break;
|
|
}
|
|
while (bRet && dwReadBytes > 0);
|
|
|
|
/* Flush any cached pager buffers */
|
|
if (bContinue)
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0);
|
|
|
|
goto Quit;
|
|
}
|
|
|
|
/* We have files: read them and output them to STDOUT */
|
|
for (i = 1; i < argc; i++)
|
|
{
|
|
if (IsFlag(argv[i]))
|
|
continue;
|
|
|
|
GetFullPathNameW(argv[i], ARRAYSIZE(szFullPath), szFullPath, NULL);
|
|
hFile = CreateFileW(szFullPath,
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ,
|
|
NULL,
|
|
OPEN_EXISTING,
|
|
0, // FILE_ATTRIBUTE_NORMAL,
|
|
NULL);
|
|
if (hFile == INVALID_HANDLE_VALUE)
|
|
{
|
|
ConResPrintf(StdErr, IDS_FILE_ACCESS, szFullPath);
|
|
goto Quit;
|
|
}
|
|
|
|
/* We currently do not support files too big */
|
|
dwFileSize = GetFileSize(hFile, NULL);
|
|
if (dwFileSize == INVALID_FILE_SIZE)
|
|
{
|
|
ConPuts(StdErr, L"ERROR: Invalid file size!\n");
|
|
CloseHandle(hFile);
|
|
continue;
|
|
}
|
|
|
|
/* We suppose we read text from the file */
|
|
|
|
/* Check whether the file is UNICODE and retrieve its encoding */
|
|
SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
|
|
bRet = ReadFile(hFile, FileCacheBuffer, FileCacheBufferSize, &dwReadBytes, NULL);
|
|
IsDataUnicode(FileCacheBuffer, dwReadBytes, &Encoding, &SkipBytes);
|
|
SetFilePointer(hFile, SkipBytes, NULL, FILE_BEGIN);
|
|
|
|
/* Reset state for paging */
|
|
s_nNextLineNo = 0;
|
|
s_bPrevLineIsBlank = FALSE;
|
|
s_fPrompt = PROMPT_PERCENT;
|
|
s_bDoNextFile = FALSE;
|
|
|
|
/* Update the statistics for PagePrompt */
|
|
dwSumReadBytes = dwSumReadChars = 0;
|
|
|
|
/* Start paging */
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0);
|
|
if (!bContinue)
|
|
{
|
|
/* We stop displaying this file */
|
|
CloseHandle(hFile);
|
|
if (s_bDoNextFile)
|
|
{
|
|
/* Bail out and continue with the other files */
|
|
continue;
|
|
}
|
|
|
|
/* We Ctrl-C/Ctrl-Break, stop everything */
|
|
goto Quit;
|
|
}
|
|
|
|
do
|
|
{
|
|
bRet = FileGetString(hFile, Encoding,
|
|
FileCacheBuffer, FileCacheBufferSize,
|
|
&StringBuffer, &StringBufferLength,
|
|
&dwReadBytes, &dwReadChars);
|
|
if (!bRet || dwReadBytes == 0 || dwReadChars == 0)
|
|
{
|
|
/*
|
|
* We failed at reading the file, bail out
|
|
* and continue with the other files.
|
|
*/
|
|
break;
|
|
}
|
|
|
|
/* Update the statistics for PagePrompt */
|
|
dwSumReadBytes += dwReadBytes;
|
|
dwSumReadChars += dwReadChars;
|
|
|
|
if ((Encoding == ENCODING_UTF16LE) || (Encoding == ENCODING_UTF16BE))
|
|
{
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
|
|
FileCacheBuffer, dwReadChars);
|
|
}
|
|
else
|
|
{
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
|
|
StringBuffer, dwReadChars);
|
|
}
|
|
if (!bContinue)
|
|
{
|
|
/* We stop displaying this file */
|
|
break;
|
|
}
|
|
}
|
|
while (bRet && dwReadBytes > 0);
|
|
|
|
/* Flush any cached pager buffers */
|
|
if (bContinue)
|
|
bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0);
|
|
|
|
CloseHandle(hFile);
|
|
|
|
/* Check whether we should stop displaying this file */
|
|
if (!bContinue)
|
|
{
|
|
if (s_bDoNextFile)
|
|
{
|
|
/* Bail out and continue with the other files */
|
|
continue;
|
|
}
|
|
|
|
/* We Ctrl-C/Ctrl-Break, stop everything */
|
|
goto Quit;
|
|
}
|
|
}
|
|
|
|
Quit:
|
|
if (StringBuffer) HeapFree(GetProcessHeap(), 0, StringBuffer);
|
|
HeapFree(GetProcessHeap(), 0, FileCacheBuffer);
|
|
CloseHandle(hKeyboard);
|
|
return 0;
|
|
}
|
|
|
|
/* EOF */
|