BLI_path_util: Add BLI_path_join
authorCampbell Barton <ideasman42@gmail.com>
Fri, 24 Mar 2017 06:29:48 +0000 (17:29 +1100)
committerCampbell Barton <ideasman42@gmail.com>
Fri, 24 Mar 2017 06:40:35 +0000 (17:40 +1100)
There weren't any convenient ways to join multiple paths in C
that accounted for corner cases.

source/blender/blenlib/BLI_path_util.h
source/blender/blenlib/intern/path_util.c
tests/gtests/blenlib/BLI_path_util_test.cc

index a4f5c3c7a013c329ffa556e053cebb3c1e5b35b1..b6a55d34d14bf2dfa27049c983a8da63fafcf826 100644 (file)
@@ -52,6 +52,9 @@ void BLI_path_append(char *__restrict dst, const size_t maxlen,
                      const char *__restrict file) ATTR_NONNULL();
 void BLI_join_dirfile(char *__restrict string, const size_t maxlen,
                       const char *__restrict dir, const char *__restrict file) ATTR_NONNULL();
+size_t BLI_path_join(
+        char *__restrict dst, const size_t dst_len,
+        const char *path_first, ...) ATTR_NONNULL(1, 3) ATTR_SENTINEL(0);
 const char *BLI_path_basename(const char *path) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
 bool BLI_path_name_at_index(
         const char *__restrict path, const int index,
index 955f603cdae1f49a70332ab696b147c8ba439857..7b765cfa93953358f992022875d47c474ad8c1b6 100644 (file)
@@ -1590,6 +1590,90 @@ void BLI_join_dirfile(char *__restrict dst, const size_t maxlen, const char *__r
        BLI_strncpy(dst + dirlen, file, maxlen - dirlen);
 }
 
+/**
+ * Join multiple strings into a path, ensuring only a single path separator between each,
+ * and trailing slash is kept.
+ *
+ * \note If you want a trailing slash, add ``SEP_STR`` as the last path argument,
+ * duplicate slashes will be cleaned up.
+ */
+size_t BLI_path_join(char *__restrict dst, const size_t dst_len, const char *path, ...)
+{
+       if (UNLIKELY(dst_len == 0)) {
+               return 0;
+       }
+       const size_t dst_last = dst_len - 1;
+       size_t ofs = BLI_strncpy_rlen(dst, path, dst_len);
+
+       if (ofs == dst_last) {
+               return ofs;
+       }
+
+       /* remove trailing slashes, unless there are _only_ trailing slashes
+        * (allow "//" as the first argument). */
+       bool has_trailing_slash = false;
+       if (ofs != 0) {
+               size_t len = ofs;
+               while ((len != 0) && ELEM(path[len - 1], SEP, ALTSEP)) {
+                       len -= 1;
+               }
+               if (len != 0) {
+                       ofs = len;
+               }
+               has_trailing_slash = (path[len] != '\0');
+       }
+
+       va_list args;
+       va_start(args, path);
+       while ((path = (const char *) va_arg(args, const char *))) {
+               has_trailing_slash = false;
+               const char *path_init = path;
+               while (ELEM(path[0], SEP, ALTSEP)) {
+                       path++;
+               }
+               size_t len = strlen(path);
+               if (len != 0) {
+                       while ((len != 0) && ELEM(path[len - 1], SEP, ALTSEP)) {
+                               len -= 1;
+                       }
+
+                       if (len != 0) {
+                               /* the very first path may have a slash at the end */
+                               if (ofs && !ELEM(dst[ofs - 1], SEP, ALTSEP)) {
+                                       dst[ofs++] = SEP;
+                                       if (ofs == dst_last) {
+                                               break;
+                                       }
+                               }
+                               has_trailing_slash = (path[len] != '\0');
+                               if (ofs + len >= dst_last) {
+                                       len = dst_last - ofs;
+                               }
+                               memcpy(&dst[ofs], path, len);
+                               ofs += len;
+                               if (ofs == dst_last) {
+                                       break;
+                               }
+                       }
+               }
+               else {
+                       has_trailing_slash = (path_init != path);
+               }
+       }
+       va_end(args);
+
+       if (has_trailing_slash) {
+               if ((ofs != dst_last) && (ofs != 0) && (ELEM(dst[ofs - 1], SEP, ALTSEP) == 0)) {
+                       dst[ofs++] = SEP;
+               }
+       }
+
+       BLI_assert(ofs <= dst_last);
+       dst[ofs] = '\0';
+
+       return ofs;
+}
+
 /**
  * like pythons os.path.basename()
  *
index ccfa59503c2fec04b993f23bad8ad6dbe0ba42ca..4c45bfd6758ca2a59b8df9be46bcba0a06736c30 100644 (file)
@@ -5,6 +5,7 @@
 extern "C" {
 #include "BLI_fileops.h"
 #include "BLI_path_util.h"
+#include "BLI_string.h"
 #include "../../../source/blender/imbuf/IMB_imbuf.h"
 
 #ifdef _WIN32
@@ -245,6 +246,103 @@ TEST(path_util, NameAtIndex_NoneComplexNeg)
 
 #undef AT_INDEX
 
+#define JOIN(str_expect, out_size, ...) \
+       { \
+               const char *expect = str_expect; \
+               char result[(out_size) + 1024]; \
+               /* check we don't write past the last byte */ \
+               result[out_size] = '\0'; \
+               BLI_path_join(result, out_size, __VA_ARGS__, NULL); \
+               /* simplify expected string */ \
+               BLI_str_replace_char(result, '\\', '/'); \
+               EXPECT_STREQ(result, expect); \
+               EXPECT_EQ(result[out_size], '\0'); \
+       } ((void)0)
+
+/* BLI_path_join */
+TEST(path_util, JoinNop)
+{
+       JOIN("", 100, "");
+       JOIN("", 100, "", "");
+       JOIN("", 100, "", "", "");
+       JOIN("/", 100, "/", "", "");
+       JOIN("/", 100, "/", "/");
+       JOIN("/", 100, "/", "", "/");
+       JOIN("/", 100, "/", "", "/", "");
+}
+
+TEST(path_util, JoinSingle)
+{
+       JOIN("test", 100, "test");
+       JOIN("", 100, "");
+       JOIN("a", 100, "a");
+       JOIN("/a", 100, "/a");
+       JOIN("a/", 100, "a/");
+       JOIN("/a/", 100, "/a/");
+       JOIN("/a/", 100, "/a//");
+       JOIN("//a/", 100, "//a//");
+}
+
+TEST(path_util, JoinTriple)
+{
+       JOIN("/a/b/c", 100, "/a", "b", "c");
+       JOIN("/a/b/c", 100, "/a/", "/b/", "/c");
+       JOIN("/a/b/c", 100, "/a/b/", "/c");
+       JOIN("/a/b/c", 100, "/a/b/c");
+       JOIN("/a/b/c", 100, "/", "a/b/c");
+
+       JOIN("/a/b/c/", 100, "/a/", "/b/", "/c/");
+       JOIN("/a/b/c/", 100, "/a/b/c/");
+       JOIN("/a/b/c/", 100, "/a/b/", "/c/");
+       JOIN("/a/b/c/", 100, "/a/b/c", "/");
+       JOIN("/a/b/c/", 100, "/", "a/b/c", "/");
+}
+
+TEST(path_util, JoinTruncateShort)
+{
+       JOIN("", 1, "/");
+       JOIN("/", 2, "/");
+       JOIN("a", 2, "", "aa");
+       JOIN("a", 2, "", "a/");
+       JOIN("a/b", 4, "a", "bc");
+       JOIN("ab/", 4, "ab", "c");
+       JOIN("/a/", 4, "/a", "b");
+       JOIN("/a/", 4, "/a/", "b/");
+       JOIN("/a/", 4, "/a", "/b/");
+       JOIN("/a/", 4, "/", "a/b/");
+       JOIN("//a", 4, "//", "a/b/");
+
+       JOIN("/a/b", 5, "/a", "b", "c");
+}
+
+TEST(path_util, JoinTruncateLong)
+{
+       JOIN("", 1, "//", "//longer", "path");
+       JOIN("/", 2, "//", "//longer", "path");
+       JOIN("//", 3, "//", "//longer", "path");
+       JOIN("//l", 4, "//", "//longer", "path");
+       /* snip */
+       JOIN("//longe", 8, "//", "//longer", "path");
+       JOIN("//longer", 9, "//", "//longer", "path");
+       JOIN("//longer/", 10, "//", "//longer", "path");
+       JOIN("//longer/p", 11, "//", "//longer", "path");
+       JOIN("//longer/pa", 12, "//", "//longer", "path");
+       JOIN("//longer/pat", 13, "//", "//longer", "path");
+       JOIN("//longer/path", 14, "//", "//longer", "path"); // not truncated
+       JOIN("//longer/path", 14, "//", "//longer", "path/");
+       JOIN("//longer/path/", 15, "//", "//longer", "path/"); // not truncated
+       JOIN("//longer/path/", 15, "//", "//longer", "path/", "trunc");
+       JOIN("//longer/path/t", 16, "//", "//longer", "path/", "trunc");
+}
+
+TEST(path_util, JoinComplex)
+{
+       JOIN("/a/b/c/d/e/f/g/", 100, "/", "\\a/b", "//////c/d", "", "e\\\\", "f", "g//");
+       JOIN("/aa/bb/cc/dd/ee/ff/gg/", 100, "/", "\\aa/bb", "//////cc/dd", "", "ee\\\\", "ff", "gg//");
+       JOIN("1/2/3/", 100, "1", "////////", "", "2", "3\\");
+}
+
+#undef JOIN
 
 /* BLI_path_frame */
 TEST(path_util, PathUtilFrame)