root/lib/libc/stdlib/strfrom.c
/*
 * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

/*
 * Shared helpers for strfromd, strfromf, and strfroml (C23 §7.24.1.3).
 */

#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <stdlib.h>

#include "strfrom.h"

struct sf_buf {
        char    *s;
        size_t   n;
        int      pos;
};

/*
 * Write c to b->s[b->pos] if within the n-1 writable bytes; always advance
 * b->pos.  n is the total buffer capacity, including the null slot.
 */
static void
sf_putc(struct sf_buf *b, char c)
{
        if (b->n > 0 && (size_t)b->pos < b->n - 1)
                b->s[b->pos] = c;
        b->pos++;
}

/*
 * Write len bytes from src.
 */
static void
sf_write(struct sf_buf *b, const char *src, int len)
{
        int i;

        for (i = 0; i < len; i++)
                sf_putc(b, src[i]);
}

/*
 * Write a null terminated string.
 */
static void
sf_puts(struct sf_buf *b, const char *src)
{
        while (*src != '\0')
                sf_putc(b, *src++);
}

/*
 * Write count copies of c.
 */
static void
sf_padc(struct sf_buf *b, char c, int count)
{
        int i;

        for (i = 0; i < count; i++)
                sf_putc(b, c);
}

/*
 * Seal the buffer: null terminate at min(pos, n-1).
 */
static void
sf_seal(struct sf_buf *b)
{
        if (b->n > 0)
                b->s[(size_t)b->pos < b->n ? b->pos : b->n - 1] = '\0';
}

/*
 * Emit the radix.  Interfaces are defined as snprintf(3) (C23 §7.24.1.3p2),
 * whose '.' is the LC_NUMERIC decimal point; localeconv() resolves the same
 * per-thread locale as vfprintf(3).  The radix may be multibyte.
 */
static void
sf_putdp(struct sf_buf *b)
{
        sf_puts(b, localeconv()->decimal_point);
}

/*
 * Emit "[eEpP][+-]ddd": exponent sign and decimal magnitude, zero-padded to at
 * least mindig digits (2 for %e/%E and %g/%G, 1 for %a/%A per C23 §7.23.6.1).
 *
 * Handles the full exponent range of every supported type.
 */
static void
sf_emit_exp(struct sf_buf *b, char ec, int exp, int mindig)
{
        char buf[16];
        int len;

        sf_putc(b, ec);
        if (exp < 0) {
                sf_putc(b, '-');
                exp = -exp;
        } else {
                sf_putc(b, '+');
        }

        len = 0;
        do {
                buf[len++] = '0' + exp % 10;
                exp /= 10;
        } while (exp != 0);
        while (len < mindig)
                buf[len++] = '0';
        while (len > 0)
                sf_putc(b, buf[--len]);
}

/*
 * Parse "%[.prec]conv" per C23 §7.24.1.3.
 *
 * Returns the conversion specifier character, or '\0' with errno set to
 * EDOOFUS if fmt is not one of the forms the standard permits; *prec is
 * -1 when the precision is absent.
 */
char
__sf_parse_fmt(const char *fmt, int *prec)
{
        const char *p;

        *prec = -1;
        if (*fmt != '%')
                return ('\0');
        p = fmt + 1;

        if (*p == '.') {
                *prec = 0;
                while (*++p >= '0' && *p <= '9')
                        *prec = *prec * 10 + (*p - '0');
        }

        switch (*p) {
        case 'a': case 'A':
        case 'e': case 'E':
        case 'f': case 'F':
        case 'g': case 'G':
                break;
        default:
                return ('\0');
        }

        return (p[1] == '\0' ? *p : '\0');
}

/*
 * Report a format string the standard does not permit.
 *
 * Sets errno and renders "EDOOFUS" under the ordinary snprintf(3) truncation
 * rules, which keeps a faulty caller alive and its output obviously wrong.
 */
int
__sf_edoofus(char *s, size_t n)
{
        struct sf_buf b;

        errno = EDOOFUS;
        b.s = s;
        b.n = n;
        b.pos = 0;
        sf_puts(&b, "EDOOFUS");
        sf_seal(&b);
        return (b.pos);
}

/*
 * Render Inf or NaN into b->s[0..n-1].
 *
 * is_nan: non-zero if digits[0] == 'N' (NaN), zero for Infinity.
 */
static int
sf_special(struct sf_buf *b, char conv, int signflag, int is_nan)
{
        int upper;

        b->pos = 0;
        upper = isupper((unsigned char)conv);
        if (!is_nan && signflag)
                sf_putc(b, '-');
        sf_puts(b, is_nan ? (upper ? "NAN" : "nan") : (upper ? "INF" : "inf"));
        sf_seal(b);
        return (b->pos);
}

/*
 * Render %e / %E.
 *
 * dtoa was called with mode=2, ndigits=prec+1.  decpt is the position of the
 * first significant digit relative to the decimal point (= exponent + 1), as
 * returned by dtoa.
 */
static int
sf_efmt(struct sf_buf *b, int prec, char conv,
    const char *digits, int ndig, int decpt, int signflag)
{
        int avail, copy;

        b->pos = 0;
        if (signflag)
                sf_putc(b, '-');

        /*
         * Leading significant digit.
         */
        sf_putc(b, ndig > 0 ? digits[0] : '0');

        if (prec > 0) {
                sf_putdp(b);
                avail = ndig > 1 ? ndig - 1 : 0;
                copy = avail < prec ? avail : prec;
                sf_write(b, digits + 1, copy);
                sf_padc(b, '0', prec - copy);
        }
        sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e', decpt - 1, 2);

        sf_seal(b);
        return (b->pos);
}

/*
 * Render %f / %F.
 *
 * dtoa was called with mode=3, ndigits=prec.  decpt gives the number of digits
 * before the decimal point (may be <= 0).
 */
static int
sf_ffmt(struct sf_buf *b, int prec, int signflag,
    const char *digits, int ndig, int decpt)
{
        int avail, copy, rem, zc;

        b->pos = 0;
        if (signflag)
                sf_putc(b, '-');

        if (decpt <= 0) {
                sf_putc(b, '0');
                if (prec > 0) {
                        sf_putdp(b);
                        zc = -decpt < prec ? -decpt : prec;
                        rem = prec - zc;
                        copy = ndig < rem ? ndig : rem;
                        sf_padc(b, '0', zc);
                        sf_write(b, digits, copy);
                        sf_padc(b, '0', rem - copy);
                }
        } else {
                /*
                 * decpt digits (or zeros) before the point.
                 */
                copy = ndig < decpt ? ndig : decpt;
                sf_write(b, digits, copy);
                sf_padc(b, '0', decpt - copy);
                if (prec > 0) {
                        sf_putdp(b);
                        avail = ndig - decpt;
                        if (avail < 0)
                                avail = 0;
                        copy = avail < prec ? avail : prec;
                        sf_write(b, digits + decpt, copy);
                        sf_padc(b, '0', prec - copy);
                }
        }

        sf_seal(b);
        return (b->pos);
}

/*
 * Render %g / %G.
 *
 * dtoa was called with mode=2, ndigits=max(1, prec).
 */
static int
sf_gfmt(struct sf_buf *b, int prec, char conv,
    const char *digits, int ndig, int decpt, int signflag)
{
        int ep, copy, frac;

        b->pos = 0;
        /*
         * Precision 0 is treated as 1 per C23 §7.23.6.1.
         */
        ep = prec == 0 ? 1 : prec;

        /*
         * Strip trailing zeros (no ALT flag is accepted).
         */
        while (ndig > 1 && digits[ndig - 1] == '0')
                ndig--;

        if (signflag)
                sf_putc(b, '-');

        /*
         * Standard says −4 ≤ exponent < P test: exponent = decpt − 1
         */
        if (decpt > -4 && decpt <= ep) {
                /*
                 * %f style, per C23 §7.23.6.1.
                 */
                if (decpt <= 0) {
                        sf_putc(b, '0');
                        if (ndig > 0) {
                                sf_putdp(b);
                                sf_padc(b, '0', -decpt);
                                sf_write(b, digits, ndig);
                        }
                } else {
                        copy = ndig < decpt ? ndig : decpt;
                        sf_write(b, digits, copy);
                        sf_padc(b, '0', decpt - copy);
                        frac = ndig - decpt;
                        if (frac > 0) {
                                sf_putdp(b);
                                sf_write(b, digits + decpt, frac);
                        }
                }
        } else {
                /*
                 * %e style, per C23 §7.23.6.1.
                 */
                sf_putc(b, ndig > 0 ? digits[0] : '0');
                if (ndig > 1) {
                        sf_putdp(b);
                        sf_write(b, digits + 1, ndig - 1);
                }
                sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e',
                    decpt - 1, 2);
        }

        sf_seal(b);
        return (b->pos);
}

/*
 * Render %a / %A.
 *
 * digits / ndig: hex significand digits from __hdtoa / __hldtoa; the first
 * digit represents the integer part of the mantissa (normally '1').
 *
 * decpt: binary exponent such that p-exponent = decpt - 1.
 *
 * user_prec: digits after the hex point (-1 for shortest-exact).
 */
static int
sf_afmt(struct sf_buf *b, int user_prec, char conv,
    const char *digits, int ndig, int decpt, int signflag)
{
        int after, copy;

        b->pos = 0;
        if (signflag)
                sf_putc(b, '-');
        sf_putc(b, '0');
        sf_putc(b, isupper((unsigned char)conv) ? 'X' : 'x');

        /*
         * Integer part of the significand.
         */
        sf_putc(b, ndig > 0 ? digits[0] : '0');

        after = ndig - 1;
        if (user_prec < 0) {
                /*
                 * Shortest-exact: emit only the non-zero significant tail.
                 */
                if (after > 0) {
                        sf_putdp(b);
                        sf_write(b, digits + 1, after);
                }
        } else if (user_prec > 0) {
                sf_putdp(b);
                copy = after < user_prec ? after : user_prec;
                sf_write(b, digits + 1, copy);
                sf_padc(b, '0', user_prec - copy);
        }
        sf_emit_exp(b, isupper((unsigned char)conv) ? 'P' : 'p', decpt - 1, 1);

        sf_seal(b);
        return (b->pos);
}

/*
 * Hex digit table for %a / %A, selected by specifier case.
 */
const char *
__sf_xdigits(char conv)
{
        return (isupper((unsigned char)conv) ? "0123456789ABCDEF" :
            "0123456789abcdef");
}

/*
 * Map a decimal specifier (lowercased) to its gdtoa mode and digit count.
 */
void
__sf_decimal_mode(char lc, int prec, int *mode, int *ndig_req)
{
        switch (lc) {
        case 'e':
                *mode = 2;
                *ndig_req = prec + 1;
                break;
        case 'f':
                *mode = 3;
                *ndig_req = prec;
                break;
        case 'g':
                *mode = 2;
                *ndig_req = prec == 0 ? 1 : prec;
                break;
        default:
                __unreachable();
        }
}

/*
 * Render a finished %a / %A conversion.  __hdtoa / __hldtoa always flag
 * Inf/NaN with decpt == INT_MAX.
 */
int
__sf_render_hex(char *s, size_t n, char conv, int prec,
    char *digits, char *dend, int decpt, int signflag)
{
        struct sf_buf b;
        int ndig, usprec;

        b.s = s;
        b.n = n;
        if (decpt == INT_MAX)
                return (sf_special(&b, conv, signflag, digits[0] == 'N'));
        ndig = (int)(dend - digits);
        usprec = prec >= 0 ? prec : ndig - 1;
        return (sf_afmt(&b, usprec, conv, digits, ndig, decpt, signflag));
}

/*
 * Render a finished %e/%f/%g conversion.  is_special is set when decpt holds
 * the caller's Inf/NaN sentinel (dtoa: 9999, __ldtoa: INT_MAX).
 */
int
__sf_render_decimal(char *s, size_t n, char conv, char lc, int prec,
    char *digits, char *dend, int decpt, int signflag, int is_special)
{
        struct sf_buf b;
        int ndig;

        b.s = s;
        b.n = n;
        if (is_special)
                return (sf_special(&b, conv, signflag, digits[0] == 'N'));
        ndig = (int)(dend - digits);
        switch (lc) {
        case 'e':
                return (sf_efmt(&b, prec, conv, digits, ndig, decpt, signflag));
        case 'f':
                return (sf_ffmt(&b, prec, signflag, digits, ndig, decpt));
        case 'g':
                return (sf_gfmt(&b, prec, conv, digits, ndig, decpt, signflag));
        default:
                __unreachable();
        }
}