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