diff --git a/include/sys/string_conv.h b/include/sys/string_conv.h new file mode 100644 index 0000000000000..c4bf85ebdb7ea --- /dev/null +++ b/include/sys/string_conv.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_SYS_STRING_CONV_H__ +#define ZEPHYR_INCLUDE_SYS_STRING_CONV_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Convert string to long param. + * + * @note On failure the passed value reference will not be altered. + * + * @param str Input string + * @param val Converted value + * + * @return 0 on success. + * @return -EINVAL on invalid string input. + * @return -ERANGE if numeric string input is to large to convert. + */ +int string_conv_str2long(const char *str, long *val); + +/** + * @brief Convert string to unsigned long param. + * + * @note On failure the passed value reference will not be altered. + * + * @param str Input string + * @param val Converted value + * + * @return 0 on success. + * @return -EINVAL on invalid string input. + * @return -ERANGE if numeric string input is to large to convert. + */ +int string_conv_str2ulong(const char *str, unsigned long *val); + +/** + * @brief Convert string to double param. + * + * @note On failure the passed value reference will not be altered. + * + * @param str Input string + * @param val Converted value + * + * @return 0 on success. + * @return -EINVAL on invalid string input. + * @return -ERANGE if numeric string input is to large to convert. + */ +int string_conv_str2dbl(const char *str, double *val); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_SYS_STRING_CONV_H__ */ diff --git a/lib/util/CMakeLists.txt b/lib/util/CMakeLists.txt index f4a2b08db17c2..59d83f1d695f9 100644 --- a/lib/util/CMakeLists.txt +++ b/lib/util/CMakeLists.txt @@ -1,3 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory_ifdef(CONFIG_FNMATCH fnmatch) +add_subdirectory(string_conv) diff --git a/lib/util/string_conv/CMakeLists.txt b/lib/util/string_conv/CMakeLists.txt new file mode 100644 index 0000000000000..48bf84d3a5cd5 --- /dev/null +++ b/lib/util/string_conv/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2022 Nordic Semiconductor +# +# SPDX-License-Identifier: Apache-2.0 +# # + +zephyr_sources(string_conv.c) diff --git a/lib/util/string_conv/string_conv.c b/lib/util/string_conv/string_conv.c new file mode 100644 index 0000000000000..23c76cd56da1d --- /dev/null +++ b/lib/util/string_conv/string_conv.c @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +static size_t whitespace_trim(char *out, size_t len, const char *str) +{ + if (len == 0) { + return 0; + } + + const char *end; + size_t out_size; + + while (str[0] == ' ') { + str++; + } + + if (*str == 0) { + *out = 0; + return 1; + } + + end = str + strlen(str) - 1; + while (end > str && (end[0] == ' ')) { + end--; + } + end++; + + out_size = (end - str) + 1; + + if (out_size > len) { + return 0; + } + + memcpy(out, str, out_size - 1); + out[out_size - 1] = 0; + + return out_size; +} + +int string_conv_str2long(char *str, long *val) +{ + char trimmed_buf[12] = { 0 }; + size_t len = whitespace_trim(trimmed_buf, sizeof(trimmed_buf), str); + + if (len < 2) { + return -EINVAL; + } + + long temp_val; + int idx = 0; + + if ((trimmed_buf[0] == '-') || (trimmed_buf[0] == '+')) { + idx++; + } + + for (int i = idx; i < (len - 1); i++) { + if (!isdigit((int)trimmed_buf[i])) { + return -EINVAL; + } + } + + errno = 0; + temp_val = strtol(trimmed_buf, NULL, 10); + + if (errno == ERANGE) { + return -ERANGE; + } + + *val = temp_val; + return 0; +} + +int string_conv_str2ulong(char *str, unsigned long *val) +{ + char trimmed_buf[12] = { 0 }; + size_t len = whitespace_trim(trimmed_buf, sizeof(trimmed_buf), str); + + if (len < 2) { + return -EINVAL; + } + + unsigned long temp_val; + int idx = 0; + + if (trimmed_buf[0] == '+') { + idx++; + } + + for (int i = idx; i < (len - 1); i++) { + if (!isdigit((int)trimmed_buf[i])) { + return -EINVAL; + } + } + + errno = 0; + temp_val = strtoul(trimmed_buf, NULL, 10); + + if (errno == ERANGE) { + return -ERANGE; + } + + *val = temp_val; + return 0; +} + +int string_conv_str2dbl(const char *str, double *val) +{ + char trimmed_buf[22] = { 0 }; + long decimal; + unsigned long frac; + double frac_dbl; + int err = 0; + + size_t len = whitespace_trim(trimmed_buf, sizeof(trimmed_buf), str); + + if (len < 2) { + return -EINVAL; + } + + int comma_idx = strcspn(trimmed_buf, "."); + int frac_len = strlen(trimmed_buf + comma_idx + 1); + + /* Covers corner case "." input */ + if (strlen(trimmed_buf) < 2 && trimmed_buf[comma_idx] != 0) { + return -EINVAL; + } + + trimmed_buf[comma_idx] = 0; + + /* Avoid fractional overflow by losing one precision point */ + if (frac_len > 9) { + trimmed_buf[comma_idx + 10] = 0; + frac_len = 9; + } + + /* Avoid doing str2long if decimal part is empty */ + if (trimmed_buf[0] == '\0') { + decimal = 0; + } else { + err = string_conv_str2long(trimmed_buf, &decimal); + + if (err) { + return err; + } + } + + /* Avoid doing str2ulong if fractional part is empty */ + if ((trimmed_buf + comma_idx + 1)[0] == '\0') { + frac = 0; + } else { + err = string_conv_str2ulong(trimmed_buf + comma_idx + 1, &frac); + + if (err) { + return err; + } + } + + frac_dbl = (double)frac; + + for (int i = 0; i < frac_len; i++) { + frac_dbl /= 10; + } + + *val = (trimmed_buf[0] == '-') ? ((double)decimal - frac_dbl) : + ((double)decimal + frac_dbl); + + return err; +} diff --git a/tests/lib/string_conv/CMakeLists.txt b/tests/lib/string_conv/CMakeLists.txt new file mode 100644 index 0000000000000..a3d0233322c27 --- /dev/null +++ b/tests/lib/string_conv/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(string_conv) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/string_conv/prj.conf b/tests/lib/string_conv/prj.conf new file mode 100644 index 0000000000000..9467c2926896d --- /dev/null +++ b/tests/lib/string_conv/prj.conf @@ -0,0 +1 @@ +CONFIG_ZTEST=y diff --git a/tests/lib/string_conv/src/main.c b/tests/lib/string_conv/src/main.c new file mode 100644 index 0000000000000..886da29cfc32a --- /dev/null +++ b/tests/lib/string_conv/src/main.c @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +struct input { + char *str; + union { + long l_val; + unsigned long ul_val; + struct { + double val; + double eps; + } dbl_val; + } exp_val; + bool is_valid; +}; + +static void test_str2long(void) +{ + long test_val = 0; + + const struct input elems[] = { + /* Test illegal boundary values */ +#if __SIZEOF_LONG__ == 4 + { .is_valid = false, .str = "-2147483649" }, + { .is_valid = false, .str = "2147483648" }, +#endif + /* Test illegal huge input */ + { .is_valid = false, .str = "2147483647000000000000" }, + /* Test corrupt input */ + { .is_valid = false, .str = "Corrupt" }, + { .is_valid = false, .str = "1234ac" }, + { .is_valid = false, .str = "-1234ac" }, + /* Test legal boundary values */ + { .is_valid = true, .str = "-2147483647", .exp_val.l_val = -2147483647 }, + { .is_valid = true, .str = "2147483646", .exp_val.l_val = 2147483646 }, + /* Test input corner cases */ + { .is_valid = true, .str = "-", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "+", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "0", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "+0", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "-0", .exp_val.ul_val = 0 }, + /* Test heading zeros */ + { .is_valid = true, .str = "0000000001", .exp_val.l_val = 1 }, + { .is_valid = true, .str = "-0000000001", .exp_val.l_val = -1 }, + /* Test whitespace correction */ + { .is_valid = true, .str = " -2147483647", .exp_val.l_val = -2147483647 }, + { .is_valid = true, .str = "2147483646 ", .exp_val.l_val = 2147483646 }, + { .is_valid = true, .str = " 1", .exp_val.l_val = 1 }, + { .is_valid = true, .str = " -1 ", .exp_val.l_val = -1 }, + { .is_valid = false, .str = " " }, + }; + + for (int i = 0; i < ARRAY_SIZE(elems); i++) { + if (elems[i].is_valid) { + zassert_true(!string_conv_str2long(elems[i].str, &test_val), + "Failed to convert: \"%s\"", elems[i].str); + zassert_true(test_val == elems[i].exp_val.l_val, "%d != %d", test_val, + elems[i].exp_val.l_val); + } else { + zassert_false(!string_conv_str2long(elems[i].str, &test_val), + "Conversion of \"%s\" did not return expected err", + elems[i].str); + } + } +} + +static void test_str2ulong(void) +{ + unsigned long test_val = 0; + + const struct input elems[] = { + /* Test illegal boundary values */ + { .is_valid = false, .str = "-1" }, +#if __SIZEOF_LONG__ == 4 + { .is_valid = false, .str = "4294967296" }, +#endif + /* Test illegal huge input */ + { .is_valid = false, .str = "4294967295000000000000" }, + /* Test corrupt input */ + { .is_valid = false, .str = "Corrupt" }, + { .is_valid = false, .str = "1234ac" }, + { .is_valid = false, .str = "-1234ac" }, + { .is_valid = false, .str = "-" }, + /* Test legal boundary values */ + { .is_valid = true, .str = "0", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "4294967295", .exp_val.ul_val = 4294967295 }, + /* Test input corner cases */ + { .is_valid = true, .str = "0", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "+0", .exp_val.ul_val = 0 }, + { .is_valid = true, .str = "+", .exp_val.ul_val = 0 }, + /* Test heading zeros */ + { .is_valid = true, .str = "0000000001", .exp_val.ul_val = 1 }, + /* Test whitespace correction */ + { .is_valid = true, .str = " 2147483646 ", .exp_val.ul_val = 2147483646 }, + { .is_valid = true, .str = " 1", .exp_val.ul_val = 1 }, + { .is_valid = false, .str = " " }, + }; + + for (int i = 0; i < ARRAY_SIZE(elems); i++) { + if (elems[i].is_valid) { + zassert_true(!string_conv_str2ulong(elems[i].str, &test_val), + "Failed to convert: \"%s\"", elems[i].str); + zassert_true(test_val == elems[i].exp_val.ul_val, "%d != %d", test_val, + elems[i].exp_val.l_val); + } else { + zassert_false(!string_conv_str2ulong(elems[i].str, &test_val), + "Conversion of \"%s\" did not return expected err", + elems[i].str); + } + } +} + +bool compare_float(double x, double y, double epsilon) +{ + double temp = x - y; + + temp = temp < 0 ? -temp : temp; + return (temp < epsilon); +} + +static void test_str2dbl(void) +{ +#ifdef CONFIG_FPU + double test_val = 0; + + const struct input elems[] = { + /* Test illegal boundary values */ +#if __SIZEOF_LONG__ == 4 + { .is_valid = false, .str = "-2147483649" }, + { .is_valid = false, .str = "2147483648" }, +#endif + /* Test illegal huge input */ + { .is_valid = false, .str = "4294967295000000000000.1" }, + /* Test corrupt input */ + { .is_valid = false, .str = "Corrupt" }, + { .is_valid = false, .str = "1234ac" }, + { .is_valid = false, .str = "-1234ac" }, + { .is_valid = false, .str = "321.-123" }, + { .is_valid = false, .str = "." }, + /* Test legal boundary values */ + { .is_valid = true, .str = "2147483647", .exp_val.dbl_val.val = 2147483647, + .exp_val.dbl_val.eps = 0.1f }, + { .is_valid = true, .str = "-2147483648", .exp_val.dbl_val.val = -2147483648, + .exp_val.dbl_val.eps = 0.1f }, + /* Test precision boundary values */ + { .is_valid = true, .str = "0.999999999", .exp_val.dbl_val.val = 0.999999999, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = "-0.999999999", .exp_val.dbl_val.val = -0.999999999, + .exp_val.dbl_val.eps = 0.000000001f }, + /* Test precision overflow */ + { .is_valid = true, .str = "-0.9999999995", .exp_val.dbl_val.val = -0.999999999, + .exp_val.dbl_val.eps = 0.000000001f }, + /* Test input corner cases */ + { .is_valid = true, .str = "-.123", .exp_val.dbl_val.val = -0.123, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = ".123", .exp_val.dbl_val.val = 0.123, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = "00.000012", .exp_val.dbl_val.val = 0.000012, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = "58754.", .exp_val.dbl_val.val = 58754, + .exp_val.dbl_val.eps = 0.000000001f }, + /* Test whitespace correction */ + { .is_valid = true, .str = " 0.999999999 ", .exp_val.dbl_val.val = 0.999999999, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = " -0.999999999 ", .exp_val.dbl_val.val = -0.999999999, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = " -.123 ", .exp_val.dbl_val.val = -0.123, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = true, .str = " 58754.123 ", .exp_val.dbl_val.val = 58754.123, + .exp_val.dbl_val.eps = 0.000000001f }, + { .is_valid = false, .str = " " }, + }; + + for (int i = 0; i < ARRAY_SIZE(elems); i++) { + if (elems[i].is_valid) { + zassert_true(!string_conv_str2dbl(elems[i].str, &test_val), + "Failed to convert: \"%s\"", elems[i].str); + zassert_true(compare_float(test_val, elems[i].exp_val.dbl_val.val, + elems[i].exp_val.dbl_val.eps), + "Float comparison for \"%s\" gave unequal values"); + } else { + zassert_false(!string_conv_str2dbl(elems[i].str, &test_val), + "Conversion of \"%s\" did not return expected err", + elems[i].str); + } + } +#endif +} + +void test_main(void) +{ + ztest_test_suite(lib_string_conv_test, + ztest_unit_test(test_str2dbl), + ztest_unit_test(test_str2long), + ztest_unit_test(test_str2ulong)); + ztest_run_test_suite(lib_string_conv_test); +} diff --git a/tests/lib/string_conv/testcase.yaml b/tests/lib/string_conv/testcase.yaml new file mode 100644 index 0000000000000..8032f8ff54fbd --- /dev/null +++ b/tests/lib/string_conv/testcase.yaml @@ -0,0 +1,12 @@ +tests: + libraries.string_conv: + tags: string to numeric conversion + integration_platforms: + - native_posix + libraries.string_conv_double: + tags: string to double numeric conversion + integration_platforms: + - native_posix + filter: CONFIG_CPU_HAS_FPU + extra_configs: + - CONFIG_FPU=y