Git conversion: Make reactos the root directory, move rosapps, rostests, wallpapers...
[reactos.git] / base / applications / cmdutils / more / more.c
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 */