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