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 #include "defines.h" 11 #include <stdio.h> 12 #include <string.h> 13 #include <stdlib.h> 14 #include "lesskey.h" 15 #include "cmd.h" 16 #include "xbuf.h" 17 18 #define CONTROL(c) ((c)&037) 19 #define ESC CONTROL('[') 20 21 extern void lesskey_parse_error(char *msg); 22 extern char *homefile(char *filename); 23 extern void *ecalloc(size_t count, size_t size); 24 extern int lstrtoi(char *str, char **end, int radix); 25 extern char version[]; 26 27 static int linenum; 28 static int errors; 29 static int less_version = 0; 30 static char *lesskey_file = NULL; 31 32 static constant struct lesskey_cmdname cmdnames[] = 33 { 34 { "back-bracket", A_B_BRACKET }, 35 { "back-line", A_B_LINE }, 36 { "back-line-force", A_BF_LINE }, 37 { "back-newline", A_B_NEWLINE }, 38 { "back-screen", A_B_SCREEN }, 39 { "back-screen-force", A_BF_SCREEN }, 40 { "back-scroll", A_B_SCROLL }, 41 { "back-search", A_B_SEARCH }, 42 { "back-window", A_B_WINDOW }, 43 { "clear-mark", A_CLRMARK }, 44 { "clear-search", A_CLR_SEARCH }, 45 { "debug", A_DEBUG }, 46 { "digit", A_DIGIT }, 47 { "display-flag", A_DISP_OPTION }, 48 { "display-option", A_DISP_OPTION }, 49 { "end", A_GOEND }, 50 { "end-scroll", A_RRSHIFT }, 51 { "examine", A_EXAMINE }, 52 { "filter", A_FILTER }, 53 { "first-cmd", A_FIRSTCMD }, 54 { "firstcmd", A_FIRSTCMD }, 55 { "flush-repaint", A_FREPAINT }, 56 { "forw-bracket", A_F_BRACKET }, 57 { "forw-forever", A_F_FOREVER }, 58 { "forw-line", A_F_LINE }, 59 { "forw-line-force", A_FF_LINE }, 60 { "forw-newline", A_F_NEWLINE }, 61 { "forw-screen", A_F_SCREEN }, 62 { "forw-screen-force", A_FF_SCREEN }, 63 { "forw-scroll", A_F_SCROLL }, 64 { "forw-search", A_F_SEARCH }, 65 { "forw-until-hilite", A_F_UNTIL_HILITE }, 66 { "forw-window", A_F_WINDOW }, 67 { "goto-end", A_GOEND }, 68 { "goto-end-buffered", A_GOEND_BUF }, 69 { "goto-line", A_GOLINE }, 70 { "goto-mark", A_GOMARK }, 71 { "help", A_HELP }, 72 { "index-file", A_INDEX_FILE }, 73 { "invalid", A_UINVALID }, 74 { "left-scroll", A_LSHIFT }, 75 { "mouse", A_X11MOUSE_IN }, 76 { "mouse6", A_X116MOUSE_IN }, 77 { "next-file", A_NEXT_FILE }, 78 { "next-tag", A_NEXT_TAG }, 79 { "no-scroll", A_LLSHIFT }, 80 { "noaction", A_NOACTION }, 81 { "osc8-forw-search", A_OSC8_F_SEARCH }, 82 { "osc8-back-search", A_OSC8_B_SEARCH }, 83 { "osc8-open", A_OSC8_OPEN }, 84 { "percent", A_PERCENT }, 85 { "pipe", A_PIPE }, 86 { "prev-file", A_PREV_FILE }, 87 { "prev-tag", A_PREV_TAG }, 88 { "pshell", A_PSHELL }, 89 { "quit", A_QUIT }, 90 { "remove-file", A_REMOVE_FILE }, 91 { "repaint", A_REPAINT }, 92 { "repaint-flush", A_FREPAINT }, 93 { "repeat-search", A_AGAIN_SEARCH }, 94 { "repeat-search-all", A_T_AGAIN_SEARCH }, 95 { "reverse-search", A_REVERSE_SEARCH }, 96 { "reverse-search-all", A_T_REVERSE_SEARCH }, 97 { "right-scroll", A_RSHIFT }, 98 { "set-mark", A_SETMARK }, 99 { "set-mark-bottom", A_SETMARKBOT }, 100 { "shell", A_SHELL }, 101 { "status", A_STAT }, 102 { "toggle-flag", A_OPT_TOGGLE }, 103 { "toggle-option", A_OPT_TOGGLE }, 104 { "undo-hilite", A_UNDO_SEARCH }, 105 { "version", A_VERSION }, 106 { "visual", A_VISUAL }, 107 { NULL, 0 } 108 }; 109 110 static constant struct lesskey_cmdname editnames[] = 111 { 112 { "back-complete", EC_B_COMPLETE }, 113 { "backspace", EC_BACKSPACE }, 114 { "delete", EC_DELETE }, 115 { "down", EC_DOWN }, 116 { "end", EC_END }, 117 { "expand", EC_EXPAND }, 118 { "forw-complete", EC_F_COMPLETE }, 119 { "home", EC_HOME }, 120 { "insert", EC_INSERT }, 121 { "invalid", EC_UINVALID }, 122 { "kill-line", EC_LINEKILL }, 123 { "abort", EC_ABORT }, 124 { "left", EC_LEFT }, 125 { "literal", EC_LITERAL }, 126 { "mouse", EC_X11MOUSE }, 127 { "mouse6", EC_X116MOUSE }, 128 { "right", EC_RIGHT }, 129 { "up", EC_UP }, 130 { "word-backspace", EC_W_BACKSPACE }, 131 { "word-delete", EC_W_DELETE }, 132 { "word-left", EC_W_LEFT }, 133 { "word-right", EC_W_RIGHT }, 134 { NULL, 0 } 135 }; 136 137 /* 138 * Print a parse error message. 139 */ 140 static void parse_error(constant char *fmt, constant char *arg1) 141 { 142 char buf[1024]; 143 int n = SNPRINTF2(buf, sizeof(buf), "%s: line %d: ", lesskey_file, linenum); 144 if (n >= 0) 145 { 146 size_t len = (size_t) n; 147 if (len < sizeof(buf)) 148 SNPRINTF1(buf+len, sizeof(buf)-len, fmt, arg1); 149 } 150 ++errors; 151 lesskey_parse_error(buf); 152 } 153 154 /* 155 * Initialize lesskey_tables. 156 */ 157 static void init_tables(struct lesskey_tables *tables) 158 { 159 tables->currtable = &tables->cmdtable; 160 161 tables->cmdtable.names = cmdnames; 162 tables->cmdtable.is_var = 0; 163 xbuf_init(&tables->cmdtable.buf); 164 165 tables->edittable.names = editnames; 166 tables->edittable.is_var = 0; 167 xbuf_init(&tables->edittable.buf); 168 169 tables->vartable.names = NULL; 170 tables->vartable.is_var = 1; 171 xbuf_init(&tables->vartable.buf); 172 } 173 174 #define CHAR_STRING_LEN 8 175 176 static constant char * char_string(char *buf, char ch, int lit) 177 { 178 if (lit || (ch >= 0x20 && ch < 0x7f)) 179 { 180 buf[0] = ch; 181 buf[1] = '\0'; 182 } else 183 { 184 SNPRINTF1(buf, CHAR_STRING_LEN, "\\x%02x", ch); 185 } 186 return buf; 187 } 188 189 /* 190 * Increment char pointer by one up to terminating nul byte. 191 */ 192 static char * increment_pointer(char *p) 193 { 194 if (*p == '\0') 195 return p; 196 return p+1; 197 } 198 199 /* 200 * Parse one character of a string. 201 */ 202 static constant char * tstr(char **pp, int xlate) 203 { 204 char *p; 205 char ch; 206 int i; 207 static char buf[CHAR_STRING_LEN]; 208 static char tstr_control_k[] = 209 { SK_SPECIAL_KEY, SK_CONTROL_K, 6, 1, 1, 1, '\0' }; 210 211 p = *pp; 212 switch (*p) 213 { 214 case '\\': 215 ++p; 216 switch (*p) 217 { 218 case '0': case '1': case '2': case '3': 219 case '4': case '5': case '6': case '7': 220 /* 221 * Parse an octal number. 222 */ 223 ch = 0; 224 i = 0; 225 do 226 ch = (char) (8*ch + (*p - '0')); 227 while (*++p >= '0' && *p <= '7' && ++i < 3); 228 *pp = p; 229 if (xlate && ch == CONTROL('K')) 230 return tstr_control_k; 231 return char_string(buf, ch, 1); 232 case 'b': 233 *pp = p+1; 234 return ("\b"); 235 case 'e': 236 *pp = p+1; 237 return char_string(buf, ESC, 1); 238 case 'n': 239 *pp = p+1; 240 return ("\n"); 241 case 'r': 242 *pp = p+1; 243 return ("\r"); 244 case 't': 245 *pp = p+1; 246 return ("\t"); 247 case 'k': 248 if (xlate) 249 { 250 switch (*++p) 251 { 252 case 'b': ch = SK_BACKSPACE; break; 253 case 'B': ch = SK_CTL_BACKSPACE; break; 254 case 'd': ch = SK_DOWN_ARROW; break; 255 case 'D': ch = SK_PAGE_DOWN; break; 256 case 'e': ch = SK_END; break; 257 case 'h': ch = SK_HOME; break; 258 case 'i': ch = SK_INSERT; break; 259 case 'l': ch = SK_LEFT_ARROW; break; 260 case 'L': ch = SK_CTL_LEFT_ARROW; break; 261 case 'r': ch = SK_RIGHT_ARROW; break; 262 case 'R': ch = SK_CTL_RIGHT_ARROW; break; 263 case 't': ch = SK_BACKTAB; break; 264 case 'u': ch = SK_UP_ARROW; break; 265 case 'U': ch = SK_PAGE_UP; break; 266 case 'x': ch = SK_DELETE; break; 267 case 'X': ch = SK_CTL_DELETE; break; 268 case '1': ch = SK_F1; break; 269 default: 270 parse_error("invalid escape sequence \"\\k%s\"", char_string(buf, *p, 0)); 271 *pp = increment_pointer(p); 272 return (""); 273 } 274 *pp = p+1; 275 buf[0] = SK_SPECIAL_KEY; 276 buf[1] = ch; 277 buf[2] = 6; 278 buf[3] = 1; 279 buf[4] = 1; 280 buf[5] = 1; 281 buf[6] = '\0'; 282 return (buf); 283 } 284 /* FALLTHRU */ 285 default: 286 /* 287 * Backslash followed by any other char 288 * just means that char. 289 */ 290 *pp = increment_pointer(p); 291 char_string(buf, *p, 1); 292 if (xlate && buf[0] == CONTROL('K')) 293 return tstr_control_k; 294 return (buf); 295 } 296 case '^': 297 /* 298 * Caret means CONTROL. 299 */ 300 *pp = increment_pointer(p+1); 301 char_string(buf, CONTROL(p[1]), 1); 302 if (xlate && buf[0] == CONTROL('K')) 303 return tstr_control_k; 304 return (buf); 305 } 306 *pp = increment_pointer(p); 307 char_string(buf, *p, 1); 308 if (xlate && buf[0] == CONTROL('K')) 309 return tstr_control_k; 310 return (buf); 311 } 312 313 static int issp(char ch) 314 { 315 return (ch == ' ' || ch == '\t'); 316 } 317 318 /* 319 * Skip leading spaces in a string. 320 */ 321 static char * skipsp(char *s) 322 { 323 while (issp(*s)) 324 s++; 325 return (s); 326 } 327 328 /* 329 * Skip non-space characters in a string. 330 */ 331 static char * skipnsp(char *s) 332 { 333 while (*s != '\0' && !issp(*s)) 334 s++; 335 return (s); 336 } 337 338 /* 339 * Clean up an input line: 340 * strip off the trailing newline & any trailing # comment. 341 */ 342 static char * clean_line(char *s) 343 { 344 int i; 345 346 s = skipsp(s); 347 for (i = 0; s[i] != '\0' && s[i] != '\n' && s[i] != '\r'; i++) 348 if (s[i] == '#' && (i == 0 || s[i-1] != '\\')) 349 break; 350 s[i] = '\0'; 351 return (s); 352 } 353 354 /* 355 * Add a byte to the output command table. 356 */ 357 static void add_cmd_char(unsigned char c, struct lesskey_tables *tables) 358 { 359 xbuf_add_byte(&tables->currtable->buf, c); 360 } 361 362 static void erase_cmd_char(struct lesskey_tables *tables) 363 { 364 xbuf_pop(&tables->currtable->buf); 365 } 366 367 /* 368 * Add a string to the output command table. 369 */ 370 static void add_cmd_str(constant char *s, struct lesskey_tables *tables) 371 { 372 for ( ; *s != '\0'; s++) 373 add_cmd_char((unsigned char) *s, tables); 374 } 375 376 /* 377 * Does a given version number match the running version? 378 * Operator compares the running version to the given version. 379 */ 380 static int match_version(char op, int ver) 381 { 382 switch (op) 383 { 384 case '>': return less_version > ver; 385 case '<': return less_version < ver; 386 case '+': return less_version >= ver; 387 case '-': return less_version <= ver; 388 case '=': return less_version == ver; 389 case '!': return less_version != ver; 390 default: return 0; /* cannot happen */ 391 } 392 } 393 394 /* 395 * Handle a #version line. 396 * If the version matches, return the part of the line that should be executed. 397 * Otherwise, return NULL. 398 */ 399 static char * version_line(char *s) 400 { 401 char op; 402 int ver; 403 char *e; 404 char buf[CHAR_STRING_LEN]; 405 406 s += strlen("#version"); 407 s = skipsp(s); 408 op = *s++; 409 /* Simplify 2-char op to one char. */ 410 switch (op) 411 { 412 case '<': if (*s == '=') { s++; op = '-'; } break; 413 case '>': if (*s == '=') { s++; op = '+'; } break; 414 case '=': if (*s == '=') { s++; } break; 415 case '!': if (*s == '=') { s++; } break; 416 default: 417 parse_error("invalid operator '%s' in #version line", char_string(buf, op, 0)); 418 return (NULL); 419 } 420 s = skipsp(s); 421 ver = lstrtoi(s, &e, 10); 422 if (e == s) 423 { 424 parse_error("non-numeric version number in #version line", ""); 425 return (NULL); 426 } 427 if (!match_version(op, ver)) 428 return (NULL); 429 return (e); 430 } 431 432 /* 433 * See if we have a special "control" line. 434 */ 435 static char * control_line(char *s, struct lesskey_tables *tables) 436 { 437 #define PREFIX(str,pat) (strncmp(str,pat,strlen(pat)) == 0) 438 439 if (PREFIX(s, "#line-edit")) 440 { 441 tables->currtable = &tables->edittable; 442 return (NULL); 443 } 444 if (PREFIX(s, "#command")) 445 { 446 tables->currtable = &tables->cmdtable; 447 return (NULL); 448 } 449 if (PREFIX(s, "#env")) 450 { 451 tables->currtable = &tables->vartable; 452 return (NULL); 453 } 454 if (PREFIX(s, "#stop")) 455 { 456 add_cmd_char('\0', tables); 457 add_cmd_char(A_END_LIST, tables); 458 return (NULL); 459 } 460 if (PREFIX(s, "#version")) 461 { 462 return (version_line(s)); 463 } 464 return (s); 465 } 466 467 /* 468 * Find an action, given the name of the action. 469 */ 470 static int findaction(char *actname, struct lesskey_tables *tables) 471 { 472 int i; 473 474 for (i = 0; tables->currtable->names[i].cn_name != NULL; i++) 475 if (strcmp(tables->currtable->names[i].cn_name, actname) == 0) 476 return (tables->currtable->names[i].cn_action); 477 parse_error("unknown action: \"%s\"", actname); 478 return (A_INVALID); 479 } 480 481 /* 482 * Parse a line describing one key binding, of the form 483 * KEY ACTION [EXTRA] 484 * where KEY is the user key sequence, ACTION is the 485 * resulting less action, and EXTRA is an "extra" user 486 * key sequence injected after the action. 487 */ 488 static void parse_cmdline(char *p, struct lesskey_tables *tables) 489 { 490 char *actname; 491 int action; 492 constant char *s; 493 char c; 494 495 /* 496 * Parse the command string and store it in the current table. 497 */ 498 do 499 { 500 s = tstr(&p, 1); 501 add_cmd_str(s, tables); 502 } while (*p != '\0' && !issp(*p)); 503 /* 504 * Terminate the command string with a null byte. 505 */ 506 add_cmd_char('\0', tables); 507 508 /* 509 * Skip white space between the command string 510 * and the action name. 511 * Terminate the action name with a null byte. 512 */ 513 p = skipsp(p); 514 if (*p == '\0') 515 { 516 parse_error("missing action", ""); 517 return; 518 } 519 actname = p; 520 p = skipnsp(p); 521 c = *p; 522 *p = '\0'; 523 524 /* 525 * Parse the action name and store it in the current table. 526 */ 527 action = findaction(actname, tables); 528 529 /* 530 * See if an extra string follows the action name. 531 */ 532 *p = c; 533 p = skipsp(p); 534 if (*p == '\0') 535 { 536 add_cmd_char((unsigned char) action, tables); 537 } else 538 { 539 /* 540 * OR the special value A_EXTRA into the action byte. 541 * Put the extra string after the action byte. 542 */ 543 add_cmd_char((unsigned char) (action | A_EXTRA), tables); 544 while (*p != '\0') 545 add_cmd_str(tstr(&p, 0), tables); 546 add_cmd_char('\0', tables); 547 } 548 } 549 550 /* 551 * Parse a variable definition line, of the form 552 * NAME = VALUE 553 */ 554 static void parse_varline(char *line, struct lesskey_tables *tables) 555 { 556 constant char *s; 557 char *p = line; 558 char *eq; 559 560 eq = strchr(line, '='); 561 if (eq != NULL && eq > line && eq[-1] == '+') 562 { 563 /* 564 * Rather ugly way of handling a += line. 565 * {{ Note that we ignore the variable name and 566 * just append to the previously defined variable. }} 567 */ 568 erase_cmd_char(tables); /* backspace over the final null */ 569 p = eq+1; 570 } else 571 { 572 do 573 { 574 s = tstr(&p, 0); 575 add_cmd_str(s, tables); 576 } while (*p != '\0' && !issp(*p) && *p != '='); 577 /* 578 * Terminate the variable name with a null byte. 579 */ 580 add_cmd_char('\0', tables); 581 p = skipsp(p); 582 if (*p++ != '=') 583 { 584 parse_error("missing = in variable definition", ""); 585 return; 586 } 587 add_cmd_char(EV_OK|A_EXTRA, tables); 588 } 589 p = skipsp(p); 590 while (*p != '\0') 591 { 592 s = tstr(&p, 0); 593 add_cmd_str(s, tables); 594 } 595 add_cmd_char('\0', tables); 596 } 597 598 /* 599 * Parse a line from the lesskey file. 600 */ 601 static void parse_line(char *line, struct lesskey_tables *tables) 602 { 603 char *p; 604 605 /* 606 * See if it is a control line. 607 */ 608 p = control_line(line, tables); 609 if (p == NULL) 610 return; 611 /* 612 * Skip leading white space. 613 * Replace the final newline with a null byte. 614 * Ignore blank lines and comments. 615 */ 616 p = clean_line(p); 617 if (*p == '\0') 618 return; 619 620 if (tables->currtable->is_var) 621 parse_varline(p, tables); 622 else 623 parse_cmdline(p, tables); 624 } 625 626 /* 627 * Parse a lesskey source file and store result in tables. 628 */ 629 int parse_lesskey(constant char *infile, struct lesskey_tables *tables) 630 { 631 FILE *desc; 632 char line[1024]; 633 634 lesskey_file = (infile != NULL) ? strdup(infile) : homefile(DEF_LESSKEYINFILE); 635 if (lesskey_file == NULL) 636 return (-1); 637 638 init_tables(tables); 639 errors = 0; 640 linenum = 0; 641 if (less_version == 0) 642 less_version = lstrtoi(version, NULL, 10); 643 644 /* 645 * Open the input file. 646 */ 647 if (strcmp(lesskey_file, "-") == 0) 648 desc = stdin; 649 else if ((desc = fopen(lesskey_file, "r")) == NULL) 650 { 651 /* parse_error("cannot open lesskey file %s", lesskey_file); */ 652 errors = -1; 653 } 654 655 /* 656 * Read and parse the input file, one line at a time. 657 */ 658 if (desc != NULL) 659 { 660 while (fgets(line, sizeof(line), desc) != NULL) 661 { 662 ++linenum; 663 parse_line(line, tables); 664 } 665 if (desc != stdin) 666 fclose(desc); 667 } 668 free(lesskey_file); 669 lesskey_file = NULL; 670 return (errors); 671 } 672 673 /* 674 * Parse a lesskey source content and store result in tables. 675 */ 676 int parse_lesskey_content(constant char *content, struct lesskey_tables *tables) 677 { 678 size_t cx = 0; 679 680 lesskey_file = "lesskey-content"; 681 init_tables(tables); 682 errors = 0; 683 linenum = 0; 684 if (less_version == 0) 685 less_version = lstrtoi(version, NULL, 10); 686 687 while (content[cx] != '\0') 688 { 689 /* Extract a line from the content buffer and parse it. */ 690 char line[1024]; 691 size_t lx = 0; 692 while (content[cx] != '\0' && content[cx] != '\n' && content[cx] != ';') 693 { 694 if (lx >= sizeof(line)-1) break; 695 if (content[cx] == '\\' && content[cx+1] == ';') 696 ++cx; /* escaped semicolon: skip the backslash */ 697 line[lx++] = content[cx++]; 698 } 699 line[lx] = '\0'; 700 ++linenum; 701 parse_line(line, tables); 702 if (content[cx] != '\0') ++cx; /* skip newline or semicolon */ 703 } 704 lesskey_file = NULL; 705 return (errors); 706 } 707