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 if cfg!(target_os = "haiku") {
61 "LIBRARY_PATH"
62 } else {
63 "LD_LIBRARY_PATH"
64 }
65}
66
67pub fn dylib_path() -> Vec<PathBuf> {
72 match env::var_os(dylib_path_envvar()) {
73 Some(var) => env::split_paths(&var).collect(),
74 None => Vec::new(),
75 }
76}
77
78pub fn normalize_path(path: &Path) -> PathBuf {
87 let mut components = path.components().peekable();
88 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
89 components.next();
90 PathBuf::from(c.as_os_str())
91 } else {
92 PathBuf::new()
93 };
94
95 for component in components {
96 match component {
97 Component::Prefix(..) => unreachable!(),
98 Component::RootDir => {
99 ret.push(Component::RootDir);
100 }
101 Component::CurDir => {}
102 Component::ParentDir => {
103 if ret.ends_with(Component::ParentDir) {
104 ret.push(Component::ParentDir);
105 } else {
106 let popped = ret.pop();
107 if !popped && !ret.has_root() {
108 ret.push(Component::ParentDir);
109 }
110 }
111 }
112 Component::Normal(c) => {
113 ret.push(c);
114 }
115 }
116 }
117 ret
118}
119
120pub fn resolve_executable(exec: &Path) -> Result<PathBuf> {
125 if exec.components().count() == 1 {
126 let paths = env::var_os("PATH").ok_or_else(|| anyhow::format_err!("no PATH"))?;
127 let candidates = env::split_paths(&paths).flat_map(|path| {
128 let candidate = path.join(&exec);
129 let with_exe = if env::consts::EXE_EXTENSION.is_empty() {
130 None
131 } else {
132 Some(candidate.with_extension(env::consts::EXE_EXTENSION))
133 };
134 iter::once(candidate).chain(with_exe)
135 });
136 for candidate in candidates {
137 if candidate.is_file() {
138 return Ok(candidate);
139 }
140 }
141
142 anyhow::bail!("no executable for `{}` found in PATH", exec.display())
143 } else {
144 Ok(exec.into())
145 }
146}
147
148pub fn metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
152 let path = path.as_ref();
153 std::fs::metadata(path)
154 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
155}
156
157pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<Metadata> {
161 let path = path.as_ref();
162 std::fs::symlink_metadata(path)
163 .with_context(|| format!("failed to load metadata for path `{}`", path.display()))
164}
165
166pub fn read(path: &Path) -> Result<String> {
170 match String::from_utf8(read_bytes(path)?) {
171 Ok(s) => Ok(s),
172 Err(_) => anyhow::bail!("path at `{}` was not valid utf-8", path.display()),
173 }
174}
175
176pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
180 fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
181}
182
183pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
187 let path = path.as_ref();
188 fs::write(path, contents.as_ref())
189 .with_context(|| format!("failed to write `{}`", path.display()))
190}
191
192pub fn write_atomic<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
197 let path = path.as_ref();
198
199 let resolved_path;
201 let path = if path.is_symlink() {
202 resolved_path = fs::read_link(path)
203 .with_context(|| format!("failed to read symlink at `{}`", path.display()))?;
204 &resolved_path
205 } else {
206 path
207 };
208
209 #[cfg(unix)]
213 let perms = path.metadata().ok().map(|meta| {
214 use std::os::unix::fs::PermissionsExt;
215
216 let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
218 let mode = meta.permissions().mode() & mask;
219
220 std::fs::Permissions::from_mode(mode)
221 });
222
223 let mut tmp = TempFileBuilder::new()
224 .prefix(path.file_name().unwrap())
225 .tempfile_in(path.parent().unwrap())?;
226 tmp.write_all(contents.as_ref())?;
227
228 #[cfg(unix)]
232 if let Some(perms) = perms {
233 tmp.as_file().set_permissions(perms)?;
234 }
235
236 tmp.persist(path)?;
237 Ok(())
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 .open(&path)?;
250 let mut orig = Vec::new();
251 f.read_to_end(&mut orig)?;
252 if orig != contents {
253 f.set_len(0)?;
254 f.seek(io::SeekFrom::Start(0))?;
255 f.write_all(contents)?;
256 }
257 Ok(())
258 })()
259 .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
260 Ok(())
261}
262
263pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
266 (|| -> Result<()> {
267 let mut f = OpenOptions::new()
268 .write(true)
269 .append(true)
270 .create(true)
271 .open(path)?;
272
273 f.write_all(contents)?;
274 Ok(())
275 })()
276 .with_context(|| format!("failed to write `{}`", path.display()))?;
277 Ok(())
278}
279
280pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
282 let path = path.as_ref();
283 File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
284}
285
286pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
288 let path = path.as_ref();
289 File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
290}
291
292pub fn mtime(path: &Path) -> Result<FileTime> {
294 let meta = metadata(path)?;
295 Ok(FileTime::from_last_modification_time(&meta))
296}
297
298pub fn mtime_recursive(path: &Path) -> Result<FileTime> {
301 let meta = metadata(path)?;
302 if !meta.is_dir() {
303 return Ok(FileTime::from_last_modification_time(&meta));
304 }
305 let max_meta = walkdir::WalkDir::new(path)
306 .follow_links(true)
307 .into_iter()
308 .filter_map(|e| match e {
309 Ok(e) => Some(e),
310 Err(e) => {
311 tracing::debug!("failed to determine mtime while walking directory: {}", e);
314 None
315 }
316 })
317 .filter_map(|e| {
318 if e.path_is_symlink() {
319 let sym_meta = match std::fs::symlink_metadata(e.path()) {
323 Ok(m) => m,
324 Err(err) => {
325 tracing::debug!(
329 "failed to determine mtime while fetching symlink metadata of {}: {}",
330 e.path().display(),
331 err
332 );
333 return None;
334 }
335 };
336 let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
337 match e.metadata() {
339 Ok(target_meta) => {
340 let target_mtime = FileTime::from_last_modification_time(&target_meta);
341 Some(sym_mtime.max(target_mtime))
342 }
343 Err(err) => {
344 tracing::debug!(
348 "failed to determine mtime of symlink target for {}: {}",
349 e.path().display(),
350 err
351 );
352 Some(sym_mtime)
353 }
354 }
355 } else {
356 let meta = match e.metadata() {
357 Ok(m) => m,
358 Err(err) => {
359 tracing::debug!(
363 "failed to determine mtime while fetching metadata of {}: {}",
364 e.path().display(),
365 err
366 );
367 return None;
368 }
369 };
370 Some(FileTime::from_last_modification_time(&meta))
371 }
372 })
373 .max()
374 .unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
376 Ok(max_meta)
377}
378
379pub fn set_invocation_time(path: &Path) -> Result<FileTime> {
382 let timestamp = path.join("invoked.timestamp");
385 write(
386 ×tamp,
387 "This file has an mtime of when this was started.",
388 )?;
389 let ft = mtime(×tamp)?;
390 tracing::debug!("invocation time for {:?} is {}", path, ft);
391 Ok(ft)
392}
393
394pub fn path2bytes(path: &Path) -> Result<&[u8]> {
396 #[cfg(unix)]
397 {
398 use std::os::unix::prelude::*;
399 Ok(path.as_os_str().as_bytes())
400 }
401 #[cfg(windows)]
402 {
403 match path.as_os_str().to_str() {
404 Some(s) => Ok(s.as_bytes()),
405 None => Err(anyhow::format_err!(
406 "invalid non-unicode path: {}",
407 path.display()
408 )),
409 }
410 }
411}
412
413pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
415 #[cfg(unix)]
416 {
417 use std::os::unix::prelude::*;
418 Ok(PathBuf::from(OsStr::from_bytes(bytes)))
419 }
420 #[cfg(windows)]
421 {
422 use std::str;
423 match str::from_utf8(bytes) {
424 Ok(s) => Ok(PathBuf::from(s)),
425 Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
426 }
427 }
428}
429
430pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
436 PathAncestors::new(path, stop_root_at)
437}
438
439pub struct PathAncestors<'a> {
440 current: Option<&'a Path>,
441 stop_at: Option<PathBuf>,
442}
443
444impl<'a> PathAncestors<'a> {
445 fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
446 let stop_at = env::var("__CARGO_TEST_ROOT")
447 .ok()
448 .map(PathBuf::from)
449 .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
450 PathAncestors {
451 current: Some(path),
452 stop_at,
454 }
455 }
456}
457
458impl<'a> Iterator for PathAncestors<'a> {
459 type Item = &'a Path;
460
461 fn next(&mut self) -> Option<&'a Path> {
462 if let Some(path) = self.current {
463 self.current = path.parent();
464
465 if let Some(ref stop_at) = self.stop_at {
466 if path == stop_at {
467 self.current = None;
468 }
469 }
470
471 Some(path)
472 } else {
473 None
474 }
475 }
476}
477
478pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
480 _create_dir_all(p.as_ref())
481}
482
483fn _create_dir_all(p: &Path) -> Result<()> {
484 fs::create_dir_all(p)
485 .with_context(|| format!("failed to create directory `{}`", p.display()))?;
486 Ok(())
487}
488
489pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
493 _remove_dir_all(p.as_ref()).or_else(|prev_err| {
494 fs::remove_dir_all(p.as_ref()).with_context(|| {
498 format!(
499 "{:?}\n\nError: failed to remove directory `{}`",
500 prev_err,
501 p.as_ref().display(),
502 )
503 })
504 })
505}
506
507fn _remove_dir_all(p: &Path) -> Result<()> {
508 if symlink_metadata(p)?.is_symlink() {
509 return remove_file(p);
510 }
511 let entries = p
512 .read_dir()
513 .with_context(|| format!("failed to read directory `{}`", p.display()))?;
514 for entry in entries {
515 let entry = entry?;
516 let path = entry.path();
517 if entry.file_type()?.is_dir() {
518 remove_dir_all(&path)?;
519 } else {
520 remove_file(&path)?;
521 }
522 }
523 remove_dir(&p)
524}
525
526pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
528 _remove_dir(p.as_ref())
529}
530
531fn _remove_dir(p: &Path) -> Result<()> {
532 fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
533 Ok(())
534}
535
536pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
543 _remove_file(p.as_ref())
544}
545
546fn _remove_file(p: &Path) -> Result<()> {
547 #[cfg(target_os = "windows")]
551 {
552 use std::os::windows::fs::FileTypeExt;
553 let metadata = symlink_metadata(p)?;
554 let file_type = metadata.file_type();
555 if file_type.is_symlink_dir() {
556 return remove_symlink_dir_with_permission_check(p);
557 }
558 }
559
560 remove_file_with_permission_check(p)
561}
562
563#[cfg(target_os = "windows")]
564fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
565 remove_with_permission_check(fs::remove_dir, p)
566 .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
567}
568
569fn remove_file_with_permission_check(p: &Path) -> Result<()> {
570 remove_with_permission_check(fs::remove_file, p)
571 .with_context(|| format!("failed to remove file `{}`", p.display()))
572}
573
574fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
575where
576 F: Fn(P) -> io::Result<()>,
577 P: AsRef<Path> + Clone,
578{
579 match remove_func(p.clone()) {
580 Ok(()) => Ok(()),
581 Err(e) => {
582 if e.kind() == io::ErrorKind::PermissionDenied
583 && set_not_readonly(p.as_ref()).unwrap_or(false)
584 {
585 remove_func(p)
586 } else {
587 Err(e)
588 }
589 }
590 }
591}
592
593fn set_not_readonly(p: &Path) -> io::Result<bool> {
594 let mut perms = p.metadata()?.permissions();
595 if !perms.readonly() {
596 return Ok(false);
597 }
598 perms.set_readonly(false);
599 fs::set_permissions(p, perms)?;
600 Ok(true)
601}
602
603pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
607 let src = src.as_ref();
608 let dst = dst.as_ref();
609 _link_or_copy(src, dst)
610}
611
612fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
613 tracing::debug!("linking {} to {}", src.display(), dst.display());
614 if same_file::is_same_file(src, dst).unwrap_or(false) {
615 return Ok(());
616 }
617
618 if fs::symlink_metadata(dst).is_ok() {
623 remove_file(&dst)?;
624 }
625
626 let link_result = if src.is_dir() {
627 #[cfg(unix)]
628 use std::os::unix::fs::symlink;
629 #[cfg(windows)]
630 use std::os::windows::fs::symlink_dir as symlink;
635
636 let dst_dir = dst.parent().unwrap();
637 let src = if src.starts_with(dst_dir) {
638 src.strip_prefix(dst_dir).unwrap()
639 } else {
640 src
641 };
642 symlink(src, dst)
643 } else {
644 if cfg!(target_os = "macos") {
645 fs::copy(src, dst).map_or_else(
656 |e| {
657 if e.raw_os_error()
658 .map_or(false, |os_err| os_err == 35 )
659 {
660 tracing::info!("copy failed {e:?}. falling back to fs::hard_link");
661
662 fs::hard_link(src, dst)
666 } else {
667 Err(e)
668 }
669 },
670 |_| Ok(()),
671 )
672 } else {
673 fs::hard_link(src, dst)
674 }
675 };
676 link_result
677 .or_else(|err| {
678 tracing::debug!("link failed {}. falling back to fs::copy", err);
679 fs::copy(src, dst).map(|_| ())
680 })
681 .with_context(|| {
682 format!(
683 "failed to link or copy `{}` to `{}`",
684 src.display(),
685 dst.display()
686 )
687 })?;
688 Ok(())
689}
690
691pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
695 let from = from.as_ref();
696 let to = to.as_ref();
697 fs::copy(from, to)
698 .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
699}
700
701pub fn set_file_time_no_err<P: AsRef<Path>>(path: P, time: FileTime) {
707 let path = path.as_ref();
708 match filetime::set_file_times(path, time, time) {
709 Ok(()) => tracing::debug!("set file mtime {} to {}", path.display(), time),
710 Err(e) => tracing::warn!(
711 "could not set mtime of {} to {}: {:?}",
712 path.display(),
713 time,
714 e
715 ),
716 }
717}
718
719pub fn strip_prefix_canonical(
725 path: impl AsRef<Path>,
726 base: impl AsRef<Path>,
727) -> Result<PathBuf, std::path::StripPrefixError> {
728 let safe_canonicalize = |path: &Path| match path.canonicalize() {
730 Ok(p) => p,
731 Err(e) => {
732 tracing::warn!("cannot canonicalize {:?}: {:?}", path, e);
733 path.to_path_buf()
734 }
735 };
736 let canon_path = safe_canonicalize(path.as_ref());
737 let canon_base = safe_canonicalize(base.as_ref());
738 canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
739}
740
741pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
749 let path = p.as_ref();
750 if path.is_dir() {
751 return Ok(());
752 }
753
754 let parent = path.parent().unwrap();
755 let base = path.file_name().unwrap();
756 create_dir_all(parent)?;
757 let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
769 exclude_from_backups(tempdir.path());
770 exclude_from_content_indexing(tempdir.path());
771 if let Err(e) = fs::rename(tempdir.path(), path) {
778 if !path.exists() {
779 return Err(anyhow::Error::from(e))
780 .with_context(|| format!("failed to create directory `{}`", path.display()));
781 }
782 }
783 Ok(())
784}
785
786pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
790 let path = p.as_ref();
791 exclude_from_backups(path);
792 exclude_from_content_indexing(path);
793}
794
795fn exclude_from_backups(path: &Path) {
803 exclude_from_time_machine_and_cloud_sync(path);
804 let file = path.join("CACHEDIR.TAG");
805 if !file.exists() {
806 let _ = std::fs::write(
807 file,
808 "Signature: 8a477f597d28d172789f06886806bc55
809# This file is a cache directory tag created by cargo.
810# For information about cache directory tags see https://bford.info/cachedir/
811",
812 );
813 }
815}
816
817fn exclude_from_content_indexing(path: &Path) {
825 #[cfg(windows)]
826 {
827 use std::iter::once;
828 use std::os::windows::prelude::OsStrExt;
829 use windows_sys::Win32::Storage::FileSystem::{
830 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
831 };
832
833 let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
834 unsafe {
835 SetFileAttributesW(
836 path.as_ptr(),
837 GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
838 );
839 }
840 }
841 #[cfg(not(windows))]
842 {
843 let _ = path;
844 }
845}
846
847#[cfg(not(target_os = "macos"))]
848fn exclude_from_time_machine_and_cloud_sync(_: &Path) {}
849
850#[cfg(target_os = "macos")]
851fn exclude_from_time_machine_and_cloud_sync(path: &Path) {
853 use core_foundation::base::TCFType;
854 use core_foundation::{number, string, url};
855 use std::ptr;
856
857 let path = match url::CFURL::from_path(path, false) {
858 Some(url) => url,
859 None => return,
860 };
861
862 const KEY_NAMES: [&str; 2] = [
864 "NSURLIsExcludedFromBackupKey", "NSURLUbiquitousItemIsExcludedFromSyncKey", ];
867
868 for key_name in KEY_NAMES {
869 let is_excluded_key = match key_name.parse::<string::CFString>() {
870 Ok(key) => key,
871 Err(_) => continue,
872 };
873 unsafe {
874 url::CFURLSetResourcePropertyForKey(
875 path.as_concrete_TypeRef(),
876 is_excluded_key.as_concrete_TypeRef(),
877 number::kCFBooleanTrue as *const _,
878 ptr::null_mut(),
879 );
880 }
881 }
882 }
885
886#[cfg(test)]
887mod tests {
888 use super::join_paths;
889 use super::normalize_path;
890 use super::write;
891 use super::write_atomic;
892
893 #[test]
894 fn test_normalize_path() {
895 let cases = &[
896 ("", ""),
897 (".", ""),
898 (".////./.", ""),
899 ("/", "/"),
900 ("/..", "/"),
901 ("/foo/bar", "/foo/bar"),
902 ("/foo/bar/", "/foo/bar"),
903 ("/foo/bar/./././///", "/foo/bar"),
904 ("/foo/bar/..", "/foo"),
905 ("/foo/bar/../..", "/"),
906 ("/foo/bar/../../..", "/"),
907 ("foo/bar", "foo/bar"),
908 ("foo/bar/", "foo/bar"),
909 ("foo/bar/./././///", "foo/bar"),
910 ("foo/bar/..", "foo"),
911 ("foo/bar/../..", ""),
912 ("foo/bar/../../..", ".."),
913 ("../../foo/bar", "../../foo/bar"),
914 ("../../foo/bar/", "../../foo/bar"),
915 ("../../foo/bar/./././///", "../../foo/bar"),
916 ("../../foo/bar/..", "../../foo"),
917 ("../../foo/bar/../..", "../.."),
918 ("../../foo/bar/../../..", "../../.."),
919 ];
920 for (input, expected) in cases {
921 let actual = normalize_path(std::path::Path::new(input));
922 assert_eq!(actual, std::path::Path::new(expected), "input: {input}");
923 }
924 }
925
926 #[test]
927 fn write_works() {
928 let original_contents = "[dependencies]\nfoo = 0.1.0";
929
930 let tmpdir = tempfile::tempdir().unwrap();
931 let path = tmpdir.path().join("Cargo.toml");
932 write(&path, original_contents).unwrap();
933 let contents = std::fs::read_to_string(&path).unwrap();
934 assert_eq!(contents, original_contents);
935 }
936 #[test]
937 fn write_atomic_works() {
938 let original_contents = "[dependencies]\nfoo = 0.1.0";
939
940 let tmpdir = tempfile::tempdir().unwrap();
941 let path = tmpdir.path().join("Cargo.toml");
942 write_atomic(&path, original_contents).unwrap();
943 let contents = std::fs::read_to_string(&path).unwrap();
944 assert_eq!(contents, original_contents);
945 }
946
947 #[test]
948 #[cfg(unix)]
949 fn write_atomic_permissions() {
950 use std::os::unix::fs::PermissionsExt;
951
952 let original_perms = std::fs::Permissions::from_mode(
953 (libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH) as u32,
954 );
955
956 let tmp = tempfile::Builder::new().tempfile().unwrap();
957
958 tmp.as_file()
960 .set_permissions(original_perms.clone())
961 .unwrap();
962
963 write_atomic(tmp.path(), "new").unwrap();
965 assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
966
967 let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
968
969 let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
970 assert_eq!(original_perms.mode(), new_perms.mode() & mask);
971 }
972
973 #[test]
974 fn join_paths_lists_paths_on_error() {
975 let valid_paths = vec!["/testing/one", "/testing/two"];
976 let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
978
979 #[cfg(unix)]
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 #[cfg(windows)]
994 {
995 let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
996 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
997 assert_eq!(
998 err.to_string(),
999 "failed to join paths from `$TESTING2` together\n\n\
1000 Check if any of path segments listed below contain an \
1001 unterminated quote character or path separator:\
1002 \n \"/testing/one\"\
1003 \n \"/testing/t\\\"wo/three\"\
1004 "
1005 );
1006 }
1007 }
1008
1009 #[test]
1010 fn write_atomic_symlink() {
1011 let tmpdir = tempfile::tempdir().unwrap();
1012 let target_path = tmpdir.path().join("target.txt");
1013 let symlink_path = tmpdir.path().join("symlink.txt");
1014
1015 write(&target_path, "initial").unwrap();
1017
1018 #[cfg(unix)]
1020 std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
1021 #[cfg(windows)]
1022 std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap();
1023
1024 write_atomic(&symlink_path, "updated").unwrap();
1026
1027 assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1029 assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated");
1030
1031 assert!(symlink_path.is_symlink());
1033 assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path);
1034 }
1035
1036 #[test]
1037 #[cfg(windows)]
1038 fn test_remove_symlink_dir() {
1039 use super::*;
1040 use std::fs;
1041 use std::os::windows::fs::symlink_dir;
1042
1043 let tmpdir = tempfile::tempdir().unwrap();
1044 let dir_path = tmpdir.path().join("testdir");
1045 let symlink_path = tmpdir.path().join("symlink");
1046
1047 fs::create_dir(&dir_path).unwrap();
1048
1049 symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
1050
1051 assert!(symlink_path.exists());
1052
1053 assert!(remove_file(symlink_path.clone()).is_ok());
1054
1055 assert!(!symlink_path.exists());
1056 assert!(dir_path.exists());
1057 }
1058
1059 #[test]
1060 #[cfg(windows)]
1061 fn test_remove_symlink_file() {
1062 use super::*;
1063 use std::fs;
1064 use std::os::windows::fs::symlink_file;
1065
1066 let tmpdir = tempfile::tempdir().unwrap();
1067 let file_path = tmpdir.path().join("testfile");
1068 let symlink_path = tmpdir.path().join("symlink");
1069
1070 fs::write(&file_path, b"test").unwrap();
1071
1072 symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
1073
1074 assert!(symlink_path.exists());
1075
1076 assert!(remove_file(symlink_path.clone()).is_ok());
1077
1078 assert!(!symlink_path.exists());
1079 assert!(file_path.exists());
1080 }
1081}