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