From 8dcb891c199aeb22fe0fe3e8d1f830c0de8a0c24 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 26 Jan 2021 12:28:23 +0100 Subject: [PATCH] path-util: add path_extract_directory(), to match path_extract_filename() These two together are a lot like dirname() + basename() but have the benefit that they return clear errors when one passes a special case path to them where the extraction doesn't make sense, i.e. "", "/", "foo", "foo/" and so on. Sooner or later we should probably port all our uses of dirname()/basename() over to this, to catch these special cases more safely. --- src/basic/path-util.c | 42 ++++++++++++++++++++++++++++ src/basic/path-util.h | 1 + src/test/test-path-util.c | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/basic/path-util.c b/src/basic/path-util.c index f3398418c4..fe8321edce 100644 --- a/src/basic/path-util.c +++ b/src/basic/path-util.c @@ -846,6 +846,48 @@ int path_extract_filename(const char *p, char **ret) { return 0; } +int path_extract_directory(const char *p, char **ret) { + _cleanup_free_ char *a = NULL; + const char *c; + + /* The inverse of path_extract_filename(), i.e. returns the directory path prefix. Returns: + * + * -EINVAL → if the passed in path is not a valid path + * -EDESTADDRREQ → if no directory was specified in the passed in path, i.e. only a filename was passed + * -EADDRNOTAVAIL → if the passed in parameter had no filename but did have a directory, i.e. the root dir itself was specified + * -ENOMEM → no memory (surprise!) + * + * This function guarantees to return a fully valid path, i.e. one that passes path_is_valid(). + */ + + if (!path_is_valid(p)) + return -EINVAL; + + /* Special case the root dir, because otherwise for an input of "///" last_path_component() returns + * the pointer to the last slash only, which might be seen as a valid path below. */ + if (path_equal(p, "/")) + return -EADDRNOTAVAIL; + + c = last_path_component(p); + + /* Delete trailing slashes, but keep one */ + while (c > p+1 && c[-1] == '/') + c--; + + if (p == c) /* No path whatsoever? Then return a recognizable error */ + return -EDESTADDRREQ; + + a = strndup(p, c - p); + if (!a) + return -ENOMEM; + + if (!path_is_valid(a)) + return -EINVAL; + + *ret = TAKE_PTR(a); + return 0; +} + bool filename_is_valid(const char *p) { const char *e; diff --git a/src/basic/path-util.h b/src/basic/path-util.h index ba12b03dbe..74ee6362ea 100644 --- a/src/basic/path-util.h +++ b/src/basic/path-util.h @@ -147,6 +147,7 @@ int fsck_exists(const char *fstype); char* dirname_malloc(const char *path); const char *last_path_component(const char *path); int path_extract_filename(const char *p, char **ret); +int path_extract_directory(const char *p, char **ret); bool filename_is_valid(const char *p) _pure_; bool path_is_valid(const char *p) _pure_; diff --git a/src/test/test-path-util.c b/src/test/test-path-util.c index 59308473c1..db6c1a9efa 100644 --- a/src/test/test-path-util.c +++ b/src/test/test-path-util.c @@ -606,6 +606,64 @@ static void test_path_extract_filename(void) { test_path_extract_filename_one("./", NULL, -EINVAL); } +static void test_path_extract_directory_one(const char *input, const char *output, int ret) { + _cleanup_free_ char *k = NULL; + int r; + + r = path_extract_directory(input, &k); + log_info_errno(r, "%s → %s/%m [expected: %s/%s]", + strnull(input), + strnull(k), /* we output strerror_safe(r) via %m here, since otherwise the error buffer might be overwritten twice */ + strnull(output), strerror_safe(ret)); + assert_se(streq_ptr(k, output)); + assert_se(r == ret); + + /* Extra safety check: let's make sure that if we split out the filename too (and it works) the + * joined parts are identical to the original again */ + if (r >= 0) { + _cleanup_free_ char *f = NULL; + + r = path_extract_filename(input, &f); + if (r >= 0) { + _cleanup_free_ char *j = NULL; + + assert_se(j = path_join(k, f)); + assert_se(path_equal(input, j)); + } + } +} + +static void test_path_extract_directory(void) { + log_info("/* %s */", __func__); + + test_path_extract_directory_one(NULL, NULL, -EINVAL); + test_path_extract_directory_one("a/b/c", "a/b", 0); + test_path_extract_directory_one("a/b/c/", "a/b", 0); + test_path_extract_directory_one("/", NULL, -EADDRNOTAVAIL); + test_path_extract_directory_one("//", NULL, -EADDRNOTAVAIL); + test_path_extract_directory_one("///", NULL, -EADDRNOTAVAIL); + test_path_extract_directory_one(".", NULL, -EDESTADDRREQ); + test_path_extract_directory_one("./.", ".", 0); + test_path_extract_directory_one("././", ".", 0); + test_path_extract_directory_one("././/", ".", 0); + test_path_extract_directory_one("/foo/a", "/foo", 0); + test_path_extract_directory_one("/foo/a/", "/foo", 0); + test_path_extract_directory_one("", NULL, -EINVAL); + test_path_extract_directory_one("a", NULL, -EDESTADDRREQ); + test_path_extract_directory_one("a/", NULL, -EDESTADDRREQ); + test_path_extract_directory_one("/a", "/", 0); + test_path_extract_directory_one("/a/", "/", 0); + test_path_extract_directory_one("/////////////a/////////////", "/", 0); + test_path_extract_directory_one("xx/.", "xx", 0); + test_path_extract_directory_one("xx/..", "xx", 0); + test_path_extract_directory_one("..", NULL, -EDESTADDRREQ); + test_path_extract_directory_one("/..", "/", 0); + test_path_extract_directory_one("../", NULL, -EDESTADDRREQ); + test_path_extract_directory_one(".", NULL, -EDESTADDRREQ); + test_path_extract_directory_one("/.", "/", 0); + test_path_extract_directory_one("./", NULL, -EDESTADDRREQ); +} + static void test_filename_is_valid(void) { char foo[NAME_MAX+2]; @@ -793,6 +851,7 @@ int main(int argc, char **argv) { test_file_in_same_dir(); test_last_path_component(); test_path_extract_filename(); + test_path_extract_directory(); test_filename_is_valid(); test_path_is_valid(); test_hidden_or_backup_file(); -- 2.25.1