xaizek / vifm (License: GPLv2+) (since 2018-12-07)
Vifm is a file manager with curses interface, which provides Vi[m]-like environment for managing objects within file systems, extended with some useful ideas from mutt.
Commit 3490c1e5c5a478f512c4a2716f56bdcf99d5d3a1

Detect and handle symlink cycles on deep copying
Detection is done by keeping track of parents and checking whether the
same directory gets visited again.

Handling is done by resorting to copying symbolic links that cause
cycles as symbolic links.
Author: xaizek
Author date (UTC): 2026-02-14 14:12
Committer name: xaizek
Committer date (UTC): 2026-02-19 09:45
Parent(s): 7bca47e521ffcb1d12f97bea12e10e5bb3684c6c
Signing key: 99DC5E4DB05F6BE2
Tree: 634329213add5703da24ba61a809d8558959fe28
File Lines added Lines deleted
src/io/ior.c 7 5
src/io/private/traverser.c 116 6
tests/ioeta/calculate.c 47 0
tests/ior/cp-symlinks.c 55 0
File src/io/ior.c changed (mode: 100644) (index 9cb067895..4a230de86)
... ... static int is_file(const char path[]);
53 53 static VisitResult mv_visitor(const char full_path[], VisitAction action, static VisitResult mv_visitor(const char full_path[], VisitAction action,
54 54 int deep, void *param); int deep, void *param);
55 55 static VisitResult cp_mv_visitor(const char full_path[], VisitAction action, static VisitResult cp_mv_visitor(const char full_path[], VisitAction action,
56 void *param, int cp);
56 void *param, int cp, int deep);
57 57 static VisitResult vr_from_io_res(IoRes result); static VisitResult vr_from_io_res(IoRes result);
58 58
59 59 IoRes IoRes
 
... ... ior_cp(io_args_t *args)
163 163 static VisitResult static VisitResult
164 164 cp_visitor(const char full_path[], VisitAction action, int deep, void *param) cp_visitor(const char full_path[], VisitAction action, int deep, void *param)
165 165 { {
166 return cp_mv_visitor(full_path, action, param, 1);
166 return cp_mv_visitor(full_path, action, param, /*cp=*/1, deep);
167 167 } }
168 168
169 169 IoRes IoRes
 
... ... is_file(const char path[])
378 378 static VisitResult static VisitResult
379 379 mv_visitor(const char full_path[], VisitAction action, int deep, void *param) mv_visitor(const char full_path[], VisitAction action, int deep, void *param)
380 380 { {
381 return cp_mv_visitor(full_path, action, param, 0);
381 return cp_mv_visitor(full_path, action, param, /*cp=*/0, /*deep=*/0);
382 382 } }
383 383
384 384 /* Generic implementation of traverse() visitor for subtree copying/moving. /* Generic implementation of traverse() visitor for subtree copying/moving.
385 385 * Returns 0 on success, otherwise non-zero is returned. */ * Returns 0 on success, otherwise non-zero is returned. */
386 386 static VisitResult static VisitResult
387 cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp)
387 cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp,
388 int deep)
388 389 { {
389 390 io_args_t *const cp_args = param; io_args_t *const cp_args = param;
390 391 const char *dst_full_path; const char *dst_full_path;
 
... ... cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp)
433 434 /* It's safe to always use fast file cloning on moving files. */ /* It's safe to always use fast file cloning on moving files. */
434 435 .arg4.fast_file_cloning = cp ? cp_args->arg4.fast_file_cloning : 1, .arg4.fast_file_cloning = cp ? cp_args->arg4.fast_file_cloning : 1,
435 436 .arg4.data_sync = cp_args->arg4.data_sync, .arg4.data_sync = cp_args->arg4.data_sync,
436 .arg4.deep_copying = cp ? cp_args->arg4.deep_copying : 0,
437 /* Deep copying may be suppressed for links that can't be copied. */
438 .arg4.deep_copying = cp ? deep && cp_args->arg4.deep_copying : 0,
437 439
438 440 .cancellation = cp_args->cancellation, .cancellation = cp_args->cancellation,
439 441 .confirm = cp_args->confirm, .confirm = cp_args->confirm,
File src/io/private/traverser.c changed (mode: 100644) (index d9e2526c9..3a2e15397)
19 19 #include "traverser.h" #include "traverser.h"
20 20
21 21 #include <stddef.h> /* NULL */ #include <stddef.h> /* NULL */
22 #include <stdio.h> /* snprintf() */
22 23 #include <stdlib.h> /* free() */ #include <stdlib.h> /* free() */
23 24
24 25 #include "../../compat/os.h" #include "../../compat/os.h"
25 26 #include "../../utils/fs.h" #include "../../utils/fs.h"
26 27 #include "../../utils/path.h" #include "../../utils/path.h"
27 28 #include "../../utils/str.h" #include "../../utils/str.h"
29 #include "../../utils/trie.h"
28 30
29 static VisitResult traverse_subtree(const char path[], int deep,
30 subtree_visitor visitor, void *param);
31 /* Data used by traverse_subtree(). */
32 typedef struct
33 {
34 subtree_visitor visitor; /* Callback to invoke for directories and files. */
35 void *param; /* Parameter to pass to the visitor. */
36 trie_t *parents; /* "List" of parents to detect symlink cycles. NULL
37 if traversal isn't deep. */
38 int deep; /* Whether symlinks in source path are resolved. */
39 }
40 traverse_data_t;
41
42 static VisitResult traverse_subtree(const char path[], traverse_data_t *data);
43 static int add_parent(traverse_data_t *data, const char path[],
44 const struct stat *st);
45 static int remove_parent(traverse_data_t *data, const char path[],
46 const struct stat *st);
31 47
32 48 IoRes IoRes
33 49 traverse(const char path[], int deep, subtree_visitor visitor, void *param) traverse(const char path[], int deep, subtree_visitor visitor, void *param)
 
... ... traverse(const char path[], int deep, subtree_visitor visitor, void *param)
44 60 } }
45 61 else else
46 62 { {
47 visit_result = traverse_subtree(path, deep, visitor, param);
63 traverse_data_t data = {
64 .deep = deep,
65 .visitor = visitor,
66 .param = param,
67 };
68
69 if(deep)
70 {
71 data.parents = trie_create(/*free_func=*/NULL);
72 if(data.parents == NULL)
73 {
74 return IO_RES_FAILED;
75 }
76 }
77
78 visit_result = traverse_subtree(path, &data);
79 trie_free(data.parents);
48 80 } }
49 81
50 82 switch(visit_result) switch(visit_result)
 
... ... traverse(const char path[], int deep, subtree_visitor visitor, void *param)
58 90
59 91 /* A generic subtree traversing. Returns status of visitation. */ /* A generic subtree traversing. Returns status of visitation. */
60 92 static VisitResult static VisitResult
61 traverse_subtree(const char path[], int deep, subtree_visitor visitor,
62 void *param)
93 traverse_subtree(const char path[], traverse_data_t *data)
63 94 { {
95 int deep = data->deep;
96 subtree_visitor visitor = data->visitor;
97 void *param = data->param;
98
99 struct stat dir_st;
100 if(deep)
101 {
102 /* Save the result of stat() both as a tiny optimization and to make sure we
103 * remove the same entry even if path ends up pointing to a different
104 * location at the end of the function. */
105 if(os_stat(path, &dir_st) != 0)
106 {
107 return VR_ERROR;
108 }
109
110 if(add_parent(data, path, &dir_st) != 0)
111 {
112 /* Copy this symbolic link to a directory which makes a cycle as a
113 * file. */
114 return visitor(path, VA_FILE, /*deep=*/0, param);
115 }
116 }
117
64 118 DIR *dir; DIR *dir;
65 119 struct dirent *d; struct dirent *d;
66 120 VisitResult enter_result; VisitResult enter_result;
 
... ... traverse_subtree(const char path[], int deep, subtree_visitor visitor,
92 146 /* Optionally treat symbolic links to directories as files as well. */ /* Optionally treat symbolic links to directories as files as well. */
93 147 if(deep ? is_dirent_targets_dir(full_path, d) : entry_is_dir(full_path, d)) if(deep ? is_dirent_targets_dir(full_path, d) : entry_is_dir(full_path, d))
94 148 { {
95 result = traverse_subtree(full_path, deep, visitor, param);
149 result = traverse_subtree(full_path, data);
96 150 } }
97 151 else else
98 152 { {
 
... ... traverse_subtree(const char path[], int deep, subtree_visitor visitor,
112 166 result = visitor(path, VA_DIR_LEAVE, deep, param); result = visitor(path, VA_DIR_LEAVE, deep, param);
113 167 } }
114 168
169 if(deep && remove_parent(data, path, &dir_st) != 0)
170 {
171 result = VR_ERROR;
172 }
173
115 174 return result; return result;
116 175 } }
117 176
177 /* Adds path to the list of active parents. Returns zero unless hit an
178 * insertion error or the parent is already present. */
179 static int
180 add_parent(traverse_data_t *data, const char path[], const struct stat *st)
181 {
182 static char mark;
183
184 #ifndef _WIN32
185 char key[40];
186 snprintf(key, sizeof(key), "%llx:%llx", (unsigned long long)st->st_dev,
187 (unsigned long long)st->st_ino);
188 #else
189 const char *key = path;
190 #endif
191
192 void *node_data;
193 if(trie_get(data->parents, key, &node_data) == 0 && node_data != NULL)
194 {
195 return 1;
196 }
197
198 /* The value of the data doesn't matter, just setting it to non-NULL. */
199 if(trie_set(data->parents, key, &mark) < 0)
200 {
201 return 1;
202 }
203
204 return 0;
205 }
206
207 /* Removes path from the list of active parents. Returns zero if the parent was
208 * in the list and got successfully removed. */
209 static int
210 remove_parent(traverse_data_t *data, const char path[], const struct stat *st)
211 {
212 #ifndef _WIN32
213 char key[40];
214 snprintf(key, sizeof(key), "%llx:%llx", (unsigned long long)st->st_dev,
215 (unsigned long long)st->st_ino);
216 #else
217 const char *key = path;
218 #endif
219
220 if(trie_set(data->parents, key, /*data=*/NULL) <= 0)
221 {
222 return 1;
223 }
224
225 return 0;
226 }
227
118 228 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
119 229 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
File tests/ioeta/calculate.c changed (mode: 100644) (index b899f5696..38af3cf38)
... ... TEST(symlink_to_files_can_be_followed, IF(not_windows))
162 162 ioeta_free(estim); ioeta_free(estim);
163 163 } }
164 164
165 TEST(dir_link_loop_is_avoided, IF(not_windows))
166 {
167 ioeta_estim_t *const estim = ioeta_alloc(NULL, no_cancellation);
168
169 {
170 io_args_t args = {
171 .arg1.path = SANDBOX_PATH "/dir",
172 .arg3.mode = 0700,
173 };
174 ioe_errlst_init(&args.result.errors);
175
176 assert_int_equal(IO_RES_SUCCEEDED, iop_mkdir(&args));
177 assert_int_equal(0, args.result.errors.error_count);
178 }
179
180 {
181 io_args_t args = {
182 .arg1.path = ".",
183 .arg2.target = SANDBOX_PATH "/dir/parent",
184 };
185 assert_int_equal(IO_RES_SUCCEEDED, iop_ln(&args));
186 }
187
188 ioeta_calculate(estim, SANDBOX_PATH "/dir", /*shallow=*/0, /*deep=*/1);
189
190 assert_int_equal(2, estim->total_items);
191 assert_int_equal(0, estim->current_item);
192 assert_int_equal(0, estim->total_bytes);
193 assert_int_equal(0, estim->current_byte);
194
195 {
196 io_args_t args = {
197 .arg1.path = SANDBOX_PATH "/dir/parent",
198 };
199 assert_int_equal(IO_RES_SUCCEEDED, iop_rmfile(&args));
200 }
201
202 {
203 io_args_t args = {
204 .arg1.path = SANDBOX_PATH "/dir",
205 };
206 assert_int_equal(IO_RES_SUCCEEDED, iop_rmdir(&args));
207 }
208
209 ioeta_free(estim);
210 }
211
165 212 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
166 213 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
File tests/ior/cp-symlinks.c changed (mode: 100644) (index cdf3de7d5..9a198a083)
... ... TEST(nested_symlink_to_dir_is_dir_after_copy, IF(not_windows))
240 240 } }
241 241 } }
242 242
243 TEST(dir_link_loop_is_avoided_on_deep_copy, IF(not_windows))
244 {
245 {
246 io_args_t args = {
247 .arg1.path = SANDBOX_PATH "/dir",
248 .arg3.mode = 0700,
249 };
250 ioe_errlst_init(&args.result.errors);
251
252 assert_int_equal(IO_RES_SUCCEEDED, iop_mkdir(&args));
253 assert_int_equal(0, args.result.errors.error_count);
254 }
255 {
256 io_args_t args = {
257 .arg1.path = ".",
258 .arg2.target = SANDBOX_PATH "/dir/parent",
259 };
260 assert_int_equal(IO_RES_SUCCEEDED, iop_ln(&args));
261 }
262
263 {
264 io_args_t args = {
265 .arg1.src = SANDBOX_PATH "/dir",
266 .arg2.dst = SANDBOX_PATH "/dir-copy",
267 .arg4.deep_copying = 1,
268 };
269 ioe_errlst_init(&args.result.errors);
270
271 assert_int_equal(IO_RES_SUCCEEDED, ior_cp(&args));
272 assert_int_equal(0, args.result.errors.error_count);
273 }
274
275 {
276 io_args_t args = {
277 .arg1.path = SANDBOX_PATH "/dir",
278 };
279 ioe_errlst_init(&args.result.errors);
280
281 assert_int_equal(IO_RES_SUCCEEDED, ior_rm(&args));
282 assert_int_equal(0, args.result.errors.error_count);
283 }
284 {
285 io_args_t args = {
286 .arg1.path = SANDBOX_PATH "/dir-copy/parent",
287 };
288 assert_int_equal(IO_RES_SUCCEEDED, iop_rmfile(&args));
289 }
290 {
291 io_args_t args = {
292 .arg1.path = SANDBOX_PATH "/dir-copy",
293 };
294 assert_int_equal(IO_RES_SUCCEEDED, iop_rmdir(&args));
295 }
296 }
297
243 298 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
244 299 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/vifm

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/vifm

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master