diff --git a/CMakeLists.txt b/CMakeLists.txt index 7997b210..08d3ab72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,8 +230,8 @@ if (BUILD_TESTS AND NOT SKIP_SRC) add_custom_target (tests DEPENDS array-test alivedetection-test boreas_error-test boreas_io-test cli-test cpeutils-test cvss-test ping-test sniffer-test util-test networking-test - passwordbasedauthentication-test xmlutils-test version-test osp-test - nvti-test hosts-test jsonpull-test) + passwordbasedauthentication-test xmlutils-test version-test osp-test + versionutils-test nvti-test hosts-test jsonpull-test) endif (BUILD_TESTS AND NOT SKIP_SRC) diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt index 6b656306..c43d9de3 100644 --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -113,12 +113,12 @@ include_directories (${GLIB_INCLUDE_DIRS} ${GPGME_INCLUDE_DIRS} ${GCRYPT_INCLUDE ${LIBXML2_INCLUDE_DIRS}) set (FILES cpeutils.c passwordbasedauthentication.c compressutils.c fileutils.c gpgmeutils.c kb.c ldaputils.c - nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c + nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c versionutils.c xmlutils.c jsonpull.c) set (HEADERS cpeutils.h passwordbasedauthentication.h authutils.h compressutils.h fileutils.h gpgmeutils.h kb.h ldaputils.h nvticache.h mqtt.h radiusutils.h serverutils.h sshutils.h - uuidutils.h xmlutils.h jsonpull.h) + uuidutils.h xmlutils.h jsonpull.h versionutils.h) if (BUILD_STATIC) add_library (gvm_util_static STATIC ${FILES}) @@ -198,6 +198,24 @@ if (BUILD_TESTS) add_custom_target (tests-cpeutils DEPENDS cpeutils-test) + add_executable (versionutils-test + EXCLUDE_FROM_ALL + versionutils_tests.c) + + add_test (versionutils-test versionutils-test) + + target_include_directories (versionutils-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (versionutils-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) + + add_custom_target (tests-versionutils + DEPENDS versionutils-test) + add_executable (xmlutils-test EXCLUDE_FROM_ALL xmlutils_tests.c) diff --git a/util/cpeutils.c b/util/cpeutils.c index 3cc988a1..a42a7261 100644 --- a/util/cpeutils.c +++ b/util/cpeutils.c @@ -11,6 +11,8 @@ * CPE or the CPE 2.3 formatted string binding of a CPE into a CPE struct * that corresponds to the WFN naming of a CPE. Further functions to convert * the CPE struct into the different bindings are provided. + * This file also contains a function that checks if one CPE (represented in a + * CPE struct) is a match for an other CPE (also represented in a CPE struct). */ #include "cpeutils.h" diff --git a/util/versionutils.c b/util/versionutils.c new file mode 100644 index 00000000..d0a2eb60 --- /dev/null +++ b/util/versionutils.c @@ -0,0 +1,363 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Functions to handle version numbers / version strings. + * + * Up to now this library provides a function to compare two version numbers / + * two version strings to decide which version is the newer one. + */ + +#include "versionutils.h" + +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +/** + * @brief GLib logging domain. + */ +#define G_LOG_DOMAIN "libgvm util" + +static gchar * +prepare_version_string (const char *); + +static int +get_release_state (const char *, int); + +static char * +get_part (const char *, int); + +static gboolean +is_text (const char *); + +static char * +str_cpy (char *, int); + +/** + * @brief Compare two version strings representing a software version + * to decide which version is newer. + * + * @param[in] version1 The first version string to compare. + * @param[in] version2 The second version string to compare. + * + * @return Returns a value > 0 if version1 is newer than version2. + * Returns 0 if version1 is the same than version2. + * Returns a value between -1 and -4 if version2 is newer + * than version1. + * Returns -5 if the result is undefined. + */ +int +cmp_versions (const char *version1, const char *version2) +{ + char *ver1, *ver2; + char *part1, *part2; + int index1 = 0, index2 = 0; + int release_state1 = 0, release_state2 = 0; + int rs1, rs2; + + ver1 = prepare_version_string (version1); + ver2 = prepare_version_string (version2); + + if (ver1 == NULL || ver2 == NULL) + { + g_free (ver1); + g_free (ver2); + return (-5); + } + if (strcmp (ver1, ver2) == 0) + { + g_free (ver1); + g_free (ver2); + return (0); + } + + if ((release_state1 = get_release_state (ver1, index1))) + index1++; + if ((release_state2 = get_release_state (ver2, index2))) + index2++; + + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + while (part1 && part2) + { + if (strcmp (part1, part2) == 0) + { + index1++; + index2++; + g_free (part1); + g_free (part2); + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + continue; + } + else + break; + } + + if (part1 == NULL && part2 == NULL) + return (release_state2 - release_state1); + + if (is_text (part1) || is_text (part2)) + { + if (part1) + g_free (part1); + if (part2) + g_free (part2); + return (-5); // undefined + } + + rs1 = get_release_state (ver1, index1); + rs2 = get_release_state (ver2, index2); + + if ((rs1 && release_state1) || (rs2 && release_state2)) + return (-5); // undefined + + if (part1 == NULL) + { + g_free (part2); + if (rs2) + return (rs2 - release_state1); + else + return (-1); + } + + if (part2 == NULL) + { + g_free (part1); + if (rs1) + return (release_state2 - rs1); + else + return (1); + } + + int ret = -5; + + if (rs1 && rs2) + ret = rs2 - rs1; + + if (rs1) + ret = -1; + + if (rs2) + ret = 1; + + if (!rs1 && !rs2 && atoi (part1) < atoi (part2)) + ret = -1; + + if (!rs1 && !rs2 && atoi (part1) == atoi (part2)) + ret = 0; + + if (!rs1 && !rs2 && atoi (part1) > atoi (part2)) + ret = 1; + + g_free (part1); + g_free (part2); + g_free (ver1); + g_free (ver2); + return (ret); +} + +/** + * @brief Prepare the version string for comparison. + * + * @param[in] version The version string to generate the prepared + * version string from. + * + * @return Returns a prepared copy of the version string version. + */ +static gchar * +prepare_version_string (const char *version) +{ + char prep_version[2048]; + char *ver; + int index_v, index_pv; + gboolean is_digit; + + if (!version) + return (NULL); + + if (strlen (version) > 1024) + return (NULL); + + ver = g_strdup (version); + + /* set all characters to lowercase */ + char *c = ver; + for (; *c; c++) + *c = tolower (*c); + + index_v = index_pv = 0; + + is_digit = g_ascii_isdigit (ver[0]); + + while (index_v < (int) strlen (ver) && index_pv < 2047) + { + if (ver[index_v] == '\\') + { + index_v++; + continue; + } + + if (ver[index_v] == '_' || ver[index_v] == '-' || ver[index_v] == '+' + || ver[index_v] == ':' || ver[index_v] == '.') + { + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + index_v++; + continue; + } + + if (is_digit != g_ascii_isdigit (ver[index_v])) + { + is_digit = !is_digit; + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + } + + if (ver[index_v] == 'r') + { + if (strstr (ver + index_v, "releasecandidate") == ver + index_v) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 16; + continue; + } + if ((strstr (ver + index_v, "release-candidate") == ver + index_v) + || (strstr (ver + index_v, "release_candidate") == ver + index_v)) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 17; + continue; + } + } + + prep_version[index_pv] = ver[index_v]; + index_v++; + index_pv++; + } + + prep_version[index_pv] = '\0'; + g_free (ver); + return (g_strdup (prep_version)); +} + +/** + * @brief Gets the release state of a specified part of the version string + * if any. + * + * @param[in] version The version string to get the release state from. + * @param[in] index The part of the version string to check. + * + * @return Returns 0 if there is no release state, returns 4 if the release + * state is "development" (dev), returns 3 if the state is "alpha", + * 2 if the state is beta and 1 if the state is release candidate (rc). + */ +static int +get_release_state (const char *version, int index) +{ + char *part; + int rel_stat = 0; + + part = get_part (version, index); + + if (part == NULL) + return (0); + + if (strcmp (part, "dev") == 0 || strcmp (part, "development") == 0) + rel_stat = 4; + if (strcmp (part, "alpha") == 0) + rel_stat = 3; + if (strcmp (part, "beta") == 0) + rel_stat = 2; + if (strcmp (part, "rc") == 0) + rel_stat = 1; + + g_free (part); + return (rel_stat); +} + +/** + * @brief Gets the part of the version string that is specified by index. + * + * @param[in] version The version string to get the part from. + * @param[in] index The part of the version string to return. + * + * @return Returns a copy of the specified part of the version string. + */ +static char * +get_part (const char *version, int index) +{ + int dot_count = 0; + int begin, end; + + for (begin = 0; begin < (int) strlen (version) && dot_count < index; begin++) + { + if (version[begin] == '.') + dot_count++; + } + + if (begin == (int) strlen (version)) + return (NULL); + + for (end = begin + 1; end < (int) strlen (version) && version[end] != '.'; + end++) + ; + + return (str_cpy ((char *) (version + begin), end - begin)); +} + +/** + * @brief Checks if a given part of the version string is plain text. + * + * @param[in] part The part of the version string to check. + * + * @return Returns TRUE if part contains only plain text, FALSE otherwise. + */ +static gboolean +is_text (const char *part) +{ + if (!part) + return (FALSE); + if (strcmp (part, "dev") == 0 || strcmp (part, "alpha") == 0 + || strcmp (part, "beta") == 0 || strcmp (part, "rc") == 0) + return (FALSE); + if (g_ascii_isdigit (*part)) + return (FALSE); + return (TRUE); +} + +/** + * @brief Copy size characters of a string to an newly allocated new string. + * + * @param[in] src The string the first size characters are to be copied + * from. + * @param[in] size The number of characters to copy. + * + * @return The copy of the first size characters of src as a new string. + */ +static char * +str_cpy (char *source, int size) +{ + char *result; + result = (char *) g_malloc (size + 1); + memset (result, 0, size + 1); + strncpy (result, source, size); + return (result); +} diff --git a/util/versionutils.h b/util/versionutils.h new file mode 100644 index 00000000..d093189c --- /dev/null +++ b/util/versionutils.h @@ -0,0 +1,20 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Headers for version utils. + */ + +#ifndef _GVM_VERSIONUTILS_H +#define _GVM_VERSIONUTILS_H + +#include +#include + +int +cmp_versions (const char *, const char *); + +#endif diff --git a/util/versionutils_tests.c b/util/versionutils_tests.c new file mode 100644 index 00000000..c7e0502b --- /dev/null +++ b/util/versionutils_tests.c @@ -0,0 +1,88 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "versionutils.c" + +#include +#include + +Describe (versionutils); +BeforeEach (versionutils) +{ +} + +AfterEach (versionutils) +{ +} + +/* parse_entity */ + +Ensure (versionutils, cmp_versions) +{ + char *version1, *version2; + int result; + + version1 = "test"; + version2 = "test-1"; + result = cmp_versions (version1, version2); + assert_that (result, is_less_than (0)); + assert_that (result, is_greater_than (-5)); + + version1 = "beta-test-2"; + version2 = "test_1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "beta-test-2"; + version2 = "test-2.beta"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (0)); + + version1 = "test-2.beta"; + version2 = "test-2.a"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "test-2.beta"; + version2 = "test-2.1"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate"; + version2 = "test-2"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate2"; + version2 = "test-2.release_candidate1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "test-2.release_candidatea"; + version2 = "test-2.release_candidateb"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "2024-06-24"; + version2 = "2024-06-23"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); +} + +/* Test suite. */ +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, versionutils, cmp_versions); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +}