Skip to content

Commit 7e2af0d

Browse files
committed
fix: strip trailing slash from argument before path operations (closes #450)
A trailing slash on a symlink caused rename() to fail with ENOTDIR because the kernel dereferenced the symlink. Strip any trailing slash from the argument early, before check_pathname_state, lstat, resolve_path, and rename, so "foo/" is treated identically to "foo". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Fixes #450
1 parent 5b36817 commit 7e2af0d

2 files changed

Lines changed: 19 additions & 7 deletions

File tree

src/main.c

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,15 @@ remove_to_waste(const int argc,
289289
char tmp[PATH_MAX];
290290
sn_check(snprintf(tmp, sizeof(tmp), "%s", argv[file_arg]), sizeof(tmp));
291291

292+
/* strip trailing slash so symlinks and dirs with "foo/" behave like "foo" */
293+
size_t tmp_len = strlen(tmp);
294+
if (tmp_len > 1 && tmp[tmp_len - 1] == '/')
295+
tmp[tmp_len - 1] = '\0';
296+
297+
/* keep a copy before basename() may clobber tmp */
298+
char arg[PATH_MAX];
299+
sn_check(snprintf(arg, sizeof(arg), "%s", tmp), sizeof(arg));
300+
292301
// If basename() is given an empty string, it returns '.'
293302
st_target.base_name = basename(tmp);
294303
if (isdotdir(st_target.base_name))
@@ -309,7 +318,7 @@ damage of 5000 hp. You feel satisfied.\n"));
309318
continue;
310319
}
311320

312-
int p_state = check_pathname_state(argv[file_arg]);
321+
int p_state = check_pathname_state(arg);
313322
if (p_state != EEXIST)
314323
{
315324
if (p_state == ENOENT)
@@ -319,10 +328,10 @@ damage of 5000 hp. You feel satisfied.\n"));
319328
}
320329

321330
struct stat st_file_arg;
322-
if (!lstat(argv[file_arg], &st_file_arg))
331+
if (!lstat(arg, &st_file_arg))
323332
{
324333
st_target.dev_num = st_file_arg.st_dev;
325-
st_target.real_path = resolve_path(argv[file_arg], st_target.base_name);
334+
st_target.real_path = resolve_path(arg, st_target.base_name);
326335
if (st_target.real_path == NULL)
327336
{
328337
n_err++;
@@ -411,7 +420,7 @@ damage of 5000 hp. You feel satisfied.\n"));
411420
int r_result = 0;
412421
if (cli_user_options->want_dry_run == false)
413422
{
414-
const char *src = argv[file_arg];
423+
const char *src = arg;
415424
const char *dst = st_target.waste_dest_name;
416425

417426
if (waste_curr->dev_num != st_target.dev_num)

test/test_restore.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,15 @@ if [ -n "$(command -v Xvfb)" ] && ! grep -q "DISABLE_CURSES" "$MESON_BUILD_ROOT/
146146
fi
147147
fi
148148

149-
# Regression test: rmw file/ (trailing slash on regular file) must not move it
149+
# Regression test: rmw file/ (trailing slash on regular file) must move it
150150
echo "$SEPARATOR"
151-
echo "Trailing slash on regular file: must be rejected cleanly"
151+
echo "Trailing slash on regular file: must be moved and restorable"
152152
cd "${RMW_FAKE_HOME}"
153153
touch trailing_slash_file.txt
154-
${RMW_TEST_CMD_STRING} trailing_slash_file.txt/ || true
154+
${RMW_TEST_CMD_STRING} trailing_slash_file.txt/
155+
test ! -f "${RMW_FAKE_HOME}/trailing_slash_file.txt"
156+
test -f "${PRIMARY_WASTE_DIR}/files/trailing_slash_file.txt"
157+
${RMW_TEST_CMD_STRING} -u
155158
test -f "${RMW_FAKE_HOME}/trailing_slash_file.txt"
156159

157160
# Regression test: rmw dir/ (trailing slash) then restore must not create dir/dir

0 commit comments

Comments
 (0)