1use std::fs;
2use std::io;
3use std::path::Path;
4
5#[cfg(unix)]
6use std::os::unix::fs::MetadataExt;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BackupMode {
11 Simple,
13 Numbered,
15 Existing,
17 None,
19}
20
21#[derive(Debug, Clone)]
23pub struct MvConfig {
24 pub force: bool,
25 pub interactive: bool,
26 pub no_clobber: bool,
27 pub verbose: bool,
28 pub update: bool,
29 pub backup: Option<BackupMode>,
30 pub suffix: String,
31 pub target_directory: Option<String>,
32 pub no_target_directory: bool,
33 pub strip_trailing_slashes: bool,
34}
35
36impl Default for MvConfig {
37 fn default() -> Self {
38 Self {
39 force: false,
40 interactive: false,
41 no_clobber: false,
42 verbose: false,
43 update: false,
44 backup: None,
45 suffix: "~".to_string(),
46 target_directory: None,
47 no_target_directory: false,
48 strip_trailing_slashes: false,
49 }
50 }
51}
52
53pub fn parse_backup_mode(s: &str) -> Option<BackupMode> {
55 match s {
56 "none" | "off" => Some(BackupMode::None),
57 "simple" | "never" => Some(BackupMode::Simple),
58 "numbered" | "t" => Some(BackupMode::Numbered),
59 "existing" | "nil" => Some(BackupMode::Existing),
60 _ => Option::None,
61 }
62}
63
64pub fn make_backup_name(dst: &Path, mode: &BackupMode, suffix: &str) -> std::path::PathBuf {
66 match mode {
67 BackupMode::Simple | BackupMode::None => {
68 let mut name = dst.as_os_str().to_os_string();
69 name.push(suffix);
70 std::path::PathBuf::from(name)
71 }
72 BackupMode::Numbered => make_numbered_backup(dst),
73 BackupMode::Existing => {
74 if has_numbered_backup(dst) {
76 make_numbered_backup(dst)
77 } else {
78 let mut name = dst.as_os_str().to_os_string();
79 name.push(suffix);
80 std::path::PathBuf::from(name)
81 }
82 }
83 }
84}
85
86fn has_numbered_backup(path: &Path) -> bool {
88 let file_name = match path.file_name() {
89 Some(n) => n.to_string_lossy().to_string(),
90 None => return false,
91 };
92 let parent = path.parent().unwrap_or_else(|| Path::new("."));
93 if let Ok(entries) = fs::read_dir(parent) {
94 for entry in entries.flatten() {
95 let name = entry.file_name().to_string_lossy().to_string();
96 if name.starts_with(&format!("{}.~", file_name)) && name.ends_with('~') {
97 let middle = &name[file_name.len() + 2..name.len() - 1];
99 if middle.parse::<u64>().is_ok() {
100 return true;
101 }
102 }
103 }
104 }
105 false
106}
107
108fn make_numbered_backup(path: &Path) -> std::path::PathBuf {
110 let mut n = 1u64;
111 loop {
112 let candidate = format!("{}.~{}~", path.display(), n);
113 let p = std::path::PathBuf::from(&candidate);
114 if !p.exists() {
115 return p;
116 }
117 n += 1;
118 }
119}
120
121pub fn mv_file(src: &Path, dst: &Path, config: &MvConfig) -> io::Result<()> {
126 if dst.exists() {
128 if config.no_clobber {
129 return Ok(());
130 }
131 if config.update {
132 let src_time = fs::metadata(src)?.modified()?;
133 let dst_time = fs::metadata(dst)?.modified()?;
134 if src_time <= dst_time {
135 return Ok(());
136 }
137 }
138 }
139
140 if dst.exists() {
142 if let Some(ref mode) = config.backup {
143 let backup_name = make_backup_name(dst, mode, &config.suffix);
144 fs::rename(dst, &backup_name)?;
145 }
146 }
147
148 match fs::rename(src, dst) {
150 Ok(()) => {
151 if config.verbose {
152 eprintln!("renamed '{}' -> '{}'", src.display(), dst.display());
153 }
154 Ok(())
155 }
156 Err(e) if e.raw_os_error() == Some(libc::EXDEV) => {
157 copy_recursive(src, dst)?;
159 remove_recursive(src)?;
160 if config.verbose {
161 eprintln!("renamed '{}' -> '{}'", src.display(), dst.display());
162 }
163 Ok(())
164 }
165 Err(e) => Err(e),
166 }
167}
168
169pub fn strip_trailing_slashes(path: &str) -> &str {
171 let trimmed = path.trim_end_matches('/');
172 if trimmed.is_empty() { "/" } else { trimmed }
173}
174
175fn preserve_metadata(src_meta: &fs::Metadata, dst: &Path) -> io::Result<()> {
178 fs::set_permissions(dst, src_meta.permissions())?;
180
181 #[cfg(unix)]
183 {
184 let atime_spec = libc::timespec {
185 tv_sec: src_meta.atime(),
186 tv_nsec: src_meta.atime_nsec(),
187 };
188 let mtime_spec = libc::timespec {
189 tv_sec: src_meta.mtime(),
190 tv_nsec: src_meta.mtime_nsec(),
191 };
192 let times = [atime_spec, mtime_spec];
193 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
194 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
195 let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) };
197 if ret != 0 {
198 return Err(io::Error::last_os_error());
199 }
200 }
201
202 #[cfg(unix)]
204 {
205 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
206 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
207 let ret = unsafe { libc::lchown(c_path.as_ptr(), src_meta.uid(), src_meta.gid()) };
209 if ret != 0 {
210 let err = io::Error::last_os_error();
211 if err.raw_os_error() != Some(libc::EPERM) {
213 return Err(err);
214 }
215 }
216 }
217
218 Ok(())
219}
220
221fn copy_recursive(src: &Path, dst: &Path) -> io::Result<()> {
223 let metadata = fs::symlink_metadata(src)?;
224
225 if metadata.is_dir() {
226 fs::create_dir_all(dst)?;
227 for entry in fs::read_dir(src)? {
228 let entry = entry?;
229 let src_child = entry.path();
230 let dst_child = dst.join(entry.file_name());
231 copy_recursive(&src_child, &dst_child)?;
232 }
233 preserve_metadata(&metadata, dst)?;
235 } else if metadata.file_type().is_symlink() {
236 let link_target = fs::read_link(src)?;
237 #[cfg(unix)]
238 {
239 std::os::unix::fs::symlink(&link_target, dst)?;
240 }
241 #[cfg(not(unix))]
242 {
243 fs::copy(src, dst)?;
245 }
246 #[cfg(unix)]
248 {
249 let c_path = std::ffi::CString::new(dst.as_os_str().as_encoded_bytes())
250 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
251 let ret = unsafe { libc::lchown(c_path.as_ptr(), metadata.uid(), metadata.gid()) };
253 if ret != 0 {
254 let err = io::Error::last_os_error();
255 if err.raw_os_error() != Some(libc::EPERM) {
256 return Err(err);
257 }
258 }
259 }
260 } else {
261 fs::copy(src, dst)?;
262 preserve_metadata(&metadata, dst)?;
263 }
264
265 Ok(())
266}
267
268fn remove_recursive(path: &Path) -> io::Result<()> {
270 let metadata = fs::symlink_metadata(path)?;
271 if metadata.is_dir() {
272 fs::remove_dir_all(path)
273 } else {
274 fs::remove_file(path)
275 }
276}