BLI: factor out BLI_path_has_hidden_component, fix it and speed it up

Factors out utility function "does the path contain any hidden file/folder
components?" function out of innards of UI code (edit_file,
is_hidden_dot_filename) into a BLI function BLI_path_has_hidden_component
and then:
- Adds unit test coverage to it, which uncovered some inconsistencies
- Fix the behavior inconsistencies:
  - A path component that is just a dot (.), was not considered hidden.
    Unless it was the first folder component (now this is fixed).
  - A path component that ended in a tilde (~) was considered hidden.
    Unless it was the first folder component (now this is fixed).
- Speedup the function by not doing several recursive scans over the
  string; instead all the logic is done in a single string scan.

Synthetic HasHiddenComponents_Performance test: 6.0s -> 1.1s for 50M calls
on Mac M1 Max.

More real world test (setup as in #120494): out of whole build_catalog_tree
time, the time taken by BLI_path_has_hidden_component drops from 37% down
to 28%

Pull Request: https://projects.blender.org/blender/blender/pulls/120541
This commit is contained in:
Aras Pranckevicius
2024-04-16 16:51:31 +02:00
committed by Aras Pranckevicius
parent 5b7bc88571
commit 9962c50b54
4 changed files with 143 additions and 42 deletions

View File

@@ -312,6 +312,17 @@ const char *BLI_path_basename(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED
/** \} */
/* -------------------------------------------------------------------- */
/** \name Path Filtering Utilities
* \{ */
/**
* Return true if any path component starts with a dot.
*/
bool BLI_path_has_hidden_component(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT;
/** \} */
/* -------------------------------------------------------------------- */
/** \name Path Append
* \{ */

View File

@@ -2013,3 +2013,39 @@ int BLI_path_cmp_normalized(const char *p1, const char *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;
}

View File

@@ -11,6 +11,8 @@
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#define DO_PERF_TESTS 0
/* -------------------------------------------------------------------- */
/** \name Local Utilities
* \{ */
@@ -1317,3 +1319,95 @@ TEST(path_util, Contains_Windows_case_insensitive)
#endif /* WIN32 */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Tests for: #BLI_path_has_hidden_component
* \{ */
TEST(path_util, HasHiddenComponents)
{
/* No hidden components: */
EXPECT_FALSE(BLI_path_has_hidden_component(""));
EXPECT_FALSE(BLI_path_has_hidden_component(" "));
EXPECT_FALSE(BLI_path_has_hidden_component(".."));
EXPECT_FALSE(BLI_path_has_hidden_component("../.."));
EXPECT_FALSE(BLI_path_has_hidden_component("..\\.."));
EXPECT_FALSE(BLI_path_has_hidden_component("a/../b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\..\\b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a"));
EXPECT_FALSE(BLI_path_has_hidden_component("a~b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a/b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a/b/c"));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\b\\c"));
EXPECT_FALSE(BLI_path_has_hidden_component("/a/b"));
EXPECT_FALSE(BLI_path_has_hidden_component("C:/a/b"));
EXPECT_FALSE(BLI_path_has_hidden_component("C:/\a\\b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a.txt"));
EXPECT_FALSE(BLI_path_has_hidden_component("a/b.txt"));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\b.txt"));
EXPECT_FALSE(BLI_path_has_hidden_component("/"));
EXPECT_FALSE(BLI_path_has_hidden_component("\\"));
EXPECT_FALSE(BLI_path_has_hidden_component("a."));
EXPECT_FALSE(BLI_path_has_hidden_component("a./b."));
/* Note: path component that is just a dot is not considered hidden. */
EXPECT_FALSE(BLI_path_has_hidden_component("."));
EXPECT_FALSE(BLI_path_has_hidden_component("a/."));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\."));
EXPECT_FALSE(BLI_path_has_hidden_component("a/./b"));
EXPECT_FALSE(BLI_path_has_hidden_component("a\\.\\b"));
EXPECT_FALSE(BLI_path_has_hidden_component("./a"));
EXPECT_FALSE(BLI_path_has_hidden_component(".\\a"));
/* Does contain hidden components: */
EXPECT_TRUE(BLI_path_has_hidden_component(".a"));
EXPECT_TRUE(BLI_path_has_hidden_component(".a.b"));
EXPECT_TRUE(BLI_path_has_hidden_component("a/.b"));
EXPECT_TRUE(BLI_path_has_hidden_component("a\\.b"));
EXPECT_TRUE(BLI_path_has_hidden_component(".a/b"));
EXPECT_TRUE(BLI_path_has_hidden_component(".a\\b"));
EXPECT_TRUE(BLI_path_has_hidden_component(".a/.b"));
EXPECT_TRUE(BLI_path_has_hidden_component(".a\\.b"));
EXPECT_TRUE(BLI_path_has_hidden_component("a/.b.c"));
EXPECT_TRUE(BLI_path_has_hidden_component("a/.b/c.txt"));
EXPECT_TRUE(BLI_path_has_hidden_component("a\\.b\\c.txt"));
/* Tilde at end of path component: considered hidden. */
EXPECT_TRUE(BLI_path_has_hidden_component("~"));
EXPECT_TRUE(BLI_path_has_hidden_component("a~"));
EXPECT_TRUE(BLI_path_has_hidden_component("a/~/c"));
EXPECT_TRUE(BLI_path_has_hidden_component("a/b~/c"));
EXPECT_TRUE(BLI_path_has_hidden_component("a\\b~\\c"));
EXPECT_TRUE(BLI_path_has_hidden_component("~/b"));
EXPECT_TRUE(BLI_path_has_hidden_component("a~/b"));
EXPECT_TRUE(BLI_path_has_hidden_component("a~\\b"));
}
#if DO_PERF_TESTS
# include "BLI_timeit.hh"
TEST(path_util, HasHiddenComponents_Performance)
{
SCOPED_TIMER(__func__);
const char *test_paths[] = {
"test.txt",
"test/a_fairly_long/path/here/shall_we/ok.txt",
"test/a_fairly_long/path/here/.with_a_hidden_component/shall_we/ok.txt",
"test/.another_path_with_hidden_component/yes.txt",
"test/another/path/with/.hidden_filename",
};
const int RUN_COUNT = 10'000'000;
int hidden = 0;
for (int i = 0; i < RUN_COUNT; i++) {
for (int j = 0; j < ARRAY_SIZE(test_paths); j++) {
hidden += BLI_path_has_hidden_component(test_paths[j]) ? 1 : 0;
}
}
EXPECT_EQ(RUN_COUNT * 3, hidden);
}
#endif
/** \} */

View File

@@ -614,46 +614,6 @@ void filelist_setsorting(FileList *filelist, const short sort, bool invert_sort)
/* ********** Filter helpers ********** */
/* True if filename is meant to be hidden, eg. starting with period. */
static bool is_hidden_dot_filename(const char *filename, const FileListInternEntry *file)
{
if (filename[0] == '.' && !ELEM(filename[1], '.', '\0')) {
return true; /* ignore .file */
}
int len = strlen(filename);
if ((len > 0) && (filename[len - 1] == '~')) {
return true; /* ignore file~ */
}
/* filename might actually be a piece of path, in which case we have to check all its parts. */
bool hidden = false;
char *sep = (char *)BLI_path_slash_rfind(filename);
if (!hidden && sep) {
char tmp_filename[FILE_MAX_LIBEXTRA];
STRNCPY(tmp_filename, filename);
sep = tmp_filename + (sep - filename);
while (sep) {
/* This happens when a path contains 'ALTSEP', '\' on Unix for e.g.
* Supporting alternate slashes in paths is a bigger task involving changes
* in many parts of the code, for now just prevent an assert, see #74579. */
#if 0
BLI_assert(sep[1] != '\0');
#endif
if (is_hidden_dot_filename(sep + 1, file)) {
hidden = true;
break;
}
*sep = '\0';
sep = (char *)BLI_path_slash_rfind(tmp_filename);
}
}
return hidden;
}
/* True if should be hidden, based on current filtering. */
static bool is_filtered_hidden(const char *filename,
const FileListFilter *filter,
@@ -675,7 +635,7 @@ static bool is_filtered_hidden(const char *filename,
#ifndef WIN32
/* Check for unix-style names starting with period. */
if ((filter->flags & FLF_HIDE_DOT) && is_hidden_dot_filename(filename, file)) {
if ((filter->flags & FLF_HIDE_DOT) && BLI_path_has_hidden_component(filename)) {
return true;
}
#endif
@@ -3131,7 +3091,7 @@ static int filelist_readjob_list_dir(FileListReadJob *job_params,
#ifndef WIN32
/* Set linux-style dot files hidden too. */
if (is_hidden_dot_filename(entry->relpath, entry)) {
if (BLI_path_has_hidden_component(entry->relpath)) {
entry->attributes |= FILE_ATTR_HIDDEN;
}
#endif