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(MINGW) || 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(MINGW) || 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 251 #if MSDOS_COMPILER==WIN32C && (defined(MINGW) || defined(_MSC_VER)) 252 if (GetACP() != CP_UTF8) /* not using a UTF-8 manifest */ 253 try_utf8_locale(&argc, &argv); 254 #endif 255 256 #ifdef __EMX__ 257 _response(&argc, &argv); 258 _wildcard(&argc, &argv); 259 #endif 260 261 progname = *argv++; 262 argc--; 263 init_secure(); 264 265 #ifdef WIN32 266 if (getenv("HOME") == NULL) 267 { 268 /* 269 * If there is no HOME environment variable, 270 * try the concatenation of HOMEDRIVE + HOMEPATH. 271 */ 272 char *drive = getenv("HOMEDRIVE"); 273 char *path = getenv("HOMEPATH"); 274 if (drive != NULL && path != NULL) 275 { 276 char *env = (char *) ecalloc(strlen(drive) + 277 strlen(path) + 6, sizeof(char)); 278 strcpy(env, "HOME="); 279 strcat(env, drive); 280 strcat(env, path); 281 putenv(env); 282 } 283 } 284 /* on failure, consoleTitle is already a valid empty string */ 285 GetConsoleTitleW(consoleTitle, countof(consoleTitle)); 286 #endif /* WIN32 */ 287 288 /* 289 * Process command line arguments and LESS environment arguments. 290 * Command line arguments override environment arguments. 291 */ 292 is_tty = isatty(1); 293 init_mark(); 294 init_cmds(); 295 init_poll(); 296 init_charset(); 297 init_line(); 298 init_cmdhist(); 299 init_option(); 300 init_search(); 301 302 /* 303 * If the name of the executable program is "more", 304 * act like LESS_IS_MORE is set. 305 */ 306 if (strcmp(last_component(progname), "more") == 0 && 307 isnullenv(lgetenv("LESS_IS_MORE"))) { 308 less_is_more = 1; 309 scan_option("-fG", FALSE); 310 } 311 312 init_prompt(); 313 314 init_unsupport(); 315 s = lgetenv(less_is_more ? "MORE" : "LESS"); 316 if (s != NULL) 317 scan_option(s, TRUE); 318 319 #define isoptstring(s) less_is_more ? (((s)[0] == '-') && (s)[1] != '\0') : \ 320 (((s)[0] == '-' || (s)[0] == '+') && (s)[1] != '\0') 321 while (argc > 0 && (isoptstring(*argv) || isoptpending())) 322 { 323 s = *argv++; 324 argc--; 325 if (strcmp(s, "--") == 0) 326 break; 327 scan_option(s, FALSE); 328 } 329 #undef isoptstring 330 331 if (isoptpending()) 332 { 333 /* 334 * Last command line option was a flag requiring a 335 * following string, but there was no following string. 336 */ 337 nopendopt(); 338 quit(QUIT_OK); 339 } 340 341 if (less_is_more) 342 no_init = TRUE; 343 344 get_term(); 345 expand_cmd_tables(); 346 347 #if EDITOR 348 editor = lgetenv("VISUAL"); 349 if (isnullenv(editor)) 350 { 351 editor = lgetenv("EDITOR"); 352 if (isnullenv(editor)) 353 editor = EDIT_PGM; 354 } 355 editproto = lgetenv("LESSEDIT"); 356 if (isnullenv(editproto)) 357 editproto = "%E ?lm+%lm. %g"; 358 #endif 359 360 /* 361 * Call get_ifile with all the command line filenames 362 * to "register" them with the ifile system. 363 */ 364 ifile = NULL_IFILE; 365 if (dohelp) 366 ifile = get_ifile(FAKE_HELPFILE, ifile); 367 while (argc-- > 0) 368 { 369 #if (MSDOS_COMPILER && MSDOS_COMPILER != DJGPPC) 370 /* 371 * Because the "shell" doesn't expand filename patterns, 372 * treat each argument as a filename pattern rather than 373 * a single filename. 374 * Expand the pattern and iterate over the expanded list. 375 */ 376 struct textlist tlist; 377 constant char *filename; 378 char *gfilename; 379 char *qfilename; 380 381 gfilename = lglob(*argv++); 382 init_textlist(&tlist, gfilename); 383 filename = NULL; 384 while ((filename = forw_textlist(&tlist, filename)) != NULL) 385 { 386 qfilename = shell_unquote(filename); 387 (void) get_ifile(qfilename, ifile); 388 free(qfilename); 389 ifile = prev_ifile(NULL_IFILE); 390 } 391 free(gfilename); 392 #else 393 (void) get_ifile(*argv++, ifile); 394 ifile = prev_ifile(NULL_IFILE); 395 #endif 396 } 397 /* 398 * Set up terminal, etc. 399 */ 400 if (!is_tty) 401 { 402 /* 403 * Output is not a tty. 404 * Just copy the input file(s) to output. 405 */ 406 set_output(1); /* write to stdout */ 407 SET_BINARY(1); 408 if (edit_first() == 0) 409 { 410 do { 411 cat_file(); 412 } while (edit_next(1) == 0); 413 } 414 quit(QUIT_OK); 415 } 416 417 if (missing_cap && !know_dumb && !less_is_more) 418 error("WARNING: terminal is not fully functional", NULL_PARG); 419 open_getchr(); 420 raw_mode(1); 421 init_signals(1); 422 #if HAVE_TIME 423 less_start_time = get_time(); 424 #endif 425 426 /* 427 * Select the first file to examine. 428 */ 429 #if TAGS 430 if (tagoption != NULL || strcmp(tags, "-") == 0) 431 { 432 /* 433 * A -t option was given. 434 * Verify that no filenames were also given. 435 * Edit the file selected by the "tags" search, 436 * and search for the proper line in the file. 437 */ 438 if (nifile() > 0) 439 { 440 error("No filenames allowed with -t option", NULL_PARG); 441 quit(QUIT_ERROR); 442 } 443 findtag(tagoption); 444 if (edit_tagfile()) /* Edit file which contains the tag */ 445 quit(QUIT_ERROR); 446 /* 447 * Search for the line which contains the tag. 448 * Set up initial_scrpos so we display that line. 449 */ 450 initial_scrpos.pos = tagsearch(); 451 if (initial_scrpos.pos == NULL_POSITION) 452 quit(QUIT_ERROR); 453 initial_scrpos.ln = jump_sline; 454 } else 455 #endif 456 { 457 if (edit_first()) 458 quit(QUIT_ERROR); 459 /* 460 * See if file fits on one screen to decide whether 461 * to send terminal init. But don't need this 462 * if -X (no_init) overrides this (see init()). 463 */ 464 if (quit_if_one_screen) 465 { 466 if (nifile() > 1) /* If more than one file, -F cannot be used */ 467 quit_if_one_screen = FALSE; 468 else if (!no_init) 469 one_screen = get_one_screen(); 470 } 471 } 472 if (init_header != NULL) 473 { 474 opt_header(TOGGLE, init_header); 475 free(init_header); 476 init_header = NULL; 477 } 478 479 if (errmsgs > 0) 480 { 481 /* 482 * We displayed some messages on error output 483 * (file descriptor 2; see flush()). 484 * Before erasing the screen contents, wait for a keystroke. 485 */ 486 less_printf("Press RETURN to continue ", NULL_PARG); 487 get_return(); 488 putchr('\n'); 489 } 490 set_output(1); 491 init(); 492 commands(); 493 quit(QUIT_OK); 494 /*NOTREACHED*/ 495 return (0); 496 } 497 498 /* 499 * Copy a string to a "safe" place 500 * (that is, to a buffer allocated by calloc). 501 */ 502 public char * saven(constant char *s, size_t n) 503 { 504 char *p = (char *) ecalloc(n+1, sizeof(char)); 505 strncpy(p, s, n); 506 p[n] = '\0'; 507 return (p); 508 } 509 510 public char * save(constant char *s) 511 { 512 return saven(s, strlen(s)); 513 } 514 515 public void out_of_memory(void) 516 { 517 error("Cannot allocate memory", NULL_PARG); 518 quit(QUIT_ERROR); 519 } 520 521 /* 522 * Allocate memory. 523 * Like calloc(), but never returns an error (NULL). 524 */ 525 public void * ecalloc(size_t count, size_t size) 526 { 527 void * p; 528 529 p = (void *) calloc(count, size); 530 if (p == NULL) 531 out_of_memory(); 532 return p; 533 } 534 535 /* 536 * Skip leading spaces in a string. 537 */ 538 public char * skipsp(char *s) 539 { 540 while (*s == ' ' || *s == '\t') 541 s++; 542 return (s); 543 } 544 545 /* {{ There must be a better way. }} */ 546 public constant char * skipspc(constant char *s) 547 { 548 while (*s == ' ' || *s == '\t') 549 s++; 550 return (s); 551 } 552 553 /* 554 * See how many characters of two strings are identical. 555 * If uppercase is true, the first string must begin with an uppercase 556 * character; the remainder of the first string may be either case. 557 */ 558 public size_t sprefix(constant char *ps, constant char *s, int uppercase) 559 { 560 char c; 561 char sc; 562 size_t len = 0; 563 564 for ( ; *s != '\0'; s++, ps++) 565 { 566 c = *ps; 567 if (uppercase) 568 { 569 if (len == 0 && ASCII_IS_LOWER(c)) 570 return (0); 571 if (ASCII_IS_UPPER(c)) 572 c = ASCII_TO_LOWER(c); 573 } 574 sc = *s; 575 if (len > 0 && ASCII_IS_UPPER(sc)) 576 sc = ASCII_TO_LOWER(sc); 577 if (c != sc) 578 break; 579 len++; 580 } 581 return (len); 582 } 583 584 /* 585 * Exit the program. 586 */ 587 public void quit(int status) 588 { 589 static int save_status; 590 591 /* 592 * Put cursor at bottom left corner, clear the line, 593 * reset the terminal modes, and exit. 594 */ 595 if (status < 0) 596 status = save_status; 597 else 598 save_status = status; 599 quitting = TRUE; 600 check_altpipe_error(); 601 if (interactive()) 602 clear_bot(); 603 deinit(); 604 flush(); 605 if (redraw_on_quit && term_init_done) 606 { 607 /* 608 * The last file text displayed might have been on an 609 * alternate screen, which now (since deinit) cannot be seen. 610 * redraw_on_quit tells us to redraw it on the main screen. 611 */ 612 first_time = TRUE; /* Don't print "skipping" or tildes */ 613 repaint(); 614 flush(); 615 } 616 edit((char*)NULL); 617 save_cmdhist(); 618 raw_mode(0); 619 #if MSDOS_COMPILER && MSDOS_COMPILER != DJGPPC 620 /* 621 * If we don't close 2, we get some garbage from 622 * 2's buffer when it flushes automatically. 623 * I cannot track this one down RB 624 * The same bug shows up if we use ^C^C to abort. 625 */ 626 close(2); 627 #endif 628 #ifdef WIN32 629 SetConsoleTitleW(consoleTitle); 630 #endif 631 close_getchr(); 632 exit(status); 633 } 634 635 /* 636 * Are all the features in the features mask allowed by security? 637 */ 638 public int secure_allow(int features) 639 { 640 return ((secure_allow_features & features) == features); 641 } 642