diff --git a/include/crm/common/scores.h b/include/crm/common/scores.h index 4b73f6610d..1e031fa00e 100644 --- a/include/crm/common/scores.h +++ b/include/crm/common/scores.h @@ -1,33 +1,34 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_SCORES__H #define PCMK__CRM_COMMON_SCORES__H #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Pacemaker APIs related to scores * \ingroup core */ //! Integer score to use to represent "infinity" #define PCMK_SCORE_INFINITY 1000000 +int pcmk_parse_score(const char *score_s, int *score, int default_score); const char *pcmk_readable_score(int score); int char2score(const char *score); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_SCORES__H diff --git a/lib/common/scores.c b/lib/common/scores.c index 39e224e930..a31e126e37 100644 --- a/lib/common/scores.c +++ b/lib/common/scores.c @@ -1,163 +1,214 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include // snprintf(), NULL #include // strcpy(), strdup() +#include // isspace() #include // size_t int pcmk__score_red = 0; int pcmk__score_green = 0; int pcmk__score_yellow = 0; /*! - * \brief Get the integer value of a score string + * \brief Parse an integer score from a string * - * Given a string representation of a score, return the integer equivalent. - * This accepts infinity strings as well as red, yellow, and green, and - * bounds the result to +/-INFINITY. + * Parse an integer score from a string. This accepts infinity strings as well + * as red, yellow, and green, and bounds the result to +/-INFINITY. * - * \param[in] score Score as string + * \param[in] score_s Score as string + * \param[out] score Where to store integer value corresponding to + * \p score_s (may be NULL to only check validity) + * \param[in] default_score Value to use if \p score_s is NULL or invalid * - * \return Integer value corresponding to \p score + * \return Standard Pacemaker return code */ int -char2score(const char *score) +pcmk_parse_score(const char *score_s, int *score, int default_score) { - if (score == NULL) { - return 0; + int rc = pcmk_rc_ok; + int local_score = 0; - } else if (pcmk_str_is_minus_infinity(score)) { - return -PCMK_SCORE_INFINITY; + // Ensure default score is in bounds + default_score = QB_MIN(default_score, PCMK_SCORE_INFINITY); + default_score = QB_MAX(default_score, -PCMK_SCORE_INFINITY); + local_score = default_score; - } else if (pcmk_str_is_infinity(score)) { - return PCMK_SCORE_INFINITY; + if (score_s == NULL) { + + } else if (pcmk_str_is_minus_infinity(score_s)) { + local_score = -PCMK_SCORE_INFINITY; - } else if (pcmk__str_eq(score, PCMK_VALUE_RED, pcmk__str_casei)) { - return pcmk__score_red; + } else if (pcmk_str_is_infinity(score_s)) { + local_score = PCMK_SCORE_INFINITY; - } else if (pcmk__str_eq(score, PCMK_VALUE_YELLOW, pcmk__str_casei)) { - return pcmk__score_yellow; + } else if (pcmk__str_eq(score_s, PCMK_VALUE_RED, pcmk__str_casei)) { + local_score = pcmk__score_red; - } else if (pcmk__str_eq(score, PCMK_VALUE_GREEN, pcmk__str_casei)) { - return pcmk__score_green; + } else if (pcmk__str_eq(score_s, PCMK_VALUE_YELLOW, pcmk__str_casei)) { + local_score = pcmk__score_yellow; + + } else if (pcmk__str_eq(score_s, PCMK_VALUE_GREEN, pcmk__str_casei)) { + local_score = pcmk__score_green; } else { - long long score_ll; + long long score_ll = 0LL; + + rc = pcmk__scan_ll(score_s, &score_ll, default_score); + if ((rc == EOVERFLOW) || (rc == ERANGE)) { + const char *c = score_s; + + while (isspace(*c)) { + ++c; + } + rc = pcmk_rc_ok; + if (*c == '-') { + score_ll = -PCMK_SCORE_INFINITY; + } else { + score_ll = PCMK_SCORE_INFINITY; + } + } + if (rc != pcmk_rc_ok) { + local_score = default_score; - pcmk__scan_ll(score, &score_ll, 0LL); - if (score_ll > PCMK_SCORE_INFINITY) { - return PCMK_SCORE_INFINITY; + } else if (score_ll > PCMK_SCORE_INFINITY) { + local_score = PCMK_SCORE_INFINITY; } else if (score_ll < -PCMK_SCORE_INFINITY) { - return -PCMK_SCORE_INFINITY; + local_score = -PCMK_SCORE_INFINITY; } else { - return (int) score_ll; + local_score = (int) score_ll; } } + + if (score != NULL) { + *score = local_score; + } + return rc; +} + +/*! + * \brief Get the integer value of a score string + * + * Given a string representation of a score, return the integer equivalent. + * This accepts infinity strings as well as red, yellow, and green, and + * bounds the result to +/-INFINITY. + * + * \param[in] score Score as string + * + * \return Integer value corresponding to \p score + */ +int +char2score(const char *score) +{ + int result = 0; + + (void) pcmk_parse_score(score, &result, 0); + return result; } /*! * \brief Return a displayable static string for a score value * * Given a score value, return a pointer to a static string representation of * the score suitable for log messages, output, etc. * * \param[in] score Score to display * * \return Pointer to static memory containing string representation of \p score * \note Subsequent calls to this function will overwrite the returned value, so * it should be used only in a local context such as a printf()-style * statement. */ const char * pcmk_readable_score(int score) { // The longest possible result is "-INFINITY" static char score_s[sizeof(PCMK_VALUE_MINUS_INFINITY)]; if (score >= PCMK_SCORE_INFINITY) { strcpy(score_s, PCMK_VALUE_INFINITY); } else if (score <= -PCMK_SCORE_INFINITY) { strcpy(score_s, PCMK_VALUE_MINUS_INFINITY); } else { // Range is limited to +/-1000000, so no chance of overflow snprintf(score_s, sizeof(score_s), "%d", score); } return score_s; } /*! * \internal * \brief Add two scores, bounding to +/-INFINITY * * \param[in] score1 First score to add * \param[in] score2 Second score to add * * \note This function does not have context about what the scores mean, so it * does not log any messages. */ int pcmk__add_scores(int score1, int score2) { /* As long as PCMK_SCORE_INFINITY is less than half of the maximum integer, * we can ignore the possibility of integer overflow. */ int result = score1 + score2; // First handle the cases where one or both is infinite if ((score1 <= -PCMK_SCORE_INFINITY) || (score2 <= -PCMK_SCORE_INFINITY)) { return -PCMK_SCORE_INFINITY; } if ((score1 >= PCMK_SCORE_INFINITY) || (score2 >= PCMK_SCORE_INFINITY)) { return PCMK_SCORE_INFINITY; } // Bound result to infinity. if (result >= PCMK_SCORE_INFINITY) { return PCMK_SCORE_INFINITY; } if (result <= -PCMK_SCORE_INFINITY) { return -PCMK_SCORE_INFINITY; } return result; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include char * score2char(int score) { return pcmk__str_copy(pcmk_readable_score(score)); } char * score2char_stack(int score, char *buf, size_t len) { CRM_CHECK((buf != NULL) && (len >= sizeof(PCMK_VALUE_MINUS_INFINITY)), return NULL); strcpy(buf, pcmk_readable_score(score)); return buf; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/tests/scores/Makefile.am b/lib/common/tests/scores/Makefile.am index cb961553f6..8a3cbb1d6b 100644 --- a/lib/common/tests/scores/Makefile.am +++ b/lib/common/tests/scores/Makefile.am @@ -1,18 +1,19 @@ # # Copyright 2020-2023 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. check_PROGRAMS = char2score_test \ pcmk__add_scores_test \ + pcmk_parse_score_test \ pcmk_readable_score_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/scores/pcmk_parse_score_test.c b/lib/common/tests/scores/pcmk_parse_score_test.c new file mode 100644 index 0000000000..01bffdfc97 --- /dev/null +++ b/lib/common/tests/scores/pcmk_parse_score_test.c @@ -0,0 +1,120 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include + +extern int pcmk__score_red; +extern int pcmk__score_green; +extern int pcmk__score_yellow; + +static int default_score = 99; + +static void +assert_score(const char *score_s, int expected_rc, int expected_score) +{ + int score = 0; + int rc = pcmk_parse_score(score_s, &score, default_score); + + assert_int_equal(rc, expected_rc); + assert_int_equal(score, expected_score); +} + +static void +null_score_string(void **state) +{ + assert_score(NULL, pcmk_rc_ok, default_score); + + // Test out-of-bounds default score + + default_score = -2000000; + assert_score(NULL, pcmk_rc_ok, -PCMK_SCORE_INFINITY); + + default_score = 2000000; + assert_score(NULL, pcmk_rc_ok, PCMK_SCORE_INFINITY); + + default_score = 99; +} + +static void +null_score(void **state) +{ + assert_int_equal(pcmk_parse_score(NULL, NULL, default_score), pcmk_rc_ok); + assert_int_equal(pcmk_parse_score("0", NULL, default_score), pcmk_rc_ok); + assert_int_equal(pcmk_parse_score("foo", NULL, default_score), + EINVAL); +} + +static void +bad_input(void **state) +{ + assert_score("redder", EINVAL, default_score); + assert_score("3.141592", pcmk_rc_ok, 3); + assert_score("0xf00d", pcmk_rc_ok, 0); +} + +static void +special_values(void **state) +{ + assert_score("-INFINITY", pcmk_rc_ok, -PCMK_SCORE_INFINITY); + assert_score("INFINITY", pcmk_rc_ok, PCMK_SCORE_INFINITY); + assert_score("+INFINITY", pcmk_rc_ok, PCMK_SCORE_INFINITY); + + pcmk__score_red = 10; + pcmk__score_green = 20; + pcmk__score_yellow = 30; + + assert_score("red", pcmk_rc_ok, pcmk__score_red); + assert_score("green", pcmk_rc_ok, pcmk__score_green); + assert_score("yellow", pcmk_rc_ok, pcmk__score_yellow); + + assert_score("ReD", pcmk_rc_ok, pcmk__score_red); + assert_score("GrEeN", pcmk_rc_ok, pcmk__score_green); + assert_score("yElLoW", pcmk_rc_ok, pcmk__score_yellow); +} + +/* These ridiculous macros turn an integer constant into a string constant. */ +#define A(x) #x +#define B(x) A(x) + +static void +outside_limits(void **state) +{ + char *very_long = crm_strdup_printf(" %lld0", LLONG_MAX); + + // Still within int range + assert_score(B(PCMK_SCORE_INFINITY) "00", pcmk_rc_ok, PCMK_SCORE_INFINITY); + assert_score("-" B(PCMK_SCORE_INFINITY) "00", pcmk_rc_ok, + -PCMK_SCORE_INFINITY); + + // Outside long long range + assert_score(very_long, pcmk_rc_ok, PCMK_SCORE_INFINITY); + very_long[0] = '-'; + assert_score(very_long, pcmk_rc_ok, -PCMK_SCORE_INFINITY); + free(very_long); +} + +static void +inside_limits(void **state) +{ + assert_score("1234", pcmk_rc_ok, 1234); + assert_score("-1234", pcmk_rc_ok, -1234); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(null_score_string), + cmocka_unit_test(null_score), + cmocka_unit_test(bad_input), + cmocka_unit_test(special_values), + cmocka_unit_test(outside_limits), + cmocka_unit_test(inside_limits))