Commit 8dcd98b5f0 for strongswan.org

commit 8dcd98b5f04915d84588744e7a5fceb4e9da831a
Author: Tobias Brunner <tobias@strongswan.org>
Date:   Fri Sep 18 09:46:33 2026 +0200

    string: Add helpers to parse fixed-size integers from strings

    We currently call strto[u]l[l]() in various locations, some with proper
    validation/range checks, most without.  These functions are meant to
    replace those calls.

    Also added a helper to determine the base from the string as we
    generally only want to use 10 and 16 (i.e. no base 8 and rarely base 2).

diff --git a/src/libstrongswan/tests/suites/test_utils.c b/src/libstrongswan/tests/suites/test_utils.c
index 7cda4acecd..7da9dc8b76 100644
--- a/src/libstrongswan/tests/suites/test_utils.c
+++ b/src/libstrongswan/tests/suites/test_utils.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2015 Tobias Brunner
+ * Copyright (C) 2013-2026 Tobias Brunner
  *
  * Copyright (C) secunet Security Networks AG
  *
@@ -126,6 +126,227 @@ START_TEST(test_timeval_add_ms)
 }
 END_TEST

+/*******************************************************************************
+ * [u]int64_from_string
+ */
+
+static struct {
+	char *s;
+	int b;
+	char e;
+	bool v;
+	uint64_t i;
+} ufs_data[] = {
+	{NULL,	0,	'\0',	FALSE,	0},
+	{"",	0,	'\0',	FALSE,	0},
+	{"  ",	0,	'\0',	FALSE,	0},
+	{"-",	0,	'\0',	FALSE,	0},
+	{"+",	0,	'\0',	FALSE,	0},
+	{"a",	0,	'\0',	FALSE,	0},
+	{"0",	0,	'\0',	TRUE,	0},
+	{"-0",	0,	'\0',	FALSE,	0},
+	{"5",	0,	'\0',	TRUE,	5},
+	{"5",	2,	'\0',	FALSE,	0},
+	{" 5",	0,	'\0',	TRUE,	5},
+	{"\t5",	0,	'\0',	TRUE,	5},
+	{"+5",	0,	'\0',	TRUE,	5},
+	{"-5",	0,	'\0',	FALSE,	0},
+	{" -5",	0,	'\0',	FALSE,	0},
+	{"5s",	0,	's',	TRUE,	5},
+	{"010",	0,	'\0',	TRUE,	8},
+	{"010",	2,	'\0',	TRUE,	2},
+	{"010",	10,	'\0',	TRUE,	10},
+	{"0x10",	0,	'\0',	TRUE,	16},
+	{"0x10",	10,	'x',	TRUE,	0},
+	{"0x10",	16,	'\0',	TRUE,	16},
+	{"0x10s",	0,	's',	TRUE,	16},
+	{"18446744073709551615",	0,	'\0',	TRUE,	UINT64_MAX},
+	{"18446744073709551615s",	0,	's',	TRUE,	UINT64_MAX},
+	{"18446744073709551616",	0,	'\0',	FALSE,	0},
+	{"18446744073709551616s",	0,	'\0',	FALSE,	0},
+};
+
+START_TEST(test_uint64_from_string)
+{
+	char *end;
+	uint64_t val = 42;
+
+	ck_assert(!uint64_from_string(ufs_data[_i].s, NULL, ufs_data[_i].b, NULL));
+	ck_assert(!uint64_from_string(ufs_data[_i].s, &end, ufs_data[_i].b, NULL));
+	ck_assert(uint64_from_string(ufs_data[_i].s, NULL, ufs_data[_i].b,
+								 &val) == ufs_data[_i].v);
+	if (ufs_data[_i].v)
+	{
+		ck_assert_int_eq(val, ufs_data[_i].i);
+	}
+	else
+	{
+		ck_assert_int_eq(val, 42);
+	}
+	val = 42;
+	ck_assert(uint64_from_string(ufs_data[_i].s, &end, ufs_data[_i].b,
+								 &val) == ufs_data[_i].v);
+	if (ufs_data[_i].v)
+	{
+		ck_assert_int_eq(val, ufs_data[_i].i);
+		ck_assert_int_eq(ufs_data[_i].e, *end);
+	}
+	else
+	{
+		ck_assert_int_eq(val, 42);
+	}
+}
+END_TEST
+
+START_TEST(test_uintx_from_string)
+{
+	char *end;
+	uint32_t val32 = 0;
+	uint16_t val16 = 0;
+	uint8_t val8 = 0;
+
+	/* the main parsing functionality is already tested above, so only check
+	 * range enforcement for smaller integer types */
+	ck_assert(!uint32_from_string("4294967295", NULL, 0, NULL));
+	ck_assert(!uint32_from_string("4294967295", &end, 0, NULL));
+	ck_assert(uint32_from_string("4294967295", NULL, 0, &val32));
+	ck_assert(uint32_from_string("4294967295", &end, 0, &val32));
+	ck_assert_int_eq(val32, UINT32_MAX);
+	ck_assert(!uint32_from_string("4294967296", &end, 0, &val32));
+
+	ck_assert(!uint16_from_string("65535", NULL, 0, NULL));
+	ck_assert(!uint16_from_string("65535", &end, 0, NULL));
+	ck_assert(uint16_from_string("65535", NULL, 0, &val16));
+	ck_assert(uint16_from_string("65535", &end, 0, &val16));
+	ck_assert_int_eq(val16, UINT16_MAX);
+	ck_assert(!uint16_from_string("65536", &end, 0, &val16));
+
+	ck_assert(!uint8_from_string("255", NULL, 0, NULL));
+	ck_assert(!uint8_from_string("255", &end, 0, NULL));
+	ck_assert(uint8_from_string("255", NULL, 0, &val8));
+	ck_assert(uint8_from_string("255", &end, 0, &val8));
+	ck_assert_int_eq(val8, UINT8_MAX);
+	ck_assert(!uint8_from_string("256", &end, 0, &val8));
+}
+END_TEST
+
+
+static struct {
+	char *s;
+	int b;
+	char e;
+	bool v;
+	int64_t i;
+} ifs_data[] = {
+	{NULL,	0,	'\0',	FALSE,	0},
+	{"",	0,	'\0',	FALSE,	0},
+	{"  ",	0,	'\0',	FALSE,	0},
+	{"-",	0,	'\0',	FALSE,	0},
+	{"+",	0,	'\0',	FALSE,	0},
+	{"a",	0,	'\0',	FALSE,	0},
+	{"0",	0,	'\0',	TRUE,	0},
+	{"-0",	0,	'\0',	TRUE,	0},
+	{"5",	0,	'\0',	TRUE,	5},
+	{"5",	2,	'\0',	FALSE,	0},
+	{" 5",	0,	'\0',	TRUE,	5},
+	{"\t5",	0,	'\0',	TRUE,	5},
+	{"+5",	0,	'\0',	TRUE,	5},
+	{"-5",	0,	'\0',	TRUE,	-5},
+	{" -5",	0,	'\0',	TRUE,	-5},
+	{"5s",	0,	's',	TRUE,	5},
+	{"010",	0,	'\0',	TRUE,	8},
+	{"010",	2,	'\0',	TRUE,	2},
+	{"010",	10,	'\0',	TRUE,	10},
+	{"0x10",	0,	'\0',	TRUE,	16},
+	{"0x10",	10,	'x',	TRUE,	0},
+	{"0x10",	16,	'\0',	TRUE,	16},
+	{"-0x10",	16,	'\0',	TRUE,	-16},
+	{"0x10s",	0,	's',	TRUE,	16},
+	{"-0x10s",	0,	's',	TRUE,	-16},
+	{"9223372036854775807",		0,	'\0',	TRUE,	INT64_MAX},
+	{"9223372036854775807s",	0,	's',	TRUE,	INT64_MAX},
+	{"9223372036854775808",		0,	'\0',	FALSE,	0},
+	{"9223372036854775808s",	0,	'\0',	FALSE,	0},
+	{"-9223372036854775808",	0,	'\0',	TRUE,	INT64_MIN},
+	{"-9223372036854775808s",	0,	's',	TRUE,	INT64_MIN},
+	{"-9223372036854775809",	0,	'\0',	FALSE,	0},
+	{"-9223372036854775809s",	0,	'\0',	FALSE,	0},
+};
+
+START_TEST(test_int64_from_string)
+{
+	char *end;
+	int64_t val = 42;
+
+	ck_assert(!int64_from_string(ifs_data[_i].s, NULL, ifs_data[_i].b, NULL));
+	ck_assert(!int64_from_string(ifs_data[_i].s, &end, ifs_data[_i].b, NULL));
+	ck_assert(int64_from_string(ifs_data[_i].s, NULL, ifs_data[_i].b,
+								&val) == ifs_data[_i].v);
+	if (ifs_data[_i].v)
+	{
+		ck_assert_int_eq(val, ifs_data[_i].i);
+	}
+	else
+	{
+		ck_assert_int_eq(val, 42);
+	}
+	val = 42;
+	ck_assert(int64_from_string(ifs_data[_i].s, &end, ifs_data[_i].b,
+								&val) == ifs_data[_i].v);
+	if (ifs_data[_i].v)
+	{
+		ck_assert_int_eq(val, ifs_data[_i].i);
+		ck_assert_int_eq(ifs_data[_i].e, *end);
+	}
+	else
+	{
+		ck_assert_int_eq(val, 42);
+	}
+}
+END_TEST
+
+START_TEST(test_intx_from_string)
+{
+	char *end;
+	int32_t val32 = 0;
+
+	/* the main parsing functionality is already tested above, so only check
+	 * range enforcement for smaller integer types */
+	ck_assert(!int32_from_string("2147483647", NULL, 0, NULL));
+	ck_assert(!int32_from_string("2147483647", &end, 0, NULL));
+	ck_assert(int32_from_string("2147483647", NULL, 0, &val32));
+	ck_assert(int32_from_string("2147483647", &end, 0, &val32));
+	ck_assert_int_eq(val32, INT32_MAX);
+	ck_assert(!int32_from_string("2147483648", &end, 0, &val32));
+
+	ck_assert(int32_from_string("-2147483648", &end, 0, &val32));
+	ck_assert_int_eq(val32, INT32_MIN);
+	ck_assert(!int32_from_string("-2147483649", &end, 0, &val32));
+}
+END_TEST
+
+/*******************************************************************************
+ * base_from_string
+ */
+
+START_TEST(test_base_from_string)
+{
+	ck_assert_int_eq(base_from_string(NULL), 10);
+	ck_assert_int_eq(base_from_string(""), 10);
+	ck_assert_int_eq(base_from_string(" "), 10);
+	ck_assert_int_eq(base_from_string("0"), 10);
+	ck_assert_int_eq(base_from_string(" 0"), 10);
+	ck_assert_int_eq(base_from_string("0x"), 16);
+	ck_assert_int_eq(base_from_string("-0x"), 16);
+	ck_assert_int_eq(base_from_string("+0x"), 16);
+	ck_assert_int_eq(base_from_string(" -0x"), 16);
+	ck_assert_int_eq(base_from_string("  0x"), 16);
+	ck_assert_int_eq(base_from_string("0x2"), 16);
+	ck_assert_int_eq(base_from_string("\t0x2"), 16);
+	ck_assert_int_eq(base_from_string("\t -0x2"), 16);
+}
+END_TEST
+
 /*******************************************************************************
  * timespan_from_string
  */
@@ -1343,6 +1564,17 @@ Suite *utils_suite_create()
 	tcase_add_test(tc, test_timeval_add_ms);
 	suite_add_tcase(s, tc);

+	tc = tcase_create("[u]int64_from_string");
+	tcase_add_loop_test(tc, test_uint64_from_string, 0, countof(ufs_data));
+	tcase_add_test(tc, test_uintx_from_string);
+	tcase_add_loop_test(tc, test_int64_from_string, 0, countof(ifs_data));
+	tcase_add_test(tc, test_intx_from_string);
+	suite_add_tcase(s, tc);
+
+	tc = tcase_create("base_from_string");
+	tcase_add_test(tc, test_base_from_string);
+	suite_add_tcase(s, tc);
+
 	tc = tcase_create("timespan_from_string");
 	tcase_add_loop_test(tc, test_timespan_from_string, 0, countof(ts_data));
 	suite_add_tcase(s, tc);
diff --git a/src/libstrongswan/utils/utils/string.c b/src/libstrongswan/utils/utils/string.c
index 20c76ca332..4ed4fcd8e5 100644
--- a/src/libstrongswan/utils/utils/string.c
+++ b/src/libstrongswan/utils/utils/string.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2014 Tobias Brunner
+ * Copyright (C) 2008-2026 Tobias Brunner
  * Copyright (C) 2005-2008 Martin Willi
  *
  * Copyright (C) secunet Security Networks AG
@@ -15,9 +15,11 @@
  * for more details.
  */

+#include <errno.h>
+
 #include <utils/utils.h>

-/**
+/*
  * Described in header.
  */
 char* translate(char *str, const char *from, const char *to)
@@ -39,7 +41,7 @@ char* translate(char *str, const char *from, const char *to)
 	return str;
 }

-/**
+/*
  * Described in header.
  */
 char* strreplace(const char *str, const char *search, const char *replace)
@@ -90,3 +92,65 @@ char* strreplace(const char *str, const char *search, const char *replace)
 	strcpy(dst, pos);
 	return res;
 }
+
+/*
+ * Described in header
+ */
+bool uint64_from_string(const char *str, char **end, int base, uint64_t *out)
+{
+	const char *startptr = str;
+	char *endptr;
+	unsigned long long val;
+
+	if (!str || !out)
+	{
+		return FALSE;
+	}
+	/* explicitly reject '-' prefixes and whitespace-only strings */
+	while (isspace((u_char)*startptr))
+	{
+		startptr++;
+	}
+	if (!*startptr || *startptr == '-')
+	{
+		return FALSE;
+	}
+	errno = 0;
+	val = strtoull(startptr, &endptr, base);
+	if (endptr == startptr || errno)
+	{
+		return FALSE;
+	}
+	*out = val;
+	if (end)
+	{
+		*end = endptr;
+	}
+	return TRUE;
+}
+
+/*
+ * Described in header
+ */
+bool int64_from_string(const char *str, char **end, int base, int64_t *out)
+{
+	char *endptr;
+	long long val;
+
+	if (!str || !out)
+	{
+		return FALSE;
+	}
+	errno = 0;
+	val = strtoll(str, &endptr, base);
+	if (endptr == str || errno)
+	{
+		return FALSE;
+	}
+	*out = val;
+	if (end)
+	{
+		*end = endptr;
+	}
+	return TRUE;
+}
diff --git a/src/libstrongswan/utils/utils/string.h b/src/libstrongswan/utils/utils/string.h
index 6c91cae0c5..a55b4438d0 100644
--- a/src/libstrongswan/utils/utils/string.h
+++ b/src/libstrongswan/utils/utils/string.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2014 Tobias Brunner
+ * Copyright (C) 2008-2026 Tobias Brunner
  * Copyright (C) 2008 Martin Willi
  *
  * Copyright (C) secunet Security Networks AG
@@ -23,6 +23,8 @@
 #ifndef STRING_H_
 #define STRING_H_

+#include <ctype.h>
+
 /**
  * Helper function that compares two strings for equality
  */
@@ -101,4 +103,164 @@ char *translate(char *str, const char *from, const char *to);
  */
 char *strreplace(const char *str, const char *search, const char *replace);

+/**
+ * Parse an unsigned 64-bit integer from a string, wrapping strtoull().
+ *
+ * It rejects the following:  empty/no-digit input, a leading '-' (which
+ * strtoull() would wrap modulo 2^64) and out-of-range values.
+ *
+ * Only the prefix is parsed, \p end (if given) points to the first unconsumed
+ * character so callers can scan suffixes.  To ensure a plain number,
+ * additionally check <tt>*end == '\0'</tt>.
+ *
+ * Prefer base 10 (with the settings_t-style explicit 0x-check for hex) because
+ * base 0 additionally enables octal, which silently reinterprets zero-padded
+ * input ("010" => 8).
+ *
+ * @param str		string to parse (NULL-safe)
+ * @param end		first unconsumed character on success (optional)
+ * @param base		numeric base, same as strtoull()
+ * @param[out] out	parsed value on success, unchanged otherwise (required)
+ * @return			TRUE if at least one digit converted without range error
+ */
+bool uint64_from_string(const char *str, char **end, int base, uint64_t *out);
+
+/**
+ * Parse an unsigned 32-bit integer from a string, wrapping strtoull().
+ *
+ * @copydetails uint64_from_string
+ */
+static inline bool uint32_from_string(const char *str, char **end, int base,
+									  uint32_t *out)
+{
+	char *endptr;
+	uint64_t val;
+
+	if (out && uint64_from_string(str, &endptr, base, &val) &&
+		val <= UINT32_MAX)
+	{
+		if (end)
+		{
+			*end = endptr;
+		}
+		*out = val;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+/**
+ * Parse an unsigned 16-bit integer from a string, wrapping strtoull().
+ *
+ * @copydetails uint64_from_string
+ */
+static inline bool uint16_from_string(const char *str, char **end, int base,
+									  uint16_t *out)
+{
+	char *endptr;
+	uint64_t val;
+
+	if (out && uint64_from_string(str, &endptr, base, &val) &&
+		val <= UINT16_MAX)
+	{
+		if (end)
+		{
+			*end = endptr;
+		}
+		*out = val;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+/**
+ * Parse an unsigned 8-bit integer from a string, wrapping strtoull().
+ *
+ * @copydetails uint64_from_string
+ */
+static inline bool uint8_from_string(const char *str, char **end, int base,
+									 uint8_t *out)
+{
+	char *endptr;
+	uint64_t val;
+
+	if (out && uint64_from_string(str, &endptr, base, &val) && val <= UINT8_MAX)
+	{
+		if (end)
+		{
+			*end = endptr;
+		}
+		*out = val;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+/**
+ * Parse a signed 64-bit integer from a string, wrapping strtoll().
+ *
+ * It rejects the following:  empty/no-digit input and out-of-range values.
+ * A leading -/+ is accepted as the value's sign.
+ *
+ * Only the prefix is parsed, \p end (if given) points to the first unconsumed
+ * character so callers can scan suffixes.  To ensure a plain number,
+ * additionally check <tt>*end == '\0'</tt>.
+ *
+ * Prefer base 10 (with the settings_t-style explicit 0x-check for hex) because
+ * base 0 additionally enables octal, which silently reinterprets zero-padded
+ * input ("010" => 8).
+ *
+ * @param str		string to parse (NULL-safe)
+ * @param end		first unconsumed character on success (optional)
+ * @param base		numeric base, same as strtoll()
+ * @param[out] out	parsed value on success, unchanged otherwise (required)
+ * @return			TRUE if at least one digit converted without range error
+ */
+bool int64_from_string(const char *str, char **end, int base, int64_t *out);
+
+/**
+ * Parse a signed 32-bit integer from a string, wrapping strtoll().
+ *
+ * @copydetails int64_from_string
+ */
+static inline bool int32_from_string(const char *str, char **end, int base,
+									 int32_t *out)
+{
+	char *endptr;
+	int64_t val;
+
+	if (out && int64_from_string(str, &endptr, base, &val) &&
+		val <= INT32_MAX && val >= INT32_MIN)
+	{
+		if (end)
+		{
+			*end = endptr;
+		}
+		*out = val;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+/**
+ * Determine the base when parsing integer strings.
+ *
+ * We generally only want to parse integers in base 10 and 16 (with 0x prefix).
+ *
+ * @param str		string to parse (NULL-safe)
+ * @return			base determined based on the given string, defaults to 10
+ */
+static inline int base_from_string(const char *str)
+{
+	while (str && isspace((u_char)*str))
+	{
+		str++;
+	}
+	if (str && (*str == '-' || *str == '+'))
+	{
+		str++;
+	}
+	return strcasepfx(str, "0x") ? 16 : 10;
+}
+
 #endif /** STRING_H_ @} */