/* Deletes current item from the trash. */ static KHandlerResponse delete_current(menu_data_t *m) { int ret; io_args_t args = { .arg1.path = trash_list[m->pos].trash_name, .cancellation.hook = &ui_cancellation_hook, }; ioe_errlst_init(&args.result.errors); ui_cancellation_enable(); ret = ior_rm(&args); ui_cancellation_disable(); if(ret != 0) { char *const errors = ioe_errlst_to_str(&args.result.errors); ioe_errlst_free(&args.result.errors); show_error_msg("File deletion error", errors); free(errors); return KHR_UNHANDLED; } ioe_errlst_free(&args.result.errors); remove_current_item(m->state); return KHR_REFRESH_WINDOW; } /* Implementation of cancellation hook for I/O unit. */ static int ui_cancellation_hook(void *arg) { return ui_cancellation_requested(); }
static void test_file_is_moved(void) { create_empty_file("binary-data"); { io_args_t args = { .arg1.src = "binary-data", .arg2.dst = "moved-binary-data", }; assert_int_equal(0, ior_mv(&args)); } assert_false(access("binary-data", F_OK) == 0); assert_int_equal(0, access("moved-binary-data", F_OK)); remove("moved-binary-data"); } static void test_empty_directory_is_moved(void) { create_empty_dir("empty-dir"); { io_args_t args = { .arg1.src = "empty-dir", .arg2.dst = "moved-empty-dir", }; assert_int_equal(0, ior_mv(&args)); } assert_false(is_dir("empty-dir")); assert_true(is_dir("moved-empty-dir")); { io_args_t args = { .arg1.path = "moved-empty-dir", }; assert_int_equal(0, iop_rmdir(&args)); } } static void test_non_empty_directory_is_moved(void) { create_non_empty_dir("non-empty-dir", "a-file"); { io_args_t args = { .arg1.src = "non-empty-dir", .arg2.dst = "moved-non-empty-dir", }; assert_int_equal(0, ior_mv(&args)); } assert_int_equal(0, access("moved-non-empty-dir/a-file", F_OK)); { io_args_t args = { .arg1.path = "moved-non-empty-dir", }; assert_int_equal(0, ior_rm(&args)); } } static void test_empty_nested_directory_is_moved(void) { create_empty_nested_dir("non-empty-dir", "empty-nested-dir"); { io_args_t args = { .arg1.src = "non-empty-dir", .arg2.dst = "moved-non-empty-dir", }; assert_int_equal(0, ior_mv(&args)); } assert_true(is_dir("moved-non-empty-dir/empty-nested-dir")); { io_args_t args = { .arg1.path = "moved-non-empty-dir", }; assert_int_equal(0, ior_rm(&args)); } } static void test_non_empty_nested_directory_is_moved(void) { create_non_empty_nested_dir("non-empty-dir", "nested-dir", "a-file"); { io_args_t args = { .arg1.src = "non-empty-dir", .arg2.dst = "moved-non-empty-dir", }; assert_int_equal(0, ior_mv(&args)); } assert_false(access("non-empty-dir/nested-dir/a-file", F_OK) == 0); assert_int_equal(0, access("moved-non-empty-dir/nested-dir/a-file", F_OK)); { io_args_t args = { .arg1.path = "moved-non-empty-dir", }; assert_int_equal(0, ior_rm(&args)); } } static void test_fails_to_overwrite_file_by_default(void) { create_empty_file("a-file"); { io_args_t args = { .arg1.src = "../read/two-lines", .arg2.dst = "a-file", }; assert_false(ior_mv(&args) == 0); } { io_args_t args = { .arg1.path = "a-file", }; assert_int_equal(0, iop_rmfile(&args)); } } static void test_fails_to_overwrite_dir_by_default(void) { create_empty_dir("empty-dir"); { io_args_t args = { .arg1.src = "../read", .arg2.dst = "empty-dir", }; assert_false(ior_mv(&args) == 0); } { io_args_t args = { .arg1.path = "empty-dir", }; assert_int_equal(0, iop_rmdir(&args)); } } static void test_overwrites_file_when_asked(void) { create_empty_file("a-file"); { io_args_t args = { .arg1.src = "../read/two-lines", .arg2.dst = "two-lines", }; assert_int_equal(0, iop_cp(&args)); } { io_args_t args = { .arg1.src = "two-lines", .arg2.dst = "a-file", .arg3.crs = IO_CRS_REPLACE_FILES, }; assert_int_equal(0, ior_mv(&args)); } { io_args_t args = { .arg1.path = "a-file", }; assert_int_equal(0, iop_rmfile(&args)); } } static void test_overwrites_dir_when_asked(void) { create_empty_dir("dir"); { io_args_t args = { .arg1.src = "../read", .arg2.dst = "read", }; assert_int_equal(0, ior_cp(&args)); } { io_args_t args = { .arg1.src = "read", .arg2.dst = "dir", .arg3.crs = IO_CRS_REPLACE_ALL, }; assert_int_equal(0, ior_mv(&args)); } { io_args_t args = { .arg1.path = "dir", }; assert_false(iop_rmdir(&args) == 0); } { io_args_t args = { .arg1.path = "dir", }; assert_int_equal(0, ior_rm(&args)); } } static void test_appending_fails_for_directories(void) { create_empty_dir("dir"); { io_args_t args = { .arg1.src = "../read", .arg2.dst = "read", }; assert_int_equal(0, ior_cp(&args)); } { io_args_t args = { .arg1.src = "read", .arg2.dst = "dir", .arg3.crs = IO_CRS_APPEND_TO_FILES, }; assert_false(ior_mv(&args) == 0); } { io_args_t args = { .arg1.path = "dir", }; assert_int_equal(0, iop_rmdir(&args)); } { io_args_t args = { .arg1.path = "read", }; assert_int_equal(0, ior_rm(&args)); } } static void test_appending_works_for_files(void) { uint64_t size; { io_args_t args = { .arg1.src = "../read/two-lines", .arg2.dst = "two-lines", }; assert_int_equal(0, iop_cp(&args)); } size = get_file_size("two-lines"); { io_args_t args = { .arg1.src = "../read/two-lines", .arg2.dst = "two-lines2", }; assert_int_equal(0, iop_cp(&args)); } { io_args_t args = { .arg1.src = "two-lines2", .arg2.dst = "two-lines", .arg3.crs = IO_CRS_APPEND_TO_FILES, }; assert_int_equal(0, ior_mv(&args)); } assert_int_equal(size, get_file_size("two-lines")); { io_args_t args = { .arg1.path = "two-lines", }; assert_int_equal(0, iop_rmfile(&args)); } } static void test_directories_can_be_merged(void) { create_empty_dir("first"); assert_int_equal(0, chdir("first")); create_empty_file("first-file"); assert_int_equal(0, chdir("..")); create_empty_dir("second"); assert_int_equal(0, chdir("second")); create_empty_file("second-file"); assert_int_equal(0, chdir("..")); { io_args_t args = { .arg1.src = "first", .arg2.dst = "second", .arg3.crs = IO_CRS_REPLACE_FILES, }; assert_int_equal(0, ior_mv(&args)); } assert_int_equal(0, access("second/second-file", F_OK)); assert_int_equal(0, access("second/first-file", F_OK)); { io_args_t args = { .arg1.path = "first", }; assert_int_equal(0, ior_rm(&args)); } { io_args_t args = { .arg1.path = "second", }; assert_int_equal(0, ior_rm(&args)); } } static void test_fails_to_move_directory_inside_itself(void) { create_empty_dir("empty-dir"); { io_args_t args = { .arg1.src = "empty-dir", .arg2.dst = "empty-dir/empty-dir-copy", }; assert_false(ior_mv(&args) == 0); } { io_args_t args = { .arg1.path = "empty-dir", }; assert_int_equal(0, iop_rmdir(&args)); } } #ifndef _WIN32 static void test_symlink_is_symlink_after_move(void) { { io_args_t args = { .arg1.path = "../read/two-lines", .arg2.target = "sym-link", }; assert_int_equal(0, iop_ln(&args)); } assert_true(is_symlink("sym-link")); { io_args_t args = { .arg1.src = "sym-link", .arg2.dst = "moved-sym-link", }; assert_int_equal(0, ior_mv(&args)); } assert_true(!is_symlink("sym-link")); assert_true(is_symlink("moved-sym-link")); { io_args_t args = { .arg1.path = "moved-sym-link", }; assert_int_equal(0, iop_rmfile(&args)); } } #endif void mv_tests(void) { test_fixture_start(); run_test(test_file_is_moved); run_test(test_empty_directory_is_moved); run_test(test_non_empty_directory_is_moved); run_test(test_empty_nested_directory_is_moved); run_test(test_non_empty_nested_directory_is_moved); run_test(test_fails_to_overwrite_file_by_default); run_test(test_fails_to_overwrite_dir_by_default); run_test(test_overwrites_file_when_asked); run_test(test_overwrites_dir_when_asked); run_test(test_appending_fails_for_directories); run_test(test_appending_works_for_files); run_test(test_directories_can_be_merged); run_test(test_fails_to_move_directory_inside_itself); #ifndef _WIN32 /* Creating symbolic links on Windows requires administrator rights. */ run_test(test_symlink_is_symlink_after_move); #endif test_fixture_end(); }
static void create_directory(const char path[], const char root[], int create_parents) { assert_int_equal(-1, access(path, F_OK)); { io_args_t args = { .arg1.path = path, .arg2.process_parents = create_parents, .arg3.mode = 0700, }; assert_int_equal(0, iop_mkdir(&args)); } assert_int_equal(0, access(path, F_OK)); assert_true(is_dir(path)); { io_args_t args = { .arg1.path = root, }; assert_int_equal(0, ior_rm(&args)); } } TEST(child_dir_is_not_created) { assert_int_equal(-1, access(NESTED_DIR_NAME, F_OK)); { io_args_t args = { .arg1.path = NESTED_DIR_NAME, .arg2.process_parents = 0, }; assert_false(iop_mkdir(&args) == 0); } assert_int_equal(-1, access(NESTED_DIR_NAME, F_OK)); } TEST(permissions_are_taken_into_account, IF(has_unix_permissions)) { { io_args_t args = { .arg1.path = DIR_NAME, .arg2.process_parents = 0, .arg3.mode = 0000, }; assert_int_equal(0, iop_mkdir(&args)); } { io_args_t args = { .arg1.path = NESTED_DIR_NAME, .arg2.process_parents = 0, }; assert_false(iop_mkdir(&args) == 0); } { io_args_t args = { .arg1.path = DIR_NAME, }; assert_int_equal(0, iop_rmdir(&args)); } } TEST(permissions_are_taken_into_account_for_the_most_nested_only, IF(has_unix_permissions)) { { io_args_t args = { .arg1.path = NESTED_DIR_NAME, .arg2.process_parents = 1, .arg3.mode = 0000, }; assert_int_equal(0, iop_mkdir(&args)); } { io_args_t args = { .arg1.path = DIR_NAME "/dir", .arg2.process_parents = 0, .arg3.mode = 0755, }; assert_int_equal(0, iop_mkdir(&args)); } { io_args_t args = { .arg1.path = NESTED_DIR_NAME "/dir", .arg2.process_parents = 0, .arg3.mode = 0755, }; assert_false(iop_mkdir(&args) == 0); } { io_args_t args = { .arg1.path = NESTED_DIR_NAME, }; assert_int_equal(0, iop_rmdir(&args)); } { io_args_t args = { .arg1.path = DIR_NAME "/dir", }; assert_int_equal(0, iop_rmdir(&args)); } { io_args_t args = { .arg1.path = DIR_NAME, }; assert_int_equal(0, iop_rmdir(&args)); } } static int has_unix_permissions(void) { #if defined(_WIN32) || defined(__CYGWIN__) return 0; #else return 1; #endif }
/* Implementation of traverse() visitor for subtree removal. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult rm_visitor(const char full_path[], VisitAction action, void *param) { io_args_t *const rm_args = param; VisitResult result = VR_OK; if(rm_args->cancellable && ui_cancellation_requested()) { return VR_CANCELLED; } switch(action) { case VA_DIR_ENTER: /* Do nothing, directories are removed on leaving them. */ result = VR_OK; break; case VA_FILE: { io_args_t args = { .arg1.path = full_path, .cancellable = rm_args->cancellable, .estim = rm_args->estim, .result = rm_args->result, }; result = (iop_rmfile(&args) == 0) ? VR_OK : VR_ERROR; rm_args->result = args.result; break; } case VA_DIR_LEAVE: { io_args_t args = { .arg1.path = full_path, .cancellable = rm_args->cancellable, .estim = rm_args->estim, .result = rm_args->result, }; result = (iop_rmdir(&args) == 0) ? VR_OK : VR_ERROR; rm_args->result = args.result; break; } } return result; } int ior_cp(io_args_t *const args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; if(is_in_subtree(dst, src)) { (void)ioe_errlst_append(&args->result.errors, src, IO_ERR_UNKNOWN, "Can't copy parent path into subpath"); return 1; } if(args->arg3.crs == IO_CRS_REPLACE_ALL) { io_args_t rm_args = { .arg1.path = dst, .cancellable = args->cancellable, .estim = args->estim, .result = args->result, }; const int result = ior_rm(&rm_args); args->result = rm_args.result; if(result != 0) { if(!args->cancellable || !ui_cancellation_requested()) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return result; } } return traverse(src, &cp_visitor, args); } /* Implementation of traverse() visitor for subtree copying. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult cp_visitor(const char full_path[], VisitAction action, void *param) { return cp_mv_visitor(full_path, action, param, 1); } int ior_mv(io_args_t *const args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; const IoCrs crs = args->arg3.crs; const io_confirm confirm = args->confirm; if(crs == IO_CRS_FAIL && path_exists(dst, DEREF) && !is_case_change(src, dst)) { (void)ioe_errlst_append(&args->result.errors, dst, EEXIST, strerror(EEXIST)); return 1; } if(crs == IO_CRS_APPEND_TO_FILES) { if(!is_file(src)) { (void)ioe_errlst_append(&args->result.errors, src, EISDIR, strerror(EISDIR)); return 1; } if(!is_file(dst)) { (void)ioe_errlst_append(&args->result.errors, dst, EISDIR, strerror(EISDIR)); return 1; } } else if(crs == IO_CRS_REPLACE_FILES && path_exists(dst, DEREF)) { /* Ask user whether to overwrite destination file. */ if(confirm != NULL && !confirm(args, src, dst)) { return 0; } } if(os_rename(src, dst) == 0) { ioeta_update(args->estim, src, dst, 1, 0); return 0; } switch(errno) { case EXDEV: { int result = ior_cp(args); if(result == 0) { io_args_t rm_args = { .arg1.path = src, .cancellable = args->cancellable, .estim = args->estim, .result = args->result, }; /* Disable progress reporting for this "secondary" operation. */ const int silent = ioeta_silent_on(rm_args.estim); result = ior_rm(&rm_args); args->result = rm_args.result; ioeta_silent_set(rm_args.estim, silent); } return result; } case EISDIR: case ENOTEMPTY: case EEXIST: #ifdef _WIN32 /* For MXE builds running in Wine. */ case EPERM: case EACCES: #endif if(crs == IO_CRS_REPLACE_ALL) { int error; io_args_t rm_args = { .arg1.path = dst, .cancellable = args->cancellable, .estim = args->estim, .result = args->result, }; /* Ask user whether to overwrite destination file. */ if(confirm != NULL && !confirm(args, src, dst)) { return 0; } error = ior_rm(&rm_args); args->result = rm_args.result; if(error != 0) { if(!args->cancellable || !ui_cancellation_requested()) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return error; } if(os_rename(src, dst) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, strerror(errno)); return 1; } return 0; } else if(crs == IO_CRS_REPLACE_FILES || (!has_atomic_file_replace() && crs == IO_CRS_APPEND_TO_FILES)) { if(!has_atomic_file_replace() && is_file(dst)) { io_args_t rm_args = { .arg1.path = dst, .cancellable = args->cancellable, .estim = args->estim, .result = args->result, }; const int error = iop_rmfile(&rm_args); args->result = rm_args.result; if(error != 0) { if(!args->cancellable || !ui_cancellation_requested()) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return error; } } return traverse(src, &mv_visitor, args); } /* Break is intentionally omitted. */ default: (void)ioe_errlst_append(&args->result.errors, src, errno, strerror(errno)); return errno; } } /* Checks that path points to a file or symbolic link. Returns non-zero if so, * otherwise zero is returned. */ static int is_file(const char path[]) { return !is_dir(path) || (is_symlink(path) && get_symlink_type(path) != SLT_UNKNOWN); } /* Implementation of traverse() visitor for subtree moving. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult mv_visitor(const char full_path[], VisitAction action, void *param) { return cp_mv_visitor(full_path, action, param, 0); } /* Generic implementation of traverse() visitor for subtree copying/moving. * Returns 0 on success, otherwise non-zero is returned. */ static VisitResult cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp) { io_args_t *const cp_args = param; const char *dst_full_path; char *free_me = NULL; VisitResult result = VR_OK; const char *rel_part; if(cp_args->cancellable && ui_cancellation_requested()) { return VR_CANCELLED; } /* TODO: come up with something better than this. */ rel_part = full_path + strlen(cp_args->arg1.src); dst_full_path = (rel_part[0] == '\0') ? cp_args->arg2.dst : (free_me = format_str("%s/%s", cp_args->arg2.dst, rel_part)); switch(action) { case VA_DIR_ENTER: if(cp_args->arg3.crs != IO_CRS_REPLACE_FILES || !is_dir(dst_full_path)) { io_args_t args = { .arg1.path = dst_full_path, /* Temporary fake rights so we can add files to the directory. */ .arg3.mode = 0700, .cancellable = cp_args->cancellable, .estim = cp_args->estim, .result = cp_args->result, }; result = (iop_mkdir(&args) == 0) ? VR_OK : VR_ERROR; cp_args->result = args.result; } break; case VA_FILE: { io_args_t args = { .arg1.src = full_path, .arg2.dst = dst_full_path, .arg3.crs = cp_args->arg3.crs, /* It's safe to always use fast file cloning on moving files. */ .arg4.fast_file_cloning = cp ? cp_args->arg4.fast_file_cloning : 1, .cancellable = cp_args->cancellable, .confirm = cp_args->confirm, .estim = cp_args->estim, .result = cp_args->result, }; result = ((cp ? iop_cp(&args) : ior_mv(&args)) == 0) ? VR_OK : VR_ERROR; cp_args->result = args.result; break; } case VA_DIR_LEAVE: { struct stat st; if(cp_args->arg3.crs == IO_CRS_REPLACE_FILES && !cp) { io_args_t rm_args = { .arg1.path = full_path, .cancellable = cp_args->cancellable, .estim = cp_args->estim, .result = cp_args->result, }; result = (iop_rmdir(&rm_args) == 0) ? VR_OK : VR_ERROR; } else if(os_stat(full_path, &st) == 0) { result = (os_chmod(dst_full_path, st.st_mode & 07777) == 0) ? VR_OK : VR_ERROR; if(result == VR_ERROR) { (void)ioe_errlst_append(&cp_args->result.errors, dst_full_path, errno, strerror(errno)); } clone_timestamps(dst_full_path, full_path, &st); } else { (void)ioe_errlst_append(&cp_args->result.errors, full_path, errno, strerror(errno)); result = VR_ERROR; } break; } } free(free_me); return result; }