xref: /reactos/base/applications/cmdutils/more/more.c (revision 2d655a48)
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 
IsBlankLine(IN PCWCH line,IN DWORD cch)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
MorePagerLine(IN OUT PCON_PAGER Pager,IN PCWCH line,IN DWORD cch)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
PagePrompt(PCON_PAGER Pager,DWORD Done,DWORD Total)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
IsDataUnicode(IN PVOID Buffer,IN DWORD BufferSize,OUT ENCODING * Encoding OPTIONAL,OUT PDWORD SkipBytes OPTIONAL)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
FileGetString(IN HANDLE hFile,IN ENCODING Encoding,IN OUT PVOID pCacheBuffer,IN DWORD CacheBufferLength,IN OUT PWCHAR * pBuffer,IN OUT PDWORD pnBufferLength,OUT PDWORD pdwReadBytes OPTIONAL,OUT PDWORD pdwReadChars OPTIONAL)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
LoadRegistrySettings(HKEY hKeyRoot)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 
IsFlag(PCWSTR param)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 
ParseArgument(PCWSTR arg,BOOL * pbHasFiles)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 
ParseMoreVariable(BOOL * pbHasFiles)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)
wmain(int argc,WCHAR * argv[])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