Line data Source code
1 : use nix::NixPath;
2 :
3 : /// Rename a file without replacing an existing file.
4 : ///
5 : /// This is a wrapper around platform-specific APIs.
6 5118 : pub fn rename_noreplace<P1: ?Sized + NixPath, P2: ?Sized + NixPath>(
7 5118 : src: &P1,
8 5118 : dst: &P2,
9 5118 : ) -> nix::Result<()> {
10 5118 : {
11 5118 : #[cfg(target_os = "linux")]
12 5118 : {
13 5118 : nix::fcntl::renameat2(
14 5118 : None,
15 5118 : src,
16 5118 : None,
17 5118 : dst,
18 5118 : nix::fcntl::RenameFlags::RENAME_NOREPLACE,
19 5118 : )
20 5118 : }
21 5118 : #[cfg(target_os = "macos")]
22 5118 : {
23 5118 : let res = src.with_nix_path(|src| {
24 5118 : dst.with_nix_path(|dst|
25 5118 : // SAFETY: `src` and `dst` are valid C strings as per the NixPath trait and they outlive the call to renamex_np.
26 5118 : unsafe {
27 5118 : nix::libc::renamex_np(src.as_ptr(), dst.as_ptr(), nix::libc::RENAME_EXCL)
28 5118 : })
29 5118 : })??;
30 5118 : nix::errno::Errno::result(res).map(drop)
31 5118 : }
32 5118 : #[cfg(not(any(target_os = "linux", target_os = "macos")))]
33 5118 : {
34 5118 : std::compile_error!("OS does not support no-replace renames");
35 5118 : }
36 5118 : }
37 5118 : }
38 :
39 : #[cfg(test)]
40 : mod test {
41 : use std::{fs, path::PathBuf};
42 :
43 : use super::*;
44 :
45 18 : fn testdir() -> camino_tempfile::Utf8TempDir {
46 18 : match crate::env::var("NEON_UTILS_RENAME_NOREPLACE_TESTDIR") {
47 0 : Some(path) => {
48 0 : let path: camino::Utf8PathBuf = path;
49 0 : camino_tempfile::tempdir_in(path).unwrap()
50 : }
51 18 : None => camino_tempfile::tempdir().unwrap(),
52 : }
53 18 : }
54 :
55 : #[test]
56 6 : fn test_absolute_paths() {
57 6 : let testdir = testdir();
58 6 : println!("testdir: {}", testdir.path());
59 6 :
60 6 : let src = testdir.path().join("src");
61 6 : let dst = testdir.path().join("dst");
62 6 :
63 6 : fs::write(&src, b"").unwrap();
64 6 : fs::write(&dst, b"").unwrap();
65 6 :
66 6 : let src = src.canonicalize().unwrap();
67 6 : assert!(src.is_absolute());
68 6 : let dst = dst.canonicalize().unwrap();
69 6 : assert!(dst.is_absolute());
70 :
71 6 : let result = rename_noreplace(&src, &dst);
72 6 : assert_eq!(result.unwrap_err(), nix::Error::EEXIST);
73 6 : }
74 :
75 : #[test]
76 6 : fn test_relative_paths() {
77 6 : let testdir = testdir();
78 6 : println!("testdir: {}", testdir.path());
79 6 :
80 6 : // this is fine because we run in nextest => process per test
81 6 : std::env::set_current_dir(testdir.path()).unwrap();
82 6 :
83 6 : let src = PathBuf::from("src");
84 6 : let dst = PathBuf::from("dst");
85 6 :
86 6 : fs::write(&src, b"").unwrap();
87 6 : fs::write(&dst, b"").unwrap();
88 6 :
89 6 : let result = rename_noreplace(&src, &dst);
90 6 : assert_eq!(result.unwrap_err(), nix::Error::EEXIST);
91 6 : }
92 :
93 : #[test]
94 6 : fn test_works_when_not_exists() {
95 6 : let testdir = testdir();
96 6 : println!("testdir: {}", testdir.path());
97 6 :
98 6 : let src = testdir.path().join("src");
99 6 : let dst = testdir.path().join("dst");
100 6 :
101 6 : fs::write(&src, b"content").unwrap();
102 6 :
103 6 : rename_noreplace(src.as_std_path(), dst.as_std_path()).unwrap();
104 6 : assert_eq!(
105 6 : "content",
106 6 : String::from_utf8(std::fs::read(&dst).unwrap()).unwrap()
107 6 : );
108 6 : }
109 : }
|