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