1mod error;
9
10use clap::builder::ValueParser;
11use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command};
12use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
13use std::collections::HashSet;
14use std::env;
15use std::ffi::OsString;
16use std::fs;
17use std::io;
18#[cfg(unix)]
19use std::os::unix;
20#[cfg(windows)]
21use std::os::windows;
22use std::path::{absolute, Path, PathBuf};
23use uucore::backup_control::{self, source_is_target_backup};
24use uucore::display::Quotable;
25use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError};
26use uucore::fs::{
27 are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, canonicalize,
28 path_ends_with_terminator, MissingHandling, ResolveMode,
29};
30#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
31use uucore::fsxattr;
32use uucore::update_control;
33
34pub use uucore::{backup_control::BackupMode, update_control::UpdateMode};
37use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show};
38
39use fs_extra::dir::{
40 get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions,
41 TransitProcess, TransitProcessResult,
42};
43
44use crate::error::MvError;
45
46#[derive(Debug, Clone, Eq, PartialEq)]
54pub struct Options {
55 pub overwrite: OverwriteMode,
60
61 pub backup: BackupMode,
63
64 pub suffix: String,
66
67 pub update: UpdateMode,
69
70 pub target_dir: Option<OsString>,
73
74 pub no_target_dir: bool,
77
78 pub verbose: bool,
80
81 pub strip_slashes: bool,
83
84 pub progress_bar: bool,
86
87 pub debug: bool,
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum OverwriteMode {
94 NoClobber,
96 Interactive,
98 Force,
100}
101
102const ABOUT: &str = help_about!("mv.md");
103const USAGE: &str = help_usage!("mv.md");
104const AFTER_HELP: &str = help_section!("after help", "mv.md");
105
106static OPT_FORCE: &str = "force";
107static OPT_INTERACTIVE: &str = "interactive";
108static OPT_NO_CLOBBER: &str = "no-clobber";
109static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes";
110static OPT_TARGET_DIRECTORY: &str = "target-directory";
111static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
112static OPT_VERBOSE: &str = "verbose";
113static OPT_PROGRESS: &str = "progress";
114static ARG_FILES: &str = "files";
115static OPT_DEBUG: &str = "debug";
116
117#[uucore::main]
118pub fn uumain(args: impl uucore::Args) -> UResult<()> {
119 let mut app = uu_app();
120 let matches = app.try_get_matches_from_mut(args)?;
121
122 let files: Vec<OsString> = matches
123 .get_many::<OsString>(ARG_FILES)
124 .unwrap_or_default()
125 .cloned()
126 .collect();
127
128 if files.len() == 1 && !matches.contains_id(OPT_TARGET_DIRECTORY) {
129 app.error(
130 ErrorKind::TooFewValues,
131 format!(
132 "The argument '<{ARG_FILES}>...' requires at least 2 values, but only 1 was provided"
133 ),
134 )
135 .exit();
136 }
137
138 let overwrite_mode = determine_overwrite_mode(&matches);
139 let backup_mode = backup_control::determine_backup_mode(&matches)?;
140 let update_mode = update_control::determine_update_mode(&matches);
141
142 if backup_mode != BackupMode::NoBackup
143 && (overwrite_mode == OverwriteMode::NoClobber
144 || update_mode == UpdateMode::ReplaceNone
145 || update_mode == UpdateMode::ReplaceNoneFail)
146 {
147 return Err(UUsageError::new(
148 1,
149 "cannot combine --backup with -n/--no-clobber or --update=none-fail",
150 ));
151 }
152
153 let backup_suffix = backup_control::determine_backup_suffix(&matches);
154
155 let target_dir = matches
156 .get_one::<OsString>(OPT_TARGET_DIRECTORY)
157 .map(OsString::from);
158
159 if let Some(ref maybe_dir) = target_dir {
160 if !Path::new(&maybe_dir).is_dir() {
161 return Err(MvError::TargetNotADirectory(maybe_dir.quote().to_string()).into());
162 }
163 }
164
165 let opts = Options {
166 overwrite: overwrite_mode,
167 backup: backup_mode,
168 suffix: backup_suffix,
169 update: update_mode,
170 target_dir,
171 no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
172 verbose: matches.get_flag(OPT_VERBOSE) || matches.get_flag(OPT_DEBUG),
173 strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
174 progress_bar: matches.get_flag(OPT_PROGRESS),
175 debug: matches.get_flag(OPT_DEBUG),
176 };
177
178 mv(&files[..], &opts)
179}
180
181pub fn uu_app() -> Command {
182 Command::new(uucore::util_name())
183 .version(crate_version!())
184 .about(ABOUT)
185 .override_usage(format_usage(USAGE))
186 .after_help(format!(
187 "{AFTER_HELP}\n\n{}",
188 backup_control::BACKUP_CONTROL_LONG_HELP
189 ))
190 .infer_long_args(true)
191 .arg(
192 Arg::new(OPT_FORCE)
193 .short('f')
194 .long(OPT_FORCE)
195 .help("do not prompt before overwriting")
196 .overrides_with_all([OPT_INTERACTIVE, OPT_NO_CLOBBER])
197 .action(ArgAction::SetTrue),
198 )
199 .arg(
200 Arg::new(OPT_INTERACTIVE)
201 .short('i')
202 .long(OPT_INTERACTIVE)
203 .help("prompt before override")
204 .overrides_with_all([OPT_FORCE, OPT_NO_CLOBBER])
205 .action(ArgAction::SetTrue),
206 )
207 .arg(
208 Arg::new(OPT_NO_CLOBBER)
209 .short('n')
210 .long(OPT_NO_CLOBBER)
211 .help("do not overwrite an existing file")
212 .overrides_with_all([OPT_FORCE, OPT_INTERACTIVE])
213 .action(ArgAction::SetTrue),
214 )
215 .arg(
216 Arg::new(OPT_STRIP_TRAILING_SLASHES)
217 .long(OPT_STRIP_TRAILING_SLASHES)
218 .help("remove any trailing slashes from each SOURCE argument")
219 .action(ArgAction::SetTrue),
220 )
221 .arg(backup_control::arguments::backup())
222 .arg(backup_control::arguments::backup_no_args())
223 .arg(backup_control::arguments::suffix())
224 .arg(update_control::arguments::update())
225 .arg(update_control::arguments::update_no_args())
226 .arg(
227 Arg::new(OPT_TARGET_DIRECTORY)
228 .short('t')
229 .long(OPT_TARGET_DIRECTORY)
230 .help("move all SOURCE arguments into DIRECTORY")
231 .value_name("DIRECTORY")
232 .value_hint(clap::ValueHint::DirPath)
233 .conflicts_with(OPT_NO_TARGET_DIRECTORY)
234 .value_parser(ValueParser::os_string()),
235 )
236 .arg(
237 Arg::new(OPT_NO_TARGET_DIRECTORY)
238 .short('T')
239 .long(OPT_NO_TARGET_DIRECTORY)
240 .help("treat DEST as a normal file")
241 .action(ArgAction::SetTrue),
242 )
243 .arg(
244 Arg::new(OPT_VERBOSE)
245 .short('v')
246 .long(OPT_VERBOSE)
247 .help("explain what is being done")
248 .action(ArgAction::SetTrue),
249 )
250 .arg(
251 Arg::new(OPT_PROGRESS)
252 .short('g')
253 .long(OPT_PROGRESS)
254 .help(
255 "Display a progress bar. \n\
256 Note: this feature is not supported by GNU coreutils.",
257 )
258 .action(ArgAction::SetTrue),
259 )
260 .arg(
261 Arg::new(ARG_FILES)
262 .action(ArgAction::Append)
263 .num_args(1..)
264 .required(true)
265 .value_parser(ValueParser::os_string())
266 .value_hint(clap::ValueHint::AnyPath),
267 )
268 .arg(
269 Arg::new(OPT_DEBUG)
270 .long(OPT_DEBUG)
271 .help("explain how a file is copied. Implies -v")
272 .action(ArgAction::SetTrue),
273 )
274}
275
276fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
277 if matches.get_flag(OPT_NO_CLOBBER) {
283 OverwriteMode::NoClobber
284 } else if matches.get_flag(OPT_INTERACTIVE) {
285 OverwriteMode::Interactive
286 } else {
287 OverwriteMode::Force
288 }
289}
290
291fn parse_paths(files: &[OsString], opts: &Options) -> Vec<PathBuf> {
292 let paths = files.iter().map(Path::new);
293
294 if opts.strip_slashes {
295 paths
296 .map(|p| p.components().as_path().to_owned())
297 .collect::<Vec<PathBuf>>()
298 } else {
299 paths.map(|p| p.to_owned()).collect::<Vec<PathBuf>>()
300 }
301}
302
303fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> {
304 if opts.backup == BackupMode::SimpleBackup
305 && source_is_target_backup(source, target, &opts.suffix)
306 {
307 return Err(io::Error::new(
308 io::ErrorKind::NotFound,
309 format!(
310 "backing up {} might destroy source; {} not moved",
311 target.quote(),
312 source.quote()
313 ),
314 )
315 .into());
316 }
317 if source.symlink_metadata().is_err() {
318 return Err(if path_ends_with_terminator(source) {
319 MvError::CannotStatNotADirectory(source.quote().to_string()).into()
320 } else {
321 MvError::NoSuchFile(source.quote().to_string()).into()
322 });
323 }
324
325 let target_is_dir = target.is_dir();
326 let source_is_dir = source.is_dir();
327
328 if path_ends_with_terminator(target)
329 && (!target_is_dir && !source_is_dir)
330 && !opts.no_target_dir
331 && opts.update != UpdateMode::ReplaceIfOlder
332 {
333 return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into());
334 }
335
336 assert_not_same_file(source, target, target_is_dir, opts)?;
337
338 if target_is_dir {
339 if opts.no_target_dir {
340 if source.is_dir() {
341 rename(source, target, opts, None).map_err_context(|| {
342 format!("cannot move {} to {}", source.quote(), target.quote())
343 })
344 } else {
345 Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
346 }
347 } else {
348 move_files_into_dir(&[source.to_path_buf()], target, opts)
349 }
350 } else if target.exists() && source.is_dir() {
351 match opts.overwrite {
352 OverwriteMode::NoClobber => return Ok(()),
353 OverwriteMode::Interactive => {
354 if !prompt_yes!("overwrite {}? ", target.quote()) {
355 return Err(io::Error::new(io::ErrorKind::Other, "").into());
356 }
357 }
358 OverwriteMode::Force => {}
359 };
360 Err(MvError::NonDirectoryToDirectory(
361 source.quote().to_string(),
362 target.quote().to_string(),
363 )
364 .into())
365 } else {
366 rename(source, target, opts, None).map_err(|e| USimpleError::new(1, format!("{e}")))
367 }
368}
369
370fn assert_not_same_file(
371 source: &Path,
372 target: &Path,
373 target_is_dir: bool,
374 opts: &Options,
375) -> UResult<()> {
376 let canonicalized_source = match canonicalize(
378 absolute(source)?,
379 MissingHandling::Normal,
380 ResolveMode::Logical,
381 ) {
382 Ok(source) if source.exists() => source,
383 _ => absolute(source)?, };
385
386 let target_is_dir = target_is_dir && !opts.no_target_dir;
388 let canonicalized_target = if target_is_dir {
389 canonicalize(
392 absolute(target)?,
393 MissingHandling::Normal,
394 ResolveMode::Logical,
395 )?
396 .join(source.file_name().unwrap_or_default())
397 } else {
398 match absolute(target)?.parent() {
401 Some(parent) if parent.to_str() != Some("") => {
402 canonicalize(parent, MissingHandling::Normal, ResolveMode::Logical)?
403 .join(target.file_name().unwrap_or_default())
404 }
405 _ => absolute(target)?, }
408 };
409
410 let same_file = (canonicalized_source.eq(&canonicalized_target)
411 || are_hardlinks_to_same_file(source, target)
412 || are_hardlinks_or_one_way_symlink_to_same_file(source, target))
413 && opts.backup == BackupMode::NoBackup;
414
415 let target_display = match source.file_name() {
418 Some(file_name) if target_is_dir => {
419 let mut path = target
421 .display()
422 .to_string()
423 .trim_end_matches("/")
424 .to_owned();
425
426 path.push('/');
427 path.push_str(&file_name.to_string_lossy());
428
429 path.quote().to_string()
430 }
431 _ => target.quote().to_string(),
432 };
433
434 if same_file
435 && (canonicalized_source.eq(&canonicalized_target)
436 || source.eq(Path::new("."))
437 || source.ends_with("/.")
438 || source.is_file())
439 {
440 return Err(MvError::SameFile(source.quote().to_string(), target_display).into());
441 } else if (same_file || canonicalized_target.starts_with(canonicalized_source))
442 && !source.is_symlink()
444 {
445 return Err(
446 MvError::SelfTargetSubdirectory(source.quote().to_string(), target_display).into(),
447 );
448 }
449 Ok(())
450}
451
452fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> {
453 if opts.no_target_dir {
454 return Err(UUsageError::new(
455 1,
456 format!("mv: extra operand {}", paths[2].quote()),
457 ));
458 }
459 let target_dir = paths.last().unwrap();
460 let sources = &paths[..paths.len() - 1];
461
462 move_files_into_dir(sources, target_dir, opts)
463}
464
465pub fn mv(files: &[OsString], opts: &Options) -> UResult<()> {
469 let paths = parse_paths(files, opts);
470
471 if let Some(ref name) = opts.target_dir {
472 return move_files_into_dir(&paths, &PathBuf::from(name), opts);
473 }
474
475 match paths.len() {
476 2 => handle_two_paths(&paths[0], &paths[1], opts),
477 _ => handle_multiple_paths(&paths, opts),
478 }
479}
480
481#[allow(clippy::cognitive_complexity)]
482fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) -> UResult<()> {
483 let mut moved_destinations: HashSet<PathBuf> = HashSet::with_capacity(files.len());
485
486 if !target_dir.is_dir() {
487 return Err(MvError::NotADirectory(target_dir.quote().to_string()).into());
488 }
489
490 let multi_progress = options.progress_bar.then(MultiProgress::new);
491
492 let count_progress = if let Some(ref multi_progress) = multi_progress {
493 if files.len() > 1 {
494 Some(multi_progress.add(
495 ProgressBar::new(files.len().try_into().unwrap()).with_style(
496 ProgressStyle::with_template("moving {msg} {wide_bar} {pos}/{len}").unwrap(),
497 ),
498 ))
499 } else {
500 None
501 }
502 } else {
503 None
504 };
505
506 for sourcepath in files {
507 if !sourcepath.exists() {
508 show!(MvError::NoSuchFile(sourcepath.quote().to_string()));
509 continue;
510 }
511
512 if let Some(ref pb) = count_progress {
513 pb.set_message(sourcepath.to_string_lossy().to_string());
514 }
515
516 let targetpath = match sourcepath.file_name() {
517 Some(name) => target_dir.join(name),
518 None => {
519 show!(MvError::NoSuchFile(sourcepath.quote().to_string()));
520 continue;
521 }
522 };
523
524 if moved_destinations.contains(&targetpath) && options.backup != BackupMode::NumberedBackup
525 {
526 show!(USimpleError::new(
528 1,
529 format!(
530 "will not overwrite just-created '{}' with '{}'",
531 targetpath.display(),
532 sourcepath.display()
533 ),
534 ));
535 continue;
536 }
537
538 if let Err(e) = assert_not_same_file(sourcepath, target_dir, true, options) {
541 show!(e);
542 continue;
543 }
544
545 match rename(sourcepath, &targetpath, options, multi_progress.as_ref()) {
546 Err(e) if e.to_string().is_empty() => set_exit_code(1),
547 Err(e) => {
548 let e = e.map_err_context(|| {
549 format!(
550 "cannot move {} to {}",
551 sourcepath.quote(),
552 targetpath.quote()
553 )
554 });
555 match multi_progress {
556 Some(ref pb) => pb.suspend(|| show!(e)),
557 None => show!(e),
558 };
559 }
560 Ok(()) => (),
561 }
562 if let Some(ref pb) = count_progress {
563 pb.inc(1);
564 }
565 moved_destinations.insert(targetpath.clone());
566 }
567 Ok(())
568}
569
570fn rename(
571 from: &Path,
572 to: &Path,
573 opts: &Options,
574 multi_progress: Option<&MultiProgress>,
575) -> io::Result<()> {
576 let mut backup_path = None;
577
578 if to.exists() {
579 if opts.update == UpdateMode::ReplaceNone {
580 if opts.debug {
581 println!("skipped {}", to.quote());
582 }
583 return Ok(());
584 }
585
586 if (opts.update == UpdateMode::ReplaceIfOlder)
587 && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()?
588 {
589 return Ok(());
590 }
591
592 if opts.update == UpdateMode::ReplaceNoneFail {
593 let err_msg = format!("not replacing {}", to.quote());
594 return Err(io::Error::new(io::ErrorKind::Other, err_msg));
595 }
596
597 match opts.overwrite {
598 OverwriteMode::NoClobber => {
599 if opts.debug {
600 println!("skipped {}", to.quote());
601 }
602 return Ok(());
603 }
604 OverwriteMode::Interactive => {
605 if !prompt_yes!("overwrite {}?", to.quote()) {
606 return Err(io::Error::new(io::ErrorKind::Other, ""));
607 }
608 }
609 OverwriteMode::Force => {}
610 };
611
612 backup_path = backup_control::get_backup_path(opts.backup, to, &opts.suffix);
613 if let Some(ref backup_path) = backup_path {
614 rename_with_fallback(to, backup_path, multi_progress)?;
615 }
616 }
617
618 if to.exists() && to.is_dir() {
620 if from.is_dir() {
622 if is_empty_dir(to) {
623 fs::remove_dir(to)?;
624 } else {
625 return Err(io::Error::new(io::ErrorKind::Other, "Directory not empty"));
626 }
627 }
628 }
629
630 rename_with_fallback(from, to, multi_progress)?;
631
632 if opts.verbose {
633 let message = match backup_path {
634 Some(path) => format!(
635 "renamed {} -> {} (backup: {})",
636 from.quote(),
637 to.quote(),
638 path.quote()
639 ),
640 None => format!("renamed {} -> {}", from.quote(), to.quote()),
641 };
642
643 match multi_progress {
644 Some(pb) => pb.suspend(|| {
645 println!("{message}");
646 }),
647 None => println!("{message}"),
648 };
649 }
650 Ok(())
651}
652
653fn rename_with_fallback(
656 from: &Path,
657 to: &Path,
658 multi_progress: Option<&MultiProgress>,
659) -> io::Result<()> {
660 if let Err(err) = fs::rename(from, to) {
661 #[cfg(windows)]
662 const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _;
663 #[cfg(unix)]
664 const EXDEV: i32 = libc::EXDEV as _;
665
666 let should_fallback = matches!(err.raw_os_error(), Some(EXDEV))
671 || (from.is_file() && can_delete_file(from).unwrap_or(false));
672 if !should_fallback {
673 return Err(err);
674 }
675
676 let metadata = from.symlink_metadata()?;
678 let file_type = metadata.file_type();
679
680 if file_type.is_symlink() {
681 rename_symlink_fallback(from, to)?;
682 } else if file_type.is_dir() {
683 if to.exists() {
687 fs::remove_dir_all(to)?;
688 }
689 let options = DirCopyOptions {
690 copy_inside: true,
694 ..DirCopyOptions::new()
695 };
696
697 let total_size = dir_get_size(from).ok();
703
704 let progress_bar =
705 if let (Some(multi_progress), Some(total_size)) = (multi_progress, total_size) {
706 let bar = ProgressBar::new(total_size).with_style(
707 ProgressStyle::with_template(
708 "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}",
709 )
710 .unwrap(),
711 );
712
713 Some(multi_progress.add(bar))
714 } else {
715 None
716 };
717
718 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
719 let xattrs =
720 fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| std::collections::HashMap::new());
721
722 let result = if let Some(ref pb) = progress_bar {
723 move_dir_with_progress(from, to, &options, |process_info: TransitProcess| {
724 pb.set_position(process_info.copied_bytes);
725 pb.set_message(process_info.file_name);
726 TransitProcessResult::ContinueOrAbort
727 })
728 } else {
729 move_dir(from, to, &options)
730 };
731
732 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
733 fsxattr::apply_xattrs(to, xattrs)?;
734
735 if let Err(err) = result {
736 return match err.kind {
737 fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new(
738 io::ErrorKind::PermissionDenied,
739 "Permission denied",
740 )),
741 _ => Err(io::Error::new(io::ErrorKind::Other, format!("{err:?}"))),
742 };
743 }
744 } else {
745 if to.is_symlink() {
746 fs::remove_file(to).map_err(|err| {
747 let to = to.to_string_lossy();
748 let from = from.to_string_lossy();
749 io::Error::new(
750 err.kind(),
751 format!(
752 "inter-device move failed: '{from}' to '{to}'\
753 ; unable to remove target: {err}"
754 ),
755 )
756 })?;
757 }
758 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
759 fs::copy(from, to)
760 .and_then(|_| fsxattr::copy_xattrs(&from, &to))
761 .and_then(|_| fs::remove_file(from))?;
762 #[cfg(any(target_os = "macos", target_os = "redox", not(unix)))]
763 fs::copy(from, to).and_then(|_| fs::remove_file(from))?;
764 }
765 }
766 Ok(())
767}
768
769#[inline]
772fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
773 let path_symlink_points_to = fs::read_link(from)?;
774 #[cfg(unix)]
775 {
776 unix::fs::symlink(path_symlink_points_to, to).and_then(|_| fs::remove_file(from))?;
777 }
778 #[cfg(windows)]
779 {
780 if path_symlink_points_to.exists() {
781 if path_symlink_points_to.is_dir() {
782 windows::fs::symlink_dir(&path_symlink_points_to, to)?;
783 } else {
784 windows::fs::symlink_file(&path_symlink_points_to, to)?;
785 }
786 fs::remove_file(from)?;
787 } else {
788 return Err(io::Error::new(
789 io::ErrorKind::NotFound,
790 "can't determine symlink type, since it is dangling",
791 ));
792 }
793 }
794 #[cfg(not(any(windows, unix)))]
795 {
796 return Err(io::Error::new(
797 io::ErrorKind::Other,
798 "your operating system does not support symlinks",
799 ));
800 }
801 Ok(())
802}
803
804fn is_empty_dir(path: &Path) -> bool {
805 match fs::read_dir(path) {
806 Ok(contents) => contents.peekable().peek().is_none(),
807 Err(_e) => false,
808 }
809}
810
811#[cfg(windows)]
813fn can_delete_file(path: &Path) -> Result<bool, io::Error> {
814 use std::{
815 os::windows::ffi::OsStrExt as _,
816 ptr::{null, null_mut},
817 };
818
819 use windows_sys::Win32::{
820 Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
821 Storage::FileSystem::{
822 CreateFileW, DELETE, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_SHARE_READ,
823 FILE_SHARE_WRITE, OPEN_EXISTING,
824 },
825 };
826
827 let wide_path = path
828 .as_os_str()
829 .encode_wide()
830 .chain([0])
831 .collect::<Vec<u16>>();
832
833 let handle = unsafe {
834 CreateFileW(
835 wide_path.as_ptr(),
836 DELETE,
837 FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
838 null(),
839 OPEN_EXISTING,
840 FILE_ATTRIBUTE_NORMAL,
841 null_mut(),
842 )
843 };
844
845 if handle == INVALID_HANDLE_VALUE {
846 return Err(io::Error::last_os_error());
847 }
848
849 unsafe { CloseHandle(handle) };
850
851 Ok(true)
852}
853
854#[cfg(not(windows))]
855fn can_delete_file(_: &Path) -> Result<bool, io::Error> {
856 Ok(false)
861}