From: Bruce Richardson <bruce.richardson@intel.com>
To: dev@dpdk.org
Cc: stephen@networkplumber.org, andremue@linux.microsoft.com,
Bruce Richardson <bruce.richardson@intel.com>,
Tyler Retzlaff <roretzla@linux.microsoft.com>,
Dmitry Kozlyuk <dmitry.kozliuk@gmail.com>
Subject: [PATCH v4 1/4] eal: add basename function for common path manipulation
Date: Thu, 31 Jul 2025 16:00:38 +0000 [thread overview]
Message-ID: <20250731160041.914837-2-bruce.richardson@intel.com> (raw)
In-Reply-To: <20250731160041.914837-1-bruce.richardson@intel.com>
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.richardson@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
next prev parent reply other threads:[~2025-07-31 16:01 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-07-04 14:05 [PATCH] app/testpmd: allow multiple cmdline-file parameters Bruce Richardson
2025-07-04 18:34 ` [PATCH v2 0/3] improve cmdline file handling in testpmd Bruce Richardson
2025-07-04 18:34 ` [PATCH v2 1/3] app/testpmd: explicitly set command echoing on file load Bruce Richardson
2025-07-04 18:34 ` [PATCH v2 2/3] app/testpmd: allow multiple commandline file parameters Bruce Richardson
2025-07-04 18:34 ` [PATCH v2 3/3] app/testpmd: improve output when processing cmdline files Bruce Richardson
2025-07-07 11:17 ` [PATCH v3 0/3] improve cmdline file handling in testpmd Bruce Richardson
2025-07-07 11:17 ` [PATCH v3 1/3] app/testpmd: explicitly set command echoing on file load Bruce Richardson
2025-07-07 11:17 ` [PATCH v3 2/3] app/testpmd: allow multiple commandline file parameters Bruce Richardson
2025-07-07 11:17 ` [PATCH v3 3/3] app/testpmd: improve output when processing cmdline files Bruce Richardson
2025-07-29 4:24 ` Stephen Hemminger
2025-07-31 16:00 ` [PATCH v4 0/4] improve cmdline file handling in testpmd Bruce Richardson
2025-07-31 16:00 ` Bruce Richardson [this message]
2025-08-01 14:25 ` [PATCH v4 1/4] eal: add basename function for common path manipulation Andre Muezerie
2025-08-01 21:56 ` Stephen Hemminger
2025-07-31 16:00 ` [PATCH v4 2/4] app/testpmd: explicitly set command echoing on file load Bruce Richardson
2025-07-31 16:00 ` [PATCH v4 3/4] app/testpmd: allow multiple commandline file parameters Bruce Richardson
2025-07-31 16:00 ` [PATCH v4 4/4] app/testpmd: improve output when processing cmdline files Bruce Richardson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250731160041.914837-2-bruce.richardson@intel.com \
--to=bruce.richardson@intel.com \
--cc=andremue@linux.microsoft.com \
--cc=dev@dpdk.org \
--cc=dmitry.kozliuk@gmail.com \
--cc=roretzla@linux.microsoft.com \
--cc=stephen@networkplumber.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).