From 83a566b98e387f96ff4245acd22e77a8b5fef471 Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Fri, 14 Nov 2025 10:38:12 -0700 Subject: [PATCH 1/2] feat: Adding string_version_compare and version_compare functions Adding functions similar to GNU's strverscmp and versioncompare so that we can use them in other OS's that do not support these in their own libc. This version is not as fast as the GNU version, but it's pretty close. Signed-off-by: Tyler Erickson --- include/string_utils.h | 17 +++++++++ include/version_sort.h | 56 ++++++++++++++++++++++++++++ meson.build | 3 +- src/string_utils.c | 85 +++++++++++++++++++++++++++++++++++++++++- src/version_sort.c | 36 ++++++++++++++++++ 5 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 include/version_sort.h create mode 100644 src/version_sort.c diff --git a/include/string_utils.h b/include/string_utils.h index 7dfd8e7..213bb84 100644 --- a/include/string_utils.h +++ b/include/string_utils.h @@ -1032,6 +1032,23 @@ extern "C" M_PARAM_RO(2) M_NULL_TERM_STRING(1) M_NULL_TERM_STRING(2) bool wildcard_case_match(const char* pattern, const char* data); + //! \fn int string_version_compare(const char* string1, const char* string2) + //! \brief Works like GNU's strvercmp function to compare two strings. + //! \details Compares two strings taking into account numerical substrings as numbers + //! rather than as text. For example, "file9.txt" is less than "file10.txt". + //! Both strings must be null-terminated ASCII strings. + //! This resolves issues where jan1, jan10, jan2 would come out from alphacompare when + //! the caller wants jan1, jan2, jan10 instead. + //! \param[in] string1 pointer to the first null-terminated string to compare + //! \param[in] string2 pointer to the second null-terminated string to compare + //! \return negative value if \a string1 < \a string2, zero if they are equal, + //! positive value if \a string1 > \a string2 + //! \note Performance not quite as good as GNU version. + M_NONNULL_PARAM_LIST(1, 2) + M_PARAM_RO(1) + M_PARAM_RO(2) + M_NULL_TERM_STRING(1) M_NULL_TERM_STRING(2) int string_version_compare(const char* string1, const char* string2); + #if defined(__cplusplus) } #endif diff --git a/include/version_sort.h b/include/version_sort.h new file mode 100644 index 0000000..bc56ddf --- /dev/null +++ b/include/version_sort.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! \file version_sort.h +//! \brief Provides GNU style version sort function. Slightly different name to +//! avoid conflict with other version sort implementations. +//! +//! \copyright +//! Do NOT modify or remove this copyright and license +//! +//! Copyright (c) 2025-2025 Seagate Technology LLC and/or its Affiliates, All Rights Reserved +//! +//! This software is subject to the terms of the Mozilla Public License, v. 2.0. +//! If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include "code_attributes.h" +#include "common_types.h" +#include "predef_env_detect.h" + +#if !defined(_WIN32) +# include +#else +# if !defined(NEED_OLD_SCANDIR_CMP_FUNC_TYPE) +# define NEED_OLD_SCANDIR_CMP_FUNC_TYPE +# endif // NEED_OLD_SCANDIR_CMP_FUNC_TYPE +#endif //_WIN32 + +#if defined(__cplusplus) +extern "C" +{ +#endif //__cplusplus + +#if defined(NEED_OLD_SCANDIR_CMP_FUNC_TYPE) + typedef int (*scandircmp)(const void*, const void*); +#else +typedef int (*scandircmp)(const struct dirent**, const struct dirent**); +#endif + +//! \fn int version_sort(const void *ptr1, const void *ptr2) +//! \brief Works like GNU's versionsort comparison function. +//! Uses opensea-common's string_version_compare to compare two version strings. +//! \param[in] ptr1 Pointer to the first directory entry to compare +//! \param[in] ptr2 Pointer to the second directory entry to compare +//! \return An integer less than, equal to, or greater than zero if \a ptr1 is found, respectively, to be less than, +//! to match, or to be greater than \a ptr2. +//! \note On Windows, this function just returns 0 always since dirent is not supported. +#if defined(NEED_OLD_SCANDIR_CMP_FUNC_TYPE) + int version_sort(const void* ptr1, const void* ptr2); +#else +int version_sort(const struct dirent** ptr1, const struct dirent** ptr2); +#endif + +#if defined(__cplusplus) +} +#endif //__cplusplus diff --git a/meson.build b/meson.build index aa29c30..74184b3 100755 --- a/meson.build +++ b/meson.build @@ -298,6 +298,7 @@ src_files = [ 'src/type_conversion.c', 'src/unit_conversion.c', 'src/validate_format.c', + 'src/version_sort.c', ] if c.has_header_symbol('stdlib.h', '__STDC_LIB_EXT1__') @@ -362,7 +363,7 @@ if c.has_function('getline', prefix: '#include ') endif # Test support for _Generic keyword. May not be available in some cases. -gernericSelectionTest = ''' +gernericSelectionTest = ''' long add_long(long x, long y) { return x + y; diff --git a/src/string_utils.c b/src/string_utils.c index 313e90e..d03abf0 100644 --- a/src/string_utils.c +++ b/src/string_utils.c @@ -14,12 +14,14 @@ #include "common_types.h" #include "constraint_handling.h" #include "env_detect.h" +#include "io_utils.h" #include "math_utils.h" #include "memory_safety.h" #include "type_conversion.h" #include #include +#include #include #include @@ -504,7 +506,7 @@ errno_t safe_strncpy_impl(char* M_RESTRICT dest, // many cases as standard which is why it's down here.-TJE error = strncpy_s(dest, destsz, src, count); #else - error = safe_memccpy(dest, destsz, src, '\0', count); + error = safe_memccpy(dest, destsz, src, '\0', count); if (srclen < count) { dest[srclen] = '\0'; // ensuring NULL termination @@ -1455,3 +1457,84 @@ bool wildcard_match(const char* pattern, const char* data) { return wildcard_match_internal(pattern, data, false); } + +// Note: Tried M_FORCEINLINE but no performance difference observed +static M_INLINE long string_version_compare_parse_number(const char** p, int* leading_zeros, size_t* digit_count) +{ + long val = 0L; + *leading_zeros = 0; + *digit_count = SIZE_T_C(0); + + // Count leading zeros + while (**p == '0') + { + ++(*leading_zeros); + ++(*digit_count); + ++(*p); + } + + // Parse digits + while (isdigit(M_STATIC_CAST(unsigned char, **p))) + { + val = val * 10L + (**p - '0'); + ++(*digit_count); + ++(*p); + } + return val; +} + +int string_version_compare(const char* string1, const char* string2) +{ + while (*string1 && *string2) + { + if (isdigit(M_STATIC_CAST(unsigned char, *string1)) && isdigit(M_STATIC_CAST(unsigned char, *string2))) + { + const char* p1 = string1; + const char* p2 = string2; + size_t len1; + size_t len2; + long num1; + long num2; + int zeros1; + int zeros2; + + num1 = string_version_compare_parse_number(&p1, &zeros1, &len1); + num2 = string_version_compare_parse_number(&p2, &zeros2, &len2); + + // Compare numeric values + if (num1 != num2) + { + return (num1 > num2) - (num1 < num2); // branchless numeric compare + } + + // Tie-break: leading zeros + if (zeros1 != zeros2) + { + return (zeros2 > zeros1) - (zeros2 < zeros1); // more zeros first + } + + // Tie-break: digit length + if (len1 != len2) + { + return (len2 > len1) - (len2 < len1); // longer segment wins + } + + // Advance past numeric segment + string1 = p1; + string2 = p2; + } + else + { + if (*string1 != *string2) + { + return M_STATIC_CAST(int, + M_STATIC_CAST(unsigned char, *string1) - M_STATIC_CAST(unsigned char, *string2)); + } + ++string1; + ++string2; + } + } + + // Final fallback: which string ended first + return (*string1 != '\0') - (*string2 != '\0'); // branchless end check +} diff --git a/src/version_sort.c b/src/version_sort.c new file mode 100644 index 0000000..d0720c8 --- /dev/null +++ b/src/version_sort.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! \file version_sort.c +//! \brief Provides GNU style version sort function. Slightly different name to +//! avoid conflict with other version sort implementations. +//! +//! \copyright +//! Do NOT modify or remove this copyright and license +//! +//! Copyright (c) 2025-2025 Seagate Technology LLC and/or its Affiliates, All Rights Reserved +//! +//! This software is subject to the terms of the Mozilla Public License, v. 2.0. +//! If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include "version_sort.h" +#include "string_utils.h" + +#if defined(NEED_OLD_SCANDIR_CMP_FUNC_TYPE) +int version_sort(const void* ptr1, const void* ptr2) +{ +# if defined(_WIN32) + M_USE_UNUSED(ptr1); + M_USE_UNUSED(ptr2); + return 0; +# else + const struct dirent* const* a = M_REINTERPRET_CAST(const struct dirent* const*, ptr1); + const struct dirent* const* b = M_REINTERPRET_CAST(const struct dirent* const*, ptr2); + return string_version_compare((*a)->d_name, (*b)->d_name); +# endif // _WIN32 +} +#else +int version_sort(const struct dirent** ptr1, const struct dirent** ptr2) +{ + return string_version_compare((*ptr1)->d_name, (*ptr2)->d_name); +} +#endif // NEED_OLD_SCANDIR_CMP_FUNC_TYPE From e71898c2fcba3ca440ea69706a36e862bdbec0fa Mon Sep 17 00:00:00 2001 From: Tyler Erickson Date: Fri, 14 Nov 2025 13:25:19 -0700 Subject: [PATCH 2/2] feat: Adding some auto-detection for when the old scandir signature is required to pass version_sort Adding some additional checks to auto-detect when to define the old signature versus the new Signed-off-by: Tyler Erickson --- include/version_sort.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/include/version_sort.h b/include/version_sort.h index bc56ddf..5f9df70 100644 --- a/include/version_sort.h +++ b/include/version_sort.h @@ -20,12 +20,16 @@ #if !defined(_WIN32) # include -#else +#endif //_WIN32 + +// This is a list of systems where we need the old prototype for the scandir comparison +// function. Newer systems use the struct dirent** version +// This is based on reading man pages online and looking at the definitions and may not be complete +#if defined(_WIN32) || (!IS_FREEBSD_VERSION(9, 0, 0) && !IS_NETBSD_VERSION(8, 0, 0) && !IS_OPENBSD_VERSION(5, 2, 0)) # if !defined(NEED_OLD_SCANDIR_CMP_FUNC_TYPE) # define NEED_OLD_SCANDIR_CMP_FUNC_TYPE # endif // NEED_OLD_SCANDIR_CMP_FUNC_TYPE -#endif //_WIN32 - +#endif // !IS_FREEBSD_VERSION(9,0,0) #if defined(__cplusplus) extern "C" {