diff options
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | src/fops_rename.c | 214 | ||||
-rw-r--r-- | tests/fileops/rename_files.c | 136 | ||||
-rw-r--r-- | tests/misc/sort.c | 13 |
4 files changed, 240 insertions, 130 deletions
@@ -269,6 +269,13 @@ Windows (primarily C:\ could be reported as non-writable). Thanks to theorlangur and bitraid. + Fixed :substitute, :tr, gu* and gU* detecting a non-existent target name + conflict when rename in a custom view would produce an identically named + files in different directories. Thanks to aleksejrs. + + Fixed lack of a redraw after running :substitute, :tr, gu* or gU* in a + custom view. + 0.13-beta to 0.13 (2023-04-04) Made "withicase" and "withrcase" affect how files are sorted before diff --git a/src/fops_rename.c b/src/fops_rename.c index 8bd8754da..1ff0874c9 100644 --- a/src/fops_rename.c +++ b/src/fops_rename.c @@ -39,15 +39,6 @@ #include "fops_common.h" #include "undo.h" -/* What to do with rename candidate name (old name and new name). */ -typedef enum -{ - RA_SKIP, /* Skip rename (when new name matches the old one). */ - RA_FAIL, /* Abort renaming (status bar error was printed). */ - RA_RENAME, /* Rename this file. */ -} -RenameAction; - static void rename_file_cb(const char new_name[], void *arg); static int complete_filename_only(const char str[], void *arg); static char ** list_files_to_rename(view_t *view, int recursive, int *len); @@ -61,10 +52,10 @@ TSTATIC const char * incdec_name(const char fname[], int k); static int count_digits(int number); static const char * substitute_tr(const char name[], const char pattern[], const char sub[]); -static RenameAction check_rename(const char old_fname[], const char new_fname[], - char **dest, int ndest); +static int schedule_a_rename(dir_entry_t *entry, const char new_fname[], + int case_change, strlist_t *new_paths); static int rename_marked(view_t *view, const char desc[], const char lhs[], - const char rhs[], char **dest); + const char rhs[], char **new_paths); void fops_rename_current(view_t *view, int name_only) @@ -639,21 +630,14 @@ count_digits(int number) int fops_case(view_t *view, int to_upper) { - char **dest; - int ndest; - dir_entry_t *entry; - int save_msg; - int err; - if(!fops_view_can_be_changed(view)) { return 0; } - entry = NULL; - ndest = 0; - dest = NULL; - err = 0; + strlist_t new_paths = {}; + dir_entry_t *entry = NULL; + int err = 0; while(iter_marked_entries(view, &entry)) { const char *const old_fname = entry->name; @@ -670,38 +654,25 @@ fops_case(view_t *view, int to_upper) (void)str_to_lower(old_fname, new_fname, sizeof(new_fname)); } - if(strcmp(new_fname, old_fname) == 0) + if(schedule_a_rename(entry, new_fname, /*case_change=*/1, &new_paths) != 0) { - entry->marked = 0; - continue; - } - - if(is_in_string_array(dest, ndest, new_fname)) - { - ui_sb_errf("Name \"%s\" duplicates", new_fname); err = 1; break; } - if(path_exists(new_fname, NODEREF) && !is_case_change(new_fname, old_fname)) - { - ui_sb_errf("File \"%s\" already exists", new_fname); - err = 1; - break; - } - - ndest = add_to_string_array(&dest, ndest, new_fname); } + int save_msg; if(err) { save_msg = 1; } else { - save_msg = rename_marked(view, to_upper ? "gU" : "gu", NULL, NULL, dest); + const char *desc = (to_upper ? "gU" : "gu"); + save_msg = rename_marked(view, desc, NULL, NULL, new_paths.items); } - free_string_array(dest, ndest); + free_string_array(new_paths.items, new_paths.nitems); return save_msg; } @@ -710,18 +681,12 @@ int fops_subst(view_t *view, const char pattern[], const char sub[], int ic, int glob) { - regex_t re; - char **dest; - int ndest; - int cflags; - dir_entry_t *entry; - int err, save_msg; - if(!fops_view_can_be_changed(view)) { return 0; } + int cflags; if(ic == 0) { cflags = get_regexp_cflags(pattern); @@ -735,29 +700,29 @@ fops_subst(view_t *view, const char pattern[], const char sub[], int ic, cflags = REG_EXTENDED; } - if((err = regexp_compile(&re, pattern, cflags)) != 0) + regex_t re; + int err = regexp_compile(&re, pattern, cflags); + if(err != 0) { ui_sb_errf("Regexp error: %s", get_regexp_error(err, &re)); regfree(&re); return 1; } - entry = NULL; - ndest = 0; - dest = NULL; + strlist_t new_paths = {}; + dir_entry_t *entry = NULL; err = 0; - while(iter_marked_entries(view, &entry) && !err) + while(iter_marked_entries(view, &entry)) { - const char *new_fname; regmatch_t matches[10]; - RenameAction action; - if(regexec(&re, entry->name, ARRAY_LEN(matches), matches, 0) != 0) { + /* Regexp didn't match. */ entry->marked = 0; continue; } + const char *new_fname; if(glob && pattern[0] != '^') { new_fname = regexp_gsubst(&re, entry->name, sub, matches); @@ -767,37 +732,26 @@ fops_subst(view_t *view, const char pattern[], const char sub[], int ic, new_fname = regexp_subst(entry->name, sub, matches, NULL); } - action = check_rename(entry->name, new_fname, dest, ndest); - switch(action) + if(schedule_a_rename(entry, new_fname, /*case_change=*/0, &new_paths) != 0) { - case RA_SKIP: - entry->marked = 0; - continue; - case RA_FAIL: - err = 1; - break; - case RA_RENAME: - ndest = add_to_string_array(&dest, ndest, new_fname); - break; - - default: - assert(0 && "Unhandled rename action."); - break; + err = 1; + break; } } regfree(&re); + int save_msg; if(err) { save_msg = 1; } else { - save_msg = rename_marked(view, "s", pattern, sub, dest); + save_msg = rename_marked(view, "s", pattern, sub, new_paths.items); } - free_string_array(dest, ndest); + free_string_array(new_paths.items, new_paths.nitems); return save_msg; } @@ -805,11 +759,6 @@ fops_subst(view_t *view, const char pattern[], const char sub[], int ic, int fops_tr(view_t *view, const char from[], const char to[]) { - char **dest; - int ndest; - dir_entry_t *entry; - int err, save_msg; - assert(strlen(from) == strlen(to) && "Lengths don't match."); if(!fops_view_can_be_changed(view)) @@ -817,46 +766,30 @@ fops_tr(view_t *view, const char from[], const char to[]) return 0; } - entry = NULL; - ndest = 0; - dest = NULL; - err = 0; - while(iter_marked_entries(view, &entry) && !err) + strlist_t new_paths = {}; + dir_entry_t *entry = NULL; + int err = 0; + while(iter_marked_entries(view, &entry)) { - const char *new_fname; - RenameAction action; - - new_fname = substitute_tr(entry->name, from, to); - - action = check_rename(entry->name, new_fname, dest, ndest); - switch(action) + const char *new_fname = substitute_tr(entry->name, from, to); + if(schedule_a_rename(entry, new_fname, /*case_change=*/0, &new_paths) != 0) { - case RA_SKIP: - entry->marked = 0; - continue; - case RA_FAIL: - err = 1; - break; - case RA_RENAME: - ndest = add_to_string_array(&dest, ndest, new_fname); - break; - - default: - assert(0 && "Unhandled rename action."); - break; + err = 1; + break; } } + int save_msg; if(err) { save_msg = 1; } else { - save_msg = rename_marked(view, "t", from, to, dest); + save_msg = rename_marked(view, "t", from, to, new_paths.items); } - free_string_array(dest, ndest); + free_string_array(new_paths.items, new_paths.nitems); return save_msg; } @@ -881,49 +814,72 @@ substitute_tr(const char name[], const char pattern[], const char sub[]) return buf; } -/* Evaluates possibility of renaming old_fname to new_fname. Returns - * resolution. */ -static RenameAction -check_rename(const char old_fname[], const char new_fname[], char **dest, - int ndest) +/* Evaluates possibility of renaming a file to new_fname. If everything is + * fine, appends new name to *new_paths or unmarks an entry if no rename is + * necessary. Returns 0 on success, otherwise non-zero is returned and an + * error message is printed on the status bar. */ +static int +schedule_a_rename(dir_entry_t *entry, const char new_fname[], int case_change, + strlist_t *new_paths) { /* Compare case sensitive strings even on Windows to let user rename file * changing only case of some characters. */ - if(strcmp(old_fname, new_fname) == 0) + if(strcmp(entry->name, new_fname) == 0) { - return RA_SKIP; + entry->marked = 0; + return 0; } - if(is_in_string_array(dest, ndest, new_fname)) + char new_path[PATH_MAX + 1]; + build_path(new_path, sizeof(new_path), entry->origin, new_fname); + + if(is_in_string_array(new_paths->items, new_paths->nitems, new_path)) { ui_sb_errf("Name \"%s\" duplicates", new_fname); - return RA_FAIL; + return 1; } if(new_fname[0] == '\0') { - ui_sb_errf("Destination name of \"%s\" is empty", old_fname); - return RA_FAIL; + ui_sb_errf("Destination name of \"%s\" is empty", entry->name); + return 1; } if(contains_slash(new_fname)) { ui_sb_errf("Destination name \"%s\" contains slash", new_fname); - return RA_FAIL; + return 1; } if(path_exists(new_fname, NODEREF)) { - ui_sb_errf("File \"%s\" already exists", new_fname); - return RA_FAIL; + /* If we're changing case and target filesystem is case insensitive, this is + * not an error condition because the same file can be accessed by + * different names, otherwise it is an error. */ + if(!(case_change && !case_sensitive_paths(entry->origin))) + { + ui_sb_errf("File \"%s\" already exists", new_fname); + return 1; + } } - return RA_RENAME; + int new_size = + add_to_string_array(&new_paths->items, new_paths->nitems, new_path); + + if(new_paths->nitems == new_size) + { + show_error_msg("Memory Error", "Unable to allocate enough memory"); + ui_sb_err("Rename operation has failed"); + return 1; + } + + new_paths->nitems = new_size; + return 0; } -/* Renames marked files using corresponding entries of the dest array. lhs and - * rhs can be NULL to omit their printing (both at the same time). Returns new - * value for save_msg flag. */ +/* Renames marked files using corresponding entries of the new_paths array. lhs + * and rhs can be NULL to omit their printing (both at the same time). Returns + * new value for save_msg flag. */ static int rename_marked(view_t *view, const char desc[], const char lhs[], - const char rhs[], char **dest) + const char rhs[], char **new_paths) { int i; int nrenamed; @@ -948,7 +904,7 @@ rename_marked(view_t *view, const char desc[], const char lhs[], entry = NULL; while(iter_marked_entries(view, &entry)) { - const char *const new_fname = dest[i++]; + const char *const new_fname = after_last(new_paths[i++], '/'); if(fops_mv_file(entry->name, entry->origin, new_fname, entry->origin, OP_MOVE, 1, NULL) == 0) { @@ -958,8 +914,16 @@ rename_marked(view_t *view, const char desc[], const char lhs[], } un_group_close(); - ui_sb_msgf("%d file%s renamed", nrenamed, (nrenamed == 1) ? "" : "s"); + if(nrenamed > 0 && flist_custom_active(view)) + { + /* Custom views don't have watchers that tell them to reload and redraw a + * file list when a change like rename happens, so schedule a redraw. + * Paths should have been updated in-place by fentry_rename(). */ + ui_view_schedule_redraw(view); + } + + ui_sb_msgf("%d file%s renamed", nrenamed, (nrenamed == 1) ? "" : "s"); return 1; } diff --git a/tests/fileops/rename_files.c b/tests/fileops/rename_files.c index faaf5f406..486147d12 100644 --- a/tests/fileops/rename_files.c +++ b/tests/fileops/rename_files.c @@ -9,6 +9,8 @@ #include "../../src/cfg/config.h" #include "../../src/compat/fs_limits.h" +#include "../../src/compat/os.h" +#include "../../src/ui/statusbar.h" #include "../../src/ui/ui.h" #include "../../src/utils/fs.h" #include "../../src/utils/path.h" @@ -20,6 +22,7 @@ static void broken_link_name(const char prompt[], const char filename[], fo_prompt_cb cb, void *cb_arg, fo_complete_cmd_func complete); +static int on_case_sensitive_fs(void); static char *saved_cwd; @@ -318,8 +321,141 @@ TEST(global_substitution_of_caret_pattern) assert_success(unlink(SANDBOX_PATH "/01")); } +TEST(case_change_works, IF(on_case_sensitive_fs)) +{ + create_file(SANDBOX_PATH "/fIlE1"); + create_file(SANDBOX_PATH "/FiLe2"); + + populate_dir_list(&lwin, 0); + lwin.dir_entry[0].marked = 1; + lwin.dir_entry[1].marked = 1; + + ui_sb_msg(""); + (void)fops_case(&lwin, /*to_upper=*/0); + assert_string_equal("2 files renamed", ui_sb_last()); + + assert_failure(unlink(SANDBOX_PATH "/fIlE1")); + assert_failure(unlink(SANDBOX_PATH "/FiLe2")); + assert_success(unlink(SANDBOX_PATH "/file1")); + assert_success(unlink(SANDBOX_PATH "/file2")); +} + +TEST(case_change_detect_dups, IF(on_case_sensitive_fs)) +{ + create_file(SANDBOX_PATH "/fIlE"); + create_file(SANDBOX_PATH "/FiLe"); + + populate_dir_list(&lwin, 0); + lwin.dir_entry[0].marked = 1; + lwin.dir_entry[1].marked = 1; + + ui_sb_msg(""); + (void)fops_case(&lwin, /*to_upper=*/0); + assert_string_equal("Name \"file\" duplicates", ui_sb_last()); + + assert_success(unlink(SANDBOX_PATH "/fIlE")); + assert_success(unlink(SANDBOX_PATH "/FiLe")); + assert_failure(unlink(SANDBOX_PATH "/file")); +} + +TEST(substitution_and_idential_names_in_different_dirs) +{ + create_dir(SANDBOX_PATH "/adir"); + create_file(SANDBOX_PATH "/adir/abc"); + create_dir(SANDBOX_PATH "/bdir"); + create_file(SANDBOX_PATH "/bdir/abc"); + + flist_custom_start(&lwin, "test"); + flist_custom_add(&lwin, "adir/abc"); + flist_custom_add(&lwin, "bdir/abc"); + assert_true(flist_custom_finish(&lwin, CV_REGULAR, 0) == 0); + assert_int_equal(2, lwin.list_rows); + + lwin.dir_entry[0].marked = 1; + lwin.dir_entry[1].marked = 1; + + ui_sb_msg(""); + (void)fops_subst(&lwin, "$", "d", /*ic=*/0, /*glob=*/0); + assert_string_equal("2 files renamed", ui_sb_last()); + + assert_failure(unlink(SANDBOX_PATH "/adir/abc")); + assert_success(unlink(SANDBOX_PATH "/adir/abcd")); + assert_success(rmdir(SANDBOX_PATH "/adir")); + assert_failure(unlink(SANDBOX_PATH "/bdir/abc")); + assert_success(unlink(SANDBOX_PATH "/bdir/abcd")); + assert_success(rmdir(SANDBOX_PATH "/bdir")); +} + +TEST(translation_and_idential_names_in_different_dirs) +{ + create_dir(SANDBOX_PATH "/adir"); + create_file(SANDBOX_PATH "/adir/abc"); + create_dir(SANDBOX_PATH "/bdir"); + create_file(SANDBOX_PATH "/bdir/abc"); + + flist_custom_start(&lwin, "test"); + flist_custom_add(&lwin, "adir/abc"); + flist_custom_add(&lwin, "bdir/abc"); + assert_true(flist_custom_finish(&lwin, CV_REGULAR, 0) == 0); + assert_int_equal(2, lwin.list_rows); + + lwin.dir_entry[0].marked = 1; + lwin.dir_entry[1].marked = 1; + + ui_sb_msg(""); + (void)fops_tr(&lwin, "abc", "xyz"); + assert_string_equal("2 files renamed", ui_sb_last()); + + assert_failure(unlink(SANDBOX_PATH "/adir/abc")); + assert_success(unlink(SANDBOX_PATH "/adir/xyz")); + assert_success(rmdir(SANDBOX_PATH "/adir")); + assert_failure(unlink(SANDBOX_PATH "/bdir/abc")); + assert_success(unlink(SANDBOX_PATH "/bdir/xyz")); + assert_success(rmdir(SANDBOX_PATH "/bdir")); +} + +TEST(case_change_and_idential_names_in_different_dirs, IF(on_case_sensitive_fs)) +{ + create_dir(SANDBOX_PATH "/adir"); + create_file(SANDBOX_PATH "/adir/abc"); + create_dir(SANDBOX_PATH "/bdir"); + create_file(SANDBOX_PATH "/bdir/abc"); + + flist_custom_start(&lwin, "test"); + flist_custom_add(&lwin, "adir/abc"); + flist_custom_add(&lwin, "bdir/abc"); + assert_true(flist_custom_finish(&lwin, CV_REGULAR, 0) == 0); + assert_int_equal(2, lwin.list_rows); + + lwin.dir_entry[0].marked = 1; + lwin.dir_entry[1].marked = 1; + + ui_sb_msg(""); + (void)fops_case(&lwin, /*to_upper=*/1); + assert_string_equal("2 files renamed", ui_sb_last()); + + assert_failure(unlink(SANDBOX_PATH "/adir/abc")); + assert_success(unlink(SANDBOX_PATH "/adir/ABC")); + assert_success(rmdir(SANDBOX_PATH "/adir")); + assert_failure(unlink(SANDBOX_PATH "/bdir/abc")); + assert_success(unlink(SANDBOX_PATH "/bdir/ABC")); + assert_success(rmdir(SANDBOX_PATH "/bdir")); +} + /* No tests for custom/tree view, because control doesn't reach necessary checks * when new filenames are provided beforehand (only when user edits them). */ +/* Checks that sandbox directory is located on a file-system that allows files + * that differ only by character case. Returns non-zero if so. */ +static int +on_case_sensitive_fs(void) +{ + /* Use of SANDBOX_PATH prevents this function from being in test utilities. */ + int result = (os_mkdir(SANDBOX_PATH "/tEsT", 0700) == 0) + && (os_rmdir(SANDBOX_PATH "/test") != 0); + (void)os_rmdir(SANDBOX_PATH "/tEsT"); + return result; +} + /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set cinoptions+=t0 : */ diff --git a/tests/misc/sort.c b/tests/misc/sort.c index 7b7132032..c01612cda 100644 --- a/tests/misc/sort.c +++ b/tests/misc/sort.c @@ -1,6 +1,6 @@ #include <stic.h> -#include <unistd.h> /* chdir() rmdir() unlink() */ +#include <unistd.h> /* chdir() unlink() */ #include <stdarg.h> /* va_list va_arg() va_copy() va_end() va_start() */ #include <string.h> /* strcpy() */ @@ -21,7 +21,7 @@ do { assert_int_equal(SIGN(a), SIGN(b)); } while(0) static void set_file_list(view_t *view, FileType def_ftype, ...); -static int case_sensitive_fs(void); +static int on_case_sensitive_fs(void); SETUP_ONCE() { @@ -494,7 +494,7 @@ TEST(case_insensitive_unicode_sorting_for_exts, IF(utf8_locale)) assert_string_equal("z.รถ", lwin.dir_entry[2].name); } -TEST(custom_view_is_sorted_by_short_paths, IF(case_sensitive_fs)) +TEST(custom_view_is_sorted_by_short_paths, IF(on_case_sensitive_fs)) { make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), /*base=*/".", /*sub=*/"", /*cwd=*/NULL); @@ -581,12 +581,15 @@ set_file_list(view_t *view, FileType def_ftype, ...) va_end(aq); } +/* Checks that sandbox directory is located on a file-system that allows files + * that differ only by character case. Returns non-zero if so. */ static int -case_sensitive_fs(void) +on_case_sensitive_fs(void) { + /* Use of SANDBOX_PATH prevents this function from being in test utilities. */ int result = (os_mkdir(SANDBOX_PATH "/tEsT", 0700) == 0) && (os_rmdir(SANDBOX_PATH "/test") != 0); - (void)rmdir(SANDBOX_PATH "/tEsT"); + (void)os_rmdir(SANDBOX_PATH "/tEsT"); return result; } |