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:		2e0caa7c7e
Reviewed by:	jhb
Differential Revision:	https://reviews.freebsd.org/D51723
This commit is contained in:
Dag-Erling Smørgrav 2025-08-06 22:34:13 +02:00
parent ad43e7f472
commit ab1c6874e5
5 changed files with 181 additions and 10 deletions

View file

@ -13,6 +13,7 @@ FBSD_1.8 {
cpuset_parselist;
domainset_parselist;
expand_number;
expand_unsigned;
flopen;
flopenat;
forkpty;

View file

@ -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 .

View file

@ -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);
}

View file

@ -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, ...);

View file

@ -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());
}