1use std::ffi::{c_char, CStr};
2use std::path::{Path, PathBuf};
3
4mod exchange;
5mod file_rename;
6mod path_checkout;
7mod types;
8
9use crate::exchange::{exchange_paths, resolve_path};
10pub use crate::types::RenameError;
11
12#[no_mangle]
13pub unsafe extern "C" fn exchange(
30 path1: *const c_char,
31 path2: *const c_char,
32 preserve_ext: bool,
33) -> i32 {
34 unsafe { convert_inputs(path1, path2) }
35 .and_then(|(path1, path2)| exchange_paths(path1, path2, preserve_ext))
36 .map(|_| 0)
37 .unwrap_or_else(|err| err.to_code())
38}
39
40pub fn exchange_rs(
51 path1: &Path,
52 path2: &Path,
53 preserve_ext: bool,
54) -> Result<(), RenameError> {
55 exchange_paths(path1.to_path_buf(), path2.to_path_buf(), preserve_ext)
56}
57
58pub fn resolve_path_rs(
68 path: &Path,
69 base_dir: &Path,
70) -> Result<(bool, PathBuf), RenameError> {
71 resolve_path(path, base_dir)
72}
73
74unsafe fn convert_inputs(
75 path1: *const c_char,
76 path2: *const c_char,
77) -> Result<(PathBuf, PathBuf), RenameError> {
78 let path1 = ptr_to_path(path1)?;
79 let path2 = ptr_to_path(path2)?;
80 Ok((path1, path2))
81}
82
83unsafe fn ptr_to_path(ptr: *const c_char) -> Result<PathBuf, RenameError> {
84 if ptr.is_null() {
85 return Err(RenameError::NotExists);
86 }
87
88 let c_str = CStr::from_ptr(ptr);
89 let raw = c_str.to_string_lossy();
90 let sanitized = sanitize_input(raw.as_ref());
91
92 if sanitized.is_empty() {
93 return Err(RenameError::NotExists);
94 }
95
96 Ok(PathBuf::from(sanitized))
97}
98
99fn sanitize_input(input: &str) -> String {
100 let trimmed = input.trim();
101 if trimmed.len() >= 2 {
103 let bytes = trimmed.as_bytes();
104 let first = bytes[0];
105 let last = bytes[bytes.len() - 1];
106 if (first == b'"' || first == b'\'') && first == last {
107 return trimmed[1..trimmed.len() - 1].to_string();
108 }
109 }
110 trimmed.to_string()
111}
112
113#[cfg(test)]
114mod tests {
115 use std::{
116 fs::{self, File},
117 io::Write,
118 path::{Path, PathBuf},
119 time::{SystemTime, UNIX_EPOCH},
120 };
121
122 struct TestDir {
123 path: PathBuf,
124 }
125
126 impl TestDir {
127 fn new(case_name: &str) -> Self {
128 let unique = SystemTime::now()
129 .duration_since(UNIX_EPOCH)
130 .unwrap()
131 .as_nanos();
132 let mut path = std::env::temp_dir();
133 path.push(format!(
134 "name_exchanger_rs_{}_{}_{}",
135 case_name,
136 std::process::id(),
137 unique
138 ));
139 fs::create_dir_all(&path).unwrap();
140 Self { path }
141 }
142
143 fn join(&self, file_name: &str) -> PathBuf {
144 self.path.join(file_name)
145 }
146 }
147
148 impl Drop for TestDir {
149 fn drop(&mut self) {
150 let _ = fs::remove_dir_all(&self.path);
151 }
152 }
153
154 fn write_text(path: &Path, content: &str) {
155 let mut file = File::create(path).unwrap();
156 file.write_all(content.as_bytes()).unwrap();
157 }
158
159 fn read_text(path: &Path) -> String {
160 fs::read_to_string(path).unwrap()
161 }
162
163 #[test]
164 fn exchange_rs_with_preserve_ext_false_swaps_full_names() {
165 let dir = TestDir::new("swap_full_name");
166 let file1 = dir.join("alpha.ext1");
167 let file2 = dir.join("beta.ext2");
168 write_text(&file1, "A");
169 write_text(&file2, "B");
170
171 super::exchange_rs(&file1, &file2, false).unwrap();
172
173 assert!(file1.exists());
174 assert!(file2.exists());
175 assert_eq!(read_text(&file1), "B");
176 assert_eq!(read_text(&file2), "A");
177 }
178
179 #[test]
180 fn exchange_rs_with_preserve_ext_true_keeps_extensions() {
181 let dir = TestDir::new("keep_extension");
182 let file1 = dir.join("alpha.ext1");
183 let file2 = dir.join("beta.ext2");
184 write_text(&file1, "A");
185 write_text(&file2, "B");
186
187 super::exchange_rs(&file1, &file2, true).unwrap();
188
189 let new_file1 = dir.join("beta.ext1");
190 let new_file2 = dir.join("alpha.ext2");
191
192 assert!(!file1.exists());
193 assert!(!file2.exists());
194 assert!(new_file1.exists());
195 assert!(new_file2.exists());
196 assert_eq!(read_text(&new_file1), "A");
197 assert_eq!(read_text(&new_file2), "B");
198 }
199
200 #[test]
201 fn exchange_rs_same_path_returns_error() {
202 let dir = TestDir::new("same_path");
203 let file = dir.join("same.ext");
204 write_text(&file, "X");
205
206 let result = super::exchange_rs(&file, &file, true);
207 assert!(matches!(result, Err(super::types::RenameError::SamePath)));
208 }
209}