There is no standard, cross-platform function to get the basename of a
file path across all the supported DPDK platforms, Linux, BSD and
Windows. Both Linux and BSD have a "basename" function in standard
library, except:
* Linux has two different basename functions, a POSIX version (which may
  or may not modify args), and a GNU one which is guaranteed *not* to
  modify the input arg and returns pointer to internal storage.
* FreeBSD has just the one basename function, but, to be different, it is
  guaranteed *always* to modify the argument and re-use it for output.
* Windows just doesn't have a basename function, but provides _split_path
  as a similar function, but with many differences over basename, e.g.
  splitting off extension, returning empty basename if path ends in "/"
  etc. etc.

Therefore, rather than just trying to implement basename for windows,
which opens the question as to whether to emulate GNU and *never* modify
arg, or emulate BSD and *always* modify arg, this patchset introduces
"rte_basename" which should have defined behaviour on all platforms. The
patch also introduces a set of test cases to confirm consistent behaviour
on all platforms too.

The behaviour is as in doxygen docs. Essentially:
- does not modify input path buffer
- returns output in a separate output buffer
- uses snprintf and strlcpy style return value to indicate truncation

Signed-off-by: Bruce Richardson <bruce.richard...@intel.com>
---
 app/test/test_string_fns.c       | 111 +++++++++++++++++++++++++++++++
 lib/eal/include/rte_string_fns.h |  32 +++++++++
 lib/eal/unix/meson.build         |   1 +
 lib/eal/unix/rte_basename.c      |  37 +++++++++++
 lib/eal/windows/meson.build      |   1 +
 lib/eal/windows/rte_basename.c   |  53 +++++++++++++++
 6 files changed, 235 insertions(+)
 create mode 100644 lib/eal/unix/rte_basename.c
 create mode 100644 lib/eal/windows/rte_basename.c

diff --git a/app/test/test_string_fns.c b/app/test/test_string_fns.c
index 3b311325dc..1a2830575e 100644
--- a/app/test/test_string_fns.c
+++ b/app/test/test_string_fns.c
@@ -205,6 +205,115 @@ test_rte_str_skip_leading_spaces(void)
        return 0;
 }
 
+static int
+test_rte_basename(void)
+{
+       /* Test case structure for positive cases */
+       struct {
+               const char *input_path;    /* Input path string */
+               const char *expected;      /* Expected result */
+       } test_cases[] = {
+               /* Test cases from man 3 basename */
+               {"/usr/lib", "lib"},
+               {"/usr/", "usr"},
+               {"usr", "usr"},
+               {"/", "/"},
+               {".", "."},
+               {"..", ".."},
+
+               /* Additional requested test cases */
+               {"/////", "/"},
+               {"/path/to/file.txt", "file.txt"},
+
+               /* Additional edge cases with trailing slashes */
+               {"///usr///", "usr"},
+               {"/a/b/c/", "c"},
+
+               /* Empty string case */
+               {"", "."},
+               {NULL, "."}  /* NULL path should return "." */
+       };
+
+       char buf[256];
+       size_t result;
+
+       /* Run positive test cases from table */
+       for (size_t i = 0; i < RTE_DIM(test_cases); i++) {
+               result = rte_basename(test_cases[i].input_path, buf, 
sizeof(buf));
+
+               if (strcmp(buf, test_cases[i].expected) != 0) {
+                       LOG("FAIL [%zu]: '%s' - buf contains '%s', expected 
'%s'\n",
+                           i, test_cases[i].input_path, buf, 
test_cases[i].expected);
+                       return -1;
+               }
+
+               /* Check that the return value matches the expected string 
length */
+               if (result != strlen(test_cases[i].expected)) {
+                       LOG("FAIL [%zu]: '%s' - returned length %zu, expected 
%zu\n",
+                           i, test_cases[i].input_path, result, 
strlen(test_cases[i].expected));
+                       return -1;
+               }
+
+               LOG("PASS [%zu]: '%s' -> '%s' (len=%zu)\n",
+                               i, test_cases[i].input_path, buf, result);
+       }
+
+       /* re-run the table above verifying that for a NULL buffer, or zero 
length, we get
+        * correct length returned.
+        */
+       for (size_t i = 0; i < RTE_DIM(test_cases); i++) {
+               result = rte_basename(test_cases[i].input_path, NULL, 0);
+               if (result != strlen(test_cases[i].expected)) {
+                       LOG("FAIL [%zu]: '%s' - returned length %zu, expected 
%zu\n",
+                           i, test_cases[i].input_path, result, 
strlen(test_cases[i].expected));
+                       return -1;
+               }
+               LOG("PASS [%zu]: '%s' -> length %zu (NULL buffer case)\n",
+                   i, test_cases[i].input_path, result);
+       }
+
+       /* Test case: buffer too small for result should truncate and return 
full length */
+       const size_t small_size = 5;
+       result = rte_basename("/path/to/very_long_filename.txt", buf, 
small_size);
+       /* Should be truncated to fit in 5 bytes (4 chars + null terminator) */
+       if (strlen(buf) >= small_size) {
+               LOG("FAIL: small buffer test - result '%s' not properly 
truncated (len=%zu, buflen=%zu)\n",
+                   buf, strlen(buf), small_size);
+               return -1;
+       }
+       /* Return value should indicate truncation occurred (>= buflen) */
+       if (result != strlen("very_long_filename.txt")) {
+               LOG("FAIL: small buffer test - return value %zu doesn't 
indicate truncation (buflen=%zu)\n",
+                   result, small_size);
+               return -1;
+       }
+       LOG("PASS: small buffer truncation -> '%s' (returned len=%zu, actual 
len=%zu)\n",
+           buf, result, strlen(buf));
+
+       /* extreme length test case -  check that even with paths longer than 
PATH_MAX we still
+        * return the last component correctly. Use "/zzz...zzz/abc.txt" and 
check we get "abc.txt"
+        */
+       char basename_val[] = "abc.txt";
+       char long_path[PATH_MAX + 50];
+       for (int i = 0; i < PATH_MAX + 20; i++)
+               long_path[i] = (i == 0) ? '/' : 'z';
+       sprintf(long_path + PATH_MAX + 20, "/%s", basename_val);
+
+       result = rte_basename(long_path, buf, sizeof(buf));
+       if (strcmp(buf, basename_val) != 0) {
+               LOG("FAIL: long path test - expected '%s', got '%s'\n",
+                   basename_val, buf);
+               return -1;
+       }
+       if (result != strlen(basename_val)) {
+               LOG("FAIL: long path test - expected length %zu, got %zu\n",
+                   strlen(basename_val), result);
+               return -1;
+       }
+       LOG("PASS: long path test -> '%s' (len=%zu)\n", buf, result);
+       return 0;
+}
+
 static int
 test_string_fns(void)
 {
@@ -214,6 +323,8 @@ test_string_fns(void)
                return -1;
        if (test_rte_str_skip_leading_spaces() < 0)
                return -1;
+       if (test_rte_basename() < 0)
+               return -1;
        return 0;
 }
 
diff --git a/lib/eal/include/rte_string_fns.h b/lib/eal/include/rte_string_fns.h
index 702bd81251..3713c94acb 100644
--- a/lib/eal/include/rte_string_fns.h
+++ b/lib/eal/include/rte_string_fns.h
@@ -149,6 +149,38 @@ rte_str_skip_leading_spaces(const char *src)
        return p;
 }
 
+/**
+ * @warning
+ * @b EXPERIMENTAL: this API may change without prior notice.
+ *
+ * Provides the final component of a path, similar to POSIX basename function.
+ *
+ * This API provides the similar behaviour on all platforms, Linux, BSD, 
Windows,
+ * hiding the implementation differences.
+ * - It does not modify the input path.
+ * - The output buffer is passed as an argument, and the result is copied into 
it.
+ * - Expected output is the last component of the path, or the path itself if
+ *   it does not contain a directory separator.
+ * - If the final component is too long to fit in the output buffer, it will 
be truncated.
+ * - For empty or NULL input paths, output buffer will contain the string ".".
+ * - Supports up to PATH_MAX (BSD/Linux) or _MAX_PATH (Windows) characters in 
the input path.
+ *
+ * @param path
+ *   The input path string. Not modified by this function.
+ * @param buf
+ *   The buffer to hold the resultant basename.
+ *   Must be large enough to hold the result, otherwise basename will be 
truncated.
+ * @param buflen
+ *   The size of the buffer in bytes.
+ * @return
+ *   The number of bytes that were written to buf (excluding the terminating 
'\0').
+ *   If the return value is >= buflen, truncation occurred.
+ *   Return (size_t)-1 on error (Windows only)
+ */
+__rte_experimental
+size_t
+rte_basename(const char *path, char *buf, size_t buflen);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/eal/unix/meson.build b/lib/eal/unix/meson.build
index f1eb82e16a..70af352dab 100644
--- a/lib/eal/unix/meson.build
+++ b/lib/eal/unix/meson.build
@@ -9,6 +9,7 @@ sources += files(
         'eal_unix_memory.c',
         'eal_unix_thread.c',
         'eal_unix_timer.c',
+        'rte_basename.c',
         'rte_thread.c',
 )
 
diff --git a/lib/eal/unix/rte_basename.c b/lib/eal/unix/rte_basename.c
new file mode 100644
index 0000000000..a72d6bb3c9
--- /dev/null
+++ b/lib/eal/unix/rte_basename.c
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Intel Corporation
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <libgen.h>
+#include <limits.h>
+
+#include <eal_export.h>
+#include <rte_string_fns.h>
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_basename, 25.11)
+size_t
+rte_basename(const char *path, char *buf, size_t buflen)
+{
+       char copy[PATH_MAX + 1];
+       size_t retval = 0;
+
+       if (path == NULL)
+               return (buf == NULL) ? strlen(".") : strlcpy(buf, ".", buflen);
+
+       /* basename is on the end, so if path is too long, use only last 
PATH_MAX bytes */
+       const size_t pathlen = strlen(path);
+       if (pathlen > PATH_MAX)
+               path = &path[pathlen - PATH_MAX];
+
+       /* make a copy of buffer since basename may modify it */
+       strlcpy(copy, path, sizeof(copy));
+
+       /* if passed a null buffer, just return length of basename, otherwise 
strlcpy it */
+       retval = (buf == NULL) ?
+                       strlen(basename(copy)) :
+                       strlcpy(buf, basename(copy), buflen);
+
+       return retval;
+}
diff --git a/lib/eal/windows/meson.build b/lib/eal/windows/meson.build
index c526ede405..e7fad1f010 100644
--- a/lib/eal/windows/meson.build
+++ b/lib/eal/windows/meson.build
@@ -19,6 +19,7 @@ sources += files(
         'eal_timer.c',
         'getline.c',
         'getopt.c',
+        'rte_basename.c',
         'rte_thread.c',
 )
 
diff --git a/lib/eal/windows/rte_basename.c b/lib/eal/windows/rte_basename.c
new file mode 100644
index 0000000000..f4dfc08a0a
--- /dev/null
+++ b/lib/eal/windows/rte_basename.c
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Intel Corporation
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <rte_string_fns.h>
+
+size_t
+rte_basename(const char *path, char *buf, size_t buflen)
+{
+       char fname[_MAX_FNAME + 1];
+       char ext[_MAX_EXT + 1];
+       char dir[_MAX_DIR + 1];
+
+       if (path == NULL || path[0] == '\0')
+               return (buf == NULL) ? strlen(".") : strlcpy(buf, ".", buflen);
+
+       /* basename is on the end, so if path is too long, use only last 
PATH_MAX bytes */
+       const size_t pathlen = strlen(path);
+       if (pathlen > _MAX_PATH)
+               path = &path[pathlen - _MAX_PATH];
+
+
+       /* Use _splitpath_s to separate the path into components */
+       int ret = _splitpath_s(path, NULL, 0, dir, sizeof(dir),
+                       fname, sizeof(fname), ext, sizeof(ext));
+       if (ret != 0)
+               return (size_t)-1;
+
+       /* if there is a trailing slash, then split_path returns no basename, 
but
+        * we want to return the last component of the path in all cases.
+        * Therefore re-run removing trailing slash from path.
+        */
+       if (fname[0] == '\0' && ext[0] == '\0') {
+               size_t dirlen = strlen(dir);
+               while (dirlen > 0 && (dir[dirlen - 1] == '\\' || dir[dirlen - 
1] == '/')) {
+                       /* special case for "/" to keep *nix compatibility */
+                       if (strcmp(dir, "/") == 0)
+                               return (buf == NULL) ? strlen(dir) : 
strlcpy(buf, dir, buflen);
+
+                       /* Remove trailing backslash */
+                       dir[--dirlen] = '\0';
+               }
+               _splitpath_s(dir, NULL, 0, NULL, 0, fname, sizeof(fname), ext, 
sizeof(ext));
+       }
+
+       if (buf == NULL)
+               return strlen(fname) + strlen(ext);
+
+       /* Combine the filename and extension into output */
+       return snprintf(buf, buflen, "%s%s", fname, ext);
+}
-- 
2.48.1

Reply via email to