xref: /reactos/base/applications/cmdutils/more/more.c (revision a3019731)
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     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 
202 Restart:
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/AutoIt/text-encoding-detect
491  */
492 typedef enum
493 {
494     ENCODING_ANSI    =  0,
495     ENCODING_UTF16LE =  1,
496     ENCODING_UTF16BE =  2,
497     ENCODING_UTF8    =  3
498 } ENCODING;
499 
500 static BOOL
501 IsDataUnicode(
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  */
652 static BOOL
653 FileGetString(
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 
767 static VOID
768 LoadRegistrySettings(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 
808 static 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 
828 static 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 
908 static 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)
941 int 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 
1199 Quit:
1200     if (StringBuffer) HeapFree(GetProcessHeap(), 0, StringBuffer);
1201     HeapFree(GetProcessHeap(), 0, FileCacheBuffer);
1202     CloseHandle(hKeyboard);
1203     return 0;
1204 }
1205 
1206 /* EOF */
1207