Skip to content

Commit fc66ab9

Browse files
authored
Fix rename's handling of trailing slashes on filenames. (#289)
* Fix `rename`'s handling of trailing slashes on filenames. When the source path ends in a slash, but names a regular file, `rename` should still fail, even though it ignores the slash when the source names a directory. * Only fix trailing slashes on Unix. And fix trailing slashes by re-appending a slash.
1 parent fb86919 commit fc66ab9

File tree

4 files changed

+73
-8
lines changed

4 files changed

+73
-8
lines changed

cap-primitives/src/fs/via_parent/rename.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use super::open_parent;
2+
#[cfg(unix)]
3+
use crate::fs::{append_dir_suffix, path_has_trailing_slash};
24
use crate::fs::{rename_unchecked, strip_dir_suffix, MaybeOwnedFile};
35
use std::path::Path;
46
use std::{fs, io};
@@ -15,13 +17,30 @@ pub(crate) fn rename(
1517
let new_start = MaybeOwnedFile::borrowed(new_start);
1618

1719
// As a special case, `rename` ignores a trailing slash rather than treating
18-
// it as equivalent to a trailing slash-dot, so strip any trailing slashes.
20+
// it as equivalent to a trailing slash-dot, so strip any trailing slashes
21+
// for the purposes of `open_parent`.
22+
//
23+
// And on Unix, remember whether the source started with a slash so that we
24+
// can still fail if it is and the source is a regular file.
25+
#[cfg(unix)]
26+
let old_starts_with_slash = path_has_trailing_slash(old_path);
1927
let old_path = strip_dir_suffix(old_path);
2028
let new_path = strip_dir_suffix(new_path);
2129

2230
let (old_dir, old_basename) = open_parent(old_start, &*old_path)?;
2331
let (new_dir, new_basename) = open_parent(new_start, &*new_path)?;
2432

33+
// On Unix, re-append a slash if needed.
34+
#[cfg(unix)]
35+
let concat;
36+
#[cfg(unix)]
37+
let old_basename = if old_starts_with_slash {
38+
concat = append_dir_suffix(old_basename.to_owned().into());
39+
concat.as_os_str()
40+
} else {
41+
old_basename
42+
};
43+
2544
rename_unchecked(
2645
&old_dir,
2746
old_basename.as_ref(),

cap-primitives/src/rustix/fs/dir_utils.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use crate::fs::OpenOptions;
22
use ambient_authority::AmbientAuthority;
33
use rustix::fs::OFlags;
4-
use std::ffi::OsStr;
4+
use std::ffi::{OsStr, OsString};
55
use std::ops::Deref;
66
#[cfg(unix)]
7-
use std::os::unix::{ffi::OsStrExt, fs::OpenOptionsExt};
7+
use std::os::unix::{
8+
ffi::{OsStrExt, OsStringExt},
9+
fs::OpenOptionsExt,
10+
};
811
#[cfg(target_os = "wasi")]
9-
use std::os::wasi::{ffi::OsStrExt, fs::OpenOptionsExt};
10-
use std::path::Path;
11-
#[cfg(racy_asserts)]
12-
use std::{ffi::OsString, os::unix::ffi::OsStringExt, path::PathBuf};
12+
use std::os::wasi::{
13+
ffi::{OsStrExt, OsStringExt},
14+
fs::OpenOptionsExt,
15+
};
16+
use std::path::{Path, PathBuf};
1317
use std::{fs, io};
1418

1519
/// Rust's `Path` implicitly strips redundant slashes, however they aren't
@@ -53,7 +57,6 @@ pub(crate) fn path_has_trailing_slash(path: &Path) -> bool {
5357

5458
/// Append a trailing `/`. This can be used to require that the given `path`
5559
/// names a directory.
56-
#[cfg(racy_asserts)]
5760
pub(crate) fn append_dir_suffix(path: PathBuf) -> PathBuf {
5861
let mut bytes = path.into_os_string().into_vec();
5962
bytes.push(b'/');
@@ -158,6 +161,10 @@ fn strip_dir_suffix_tests() {
158161
assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo"));
159162
assert_eq!(&*strip_dir_suffix(Path::new("/")), Path::new("/"));
160163
assert_eq!(&*strip_dir_suffix(Path::new("//")), Path::new("/"));
164+
assert_eq!(&*strip_dir_suffix(Path::new("/.")), Path::new("/."));
165+
assert_eq!(&*strip_dir_suffix(Path::new("//.")), Path::new("/."));
166+
assert_eq!(&*strip_dir_suffix(Path::new(".")), Path::new("."));
167+
assert_eq!(&*strip_dir_suffix(Path::new("foo/.")), Path::new("foo/."));
161168
}
162169

163170
#[test]

tests/fs_additional.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,47 @@ fn rename_slashdots() {
178178
check!(tmpdir.rename("dir", &tmpdir, "dir"));
179179
check!(tmpdir.rename("dir", &tmpdir, "dir/"));
180180
check!(tmpdir.rename("dir/", &tmpdir, "dir"));
181+
check!(tmpdir.rename("dir/", &tmpdir, "dir/"));
181182

182183
// TODO: Platform-specific error code.
183184
error_contains!(tmpdir.rename("dir", &tmpdir, "dir/."), "");
184185
error_contains!(tmpdir.rename("dir/.", &tmpdir, "dir"), "");
185186
}
186187

188+
#[test]
189+
#[cfg_attr(windows, ignore)] // TODO investigate why this one is failing
190+
fn rename_slashdots_ambient() {
191+
let dir = tempfile::tempdir().unwrap();
192+
193+
check!(std::fs::create_dir_all(dir.path().join("dir")));
194+
check!(std::fs::rename(
195+
dir.path().join("dir"),
196+
dir.path().join("dir")
197+
));
198+
check!(std::fs::rename(
199+
dir.path().join("dir"),
200+
dir.path().join("dir/")
201+
));
202+
check!(std::fs::rename(
203+
dir.path().join("dir/"),
204+
dir.path().join("dir")
205+
));
206+
check!(std::fs::rename(
207+
dir.path().join("dir/"),
208+
dir.path().join("dir/")
209+
));
210+
211+
// TODO: Platform-specific error code.
212+
error_contains!(
213+
std::fs::rename(dir.path().join("dir"), dir.path().join("dir/.")),
214+
""
215+
);
216+
error_contains!(
217+
std::fs::rename(dir.path().join("dir/."), dir.path().join("dir")),
218+
""
219+
);
220+
}
221+
187222
#[test]
188223
fn optionally_recursive_mkdir() {
189224
let tmpdir = tmpdir();

tests/rename.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ fn rename_basics() {
137137
tmpdir.rename("/..", &tmpdir, "nope.txt"),
138138
"a path led outside of the filesystem"
139139
);
140+
error_contains!(
141+
tmpdir.rename("file.txt/", &tmpdir, "nope.txt"),
142+
"Not a directory"
143+
);
140144

141145
/* // TODO: Platform-specific error code.
142146
error!(

0 commit comments

Comments
 (0)