The branch main has been updated by fuz:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=f68d7bfc1479042184e09431bd55771c50c47f68

commit f68d7bfc1479042184e09431bd55771c50c47f68
Author:     Faraz Vahedi <[email protected]>
AuthorDate: 2026-06-27 13:34:21 +0000
Commit:     Robert Clausecker <[email protected]>
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 <[email protected]>
---
 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 <[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();
+       }
+}
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 <[email protected]>
+ *
+ * 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 <[email protected]>
+.\"
+.\" 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 <[email protected]>
+ *
+ * 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 <[email protected]>
+ *
+ * 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 <[email protected]>
+ *
+ * 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 <[email protected]>
+ *
+ * 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 ***

Reply via email to