Skip to main content

exchange_name_lib/
lib.rs

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]
13/// # Safety
14/// C interface function for swapping names of two files or directories
15///
16/// ### Parameters
17/// * `path1` - First file or directory path (C string pointer)
18/// * `path2` - Second file or directory path (C string pointer)
19/// * `preserve_ext` - If true, keep each file's own extension and swap only stems
20///
21/// ### Return Value
22/// * `0` - Success
23/// * `1` - File does not exist
24/// * `2` - Permission denied
25/// * `3` - Target file already exists
26/// * `4` - Two paths refer to the same file
27/// * `5` - Invalid path (e.g. non-UTF-8)
28/// * `255` - Unknown error
29pub 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
40/// Rust interface function for swapping names of two files or directories
41///
42/// ### Parameters
43/// * `path1` - First file or directory path
44/// * `path2` - Second file or directory path
45/// * `preserve_ext` - If true, keep each file's own extension and swap only stems
46///
47/// ### Return Value
48/// * `Ok(())` - Success
49/// * `Err(RenameError)` - Error information
50pub 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
58/// Resolve and normalize path
59///
60/// ### Parameters
61/// * `path` - Original path
62/// * `base_dir` - Base directory path
63///
64/// ### Return Value
65/// * `Ok((bool, PathBuf))` - Tuple of (is_path_exists, normalized_path)
66/// * `Err(RenameError)` - Path resolution failure
67pub 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    // Strip at most one layer of matching quotes (shell-style quoting)
102    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}