Skip to content

Commit b20063f

Browse files
committed
feat(mv): add symlink handling in mv operations
- Modified HardlinkGroupScanner to skip symlinks using symlink_metadata, as hardlink preservation does not apply to them - Added copy_symlink functions for Unix, Windows, and other platforms to copy symlinks without dereferencing - Updated copy_dir_contents_recursive to detect and copy symlinks, including verbose output, preventing dereferencing during directory moves
1 parent dca5a1b commit b20063f

File tree

2 files changed

+86
-18
lines changed

2 files changed

+86
-18
lines changed

src/uu/mv/src/hardlink.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,23 @@ impl HardlinkGroupScanner {
217217
fn scan_single_path(&mut self, path: &Path) -> io::Result<()> {
218218
use std::os::unix::fs::MetadataExt;
219219

220-
if path.is_dir() {
220+
let metadata = path.symlink_metadata()?;
221+
let file_type = metadata.file_type();
222+
223+
if file_type.is_symlink() {
224+
// Hardlink preservation does not apply to symlinks.
225+
return Ok(());
226+
}
227+
228+
if file_type.is_dir() {
221229
// Recursively scan directory contents
222230
self.scan_directory_recursive(path)?;
223-
} else {
224-
let metadata = path.metadata()?;
225-
if metadata.nlink() > 1 {
226-
let key = (metadata.dev(), metadata.ino());
227-
self.hardlink_groups
228-
.entry(key)
229-
.or_default()
230-
.push(path.to_path_buf());
231-
}
231+
} else if metadata.nlink() > 1 {
232+
let key = (metadata.dev(), metadata.ino());
233+
self.hardlink_groups
234+
.entry(key)
235+
.or_default()
236+
.push(path.to_path_buf());
232237
}
233238
Ok(())
234239
}
@@ -242,14 +247,19 @@ impl HardlinkGroupScanner {
242247
let entry = entry?;
243248
let path = entry.path();
244249

245-
if path.is_dir() {
250+
let metadata = path.symlink_metadata()?;
251+
let file_type = metadata.file_type();
252+
253+
if file_type.is_symlink() {
254+
// Skip symlinks to avoid following targets (including dangling links).
255+
continue;
256+
}
257+
258+
if file_type.is_dir() {
246259
self.scan_directory_recursive(&path)?;
247-
} else {
248-
let metadata = path.metadata()?;
249-
if metadata.nlink() > 1 {
250-
let key = (metadata.dev(), metadata.ino());
251-
self.hardlink_groups.entry(key).or_default().push(path);
252-
}
260+
} else if metadata.nlink() > 1 {
261+
let key = (metadata.dev(), metadata.ino());
262+
self.hardlink_groups.entry(key).or_default().push(path);
253263
}
254264
}
255265
Ok(())

src/uu/mv/src/mv.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,45 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
984984
))
985985
}
986986

987+
/// Copy the given symlink to the given destination without dereferencing.
988+
/// On Windows, dangling symlinks return an error.
989+
#[cfg(unix)]
990+
fn copy_symlink(from: &Path, to: &Path) -> io::Result<()> {
991+
let from_meta = from.symlink_metadata()?;
992+
let path_symlink_points_to = fs::read_link(from)?;
993+
unix::fs::symlink(path_symlink_points_to, to).and_then(|_| {
994+
try_preserve_ownership(&from_meta, to, false);
995+
Ok(())
996+
})
997+
}
998+
999+
#[cfg(windows)]
1000+
fn copy_symlink(from: &Path, to: &Path) -> io::Result<()> {
1001+
let path_symlink_points_to = fs::read_link(from)?;
1002+
if path_symlink_points_to.exists() {
1003+
if path_symlink_points_to.is_dir() {
1004+
windows::fs::symlink_dir(&path_symlink_points_to, to)?;
1005+
} else {
1006+
windows::fs::symlink_file(&path_symlink_points_to, to)?;
1007+
}
1008+
Ok(())
1009+
} else {
1010+
Err(io::Error::new(
1011+
io::ErrorKind::NotFound,
1012+
translate!("mv-error-dangling-symlink"),
1013+
))
1014+
}
1015+
}
1016+
1017+
#[cfg(not(any(windows, unix)))]
1018+
fn copy_symlink(from: &Path, to: &Path) -> io::Result<()> {
1019+
let _ = (from, to);
1020+
Err(io::Error::new(
1021+
io::ErrorKind::Other,
1022+
translate!("mv-error-no-symlink-support"),
1023+
))
1024+
}
1025+
9871026
fn rename_dir_fallback(
9881027
from: &Path,
9891028
to: &Path,
@@ -1108,7 +1147,26 @@ fn copy_dir_contents_recursive(
11081147
pb.set_message(from_path.to_string_lossy().to_string());
11091148
}
11101149

1111-
if from_path.is_dir() {
1150+
let entry_type = entry.file_type()?;
1151+
1152+
if entry_type.is_symlink() {
1153+
copy_symlink(&from_path, &to_path)?;
1154+
1155+
// Print verbose message for symlink
1156+
if verbose {
1157+
let message = translate!(
1158+
"mv-verbose-renamed",
1159+
"from" => from_path.quote(),
1160+
"to" => to_path.quote()
1161+
);
1162+
match display_manager {
1163+
Some(pb) => pb.suspend(|| {
1164+
println!("{message}");
1165+
}),
1166+
None => println!("{message}"),
1167+
}
1168+
}
1169+
} else if entry_type.is_dir() {
11121170
// Recursively copy subdirectory
11131171
fs::create_dir_all(&to_path)?;
11141172

0 commit comments

Comments
 (0)