/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved. * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup bli * Various string, file, list operations. */ #include /* For `min/max`. */ #include #include #include #include "BLI_fileops.h" #include "BLI_fnmatch.h" #include "BLI_path_utils.hh" #include "BLI_string.h" #include "BLI_string_utf8.h" #include "BLI_string_utils.hh" #include "BLI_utildefines.h" #ifdef WIN32 # include "utf_winfunc.hh" # include "utfconv.hh" # include # ifdef _WIN32_IE # undef _WIN32_IE # endif # define _WIN32_IE 0x0501 # include "BLI_alloca.h" # include "BLI_winstuff.h" # include # include #else # include #endif /* WIN32 */ #include "MEM_guardedalloc.h" /* Declarations. */ static int BLI_path_unc_prefix_len(const char *path); #ifdef WIN32 static bool BLI_path_is_abs_win32(const char *path); static int BLI_path_win32_prefix_len(const char *path); #endif /* WIN32 */ /** * The maximum number of `#` characters expanded for #BLI_path_frame & #BLI_path_frame_range * Typically 12 is enough and even 16 is very large. * Use a much larger value so hitting the upper limit is not an issue. * Exceeding this limit won't fail either, it will just not insert so many leading zeros. */ #define FILENAME_FRAME_CHARS_MAX FILE_MAX int BLI_path_sequence_decode(const char *path, char *head, const size_t head_maxncpy, char *tail, const size_t tail_maxncpy, ushort *r_digits_len) { if (head) { BLI_string_debug_size(head, head_maxncpy); } if (tail) { BLI_string_debug_size(tail, tail_maxncpy); } uint nums = 0, nume = 0; int i; bool found_digit = false; const char *const lslash = BLI_path_slash_rfind(path); const char *const extension = BLI_path_extension_or_end(lslash ? lslash : path); const uint lslash_len = lslash != nullptr ? int(lslash - path) : 0; const uint name_end = uint(extension - path); for (i = name_end - 1; i >= int(lslash_len); i--) { if (isdigit(path[i])) { if (found_digit) { nums = i; } else { nume = i; nums = i; found_digit = true; } } else { if (found_digit) { break; } } } if (found_digit) { const long long int ret = strtoll(&(path[nums]), nullptr, 10); if (ret >= INT_MIN && ret <= INT_MAX) { if (tail) { BLI_strncpy(tail, &path[nume + 1], tail_maxncpy); } if (head) { BLI_strncpy(head, path, std::min(head_maxncpy, nums + 1)); } if (r_digits_len) { *r_digits_len = nume - nums + 1; } return int(ret); } } if (tail) { BLI_strncpy(tail, path + name_end, tail_maxncpy); } if (head) { /* Name_end points to last character of head, * make it +1 so null-terminator is nicely placed. */ BLI_strncpy(head, path, std::min(head_maxncpy, name_end + 1)); } if (r_digits_len) { *r_digits_len = 0; } return 0; } void BLI_path_sequence_encode(char *path, const size_t path_maxncpy, const char *head, const char *tail, ushort numlen, int pic) { BLI_string_debug_size(path, path_maxncpy); /* FIXME: MAX_ID_NAME & FILE_MAXFILE * * As this function directly works on a full file path (typically a FILE_MAX long char buffer), * and does not perform any check on the filename part of the path, it can easily generate final * paths containing a filename longer than the max supported length (FILE_MAXFILE). */ BLI_snprintf(path, path_maxncpy, "%s%.*d%s", head, numlen, std::max(0, pic), tail); } /** * Implementation for #BLI_path_normalize & #BLI_path_normalize_native. * \return The path length. */ static int path_normalize_impl(char *path, bool check_blend_relative_prefix) { const char *path_orig = path; int path_len = strlen(path); /* * Skip absolute prefix. * --------------------- */ if (check_blend_relative_prefix && (path[0] == '/' && path[1] == '/')) { path = path + 2; /* Leave the initial `//` untouched. */ path_len -= 2; /* Strip leading slashes, as they will interfere with the absolute/relative check * (besides being redundant). */ int i = 0; while (path[i] == SEP) { i++; } if (i != 0) { memmove(path, path + i, (path_len - i) + 1); path_len -= i; } BLI_assert(path_len == strlen(path)); } #ifdef WIN32 /* Skip to the first slash of the drive or UNC path, * so additional slashes are treated as doubles. */ if (path_orig == path) { int path_unc_len = BLI_path_unc_prefix_len(path); if (path_unc_len) { path_unc_len -= 1; BLI_assert(path_unc_len > 0 && path[path_unc_len] == SEP); path += path_unc_len; path_len -= path_unc_len; } else if (BLI_path_is_win32_drive(path)) { /* Check for `C:` (2 characters only). */ path += 2; path_len -= 2; } } #endif /* WIN32 */ /* Works on WIN32 as well, because the drive component is skipped. */ const bool is_relative = path[0] && (path[0] != SEP); /* * Strip redundant path components. * -------------------------------- */ /* NOTE(@ideasman42): * `memmove(start, eind, strlen(eind) + 1);` * is the same as * `BLI_strncpy(start, eind, ...);` * except string-copy should not be used because there is overlap, * so use `memmove` 's slightly more obscure syntax. */ /* Inline replacement: * - `/./` -> `/`. * - `//` -> `/`. * Performed until no more replacements can be made. */ if (path_len > 1) { for (int i = path_len - 1; i > 0; i--) { /* Calculate the redundant slash span (if any). */ if (path[i] == SEP) { const int i_end = i; do { /* Stepping over elements assumes 'i' references a separator. */ BLI_assert(path[i] == SEP); if (path[i - 1] == SEP) { i -= 1; /* Found `//`, replace with `/`. */ } else if (i >= 2 && path[i - 1] == '.' && path[i - 2] == SEP) { i -= 2; /* Found `/./`, replace with `/`. */ } else { break; } } while (i > 0); if (i < i_end) { memmove(path + i, path + i_end, (path_len - i_end) + 1); path_len -= i_end - i; BLI_assert(strlen(path) == path_len); } } } } /* Remove redundant `./` prefix as it's redundant & complicates collapsing directories. */ if (is_relative) { if ((path_len > 2) && (path[0] == '.') && (path[1] == SEP)) { memmove(path, path + 2, (path_len - 2) + 1); path_len -= 2; } } /* * Collapse Parent Directories. * ---------------------------- * * Example: `//../` -> `/` * * Notes: * - Leading `../` are skipped as they cannot be collapsed (see `start_base`). * - Multiple parent directories are handled at once to reduce number of `memmove` calls. */ #define IS_PARENT_DIR(p) ((p)[0] == '.' && (p)[1] == '.' && ELEM((p)[2], SEP, '\0')) /* First non prefix path component. */ char *path_first_non_slash_part = path; while (*path_first_non_slash_part && *path_first_non_slash_part == SEP) { path_first_non_slash_part++; } /* Maintain a pointer to the end of leading `..` component. * Skip leading parent directories because logically they cannot be collapsed. */ char *start_base = path_first_non_slash_part; while (IS_PARENT_DIR(start_base)) { start_base += 3; } /* It's possible the entire path is made of up `../`, * in this case there is nothing to do. */ if (start_base < path + path_len) { /* Step over directories, always starting out on the character after the slash. */ char *start = start_base; char *start_temp; while ((start_temp = strstr(start, SEP_STR ".." SEP_STR)) || /* Check if the string ends with `/..` & assign when found, else nullptr. */ (start_temp = ((start <= &path[path_len - 3]) && STREQ(&path[path_len - 3], SEP_STR "..")) ? &path[path_len - 3] : nullptr)) { start = start_temp + 1; /* Skip the `/`. */ BLI_assert(start_base != start); /* Step `end_all` forwards (over all `..`). */ char *end_all = start; do { BLI_assert(IS_PARENT_DIR(end_all)); end_all += 3; BLI_assert(end_all <= path + path_len + 1); } while (IS_PARENT_DIR(end_all)); /* Step `start` backwards (until `end` meets `end_all` or `start` meets `start_base`). */ char *end = start; do { BLI_assert(start_base < start); BLI_assert(*(start - 1) == SEP); /* Step `start` backwards one. */ do { start--; } while (start_base < start && *(start - 1) != SEP); BLI_assert(*start != SEP); /* Ensure the loop ran at least once. */ BLI_assert(!IS_PARENT_DIR(start)); /* Clamping by `start_base` prevents this. */ end += 3; } while ((start != start_base) && (end < end_all)); if (end > path + path_len) { BLI_assert(*(end - 1) == '\0'); end--; end_all--; } BLI_assert(start < end && start >= start_base); const size_t start_len = path_len - (end - path); memmove(start, end, start_len + 1); path_len -= end - start; BLI_assert(strlen(path) == path_len); /* Other `..` directories may have been moved to the front, step `start_base` past them. */ if (UNLIKELY(start == start_base && (end != end_all))) { start_base += (end_all - end); start = (start_base < path + path_len) ? start_base : start_base - 1; } } } BLI_assert(strlen(path) == path_len); /* Characters before the `start_base` must *only* be `../../../` (multiples of 3). */ BLI_assert((start_base - path_first_non_slash_part) % 3 == 0); /* All `..` ahead of `start_base` were collapsed (including trailing `/..`). */ BLI_assert(!(start_base < path + path_len) || (!strstr(start_base, SEP_STR ".." SEP_STR) && !(path_len >= 3 && STREQ(&path[path_len - 3], SEP_STR "..")))); /* * Final Prefix Cleanup. * --------------------- */ if (is_relative) { if (path_len == 0 && (path == path_orig)) { path[0] = '.'; path[1] = '\0'; path_len = 1; } } else { /* Support for odd paths: eg `/../home/me` --> `/home/me` * this is a valid path in blender but we can't handle this the usual way below * simply strip this prefix then evaluate the path as usual. * Python's `os.path.normpath()` does this. */ if (start_base != path_first_non_slash_part) { char *start = start_base > path + path_len ? start_base - 1 : start_base; /* As long as `start` is set correctly, it should never begin with `../` * as these directories are expected to be skipped. */ BLI_assert(!IS_PARENT_DIR(start)); const size_t start_len = path_len - (start - path); BLI_assert(strlen(start) == start_len); memmove(path_first_non_slash_part, start, start_len + 1); path_len -= start - path_first_non_slash_part; BLI_assert(strlen(path) == path_len); } } BLI_assert(strlen(path) == path_len); #undef IS_PARENT_DIR return (path - path_orig) + path_len; } int BLI_path_normalize(char *path) { return path_normalize_impl(path, true); } int BLI_path_normalize_native(char *path) { return path_normalize_impl(path, false); } int BLI_path_normalize_dir(char *dir, size_t dir_maxncpy) { /* Would just create an unexpected "/" path, just early exit entirely. */ if (dir[0] == '\0') { return 0; } int dir_len = BLI_path_normalize(dir); return BLI_path_slash_ensure_ex(dir, dir_maxncpy, dir_len); } int BLI_path_canonicalize_native(char *path, int path_maxncpy) { BLI_path_abs_from_cwd(path, path_maxncpy); /* As these are system level paths, only convert slashes * if the alternate direction is accepted as a slash. */ if (BLI_path_slash_is_native_compat(ALTSEP)) { BLI_path_slash_native(path); } int path_len = BLI_path_normalize_native(path); /* Strip trailing slash but don't strip `/` away to nothing. */ if (path_len > 1 && path[path_len - 1] == SEP) { #ifdef WIN32 /* Don't strip `C:\` -> `C:` as this is no longer a valid directory. */ if (BLI_path_win32_prefix_len(path) + 1 < path_len) #endif { path_len -= 1; path[path_len] = '\0'; } } return path_len; } bool BLI_path_make_safe_filename_ex(char *filename, bool allow_tokens) { #define INVALID_CHARS \ "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" \ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \ "/\\?*:|\"" #define INVALID_TOKENS "<>" const char *invalid = allow_tokens ? INVALID_CHARS : INVALID_CHARS INVALID_TOKENS; #undef INVALID_CHARS #undef INVALID_TOKENS char *fn; bool changed = false; if (*filename == '\0') { return changed; } for (fn = filename; *fn && (fn = strpbrk(fn, invalid)); fn++) { *fn = '_'; changed = true; } /* Forbid only dots. */ for (fn = filename; *fn == '.'; fn++) { /* Pass. */ } if (*fn == '\0') { *filename = '_'; changed = true; } #ifdef WIN32 { const char *invalid_names[] = { "con", "prn", "aux", "null", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", nullptr, }; const size_t len = strlen(filename); char *filename_lower = BLI_strdupn(filename, len); const char **iname; /* Forbid trailing dot (trailing space has already been replaced above). */ if (filename[len - 1] == '.') { filename[len - 1] = '_'; changed = true; } /* Check for forbidden names - not we have to check all combination * of upper and lower cases, hence the usage of filename_lower * (more efficient than using #BLI_strcasestr repeatedly). */ BLI_str_tolower_ascii(filename_lower, len); for (iname = invalid_names; *iname; iname++) { if (strstr(filename_lower, *iname) == filename_lower) { const size_t iname_len = strlen(*iname); /* Only invalid if the whole name is made of the invalid chunk, or it has an * (assumed extension) dot just after. This means it will also catch *valid* * names like `aux.foo.bar`, but should be good enough for us! */ if ((iname_len == len) || (filename_lower[iname_len] == '.')) { *filename = '_'; changed = true; break; } } } MEM_freeN(filename_lower); } #endif return changed; } bool BLI_path_make_safe_filename(char *filename) { return BLI_path_make_safe_filename_ex(filename, false); } bool BLI_path_make_safe(char *path) { /* Simply apply #BLI_path_make_safe_filename() over each component of the path. * Luckily enough, same *safe* rules applies to file & directory names. */ char *curr_slash, *curr_path = path; bool changed = false; bool skip_first = false; #ifdef WIN32 if (BLI_path_is_abs_win32(path)) { /* Do not make safe `C:` in `C:\foo\bar`. */ skip_first = true; } #endif for (curr_slash = (char *)BLI_path_slash_find(curr_path); curr_slash; curr_slash = (char *)BLI_path_slash_find(curr_path)) { const char backup = *curr_slash; *curr_slash = '\0'; if (!skip_first && (*curr_path != '\0') && BLI_path_make_safe_filename(curr_path)) { changed = true; } skip_first = false; curr_path = curr_slash + 1; *curr_slash = backup; } if (BLI_path_make_safe_filename(curr_path)) { changed = true; } return changed; } bool BLI_path_is_rel(const char *path) { return path[0] == '/' && path[1] == '/'; } bool BLI_path_is_unc(const char *path) { return path[0] == '\\' && path[1] == '\\'; } /** * Returns the length of the identifying prefix * of a UNC path which can start with '\\' (short version) * or '\\?\' (long version) * If the path is not a UNC path, return 0 */ static int BLI_path_unc_prefix_len(const char *path) { if (BLI_path_is_unc(path)) { if ((path[2] == '?') && (path[3] == '\\')) { /* We assume long UNC path like `\\?\server\share\folder` etc. */ return 4; } return 2; } return 0; } #ifdef WIN32 static int BLI_path_win32_prefix_len(const char *path) { if (BLI_path_is_win32_drive(path)) { return 2; } return BLI_path_unc_prefix_len(path); } #endif bool BLI_path_is_win32_drive(const char *path) { return isalpha(path[0]) && (path[1] == ':'); } bool BLI_path_is_win32_drive_only(const char *path) { return isalpha(path[0]) && (path[1] == ':') && (path[2] == '\0'); } bool BLI_path_is_win32_drive_with_slash(const char *path) { return isalpha(path[0]) && (path[1] == ':') && ELEM(path[2], '\\', '/'); } #if defined(WIN32) /** * Return true if the path is an absolute path on a WIN32 file-system, it either: * - Starts with a drive specifier* (eg `A:\`). * - Is a UNC path. * * \note Not to be confused with the opposite of #BLI_path_is_rel which checks for the * Blender specific convention of using `//` prefix for blend-file relative paths. */ static bool BLI_path_is_abs_win32(const char *path) { /* Don't use the `BLI_path_is_win32_drive_with_slash` * since paths such as `C:` are valid on their own. */ return BLI_path_is_win32_drive(path) || BLI_path_is_unc(path); } static wchar_t *next_slash(wchar_t *path) { wchar_t *slash = path; while (*slash && *slash != L'\\') { slash++; } return slash; } /* Adds a slash if the UNC path points to a share. */ static void BLI_path_add_slash_to_share(wchar_t *uncpath) { wchar_t *slash_after_server = next_slash(uncpath + 2); if (*slash_after_server) { wchar_t *slash_after_share = next_slash(slash_after_server + 1); if (!(*slash_after_share)) { slash_after_share[0] = L'\\'; slash_after_share[1] = L'\0'; } } } static void BLI_path_unc_to_short(wchar_t *unc) { wchar_t tmp[PATH_MAX]; int len = wcslen(unc); /* Convert: * - `\\?\UNC\server\share\folder\...` to `\\server\share\folder\...` * - `\\?\C:\` to `C:\` * - `\\?\C:\folder\...` to `C:\folder\...` */ if ((len > 3) && (unc[0] == L'\\') && (unc[1] == L'\\') && (unc[2] == L'?') && ELEM(unc[3], L'\\', L'/')) { if ((len > 5) && (unc[5] == L':')) { wcsncpy(tmp, unc + 4, len - 4); tmp[len - 4] = L'\0'; wcscpy(unc, tmp); } else if ((len > 7) && (wcsncmp(&unc[4], L"UNC", 3) == 0) && ELEM(unc[7], L'\\', L'/')) { tmp[0] = L'\\'; tmp[1] = L'\\'; wcsncpy(tmp + 2, unc + 8, len - 8); tmp[len - 6] = L'\0'; wcscpy(unc, tmp); } } } void BLI_path_normalize_unc(char *path, int path_maxncpy) { wchar_t *tmp_16 = alloc_utf16_from_8(path, 1); BLI_path_normalize_unc_16(tmp_16); conv_utf_16_to_8(tmp_16, path, path_maxncpy); } void BLI_path_normalize_unc_16(wchar_t *path_16) { BLI_path_unc_to_short(path_16); BLI_path_add_slash_to_share(path_16); } #endif void BLI_path_rel(char path[FILE_MAX], const char *basepath) { BLI_string_debug_size_after_nil(path, FILE_MAX); /* A `basepath` starting with `//` will be made relative multiple times. */ BLI_assert_msg(!BLI_path_is_rel(basepath), "The 'basepath' cannot start with '//'!"); const char *lslash; char temp[FILE_MAX]; /* If path is already relative, bail out. */ if (BLI_path_is_rel(path)) { return; } /* Also bail out if relative path is not set. */ if (basepath[0] == '\0') { return; } #ifdef WIN32 if (BLI_strnlen(basepath, 3) > 2 && !BLI_path_is_abs_win32(basepath)) { char *ptemp; /* Fix missing volume name in relative base, * can happen with old `recent-files.txt` files. */ BLI_windows_get_default_root_dir(temp); ptemp = &temp[2]; if (!ELEM(basepath[0], '\\', '/')) { ptemp++; } BLI_strncpy(ptemp, basepath, FILE_MAX - 3); } else { BLI_strncpy(temp, basepath, FILE_MAX); } if (BLI_strnlen(path, 3) > 2) { bool is_unc = BLI_path_is_unc(path); /* Ensure paths are both UNC paths or are both drives. */ if (BLI_path_is_unc(temp) != is_unc) { return; } /* Ensure both UNC paths are on the same share. */ if (is_unc) { int off; int slash = 0; for (off = 0; temp[off] && slash < 4; off++) { if (temp[off] != path[off]) { return; } if (temp[off] == '\\') { slash++; } } } else if ((temp[1] == ':' && path[1] == ':') && (tolower(temp[0]) != tolower(path[0]))) { return; } } #else STRNCPY(temp, basepath); #endif BLI_string_replace_char(temp + BLI_path_unc_prefix_len(temp), '\\', '/'); BLI_string_replace_char(path + BLI_path_unc_prefix_len(path), '\\', '/'); /* Remove `/./` which confuse the following slash counting. */ BLI_path_normalize(path); BLI_path_normalize(temp); /* The last slash in the path indicates where the path part ends. */ lslash = BLI_path_slash_rfind(temp); if (lslash) { /* Find the prefix of the filename that is equal for both filenames. * This is replaced by the two slashes at the beginning. */ const char *p = temp; const char *q = path; #ifdef WIN32 while (tolower(*p) == tolower(*q)) #else while (*p == *q) #endif { p++; q++; /* Don't search beyond the end of the string in the rare case they match. */ if ((*p == '\0') || (*q == '\0')) { break; } } /* We might have passed the slash when the beginning of a dir matches * so we rewind. Only check on the actual filename. */ if (*q != '/') { while ((q >= path) && (*q != '/')) { q--; p--; } } else if (*p != '/') { while ((p >= temp) && (*p != '/')) { p--; q--; } } char res[FILE_MAX] = "//"; char *r = res + 2; /* `p` now points to the slash that is at the beginning of the part * where the path is different from the relative path. * We count the number of directories we need to go up in the * hierarchy to arrive at the common prefix of the path. */ if (p < temp) { p = temp; } while (p && p < lslash) { if (*p == '/') { r += BLI_strncpy_rlen(r, "../", sizeof(res) - (r - res)); } p++; } /* Don't copy the slash at the beginning. */ r += BLI_strncpy_rlen(r, q + 1, sizeof(res) - (r - res)); UNUSED_VARS(r); #ifdef WIN32 BLI_string_replace_char(res + 2, '/', '\\'); #endif BLI_strncpy(path, res, FILE_MAX); } } bool BLI_path_suffix(char *path, size_t path_maxncpy, const char *suffix, const char *sep) { BLI_string_debug_size_after_nil(path, path_maxncpy); const size_t suffix_len = strlen(suffix); const size_t sep_len = strlen(sep); char *extension = (char *)BLI_path_extension_or_end(path); const size_t extension_len = strlen(extension); const size_t path_end = extension - path; const size_t path_len = path_end + extension_len; if (path_len + sep_len + suffix_len >= path_maxncpy) { return false; } if (extension_len) { memmove(extension + (sep_len + suffix_len), extension, extension_len); } char *c = path + path_end; if (sep_len) { memcpy(c, sep, sep_len); c += sep_len; } if (suffix_len) { memcpy(c, suffix, suffix_len); c += suffix_len; } c += extension_len; *c = '\0'; return true; } const char *BLI_path_parent_dir_end(const char *path, size_t path_len) { const char *path_end = path + path_len - 1; const char *p = path_end; while (p >= path) { if (BLI_path_slash_is_native_compat(*p)) { break; } p--; } while (p > path) { if (BLI_path_slash_is_native_compat(*(p - 1))) { p -= 1; /* Skip `/`. */ } else if ((p + 1 > path) && (*(p - 1) == '.') && BLI_path_slash_is_native_compat(*p - 2)) { p -= 2; /* Skip `/.` (actually `/./` but the last slash was already skipped) */ } else { break; } } if ((p > path) && (p != path_end)) { return p; } return nullptr; } bool BLI_path_parent_dir(char *path) { /* Use #BLI_path_name_at_index instead of checking if the strings ends with `parent_dir` * to ensure the logic isn't confused by: * - Directory names that happen to end with `..`. * - When `path` is empty, the contents will be `../` * which would cause checking for a tailing `/../` fail. * Extracting the span of the final directory avoids both these issues. */ int tail_ofs = 0, tail_len = 0; if (!BLI_path_name_at_index(path, -1, &tail_ofs, &tail_len)) { return false; } if (tail_len == 1) { /* Last path is `.`, as normalize should remove this, it's safe to assume failure. * This happens when the input a single period (possibly with slashes before or after). */ if (path[tail_ofs] == '.') { return false; } } /* Input paths should already be normalized if `..` is part of the path. */ BLI_assert(!((tail_len == 2) && (path[tail_ofs] == '.') && (path[tail_ofs + 1] == '.'))); path[tail_ofs] = '\0'; return true; } bool BLI_path_parent_dir_until_exists(char *path) { bool valid_path = true; /* Loop as long as cur path is not a dir, and we can get a parent path. */ while ((BLI_access(path, R_OK) != 0) && (valid_path = BLI_path_parent_dir(path))) { /* Pass. */ } return (valid_path && path[0]); } /** * Looks for a sequence of "#" characters in the last slash-separated component of `path`, * returning the indexes of the first and one past the last character in the sequence in * `r_char_start` and `r_char_end` respectively. * * \param r_char_start: The first `#` character. * \param r_char_end: The last `#` character +1. * * \return true if a frame sequence range was found. */ static bool path_frame_chars_find_range(const char *path, int *r_char_start, int *r_char_end) { uint ch_sta, ch_end, i; /* Insert current frame: `file###` -> `file001`. */ ch_sta = ch_end = 0; for (i = 0; path[i] != '\0'; i++) { if (ELEM(path[i], '\\', '/')) { ch_end = 0; /* This is a directory name, don't use any hashes we found. */ } else if (path[i] == '#') { ch_sta = i; ch_end = ch_sta + 1; while (path[ch_end] == '#') { ch_end++; } i = ch_end - 1; /* Keep searching. */ /* Don't break, there may be a slash after this that invalidates the previous #'s. */ } } if (ch_end) { *r_char_start = ch_sta; *r_char_end = ch_end; return true; } *r_char_start = -1; *r_char_end = -1; return false; } /** * Ensure `path` contains at least one "#" character in its last slash-separated * component, appending one digits long if not. */ static void ensure_digits(char *path, int digits) { char *file = (char *)BLI_path_basename(path); if (strrchr(file, '#') == nullptr) { int len = strlen(file); while (digits--) { file[len++] = '#'; } file[len] = '\0'; } } bool BLI_path_frame(char *path, size_t path_maxncpy, int frame, int digits) { BLI_string_debug_size_after_nil(path, path_maxncpy); int ch_sta, ch_end; if (digits) { ensure_digits(path, digits); } if (path_frame_chars_find_range(path, &ch_sta, &ch_end)) { char frame_str[FILENAME_FRAME_CHARS_MAX + 1]; /* One for null. */ const int ch_span = std::min(ch_end - ch_sta, FILENAME_FRAME_CHARS_MAX); SNPRINTF(frame_str, "%.*d", ch_span, frame); BLI_string_replace_range(path, path_maxncpy, ch_sta, ch_end, frame_str); return true; } return false; } bool BLI_path_frame_range(char *path, size_t path_maxncpy, int sta, int end, int digits) { BLI_string_debug_size_after_nil(path, path_maxncpy); int ch_sta, ch_end; if (digits) { ensure_digits(path, digits); } if (path_frame_chars_find_range(path, &ch_sta, &ch_end)) { char frame_str[(FILENAME_FRAME_CHARS_MAX * 2) + 1 + 1]; /* One for null, one for the '-' */ const int ch_span = std::min(ch_end - ch_sta, FILENAME_FRAME_CHARS_MAX); SNPRINTF(frame_str, "%.*d-%.*d", ch_span, sta, ch_span, end); BLI_string_replace_range(path, path_maxncpy, ch_sta, ch_end, frame_str); return true; } return false; } bool BLI_path_frame_get(const char *path, int *r_frame, int *r_digits_len) { if (*path == '\0') { return false; } *r_digits_len = 0; const char *file = BLI_path_basename(path); const char *file_ext = BLI_path_extension_or_end(file); const char *c = file_ext; /* Find start of number (if there is one). */ int digits_len = 0; while (c-- != file && isdigit(*c)) { digits_len++; } c++; if (digits_len == 0) { return false; } /* No need to trim the string, `atio` ignores non-digits. */ *r_frame = atoi(c); *r_digits_len = digits_len; return true; } void BLI_path_frame_strip(char *path, char *r_ext, const size_t ext_maxncpy) { BLI_string_debug_size(r_ext, ext_maxncpy); *r_ext = '\0'; if (*path == '\0') { return; } char *file = (char *)BLI_path_basename(path); char *file_ext = (char *)BLI_path_extension_or_end(file); char *c = file_ext; /* Find start of number (if there is one). */ int digits_len = 0; while (c-- != file && isdigit(*c)) { digits_len++; } c++; BLI_strncpy(r_ext, file_ext, ext_maxncpy); /* Replace the number with the suffix and terminate the string. */ while (digits_len--) { *c++ = '#'; } *c = '\0'; } bool BLI_path_frame_check_chars(const char *path) { int ch_sta_dummy, ch_end_dummy; return path_frame_chars_find_range(path, &ch_sta_dummy, &ch_end_dummy); } void BLI_path_to_display_name(char *display_name, int display_name_maxncpy, const char *name) { BLI_string_debug_size(display_name, display_name_maxncpy); /* Strip leading underscores and spaces. */ int strip_offset = 0; while (ELEM(name[strip_offset], '_', ' ')) { strip_offset++; } int display_name_len = BLI_strncpy_rlen(display_name, name + strip_offset, display_name_maxncpy); /* Replace underscores with spaces. */ BLI_string_replace_char(display_name, '_', ' '); /* In rare cases file paths may not contain valid UTF8 sequences. * In this case show *something*, since it's possible stripping would remove all text. * Use a dummy character as a hint the character could not be shown. */ BLI_str_utf8_invalid_substitute(display_name, display_name_len, '_'); BLI_path_extension_strip(display_name); /* Test if string has any upper case characters. */ bool all_lower = true; for (int i = 0; display_name[i]; i++) { if (isupper(display_name[i])) { all_lower = false; break; } } if (all_lower) { /* For full lowercase string, use title case. */ bool prevspace = true; for (int i = 0; display_name[i]; i++) { if (prevspace) { display_name[i] = toupper(display_name[i]); } prevspace = isspace(display_name[i]); } } } bool BLI_path_abs(char path[FILE_MAX], const char *basepath) { BLI_string_debug_size_after_nil(path, FILE_MAX); /* A `basepath` starting with `//` will be made absolute multiple times. */ BLI_assert_msg(!BLI_path_is_rel(basepath), "The 'basepath' cannot start with '//'!"); const bool wasrelative = BLI_path_is_rel(path); char tmp[FILE_MAX]; char base[FILE_MAX]; #ifdef WIN32 /* Without this, an empty string converts to: `C:\` */ if (*path == '\0') { return wasrelative; } /* We are checking here if we have an absolute path that is not in the current `.blend` file * as a lib main - we are basically checking for the case that a UNIX root `/` is passed. */ if (!wasrelative && !BLI_path_is_abs_win32(path)) { const size_t root_dir_len = 3; char *p = path; BLI_windows_get_default_root_dir(tmp); BLI_assert(strlen(tmp) == root_dir_len); /* Step over the slashes at the beginning of the path. */ p = (char *)BLI_path_slash_skip(p); BLI_strncpy(tmp + root_dir_len, p, sizeof(tmp) - root_dir_len); } else { STRNCPY(tmp, path); } #else STRNCPY(tmp, path); /* Check for loading a MS-Windows path on a POSIX system * in this case, there is no use in trying `C:/` since it * will never exist on a Unix system. * * Add a `/` prefix and lowercase the drive-letter, remove the `:`. * `C:\foo.JPG` -> `/c/foo.JPG` */ if (BLI_path_is_win32_drive_with_slash(tmp)) { tmp[1] = tolower(tmp[0]); /* Replace `:` with drive-letter. */ tmp[0] = '/'; /* `\` the slash will be converted later. */ } #endif /* NOTE(@jesterKing): push slashes into unix mode - strings entering this part are * potentially messed up: having both back- and forward slashes. * Here we push into one conform direction, and at the end we * push them into the system specific dir. This ensures uniformity * of paths and solving some problems (and prevent potential future ones). * * NOTE(@elubie): For UNC paths the first characters containing the UNC prefix * shouldn't be switched as we need to distinguish them from * paths relative to the `.blend` file. */ BLI_string_replace_char(tmp + BLI_path_unc_prefix_len(tmp), '\\', '/'); /* Paths starting with `//` will get the blend file as their base, * this isn't standard in any OS but is used in blender all over the place. */ if (wasrelative) { const char *lslash; STRNCPY(base, basepath); /* File component is ignored, so don't bother with the trailing slash. */ BLI_path_normalize(base); lslash = BLI_path_slash_rfind(base); BLI_string_replace_char(base + BLI_path_unc_prefix_len(base), '\\', '/'); if (lslash) { /* Length up to and including last `/`. */ const int baselen = int(lslash - base) + 1; /* Use path for temp storage here, we copy back over it right away. */ BLI_strncpy(path, tmp + 2, FILE_MAX); /* Strip `//` prefix. */ memcpy(tmp, base, baselen); /* Prefix with base up to last `/`. */ BLI_strncpy(tmp + baselen, path, sizeof(tmp) - baselen); /* Append path after `//`. */ BLI_strncpy(path, tmp, FILE_MAX); /* Return as result. */ } else { /* Base doesn't seem to be a directory, ignore it and just strip `//` prefix on path. */ BLI_strncpy(path, tmp + 2, FILE_MAX); } } else { /* Base ignored. */ BLI_strncpy(path, tmp, FILE_MAX); } #ifdef WIN32 /* NOTE(@jesterking): Skip first two chars, which in case of absolute path will * be `drive:/blabla` and in case of `relpath` `//blabla/`. * So `relpath` `//` will be retained, rest will be nice and shiny WIN32 backward slashes. */ BLI_string_replace_char(path + 2, '/', '\\'); #endif /* Ensure this is after correcting for path switch. */ BLI_path_normalize(path); return wasrelative; } bool BLI_path_is_abs_from_cwd(const char *path) { bool is_abs = false; #ifdef WIN32 if (BLI_path_is_abs_win32(path)) { is_abs = true; } #else if (path[0] == '/') { is_abs = true; } #endif return is_abs; } bool BLI_path_abs_from_cwd(char *path, const size_t path_maxncpy) { BLI_string_debug_size_after_nil(path, path_maxncpy); if (!BLI_path_is_abs_from_cwd(path)) { char cwd[PATH_MAX]; /* In case the full path to the blend isn't used. */ if (BLI_current_working_dir(cwd, sizeof(cwd))) { char origpath[PATH_MAX]; STRNCPY(origpath, path); BLI_path_join(path, path_maxncpy, cwd, origpath); } else { printf("Could not get the current working directory - $PWD for an unknown reason.\n"); } return true; } return false; } #ifdef _WIN32 /** * Tries appending each of the semicolon-separated extensions in the `PATHEXT` * environment variable (Windows-only) onto `program_name` in turn until such a file is found. * Returns success/failure. */ bool BLI_path_program_extensions_add_win32(char *program_name, const size_t program_name_maxncpy) { bool retval = false; int type; type = BLI_exists(program_name); if ((type == 0) || S_ISDIR(type)) { /* Typically 3-5, ".EXE", ".BAT"... etc. */ const int ext_max = 12; const char *ext = BLI_getenv("PATHEXT"); if (ext) { const int program_name_len = strlen(program_name); char *filename = static_cast(alloca(program_name_len + ext_max)); char *filename_ext; const char *ext_next; /* Null terminated in the loop. */ memcpy(filename, program_name, program_name_len); filename_ext = filename + program_name_len; do { int ext_len; ext_next = strchr(ext, ';'); ext_len = ext_next ? ((ext_next++) - ext) : strlen(ext); if (LIKELY(ext_len < ext_max)) { memcpy(filename_ext, ext, ext_len); filename_ext[ext_len] = '\0'; type = BLI_exists(filename); if (type && (!S_ISDIR(type))) { retval = true; BLI_strncpy(program_name, filename, program_name_maxncpy); break; } } } while ((ext = ext_next)); } } else { retval = true; } return retval; } #endif /* WIN32 */ bool BLI_path_program_search(char *program_filepath, const size_t program_filepath_maxncpy, const char *program_name) { BLI_string_debug_size(program_filepath, program_filepath_maxncpy); const char *path; bool retval = false; #ifdef _WIN32 const char separator = ';'; #else const char separator = ':'; #endif path = BLI_getenv("PATH"); if (path) { char filepath_test[PATH_MAX]; const char *temp; do { temp = strchr(path, separator); if (temp) { memcpy(filepath_test, path, temp - path); filepath_test[temp - path] = 0; path = temp + 1; } else { STRNCPY(filepath_test, path); } BLI_path_append(filepath_test, program_filepath_maxncpy, program_name); if ( #ifdef _WIN32 BLI_path_program_extensions_add_win32(filepath_test, sizeof(filepath_test)) #else BLI_exists(filepath_test) #endif ) { BLI_strncpy(program_filepath, filepath_test, program_filepath_maxncpy); retval = true; break; } } while (temp); } if (retval == false) { *program_filepath = '\0'; } return retval; } void BLI_setenv(const char *env, const char *val) { #if (defined(_WIN32) || defined(_WIN64)) /* MS-Windows. */ uputenv(env, val); #else /* Linux/macOS/BSD */ if (val) { setenv(env, val, 1); } else { unsetenv(env); } #endif } void BLI_setenv_if_new(const char *env, const char *val) { if (BLI_getenv(env) == nullptr) { BLI_setenv(env, val); } } const char *BLI_getenv(const char *env) { #ifdef _MSC_VER const char *result = nullptr; /* 32767 is the maximum size of the environment variable on windows, * reserve one more character for the zero terminator. */ static wchar_t buffer[32768]; wchar_t *env_16 = alloc_utf16_from_8(env, 0); if (env_16) { if (GetEnvironmentVariableW(env_16, buffer, ARRAY_SIZE(buffer))) { char *res_utf8 = alloc_utf_8_from_16(buffer, 0); /* Make sure the result is valid, and will fit into our temporary storage buffer. */ if (res_utf8) { if (strlen(res_utf8) + 1 < sizeof(buffer)) { /* We are re-using the utf16 buffer here, since allocating a second static buffer to * contain the UTF8 version to return would be wasteful. */ memcpy(buffer, res_utf8, strlen(res_utf8) + 1); result = (const char *)buffer; } free(res_utf8); } } } return result; #else return getenv(env); #endif } static bool path_extension_check_ex(const char *path, const size_t path_len, const char *ext, const size_t ext_len) { BLI_assert(strlen(path) == path_len); BLI_assert(strlen(ext) == ext_len); return (((path_len == 0 || ext_len == 0 || ext_len >= path_len) == 0) && (BLI_strcasecmp(ext, path + path_len - ext_len) == 0)); } bool BLI_path_extension_check(const char *path, const char *ext) { return path_extension_check_ex(path, strlen(path), ext, strlen(ext)); } bool BLI_path_extension_check_n(const char *path, ...) { const size_t path_len = strlen(path); va_list args; const char *ext; bool ret = false; va_start(args, path); while ((ext = (const char *)va_arg(args, void *))) { if (path_extension_check_ex(path, path_len, ext, strlen(ext))) { ret = true; break; } } va_end(args); return ret; } bool BLI_path_extension_check_array(const char *path, const char **ext_array) { const size_t path_len = strlen(path); int i = 0; while (ext_array[i]) { if (path_extension_check_ex(path, path_len, ext_array[i], strlen(ext_array[i]))) { return true; } i++; } return false; } bool BLI_path_extension_check_glob(const char *path, const char *ext_fnmatch) { const char *ext_step = ext_fnmatch; char pattern[16]; while (ext_step[0]) { const char *ext_next; size_t len_ext; if ((ext_next = strchr(ext_step, ';'))) { len_ext = ext_next - ext_step + 1; BLI_strncpy(pattern, ext_step, (len_ext > sizeof(pattern)) ? sizeof(pattern) : len_ext); } else { len_ext = STRNCPY_RLEN(pattern, ext_step); } if (fnmatch(pattern, path, FNM_CASEFOLD) == 0) { return true; } ext_step += len_ext; } return false; } bool BLI_path_extension_glob_validate(char *ext_fnmatch) { bool only_wildcards = false; for (size_t i = strlen(ext_fnmatch); i-- > 0;) { if (ext_fnmatch[i] == ';') { /* Group separator, we truncate here if we only had wildcards so far. * Otherwise, all is sound and fine. */ if (only_wildcards) { ext_fnmatch[i] = '\0'; return true; } return false; } if (!ELEM(ext_fnmatch[i], '?', '*')) { /* Non-wildcard char, we can break here and consider the pattern valid. */ return false; } /* So far, only wildcards in last group of the pattern. */ only_wildcards = true; } /* Only one group in the pattern, so even if its only made of wildcard(s), * it is assumed valid. */ return false; } bool BLI_path_extension_replace(char *path, size_t path_maxncpy, const char *ext) { BLI_string_debug_size_after_nil(path, path_maxncpy); char *path_ext = (char *)BLI_path_extension_or_end(path); const size_t ext_len = strlen(ext); if ((path_ext - path) + ext_len >= path_maxncpy) { return false; } memcpy(path_ext, ext, ext_len + 1); return true; } bool BLI_path_extension_strip(char *path) { char *path_ext = (char *)BLI_path_extension(path); if (path_ext == nullptr) { return false; } *path_ext = '\0'; return true; } bool BLI_path_extension_ensure(char *path, size_t path_maxncpy, const char *ext) { BLI_string_debug_size_after_nil(path, path_maxncpy); /* First check the extension is already there. * If `path_ext` is the end of the string this is simply checking if `ext` is also empty. */ const char *path_ext = BLI_path_extension_or_end(path); if (STREQ(path_ext, ext)) { return true; } const size_t path_len = strlen(path); const size_t ext_len = strlen(ext); int64_t a; for (a = path_len - 1; a >= 0; a--) { if (path[a] == '.') { path[a] = '\0'; } else { break; } } a++; if (a + ext_len >= path_maxncpy) { return false; } memcpy(path + a, ext, ext_len + 1); return true; } bool BLI_path_filename_ensure(char *filepath, size_t filepath_maxncpy, const char *filename) { BLI_string_debug_size_after_nil(filepath, filepath_maxncpy); char *c = (char *)BLI_path_basename(filepath); const size_t filename_size = strlen(filename) + 1; if (filename_size <= filepath_maxncpy - (c - filepath)) { memcpy(c, filename, filename_size); return true; } return false; } void BLI_path_split_dir_file(const char *filepath, char *dir, const size_t dir_maxncpy, char *file, const size_t file_maxncpy) { BLI_string_debug_size(dir, dir_maxncpy); BLI_string_debug_size(file, file_maxncpy); const char *basename = BLI_path_basename(filepath); if (basename != filepath) { const size_t dir_size = (basename - filepath) + 1; BLI_strncpy(dir, filepath, std::min(dir_maxncpy, dir_size)); } else { dir[0] = '\0'; } BLI_strncpy(file, basename, file_maxncpy); } void BLI_path_split_dir_part(const char *filepath, char *dir, const size_t dir_maxncpy) { BLI_string_debug_size(dir, dir_maxncpy); const char *basename = BLI_path_basename(filepath); if (basename != filepath) { const size_t dir_size = (basename - filepath) + 1; BLI_strncpy(dir, filepath, std::min(dir_maxncpy, dir_size)); } else { dir[0] = '\0'; } } void BLI_path_split_file_part(const char *filepath, char *file, const size_t file_maxncpy) { BLI_string_debug_size(file, file_maxncpy); const char *basename = BLI_path_basename(filepath); BLI_strncpy(file, basename, file_maxncpy); } const char *BLI_path_extension_or_end(const char *filepath) { /* NOTE(@ideasman42): Skip the extension when there are no preceding non-extension characters in * the file name. This ignores extensions at the beginning of a string or directly after a slash. * Only using trailing extension characters has the advantage that stripping the extension * never leads to a blank string (which can't be used as a file path). * Matches Python's `os.path.splitext`. */ const char *ext = nullptr; bool has_non_ext = false; const char *c = filepath; for (; *c; c++) { switch (*c) { case '.': { if (has_non_ext) { ext = c; } break; } case SEP: case ALTSEP: { ext = nullptr; has_non_ext = false; break; } default: { has_non_ext = true; break; } } } if (ext) { return ext; } BLI_assert(*c == '\0'); return c; } const char *BLI_path_extension(const char *filepath) { const char *ext = BLI_path_extension_or_end(filepath); return *ext ? ext : nullptr; } size_t BLI_path_append(char *__restrict dst, const size_t dst_maxncpy, const char *__restrict file) { /* Slash ensure uses #BLI_string_debug_size */ int dst_len = BLI_path_slash_ensure(dst, dst_maxncpy); if (dst_len + 1 < dst_maxncpy) { dst_len += BLI_strncpy_rlen(dst + dst_len, file, dst_maxncpy - dst_len); } return dst_len; } size_t BLI_path_append_dir(char *__restrict dst, const size_t dst_maxncpy, const char *__restrict dir) { size_t dst_len = BLI_path_append(dst, dst_maxncpy, dir); return BLI_path_slash_ensure_ex(dst, dst_maxncpy, dst_len); } size_t BLI_path_join_array(char *__restrict dst, const size_t dst_maxncpy, const char *path_array[], const int path_array_num) { BLI_assert(path_array_num > 0); BLI_string_debug_size(dst, dst_maxncpy); if (UNLIKELY(dst_maxncpy == 0)) { return 0; } const char *path = path_array[0]; const size_t dst_last = dst_maxncpy - 1; size_t ofs = BLI_strncpy_rlen(dst, path, dst_maxncpy); if (ofs == dst_last) { return ofs; } #ifdef WIN32 /* Special case `//` for relative paths, don't use separator #SEP * as this has a special meaning on both WIN32 & UNIX. * Without this check joining `"//", "path"`. results in `"//\path"`. */ if (ofs != 0) { size_t i; for (i = 0; i < ofs; i++) { if (dst[i] != '/') { break; } } if (i == ofs) { /* All slashes, keep them as-is, and join the remaining path array. */ return path_array_num > 1 ? BLI_path_join_array( dst + ofs, dst_maxncpy - ofs, &path_array[1], path_array_num - 1) : ofs; } } #endif /* Remove trailing slashes, unless there are *only* trailing slashes * (allow `//` or `//some_path` as the first argument). */ bool has_trailing_slash = false; if (ofs != 0) { size_t len = ofs; while ((len != 0) && BLI_path_slash_is_native_compat(path[len - 1])) { len -= 1; } if (len != 0) { ofs = len; } has_trailing_slash = (path[len] != '\0'); } for (int path_index = 1; path_index < path_array_num; path_index++) { path = path_array[path_index]; has_trailing_slash = false; const char *path_init = path; while (BLI_path_slash_is_native_compat(path[0])) { path++; } size_t len = strlen(path); if (len != 0) { while ((len != 0) && BLI_path_slash_is_native_compat(path[len - 1])) { len -= 1; } if (len != 0) { /* The very first path may have a slash at the end. */ if (ofs && !BLI_path_slash_is_native_compat(dst[ofs - 1])) { 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); } } if (has_trailing_slash) { if ((ofs != dst_last) && (ofs != 0) && !BLI_path_slash_is_native_compat(dst[ofs - 1])) { dst[ofs++] = SEP; } } BLI_assert(ofs <= dst_last); dst[ofs] = '\0'; return ofs; } const char *BLI_path_basename(const char *path) { const char *const filename = BLI_path_slash_rfind(path); return filename ? filename + 1 : path; } static bool path_name_at_index_forward(const char *__restrict path, const int index, int *__restrict r_offset, int *__restrict r_len) { BLI_assert(index >= 0); int index_step = 0; int prev = -1; int i = 0; while (true) { const char c = path[i]; if ((c == '\0') || BLI_path_slash_is_native_compat(c)) { if (prev + 1 != i) { prev += 1; /* Skip `/./` (behave as if they don't exist). */ if (!((i - prev == 1) && (prev != 0) && (path[prev] == '.'))) { if (index_step == index) { *r_offset = prev; *r_len = i - prev; return true; } index_step += 1; } } if (c == '\0') { break; } prev = i; } i += 1; } return false; } static bool path_name_at_index_backward(const char *__restrict path, const int index, int *__restrict r_offset, int *__restrict r_len) { /* Negative number, reverse where -1 is the last element. */ BLI_assert(index < 0); int index_step = -1; int prev = strlen(path); int i = prev - 1; while (true) { const char c = i >= 0 ? path[i] : '\0'; if ((c == '\0') || BLI_path_slash_is_native_compat(c)) { if (prev - 1 != i) { i += 1; /* Skip `/./` (behave as if they don't exist). */ if (!((prev - i == 1) && (i != 0) && (path[i] == '.'))) { if (index_step == index) { *r_offset = i; *r_len = prev - i; return true; } index_step -= 1; } } if (c == '\0') { break; } prev = i; } i -= 1; } return false; } bool BLI_path_name_at_index(const char *__restrict path, const int index, int *__restrict r_offset, int *__restrict r_len) { return (index >= 0) ? path_name_at_index_forward(path, index, r_offset, r_len) : path_name_at_index_backward(path, index, r_offset, r_len); } bool BLI_path_contains(const char *container_path, const char *containee_path) { char container_native[PATH_MAX]; char containee_native[PATH_MAX]; /* Keep space for a trailing slash. If the path is truncated by this, the containee path is * longer than #PATH_MAX and the result is ill-defined. */ BLI_strncpy(container_native, container_path, PATH_MAX - 1); STRNCPY(containee_native, containee_path); BLI_path_slash_native(container_native); BLI_path_slash_native(containee_native); BLI_path_normalize(container_native); BLI_path_normalize(containee_native); #ifdef WIN32 BLI_str_tolower_ascii(container_native, PATH_MAX); BLI_str_tolower_ascii(containee_native, PATH_MAX); #endif if (STREQ(container_native, containee_native)) { /* The paths are equal, they contain each other. */ return true; } /* Add a trailing slash to prevent same-prefix directories from matching. * e.g. `/some/path` doesn't contain `/some/path_lib`. */ BLI_path_slash_ensure(container_native, sizeof(container_native)); return BLI_str_startswith(containee_native, container_native); } const char *BLI_path_slash_find(const char *path) { const char *const ffslash = strchr(path, '/'); const char *const fbslash = strchr(path, '\\'); if (!ffslash) { return fbslash; } if (!fbslash) { return ffslash; } return (ffslash < fbslash) ? ffslash : fbslash; } const char *BLI_path_slash_rfind(const char *path) { const char *const lfslash = strrchr(path, '/'); const char *const lbslash = strrchr(path, '\\'); if (!lfslash) { return lbslash; } if (!lbslash) { return lfslash; } return (lfslash > lbslash) ? lfslash : lbslash; } int BLI_path_slash_ensure_ex(char *path, size_t path_maxncpy, size_t path_len) { BLI_string_debug_size_after_nil(path, path_maxncpy); BLI_assert(strlen(path) == path_len); BLI_assert(path_len < path_maxncpy); if (path_len == 0 || !BLI_path_slash_is_native_compat(path[path_len - 1])) { /* Avoid unlikely buffer overflow. */ if (path_len + 1 < path_maxncpy) { path[path_len++] = SEP; path[path_len] = '\0'; } } return path_len; } int BLI_path_slash_ensure(char *path, size_t path_maxncpy) { return BLI_path_slash_ensure_ex(path, path_maxncpy, strlen(path)); } void BLI_path_slash_rstrip(char *path) { int len = strlen(path); while (len) { if (BLI_path_slash_is_native_compat(path[len - 1])) { path[len - 1] = '\0'; len--; } else { break; } } } const char *BLI_path_slash_skip(const char *path) { /* This accounts for a null byte too. */ while (BLI_path_slash_is_native_compat(*path)) { path++; } return path; } void BLI_path_slash_native(char *path) { #ifdef WIN32 if (path && BLI_strnlen(path, 3) > 2) { BLI_string_replace_char(path + 2, ALTSEP, SEP); } #else BLI_string_replace_char(path + BLI_path_unc_prefix_len(path), ALTSEP, SEP); #endif } int BLI_path_cmp_normalized(const char *p1, const char *p2) { BLI_assert_msg(!BLI_path_is_rel(p1) && !BLI_path_is_rel(p2), "Paths arguments must be absolute"); /* Normalize the paths so we can compare them. */ char norm_p1_buf[256]; char norm_p2_buf[256]; const size_t p1_size = strlen(p1) + 1; const size_t p2_size = strlen(p2) + 1; char *norm_p1 = (p1_size <= sizeof(norm_p1_buf)) ? norm_p1_buf : MEM_calloc_arrayN(p1_size, __func__); char *norm_p2 = (p2_size <= sizeof(norm_p2_buf)) ? norm_p2_buf : MEM_calloc_arrayN(p2_size, __func__); memcpy(norm_p1, p1, p1_size); memcpy(norm_p2, p2, p2_size); BLI_path_slash_native(norm_p1); BLI_path_slash_native(norm_p2); /* One of the paths ending with a slash does not make them different, strip both. */ BLI_path_slash_rstrip(norm_p1); BLI_path_slash_rstrip(norm_p2); BLI_path_normalize(norm_p1); BLI_path_normalize(norm_p2); const int result = BLI_path_cmp(norm_p1, norm_p2); if (norm_p1 != norm_p1_buf) { MEM_freeN(norm_p1); } if (norm_p2 != norm_p2_buf) { MEM_freeN(norm_p2); } return result; } bool BLI_path_has_hidden_component(const char *path) { bool component_start = true; char cur_char = path[0]; char prev_char = '\0'; while (cur_char != '\0') { char next_char = path[1]; /* If we're at a start of path component, current is '.' * and next one is not '.', end or separator: hidden. */ if (component_start && cur_char == '.') { if (!ELEM(path[1], '.', '\0', '/', '\\')) { return true; } } component_start = ELEM(cur_char, '/', '\\'); /* Separator, and previous was tilde: hidden. */ if (component_start && prev_char == '~') { return true; } path++; prev_char = cur_char; cur_char = next_char; } /* Was a tilde right at end of path: hidden. */ if (prev_char == '~') { return true; } /* Nothing was hidden. */ return false; }