git: ab1c6874e500 - main - libutil: Backward compatibility for expand_number()

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 06 Aug 2025 20:43:40 UTC
The branch main has been updated by des:

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

commit ab1c6874e5004a8ac4e6b077a4aee307e22628b1
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-08-06 20:34:13 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-08-06 20:43:13 +0000

    libutil: Backward compatibility for expand_number()
    
    Reimplement expand_number() in terms of expand_unsigned(), which takes
    a pointer to uint64_t like expand_number() did before.  Provide a macro
    that picks the correct version based on the type of the argument.
    
    Fixes:          2e0caa7c7e14
    Reviewed by:    jhb
    Differential Revision:  https://reviews.freebsd.org/D51723
---
 lib/libutil/Symbol.map                 |  1 +
 lib/libutil/expand_number.3            | 58 +++++++++++++++++++++++---
 lib/libutil/expand_number.c            | 49 +++++++++++++++++++---
 lib/libutil/libutil.h                  |  8 ++++
 lib/libutil/tests/expand_number_test.c | 75 ++++++++++++++++++++++++++++++++++
 5 files changed, 181 insertions(+), 10 deletions(-)

diff --git a/lib/libutil/Symbol.map b/lib/libutil/Symbol.map
index 8c8fff451cd1..6b8a1ec099bf 100644
--- a/lib/libutil/Symbol.map
+++ b/lib/libutil/Symbol.map
@@ -13,6 +13,7 @@ FBSD_1.8 {
 	cpuset_parselist;
 	domainset_parselist;
 	expand_number;
+	expand_unsigned;
 	flopen;
 	flopenat;
 	forkpty;
diff --git a/lib/libutil/expand_number.3 b/lib/libutil/expand_number.3
index 1b932400de69..b1833cedf406 100644
--- a/lib/libutil/expand_number.3
+++ b/lib/libutil/expand_number.3
@@ -24,11 +24,12 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd July 25, 2025
+.Dd August 6, 2025
 .Dt EXPAND_NUMBER 3
 .Os
 .Sh NAME
-.Nm expand_number
+.Nm expand_number ,
+.Nm expand_unsigned
 .Nd parse a number from human readable form
 .Sh LIBRARY
 .Lb libutil
@@ -38,6 +39,10 @@
 .Fo expand_number
 .Fa "const char *buf" "int64_t *num"
 .Fc
+.Ft int
+.Fo expand_unsigned
+.Fa "const char *buf" "uint64_t *num"
+.Fc
 .Sh DESCRIPTION
 The
 .Fn expand_number
@@ -48,6 +53,17 @@ quantity in the location pointed to by its
 .Fa *num
 argument.
 .Pp
+The
+.Fn expand_unsigned
+function is similar to
+.Fn expand_number ,
+but accepts only positive numbers in the range
+.Bq 0, Ns Dv UINT64_MAX .
+.Pp
+Both functions interpret the input
+.Dq -0
+as 0.
+.Pp
 The input string must consist of a decimal number, optionally preceded
 by a
 .Sq +
@@ -81,20 +97,38 @@ is interpreted as 5, and
 .Dq 5kb
 is interpreted as 5,120).
 However, the usage of this suffix is discouraged.
+.Pp
+For backward compatibility reasons, if the compiler supports generic
+selection, a macro is provided which automatically replaces calls to
+.Fn expand_number
+with calls to
+.Fn expand_unsigned
+if the type of the actual
+.Va num
+argument is compatible with
+.Vt uint64_t * .
 .Sh RETURN VALUES
 .Rv -std
 .Sh ERRORS
 The
 .Fn expand_number
-function will fail if:
+and
+.Fn expand_unsigned
+functions will fail if:
 .Bl -tag -width Er
 .It Bq Er EINVAL
 The given string does not contain a valid number.
 .It Bq Er EINVAL
 An unrecognized suffix was encountered.
 .It Bq Er ERANGE
-The given string represents a number which does not fit into a
-.Vt int64_t .
+The given string represents a number which does not fit into an
+.Vt int64_t
+(for
+.Fn expand_number )
+or
+.Vt uint64_t
+(for
+.Fn expand_unsigned ) .
 .El
 .Sh SEE ALSO
 .Xr humanize_number 3
@@ -103,3 +137,17 @@ The
 .Fn expand_number
 function first appeared in
 .Fx 6.3 .
+The original implementation did not handle negative numbers correctly,
+and it was switched to taking a
+.Vt uint64_t *
+and accepting only positive numbers in
+.Fx 9.0 .
+The
+.Fn expand_unsigned
+function was added,
+and
+.Fn expand_number
+switched back to
+.Vt int64_t * ,
+in
+.Fx 15.0 .
diff --git a/lib/libutil/expand_number.c b/lib/libutil/expand_number.c
index f4c19d7867a3..a3313ba39d98 100644
--- a/lib/libutil/expand_number.c
+++ b/lib/libutil/expand_number.c
@@ -37,13 +37,12 @@
 #include <stdbool.h>
 #include <stdint.h>
 
-int
-expand_number(const char *buf, int64_t *num)
+static int
+expand_impl(const char *buf, uint64_t *num, bool *neg)
 {
 	char *endptr;
 	uintmax_t number;
 	unsigned int shift;
-	bool neg;
 	int serrno;
 
 	/*
@@ -52,10 +51,10 @@ expand_number(const char *buf, int64_t *num)
 	while (isspace((unsigned char)*buf))
 		buf++;
 	if (*buf == '-') {
-		neg = true;
+		*neg = true;
 		buf++;
 	} else {
-		neg = false;
+		*neg = false;
 		if (*buf == '+')
 			buf++;
 	}
@@ -127,6 +126,22 @@ expand_number(const char *buf, int64_t *num)
 	}
 	number <<= shift;
 
+	*num = number;
+	return (0);
+}
+
+int
+(expand_number)(const char *buf, int64_t *num)
+{
+	uint64_t number;
+	bool neg;
+
+	/*
+	 * Parse the number.
+	 */
+	if (expand_impl(buf, &number, &neg) != 0)
+		return (-1);
+
 	/*
 	 * Apply the sign and check for overflow.
 	 */
@@ -146,3 +161,27 @@ expand_number(const char *buf, int64_t *num)
 
 	return (0);
 }
+
+int
+expand_unsigned(const char *buf, uint64_t *num)
+{
+	uint64_t number;
+	bool neg;
+
+	/*
+	 * Parse the number.
+	 */
+	if (expand_impl(buf, &number, &neg) != 0)
+		return (-1);
+
+	/*
+	 * Negative numbers are out of range.
+	 */
+	if (neg && number > 0) {
+		errno = ERANGE;
+		return (-1);
+	}
+
+	*num = number;
+	return (0);
+}
diff --git a/lib/libutil/libutil.h b/lib/libutil/libutil.h
index d27262e44daf..9b5b2abe7f09 100644
--- a/lib/libutil/libutil.h
+++ b/lib/libutil/libutil.h
@@ -89,6 +89,14 @@ __BEGIN_DECLS
 void	clean_environment(const char * const *_white,
 	    const char * const *_more_white);
 int	expand_number(const char *_buf, int64_t *_num);
+int	expand_unsigned(const char *_buf, uint64_t *_num);
+#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L) ||	\
+	__has_extension(c_generic_selections)
+#define expand_number(_buf, _num)					\
+	_Generic((_num),						\
+		 uint64_t *: expand_unsigned,				\
+		 default: expand_number)((_buf), (_num))
+#endif
 int	extattr_namespace_to_string(int _attrnamespace, char **_string);
 int	extattr_string_to_namespace(const char *_string, int *_attrnamespace);
 int	flopen(const char *_path, int _flags, ...);
diff --git a/lib/libutil/tests/expand_number_test.c b/lib/libutil/tests/expand_number_test.c
index 8e7458994de4..8ff56e1ed01f 100644
--- a/lib/libutil/tests/expand_number_test.c
+++ b/lib/libutil/tests/expand_number_test.c
@@ -206,10 +206,85 @@ ATF_TC_BODY(expand_number__bad, tp)
 	require_error(" + 1", EINVAL);
 }
 
+ATF_TC_WITHOUT_HEAD(expand_unsigned);
+ATF_TC_BODY(expand_unsigned, tp)
+{
+	static struct tc {
+		const char *str;
+		uint64_t num;
+		int error;
+	} tcs[] = {
+		{ "0", 0, 0 },
+		{ "+0", 0, 0 },
+		{ "-0", 0, 0 },
+		{ "1", 1, 0 },
+		{ "+1", 1, 0 },
+		{ "-1", 0, ERANGE },
+		{ "18446744073709551615", UINT64_MAX, 0 },
+		{ "+18446744073709551615", UINT64_MAX, 0 },
+		{ "-18446744073709551615", 0, ERANGE },
+		{ 0 },
+	};
+	struct tc *tc;
+	uint64_t num;
+	int error, ret;
+
+	for (tc = tcs; tc->str != NULL; tc++) {
+		ret = expand_number(tc->str, &num);
+		error = errno;
+		if (tc->error == 0) {
+			ATF_REQUIRE_EQ_MSG(0, ret,
+			    "%s ret = %d", tc->str, ret);
+			ATF_REQUIRE_EQ_MSG(tc->num, num,
+			    "%s num = %ju", tc->str, (uintmax_t)num);
+		} else {
+			ATF_REQUIRE_EQ_MSG(-1, ret,
+			    "%s ret = %d", tc->str, ret);
+			ATF_REQUIRE_EQ_MSG(tc->error, error,
+			    "%s errno = %d", tc->str, error);
+		}
+	}
+}
+
+ATF_TC_WITHOUT_HEAD(expand_generic);
+ATF_TC_BODY(expand_generic, tp)
+{
+	uint64_t uint64;
+	int64_t int64;
+	size_t size;
+	off_t off;
+
+	ATF_REQUIRE_EQ(0, expand_number("18446744073709551615", &uint64));
+	ATF_REQUIRE_EQ(UINT64_MAX, uint64);
+	ATF_REQUIRE_EQ(-1, expand_number("-1", &uint64));
+	ATF_REQUIRE_EQ(ERANGE, errno);
+
+	ATF_REQUIRE_EQ(0, expand_number("9223372036854775807", &int64));
+	ATF_REQUIRE_EQ(INT64_MAX, int64);
+	ATF_REQUIRE_EQ(-1, expand_number("9223372036854775808", &int64));
+	ATF_REQUIRE_EQ(ERANGE, errno);
+	ATF_REQUIRE_EQ(0, expand_number("-9223372036854775808", &int64));
+	ATF_REQUIRE_EQ(INT64_MIN, int64);
+
+	ATF_REQUIRE_EQ(0, expand_number("18446744073709551615", &size));
+	ATF_REQUIRE_EQ(UINT64_MAX, size);
+	ATF_REQUIRE_EQ(-1, expand_number("-1", &size));
+	ATF_REQUIRE_EQ(ERANGE, errno);
+
+	ATF_REQUIRE_EQ(0, expand_number("9223372036854775807", &off));
+	ATF_REQUIRE_EQ(INT64_MAX, off);
+	ATF_REQUIRE_EQ(-1, expand_number("9223372036854775808", &off));
+	ATF_REQUIRE_EQ(ERANGE, errno);
+	ATF_REQUIRE_EQ(0, expand_number("-9223372036854775808", &off));
+	ATF_REQUIRE_EQ(INT64_MIN, off);
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 	ATF_TP_ADD_TC(tp, expand_number__ok);
 	ATF_TP_ADD_TC(tp, expand_number__bad);
+	ATF_TP_ADD_TC(tp, expand_unsigned);
+	ATF_TP_ADD_TC(tp, expand_generic);
 
 	return (atf_no_error());
 }