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 let target = fs::read_link(path)
203 .with_context(|| format!("failed to read symlink at `{}`", path.display()))?;
204 resolved_path = path.parent().unwrap().join(target);
205 &resolved_path
206 } else {
207 path
208 };
209
210 #[cfg(unix)]
214 let perms = path.metadata().ok().map(|meta| {
215 use std::os::unix::fs::PermissionsExt;
216
217 let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
219 let mode = meta.permissions().mode() & mask;
220
221 std::fs::Permissions::from_mode(mode)
222 });
223
224 let mut tmp = TempFileBuilder::new()
225 .prefix(path.file_name().unwrap())
226 .tempfile_in(path.parent().unwrap())?;
227 tmp.write_all(contents.as_ref())?;
228
229 #[cfg(unix)]
233 if let Some(perms) = perms {
234 tmp.as_file().set_permissions(perms)?;
235 }
236
237 tmp.persist(path)?;
238 Ok(())
239}
240
241pub fn write_if_changed<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
244 (|| -> Result<()> {
245 let contents = contents.as_ref();
246 let mut f = OpenOptions::new()
247 .read(true)
248 .write(true)
249 .create(true)
250 .open(&path)?;
251 let mut orig = Vec::new();
252 f.read_to_end(&mut orig)?;
253 if orig != contents {
254 f.set_len(0)?;
255 f.seek(io::SeekFrom::Start(0))?;
256 f.write_all(contents)?;
257 }
258 Ok(())
259 })()
260 .with_context(|| format!("failed to write `{}`", path.as_ref().display()))?;
261 Ok(())
262}
263
264pub fn append(path: &Path, contents: &[u8]) -> Result<()> {
267 (|| -> Result<()> {
268 let mut f = OpenOptions::new()
269 .write(true)
270 .append(true)
271 .create(true)
272 .open(path)?;
273
274 f.write_all(contents)?;
275 Ok(())
276 })()
277 .with_context(|| format!("failed to write `{}`", path.display()))?;
278 Ok(())
279}
280
281pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
283 let path = path.as_ref();
284 File::create(path).with_context(|| format!("failed to create file `{}`", path.display()))
285}
286
287pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
289 let path = path.as_ref();
290 File::open(path).with_context(|| format!("failed to open file `{}`", path.display()))
291}
292
293pub fn mtime(path: &Path) -> Result<FileTime> {
295 let meta = metadata(path)?;
296 Ok(FileTime::from_last_modification_time(&meta))
297}
298
299pub fn mtime_recursive(path: &Path) -> Result<FileTime> {
302 let meta = metadata(path)?;
303 if !meta.is_dir() {
304 return Ok(FileTime::from_last_modification_time(&meta));
305 }
306 let max_meta = walkdir::WalkDir::new(path)
307 .follow_links(true)
308 .into_iter()
309 .filter_map(|e| match e {
310 Ok(e) => Some(e),
311 Err(e) => {
312 tracing::debug!("failed to determine mtime while walking directory: {}", e);
315 None
316 }
317 })
318 .filter_map(|e| {
319 if e.path_is_symlink() {
320 let sym_meta = match std::fs::symlink_metadata(e.path()) {
324 Ok(m) => m,
325 Err(err) => {
326 tracing::debug!(
330 "failed to determine mtime while fetching symlink metadata of {}: {}",
331 e.path().display(),
332 err
333 );
334 return None;
335 }
336 };
337 let sym_mtime = FileTime::from_last_modification_time(&sym_meta);
338 match e.metadata() {
340 Ok(target_meta) => {
341 let target_mtime = FileTime::from_last_modification_time(&target_meta);
342 Some(sym_mtime.max(target_mtime))
343 }
344 Err(err) => {
345 tracing::debug!(
349 "failed to determine mtime of symlink target for {}: {}",
350 e.path().display(),
351 err
352 );
353 Some(sym_mtime)
354 }
355 }
356 } else {
357 let meta = match e.metadata() {
358 Ok(m) => m,
359 Err(err) => {
360 tracing::debug!(
364 "failed to determine mtime while fetching metadata of {}: {}",
365 e.path().display(),
366 err
367 );
368 return None;
369 }
370 };
371 Some(FileTime::from_last_modification_time(&meta))
372 }
373 })
374 .max()
375 .unwrap_or_else(|| FileTime::from_last_modification_time(&meta));
377 Ok(max_meta)
378}
379
380pub fn set_invocation_time(path: &Path) -> Result<FileTime> {
383 let timestamp = path.join("invoked.timestamp");
386 write(
387 ×tamp,
388 "This file has an mtime of when this was started.",
389 )?;
390 let ft = mtime(×tamp)?;
391 tracing::debug!("invocation time for {:?} is {}", path, ft);
392 Ok(ft)
393}
394
395pub fn path2bytes(path: &Path) -> Result<&[u8]> {
397 #[cfg(unix)]
398 {
399 use std::os::unix::prelude::*;
400 Ok(path.as_os_str().as_bytes())
401 }
402 #[cfg(windows)]
403 {
404 match path.as_os_str().to_str() {
405 Some(s) => Ok(s.as_bytes()),
406 None => Err(anyhow::format_err!(
407 "invalid non-unicode path: {}",
408 path.display()
409 )),
410 }
411 }
412}
413
414pub fn bytes2path(bytes: &[u8]) -> Result<PathBuf> {
416 #[cfg(unix)]
417 {
418 use std::os::unix::prelude::*;
419 Ok(PathBuf::from(OsStr::from_bytes(bytes)))
420 }
421 #[cfg(windows)]
422 {
423 use std::str;
424 match str::from_utf8(bytes) {
425 Ok(s) => Ok(PathBuf::from(s)),
426 Err(..) => Err(anyhow::format_err!("invalid non-unicode path")),
427 }
428 }
429}
430
431pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
437 PathAncestors::new(path, stop_root_at)
438}
439
440pub struct PathAncestors<'a> {
441 current: Option<&'a Path>,
442 stop_at: Option<PathBuf>,
443}
444
445impl<'a> PathAncestors<'a> {
446 fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> {
447 let stop_at = env::var("__CARGO_TEST_ROOT")
448 .ok()
449 .map(PathBuf::from)
450 .or_else(|| stop_root_at.map(|p| p.to_path_buf()));
451 PathAncestors {
452 current: Some(path),
453 stop_at,
455 }
456 }
457}
458
459impl<'a> Iterator for PathAncestors<'a> {
460 type Item = &'a Path;
461
462 fn next(&mut self) -> Option<&'a Path> {
463 if let Some(path) = self.current {
464 self.current = path.parent();
465
466 if let Some(ref stop_at) = self.stop_at {
467 if path == stop_at {
468 self.current = None;
469 }
470 }
471
472 Some(path)
473 } else {
474 None
475 }
476 }
477}
478
479pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
481 _create_dir_all(p.as_ref())
482}
483
484fn _create_dir_all(p: &Path) -> Result<()> {
485 fs::create_dir_all(p)
486 .with_context(|| format!("failed to create directory `{}`", p.display()))?;
487 Ok(())
488}
489
490pub fn remove_dir_all<P: AsRef<Path>>(p: P) -> Result<()> {
494 _remove_dir_all(p.as_ref()).or_else(|prev_err| {
495 fs::remove_dir_all(p.as_ref()).with_context(|| {
499 format!(
500 "{:?}\n\nError: failed to remove directory `{}`",
501 prev_err,
502 p.as_ref().display(),
503 )
504 })
505 })
506}
507
508fn _remove_dir_all(p: &Path) -> Result<()> {
509 if symlink_metadata(p)?.is_symlink() {
510 return remove_file(p);
511 }
512 let entries = p
513 .read_dir()
514 .with_context(|| format!("failed to read directory `{}`", p.display()))?;
515 for entry in entries {
516 let entry = entry?;
517 let path = entry.path();
518 if entry.file_type()?.is_dir() {
519 remove_dir_all(&path)?;
520 } else {
521 remove_file(&path)?;
522 }
523 }
524 remove_dir(&p)
525}
526
527pub fn remove_dir<P: AsRef<Path>>(p: P) -> Result<()> {
529 _remove_dir(p.as_ref())
530}
531
532fn _remove_dir(p: &Path) -> Result<()> {
533 fs::remove_dir(p).with_context(|| format!("failed to remove directory `{}`", p.display()))?;
534 Ok(())
535}
536
537pub fn remove_file<P: AsRef<Path>>(p: P) -> Result<()> {
544 _remove_file(p.as_ref())
545}
546
547fn _remove_file(p: &Path) -> Result<()> {
548 #[cfg(target_os = "windows")]
552 {
553 use std::os::windows::fs::FileTypeExt;
554 let metadata = symlink_metadata(p)?;
555 let file_type = metadata.file_type();
556 if file_type.is_symlink_dir() {
557 return remove_symlink_dir_with_permission_check(p);
558 }
559 }
560
561 remove_file_with_permission_check(p)
562}
563
564#[cfg(target_os = "windows")]
565fn remove_symlink_dir_with_permission_check(p: &Path) -> Result<()> {
566 remove_with_permission_check(fs::remove_dir, p)
567 .with_context(|| format!("failed to remove symlink dir `{}`", p.display()))
568}
569
570fn remove_file_with_permission_check(p: &Path) -> Result<()> {
571 remove_with_permission_check(fs::remove_file, p)
572 .with_context(|| format!("failed to remove file `{}`", p.display()))
573}
574
575fn remove_with_permission_check<F, P>(remove_func: F, p: P) -> io::Result<()>
576where
577 F: Fn(P) -> io::Result<()>,
578 P: AsRef<Path> + Clone,
579{
580 match remove_func(p.clone()) {
581 Ok(()) => Ok(()),
582 Err(e) => {
583 if e.kind() == io::ErrorKind::PermissionDenied
584 && set_not_readonly(p.as_ref()).unwrap_or(false)
585 {
586 remove_func(p)
587 } else {
588 Err(e)
589 }
590 }
591 }
592}
593
594fn set_not_readonly(p: &Path) -> io::Result<bool> {
595 let mut perms = p.metadata()?.permissions();
596 if !perms.readonly() {
597 return Ok(false);
598 }
599 perms.set_readonly(false);
600 fs::set_permissions(p, perms)?;
601 Ok(true)
602}
603
604pub fn link_or_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
608 let src = src.as_ref();
609 let dst = dst.as_ref();
610 _link_or_copy(src, dst)
611}
612
613fn _link_or_copy(src: &Path, dst: &Path) -> Result<()> {
614 tracing::debug!("linking {} to {}", src.display(), dst.display());
615 if same_file::is_same_file(src, dst).unwrap_or(false) {
616 return Ok(());
617 }
618
619 if fs::symlink_metadata(dst).is_ok() {
624 remove_file(&dst)?;
625 }
626
627 let link_result = if src.is_dir() {
628 #[cfg(unix)]
629 use std::os::unix::fs::symlink;
630 #[cfg(windows)]
631 use std::os::windows::fs::symlink_dir as symlink;
636
637 let dst_dir = dst.parent().unwrap();
638 let src = if src.starts_with(dst_dir) {
639 src.strip_prefix(dst_dir).unwrap()
640 } else {
641 src
642 };
643 symlink(src, dst)
644 } else {
645 if cfg!(target_os = "macos") {
646 fs::copy(src, dst).map_or_else(
657 |e| {
658 if e.raw_os_error()
659 .map_or(false, |os_err| os_err == 35 )
660 {
661 tracing::info!("copy failed {e:?}. falling back to fs::hard_link");
662
663 fs::hard_link(src, dst)
667 } else {
668 Err(e)
669 }
670 },
671 |_| Ok(()),
672 )
673 } else {
674 fs::hard_link(src, dst)
675 }
676 };
677 link_result
678 .or_else(|err| {
679 tracing::debug!("link failed {}. falling back to fs::copy", err);
680 fs::copy(src, dst).map(|_| ())
681 })
682 .with_context(|| {
683 format!(
684 "failed to link or copy `{}` to `{}`",
685 src.display(),
686 dst.display()
687 )
688 })?;
689 Ok(())
690}
691
692pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<u64> {
696 let from = from.as_ref();
697 let to = to.as_ref();
698 fs::copy(from, to)
699 .with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()))
700}
701
702pub fn set_file_time_no_err<P: AsRef<Path>>(path: P, time: FileTime) {
708 let path = path.as_ref();
709 let times = fs::FileTimes::new()
710 .set_accessed(time.into())
711 .set_modified(time.into());
712 match fs::set_times(path, times) {
713 Ok(()) => tracing::debug!("set file mtime {} to {}", path.display(), time),
714 Err(e) => tracing::warn!(
715 "could not set mtime of {} to {}: {:?}",
716 path.display(),
717 time,
718 e
719 ),
720 }
721}
722
723pub fn strip_prefix_canonical(
729 path: impl AsRef<Path>,
730 base: impl AsRef<Path>,
731) -> Result<PathBuf, std::path::StripPrefixError> {
732 let safe_canonicalize = |path: &Path| match path.canonicalize() {
734 Ok(p) => p,
735 Err(e) => {
736 tracing::warn!("cannot canonicalize {:?}: {:?}", path, e);
737 path.to_path_buf()
738 }
739 };
740 let canon_path = safe_canonicalize(path.as_ref());
741 let canon_base = safe_canonicalize(base.as_ref());
742 canon_path.strip_prefix(canon_base).map(|p| p.to_path_buf())
743}
744
745pub fn create_dir_all_excluded_from_backups_atomic(p: impl AsRef<Path>) -> Result<()> {
753 let path = p.as_ref();
754 if path.is_dir() {
755 return Ok(());
756 }
757
758 let parent = path.parent().unwrap();
759 let base = path.file_name().unwrap();
760 create_dir_all(parent)?;
761 let tempdir = TempFileBuilder::new().prefix(base).tempdir_in(parent)?;
773 exclude_from_backups(tempdir.path());
774 exclude_from_content_indexing(tempdir.path());
775 if let Err(e) = fs::rename(tempdir.path(), path) {
782 if !path.exists() {
783 return Err(anyhow::Error::from(e))
784 .with_context(|| format!("failed to create directory `{}`", path.display()));
785 }
786 }
787 Ok(())
788}
789
790pub fn exclude_from_backups_and_indexing(p: impl AsRef<Path>) {
794 let path = p.as_ref();
795 exclude_from_backups(path);
796 exclude_from_content_indexing(path);
797}
798
799fn exclude_from_backups(path: &Path) {
807 exclude_from_time_machine_and_cloud_sync(path);
808 let file = path.join("CACHEDIR.TAG");
809 if !file.exists() {
810 let _ = std::fs::write(
811 file,
812 "Signature: 8a477f597d28d172789f06886806bc55
813# This file is a cache directory tag created by cargo.
814# For information about cache directory tags see https://bford.info/cachedir/
815",
816 );
817 }
819}
820
821fn exclude_from_content_indexing(path: &Path) {
829 #[cfg(windows)]
830 {
831 use std::iter::once;
832 use std::os::windows::prelude::OsStrExt;
833 use windows_sys::Win32::Storage::FileSystem::{
834 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, GetFileAttributesW, SetFileAttributesW,
835 };
836
837 let path: Vec<u16> = path.as_os_str().encode_wide().chain(once(0)).collect();
838 unsafe {
839 SetFileAttributesW(
840 path.as_ptr(),
841 GetFileAttributesW(path.as_ptr()) | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
842 );
843 }
844 }
845 #[cfg(not(windows))]
846 {
847 let _ = path;
848 }
849}
850
851#[cfg(not(target_os = "macos"))]
852fn exclude_from_time_machine_and_cloud_sync(_: &Path) {}
853
854#[cfg(target_os = "macos")]
855fn exclude_from_time_machine_and_cloud_sync(path: &Path) {
857 use core_foundation::base::TCFType;
858 use core_foundation::{number, string, url};
859 use std::ptr;
860
861 let path = match url::CFURL::from_path(path, false) {
862 Some(url) => url,
863 None => return,
864 };
865
866 const KEY_NAMES: [&str; 2] = [
868 "NSURLIsExcludedFromBackupKey", "NSURLUbiquitousItemIsExcludedFromSyncKey", ];
871
872 for key_name in KEY_NAMES {
873 let is_excluded_key = match key_name.parse::<string::CFString>() {
874 Ok(key) => key,
875 Err(_) => continue,
876 };
877 unsafe {
878 url::CFURLSetResourcePropertyForKey(
879 path.as_concrete_TypeRef(),
880 is_excluded_key.as_concrete_TypeRef(),
881 number::kCFBooleanTrue as *const _,
882 ptr::null_mut(),
883 );
884 }
885 }
886 }
889
890#[cfg(test)]
891mod tests {
892 use super::join_paths;
893 use super::normalize_path;
894 use super::write;
895 use super::write_atomic;
896
897 #[test]
898 fn test_normalize_path() {
899 let cases = &[
900 ("", ""),
901 (".", ""),
902 (".////./.", ""),
903 ("/", "/"),
904 ("/..", "/"),
905 ("/foo/bar", "/foo/bar"),
906 ("/foo/bar/", "/foo/bar"),
907 ("/foo/bar/./././///", "/foo/bar"),
908 ("/foo/bar/..", "/foo"),
909 ("/foo/bar/../..", "/"),
910 ("/foo/bar/../../..", "/"),
911 ("foo/bar", "foo/bar"),
912 ("foo/bar/", "foo/bar"),
913 ("foo/bar/./././///", "foo/bar"),
914 ("foo/bar/..", "foo"),
915 ("foo/bar/../..", ""),
916 ("foo/bar/../../..", ".."),
917 ("../../foo/bar", "../../foo/bar"),
918 ("../../foo/bar/", "../../foo/bar"),
919 ("../../foo/bar/./././///", "../../foo/bar"),
920 ("../../foo/bar/..", "../../foo"),
921 ("../../foo/bar/../..", "../.."),
922 ("../../foo/bar/../../..", "../../.."),
923 ];
924 for (input, expected) in cases {
925 let actual = normalize_path(std::path::Path::new(input));
926 assert_eq!(actual, std::path::Path::new(expected), "input: {input}");
927 }
928 }
929
930 #[test]
931 fn write_works() {
932 let original_contents = "[dependencies]\nfoo = 0.1.0";
933
934 let tmpdir = tempfile::tempdir().unwrap();
935 let path = tmpdir.path().join("Cargo.toml");
936 write(&path, original_contents).unwrap();
937 let contents = std::fs::read_to_string(&path).unwrap();
938 assert_eq!(contents, original_contents);
939 }
940 #[test]
941 fn write_atomic_works() {
942 let original_contents = "[dependencies]\nfoo = 0.1.0";
943
944 let tmpdir = tempfile::tempdir().unwrap();
945 let path = tmpdir.path().join("Cargo.toml");
946 write_atomic(&path, original_contents).unwrap();
947 let contents = std::fs::read_to_string(&path).unwrap();
948 assert_eq!(contents, original_contents);
949 }
950
951 #[test]
952 #[cfg(unix)]
953 fn write_atomic_permissions() {
954 use std::os::unix::fs::PermissionsExt;
955
956 let original_perms = std::fs::Permissions::from_mode(
957 (libc::S_IRWXU | libc::S_IRGRP | libc::S_IWGRP | libc::S_IROTH) as u32,
958 );
959
960 let tmp = tempfile::Builder::new().tempfile().unwrap();
961
962 tmp.as_file()
964 .set_permissions(original_perms.clone())
965 .unwrap();
966
967 write_atomic(tmp.path(), "new").unwrap();
969 assert_eq!(std::fs::read_to_string(tmp.path()).unwrap(), "new");
970
971 let new_perms = std::fs::metadata(tmp.path()).unwrap().permissions();
972
973 let mask = (libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) as u32;
974 assert_eq!(original_perms.mode(), new_perms.mode() & mask);
975 }
976
977 #[test]
978 fn join_paths_lists_paths_on_error() {
979 let valid_paths = vec!["/testing/one", "/testing/two"];
980 let _joined = join_paths(&valid_paths, "TESTING1").unwrap();
982
983 #[cfg(unix)]
984 {
985 let invalid_paths = vec!["/testing/one", "/testing/t:wo/three"];
986 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
987 assert_eq!(
988 err.to_string(),
989 "failed to join paths from `$TESTING2` together\n\n\
990 Check if any of path segments listed below contain an \
991 unterminated quote character or path separator:\
992 \n \"/testing/one\"\
993 \n \"/testing/t:wo/three\"\
994 "
995 );
996 }
997 #[cfg(windows)]
998 {
999 let invalid_paths = vec!["/testing/one", "/testing/t\"wo/three"];
1000 let err = join_paths(&invalid_paths, "TESTING2").unwrap_err();
1001 assert_eq!(
1002 err.to_string(),
1003 "failed to join paths from `$TESTING2` together\n\n\
1004 Check if any of path segments listed below contain an \
1005 unterminated quote character or path separator:\
1006 \n \"/testing/one\"\
1007 \n \"/testing/t\\\"wo/three\"\
1008 "
1009 );
1010 }
1011 }
1012
1013 #[test]
1014 fn write_atomic_symlink() {
1015 let tmpdir = tempfile::tempdir().unwrap();
1016 let target_path = tmpdir.path().join("target.txt");
1017 let symlink_path = tmpdir.path().join("symlink.txt");
1018
1019 write(&target_path, "initial").unwrap();
1021
1022 #[cfg(unix)]
1024 std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
1025 #[cfg(windows)]
1026 std::os::windows::fs::symlink_file(&target_path, &symlink_path).unwrap();
1027
1028 write_atomic(&symlink_path, "updated").unwrap();
1030
1031 assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1033 assert_eq!(std::fs::read_to_string(&symlink_path).unwrap(), "updated");
1034
1035 assert!(symlink_path.is_symlink());
1037 assert_eq!(std::fs::read_link(&symlink_path).unwrap(), target_path);
1038 }
1039
1040 #[test]
1041 fn write_atomic_relative_symlink() {
1042 let tmpdir = tempfile::tempdir().unwrap();
1043 let link_dir = tmpdir.path().join("project");
1044 let target_dir = link_dir.join("generated");
1045 let target_path = target_dir.join("target.txt");
1046 let symlink_path = link_dir.join("symlink.txt");
1047 let relative_target = std::path::Path::new("generated/target.txt");
1048
1049 std::fs::create_dir_all(&target_dir).unwrap();
1050 write(&target_path, "initial").unwrap();
1051
1052 #[cfg(unix)]
1053 std::os::unix::fs::symlink(relative_target, &symlink_path).unwrap();
1054 #[cfg(windows)]
1055 std::os::windows::fs::symlink_file(relative_target, &symlink_path).unwrap();
1056
1057 write_atomic(&symlink_path, "updated").unwrap();
1058
1059 assert_eq!(std::fs::read_to_string(&target_path).unwrap(), "updated");
1060 assert!(symlink_path.is_symlink());
1061 assert_eq!(std::fs::read_link(&symlink_path).unwrap(), relative_target);
1062 }
1063
1064 #[test]
1065 #[cfg(windows)]
1066 fn test_remove_symlink_dir() {
1067 use super::*;
1068 use std::fs;
1069 use std::os::windows::fs::symlink_dir;
1070
1071 let tmpdir = tempfile::tempdir().unwrap();
1072 let dir_path = tmpdir.path().join("testdir");
1073 let symlink_path = tmpdir.path().join("symlink");
1074
1075 fs::create_dir(&dir_path).unwrap();
1076
1077 symlink_dir(&dir_path, &symlink_path).expect("failed to create symlink");
1078
1079 assert!(symlink_path.exists());
1080
1081 assert!(remove_file(symlink_path.clone()).is_ok());
1082
1083 assert!(!symlink_path.exists());
1084 assert!(dir_path.exists());
1085 }
1086
1087 #[test]
1088 #[cfg(windows)]
1089 fn test_remove_symlink_file() {
1090 use super::*;
1091 use std::fs;
1092 use std::os::windows::fs::symlink_file;
1093
1094 let tmpdir = tempfile::tempdir().unwrap();
1095 let file_path = tmpdir.path().join("testfile");
1096 let symlink_path = tmpdir.path().join("symlink");
1097
1098 fs::write(&file_path, b"test").unwrap();
1099
1100 symlink_file(&file_path, &symlink_path).expect("failed to create symlink");
1101
1102 assert!(symlink_path.exists());
1103
1104 assert!(remove_file(symlink_path.clone()).is_ok());
1105
1106 assert!(!symlink_path.exists());
1107 assert!(file_path.exists());
1108 }
1109}