xref: /reactos/base/applications/cmdutils/more/more.c (revision 5100859e)
1 /*
2  * COPYRIGHT:       See COPYING in the top level directory
3  * PROJECT:         ReactOS More Command
4  * FILE:            base/applications/cmdutils/more/more.c
5  * PURPOSE:         Displays text stream from STDIN or from an arbitrary number
6  *                  of files to STDOUT, with screen capabilities (more than CAT,
7  *                  but less than LESS ^^).
8  * PROGRAMMERS:     Paolo Pantaleo
9  *                  Timothy Schepens
10  *                  Hermes Belusca-Maito (hermes.belusca@sfr.fr)
11  */
12 /*
13  * MORE.C - external command.
14  *
15  * clone from 4nt more command
16  *
17  * 26 Sep 1999 - Paolo Pantaleo <paolopan@freemail.it>
18  *     started
19  *
20  * Oct 2003 - Timothy Schepens <tischepe at fastmail dot fm>
21  *     use window size instead of buffer size.
22  */
23 
24 #include <stdio.h>
25 
26 #include <windef.h>
27 #include <winbase.h>
28 #include <winnls.h>
29 #include <winuser.h>
30 
31 #include <conutils.h>
32 
33 #include "resource.h"
34 
35 /* PagePrompt statistics for the current file */
36 DWORD dwFileSize; // In bytes
37 DWORD dwSumReadBytes, dwSumReadChars;
38 // The average number of bytes per character is equal to
39 // dwSumReadBytes / dwSumReadChars. Note that dwSumReadChars
40 // will never be == 0 when ConWritePaging (and possibly PagePrompt)
41 // is called.
42 
43 /* Handles for file and console */
44 HANDLE hFile = INVALID_HANDLE_VALUE;
45 HANDLE hStdIn, hStdOut;
46 HANDLE hKeyboard;
47 
48 
49 static BOOL
50 __stdcall
51 PagePrompt(PCON_PAGER Pager, DWORD Done, DWORD Total)
52 {
53     HANDLE hInput = ConStreamGetOSHandle(StdIn);
54     DWORD dwMode;
55     KEY_EVENT_RECORD KeyEvent;
56 
57     /*
58      * Just use the simple prompt if the file being displayed is the STDIN,
59      * otherwise use the prompt with progress percentage.
60      *
61      * The progress percentage is evaluated as follows.
62      * So far we have read a total of 'dwSumReadBytes' bytes from the file.
63      * Amongst those is the latest read chunk of 'dwReadBytes' bytes, to which
64      * correspond a number of 'dwReadChars' characters with which we have called
65      * ConWritePaging who called PagePrompt. We then have: Total == dwReadChars.
66      * During this ConWritePaging call the PagePrompt was called after 'Done'
67      * number of characters over 'Total'.
68      * It should be noted that for 'dwSumReadBytes' number of bytes read it
69      * *roughly* corresponds 'dwSumReadChars' number of characters. This is
70      * because there may be some failures happening during the conversion of
71      * the bytes read to the character string for a given encoding.
72      * Therefore the number of characters displayed on screen is equal to:
73      *   dwSumReadChars - Total + Done ,
74      * but the best corresponding approximed number of bytes would be:
75      *   dwSumReadBytes - (Total - Done) * (dwSumReadBytes / dwSumReadChars) ,
76      * where the ratio is the average number of bytes per character.
77      * The percentage is then computed relative to the total file size.
78      */
79     if (hFile == hStdIn)
80     {
81         ConResPuts(Pager->Screen->Stream, IDS_CONTINUE);
82     }
83     else
84     {
85         ConResPrintf(Pager->Screen->Stream, IDS_CONTINUE_PROGRESS,
86                      // (dwSumReadChars - Total + Done) * 100 / dwFileSize
87                      (dwSumReadBytes - (Total - Done) *
88                         (dwSumReadBytes / dwSumReadChars)) * 100 / dwFileSize
89                      );
90     }
91 
92     // TODO: Implement prompt read line!
93 
94     // FIXME: Does not support TTY yet!
95 
96     /* RemoveBreakHandler */
97     SetConsoleCtrlHandler(NULL, TRUE);
98     /* ConInDisable */
99     GetConsoleMode(hInput, &dwMode);
100     dwMode &= ~ENABLE_PROCESSED_INPUT;
101     SetConsoleMode(hInput, dwMode);
102 
103     do
104     {
105         // FIXME: Does not support TTY yet!
106 
107         // ConInKey(&KeyEvent);
108         INPUT_RECORD ir;
109         DWORD dwRead;
110         do
111         {
112             ReadConsoleInput(hInput, &ir, 1, &dwRead);
113         }
114         while ((ir.EventType != KEY_EVENT) || (!ir.Event.KeyEvent.bKeyDown));
115 
116         /* Got our key, return to caller */
117         KeyEvent = ir.Event.KeyEvent;
118     }
119     while ((KeyEvent.wVirtualKeyCode == VK_SHIFT) ||
120            (KeyEvent.wVirtualKeyCode == VK_MENU) ||
121            (KeyEvent.wVirtualKeyCode == VK_CONTROL));
122 
123     /* AddBreakHandler */
124     SetConsoleCtrlHandler(NULL, FALSE);
125     /* ConInEnable */
126     GetConsoleMode(hInput, &dwMode);
127     dwMode |= ENABLE_PROCESSED_INPUT;
128     SetConsoleMode(hInput, dwMode);
129 
130     /*
131      * Erase the full line where the cursor is, and move
132      * the cursor back to the beginning of the line.
133      */
134     ConClearLine(Pager->Screen->Stream);
135 
136     if ((KeyEvent.wVirtualKeyCode == VK_ESCAPE) ||
137         ((KeyEvent.wVirtualKeyCode == L'C') &&
138          (KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))))
139     {
140         /* We break, output a newline */
141         WCHAR ch = L'\n';
142         ConStreamWrite(Pager->Screen->Stream, &ch, 1);
143         return FALSE;
144     }
145 
146     return TRUE;
147 }
148 
149 /*
150  * See base/applications/cmdutils/clip/clip.c!IsDataUnicode()
151  * and base/applications/notepad/text.c!ReadText() for more details.
152  * Also some good code example can be found at:
153  * https://github.com/AutoIt/text-encoding-detect
154  */
155 typedef enum
156 {
157     ENCODING_ANSI    =  0,
158     ENCODING_UTF16LE =  1,
159     ENCODING_UTF16BE =  2,
160     ENCODING_UTF8    =  3
161 } ENCODING;
162 
163 static BOOL
164 IsDataUnicode(
165     IN PVOID Buffer,
166     IN DWORD BufferSize,
167     OUT ENCODING* Encoding OPTIONAL,
168     OUT PDWORD SkipBytes OPTIONAL)
169 {
170     PBYTE pBytes = Buffer;
171     ENCODING encFile = ENCODING_ANSI;
172     DWORD dwPos = 0;
173 
174     /*
175      * See http://archives.miloush.net/michkap/archive/2007/04/22/2239345.html
176      * for more details about the algorithm and the pitfalls behind it.
177      * Of course it would be actually great to make a nice function that
178      * would work, once and for all, and put it into a library.
179      */
180 
181     /* Look for Byte Order Marks */
182     if ((BufferSize >= 2) && (pBytes[0] == 0xFF) && (pBytes[1] == 0xFE))
183     {
184         encFile = ENCODING_UTF16LE;
185         dwPos = 2;
186     }
187     else if ((BufferSize >= 2) && (pBytes[0] == 0xFE) && (pBytes[1] == 0xFF))
188     {
189         encFile = ENCODING_UTF16BE;
190         dwPos = 2;
191     }
192     else if ((BufferSize >= 3) && (pBytes[0] == 0xEF) && (pBytes[1] == 0xBB) && (pBytes[2] == 0xBF))
193     {
194         encFile = ENCODING_UTF8;
195         dwPos = 3;
196     }
197     else
198     {
199         /*
200          * Try using statistical analysis. Do not rely on the return value of
201          * IsTextUnicode as we can get FALSE even if the text is in UTF-16 BE
202          * (i.e. we have some of the IS_TEXT_UNICODE_REVERSE_MASK bits set).
203          * Instead, set all the tests we want to perform, then just check
204          * the passed tests and try to deduce the string properties.
205          */
206 
207 /*
208  * This mask contains the 3 highest bits from IS_TEXT_UNICODE_NOT_ASCII_MASK
209  * and the 1st highest bit from IS_TEXT_UNICODE_NOT_UNICODE_MASK.
210  */
211 #define IS_TEXT_UNKNOWN_FLAGS_MASK  ((7 << 13) | (1 << 11))
212 
213         /* Flag out the unknown flags here, the passed tests will not have them either */
214         INT Tests = (IS_TEXT_UNICODE_NOT_ASCII_MASK   |
215                      IS_TEXT_UNICODE_NOT_UNICODE_MASK |
216                      IS_TEXT_UNICODE_REVERSE_MASK | IS_TEXT_UNICODE_UNICODE_MASK)
217                         & ~IS_TEXT_UNKNOWN_FLAGS_MASK;
218         INT Results;
219 
220         IsTextUnicode(Buffer, BufferSize, &Tests);
221         Results = Tests;
222 
223         /*
224          * As the IS_TEXT_UNICODE_NULL_BYTES or IS_TEXT_UNICODE_ILLEGAL_CHARS
225          * flags are expected to be potentially present in the result without
226          * modifying our expectations, filter them out now.
227          */
228         Results &= ~(IS_TEXT_UNICODE_NULL_BYTES | IS_TEXT_UNICODE_ILLEGAL_CHARS);
229 
230         /*
231          * NOTE: The flags IS_TEXT_UNICODE_ASCII16 and
232          * IS_TEXT_UNICODE_REVERSE_ASCII16 are not reliable.
233          *
234          * NOTE2: Check for potential "bush hid the facts" effect by also
235          * checking the original results (in 'Tests') for the absence of
236          * the IS_TEXT_UNICODE_NULL_BYTES flag, as we may presumably expect
237          * that in UTF-16 text there will be at some point some NULL bytes.
238          * If not, fall back to ANSI. This shows the limitations of using the
239          * IsTextUnicode API to perform such tests, and the usage of a more
240          * improved encoding detection algorithm would be really welcome.
241          */
242         if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) &&
243             !(Results & IS_TEXT_UNICODE_REVERSE_MASK)     &&
244              (Results & IS_TEXT_UNICODE_UNICODE_MASK)     &&
245              (Tests   & IS_TEXT_UNICODE_NULL_BYTES))
246         {
247             encFile = ENCODING_UTF16LE;
248             dwPos = (Results & IS_TEXT_UNICODE_SIGNATURE) ? 2 : 0;
249         }
250         else
251         if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) &&
252             !(Results & IS_TEXT_UNICODE_UNICODE_MASK)     &&
253              (Results & IS_TEXT_UNICODE_REVERSE_MASK)     &&
254              (Tests   & IS_TEXT_UNICODE_NULL_BYTES))
255         {
256             encFile = ENCODING_UTF16BE;
257             dwPos = (Results & IS_TEXT_UNICODE_REVERSE_SIGNATURE) ? 2 : 0;
258         }
259         else
260         {
261             /*
262              * Either 'Results' has neither of those masks set, as it can be
263              * the case for UTF-8 text (or ANSI), or it has both as can be the
264              * case when analysing pure binary data chunk. This is therefore
265              * invalid and we fall back to ANSI encoding.
266              * FIXME: In case of failure, assume ANSI (as long as we do not have
267              * correct tests for UTF8, otherwise we should do them, and at the
268              * very end, assume ANSI).
269              */
270             encFile = ENCODING_ANSI; // ENCODING_UTF8;
271             dwPos = 0;
272         }
273     }
274 
275     if (Encoding)
276         *Encoding = encFile;
277     if (SkipBytes)
278         *SkipBytes = dwPos;
279 
280     return (encFile != ENCODING_ANSI);
281 }
282 
283 /*
284  * Adapted from base/shell/cmd/misc.c!FileGetString(), but with correct
285  * text encoding support. Also please note that similar code should be
286  * also used in the CMD.EXE 'TYPE' command.
287  * Contrary to CMD's FileGetString() we do not stop at new-lines.
288  *
289  * Read text data from a file and convert it from a given encoding to UTF-16.
290  *
291  *   IN OUT PVOID pCacheBuffer and IN DWORD CacheBufferLength :
292  *     Implementation detail so that the function uses an external user-provided
293  *     buffer to store the data temporarily read from the file. The function
294  *     could have used an internal buffer instead. The length is in number of bytes.
295  *
296  *   IN OUT PWSTR* pBuffer and IN OUT PDWORD pnBufferLength :
297  *     Reallocated buffer containing the string data converted to UTF-16.
298  *     In input, contains a pointer to the original buffer and its length.
299  *     In output, contains a pointer to the reallocated buffer and its length.
300  *     The length is in number of characters.
301  *
302  *     At first call to this function, pBuffer can be set to NULL, in which case
303  *     when the function returns the pointer will point to a valid buffer.
304  *     After the last call to this function, free the pBuffer pointer with:
305  *     HeapFree(GetProcessHeap(), 0, *pBuffer);
306  *
307  *     If Encoding is set to ENCODING_UTF16LE or ENCODING_UTF16BE, since we are
308  *     compiled in UNICODE, no extra conversion is performed and therefore
309  *     pBuffer is unused (remains unallocated) and one can directly use the
310  *     contents of pCacheBuffer as it is expected to contain valid UTF-16 text.
311  *
312  *   OUT PDWORD pdwReadBytes : Number of bytes read from the file (optional).
313  *   OUT PDWORD pdwReadChars : Corresponding number of characters read (optional).
314  */
315 static BOOL
316 FileGetString(
317     IN HANDLE hFile,
318     IN ENCODING Encoding,
319     IN OUT PVOID pCacheBuffer,
320     IN DWORD CacheBufferLength,
321     IN OUT PWCHAR* pBuffer,
322     IN OUT PDWORD pnBufferLength,
323     OUT PDWORD pdwReadBytes OPTIONAL,
324     OUT PDWORD pdwReadChars OPTIONAL)
325 {
326     BOOL Success;
327     UINT CodePage = (UINT)-1;
328     DWORD dwReadBytes;
329     INT len;
330 
331     // ASSERT(pCacheBuffer && (CacheBufferLength > 0));
332     // ASSERT(CacheBufferLength % 2 == 0); // Cache buffer length MUST BE even!
333     // ASSERT(pBuffer && pnBufferLength);
334 
335     /* Always reset the retrieved number of bytes/characters */
336     if (pdwReadBytes) *pdwReadBytes = 0;
337     if (pdwReadChars) *pdwReadChars = 0;
338 
339     Success = ReadFile(hFile, pCacheBuffer, CacheBufferLength, &dwReadBytes, NULL);
340     if (!Success || dwReadBytes == 0)
341         return FALSE;
342 
343     if (pdwReadBytes) *pdwReadBytes = dwReadBytes;
344 
345     if ((Encoding == ENCODING_ANSI) || (Encoding == ENCODING_UTF8))
346     {
347         /* Conversion is needed */
348 
349         if (Encoding == ENCODING_ANSI)
350             CodePage = GetConsoleCP(); // CP_ACP; // FIXME: Cache GetConsoleCP() value.
351         else // if (Encoding == ENCODING_UTF8)
352             CodePage = CP_UTF8;
353 
354         /* Retrieve the needed buffer size */
355         len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes,
356                                   NULL, 0);
357         if (len == 0)
358         {
359             /* Failure, bail out */
360             return FALSE;
361         }
362 
363         /* Initialize the conversion buffer if needed... */
364         if (*pBuffer == NULL)
365         {
366             *pnBufferLength = len;
367             *pBuffer = HeapAlloc(GetProcessHeap(), 0, *pnBufferLength * sizeof(WCHAR));
368             if (*pBuffer == NULL)
369             {
370                 // *pBuffer = NULL;
371                 *pnBufferLength = 0;
372                 // WARN("DEBUG: Cannot allocate memory for *pBuffer!\n");
373                 // ConErrFormatMessage(GetLastError());
374                 return FALSE;
375             }
376         }
377         /* ... or reallocate only if the new length is greater than the old one */
378         else if (len > *pnBufferLength)
379         {
380             PWSTR OldBuffer = *pBuffer;
381 
382             *pnBufferLength = len;
383             *pBuffer = HeapReAlloc(GetProcessHeap(), 0, *pBuffer, *pnBufferLength * sizeof(WCHAR));
384             if (*pBuffer == NULL)
385             {
386                 /* Do not leak old buffer */
387                 HeapFree(GetProcessHeap(), 0, OldBuffer);
388                 // *pBuffer = NULL;
389                 *pnBufferLength = 0;
390                 // WARN("DEBUG: Cannot reallocate memory for *pBuffer!\n");
391                 // ConErrFormatMessage(GetLastError());
392                 return FALSE;
393             }
394         }
395 
396         /* Now perform the conversion proper */
397         len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes,
398                                   *pBuffer, len);
399         dwReadBytes = len;
400     }
401     else
402     {
403         /*
404          * No conversion needed, just convert from big to little endian if needed.
405          * pBuffer and pnBufferLength are left untouched and pCacheBuffer can be
406          * directly used.
407          */
408         PWCHAR pWChars = pCacheBuffer;
409         DWORD i;
410 
411         dwReadBytes /= sizeof(WCHAR);
412 
413         if (Encoding == ENCODING_UTF16BE)
414         {
415             for (i = 0; i < dwReadBytes; i++)
416             {
417                 /* Equivalent to RtlUshortByteSwap: reverse high/low bytes */
418                 pWChars[i] = MAKEWORD(HIBYTE(pWChars[i]), LOBYTE(pWChars[i]));
419             }
420         }
421         // else if (Encoding == ENCODING_UTF16LE), we are good, nothing to do.
422     }
423 
424     /* Return the number of characters (dwReadBytes is converted) */
425     if (pdwReadChars) *pdwReadChars = dwReadBytes;
426 
427     return TRUE;
428 }
429 
430 // INT CommandMore(LPTSTR cmd, LPTSTR param)
431 int wmain(int argc, WCHAR* argv[])
432 {
433     // FIXME this stuff!
434     CON_SCREEN Screen = {StdOut};
435     CON_PAGER Pager = {&Screen, 0};
436 
437     int i;
438 
439     BOOL bRet, bContinue;
440 
441     ENCODING Encoding;
442     DWORD SkipBytes = 0;
443 
444 #define FileCacheBufferSize 4096
445     PVOID FileCacheBuffer = NULL;
446     PWCHAR StringBuffer = NULL;
447     DWORD StringBufferLength = 0;
448     DWORD dwReadBytes, dwReadChars;
449 
450     TCHAR szFullPath[MAX_PATH];
451 
452     hStdIn = GetStdHandle(STD_INPUT_HANDLE);
453     hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
454 
455     /* Initialize the Console Standard Streams */
456     ConStreamInit(StdIn , GetStdHandle(STD_INPUT_HANDLE) , UTF8Text, INVALID_CP);
457     ConStreamInit(StdOut, GetStdHandle(STD_OUTPUT_HANDLE), UTF8Text, INVALID_CP);
458     ConStreamInit(StdErr, GetStdHandle(STD_ERROR_HANDLE) , UTF8Text, INVALID_CP);
459 
460     /*
461      * Bad usage (too much options) or we use the /? switch.
462      * Display help for the MORE command.
463      */
464     if (argc > 1 && wcscmp(argv[1], L"/?") == 0)
465     {
466         ConResPuts(StdOut, IDS_USAGE);
467         return 0;
468     }
469 
470     // FIXME: Parse all the remaining parameters.
471     // Then the file list can be found at the very end.
472     // FIXME2: Use the PARSER api that can be found in EVENTCREATE.
473 
474     // NOTE: We might try to duplicate the ConOut for read access... ?
475     hKeyboard = CreateFileW(L"CONIN$", GENERIC_READ|GENERIC_WRITE,
476                             FILE_SHARE_READ|FILE_SHARE_WRITE, NULL,
477                             OPEN_EXISTING, 0, NULL);
478     FlushConsoleInputBuffer(hKeyboard);
479     ConStreamSetOSHandle(StdIn, hKeyboard);
480 
481 
482     FileCacheBuffer = HeapAlloc(GetProcessHeap(), 0, FileCacheBufferSize);
483     if (!FileCacheBuffer)
484     {
485         ConPuts(StdErr, L"Error: no memory\n");
486         CloseHandle(hKeyboard);
487         return 1;
488     }
489 
490     /* Special case where we run 'MORE' without any argument: we use STDIN */
491     if (argc <= 1)
492     {
493         /*
494          * Assign STDIN handle to hFile so that the page prompt function will
495          * know the data comes from STDIN, and will take different actions.
496          */
497         hFile = hStdIn;
498 
499         /* Update the statistics for PagePrompt */
500         dwFileSize = 0;
501         dwSumReadBytes = dwSumReadChars = 0;
502 
503         /* We suppose we read text from the file */
504 
505         /* For STDIN we always suppose we are in ANSI mode */
506         // SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
507         Encoding = ENCODING_ANSI; // ENCODING_UTF8;
508 
509         bContinue = ConPutsPaging(&Pager, PagePrompt, TRUE, L"");
510         if (!bContinue)
511             goto Quit;
512 
513         do
514         {
515             bRet = FileGetString(hFile, Encoding,
516                                  FileCacheBuffer, FileCacheBufferSize,
517                                  &StringBuffer, &StringBufferLength,
518                                  &dwReadBytes, &dwReadChars);
519             if (!bRet || dwReadBytes == 0 || dwReadChars == 0)
520             {
521                 /* We failed at reading the file, bail out */
522                 break;
523             }
524 
525             /* Update the statistics for PagePrompt */
526             dwSumReadBytes += dwReadBytes;
527             dwSumReadChars += dwReadChars;
528 
529             bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
530                                        StringBuffer, dwReadChars);
531             /* If we Ctrl-C/Ctrl-Break, stop everything */
532             if (!bContinue)
533                 goto Quit;
534         }
535         while (bRet && dwReadBytes > 0);
536         goto Quit;
537     }
538 
539     /* We have files: read them and output them to STDOUT */
540     for (i = 1; i < argc; i++)
541     {
542         GetFullPathNameW(argv[i], ARRAYSIZE(szFullPath), szFullPath, NULL);
543         hFile = CreateFileW(szFullPath,
544                             GENERIC_READ,
545                             FILE_SHARE_READ,
546                             NULL,
547                             OPEN_EXISTING,
548                             0, // FILE_ATTRIBUTE_NORMAL,
549                             NULL);
550         if (hFile == INVALID_HANDLE_VALUE)
551         {
552             ConResPrintf(StdErr, IDS_FILE_ACCESS, szFullPath);
553             continue;
554         }
555 
556         /* We currently do not support files too big */
557         dwFileSize = GetFileSize(hFile, NULL);
558         if (dwFileSize == INVALID_FILE_SIZE)
559         {
560             ConPuts(StdErr, L"ERROR: Invalid file size!\n");
561             CloseHandle(hFile);
562             continue;
563         }
564 
565         /* We suppose we read text from the file */
566 
567         /* Check whether the file is UNICODE and retrieve its encoding */
568         SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
569         bRet = ReadFile(hFile, FileCacheBuffer, FileCacheBufferSize, &dwReadBytes, NULL);
570         IsDataUnicode(FileCacheBuffer, dwReadBytes, &Encoding, &SkipBytes);
571         SetFilePointer(hFile, SkipBytes, NULL, FILE_BEGIN);
572 
573         /* Update the statistics for PagePrompt */
574         dwSumReadBytes = dwSumReadChars = 0;
575 
576         bContinue = ConPutsPaging(&Pager, PagePrompt, TRUE, L"");
577         if (!bContinue)
578         {
579             CloseHandle(hFile);
580             goto Quit;
581         }
582 
583         do
584         {
585             bRet = FileGetString(hFile, Encoding,
586                                  FileCacheBuffer, FileCacheBufferSize,
587                                  &StringBuffer, &StringBufferLength,
588                                  &dwReadBytes, &dwReadChars);
589             if (!bRet || dwReadBytes == 0 || dwReadChars == 0)
590             {
591                 /*
592                  * We failed at reading the file, bail out and
593                  * continue with the other files.
594                  */
595                 break;
596             }
597 
598             /* Update the statistics for PagePrompt */
599             dwSumReadBytes += dwReadBytes;
600             dwSumReadChars += dwReadChars;
601 
602             if ((Encoding == ENCODING_UTF16LE) || (Encoding == ENCODING_UTF16BE))
603             {
604                 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
605                                            FileCacheBuffer, dwReadChars);
606             }
607             else
608             {
609                 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE,
610                                            StringBuffer, dwReadChars);
611             }
612             /* If we Ctrl-C/Ctrl-Break, stop everything */
613             if (!bContinue)
614             {
615                 CloseHandle(hFile);
616                 goto Quit;
617             }
618         }
619         while (bRet && dwReadBytes > 0);
620 
621         CloseHandle(hFile);
622     }
623 
624 Quit:
625     if (StringBuffer) HeapFree(GetProcessHeap(), 0, StringBuffer);
626     HeapFree(GetProcessHeap(), 0, FileCacheBuffer);
627     CloseHandle(hKeyboard);
628     return 0;
629 }
630 
631 /* EOF */
632