Files
Faraz Vahedi f68d7bfc14 libc: Add strfromd, strfromf, and strfroml per C23
strfromd(), strfromf(), and strfroml() are implemented directly
in terms of gdtoa.  If a non-conforming format string is passed,
the string "EDOOFUS" is returned and errno set to EDOOFUS as an
extension.

Reviewed by:	fuz
MFC after:	1 month
Pull-Request:	https://github.com/freebsd/freebsd-src/pull/2301
Signed-off-by:	Faraz Vahedi <[email protected]>
2026-08-02 14:10:02 +02:00

477 lines
9.4 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2026 Faraz Vahedi <[email protected]>
*
* 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();
}
}