1 /* 2 * Copyright (C) 1984-2025 Mark Nudelman 3 * 4 * You may distribute under the terms of either the GNU General Public 5 * License or the Less License, as specified in the README file. 6 * 7 * For more information, see the README file. 8 */ 9 10 11 /* 12 * Entry point, initialization, miscellaneous routines. 13 */ 14 15 #include "less.h" 16 #if MSDOS_COMPILER==WIN32C 17 #define WIN32_LEAN_AND_MEAN 18 #include <windows.h> 19 20 #if defined(__MINGW32__) || defined(_MSC_VER) 21 #include <locale.h> 22 #include <shellapi.h> 23 #endif 24 25 public unsigned less_acp = CP_ACP; 26 #endif 27 28 #include "option.h" 29 30 public char * every_first_cmd = NULL; 31 public lbool new_file; 32 public int is_tty; 33 public IFILE curr_ifile = NULL_IFILE; 34 public IFILE old_ifile = NULL_IFILE; 35 public struct scrpos initial_scrpos; 36 public POSITION start_attnpos = NULL_POSITION; 37 public POSITION end_attnpos = NULL_POSITION; 38 public int wscroll; 39 public constant char *progname; 40 public lbool quitting = FALSE; 41 public int dohelp; 42 public char * init_header = NULL; 43 static int secure_allow_features; 44 45 #if LOGFILE 46 public int logfile = -1; 47 public lbool force_logfile = FALSE; 48 public char * namelogfile = NULL; 49 #endif 50 51 #if EDITOR 52 public constant char * editor; 53 public constant char * editproto; 54 #endif 55 56 #if TAGS 57 extern char * tags; 58 extern char * tagoption; 59 extern int jump_sline; 60 #endif 61 62 #if HAVE_TIME 63 public time_type less_start_time; 64 #endif 65 66 #ifdef WIN32 67 static wchar_t consoleTitle[256]; 68 #endif 69 70 public int one_screen; 71 extern int less_is_more; 72 extern lbool missing_cap; 73 extern int know_dumb; 74 extern int quit_if_one_screen; 75 extern int no_init; 76 extern int errmsgs; 77 extern int redraw_on_quit; 78 extern int term_init_done; 79 extern lbool first_time; 80 81 #if MSDOS_COMPILER==WIN32C && (defined(__MINGW32__) || defined(_MSC_VER)) 82 /* malloc'ed 0-terminated utf8 of 0-terminated wide ws, or null on errors */ 83 static char *utf8_from_wide(constant wchar_t *ws) 84 { 85 char *u8 = NULL; 86 int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); 87 if (n > 0) 88 { 89 u8 = ecalloc(n, sizeof(char)); 90 WideCharToMultiByte(CP_UTF8, 0, ws, -1, u8, n, NULL, NULL); 91 } 92 return u8; 93 } 94 95 /* 96 * similar to using UTF8 manifest to make the ANSI APIs UTF8, but dynamically 97 * with setlocale. unlike the manifest, argv and environ are already ACP, so 98 * make them UTF8. Additionally, this affects only the libc/crt API, and so 99 * e.g. fopen filename becomes UTF-8, but CreateFileA filename remains CP_ACP. 100 * CP_ACP remains the original codepage - use the dynamic less_acp instead. 101 * effective on win 10 1803 or later when compiled with ucrt, else no-op. 102 */ 103 static void try_utf8_locale(int *pargc, constant char ***pargv) 104 { 105 char *locale_orig = strdup(setlocale(LC_ALL, NULL)); 106 wchar_t **wargv = NULL, *wenv, *wp; 107 constant char **u8argv; 108 char *u8e; 109 int i, n; 110 111 if (!setlocale(LC_ALL, ".UTF8")) 112 goto cleanup; /* not win10 1803+ or not ucrt */ 113 114 /* 115 * wargv is before glob expansion. some ucrt builds may expand globs 116 * before main is entered, so n may be smaller than the original argc. 117 * that's ok, because later code at main expands globs anyway. 118 */ 119 wargv = CommandLineToArgvW(GetCommandLineW(), &n); 120 if (!wargv) 121 goto bad_args; 122 123 u8argv = (constant char **) ecalloc(n + 1, sizeof(char *)); 124 for (i = 0; i < n; ++i) 125 { 126 if (!(u8argv[i] = utf8_from_wide(wargv[i]))) 127 goto bad_args; 128 } 129 u8argv[n] = 0; 130 131 less_acp = CP_UTF8; 132 *pargc = n; 133 *pargv = u8argv; /* leaked on exit */ 134 135 /* convert wide env to utf8 where we can, but don't abort on errors */ 136 if ((wenv = GetEnvironmentStringsW())) 137 { 138 for (wp = wenv; *wp; wp += wcslen(wp) + 1) 139 { 140 if ((u8e = utf8_from_wide(wp))) 141 _putenv(u8e); 142 free(u8e); /* windows putenv makes a copy */ 143 } 144 FreeEnvironmentStringsW(wenv); 145 } 146 147 goto cleanup; 148 149 bad_args: 150 error("WARNING: cannot use unicode arguments", NULL_PARG); 151 setlocale(LC_ALL, locale_orig); 152 153 cleanup: 154 free(locale_orig); 155 LocalFree(wargv); 156 } 157 #endif 158 159 #if !SECURE 160 static int security_feature_error(constant char *type, size_t len, constant char *name) 161 { 162 PARG parg; 163 size_t msglen = len + strlen(type) + 64; 164 char *msg = ecalloc(msglen, sizeof(char)); 165 SNPRINTF3(msg, msglen, "LESSSECURE_ALLOW: %s feature name \"%.*s\"", type, (int) len, name); 166 parg.p_string = msg; 167 error("%s", &parg); 168 free(msg); 169 return 0; 170 } 171 172 /* 173 * Return the SF_xxx value of a secure feature given the name of the feature. 174 */ 175 static int security_feature(constant char *name, size_t len) 176 { 177 struct secure_feature { constant char *name; int sf_value; }; 178 static struct secure_feature features[] = { 179 { "edit", SF_EDIT }, 180 { "examine", SF_EXAMINE }, 181 { "glob", SF_GLOB }, 182 { "history", SF_HISTORY }, 183 { "lesskey", SF_LESSKEY }, 184 { "lessopen", SF_LESSOPEN }, 185 { "logfile", SF_LOGFILE }, 186 { "osc8", SF_OSC8_OPEN }, 187 { "pipe", SF_PIPE }, 188 { "shell", SF_SHELL }, 189 { "stop", SF_STOP }, 190 { "tags", SF_TAGS }, 191 }; 192 int i; 193 int match = -1; 194 195 for (i = 0; i < countof(features); i++) 196 { 197 if (strncmp(features[i].name, name, len) == 0) 198 { 199 if (match >= 0) /* name is ambiguous */ 200 return security_feature_error("ambiguous", len, name); 201 match = i; 202 } 203 } 204 if (match < 0) 205 return security_feature_error("invalid", len, name); 206 return features[match].sf_value; 207 } 208 #endif /* !SECURE */ 209 210 /* 211 * Set the secure_allow_features bitmask, which controls 212 * whether certain secure features are allowed. 213 */ 214 static void init_secure(void) 215 { 216 #if SECURE 217 secure_allow_features = 0; 218 #else 219 constant char *str = lgetenv("LESSSECURE"); 220 if (isnullenv(str)) 221 secure_allow_features = ~0; /* allow everything */ 222 else 223 secure_allow_features = 0; /* allow nothing */ 224 225 str = lgetenv("LESSSECURE_ALLOW"); 226 if (!isnullenv(str)) 227 { 228 for (;;) 229 { 230 constant char *estr; 231 while (*str == ' ' || *str == ',') ++str; /* skip leading spaces/commas */ 232 if (*str == '\0') break; 233 estr = strchr(str, ','); 234 if (estr == NULL) estr = str + strlen(str); 235 while (estr > str && estr[-1] == ' ') --estr; /* trim trailing spaces */ 236 secure_allow_features |= security_feature(str, ptr_diff(estr, str)); 237 str = estr; 238 } 239 } 240 #endif 241 } 242 243 /* 244 * Entry point. 245 */ 246 int main(int argc, constant char *argv[]) 247 { 248 IFILE ifile; 249 constant char *s; 250 int i; 251 struct xbuffer xfiles; 252 constant int *files; 253 size_t num_files; 254 lbool end_opts = FALSE; 255 lbool posixly_correct = FALSE; 256 257 #if MSDOS_COMPILER==WIN32C && (defined(__MINGW32__) || defined(_MSC_VER)) 258 if (GetACP() != CP_UTF8) /* not using a UTF-8 manifest */ 259 try_utf8_locale(&argc, &argv); 260 #endif 261 262 #ifdef __EMX__ 263 _response(&argc, &argv); 264 _wildcard(&argc, &argv); 265 #endif 266 267 progname = *argv++; 268 argc--; 269 init_secure(); 270 271 #ifdef WIN32 272 if (getenv("HOME") == NULL) 273 { 274 /* 275 * If there is no HOME environment variable, 276 * try the concatenation of HOMEDRIVE + HOMEPATH. 277 */ 278 char *drive = getenv("HOMEDRIVE"); 279 char *path = getenv("HOMEPATH"); 280 if (drive != NULL && path != NULL) 281 { 282 char *env = (char *) ecalloc(strlen(drive) + 283 strlen(path) + 6, sizeof(char)); 284 strcpy(env, "HOME="); 285 strcat(env, drive); 286 strcat(env, path); 287 putenv(env); 288 } 289 } 290 /* on failure, consoleTitle is already a valid empty string */ 291 GetConsoleTitleW(consoleTitle, countof(consoleTitle)); 292 #endif /* WIN32 */ 293 294 /* 295 * Process command line arguments and LESS environment arguments. 296 * Command line arguments override environment arguments. 297 */ 298 is_tty = isatty(1); 299 init_mark(); 300 init_cmds(); 301 init_poll(); 302 init_charset(); 303 init_line(); 304 init_cmdhist(); 305 init_option(); 306 init_search(); 307 308 /* 309 * If the name of the executable program is "more", 310 * act like LESS_IS_MORE is set. 311 */ 312 if (strcmp(last_component(progname), "more") == 0 && 313 isnullenv(lgetenv("LESS_IS_MORE"))) { 314 less_is_more = 1; 315 scan_option("-fG", FALSE); 316 } 317 318 init_prompt(); 319 320 init_unsupport(); 321 s = lgetenv(less_is_more ? "MORE" : "LESS"); 322 if (s != NULL) 323 scan_option(s, TRUE); 324 325 #define isoptstring(s) less_is_more ? (((s)[0] == '-') && (s)[1] != '\0') : \ 326 (((s)[0] == '-' || (s)[0] == '+') && (s)[1] != '\0') 327 xbuf_init(&xfiles); 328 posixly_correct = (getenv("POSIXLY_CORRECT") != NULL); 329 for (i = 0; i < argc; i++) 330 { 331 if (strcmp(argv[i], "--") == 0) 332 end_opts = TRUE; 333 else if (!end_opts && (isoptstring(argv[i]) || isoptpending())) 334 scan_option(argv[i], FALSE); 335 else 336 { 337 if (posixly_correct) 338 end_opts = TRUE; 339 xbuf_add_data(&xfiles, (constant unsigned char *) &i, sizeof(i)); 340 } 341 } 342 #undef isoptstring 343 344 if (isoptpending()) 345 { 346 /* 347 * Last command line option was a flag requiring a 348 * following string, but there was no following string. 349 */ 350 nopendopt(); 351 quit(QUIT_OK); 352 } 353 354 if (less_is_more) 355 no_init = TRUE; 356 357 get_term(); 358 expand_cmd_tables(); 359 360 #if EDITOR 361 editor = lgetenv("VISUAL"); 362 if (isnullenv(editor)) 363 { 364 editor = lgetenv("EDITOR"); 365 if (isnullenv(editor)) 366 editor = EDIT_PGM; 367 } 368 editproto = lgetenv("LESSEDIT"); 369 if (isnullenv(editproto)) 370 editproto = "%E ?lm+%lm. %g"; 371 #endif 372 373 /* 374 * Call get_ifile with all the command line filenames 375 * to "register" them with the ifile system. 376 */ 377 ifile = NULL_IFILE; 378 if (dohelp) 379 ifile = get_ifile(FAKE_HELPFILE, ifile); 380 files = (constant int *) xfiles.data; 381 num_files = xfiles.end / sizeof(int); 382 for (i = 0; i < num_files; i++) 383 { 384 #if (MSDOS_COMPILER && MSDOS_COMPILER != DJGPPC) 385 /* 386 * Because the "shell" doesn't expand filename patterns, 387 * treat each argument as a filename pattern rather than 388 * a single filename. 389 * Expand the pattern and iterate over the expanded list. 390 */ 391 struct textlist tlist; 392 constant char *filename; 393 char *gfilename; 394 char *qfilename; 395 396 gfilename = lglob(argv[files[i]]); 397 init_textlist(&tlist, gfilename); 398 filename = NULL; 399 while ((filename = forw_textlist(&tlist, filename)) != NULL) 400 { 401 qfilename = shell_unquote(filename); 402 (void) get_ifile(qfilename, ifile); 403 free(qfilename); 404 ifile = prev_ifile(NULL_IFILE); 405 } 406 free(gfilename); 407 #else 408 (void) get_ifile(argv[files[i]], ifile); 409 ifile = prev_ifile(NULL_IFILE); 410 #endif 411 } 412 xbuf_deinit(&xfiles); 413 414 /* 415 * Set up terminal, etc. 416 */ 417 if (!is_tty) 418 { 419 /* 420 * Output is not a tty. 421 * Just copy the input file(s) to output. 422 */ 423 set_output(1); /* write to stdout */ 424 SET_BINARY(1); 425 if (edit_first() == 0) 426 { 427 do { 428 cat_file(); 429 } while (edit_next(1) == 0); 430 } 431 quit(QUIT_OK); 432 } 433 434 if (missing_cap && !know_dumb && !less_is_more) 435 error("WARNING: terminal is not fully functional", NULL_PARG); 436 open_getchr(); 437 raw_mode(1); 438 init_signals(1); 439 #if HAVE_TIME 440 less_start_time = get_time(); 441 #endif 442 443 /* 444 * Select the first file to examine. 445 */ 446 #if TAGS 447 if (tagoption != NULL || strcmp(tags, "-") == 0) 448 { 449 /* 450 * A -t option was given. 451 * Verify that no filenames were also given. 452 * Edit the file selected by the "tags" search, 453 * and search for the proper line in the file. 454 */ 455 if (nifile() > 0) 456 { 457 error("No filenames allowed with -t option", NULL_PARG); 458 quit(QUIT_ERROR); 459 } 460 findtag(tagoption); 461 if (edit_tagfile()) /* Edit file which contains the tag */ 462 quit(QUIT_ERROR); 463 /* 464 * Search for the line which contains the tag. 465 * Set up initial_scrpos so we display that line. 466 */ 467 initial_scrpos.pos = tagsearch(); 468 if (initial_scrpos.pos == NULL_POSITION) 469 quit(QUIT_ERROR); 470 initial_scrpos.ln = jump_sline; 471 } else 472 #endif 473 { 474 if (edit_first()) 475 quit(QUIT_ERROR); 476 /* 477 * See if file fits on one screen to decide whether 478 * to send terminal init. But don't need this 479 * if -X (no_init) overrides this (see init()). 480 */ 481 if (quit_if_one_screen) 482 { 483 if (nifile() > 1) /* If more than one file, -F cannot be used */ 484 quit_if_one_screen = FALSE; 485 else if (!no_init) 486 one_screen = get_one_screen(); 487 } 488 } 489 if (init_header != NULL) 490 { 491 opt_header(TOGGLE, init_header); 492 free(init_header); 493 init_header = NULL; 494 } 495 496 if (errmsgs > 0) 497 { 498 /* 499 * We displayed some messages on error output 500 * (file descriptor 2; see flush()). 501 * Before erasing the screen contents, wait for a keystroke. 502 */ 503 less_printf("Press RETURN to continue ", NULL_PARG); 504 get_return(); 505 putchr('\n'); 506 } 507 set_output(1); 508 init(); 509 commands(); 510 quit(QUIT_OK); 511 /*NOTREACHED*/ 512 return (0); 513 } 514 515 /* 516 * Copy a string to a "safe" place 517 * (that is, to a buffer allocated by calloc). 518 */ 519 public char * saven(constant char *s, size_t n) 520 { 521 char *p = (char *) ecalloc(n+1, sizeof(char)); 522 strncpy(p, s, n); 523 p[n] = '\0'; 524 return (p); 525 } 526 527 public char * save(constant char *s) 528 { 529 return saven(s, strlen(s)); 530 } 531 532 public void out_of_memory(void) 533 { 534 error("Cannot allocate memory", NULL_PARG); 535 quit(QUIT_ERROR); 536 } 537 538 /* 539 * Allocate memory. 540 * Like calloc(), but never returns an error (NULL). 541 */ 542 public void * ecalloc(size_t count, size_t size) 543 { 544 void * p; 545 546 p = (void *) calloc(count, size); 547 if (p == NULL) 548 out_of_memory(); 549 return p; 550 } 551 552 /* 553 * Skip leading spaces in a string. 554 */ 555 public char * skipsp(char *s) 556 { 557 while (*s == ' ' || *s == '\t') 558 s++; 559 return (s); 560 } 561 562 /* {{ There must be a better way. }} */ 563 public constant char * skipspc(constant char *s) 564 { 565 while (*s == ' ' || *s == '\t') 566 s++; 567 return (s); 568 } 569 570 /* 571 * See how many characters of two strings are identical. 572 * If uppercase is true, the first string must begin with an uppercase 573 * character; the remainder of the first string may be either case. 574 */ 575 public size_t sprefix(constant char *ps, constant char *s, int uppercase) 576 { 577 char c; 578 char sc; 579 size_t len = 0; 580 581 for ( ; *s != '\0'; s++, ps++) 582 { 583 c = *ps; 584 if (uppercase) 585 { 586 if (len == 0 && ASCII_IS_LOWER(c)) 587 return (0); 588 if (ASCII_IS_UPPER(c)) 589 c = ASCII_TO_LOWER(c); 590 } 591 sc = *s; 592 if (len > 0 && ASCII_IS_UPPER(sc)) 593 sc = ASCII_TO_LOWER(sc); 594 if (c != sc) 595 break; 596 len++; 597 } 598 return (len); 599 } 600 601 /* 602 * Exit the program. 603 */ 604 public void quit(int status) 605 { 606 static int save_status; 607 608 /* 609 * Put cursor at bottom left corner, clear the line, 610 * reset the terminal modes, and exit. 611 */ 612 if (status < 0) 613 status = save_status; 614 else 615 save_status = status; 616 quitting = TRUE; 617 check_altpipe_error(); 618 if (interactive()) 619 clear_bot(); 620 deinit(); 621 flush(); 622 if (redraw_on_quit && term_init_done) 623 { 624 /* 625 * The last file text displayed might have been on an 626 * alternate screen, which now (since deinit) cannot be seen. 627 * redraw_on_quit tells us to redraw it on the main screen. 628 */ 629 first_time = TRUE; /* Don't print "skipping" or tildes */ 630 repaint(); 631 flush(); 632 } 633 edit((char*)NULL); 634 save_cmdhist(); 635 raw_mode(0); 636 #if MSDOS_COMPILER && MSDOS_COMPILER != DJGPPC 637 /* 638 * If we don't close 2, we get some garbage from 639 * 2's buffer when it flushes automatically. 640 * I cannot track this one down RB 641 * The same bug shows up if we use ^C^C to abort. 642 */ 643 close(2); 644 #endif 645 #ifdef WIN32 646 SetConsoleTitleW(consoleTitle); 647 #endif 648 close_getchr(); 649 exit(status); 650 } 651 652 /* 653 * Are all the features in the features mask allowed by security? 654 */ 655 public int secure_allow(int features) 656 { 657 return ((secure_allow_features & features) == features); 658 } 659