1use anyhow::{Context, Result};
4use filetime::FileTime;
5use std::env;
6use std::ffi::{OsStr, OsString};
7use std::fs::{self, File, Metadata, OpenOptions};
8use std::io;
9use std::io::prelude::*;
10use std::iter;
11use std::path::{Component, Path, PathBuf};
12use tempfile::Builder as TempFileBuilder;
13
14pub fn join_paths<T: AsRef<OsStr>>(paths: &[T], env: &str) -> Result<OsString> {
21 env::join_paths(paths.iter()).with_context(|| {
22 let mut message = format!(
23 "failed to join paths from `${env}` together\n\n\
24 Check if any of path segments listed below contain an \
25 unterminated quote character or path separator:"
26 );
27 for path in paths {
28 use std::fmt::Write;
29 write!(&mut message, "\n {:?}", Path::new(path)).unwrap();
30 }
31
32 message
33 })
34}
35
36pub fn dylib_path_envvar() -> &'static str {
39 if cfg!(windows) {
40 "PATH"
41 } else if cfg!(target_os = "macos") {
42 "DYLD_FALLBACK_LIBRARY_PATH"
58 } else if cfg!(target_os = "aix") {
59 "LIBPATH"
60 } else {
61 "LD_LIBRARY_PATH"
62 }
63}
64
65pub fn dylib_path() -> Vec<PathBuf> {
70 match env::var_os(dylib_path_envvar()) {
71 Some(var) => env::split_paths(&var).collect(),
72 None => Vec::new(),
73 }
74}
75
76pub fn normalize_path(path: &Path) -> PathBuf {
85 let mut components = path.components().peekable();
86 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
87 components.next();
88 PathBuf::from(c.as_os_str())
89 } else {
90 PathBuf::new()
91 };
92
93 for component in components {
94 match component {
95 Component::Prefix(..) => unreachable!(),
96 Component::RootDir => {
97 ret.push(Component::RootDir);
98 }
99 Component::CurDir => {}
100 Component::ParentDir => {
101 if ret.ends_with(Component::ParentDir) {
102 ret.push(Component::ParentDir);
103 } else {
104 let popped = ret.pop();
105 if !popped && !ret.has_root() {
106 ret.push(Component::ParentDir);
107 }
108 }
109 }
110 Component::Normal(c) => {
111 ret.push(c);
112 }
113 }
114 }
115 ret
116}
117
118pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
123 if exec.components().count() == 1 {
124 let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
125 let candidates = env::split_paths(&paths).flat_map(|path| {
126 let candidate = path.join(&exec);
127 let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
128 None
129 } else {
130 Some(candidate.with_extension(env::consts::EXE_EXTENSION))
131 };
132 iter::once(candidate).chain(with_exe)
133 });
134 for candidate in candidates {
135 if candidate.is_file() {
136 return Ok(candidate);
137 }
138 }
139
140 anyhow::bail!("no executable for `{}` found in PATH", exec.display())
141 } else {
142 Ok(exec.into())
143 }
144}
145
146pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
150 let path = path.as_ref();
151 std::fs::metadata(path)
152 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
153}
154
155pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
159 let path = path.as_ref();
160 std::fs::symlink_metadata(path)
161 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
162}
163
164pub fn read(path: &Path) -> Result<String> {
168 match String::from_utf8(read_bytes(path)?) {
169 Ok(s) => Ok(s),
170 Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
171 }
172}
173
174pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
178 fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
179}
180
181pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
185 let path = path.as_ref();
186 fs::write(path, contents.as_ref())
187 .with_context(|| format!("failed to write `{}`", path.display()))
188}
189
190pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
195 let path = path.as_ref();
196
197 let resolved_path;
199 let path = if path.is_symlink() {
200 resolved_path = fs::read_link(path)
201 .with_context(|| format!("failed to read symlink at `{}`", path.display()))?;
202 &resolved_path
203 } else {
204 path
205 };
206
207 #[cfg(unix)]
211 let perms = path.metadata().ok().map(|meta| {
212 use std::os::unix::fs::PermissionsExt;
213
214 let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
216 let mode = meta.permissions().mode() & mask;
217
218 std::fs::Permissions::from_mode(mode)
219 });
220
221 let mut tmp = TempFileBuilder::new()
222 .prefix(path.file_name().unwrap())
223 .tempfile_in(path.parent().unwrap())?;
224 tmp.write_all(contents.as_ref())?;
225
226 #[cfg(unix)]
230 if let Some(perms) = perms {
231 tmp.as_file().set_permissions(perms)?;
232 }
233
234 tmp.persist(path)?;
235 Ok(())
236}
237
238pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
241 (|| -> Result<()> {
242 let contents = contents.as_ref();
243 let mut f = OpenOptions::new()
244 .read(true)
245 .write(true)
246 .create(true)
247 .open(&path)?;
248 let mut orig = Vec::new();
249 f.read_to_end(&mut orig)?;
250 if orig != contents {
251 f.set_len(0)?;
252 f.seek(io::SeekFrom::Start(0))?;
253 f.write_all(contents)?;
254 }
255 Ok(())
256 })()
257 .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
258 Ok(())
259}
260
261pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
264 (|| -> Result<()> {
265 let mut f = OpenOptions::new()
266 .write(true)
267 .append(true)
268 .create(true)
269 .open(path)?;
270
271 f.write_all(contents)?;
272 Ok(())
273 })()
274 .with_context(|| format!("failed to write `{}`", path.display()))?;
275 Ok(())
276}
277
278pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
280 let path = path.as_ref();
281 File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
282}
283
284pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
286 let path = path.as_ref();
287 File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
288}
289
290pub fn mtime(path: &Path) -> Result<FileTime> {
292 let meta = metadata(path)?;
293 Ok(FileTime::from_last_modification_time(&meta))
294}
295
296pub fn mtime_recursive(path: &Path) -> Result<FileTime> {
299 let meta = metadata(path)?;
300 if !meta.is_dir() {
301 return Ok(FileTime::from_last_modification_time(&meta));
302 }
303 let max_meta = walkdir::WalkDir::new(path)
304 .follow_links(true)
305 .into_iter()
306 .filter_map(|e| match e {
307 Ok(e) => Some(e),
308 Err(e) => {
309 tracing::debug!("failed to determine mtime while walking directory: {}", e);
312 None
313 }
314 })
315 .filter_map(|e| {
316 if e.path_is_symlink() {
317 let sym_meta = match std::fs::symlink_metadata(e.path()) {
321 Ok(m) => m,
322 Err(err) => {
323 tracing::debug!(
327 "failed to determine mtime while fetching symlink metadata of {}: {}",
328 e.path().display(),
329 err
330 );
331 return None;
332 }
333 };
334 let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
335 match e.metadata() {
337 Ok(target_meta) => {
338 let target_mtime = FileTime::from_last_modification_time(&target_meta);
339 Some(sym_mtime.max(target_mtime))
340 }
341 Err(err) => {
342 tracing::debug!(
346 "failed to determine mtime of symlink target for {}: {}",
347 e.path().display(),
348 err
349 );
350 Some(sym_mtime)
351 }
352 }
353 } else {
354 let meta = match e.metadata() {
355 Ok(m) => m,
356 Err(err) => {
357 tracing::debug!(
361 "failed to determine mtime while fetching metadata of {}: {}",
362 e.path().display(),
363 err
364 );
365 return None;
366 }
367 };
368 Some(FileTime::from_last_modification_time(&meta))
369 }
370 })
371 .max()
372 .unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
374 Ok(max_meta)
375}
376
377pub fn set_invocation_time(path: &Path) -> Result<FileTime> {
380 let timestamp = path.join("invoked.timestamp");
383 write(
384 ×tamp,
385 "This file has an mtime of when this was started.",
386 )?;
387 let ft = mtime(×tamp)?;
388 tracing::debug!("invocation time for {:?} is {}", path, ft);
389 Ok(ft)
390}
391
392pub fn path2bytes(path: &Path) -> Result<&[u8]> {
394 #[cfg(unix)]
395 {
396 use std::os::unix::prelude::*;
397 Ok(path.as_os_str().as_bytes())
398 }
399 #[cfg(windows)]
400 {
401 match path.as_os_str().to_str() {
402 Some(s) => Ok(s.as_bytes()),
403 None => Err(anyhow::format_err!(
404 "invalid non-unicode path: {}",
405 path.display()
406 )),
407 }
408 }
409}
410
411pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
413 #[cfg(unix)]
414 {
415 use std::os::unix::prelude::*;
416 Ok(PathBuf::from(OsStr::from_bytes(bytes)))
417 }
418 #[cfg(windows)]
419 {
420 use std::str;
421 match str::from_utf8(bytes) {
422 Ok(s) => Ok(PathBuf::from(s)),
423 Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
424 }
425 }
426}
427
428pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
434 PathAncestors::new(path, stop_root_at)
435}
436
437pub struct PathAncestors<'a> {
438 current: Option<&'a Path>,
439 stop_at: Option<PathBuf>,
440}
441
442impl<'a> PathAncestors<'a> {
443 fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
444 let stop_at = env::var("__CARGO_TEST_ROOT")
445 .ok()
446 .map(PathBuf::from)
447 .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
448 PathAncestors {
449 current: Some(path),
450 stop_at,
452 }
453 }
454}
455
456impl<'a> Iterator for PathAncestors<'a> {
457 type Item = &'a Path;
458
459 fn next(&mut self) -> Option<&'a Path> {
460 if let Some(path) = self.current {
461 self.current = path.parent();
462
463 if let Some(ref stop_at) = self.stop_at {
464 if path == stop_at {
465 self.current = None;
466 }
467 }
468
469 Some(path)
470 } else {
471 None
472 }
473 }
474}
475
476pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
478 _create_dir_all(p.as_ref())
479}
480
481fn _create_dir_all(p: &Path) -> Result<()> {
482 fs::create_dir_all(p)
483 .with_context(|| format!("failed to create directory `{}`", p.display()))?;
484 Ok(())
485}
486
487pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
491 _remove_dir_all(p.as_ref()).or_else(|prev_err| {
492 fs::remove_dir_all(p.as_ref()).with_context(|| {
496 format!(
497 "{:?}\n\nError: failed to remove directory `{}`",
498 prev_err,
499 p.as_ref().display(),
500 )
501 })
502 })
503}
504
505fn _remove_dir_all(p: &Path) -> Result<()> {
506 if symlink_metadata(p)?.is_symlink() {
507 return remove_file(p);
508 }
509 let entries = p
510 .read_dir()
511 .with_context(|| format!("failed to read directory `{}`", p.display()))?;
512 for entry in entries {
513 let entry = entry?;
514 let path = entry.path();
515 if entry.file_type()?.is_dir() {
516 remove_dir_all(&path)?;
517 } else {
518 remove_file(&path)?;
519 }
520 }
521 remove_dir(&p)
522}
523
524pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
526 _remove_dir(p.as_ref())
527}
528
529fn _remove_dir(p: &Path) -> Result<()> {
530 fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
531 Ok(())
532}
533
534pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
541 _remove_file(p.as_ref())
542}
543
544fn _remove_file(p: &Path) -> Result<()> {
545 #[cfg(target_os = "windows")]
549 {
550 use std::os::windows::fs::FileTypeExt;
551 let metadata = symlink_metadata(p)?;
552 let file_type = metadata.file_type();
553 if file_type.is_symlink_dir() {
554 return remove_symlink_dir_with_permission_check(p);
555 }
556 }
557
558 remove_file_with_permission_check(p)
559}
560
561#[cfg(target_os = "windows")]
562fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
563 remove_with_permission_check(fs::remove_dir, p)
564 .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
565}
566
567fn remove_file_with_permission_check(p: &Path) -> Result<()> {
568 remove_with_permission_check(fs::remove_file, p)
569 .with_context(|| format!("failed to remove file `{}`", p.display()))
570}
571
572fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
573where
574 F: Fn(P) -> io::Result<()>,
575 P: AsRef<Path> + Clone,
576{
577 match remove_func(p.clone()) {
578 Ok(()) => Ok(()),
579 Err(e) => {
580 if e.kind() == io::ErrorKind::PermissionDenied
581 && set_not_readonly(p.as_ref()).unwrap_or(false)
582 {
583 remove_func(p)
584 } else {
585 Err(e)
586 }
587 }
588 }
589}
590
591fn set_not_readonly(p: &Path) -> io::Result<bool> {
592 let mut perms = p.metadata()?.permissions();
593 if !perms.readonly() {
594 return Ok(false);
595 }
596 perms.set_readonly(false);
597 fs::set_permissions(p, perms)?;
598 Ok(true)
599}
600
601pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
605 let src = src.as_ref();
606 let dst = dst.as_ref();
607 _link_or_copy(src, dst)
608}
609
610fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
611 tracing::debug!("linking {} to {}", src.display(), dst.display());
612 if same_file::is_same_file(src, dst).unwrap_or(false) {
613 return Ok(());
614 }
615
616 if fs::symlink_metadata(dst).is_ok() {
621 remove_file(&dst)?;
622 }
623
624 let link_result = if src.is_dir() {
625 #[cfg(unix)]
626 use std::os::unix::fs::symlink;
627 #[cfg(windows)]
628 use std::os::windows::fs::symlink_dir as symlink;
633
634 let dst_dir = dst.parent().unwrap();
635 let src = if src.starts_with(dst_dir) {
636 src.strip_prefix(dst_dir).unwrap()
637 } else {
638 src
639 };
640 symlink(src, dst)
641 } else {
642 if cfg!(target_os = "macos") {
643 fs::copy(src, dst).map_or_else(
654 |e| {
655 if e.raw_os_error()
656 .map_or(false, |os_err| os_err == 35 )
657 {
658 tracing::info!("copy failed {e:?}. falling back to fs::hard_link");
659
660 fs::hard_link(src, dst)
664 } else {
665 Err(e)
666 }
667 },
668 |_| Ok(()),
669 )
670 } else {
671 fs::hard_link(src, dst)
672 }
673 };
674 link_result
675 .or_else(|err| {
676 tracing::debug!("link failed {}. falling back to fs::copy", err);
677 fs::copy(src, dst).map(|_| ())
678 })
679 .with_context(|| {
680 format!(
681 "failed to link or copy `{}` to `{}`",
682 src.display(),
683 dst.display()
684 )
685 })?;
686 Ok(())
687}
688
689pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
693 let from = from.as_ref();
694 let to = to.as_ref();
695 fs::copy(from, to)
696 .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
697}
698
699pub fn set_file_time_no_err<P: AsRef<Path>>(path: P, time: FileTime) {
705 let path = path.as_ref();
706 match filetime::set_file_times(path, time, time) {
707 Ok(()) => tracing::debug!("set file mtime {} to {}", path.display(), time),
708 Err(e) => tracing::warn!(
709 "could not set mtime of {} to {}: {:?}",
710 path.display(),
711 time,
712 e
713 ),
714 }
715}
716
717pub fn strip_prefix_canonical(
723 path: impl AsRef<Path>,
724 base: impl AsRef<Path>,
725) -> Result<PathBuf, std::path::StripPrefixError> {
726 let safe_canonicalize = |path: &Path| match path.canonicalize() {
728 Ok(p) => p,
729 Err(e) => {
730 tracing::warn!("cannot canonicalize {:?}: {:?}", path, e);
731 path.to_path_buf()
732 }
733 };
734 let canon_path = safe_canonicalize(path.as_ref());
735 let canon_base = safe_canonicalize(base.as_ref());
736 canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
737}
738
739pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
747 let path = p.as_ref();
748 if path.is_dir() {
749 return Ok(());
750 }
751
752 let parent = path.parent().unwrap();
753 let base = path.file_name().unwrap();
754 create_dir_all(parent)?;
755 let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
767 exclude_from_backups(tempdir.path());
768 exclude_from_content_indexing(tempdir.path());
769 if let Err(e) = fs::rename(tempdir.path(), path) {
776 if !path.exists() {
777 return Err(anyhow::Error::from(e))
778 .with_context(|| format!("failed to create directory `{}`", path.display()));
779 }
780 }
781 Ok(())
782}
783
784pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
788 let path = p.as_ref();
789 exclude_from_backups(path);
790 exclude_from_content_indexing(path);
791}
792
793fn exclude_from_backups(path: &Path) {
801 exclude_from_time_machine(path);
802 let file = path.join("CACHEDIR.TAG");
803 if !file.exists() {
804 let _ = std::fs::write(
805 file,
806 "Signature: 8a477f597d28d172789f06886806bc55
807# This file is a cache directory tag created by cargo.
808# For information about cache directory tags see https://bford.info/cachedir/
809",
810 );
811 }
813}
814
815fn exclude_from_content_indexing(path: &Path) {
823 #[cfg(windows)]
824 {
825 use std::iter::once;
826 use std::os::windows::prelude::OsStrExt;
827 use windows_sys::Win32::Storage::FileSystem::{
828 GetFileAttributesW, SetFileAttributesW, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
829 };
830
831 let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
832 unsafe {
833 SetFileAttributesW(
834 path.as_ptr(),
835 GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
836 );
837 }
838 }
839 #[cfg(not(windows))]
840 {
841 let _ = path;
842 }
843}
844
845#[cfg(not(target_os = "macos"))]
846fn exclude_from_time_machine(_: &Path) {}
847
848#[cfg(target_os = "macos")]
849fn exclude_from_time_machine(path: &Path) {
851 use core_foundation::base::TCFType;
852 use core_foundation::{number, string, url};
853 use std::ptr;
854
855 let is_excluded_key: Result<string::CFString, _> = "NSURLIsExcludedFromBackupKey".parse();
857 let path = url::CFURL::from_path(path, false);
858 if let (Some(path), Ok(is_excluded_key)) = (path, is_excluded_key) {
859 unsafe {
860 url::CFURLSetResourcePropertyForKey(
861 path.as_concrete_TypeRef(),
862 is_excluded_key.as_concrete_TypeRef(),
863 number::kCFBooleanTrue as *const _,
864 ptr::null_mut(),
865 );
866 }
867 }
868 }
871
872#[cfg(test)]
873mod tests {
874 use super::join_paths;
875 use super::normalize_path;
876 use super::write;
877 use super::write_atomic;
878
879 #[test]
880 fn test_normalize_path() {
881 let cases = &[
882 ("", ""),
883 (".", ""),
884 (".////./.", ""),
885 ("/", "/"),
886 ("/..", "/"),
887 ("/foo/bar", "/foo/bar"),
888 ("/foo/bar/", "/foo/bar"),
889 ("/foo/bar/./././///", "/foo/bar"),
890 ("/foo/bar/..", "/foo"),
891 ("/foo/bar/../..", "/"),
892 ("/foo/bar/../../..", "/"),
893 ("foo/bar", "foo/bar"),
894 ("foo/bar/", "foo/bar"),
895 ("foo/bar/./././///", "foo/bar"),
896 ("foo/bar/..", "foo"),
897 ("foo/bar/../..", ""),
898 ("foo/bar/../../..", ".."),
899 ("../../foo/bar", "../../foo/bar"),
900 ("../../foo/bar/", "../../foo/bar"),
901 ("../../foo/bar/./././///", "../../foo/bar"),
902 ("../../foo/bar/..", "../../foo"),
903 ("../../foo/bar/../..", "../.."),
904 ("../../foo/bar/../../..", "../../.."),
905 ];
906 for (input, expected) in cases {
907 let actual = normalize_path(std::path::Path::new(input));
908 assert_eq!(actual, std::path::Path::new(expected), "input: {input}");
909 }
910 }
911
912 #[test]
913 fn write_works() {
914 let original_contents = "[dependencies]\nfoo = 0.1.0";
915
916 let tmpdir = tempfile::tempdir().unwrap();
917 let path = tmpdir.path().join("Cargo.toml");
918 write(&path, original_contents).unwrap();
919 let contents = std::fs::read_to_string(&path).unwrap();
920 assert_eq!(contents, original_contents);
921 }
922 #[test]
923 fn write_atomic_works() {
924 let original_contents = "[dependencies]\nfoo = 0.1.0";
925
926 let tmpdir = tempfile::tempdir().unwrap();
927 let path = tmpdir.path().join("Cargo.toml");
928 write_atomic(&path, original_contents).unwrap();
929 let contents = std::fs::read_to_string(&path).unwrap();
930 assert_eq!(contents, original_contents);
931 }
932
933 #[test]
934 #[cfg(unix)]
935 fn write_atomic_permissions() {
936 use std::os::unix::fs::PermissionsExt;
937
938 let original_perms = std::fs::Permissions::from_mode(u32::from(
939 libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH,
940 ));
941
942 let tmp = tempfile::Builder::new().tempfile().unwrap();
943
944 tmp.as_file()
946 .set_permissions(original_perms.clone())
947 .unwrap();
948
949 write_atomic(tmp.path(), "new").unwrap();
951 assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
952
953 let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
954
955 let mask = u32::from(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO);
956 assert_eq!(original_perms.mode(), new_perms.mode() & mask);
957 }
958
959 #[test]
960 fn join_paths_lists_paths_on_error() {
961 let valid_paths = vec!["/testing/one", "/testing/two"];
962 let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
964
965 #[cfg(unix)]
966 {
967 let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
968 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
969 assert_eq!(
970 err.to_string(),
971 "failed to join paths from `$TESTING2` together\n\n\
972 Check if any of path segments listed below contain an \
973 unterminated quote character or path separator:\
974 \n \"/testing/one\"\
975 \n \"/testing/t:wo/three\"\
976 "
977 );
978 }
979 #[cfg(windows)]
980 {
981 let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
982 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
983 assert_eq!(
984 err.to_string(),
985 "failed to join paths from `$TESTING2` together\n\n\
986 Check if any of path segments listed below contain an \
987 unterminated quote character or path separator:\
988 \n \"/testing/one\"\
989 \n \"/testing/t\\\"wo/three\"\
990 "
991 );
992 }
993 }
994
995 #[test]
996 fn write_atomic_symlink() {
997 let tmpdir = tempfile::tempdir().unwrap();
998 let target_path = tmpdir.path().join("target.txt");
999 let symlink_path = tmpdir.path().join("symlink.txt");
1000
1001 write(&target_path, "initial").unwrap();
1003
1004 #[cfg(unix)]
1006 std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
1007 #[cfg(windows)]
1008 std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap();
1009
1010 write_atomic(&symlink_path, "updated").unwrap();
1012
1013 assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1015 assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated");
1016
1017 assert!(symlink_path.is_symlink());
1019 assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path);
1020 }
1021
1022 #[test]
1023 #[cfg(windows)]
1024 fn test_remove_symlink_dir() {
1025 use super::*;
1026 use std::fs;
1027 use std::os::windows::fs::symlink_dir;
1028
1029 let tmpdir = tempfile::tempdir().unwrap();
1030 let dir_path = tmpdir.path().join("testdir");
1031 let symlink_path = tmpdir.path().join("symlink");
1032
1033 fs::create_dir(&dir_path).unwrap();
1034
1035 symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
1036
1037 assert!(symlink_path.exists());
1038
1039 assert!(remove_file(symlink_path.clone()).is_ok());
1040
1041 assert!(!symlink_path.exists());
1042 assert!(dir_path.exists());
1043 }
1044
1045 #[test]
1046 #[cfg(windows)]
1047 fn test_remove_symlink_file() {
1048 use super::*;
1049 use std::fs;
1050 use std::os::windows::fs::symlink_file;
1051
1052 let tmpdir = tempfile::tempdir().unwrap();
1053 let file_path = tmpdir.path().join("testfile");
1054 let symlink_path = tmpdir.path().join("symlink");
1055
1056 fs::write(&file_path, b"test").unwrap();
1057
1058 symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
1059
1060 assert!(symlink_path.exists());
1061
1062 assert!(remove_file(symlink_path.clone()).is_ok());
1063
1064 assert!(!symlink_path.exists());
1065 assert!(file_path.exists());
1066 }
1067}