xref: /reactos/base/applications/cmdutils/more/more.c (revision 463784c5)
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