1 /* 2 * PROJECT: ReactOS More Command 3 * LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+) 4 * PURPOSE: Displays text stream from STDIN or from an arbitrary number 5 * of files to STDOUT, with screen capabilities (more than CAT, 6 * but less than LESS ^^). 7 * COPYRIGHT: Copyright 1999 Paolo Pantaleo 8 * Copyright 2003 Timothy Schepens 9 * Copyright 2016-2021 Hermes Belusca-Maito 10 * Copyright 2021 Katayama Hirofumi MZ 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 #include <stdlib.h> 26 27 #include <windef.h> 28 #include <winbase.h> 29 #include <winnt.h> 30 #include <winnls.h> 31 #include <winreg.h> 32 #include <winuser.h> 33 34 #include <conutils.h> 35 #include <strsafe.h> 36 37 #include "resource.h" 38 39 /* PagePrompt statistics for the current file */ 40 DWORD dwFileSize; // In bytes 41 DWORD dwSumReadBytes, dwSumReadChars; 42 // The average number of bytes per character is equal to 43 // dwSumReadBytes / dwSumReadChars. Note that dwSumReadChars 44 // will never be == 0 when ConWritePaging (and possibly PagePrompt) 45 // is called. 46 47 /* Handles for file and console */ 48 HANDLE hFile = INVALID_HANDLE_VALUE; 49 HANDLE hStdIn, hStdOut; 50 HANDLE hKeyboard; 51 52 /* Enable/Disable extensions */ 53 BOOL bEnableExtensions = TRUE; // FIXME: By default, it should be FALSE. 54 55 /* Parser flags */ 56 #define FLAG_HELP (1 << 0) 57 #define FLAG_E (1 << 1) 58 #define FLAG_C (1 << 2) 59 #define FLAG_P (1 << 3) 60 #define FLAG_S (1 << 4) 61 #define FLAG_Tn (1 << 5) 62 #define FLAG_PLUSn (1 << 6) 63 64 /* Prompt flags */ 65 #define PROMPT_PERCENT (1 << 0) 66 #define PROMPT_LINE_AT (1 << 1) 67 #define PROMPT_OPTIONS (1 << 2) 68 #define PROMPT_LINES (1 << 3) 69 70 static DWORD s_dwFlags = 0; 71 static LONG s_nTabWidth = 8; 72 static DWORD s_nNextLineNo = 0; 73 static BOOL s_bPrevLineIsBlank = FALSE; 74 static WORD s_fPrompt = 0; 75 static BOOL s_bDoNextFile = FALSE; 76 77 static BOOL IsBlankLine(IN PCWCH line, IN DWORD cch) 78 { 79 DWORD ich; 80 WORD wType; 81 for (ich = 0; ich < cch; ++ich) 82 { 83 /* 84 * Explicitly exclude FORM-FEED from the check, 85 * so that the pager can handle it. 86 */ 87 if (line[ich] == L'\f') 88 return FALSE; 89 90 /* 91 * Otherwise do the extended blanks check. 92 * Note that MS MORE.COM only checks for spaces (\x20) and TABs (\x09). 93 * See http://archives.miloush.net/michkap/archive/2007/06/11/3230072.html 94 * for more information. 95 */ 96 wType = 0; 97 GetStringTypeW(CT_CTYPE1, &line[ich], 1, &wType); 98 if (!(wType & (C1_BLANK | C1_SPACE))) 99 return FALSE; 100 } 101 return TRUE; 102 } 103 104 static BOOL 105 __stdcall 106 MorePagerLine( 107 IN OUT PCON_PAGER Pager, 108 IN PCWCH line, 109 IN DWORD cch) 110 { 111 if (s_dwFlags & FLAG_PLUSn) /* Skip lines */ 112 { 113 if (Pager->lineno < s_nNextLineNo) 114 { 115 s_bPrevLineIsBlank = FALSE; 116 return TRUE; /* Handled */ 117 } 118 s_dwFlags &= ~FLAG_PLUSn; 119 } 120 121 if (s_dwFlags & FLAG_S) /* Shrink blank lines */ 122 { 123 if (IsBlankLine(line, cch)) 124 { 125 if (s_bPrevLineIsBlank) 126 return TRUE; /* Handled */ 127 128 /* 129 * Display a single blank line, independently of the actual size 130 * of the current line, by displaying just one space: this is 131 * especially needed in order to force line wrapping when the 132 * ENABLE_VIRTUAL_TERMINAL_PROCESSING or DISABLE_NEWLINE_AUTO_RETURN 133 * console modes are enabled. 134 * Then, reposition the cursor to the next line, first column. 135 */ 136 if (Pager->PageColumns > 0) 137 ConStreamWrite(Pager->Screen->Stream, TEXT(" "), 1); 138 ConStreamWrite(Pager->Screen->Stream, TEXT("\n"), 1); 139 Pager->iLine++; 140 Pager->iColumn = 0; 141 142 s_bPrevLineIsBlank = TRUE; 143 s_nNextLineNo = 0; 144 145 return TRUE; /* Handled */ 146 } 147 else 148 { 149 s_bPrevLineIsBlank = FALSE; 150 } 151 } 152 153 s_nNextLineNo = 0; 154 /* Not handled, let the pager do the default action */ 155 return FALSE; 156 } 157 158 static BOOL 159 __stdcall 160 PagePrompt(PCON_PAGER Pager, DWORD Done, DWORD Total) 161 { 162 HANDLE hInput = ConStreamGetOSHandle(StdIn); 163 HANDLE hOutput = ConStreamGetOSHandle(Pager->Screen->Stream); 164 CONSOLE_SCREEN_BUFFER_INFO csbi; 165 COORD orgCursorPosition; 166 DWORD dwMode; 167 168 KEY_EVENT_RECORD KeyEvent; 169 BOOL fCtrl; 170 DWORD nLines; 171 WCHAR chSubCommand = 0; 172 173 /* Prompt strings (small size since the prompt should 174 * hold ideally on one <= 80-character line) */ 175 static WCHAR StrPercent[80] = L""; 176 static WCHAR StrLineAt[80] = L""; 177 static WCHAR StrOptions[80] = L""; 178 static WCHAR StrLines[80] = L""; 179 180 WCHAR szPercent[80] = L""; 181 WCHAR szLineAt[80] = L""; 182 183 /* Load the prompt strings */ 184 if (!*StrPercent) 185 K32LoadStringW(NULL, IDS_CONTINUE_PERCENT, StrPercent, ARRAYSIZE(StrPercent)); 186 if (!*StrLineAt) 187 K32LoadStringW(NULL, IDS_CONTINUE_LINE_AT, StrLineAt, ARRAYSIZE(StrLineAt)); 188 if (!*StrOptions) 189 K32LoadStringW(NULL, IDS_CONTINUE_OPTIONS, StrOptions, ARRAYSIZE(StrOptions)); 190 if (!*StrLines) 191 K32LoadStringW(NULL, IDS_CONTINUE_LINES, StrLines, ARRAYSIZE(StrLines)); 192 193 /* 194 * Check whether the pager is prompting, but we have actually finished 195 * to display a given file, or no data is present in STDIN anymore. 196 * In this case, skip the prompt altogether. The only exception is when 197 * we are displaying other files. 198 */ 199 // TODO: Implement! 200 201 Restart: 202 nLines = 0; 203 204 /* Do not show the progress percentage when STDIN is being displayed */ 205 if (s_fPrompt & PROMPT_PERCENT) // && (hFile != hStdIn) 206 { 207 /* 208 * The progress percentage is evaluated as follows. 209 * So far we have read a total of 'dwSumReadBytes' bytes from the file. 210 * Amongst those is the latest read chunk of 'dwReadBytes' bytes, to which 211 * correspond a number of 'dwReadChars' characters with which we have called 212 * ConWritePaging who called PagePrompt. We then have: Total == dwReadChars. 213 * During this ConWritePaging call the PagePrompt was called after 'Done' 214 * number of characters over 'Total'. 215 * It should be noted that for 'dwSumReadBytes' number of bytes read it 216 * *roughly* corresponds 'dwSumReadChars' number of characters. This is 217 * because there may be some failures happening during the conversion of 218 * the bytes read to the character string for a given encoding. 219 * Therefore the number of characters displayed on screen is equal to: 220 * dwSumReadChars - Total + Done , 221 * but the best corresponding approximed number of bytes would be: 222 * dwSumReadBytes - (Total - Done) * (dwSumReadBytes / dwSumReadChars) , 223 * where the ratio is the average number of bytes per character. 224 * The percentage is then computed relative to the total file size. 225 */ 226 DWORD dwPercent = (dwSumReadBytes - (Total - Done) * 227 (dwSumReadBytes / dwSumReadChars)) * 100 / dwFileSize; 228 StringCchPrintfW(szPercent, ARRAYSIZE(szPercent), StrPercent, dwPercent); 229 } 230 if (s_fPrompt & PROMPT_LINE_AT) 231 { 232 StringCchPrintfW(szLineAt, ARRAYSIZE(szLineAt), StrLineAt, Pager->lineno); 233 } 234 235 /* Suitably format and display the prompt */ 236 ConResMsgPrintf(Pager->Screen->Stream, 0, IDS_CONTINUE_PROMPT, 237 (s_fPrompt & PROMPT_PERCENT ? szPercent : L""), 238 (s_fPrompt & PROMPT_LINE_AT ? szLineAt : L""), 239 (s_fPrompt & PROMPT_OPTIONS ? StrOptions : L""), 240 (s_fPrompt & PROMPT_LINES ? StrLines : L"")); 241 242 /* Reset the prompt to a default state */ 243 s_fPrompt &= ~(PROMPT_LINE_AT | PROMPT_OPTIONS | PROMPT_LINES); 244 245 /* RemoveBreakHandler */ 246 SetConsoleCtrlHandler(NULL, TRUE); 247 /* ConInDisable */ 248 GetConsoleMode(hInput, &dwMode); 249 dwMode &= ~ENABLE_PROCESSED_INPUT; 250 SetConsoleMode(hInput, dwMode); 251 252 // FIXME: Does not support TTY yet! 253 ConGetScreenInfo(Pager->Screen, &csbi); 254 orgCursorPosition = csbi.dwCursorPosition; 255 for (;;) 256 { 257 INPUT_RECORD ir = {0}; 258 DWORD dwRead; 259 WCHAR ch; 260 261 do 262 { 263 ReadConsoleInput(hInput, &ir, 1, &dwRead); 264 } 265 while ((ir.EventType != KEY_EVENT) || (!ir.Event.KeyEvent.bKeyDown)); 266 267 /* Got our key */ 268 KeyEvent = ir.Event.KeyEvent; 269 270 /* Ignore any unsupported keyboard press */ 271 if ((KeyEvent.wVirtualKeyCode == VK_SHIFT) || 272 (KeyEvent.wVirtualKeyCode == VK_MENU) || 273 (KeyEvent.wVirtualKeyCode == VK_CONTROL)) 274 { 275 continue; 276 } 277 278 /* Ctrl key is pressed? */ 279 fCtrl = !!(KeyEvent.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)); 280 281 /* Ctrl+C or Ctrl+Esc? */ 282 if (fCtrl && ((KeyEvent.wVirtualKeyCode == VK_ESCAPE) || 283 (KeyEvent.wVirtualKeyCode == L'C'))) 284 { 285 chSubCommand = 0; 286 break; 287 } 288 289 /* If extended features are unavailable, or no 290 * pending commands, don't do more processing. */ 291 if (!(s_dwFlags & FLAG_E) || (chSubCommand == 0)) 292 break; 293 294 ch = KeyEvent.uChar.UnicodeChar; 295 if (L'0' <= ch && ch <= L'9') 296 { 297 nLines *= 10; 298 nLines += ch - L'0'; 299 ConStreamWrite(Pager->Screen->Stream, &ch, 1); 300 continue; 301 } 302 else if (KeyEvent.wVirtualKeyCode == VK_RETURN) 303 { 304 /* Validate the line number */ 305 break; 306 } 307 else if (KeyEvent.wVirtualKeyCode == VK_ESCAPE) 308 { 309 /* Cancel the current command */ 310 chSubCommand = 0; 311 break; 312 } 313 else if (KeyEvent.wVirtualKeyCode == VK_BACK) 314 { 315 if (nLines != 0) 316 nLines /= 10; 317 318 /* Erase the current character */ 319 ConGetScreenInfo(Pager->Screen, &csbi); 320 if ( (csbi.dwCursorPosition.Y > orgCursorPosition.Y) || 321 ((csbi.dwCursorPosition.Y == orgCursorPosition.Y) && 322 (csbi.dwCursorPosition.X > orgCursorPosition.X)) ) 323 { 324 if (csbi.dwCursorPosition.X > 0) 325 { 326 csbi.dwCursorPosition.X = csbi.dwCursorPosition.X - 1; 327 } 328 else if (csbi.dwCursorPosition.Y > 0) 329 { 330 csbi.dwCursorPosition.Y = csbi.dwCursorPosition.Y - 1; 331 csbi.dwCursorPosition.X = (csbi.dwSize.X ? csbi.dwSize.X - 1 : 0); 332 } 333 334 SetConsoleCursorPosition(hOutput, csbi.dwCursorPosition); 335 336 ch = L' '; 337 ConStreamWrite(Pager->Screen->Stream, &ch, 1); 338 SetConsoleCursorPosition(hOutput, csbi.dwCursorPosition); 339 } 340 341 continue; 342 } 343 } 344 345 /* AddBreakHandler */ 346 SetConsoleCtrlHandler(NULL, FALSE); 347 /* ConInEnable */ 348 GetConsoleMode(hInput, &dwMode); 349 dwMode |= ENABLE_PROCESSED_INPUT; 350 SetConsoleMode(hInput, dwMode); 351 352 /* Refresh the screen information, as the console may have been 353 * redimensioned. Update also the default number of lines to scroll. */ 354 ConGetScreenInfo(Pager->Screen, &csbi); 355 Pager->ScrollRows = csbi.srWindow.Bottom - csbi.srWindow.Top; 356 357 /* 358 * Erase the full line where the cursor is, and move 359 * the cursor back to the beginning of the line. 360 */ 361 ConClearLine(Pager->Screen->Stream); 362 363 /* Ctrl+C or Ctrl+Esc: Control Break */ 364 if (fCtrl && ((KeyEvent.wVirtualKeyCode == VK_ESCAPE) || 365 (KeyEvent.wVirtualKeyCode == L'C'))) 366 { 367 /* We break, output a newline */ 368 WCHAR ch = L'\n'; 369 ConStreamWrite(Pager->Screen->Stream, &ch, 1); 370 return FALSE; 371 } 372 373 switch (chSubCommand) 374 { 375 case L'P': 376 { 377 /* If we don't display other lines, just restart the prompt */ 378 if (nLines == 0) 379 { 380 chSubCommand = 0; 381 goto Restart; 382 } 383 /* Otherwise tell the pager to display them */ 384 Pager->ScrollRows = nLines; 385 return TRUE; 386 } 387 case L'S': 388 { 389 s_dwFlags |= FLAG_PLUSn; 390 s_nNextLineNo = Pager->lineno + nLines; 391 /* Use the default Pager->ScrollRows value */ 392 return TRUE; 393 } 394 default: 395 chSubCommand = 0; 396 break; 397 } 398 399 /* If extended features are available */ 400 if (s_dwFlags & FLAG_E) 401 { 402 /* Ignore any key presses if Ctrl is pressed */ 403 if (fCtrl) 404 { 405 chSubCommand = 0; 406 goto Restart; 407 } 408 409 /* 'Q': Quit */ 410 if (KeyEvent.wVirtualKeyCode == L'Q') 411 { 412 /* We break, output a newline */ 413 WCHAR ch = L'\n'; 414 ConStreamWrite(Pager->Screen->Stream, &ch, 1); 415 return FALSE; 416 } 417 418 /* 'F': Next file */ 419 if (KeyEvent.wVirtualKeyCode == L'F') 420 { 421 s_bDoNextFile = TRUE; 422 return FALSE; 423 } 424 425 /* '?': Show Options */ 426 if (KeyEvent.uChar.UnicodeChar == L'?') 427 { 428 s_fPrompt |= PROMPT_OPTIONS; 429 goto Restart; 430 } 431 432 /* [Enter] key: Display one line */ 433 if (KeyEvent.wVirtualKeyCode == VK_RETURN) 434 { 435 Pager->ScrollRows = 1; 436 return TRUE; 437 } 438 439 /* [Space] key: Display one page */ 440 if (KeyEvent.wVirtualKeyCode == VK_SPACE) 441 { 442 if (s_dwFlags & FLAG_C) 443 { 444 /* Clear the screen */ 445 ConClearScreen(Pager->Screen); 446 } 447 /* Use the default Pager->ScrollRows value */ 448 return TRUE; 449 } 450 451 /* 'P': Display n lines */ 452 if (KeyEvent.wVirtualKeyCode == L'P') 453 { 454 s_fPrompt |= PROMPT_LINES; 455 chSubCommand = L'P'; 456 goto Restart; 457 } 458 459 /* 'S': Skip n lines */ 460 if (KeyEvent.wVirtualKeyCode == L'S') 461 { 462 s_fPrompt |= PROMPT_LINES; 463 chSubCommand = L'S'; 464 goto Restart; 465 } 466 467 /* '=': Show current line number */ 468 if (KeyEvent.uChar.UnicodeChar == L'=') 469 { 470 s_fPrompt |= PROMPT_LINE_AT; 471 goto Restart; 472 } 473 474 chSubCommand = 0; 475 goto Restart; 476 } 477 else 478 { 479 /* Extended features are unavailable: display one page */ 480 /* Use the default Pager->ScrollRows value */ 481 return TRUE; 482 } 483 } 484 485 /* 486 * See base/applications/cmdutils/clip/clip.c!IsDataUnicode() 487 * and base/applications/notepad/text.c!ReadText() for more details. 488 * Also some good code example can be found at: 489 * https://github.com/AutoIt/text-encoding-detect 490 */ 491 typedef enum 492 { 493 ENCODING_ANSI = 0, 494 ENCODING_UTF16LE = 1, 495 ENCODING_UTF16BE = 2, 496 ENCODING_UTF8 = 3 497 } ENCODING; 498 499 static BOOL 500 IsDataUnicode( 501 IN PVOID Buffer, 502 IN DWORD BufferSize, 503 OUT ENCODING* Encoding OPTIONAL, 504 OUT PDWORD SkipBytes OPTIONAL) 505 { 506 PBYTE pBytes = Buffer; 507 ENCODING encFile = ENCODING_ANSI; 508 DWORD dwPos = 0; 509 510 /* 511 * See http://archives.miloush.net/michkap/archive/2007/04/22/2239345.html 512 * for more details about the algorithm and the pitfalls behind it. 513 * Of course it would be actually great to make a nice function that 514 * would work, once and for all, and put it into a library. 515 */ 516 517 /* Look for Byte Order Marks */ 518 if ((BufferSize >= 2) && (pBytes[0] == 0xFF) && (pBytes[1] == 0xFE)) 519 { 520 encFile = ENCODING_UTF16LE; 521 dwPos = 2; 522 } 523 else if ((BufferSize >= 2) && (pBytes[0] == 0xFE) && (pBytes[1] == 0xFF)) 524 { 525 encFile = ENCODING_UTF16BE; 526 dwPos = 2; 527 } 528 else if ((BufferSize >= 3) && (pBytes[0] == 0xEF) && (pBytes[1] == 0xBB) && (pBytes[2] == 0xBF)) 529 { 530 encFile = ENCODING_UTF8; 531 dwPos = 3; 532 } 533 else 534 { 535 /* 536 * Try using statistical analysis. Do not rely on the return value of 537 * IsTextUnicode as we can get FALSE even if the text is in UTF-16 BE 538 * (i.e. we have some of the IS_TEXT_UNICODE_REVERSE_MASK bits set). 539 * Instead, set all the tests we want to perform, then just check 540 * the passed tests and try to deduce the string properties. 541 */ 542 543 /* 544 * This mask contains the 3 highest bits from IS_TEXT_UNICODE_NOT_ASCII_MASK 545 * and the 1st highest bit from IS_TEXT_UNICODE_NOT_UNICODE_MASK. 546 */ 547 #define IS_TEXT_UNKNOWN_FLAGS_MASK ((7 << 13) | (1 << 11)) 548 549 /* Flag out the unknown flags here, the passed tests will not have them either */ 550 INT Tests = (IS_TEXT_UNICODE_NOT_ASCII_MASK | 551 IS_TEXT_UNICODE_NOT_UNICODE_MASK | 552 IS_TEXT_UNICODE_REVERSE_MASK | IS_TEXT_UNICODE_UNICODE_MASK) 553 & ~IS_TEXT_UNKNOWN_FLAGS_MASK; 554 INT Results; 555 556 IsTextUnicode(Buffer, BufferSize, &Tests); 557 Results = Tests; 558 559 /* 560 * As the IS_TEXT_UNICODE_NULL_BYTES or IS_TEXT_UNICODE_ILLEGAL_CHARS 561 * flags are expected to be potentially present in the result without 562 * modifying our expectations, filter them out now. 563 */ 564 Results &= ~(IS_TEXT_UNICODE_NULL_BYTES | IS_TEXT_UNICODE_ILLEGAL_CHARS); 565 566 /* 567 * NOTE: The flags IS_TEXT_UNICODE_ASCII16 and 568 * IS_TEXT_UNICODE_REVERSE_ASCII16 are not reliable. 569 * 570 * NOTE2: Check for potential "bush hid the facts" effect by also 571 * checking the original results (in 'Tests') for the absence of 572 * the IS_TEXT_UNICODE_NULL_BYTES flag, as we may presumably expect 573 * that in UTF-16 text there will be at some point some NULL bytes. 574 * If not, fall back to ANSI. This shows the limitations of using the 575 * IsTextUnicode API to perform such tests, and the usage of a more 576 * improved encoding detection algorithm would be really welcome. 577 */ 578 if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) && 579 !(Results & IS_TEXT_UNICODE_REVERSE_MASK) && 580 (Results & IS_TEXT_UNICODE_UNICODE_MASK) && 581 (Tests & IS_TEXT_UNICODE_NULL_BYTES)) 582 { 583 encFile = ENCODING_UTF16LE; 584 dwPos = (Results & IS_TEXT_UNICODE_SIGNATURE) ? 2 : 0; 585 } 586 else 587 if (!(Results & IS_TEXT_UNICODE_NOT_UNICODE_MASK) && 588 !(Results & IS_TEXT_UNICODE_UNICODE_MASK) && 589 (Results & IS_TEXT_UNICODE_REVERSE_MASK) && 590 (Tests & IS_TEXT_UNICODE_NULL_BYTES)) 591 { 592 encFile = ENCODING_UTF16BE; 593 dwPos = (Results & IS_TEXT_UNICODE_REVERSE_SIGNATURE) ? 2 : 0; 594 } 595 else 596 { 597 /* 598 * Either 'Results' has neither of those masks set, as it can be 599 * the case for UTF-8 text (or ANSI), or it has both as can be the 600 * case when analysing pure binary data chunk. This is therefore 601 * invalid and we fall back to ANSI encoding. 602 * FIXME: In case of failure, assume ANSI (as long as we do not have 603 * correct tests for UTF8, otherwise we should do them, and at the 604 * very end, assume ANSI). 605 */ 606 encFile = ENCODING_ANSI; // ENCODING_UTF8; 607 dwPos = 0; 608 } 609 } 610 611 if (Encoding) 612 *Encoding = encFile; 613 if (SkipBytes) 614 *SkipBytes = dwPos; 615 616 return (encFile != ENCODING_ANSI); 617 } 618 619 /* 620 * Adapted from base/shell/cmd/misc.c!FileGetString(), but with correct 621 * text encoding support. Also please note that similar code should be 622 * also used in the CMD.EXE 'TYPE' command. 623 * Contrary to CMD's FileGetString() we do not stop at new-lines. 624 * 625 * Read text data from a file and convert it from a given encoding to UTF-16. 626 * 627 * IN OUT PVOID pCacheBuffer and IN DWORD CacheBufferLength : 628 * Implementation detail so that the function uses an external user-provided 629 * buffer to store the data temporarily read from the file. The function 630 * could have used an internal buffer instead. The length is in number of bytes. 631 * 632 * IN OUT PWSTR* pBuffer and IN OUT PDWORD pnBufferLength : 633 * Reallocated buffer containing the string data converted to UTF-16. 634 * In input, contains a pointer to the original buffer and its length. 635 * In output, contains a pointer to the reallocated buffer and its length. 636 * The length is in number of characters. 637 * 638 * At first call to this function, pBuffer can be set to NULL, in which case 639 * when the function returns the pointer will point to a valid buffer. 640 * After the last call to this function, free the pBuffer pointer with: 641 * HeapFree(GetProcessHeap(), 0, *pBuffer); 642 * 643 * If Encoding is set to ENCODING_UTF16LE or ENCODING_UTF16BE, since we are 644 * compiled in UNICODE, no extra conversion is performed and therefore 645 * pBuffer is unused (remains unallocated) and one can directly use the 646 * contents of pCacheBuffer as it is expected to contain valid UTF-16 text. 647 * 648 * OUT PDWORD pdwReadBytes : Number of bytes read from the file (optional). 649 * OUT PDWORD pdwReadChars : Corresponding number of characters read (optional). 650 */ 651 static BOOL 652 FileGetString( 653 IN HANDLE hFile, 654 IN ENCODING Encoding, 655 IN OUT PVOID pCacheBuffer, 656 IN DWORD CacheBufferLength, 657 IN OUT PWCHAR* pBuffer, 658 IN OUT PDWORD pnBufferLength, 659 OUT PDWORD pdwReadBytes OPTIONAL, 660 OUT PDWORD pdwReadChars OPTIONAL) 661 { 662 BOOL Success; 663 UINT CodePage = (UINT)-1; 664 DWORD dwReadBytes; 665 INT len; 666 667 // ASSERT(pCacheBuffer && (CacheBufferLength > 0)); 668 // ASSERT(CacheBufferLength % 2 == 0); // Cache buffer length MUST BE even! 669 // ASSERT(pBuffer && pnBufferLength); 670 671 /* Always reset the retrieved number of bytes/characters */ 672 if (pdwReadBytes) *pdwReadBytes = 0; 673 if (pdwReadChars) *pdwReadChars = 0; 674 675 Success = ReadFile(hFile, pCacheBuffer, CacheBufferLength, &dwReadBytes, NULL); 676 if (!Success || dwReadBytes == 0) 677 return FALSE; 678 679 if (pdwReadBytes) *pdwReadBytes = dwReadBytes; 680 681 if ((Encoding == ENCODING_ANSI) || (Encoding == ENCODING_UTF8)) 682 { 683 /* Conversion is needed */ 684 685 if (Encoding == ENCODING_ANSI) 686 CodePage = GetConsoleCP(); // CP_ACP; // FIXME: Cache GetConsoleCP() value. 687 else // if (Encoding == ENCODING_UTF8) 688 CodePage = CP_UTF8; 689 690 /* Retrieve the needed buffer size */ 691 len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes, 692 NULL, 0); 693 if (len == 0) 694 { 695 /* Failure, bail out */ 696 return FALSE; 697 } 698 699 /* Initialize the conversion buffer if needed... */ 700 if (*pBuffer == NULL) 701 { 702 *pnBufferLength = len; 703 *pBuffer = HeapAlloc(GetProcessHeap(), 0, *pnBufferLength * sizeof(WCHAR)); 704 if (*pBuffer == NULL) 705 { 706 // *pBuffer = NULL; 707 *pnBufferLength = 0; 708 // WARN("DEBUG: Cannot allocate memory for *pBuffer!\n"); 709 // ConErrFormatMessage(GetLastError()); 710 return FALSE; 711 } 712 } 713 /* ... or reallocate only if the new length is greater than the old one */ 714 else if (len > *pnBufferLength) 715 { 716 PWSTR OldBuffer = *pBuffer; 717 718 *pnBufferLength = len; 719 *pBuffer = HeapReAlloc(GetProcessHeap(), 0, *pBuffer, *pnBufferLength * sizeof(WCHAR)); 720 if (*pBuffer == NULL) 721 { 722 /* Do not leak old buffer */ 723 HeapFree(GetProcessHeap(), 0, OldBuffer); 724 // *pBuffer = NULL; 725 *pnBufferLength = 0; 726 // WARN("DEBUG: Cannot reallocate memory for *pBuffer!\n"); 727 // ConErrFormatMessage(GetLastError()); 728 return FALSE; 729 } 730 } 731 732 /* Now perform the conversion proper */ 733 len = MultiByteToWideChar(CodePage, 0, pCacheBuffer, dwReadBytes, 734 *pBuffer, len); 735 dwReadBytes = len; 736 } 737 else 738 { 739 /* 740 * No conversion needed, just convert from big to little endian if needed. 741 * pBuffer and pnBufferLength are left untouched and pCacheBuffer can be 742 * directly used. 743 */ 744 PWCHAR pWChars = pCacheBuffer; 745 DWORD i; 746 747 dwReadBytes /= sizeof(WCHAR); 748 749 if (Encoding == ENCODING_UTF16BE) 750 { 751 for (i = 0; i < dwReadBytes; i++) 752 { 753 /* Equivalent to RtlUshortByteSwap: reverse high/low bytes */ 754 pWChars[i] = MAKEWORD(HIBYTE(pWChars[i]), LOBYTE(pWChars[i])); 755 } 756 } 757 // else if (Encoding == ENCODING_UTF16LE), we are good, nothing to do. 758 } 759 760 /* Return the number of characters (dwReadBytes is converted) */ 761 if (pdwReadChars) *pdwReadChars = dwReadBytes; 762 763 return TRUE; 764 } 765 766 static VOID 767 LoadRegistrySettings(HKEY hKeyRoot) 768 { 769 LONG lRet; 770 HKEY hKey; 771 DWORD dwType, len; 772 /* 773 * Buffer big enough to hold the string L"4294967295", 774 * corresponding to the literal 0xFFFFFFFF (MAXULONG) in decimal. 775 */ 776 WCHAR Buffer[sizeof("4294967295")]; 777 C_ASSERT(sizeof(Buffer) >= sizeof(DWORD)); 778 779 lRet = RegOpenKeyExW(hKeyRoot, 780 L"Software\\Microsoft\\Command Processor", 781 0, 782 KEY_QUERY_VALUE, 783 &hKey); 784 if (lRet != ERROR_SUCCESS) 785 return; 786 787 len = sizeof(Buffer); 788 lRet = RegQueryValueExW(hKey, 789 L"EnableExtensions", 790 NULL, 791 &dwType, 792 (PBYTE)&Buffer, 793 &len); 794 if (lRet == ERROR_SUCCESS) 795 { 796 /* Overwrite the default setting */ 797 if (dwType == REG_DWORD) 798 bEnableExtensions = !!*(PDWORD)Buffer; 799 else if (dwType == REG_SZ) 800 bEnableExtensions = (_wtol((PWSTR)Buffer) == 1); 801 } 802 // else, use the default setting set globally. 803 804 RegCloseKey(hKey); 805 } 806 807 static BOOL IsFlag(PCWSTR param) 808 { 809 PCWSTR pch; 810 PWCHAR endptr; 811 812 if (param[0] == L'/') 813 return TRUE; 814 815 if (param[0] == L'+') 816 { 817 pch = param + 1; 818 if (*pch) 819 { 820 (void)wcstol(pch, &endptr, 10); 821 return (*endptr == 0); 822 } 823 } 824 return FALSE; 825 } 826 827 static BOOL ParseArgument(PCWSTR arg, BOOL* pbHasFiles) 828 { 829 PWCHAR endptr; 830 831 if (arg[0] == L'/') 832 { 833 switch (towupper(arg[1])) 834 { 835 case L'?': 836 if (arg[2] == 0) 837 { 838 s_dwFlags |= FLAG_HELP; 839 return TRUE; 840 } 841 break; 842 case L'E': 843 if (arg[2] == 0) 844 { 845 s_dwFlags |= FLAG_E; 846 return TRUE; 847 } 848 break; 849 case L'C': 850 if (arg[2] == 0) 851 { 852 s_dwFlags |= FLAG_C; 853 return TRUE; 854 } 855 break; 856 case L'P': 857 if (arg[2] == 0) 858 { 859 s_dwFlags |= FLAG_P; 860 return TRUE; 861 } 862 break; 863 case L'S': 864 if (arg[2] == 0) 865 { 866 s_dwFlags |= FLAG_S; 867 return TRUE; 868 } 869 break; 870 case L'T': 871 if (arg[2] != 0) 872 { 873 s_dwFlags |= FLAG_Tn; 874 s_nTabWidth = wcstol(&arg[2], &endptr, 10); 875 if (*endptr == 0) 876 return TRUE; 877 } 878 break; 879 default: 880 break; 881 } 882 } 883 else if (arg[0] == L'+') 884 { 885 if (arg[1] != 0) 886 { 887 s_dwFlags |= FLAG_PLUSn; 888 s_nNextLineNo = wcstol(&arg[1], &endptr, 10) + 1; 889 if (*endptr == 0) 890 return TRUE; 891 } 892 } 893 894 if (IsFlag(arg)) 895 { 896 ConResPrintf(StdErr, IDS_BAD_FLAG, arg); 897 return FALSE; 898 } 899 else 900 { 901 *pbHasFiles = TRUE; 902 } 903 904 return TRUE; 905 } 906 907 static BOOL ParseMoreVariable(BOOL* pbHasFiles) 908 { 909 BOOL ret = TRUE; 910 PWSTR psz; 911 PWCHAR pch; 912 DWORD cch; 913 914 cch = GetEnvironmentVariableW(L"MORE", NULL, 0); 915 if (cch == 0) 916 return TRUE; 917 918 psz = (PWSTR)malloc((cch + 1) * sizeof(WCHAR)); 919 if (!psz) 920 return TRUE; 921 922 if (!GetEnvironmentVariableW(L"MORE", psz, cch + 1)) 923 { 924 free(psz); 925 return TRUE; 926 } 927 928 for (pch = wcstok(psz, L" "); pch; pch = wcstok(NULL, L" ")) 929 { 930 ret = ParseArgument(pch, pbHasFiles); 931 if (!ret) 932 break; 933 } 934 935 free(psz); 936 return ret; 937 } 938 939 // INT CommandMore(LPTSTR cmd, LPTSTR param) 940 int wmain(int argc, WCHAR* argv[]) 941 { 942 // FIXME this stuff! 943 CON_SCREEN Screen = {StdOut}; 944 CON_PAGER Pager = {&Screen, 0}; 945 946 int i; 947 948 BOOL bRet, bContinue; 949 950 ENCODING Encoding; 951 DWORD SkipBytes = 0; 952 BOOL HasFiles; 953 954 #define FileCacheBufferSize 4096 955 PVOID FileCacheBuffer = NULL; 956 PWCHAR StringBuffer = NULL; 957 DWORD StringBufferLength = 0; 958 DWORD dwReadBytes = 0, dwReadChars = 0; 959 960 TCHAR szFullPath[MAX_PATH]; 961 962 hStdIn = GetStdHandle(STD_INPUT_HANDLE); 963 hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); 964 965 /* Initialize the Console Standard Streams */ 966 ConStreamInit(StdIn , GetStdHandle(STD_INPUT_HANDLE) , UTF8Text, INVALID_CP); 967 ConStreamInit(StdOut, GetStdHandle(STD_OUTPUT_HANDLE), UTF8Text, INVALID_CP); 968 ConStreamInit(StdErr, GetStdHandle(STD_ERROR_HANDLE) , UTF8Text, INVALID_CP); 969 970 /* 971 * Bad usage (too much options) or we use the /? switch. 972 * Display help for the MORE command. 973 */ 974 if (argc > 1 && wcscmp(argv[1], L"/?") == 0) 975 { 976 ConResPuts(StdOut, IDS_USAGE); 977 return 0; 978 } 979 980 /* Load the registry settings */ 981 LoadRegistrySettings(HKEY_LOCAL_MACHINE); 982 LoadRegistrySettings(HKEY_CURRENT_USER); 983 if (bEnableExtensions) 984 s_dwFlags |= FLAG_E; 985 986 // NOTE: We might try to duplicate the ConOut for read access... ? 987 hKeyboard = CreateFileW(L"CONIN$", GENERIC_READ|GENERIC_WRITE, 988 FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, 989 OPEN_EXISTING, 0, NULL); 990 FlushConsoleInputBuffer(hKeyboard); 991 ConStreamSetOSHandle(StdIn, hKeyboard); 992 993 FileCacheBuffer = HeapAlloc(GetProcessHeap(), 0, FileCacheBufferSize); 994 if (!FileCacheBuffer) 995 { 996 ConPuts(StdErr, L"Error: no memory\n"); 997 CloseHandle(hKeyboard); 998 return 1; 999 } 1000 1001 /* First, load the "MORE" environment variable and parse it, 1002 * then parse the command-line parameters. */ 1003 HasFiles = FALSE; 1004 if (!ParseMoreVariable(&HasFiles)) 1005 return 1; 1006 for (i = 1; i < argc; i++) 1007 { 1008 if (!ParseArgument(argv[i], &HasFiles)) 1009 return 1; 1010 } 1011 1012 if (s_dwFlags & FLAG_HELP) 1013 { 1014 ConResPuts(StdOut, IDS_USAGE); 1015 return 0; 1016 } 1017 1018 Pager.PagerLine = MorePagerLine; 1019 Pager.dwFlags |= CON_PAGER_EXPAND_TABS | CON_PAGER_CACHE_INCOMPLETE_LINE; 1020 if (s_dwFlags & FLAG_P) 1021 Pager.dwFlags |= CON_PAGER_EXPAND_FF; 1022 Pager.nTabWidth = s_nTabWidth; 1023 1024 /* Special case where we run 'MORE' without any argument: we use STDIN */ 1025 if (!HasFiles) 1026 { 1027 /* 1028 * Assign STDIN handle to hFile so that the page prompt function will 1029 * know the data comes from STDIN, and will take different actions. 1030 */ 1031 hFile = hStdIn; 1032 1033 /* Update the statistics for PagePrompt */ 1034 dwFileSize = 0; 1035 dwSumReadBytes = dwSumReadChars = 0; 1036 1037 /* We suppose we read text from the file */ 1038 1039 /* For STDIN we always suppose we are in ANSI mode */ 1040 // SetFilePointer(hFile, 0, NULL, FILE_BEGIN); 1041 Encoding = ENCODING_ANSI; // ENCODING_UTF8; 1042 1043 /* Start paging */ 1044 bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0); 1045 if (!bContinue) 1046 goto Quit; 1047 1048 do 1049 { 1050 bRet = FileGetString(hFile, Encoding, 1051 FileCacheBuffer, FileCacheBufferSize, 1052 &StringBuffer, &StringBufferLength, 1053 &dwReadBytes, &dwReadChars); 1054 if (!bRet || dwReadBytes == 0 || dwReadChars == 0) 1055 { 1056 /* We failed at reading the file, bail out */ 1057 break; 1058 } 1059 1060 /* Update the statistics for PagePrompt */ 1061 dwSumReadBytes += dwReadBytes; 1062 dwSumReadChars += dwReadChars; 1063 1064 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, 1065 StringBuffer, dwReadChars); 1066 /* If we Ctrl-C/Ctrl-Break, stop everything */ 1067 if (!bContinue) 1068 break; 1069 } 1070 while (bRet && dwReadBytes > 0); 1071 1072 /* Flush any cached pager buffers */ 1073 if (bContinue) 1074 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0); 1075 1076 goto Quit; 1077 } 1078 1079 /* We have files: read them and output them to STDOUT */ 1080 for (i = 1; i < argc; i++) 1081 { 1082 if (IsFlag(argv[i])) 1083 continue; 1084 1085 GetFullPathNameW(argv[i], ARRAYSIZE(szFullPath), szFullPath, NULL); 1086 hFile = CreateFileW(szFullPath, 1087 GENERIC_READ, 1088 FILE_SHARE_READ, 1089 NULL, 1090 OPEN_EXISTING, 1091 0, // FILE_ATTRIBUTE_NORMAL, 1092 NULL); 1093 if (hFile == INVALID_HANDLE_VALUE) 1094 { 1095 ConResPrintf(StdErr, IDS_FILE_ACCESS, szFullPath); 1096 goto Quit; 1097 } 1098 1099 /* We currently do not support files too big */ 1100 dwFileSize = GetFileSize(hFile, NULL); 1101 if (dwFileSize == INVALID_FILE_SIZE) 1102 { 1103 ConPuts(StdErr, L"ERROR: Invalid file size!\n"); 1104 CloseHandle(hFile); 1105 continue; 1106 } 1107 1108 /* We suppose we read text from the file */ 1109 1110 /* Check whether the file is UNICODE and retrieve its encoding */ 1111 SetFilePointer(hFile, 0, NULL, FILE_BEGIN); 1112 bRet = ReadFile(hFile, FileCacheBuffer, FileCacheBufferSize, &dwReadBytes, NULL); 1113 IsDataUnicode(FileCacheBuffer, dwReadBytes, &Encoding, &SkipBytes); 1114 SetFilePointer(hFile, SkipBytes, NULL, FILE_BEGIN); 1115 1116 /* Reset state for paging */ 1117 s_nNextLineNo = 0; 1118 s_bPrevLineIsBlank = FALSE; 1119 s_fPrompt = PROMPT_PERCENT; 1120 s_bDoNextFile = FALSE; 1121 1122 /* Update the statistics for PagePrompt */ 1123 dwSumReadBytes = dwSumReadChars = 0; 1124 1125 /* Start paging */ 1126 bContinue = ConWritePaging(&Pager, PagePrompt, TRUE, NULL, 0); 1127 if (!bContinue) 1128 { 1129 /* We stop displaying this file */ 1130 CloseHandle(hFile); 1131 if (s_bDoNextFile) 1132 { 1133 /* Bail out and continue with the other files */ 1134 continue; 1135 } 1136 1137 /* We Ctrl-C/Ctrl-Break, stop everything */ 1138 goto Quit; 1139 } 1140 1141 do 1142 { 1143 bRet = FileGetString(hFile, Encoding, 1144 FileCacheBuffer, FileCacheBufferSize, 1145 &StringBuffer, &StringBufferLength, 1146 &dwReadBytes, &dwReadChars); 1147 if (!bRet || dwReadBytes == 0 || dwReadChars == 0) 1148 { 1149 /* 1150 * We failed at reading the file, bail out 1151 * and continue with the other files. 1152 */ 1153 break; 1154 } 1155 1156 /* Update the statistics for PagePrompt */ 1157 dwSumReadBytes += dwReadBytes; 1158 dwSumReadChars += dwReadChars; 1159 1160 if ((Encoding == ENCODING_UTF16LE) || (Encoding == ENCODING_UTF16BE)) 1161 { 1162 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, 1163 FileCacheBuffer, dwReadChars); 1164 } 1165 else 1166 { 1167 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, 1168 StringBuffer, dwReadChars); 1169 } 1170 if (!bContinue) 1171 { 1172 /* We stop displaying this file */ 1173 break; 1174 } 1175 } 1176 while (bRet && dwReadBytes > 0); 1177 1178 /* Flush any cached pager buffers */ 1179 if (bContinue) 1180 bContinue = ConWritePaging(&Pager, PagePrompt, FALSE, NULL, 0); 1181 1182 CloseHandle(hFile); 1183 1184 /* Check whether we should stop displaying this file */ 1185 if (!bContinue) 1186 { 1187 if (s_bDoNextFile) 1188 { 1189 /* Bail out and continue with the other files */ 1190 continue; 1191 } 1192 1193 /* We Ctrl-C/Ctrl-Break, stop everything */ 1194 goto Quit; 1195 } 1196 } 1197 1198 Quit: 1199 if (StringBuffer) HeapFree(GetProcessHeap(), 0, StringBuffer); 1200 HeapFree(GetProcessHeap(), 0, FileCacheBuffer); 1201 CloseHandle(hKeyboard); 1202 return 0; 1203 } 1204 1205 /* EOF */ 1206