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