printf: Fix multiple flags and read overflow - sbase - suckless unix tools
 (HTM) git clone git://git.suckless.org/sbase
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit d77328ca4d1a7aab1905a85faea8bacdcb0cb293
 (DIR) parent 4593bfb3596df8e69a178329283c9d07ed0eddbd
 (HTM) Author: Santtu Lakkala <inz@inz.fi>
       Date:   Wed, 12 Nov 2025 17:49:54 +0200
       
       printf: Fix multiple flags and read overflow
       
       Support having multiple flags for a single conversion; at least '+'/'#' and
       '0' are not exclusive.
       
       Further use strspn() instead of inlined version for correct handling of
       string ending with allowed characters.
       
       Diffstat:
         M printf.c                            |      37 ++++++++++++++++---------------
         A tests/0002-printf.sh                |      44 +++++++++++++++++++++++++++++++
       
       2 files changed, 63 insertions(+), 18 deletions(-)
       ---
 (DIR) diff --git a/printf.c b/printf.c
       @@ -19,11 +19,11 @@ int
        main(int argc, char *argv[])
        {
                Rune *rarg;
       -        size_t i, j, argi, lastargi, formatlen, blen;
       +        size_t i, j, f, argi, lastargi, formatlen, blen, nflags;
                long long num;
                double dou;
                int cooldown = 0, width, precision, ret = 0;
       -        char *format, *tmp, *arg, *fmt, flag;
       +        char *format, *tmp, *arg, *fmt;
        
                argv0 = argv[0];
                if (argc < 2)
       @@ -44,15 +44,19 @@ main(int argc, char *argv[])
                                        break;
                                lastargi = argi;
                        }
       +
                        if (format[i] != '%') {
                                putchar(format[i]);
                                continue;
                        }
        
                        /* flag */
       -                for (flag = '\0', i++; strchr("#-+ 0", format[i]); i++) {
       -                        flag = format[i];
       -                }
       +                f = ++i;
       +                nflags = strspn(&format[f], "#-+ 0");
       +                i += nflags;
       +
       +                if (nflags > INT_MAX)
       +                        eprintf("Too many flags in format\n");
        
                        /* field width */
                        width = -1;
       @@ -64,7 +68,7 @@ main(int argc, char *argv[])
                                i++;
                        } else {
                                j = i;
       -                        for (; strchr("+-0123456789", format[i]); i++);
       +                        i += strspn(&format[i], "+-0123456789");
                                if (j != i) {
                                        tmp = estrndup(format + j, i - j);
                                        width = estrtonum(tmp, 0, INT_MAX);
       @@ -85,7 +89,7 @@ main(int argc, char *argv[])
                                        i++;
                                } else {
                                        j = i;
       -                                for (; strchr("+-0123456789", format[i]); i++);
       +                                i += strspn(&format[i], "+-0123456789");
                                        if (j != i) {
                                                tmp = estrndup(format + j, i - j);
                                                precision = estrtonum(tmp, 0, INT_MAX);
       @@ -127,9 +131,8 @@ main(int argc, char *argv[])
                                free(rarg);
                                break;
                        case 's':
       -                        fmt = estrdup(flag ? "%#*.*s" : "%*.*s");
       -                        if (flag)
       -                                fmt[1] = flag;
       +                        fmt = emalloc(sizeof("%*.*s" + nflags));
       +                        sprintf(fmt, "%%%.*s*.*s", (int)nflags, &format[f]);
                                printf(fmt, width, precision, arg);
                                free(fmt);
                                break;
       @@ -161,22 +164,20 @@ main(int argc, char *argv[])
                                } else {
                                                num = 0;
                                }
       -                        fmt = estrdup(flag ? "%#*.*ll#" : "%*.*ll#");
       -                        if (flag)
       -                                fmt[1] = flag;
       -                        fmt[flag ? 7 : 6] = format[i];
       +                        fmt = emalloc(sizeof("%*.*ll#") + nflags);
       +                        sprintf(fmt, "%%%.*s*.*ll%c", (int)nflags, &format[f], format[i]);
                                printf(fmt, width, precision, num);
                                free(fmt);
                                break;
                        case 'a': case 'A': case 'e': case 'E': case 'f': case 'F': case 'g': case 'G':
       -                        fmt = estrdup(flag ? "%#*.*#" : "%*.*#");
       -                        if (flag)
       -                                fmt[1] = flag;
       -                        fmt[flag ? 5 : 4] = format[i];
       +                        fmt = emalloc(sizeof("%*.*#") + nflags);
       +                        sprintf(fmt, "%%%.*s*.*%c", (int)nflags, &format[f], format[i]);
                                dou = (strlen(arg) > 0) ? estrtod(arg) : 0;
                                printf(fmt, width, precision, dou);
                                free(fmt);
                                break;
       +                case '\0':
       +                        eprintf("Missing format specifier.\n");
                        default:
                                eprintf("Invalid format specifier '%c'.\n", format[i]);
                        }
 (DIR) diff --git a/tests/0002-printf.sh b/tests/0002-printf.sh
       @@ -0,0 +1,44 @@
       +#!/bin/sh
       +
       +set -e
       +
       +exp1=exp1.$$
       +exp2=exp2.$$
       +res1=res1.$$
       +res2=res2.$$
       +
       +cleanup()
       +{
       +        st=$?
       +        rm -f $exp1 $exp2 $res1 $res2
       +        exit $st
       +}
       +
       +trap cleanup EXIT HUP INT TERM
       +
       +cat <<'EOF' > $exp1
       +123
       +0
       +foo
       +bar
       ++001   +2 +003 -400 
       +Expected failure
       +EOF
       +
       +cat <<'EOF' > $exp2
       +../printf: Missing format specifier.
       +EOF
       +
       +(
       +        ../printf '123\n'
       +        ../printf '%d\n'
       +        ../printf '%b' 'foo\nbar\n'
       +
       +        # Two flags used simulatenously, + and 0
       +        ../printf '%+04d %+4d ' 1 2 3 -400; ../printf "\n"
       +        # Missing format specifier; should have sane error message
       +        ../printf '%000' FOO || echo "Expected failure"
       +) > $res1 2> $res2
       +
       +diff -u $exp1 $res1
       +diff -u $exp2 $res2