1 /*-
2 * Copyright (c) 2013-2018 Devin Teske <dteske@FreeBSD.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 * SUCH DAMAGE.
25 */
26
27 #include <sys/cdefs.h>
28 #include <sys/ioctl.h>
29
30 #include <ctype.h>
31 #include <err.h>
32 #include <fcntl.h>
33 #include <limits.h>
34 #include <spawn.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <termios.h>
39 #include <unistd.h>
40
41 #include "dialog_util.h"
42 #include "dpv.h"
43 #include "dpv_private.h"
44
45 extern char **environ;
46
47 #define TTY_DEFAULT_ROWS 24
48 #define TTY_DEFAULT_COLS 80
49
50 /* [X]dialog(1) characteristics */
51 uint8_t dialog_test = 0;
52 uint8_t use_dialog = 0;
53 uint8_t use_libdialog = 1;
54 uint8_t use_xdialog = 0;
55 uint8_t use_color = 1;
56 char dialog[PATH_MAX] = DIALOG;
57
58 /* [X]dialog(1) functionality */
59 char *title = NULL;
60 char *backtitle = NULL;
61 int dheight = 0;
62 int dwidth = 0;
63 static char *dargv[64] = { NULL };
64
65 /* TTY/Screen characteristics */
66 static struct winsize *maxsize = NULL;
67
68 /* Function prototypes */
69 static void tty_maxsize_update(void);
70 static void x11_maxsize_update(void);
71
72 /*
73 * Update row/column fields of `maxsize' global (used by dialog_maxrows() and
74 * dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.
75 * The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current
76 * maximum height and width (respectively) for a dialog(1) widget based on the
77 * active TTY size.
78 *
79 * This function is called automatically by dialog_maxrows/cols() to reflect
80 * changes in terminal size in-between calls.
81 */
82 static void
tty_maxsize_update(void)83 tty_maxsize_update(void)
84 {
85 int fd = STDIN_FILENO;
86 struct termios t;
87
88 if (maxsize == NULL) {
89 if ((maxsize = malloc(sizeof(struct winsize))) == NULL)
90 errx(EXIT_FAILURE, "Out of memory?!");
91 memset((void *)maxsize, '\0', sizeof(struct winsize));
92 }
93
94 if (!isatty(fd))
95 fd = open("/dev/tty", O_RDONLY);
96 if ((tcgetattr(fd, &t) < 0) || (ioctl(fd, TIOCGWINSZ, maxsize) < 0)) {
97 maxsize->ws_row = TTY_DEFAULT_ROWS;
98 maxsize->ws_col = TTY_DEFAULT_COLS;
99 }
100 }
101
102 /*
103 * Update row/column fields of `maxsize' global (used by dialog_maxrows() and
104 * dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.
105 * The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current
106 * maximum height and width (respectively) for an Xdialog(1) widget based on
107 * the active video resolution of the X11 environment.
108 *
109 * This function is called automatically by dialog_maxrows/cols() to initialize
110 * `maxsize'. Since video resolution changes are less common and more obtrusive
111 * than changes to terminal size, the dialog_maxrows/cols() functions only call
112 * this function when `maxsize' is set to NULL.
113 */
114 static void
x11_maxsize_update(void)115 x11_maxsize_update(void)
116 {
117 FILE *f = NULL;
118 char *cols;
119 char *cp;
120 char *rows;
121 char cmdbuf[LINE_MAX];
122 char rbuf[LINE_MAX];
123
124 if (maxsize == NULL) {
125 if ((maxsize = malloc(sizeof(struct winsize))) == NULL)
126 errx(EXIT_FAILURE, "Out of memory?!");
127 memset((void *)maxsize, '\0', sizeof(struct winsize));
128 }
129
130 /* Assemble the command necessary to get X11 sizes */
131 snprintf(cmdbuf, LINE_MAX, "%s --print-maxsize 2>&1", dialog);
132
133 fflush(STDIN_FILENO); /* prevent popen(3) from seeking on stdin */
134
135 if ((f = popen(cmdbuf, "r")) == NULL) {
136 if (debug)
137 warnx("WARNING! Command `%s' failed", cmdbuf);
138 return;
139 }
140
141 /* Read in the line returned from Xdialog(1) */
142 if ((fgets(rbuf, LINE_MAX, f) == NULL) || (pclose(f) < 0))
143 return;
144
145 /* Check for X11-related errors */
146 if (strncmp(rbuf, "Xdialog: Error", 14) == 0)
147 return;
148
149 /* Parse expected output: MaxSize: YY, XXX */
150 if ((rows = strchr(rbuf, ' ')) == NULL)
151 return;
152 if ((cols = strchr(rows, ',')) != NULL) {
153 /* strtonum(3) doesn't like trailing junk */
154 *(cols++) = '\0';
155 if ((cp = strchr(cols, '\n')) != NULL)
156 *cp = '\0';
157 }
158
159 /* Convert to unsigned short */
160 maxsize->ws_row = (unsigned short)strtonum(
161 rows, 0, USHRT_MAX, (const char **)NULL);
162 maxsize->ws_col = (unsigned short)strtonum(
163 cols, 0, USHRT_MAX, (const char **)NULL);
164 }
165
166 /*
167 * Return the current maximum height (rows) for an [X]dialog(1) widget.
168 */
169 int
dialog_maxrows(void)170 dialog_maxrows(void)
171 {
172
173 if (use_xdialog && maxsize == NULL)
174 x11_maxsize_update(); /* initialize maxsize for GUI */
175 else if (!use_xdialog)
176 tty_maxsize_update(); /* update maxsize for TTY */
177 return (maxsize->ws_row);
178 }
179
180 /*
181 * Return the current maximum width (cols) for an [X]dialog(1) widget.
182 */
183 int
dialog_maxcols(void)184 dialog_maxcols(void)
185 {
186
187 if (use_xdialog && maxsize == NULL)
188 x11_maxsize_update(); /* initialize maxsize for GUI */
189 else if (!use_xdialog)
190 tty_maxsize_update(); /* update maxsize for TTY */
191
192 if (use_dialog || use_libdialog) {
193 if (use_shadow)
194 return (maxsize->ws_col - 2);
195 else
196 return (maxsize->ws_col);
197 } else
198 return (maxsize->ws_col);
199 }
200
201 /*
202 * Return the current maximum width (cols) for the terminal.
203 */
204 int
tty_maxcols(void)205 tty_maxcols(void)
206 {
207
208 if (use_xdialog && maxsize == NULL)
209 x11_maxsize_update(); /* initialize maxsize for GUI */
210 else if (!use_xdialog)
211 tty_maxsize_update(); /* update maxsize for TTY */
212
213 return (maxsize->ws_col);
214 }
215
216 /*
217 * Spawn an [X]dialog(1) `--gauge' box with a `--prompt' value of init_prompt.
218 * Writes the resulting process ID to the pid_t pointed at by `pid'. Returns a
219 * file descriptor (int) suitable for writing data to the [X]dialog(1) instance
220 * (data written to the file descriptor is seen as standard-in by the spawned
221 * [X]dialog(1) process).
222 */
223 int
dialog_spawn_gauge(char * init_prompt,pid_t * pid)224 dialog_spawn_gauge(char *init_prompt, pid_t *pid)
225 {
226 char dummy_init[2] = "";
227 char *cp;
228 int height;
229 int width;
230 int error;
231 posix_spawn_file_actions_t action;
232 #if DIALOG_SPAWN_DEBUG
233 unsigned int i;
234 #endif
235 unsigned int n = 0;
236 int stdin_pipe[2] = { -1, -1 };
237
238 /* Override `dialog' with a path from ENV_DIALOG if provided */
239 if ((cp = getenv(ENV_DIALOG)) != NULL)
240 snprintf(dialog, PATH_MAX, "%s", cp);
241
242 /* For Xdialog(1), set ENV_XDIALOG_HIGH_DIALOG_COMPAT */
243 setenv(ENV_XDIALOG_HIGH_DIALOG_COMPAT, "1", 1);
244
245 /* Constrain the height/width */
246 height = dialog_maxrows();
247 if (backtitle != NULL)
248 height -= use_shadow ? 5 : 4;
249 if (dheight < height)
250 height = dheight;
251 width = dialog_maxcols();
252 if (dwidth < width)
253 width = dwidth;
254
255 /* Populate argument array */
256 dargv[n++] = dialog;
257 if (title != NULL) {
258 if ((dargv[n] = malloc(8)) == NULL)
259 errx(EXIT_FAILURE, "Out of memory?!");
260 sprintf(dargv[n++], "--title");
261 dargv[n++] = title;
262 } else {
263 if ((dargv[n] = malloc(8)) == NULL)
264 errx(EXIT_FAILURE, "Out of memory?!");
265 sprintf(dargv[n++], "--title");
266 if ((dargv[n] = malloc(1)) == NULL)
267 errx(EXIT_FAILURE, "Out of memory?!");
268 *dargv[n++] = '\0';
269 }
270 if (backtitle != NULL) {
271 if ((dargv[n] = malloc(12)) == NULL)
272 errx(EXIT_FAILURE, "Out of memory?!");
273 sprintf(dargv[n++], "--backtitle");
274 dargv[n++] = backtitle;
275 }
276 if (use_color) {
277 if ((dargv[n] = malloc(11)) == NULL)
278 errx(EXIT_FAILURE, "Out of memory?!");
279 sprintf(dargv[n++], "--colors");
280 }
281 if (use_xdialog) {
282 if ((dargv[n] = malloc(7)) == NULL)
283 errx(EXIT_FAILURE, "Out of memory?!");
284 sprintf(dargv[n++], "--left");
285
286 /*
287 * NOTE: Xdialog(1)'s `--wrap' appears to be broken for the
288 * `--gauge' widget prompt-updates. Add it anyway (in-case it
289 * gets fixed in some later release).
290 */
291 if ((dargv[n] = malloc(7)) == NULL)
292 errx(EXIT_FAILURE, "Out of memory?!");
293 sprintf(dargv[n++], "--wrap");
294 }
295 if ((dargv[n] = malloc(8)) == NULL)
296 errx(EXIT_FAILURE, "Out of memory?!");
297 sprintf(dargv[n++], "--gauge");
298 dargv[n++] = use_xdialog ? dummy_init : init_prompt;
299 if ((dargv[n] = malloc(40)) == NULL)
300 errx(EXIT_FAILURE, "Out of memory?!");
301 snprintf(dargv[n++], 40, "%u", height);
302 if ((dargv[n] = malloc(40)) == NULL)
303 errx(EXIT_FAILURE, "Out of memory?!");
304 snprintf(dargv[n++], 40, "%u", width);
305 dargv[n] = NULL;
306
307 /* Open a pipe(2) to communicate with [X]dialog(1) */
308 if (pipe(stdin_pipe) < 0)
309 err(EXIT_FAILURE, "%s: pipe(2)", __func__);
310
311 /* Fork [X]dialog(1) process */
312 #if DIALOG_SPAWN_DEBUG
313 fprintf(stderr, "%s: spawning `", __func__);
314 for (i = 0; i < n; i++) {
315 if (i == 0)
316 fprintf(stderr, "%s", dargv[i]);
317 else if (*dargv[i] == '-' && *(dargv[i] + 1) == '-')
318 fprintf(stderr, " %s", dargv[i]);
319 else
320 fprintf(stderr, " \"%s\"", dargv[i]);
321 }
322 fprintf(stderr, "'\n");
323 #endif
324 posix_spawn_file_actions_init(&action);
325 posix_spawn_file_actions_adddup2(&action, stdin_pipe[0], STDIN_FILENO);
326 posix_spawn_file_actions_addclose(&action, stdin_pipe[1]);
327 error = posix_spawnp(pid, dialog, &action,
328 (const posix_spawnattr_t *)NULL, dargv, environ);
329 if (error != 0) err(EXIT_FAILURE, "%s", dialog);
330
331 /* NB: Do not free(3) *dargv[], else SIGSEGV */
332
333 return (stdin_pipe[1]);
334 }
335
336 /*
337 * Returns the number of lines in buffer pointed to by `prompt'. Takes both
338 * newlines and escaped-newlines into account.
339 */
340 unsigned int
dialog_prompt_numlines(const char * prompt,uint8_t nlstate)341 dialog_prompt_numlines(const char *prompt, uint8_t nlstate)
342 {
343 uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
344 const char *cp = prompt;
345 unsigned int nlines = 1;
346
347 if (prompt == NULL || *prompt == '\0')
348 return (0);
349
350 while (*cp != '\0') {
351 if (use_dialog) {
352 if (strncmp(cp, "\\n", 2) == 0) {
353 cp++;
354 nlines++;
355 nls = TRUE; /* See declaration comment */
356 } else if (*cp == '\n') {
357 if (!nls)
358 nlines++;
359 nls = FALSE; /* See declaration comment */
360 }
361 } else if (use_libdialog) {
362 if (*cp == '\n')
363 nlines++;
364 } else if (strncmp(cp, "\\n", 2) == 0) {
365 cp++;
366 nlines++;
367 }
368 cp++;
369 }
370
371 return (nlines);
372 }
373
374 /*
375 * Returns the length in bytes of the longest line in buffer pointed to by
376 * `prompt'. Takes newlines and escaped newlines into account. Also discounts
377 * dialog(1) color escape codes if enabled (via `use_color' global).
378 */
379 unsigned int
dialog_prompt_longestline(const char * prompt,uint8_t nlstate)380 dialog_prompt_longestline(const char *prompt, uint8_t nlstate)
381 {
382 uint8_t backslash = 0;
383 uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
384 const char *p = prompt;
385 int longest = 0;
386 int n = 0;
387
388 /* `prompt' parameter is required */
389 if (prompt == NULL)
390 return (0);
391 if (*prompt == '\0')
392 return (0); /* shortcut */
393
394 /* Loop until the end of the string */
395 while (*p != '\0') {
396 /* dialog(1) and dialog(3) will render literal newlines */
397 if (use_dialog || use_libdialog) {
398 if (*p == '\n') {
399 if (!use_libdialog && nls)
400 n++;
401 else {
402 if (n > longest)
403 longest = n;
404 n = 0;
405 }
406 nls = FALSE; /* See declaration comment */
407 p++;
408 continue;
409 }
410 }
411
412 /* Check for backslash character */
413 if (*p == '\\') {
414 /* If second backslash, count as a single-char */
415 if ((backslash ^= 1) == 0)
416 n++;
417 } else if (backslash) {
418 if (*p == 'n' && !use_libdialog) { /* new line */
419 /* NB: dialog(3) ignores escaped newlines */
420 nls = TRUE; /* See declaration comment */
421 if (n > longest)
422 longest = n;
423 n = 0;
424 } else if (use_color && *p == 'Z') {
425 if (*++p != '\0')
426 p++;
427 backslash = 0;
428 continue;
429 } else /* [X]dialog(1)/dialog(3) only expand those */
430 n += 2;
431
432 backslash = 0;
433 } else
434 n++;
435 p++;
436 }
437 if (n > longest)
438 longest = n;
439
440 return (longest);
441 }
442
443 /*
444 * Returns a pointer to the last line in buffer pointed to by `prompt'. Takes
445 * both newlines (if using dialog(1) versus Xdialog(1)) and escaped newlines
446 * into account. If no newlines (escaped or otherwise) appear in the buffer,
447 * `prompt' is returned. If passed a NULL pointer, returns NULL.
448 */
449 char *
dialog_prompt_lastline(char * prompt,uint8_t nlstate)450 dialog_prompt_lastline(char *prompt, uint8_t nlstate)
451 {
452 uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
453 char *lastline;
454 char *p;
455
456 if (prompt == NULL)
457 return (NULL);
458 if (*prompt == '\0')
459 return (prompt); /* shortcut */
460
461 lastline = p = prompt;
462 while (*p != '\0') {
463 /* dialog(1) and dialog(3) will render literal newlines */
464 if (use_dialog || use_libdialog) {
465 if (*p == '\n') {
466 if (use_libdialog || !nls)
467 lastline = p + 1;
468 nls = FALSE; /* See declaration comment */
469 }
470 }
471 /* dialog(3) does not expand escaped newlines */
472 if (use_libdialog) {
473 p++;
474 continue;
475 }
476 if (*p == '\\' && *(p + 1) != '\0' && *(++p) == 'n') {
477 nls = TRUE; /* See declaration comment */
478 lastline = p + 1;
479 }
480 p++;
481 }
482
483 return (lastline);
484 }
485
486 /*
487 * Returns the number of extra lines generated by wrapping the text in buffer
488 * pointed to by `prompt' within `ncols' columns (for prompts, this should be
489 * dwidth - 4). Also discounts dialog(1) color escape codes if enabled (via
490 * `use_color' global).
491 */
492 int
dialog_prompt_wrappedlines(char * prompt,int ncols,uint8_t nlstate)493 dialog_prompt_wrappedlines(char *prompt, int ncols, uint8_t nlstate)
494 {
495 uint8_t backslash = 0;
496 uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
497 char *cp;
498 char *p = prompt;
499 int n = 0;
500 int wlines = 0;
501
502 /* `prompt' parameter is required */
503 if (p == NULL)
504 return (0);
505 if (*p == '\0')
506 return (0); /* shortcut */
507
508 /* Loop until the end of the string */
509 while (*p != '\0') {
510 /* dialog(1) and dialog(3) will render literal newlines */
511 if (use_dialog || use_libdialog) {
512 if (*p == '\n') {
513 if (use_dialog || !nls)
514 n = 0;
515 nls = FALSE; /* See declaration comment */
516 }
517 }
518
519 /* Check for backslash character */
520 if (*p == '\\') {
521 /* If second backslash, count as a single-char */
522 if ((backslash ^= 1) == 0)
523 n++;
524 } else if (backslash) {
525 if (*p == 'n' && !use_libdialog) { /* new line */
526 /* NB: dialog(3) ignores escaped newlines */
527 nls = TRUE; /* See declaration comment */
528 n = 0;
529 } else if (use_color && *p == 'Z') {
530 if (*++p != '\0')
531 p++;
532 backslash = 0;
533 continue;
534 } else /* [X]dialog(1)/dialog(3) only expand those */
535 n += 2;
536
537 backslash = 0;
538 } else
539 n++;
540
541 /* Did we pass the width barrier? */
542 if (n > ncols) {
543 /*
544 * Work backward to find the first whitespace on-which
545 * dialog(1) will wrap the line (but don't go before
546 * the start of this line).
547 */
548 cp = p;
549 while (n > 1 && !isspace(*cp)) {
550 cp--;
551 n--;
552 }
553 if (n > 0 && isspace(*cp))
554 p = cp;
555 wlines++;
556 n = 1;
557 }
558
559 p++;
560 }
561
562 return (wlines);
563 }
564
565 /*
566 * Returns zero if the buffer pointed to by `prompt' contains an escaped
567 * newline but only if appearing after any/all literal newlines. This is
568 * specific to dialog(1) and does not apply to Xdialog(1).
569 *
570 * As an attempt to make shell scripts easier to read, dialog(1) will "eat"
571 * the first literal newline after an escaped newline. This however has a bug
572 * in its implementation in that rather than allowing `\\n\n' to be treated
573 * similar to `\\n' or `\n', dialog(1) expands the `\\n' and then translates
574 * the following literal newline (with or without characters between [!]) into
575 * a single space.
576 *
577 * If you want to be compatible with Xdialog(1), it is suggested that you not
578 * use literal newlines (they aren't supported); but if you have to use them,
579 * go right ahead. But be forewarned... if you set $DIALOG in your environment
580 * to something other than `cdialog' (our current dialog(1)), then it should
581 * do the same thing w/respect to how to handle a literal newline after an
582 * escaped newline (you could do no wrong by translating every literal newline
583 * into a space but only when you've previously encountered an escaped one;
584 * this is what dialog(1) is doing).
585 *
586 * The ``newline state'' (or nlstate for short; as I'm calling it) is helpful
587 * if you plan to combine multiple strings into a single prompt text. In lead-
588 * up to this procedure, a common task is to calculate and utilize the widths
589 * and heights of each piece of prompt text to later be combined. However, if
590 * (for example) the first string ends in a positive newline state (has an
591 * escaped newline without trailing literal), the first literal newline in the
592 * second string will be mangled.
593 *
594 * The return value of this function should be used as the `nlstate' argument
595 * to dialog_*() functions that require it to allow accurate calculations in
596 * the event such information is needed.
597 */
598 uint8_t
dialog_prompt_nlstate(const char * prompt)599 dialog_prompt_nlstate(const char *prompt)
600 {
601 const char *cp;
602
603 if (prompt == NULL)
604 return 0;
605
606 /*
607 * Work our way backward from the end of the string for efficiency.
608 */
609 cp = prompt + strlen(prompt);
610 while (--cp >= prompt) {
611 /*
612 * If we get to a literal newline first, this prompt ends in a
613 * clean state for rendering with dialog(1). Otherwise, if we
614 * get to an escaped newline first, this prompt ends in an un-
615 * clean state (following literal will be mangled; see above).
616 */
617 if (*cp == '\n')
618 return (0);
619 else if (*cp == 'n' && --cp > prompt && *cp == '\\')
620 return (1);
621 }
622
623 return (0); /* no newlines (escaped or otherwise) */
624 }
625
626 /*
627 * Free allocated items initialized by tty_maxsize_update() and
628 * x11_maxsize_update()
629 */
630 void
dialog_maxsize_free(void)631 dialog_maxsize_free(void)
632 {
633 if (maxsize != NULL) {
634 free(maxsize);
635 maxsize = NULL;
636 }
637 }
638