Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions include/string_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
60 changes: 60 additions & 0 deletions include/version_sort.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 <dirent.h>
#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 // !IS_FREEBSD_VERSION(9,0,0)
#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
3 changes: 2 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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__')
Expand Down Expand Up @@ -362,7 +363,7 @@ if c.has_function('getline', prefix: '#include <stdio.h>')
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;
Expand Down
85 changes: 84 additions & 1 deletion src/string_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ctype.h>
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
36 changes: 36 additions & 0 deletions src/version_sort.c
Original file line number Diff line number Diff line change
@@ -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