1use std::env;
5use std::ffi::{OsStr, OsString};
6use std::fs::{self, File, Metadata, OpenOptions};
7use std::io;
8use std::io::prelude::*;
9use std::iter;
10use std::path::{Component, Path, PathBuf};
11
12use anyhow::{Context, Result};
13use tempfile::Builder as TempFileBuilder;
14
15pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> Result<OsString> {
22 env::join_paths(paths.iter()).with_context(|| {
23 let mut message = format!(
24 "failed to join paths from `${env}` together\n\n\
25 Check if any of path segments listed below contain an \
26 unterminated quote character or path separator:"
27 );
28 for path in paths {
29 use std::fmt::Write;
30 write!(&mut message, "\n {:?}", Path::new(path)).unwrap();
31 }
32
33 message
34 })
35}
36
37pub fn dylib_path_envvar() -> &'static str {
40 if cfg!(windows) {
41 "PATH"
42 } else if cfg!(target_os = "macos") {
43 "DYLD_FALLBACK_LIBRARY_PATH"
59 } else if cfg!(target_os = "aix") {
60 "LIBPATH"
61 } else {
62 "LD_LIBRARY_PATH"
63 }
64}
65
66pub fn dylib_path() -> Vec<PathBuf> {
71 match env::var_os(dylib_path_envvar()) {
72 Some(var) => env::split_paths(&var).collect(),
73 None => Vec::new(),
74 }
75}
76
77pub fn normalize_path(path: &Path) -> PathBuf {
86 let mut components = path.components().peekable();
87 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
88 components.next();
89 PathBuf::from(c.as_os_str())
90 } else {
91 PathBuf::new()
92 };
93
94 for component in components {
95 match component {
96 Component::Prefix(..) => unreachable!(),
97 Component::RootDir => {
98 ret.push(component.as_os_str());
99 }
100 Component::CurDir => {}
101 Component::ParentDir => {
102 ret.pop();
103 }
104 Component::Normal(c) => {
105 ret.push(c);
106 }
107 }
108 }
109 ret
110}
111
112pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
117 if exec.components().count() == 1 {
118 let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
119 let candidates = env::split_paths(&paths).flat_map(|path| {
120 let candidate = path.join(exec);
121 let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
122 None
123 } else {
124 Some(candidate.with_extension(env::consts::EXE_EXTENSION))
125 };
126 iter::once(candidate).chain(with_exe)
127 });
128 for candidate in candidates {
129 if candidate.is_file() {
130 return Ok(candidate);
131 }
132 }
133
134 anyhow::bail!("no executable for `{}` found in PATH", exec.display())
135 } else {
136 Ok(exec.into())
137 }
138}
139
140pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
144 let path = path.as_ref();
145 std::fs::metadata(path)
146 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
147}
148
149pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
153 let path = path.as_ref();
154 std::fs::symlink_metadata(path)
155 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
156}
157
158pub fn read(path: &Path) -> Result<String> {
162 match String::from_utf8(read_bytes(path)?) {
163 Ok(s) => Ok(s),
164 Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
165 }
166}
167
168pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
172 fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
173}
174
175pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
179 let path = path.as_ref();
180 fs::write(path, contents.as_ref())
181 .with_context(|| format!("failed to write `{}`", path.display()))
182}
183
184pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
188 let path = path.as_ref();
189
190 #[cfg(unix)]
195 let perms = path.metadata().ok().map(|meta| {
196 use std::os::unix::fs::PermissionsExt;
197
198 #[allow(clippy::useless_conversion)]
200 let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
201 let mode = meta.permissions().mode() & mask;
202
203 std::fs::Permissions::from_mode(mode)
204 });
205
206 let mut tmp = TempFileBuilder::new()
207 .prefix(path.file_name().unwrap())
208 .tempfile_in(path.parent().unwrap())?;
209 tmp.write_all(contents.as_ref())?;
210
211 #[cfg(unix)]
215 if let Some(perms) = perms {
216 tmp.as_file().set_permissions(perms)?;
217 }
218
219 tmp.persist(path)?;
220 Ok(())
221}
222
223pub fn temp_dir_in<P: AsRef<Path>, T>(path: P, f: impl FnOnce(&Path) -> Result<T>) -> Result<T> {
231 let path = path.as_ref();
232
233 std::fs::create_dir_all(path)
234 .with_context(|| format!("failed to create directory for tmpdir `{}`", path.display()))?;
235
236 let tmp = TempFileBuilder::new().tempdir_in(path)?;
237 f(tmp.path())
238}
239
240pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
243 (|| -> Result<()> {
244 let contents = contents.as_ref();
245 let mut f = OpenOptions::new()
246 .read(true)
247 .write(true)
248 .create(true)
249 .truncate(false)
252 .open(&path)?;
253 let mut orig = Vec::new();
254 f.read_to_end(&mut orig)?;
255 if orig != contents {
256 f.set_len(0)?;
257 f.seek(io::SeekFrom::Start(0))?;
258 f.write_all(contents)?;
259 }
260 Ok(())
261 })()
262 .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
263 Ok(())
264}
265
266pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
269 (|| -> Result<()> {
270 let mut f = OpenOptions::new().append(true).create(true).open(path)?;
271
272 f.write_all(contents)?;
273 Ok(())
274 })()
275 .with_context(|| format!("failed to write `{}`", path.display()))?;
276 Ok(())
277}
278
279pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
281 let path = path.as_ref();
282 File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
283}
284
285pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
287 let path = path.as_ref();
288 File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
289}
290
291pub fn path2bytes(path: &Path) -> Result<&[u8]> {
293 #[cfg(unix)]
294 {
295 use std::os::unix::prelude::*;
296 Ok(path.as_os_str().as_bytes())
297 }
298 #[cfg(windows)]
299 {
300 match path.as_os_str().to_str() {
301 Some(s) => Ok(s.as_bytes()),
302 None => Err(anyhow::format_err!(
303 "invalid non-unicode path: {}",
304 path.display()
305 )),
306 }
307 }
308}
309
310pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
312 #[cfg(unix)]
313 {
314 use std::os::unix::prelude::*;
315 Ok(PathBuf::from(OsStr::from_bytes(bytes)))
316 }
317 #[cfg(windows)]
318 {
319 use std::str;
320 match str::from_utf8(bytes) {
321 Ok(s) => Ok(PathBuf::from(s)),
322 Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
323 }
324 }
325}
326
327pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
333 PathAncestors::new(path, stop_root_at)
334}
335
336pub struct PathAncestors<'a> {
338 current: Option<&'a Path>,
339 stop_at: Option<PathBuf>,
340}
341
342impl<'a> PathAncestors<'a> {
343 fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
344 let stop_at = env::var("__CARGO_TEST_ROOT")
345 .ok()
346 .map(PathBuf::from)
347 .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
348 PathAncestors {
349 current: Some(path),
350 stop_at,
352 }
353 }
354}
355
356impl<'a> Iterator for PathAncestors<'a> {
357 type Item = &'a Path;
358
359 fn next(&mut self) -> Option<&'a Path> {
360 if let Some(path) = self.current {
361 self.current = path.parent();
362
363 if let Some(ref stop_at) = self.stop_at
364 && path == stop_at
365 {
366 self.current = None;
367 }
368
369 Some(path)
370 } else {
371 None
372 }
373 }
374}
375
376pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
378 _create_dir_all(p.as_ref())
379}
380
381fn _create_dir_all(p: &Path) -> Result<()> {
382 fs::create_dir_all(p)
383 .with_context(|| format!("failed to create directory `{}`", p.display()))?;
384 Ok(())
385}
386
387pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
391 _remove_dir_all(p.as_ref()).or_else(|prev_err| {
392 fs::remove_dir_all(p.as_ref()).with_context(|| {
396 format!(
397 "{:?}\n\nError: failed to remove directory `{}`",
398 prev_err,
399 p.as_ref().display(),
400 )
401 })
402 })
403}
404
405fn _remove_dir_all(p: &Path) -> Result<()> {
406 if symlink_metadata(p)?.is_symlink() {
407 return remove_file(p);
408 }
409 let entries = p
410 .read_dir()
411 .with_context(|| format!("failed to read directory `{}`", p.display()))?;
412 for entry in entries {
413 let entry = entry?;
414 let path = entry.path();
415 if entry.file_type()?.is_dir() {
416 remove_dir_all(&path)?;
417 } else {
418 remove_file(&path)?;
419 }
420 }
421 remove_dir(p)
422}
423
424pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
426 _remove_dir(p.as_ref())
427}
428
429fn _remove_dir(p: &Path) -> Result<()> {
430 fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
431 Ok(())
432}
433
434pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
441 _remove_file(p.as_ref())
442}
443
444fn _remove_file(p: &Path) -> Result<()> {
445 #[cfg(target_os = "windows")]
449 {
450 use std::os::windows::fs::FileTypeExt;
451 let metadata = symlink_metadata(p)?;
452 let file_type = metadata.file_type();
453 if file_type.is_symlink_dir() {
454 return remove_symlink_dir_with_permission_check(p);
455 }
456 }
457
458 remove_file_with_permission_check(p)
459}
460
461#[cfg(target_os = "windows")]
462fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
463 remove_with_permission_check(fs::remove_dir, p)
464 .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
465}
466
467fn remove_file_with_permission_check(p: &Path) -> Result<()> {
468 remove_with_permission_check(fs::remove_file, p)
469 .with_context(|| format!("failed to remove file `{}`", p.display()))
470}
471
472fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
473where
474 F: Fn(P) -> io::Result<()>,
475 P: AsRef<Path> + Clone,
476{
477 match remove_func(p.clone()) {
478 Ok(()) => Ok(()),
479 Err(e) => {
480 if e.kind() == io::ErrorKind::PermissionDenied
481 && set_not_readonly(p.as_ref()).unwrap_or(false)
482 {
483 remove_func(p)
484 } else {
485 Err(e)
486 }
487 }
488 }
489}
490
491fn set_not_readonly(p: &Path) -> io::Result<bool> {
492 let mut perms = p.metadata()?.permissions();
493 if !perms.readonly() {
494 return Ok(false);
495 }
496
497 #[cfg(unix)]
498 {
499 use std::os::unix::fs::PermissionsExt;
500 perms.set_mode(0o640);
501 }
502 #[cfg(not(unix))]
503 #[allow(clippy::permissions_set_readonly_false)]
504 {
505 perms.set_readonly(false);
506 }
507
508 fs::set_permissions(p, perms)?;
509 Ok(true)
510}
511
512pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
516 let src = src.as_ref();
517 let dst = dst.as_ref();
518 _link_or_copy(src, dst)
519}
520
521fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
522 log::debug!("linking {} to {}", src.display(), dst.display());
523 if same_file::is_same_file(src, dst).unwrap_or(false) {
524 return Ok(());
525 }
526
527 if fs::symlink_metadata(dst).is_ok() {
532 remove_file(dst)?;
533 }
534
535 let link_result = if src.is_dir() {
536 #[cfg(target_os = "redox")]
537 use std::os::redox::fs::symlink;
538 #[cfg(unix)]
539 use std::os::unix::fs::symlink;
540 #[cfg(windows)]
541 use std::os::windows::fs::symlink_dir as symlink;
546
547 let dst_dir = dst.parent().unwrap();
548 let src = if src.starts_with(dst_dir) {
549 src.strip_prefix(dst_dir).unwrap()
550 } else {
551 src
552 };
553 symlink(src, dst)
554 } else if cfg!(target_os = "macos") {
555 fs::copy(src, dst).map_or_else(
566 |e| {
567 if e.raw_os_error()
568 .map_or(false, |os_err| os_err == 35 )
569 {
570 log::info!("copy failed {e:?}. falling back to fs::hard_link");
571
572 fs::hard_link(src, dst)
576 } else {
577 Err(e)
578 }
579 },
580 |_| Ok(()),
581 )
582 } else {
583 fs::hard_link(src, dst)
584 };
585 link_result
586 .or_else(|err| {
587 log::debug!("link failed {err}. falling back to fs::copy");
588 fs::copy(src, dst).map(|_| ())
589 })
590 .with_context(|| {
591 format!(
592 "failed to link or copy `{}` to `{}`",
593 src.display(),
594 dst.display()
595 )
596 })?;
597 Ok(())
598}
599
600pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
604 let from = from.as_ref();
605 let to = to.as_ref();
606 fs::copy(from, to)
607 .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
608}
609
610pub fn strip_prefix_canonical<P: AsRef<Path>>(
616 path: P,
617 base: P,
618) -> Result<PathBuf, std::path::StripPrefixError> {
619 let safe_canonicalize = |path: &Path| match path.canonicalize() {
621 Ok(p) => p,
622 Err(e) => {
623 log::warn!("cannot canonicalize {path:?}: {e:?}");
624 path.to_path_buf()
625 }
626 };
627 let canon_path = safe_canonicalize(path.as_ref());
628 let canon_base = safe_canonicalize(base.as_ref());
629 canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
630}
631
632pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
642 let path = p.as_ref();
643 if path.is_dir() {
644 return Ok(());
645 }
646
647 let parent = path.parent().unwrap();
648 let base = path.file_name().unwrap();
649 create_dir_all(parent)?;
650 let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
662 exclude_from_backups(tempdir.path());
663 exclude_from_content_indexing(tempdir.path());
664 if let Err(e) = fs::rename(tempdir.path(), path)
672 && !path.exists()
673 {
674 return Err(anyhow::Error::from(e))
675 .with_context(|| format!("failed to create directory `{}`", path.display()));
676 }
677 Ok(())
678}
679
680pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
684 let path = p.as_ref();
685 exclude_from_backups(path);
686 exclude_from_content_indexing(path);
687}
688
689fn exclude_from_backups(path: &Path) {
698 exclude_from_time_machine(path);
699 let file = path.join("CACHEDIR.TAG");
700 if !file.exists() {
701 let _ = std::fs::write(
702 file,
703 "Signature: 8a477f597d28d172789f06886806bc55
704# This file is a cache directory tag created by cargo.
705# For information about cache directory tags see https://bford.info/cachedir/
706",
707 );
708 }
711}
712
713fn exclude_from_content_indexing(path: &Path) {
721 #[cfg(windows)]
722 {
723 use std::iter::once;
724 use std::os::windows::prelude::OsStrExt;
725 use windows_sys::Win32::Storage::FileSystem::{
726 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
727 };
728
729 let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
730 unsafe {
732 SetFileAttributesW(
733 path.as_ptr(),
734 GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
735 );
736 }
737 }
738 #[cfg(not(windows))]
739 {
740 let _ = path;
741 }
742}
743
744#[cfg(not(target_os = "macos"))]
745fn exclude_from_time_machine(_: &Path) {}
746
747#[cfg(target_os = "macos")]
748fn exclude_from_time_machine(path: &Path) {
750 use core_foundation::base::TCFType;
751 use core_foundation::{number, string, url};
752 use std::ptr;
753
754 let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
757 let path = url::CFURL::from_path(path, false);
758 if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
759 unsafe {
760 url::CFURLSetResourcePropertyForKey(
761 path.as_concrete_TypeRef(),
762 is_excluded_key.as_concrete_TypeRef(),
763 number::kCFBooleanTrue as *const _,
764 ptr::null_mut(),
765 );
766 }
767 }
768 }
771
772#[cfg(test)]
773mod tests {
774 use super::join_paths;
775 use super::write;
776 use super::write_atomic;
777
778 #[test]
779 fn write_works() {
780 let original_contents = "[dependencies]\nfoo = 0.1.0";
781
782 let tmpdir = tempfile::tempdir().unwrap();
783 let path = tmpdir.path().join("Cargo.toml");
784 write(&path, original_contents).unwrap();
785 let contents = std::fs::read_to_string(&path).unwrap();
786 assert_eq!(contents, original_contents);
787 }
788 #[test]
789 fn write_atomic_works() {
790 let original_contents = "[dependencies]\nfoo = 0.1.0";
791
792 let tmpdir = tempfile::tempdir().unwrap();
793 let path = tmpdir.path().join("Cargo.toml");
794 write_atomic(&path, original_contents).unwrap();
795 let contents = std::fs::read_to_string(&path).unwrap();
796 assert_eq!(contents, original_contents);
797 }
798
799 #[test]
800 #[cfg(unix)]
801 fn write_atomic_permissions() {
802 use std::os::unix::fs::PermissionsExt;
803
804 #[allow(clippy::useless_conversion)]
805 let perms = u32::from(libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH);
806 let original_perms = std::fs::Permissions::from_mode(perms);
807
808 let tmp = tempfile::Builder::new().tempfile().unwrap();
809
810 tmp.as_file()
812 .set_permissions(original_perms.clone())
813 .unwrap();
814
815 write_atomic(tmp.path(), "new").unwrap();
818 assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
819
820 let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
821
822 #[allow(clippy::useless_conversion)]
823 let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
824 assert_eq!(original_perms.mode(), new_perms.mode() & mask);
825 }
826
827 #[test]
828 fn join_paths_lists_paths_on_error() {
829 let valid_paths = vec!["/testing/one", "/testing/two"];
830 let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
832
833 #[cfg(unix)]
834 {
835 let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
836 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
837 assert_eq!(
838 err.to_string(),
839 "failed to join paths from `$TESTING2` together\n\n\
840 Check if any of path segments listed below contain an \
841 unterminated quote character or path separator:\
842 \n \"/testing/one\"\
843 \n \"/testing/t:wo/three\"\
844 "
845 );
846 }
847 #[cfg(windows)]
848 {
849 let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
850 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
851 assert_eq!(
852 err.to_string(),
853 "failed to join paths from `$TESTING2` together\n\n\
854 Check if any of path segments listed below contain an \
855 unterminated quote character or path separator:\
856 \n \"/testing/one\"\
857 \n \"/testing/t\\\"wo/three\"\
858 "
859 );
860 }
861 }
862
863 #[test]
864 #[cfg(windows)]
865 fn test_remove_symlink_dir() {
866 use super::*;
867 use std::fs;
868 use std::os::windows::fs::symlink_dir;
869
870 let tmpdir = tempfile::tempdir().unwrap();
871 let dir_path = tmpdir.path().join("testdir");
872 let symlink_path = tmpdir.path().join("symlink");
873
874 fs::create_dir(&dir_path).unwrap();
875
876 symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
877
878 assert!(symlink_path.exists());
879
880 assert!(remove_file(symlink_path.clone()).is_ok());
881
882 assert!(!symlink_path.exists());
883 assert!(dir_path.exists());
884 }
885
886 #[test]
887 #[cfg(windows)]
888 fn test_remove_symlink_file() {
889 use super::*;
890 use std::fs;
891 use std::os::windows::fs::symlink_file;
892
893 let tmpdir = tempfile::tempdir().unwrap();
894 let file_path = tmpdir.path().join("testfile");
895 let symlink_path = tmpdir.path().join("symlink");
896
897 fs::write(&file_path, b"test").unwrap();
898
899 symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
900
901 assert!(symlink_path.exists());
902
903 assert!(remove_file(symlink_path.clone()).is_ok());
904
905 assert!(!symlink_path.exists());
906 assert!(file_path.exists());
907 }
908}