From 43fc022dc881fa014965bf6e68905d8455086028 Mon Sep 17 00:00:00 2001 From: Martin Buck Date: Fri, 5 Jan 2024 10:34:25 +0100 Subject: [PATCH] Properly handle multi-line strings and newline returned by readline() Fix for #138 According to https://lists.gnu.org/archive/html/bug-readline/2024-01/msg00000.html it's OK for readline() to return multi-line strings and/or newlines in case of bracketed paste (enabled by default since readline 8.1) and also in other situations even though its documentation explicitly states the opposite. So we need to handle this properly in calc instead of just using the first line and dropping the rest: Split the string returned by readline() into lines and return line by line with each invocation of hist_getline(), each possbily adding a terminating newline. --- hist.c | 78 +++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/hist.c b/hist.c index e55918b09..c05c88ee6 100644 --- a/hist.c +++ b/hist.c @@ -73,6 +73,8 @@ #include "banned.h" /* include after system header <> includes */ +#define MIN(a,b) (((a) <= (b)) ? (a) : (b)) + #if !defined(USE_READLINE) E_FUNC FILE *curstream(void); @@ -1472,37 +1474,69 @@ quit_calc(int UNUSED(ch)) * The readline/history libs do most of the dirty work for us, so we can * replace hist_init() and hist_term() with dummies when using readline. * For hist_getline() we have to add a newline that readline removed but - * calc expects. For hist_saveline(), we have to undo this. hist_getline() + * calc expects. For hist_saveline(), we have to undo this. hist_getline() * also has to cope with the different memory management schemes of calc and - * readline. + * readline (pointer to target buffer passed to hist_getline() vs. returned + * malloc()ed buffer from readline()). While doing that, we also split + * multi-line strings potentially returned by readline() in case of + * bracketed paste mode even though its documentation promises to only return + * single lines. For details, see https://github.com/lcn2/calc/issues/138 + * and https://lists.gnu.org/archive/html/bug-readline/2024-01/msg00000.html */ size_t hist_getline(char *prompt, char *buf, size_t len) { - char *line; - - buf[0] = '\0'; - line = readline(prompt); - if (!line) { - switch (conf->ctrl_d) { - case CTRL_D_NEVER_EOF: - return 0; - case CTRL_D_VIRGIN_EOF: - case CTRL_D_EMPTY_EOF: - default: - quit_calc(0); - not_reached(); + STATIC char *rlbuf, *rlcur; + + if (!rlbuf) { + rlbuf = rlcur = readline(prompt); + if (!rlbuf) { + buf[0] = '\0'; + switch (conf->ctrl_d) { + case CTRL_D_NEVER_EOF: + return 0; + case CTRL_D_VIRGIN_EOF: + case CTRL_D_EMPTY_EOF: + default: + quit_calc(0); + not_reached(); + } } } - strlcpy(buf, line, len); - buf[len - 2] = '\0'; - len = strlen(buf); - buf[len] = '\n'; - buf[len + 1] = '\0'; - free(line); - return len + 1; + + /* eol: pointer to trailing newline (if there is one) or \0 */ + char *eol = strchr(rlcur, '\n'); + if (!eol) { + eol = rlcur + strlen(rlcur); + } + /* len: length of line in target buffer including (possibly added) + * newline, truncated if buffer is too small. Note that we reduce + * the available buffer size by 1 so that we can safely add the + * newline below. + */ + len = MIN(len - 1, (size_t)(eol - rlcur + 1)); + strlcpy(buf, rlcur, len); + /* make sure we have a newline and NUL */ + buf[len - 1] = '\n'; + buf[len] = '\0'; + + /* skip over newline in readline buffer */ + if (*eol) { + eol++; + } + /* prepare for next invocation: point to next line or free readline + * buffer if we've reached EOL + */ + if (*eol) { + rlcur = eol; + } else { + free(rlbuf); + rlbuf = rlcur = NULL; + } + + return len; }