Date: Sun, 02 Aug 2026 12:10:52 +0000 From: Robert Clausecker <fuz@FreeBSD.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Faraz Vahedi <kfv@kfv.io> Subject: git: f68d7bfc1479 - main - libc: Add strfromd, strfromf, and strfroml per C23 Message-ID: <6a6f33cc.1cf60.548886f9@gitrepo.freebsd.org>
index | next in thread | raw e-mail
The branch main has been updated by fuz: URL: https://cgit.FreeBSD.org/src/commit/?id=f68d7bfc1479042184e09431bd55771c50c47f68 commit f68d7bfc1479042184e09431bd55771c50c47f68 Author: Faraz Vahedi <kfv@kfv.io> AuthorDate: 2026-06-27 13:34:21 +0000 Commit: Robert Clausecker <fuz@FreeBSD.org> CommitDate: 2026-08-02 12:10:02 +0000 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 <kfv@kfv.io> --- include/stdlib.h | 4 + lib/libc/stdlib/Makefile.inc | 8 +- lib/libc/stdlib/Symbol.map | 3 + lib/libc/stdlib/strfrom.c | 476 ++++++++++++++++++++++++++++ lib/libc/stdlib/strfrom.h | 35 +++ lib/libc/stdlib/strfromd.3 | 109 +++++++ lib/libc/stdlib/strfromd.c | 45 +++ lib/libc/stdlib/strfromf.c | 47 +++ lib/libc/stdlib/strfroml.c | 46 +++ lib/libc/tests/stdlib/Makefile | 1 + lib/libc/tests/stdlib/strfrom_test.c | 590 +++++++++++++++++++++++++++++++++++ 11 files changed, 1363 insertions(+), 1 deletion(-) diff --git a/include/stdlib.h b/include/stdlib.h index 82a347f5317e..c1f465e21fd1 100644 --- a/include/stdlib.h +++ b/include/stdlib.h @@ -175,6 +175,10 @@ _Noreturn void size_t memalignment(const void *) __pure2; void free_sized(void *, size_t) __noexcept; void free_aligned_sized(void *, size_t, size_t) __noexcept; +int strfromd(char * __restrict, size_t, const char * __restrict, double); +int strfromf(char * __restrict, size_t, const char * __restrict, float); +int strfroml(char * __restrict, size_t, + const char * __restrict, long double); #endif /* __ISO_C_VISIBLE >= 2023 */ /* diff --git a/lib/libc/stdlib/Makefile.inc b/lib/libc/stdlib/Makefile.inc index 2cdff5b934cb..7ff6a6c46ea5 100644 --- a/lib/libc/stdlib/Makefile.inc +++ b/lib/libc/stdlib/Makefile.inc @@ -55,6 +55,10 @@ MISRCS+= \ remque.c \ set_constraint_handler_s.c \ strfmon.c \ + strfrom.c \ + strfromd.c \ + strfromf.c \ + strfroml.c \ strtoimax.c \ strtol.c \ strtold.c \ @@ -96,7 +100,7 @@ MAN+= a64l.3 abort.3 abs.3 atexit.3 atof.3 \ quick_exit.3 \ radixsort.3 rand.3 random.3 reallocarray.3 reallocf.3 realpath.3 \ set_constraint_handler_s.3 \ - strfmon.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \ + strfmon.3 strfromd.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \ tsearch.3 MLINKS+=a64l.3 l64a.3 \ @@ -143,6 +147,8 @@ MLINKS+=reallocarray.3 recallocarray.3 MLINKS+=set_constraint_handler_s.3 abort_handler_s.3 MLINKS+=set_constraint_handler_s.3 ignore_handler_s.3 MLINKS+=strfmon.3 strfmon_l.3 +MLINKS+=strfromd.3 strfromf.3 \ + strfromd.3 strfroml.3 MLINKS+=strtod.3 strtof.3 \ strtod.3 strtold.3 MLINKS+=strtol.3 strtoll.3 \ diff --git a/lib/libc/stdlib/Symbol.map b/lib/libc/stdlib/Symbol.map index 373006b4a388..df4922f1df60 100644 --- a/lib/libc/stdlib/Symbol.map +++ b/lib/libc/stdlib/Symbol.map @@ -134,6 +134,9 @@ FBSD_1.8 { FBSD_1.9 { memalignment; recallocarray; + strfromd; + strfromf; + strfroml; strtonumx; tdestroy; }; diff --git a/lib/libc/stdlib/strfrom.c b/lib/libc/stdlib/strfrom.c new file mode 100644 index 000000000000..01725ba30d1f --- /dev/null +++ b/lib/libc/stdlib/strfrom.c @@ -0,0 +1,476 @@ +/* + * 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(); + } +} diff --git a/lib/libc/stdlib/strfrom.h b/lib/libc/stdlib/strfrom.h new file mode 100644 index 000000000000..967897ea7f12 --- /dev/null +++ b/lib/libc/stdlib/strfrom.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * Internal declarations for strfrom* numeric conversion (C23 §7.24.1.3). + * + * Note: gdtoa dtoa() modes used: + * 2 – ndigits significant digits (%e, %g) + * 3 – ndigits digits after '.' (%f) + */ + +#ifndef STRFROM_H +#define STRFROM_H + +#include <limits.h> +#include <stddef.h> + +#define dtoa __dtoa +#define freedtoa __freedtoa + +#include "../stdio/floatio.h" +#include "gdtoa.h" + +char __sf_parse_fmt(const char *, int *); +int __sf_edoofus(char *, size_t); +const char *__sf_xdigits(char); +void __sf_decimal_mode(char, int, int *, int *); +int __sf_render_hex(char *, size_t, char, int, char *, char *, int, int); +int __sf_render_decimal(char *, size_t, char, char, int, char *, char *, + int, int, int); + +#endif /* STRFROM_H */ diff --git a/lib/libc/stdlib/strfromd.3 b/lib/libc/stdlib/strfromd.3 new file mode 100644 index 000000000000..acd3867c0425 --- /dev/null +++ b/lib/libc/stdlib/strfromd.3 @@ -0,0 +1,109 @@ +.\" +.\" Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> +.\" +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.Dd June 27, 2026 +.Dt STRFROMD 3 +.Os +.Sh NAME +.Nm strfromd , +.Nm strfromf , +.Nm strfroml +.Nd convert floating point value to +.Tn ASCII +string +.Sh LIBRARY +.Lb libc +.Sh SYNOPSIS +.In stdlib.h +.Ft int +.Fn strfromd "char * restrict str" "size_t size" "const char * restrict format" "double fp" +.Ft int +.Fn strfromf "char * restrict str" "size_t size" "const char * restrict format" "float fp" +.Ft int +.Fn strfroml "char * restrict str" "size_t size" "const char * restrict format" "long double fp" +.Sh DESCRIPTION +Functions +.Fn strfromd , +.Fn strfromf , +and +.Fn strfroml +convert the floating-point value +.Fa fp +to a null terminated string, storing at most +.Fa size +bytes +.Pq including the terminating null character +into the array pointed to by +.Fa str . +.Pp +These functions are equivalent to +.Fn snprintf "s" "n" "format" "fp" , +except that default argument promotions are not applied to +.Fa fp , +and the +.Fa format +string is restricted. +It shall consist of exactly the character +.Ql % , +followed by an optional precision that does not contain an asterisk +.Ql * , +followed by one of the conversion specifiers +.Cm a , A , e , E , f , F , g , +or +.Cm G . +The conversion specifier applies to the type indicated by the +function suffix, rather than by a length modifier. +.Pp +Use of any other format string results in undefined behaviour, +as does any undefined behaviour inherited from +.Xr snprintf 3 +per se. +In case of a format string not specified by the standard, the string +.Ql EDOOFUS +is written to +.Fa str , +subject to the same truncation rules as a successful conversion, and +.Va errno +is set to +.Er EDOOFUS . +.Sh RETURN VALUES +The +.Fn strfromd , +.Fn strfromf , +and +.Fn strfroml +functions, as per +.Xr snprintf 3 , +return the number of characters +.Po +not counting the terminating null character +.Pc +that would have been written +had +.Fa n +been sufficiently large. +Thus, the null terminated output has been completely written if +and only if the returned value is both nonnegative and less than +.Fa size . +.Sh ERRORS +.Bl -tag -width Er +.It Bq Er EDOOFUS +The +.Fa format +string is not of the form the standard specifies. +.El +.Sh SEE ALSO +.Xr snprintf 3 , +.Xr strtod 3 , +.Xr strtol 3 , +.Xr strtoul 3 +.Sh STANDARDS +Functions +.Fn strfromd , +.Fn strfromf , +and +.Fn strfroml +conform to +.St -isoC-2023 . diff --git a/lib/libc/stdlib/strfromd.c b/lib/libc/stdlib/strfromd.c new file mode 100644 index 000000000000..b7d364c0d9cd --- /dev/null +++ b/lib/libc/stdlib/strfromd.c @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <ctype.h> +#include <stdlib.h> + +#include "strfrom.h" + +int +strfromd(char * __restrict s, size_t n, const char * __restrict fmt, double fp) +{ + char conv, lc, *digits, *dend; + int prec, decpt, signflag, ret, mode, ndig_req; + + conv = __sf_parse_fmt(fmt, &prec); + if (conv == '\0') + return (__sf_edoofus(s, n)); + lc = tolower((unsigned char)conv); + + if (lc == 'a') { + digits = __hdtoa(fp, __sf_xdigits(conv), + prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend); + ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt, + signflag); + freedtoa(digits); + return (ret); + } + + if (prec < 0) + prec = 6; + + __sf_decimal_mode(lc, prec, &mode, &ndig_req); + + digits = dtoa(fp, mode, ndig_req, &decpt, &signflag, &dend); + + ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt, + signflag, decpt == 9999); + + freedtoa(digits); + + return (ret); +} diff --git a/lib/libc/stdlib/strfromf.c b/lib/libc/stdlib/strfromf.c new file mode 100644 index 000000000000..adb87cf2fdda --- /dev/null +++ b/lib/libc/stdlib/strfromf.c @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <ctype.h> +#include <stdlib.h> + +#include "strfrom.h" + +int +strfromf(char * __restrict s, size_t n, const char * __restrict fmt, float fp) +{ + char conv, lc, *digits, *dend; + int prec, decpt, signflag, ret, mode, ndig_req; + double dbl; + + dbl = (double)fp; + conv = __sf_parse_fmt(fmt, &prec); + if (conv == '\0') + return (__sf_edoofus(s, n)); + lc = tolower((unsigned char)conv); + + if (lc == 'a') { + digits = __hdtoa(dbl, __sf_xdigits(conv), + prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend); + ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt, + signflag); + freedtoa(digits); + return (ret); + } + + if (prec < 0) + prec = 6; + + __sf_decimal_mode(lc, prec, &mode, &ndig_req); + + digits = dtoa(dbl, mode, ndig_req, &decpt, &signflag, &dend); + + ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt, + signflag, decpt == 9999); + + freedtoa(digits); + + return (ret); +} diff --git a/lib/libc/stdlib/strfroml.c b/lib/libc/stdlib/strfroml.c new file mode 100644 index 000000000000..9e213a5ecbee --- /dev/null +++ b/lib/libc/stdlib/strfroml.c @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <ctype.h> +#include <stdlib.h> + +#include "strfrom.h" + +int +strfroml(char * __restrict s, size_t n, const char * __restrict fmt, + long double fp) +{ + char conv, lc, *digits, *dend; + int prec, decpt, signflag, ret, mode, ndig_req; + + conv = __sf_parse_fmt(fmt, &prec); + if (conv == '\0') + return (__sf_edoofus(s, n)); + lc = tolower((unsigned char)conv); + + if (lc == 'a') { + digits = __hldtoa(fp, __sf_xdigits(conv), + prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend); + ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt, + signflag); + freedtoa(digits); + return (ret); + } + + if (prec < 0) + prec = 6; + + __sf_decimal_mode(lc, prec, &mode, &ndig_req); + + digits = __ldtoa(&fp, mode, ndig_req, &decpt, &signflag, &dend); + + ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt, + signflag, decpt == INT_MAX); + + freedtoa(digits); + + return (ret); +} diff --git a/lib/libc/tests/stdlib/Makefile b/lib/libc/tests/stdlib/Makefile index 0dbe059e5264..1a0206bcdab9 100644 --- a/lib/libc/tests/stdlib/Makefile +++ b/lib/libc/tests/stdlib/Makefile @@ -17,6 +17,7 @@ ATF_TESTS_C+= qsort_s_test ATF_TESTS_C+= qsort_bench ATF_TESTS_C+= set_constraint_handler_s_test ATF_TESTS_C+= strfmon_test +ATF_TESTS_C+= strfrom_test ATF_TESTS_C+= system_test ATF_TESTS_C+= tsearch_test ATF_TESTS_CXX+= cxa_thread_atexit_test diff --git a/lib/libc/tests/stdlib/strfrom_test.c b/lib/libc/tests/stdlib/strfrom_test.c new file mode 100644 index 000000000000..8898cde200a6 --- /dev/null +++ b/lib/libc/tests/stdlib/strfrom_test.c @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * Tests for strfromd(3), strfromf(3), and strfroml(3). + * + * C23 §7.24.1.3 states that these functions are equivalent to snprintf(3), + * albeit with some subtle exceptions, and therefore snprintf(3) is used as + * the correctness oracle. Hardcoded string checks guard against both + * producing the same wrong answer. + */ + +#include <errno.h> +#include <float.h> +#include <locale.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <atf-c.h> + +/* + * Buffer large enough for any finite value in %f form, including LDBL_MAX on + * platforms with 80-bit or 128-bit long double. Sizing for the full output + * keeps the snprintf(3) oracle a full-length comparison rather than letting + * both sides truncate alike. + */ +#define FBUF 8192 + +static void +check_d(const char *fmt, double val) +{ + char got[FBUF], ref[FBUF]; + int rlen, glen; + + rlen = snprintf(ref, sizeof(ref), fmt, val); + glen = strfromd(got, sizeof(got), fmt, val); + ATF_CHECK_MSG(glen == rlen, + "strfromd(\"%s\", %g): return %d, want %d", fmt, val, glen, rlen); + ATF_CHECK_STREQ_MSG(ref, got, "strfromd(\"%s\", %g)", fmt, val); +} + +static void +check_f(const char *fmt, float val) +{ + char got[FBUF], ref[FBUF]; + int rlen, glen; + + rlen = snprintf(ref, sizeof(ref), fmt, (double)val); + glen = strfromf(got, sizeof(got), fmt, val); + ATF_CHECK_MSG(glen == rlen, + "strfromf(\"%s\", %g): return %d, want %d", + fmt, (double)val, glen, rlen); + ATF_CHECK_STREQ_MSG(ref, got, "strfromf(\"%s\", %g)", fmt, (double)val); +} + +static void +check_l(const char *fmt, long double val) +{ + char got[FBUF], ref[FBUF], lfmt[32]; + int rlen, glen; + + /* + * snprintf requires the 'L' length modifier for long double. + */ + snprintf(lfmt, sizeof(lfmt), "%.*sL%c", + (int)(strlen(fmt) - 1), fmt, fmt[strlen(fmt) - 1]); + rlen = snprintf(ref, sizeof(ref), lfmt, val); + glen = strfroml(got, sizeof(got), fmt, val); + ATF_CHECK_MSG(glen == rlen, + "strfroml(\"%s\", %Lg): return %d, want %d", fmt, val, glen, rlen); + ATF_CHECK_STREQ_MSG(ref, got, "strfroml(\"%s\", %Lg)", fmt, val); +} *** 513 LINES SKIPPED ***home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a6f33cc.1cf60.548886f9>
