1use crate::errors::{CascadeError, Result};
2use serde::Serialize;
3use std::fs;
4use std::path::Path;
5
6pub mod platform;
8
9pub mod atomic_file {
11 use super::*;
12
13 pub fn write_json<T: Serialize>(path: &Path, data: &T) -> Result<()> {
15 with_concurrent_file_lock(path, || {
16 let content = serde_json::to_string_pretty(data)
17 .map_err(|e| CascadeError::config(format!("Failed to serialize data: {e}")))?;
18
19 write_string_unlocked(path, &content)
20 })
21 }
22
23 pub fn write_string(path: &Path, content: &str) -> Result<()> {
25 with_concurrent_file_lock(path, || write_string_unlocked(path, content))
26 }
27
28 fn with_concurrent_file_lock<F, R>(file_path: &Path, operation: F) -> Result<R>
30 where
31 F: FnOnce() -> Result<R>,
32 {
33 let use_aggressive =
35 std::env::var("CI").is_ok() || std::env::var("CONCURRENT_ACCESS_EXPECTED").is_ok();
36
37 let _lock = if use_aggressive {
38 crate::utils::file_locking::FileLock::acquire_aggressive(file_path)?
39 } else {
40 crate::utils::file_locking::FileLock::acquire(file_path)?
41 };
42
43 operation()
44 }
45
46 fn write_string_unlocked(path: &Path, content: &str) -> Result<()> {
48 let temp_path = path.with_extension("tmp");
50
51 fs::write(&temp_path, content)
53 .map_err(|e| CascadeError::config(format!("Failed to write temporary file: {e}")))?;
54
55 atomic_rename(&temp_path, path)
57 }
58
59 #[cfg(windows)]
61 fn atomic_rename(temp_path: &Path, final_path: &Path) -> Result<()> {
62 const MAX_RETRIES: u32 = 3;
64 const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(100);
65
66 for attempt in 1..=MAX_RETRIES {
67 match fs::rename(temp_path, final_path) {
68 Ok(()) => return Ok(()),
69 Err(e) => {
70 if attempt == MAX_RETRIES {
71 let _ = fs::remove_file(temp_path);
73 return Err(CascadeError::config(format!(
74 "Failed to finalize file write after {MAX_RETRIES} attempts on Windows: {e}"
75 )));
76 }
77
78 std::thread::sleep(RETRY_DELAY);
80 }
81 }
82 }
83
84 unreachable!("Loop should have returned or failed by now")
85 }
86
87 #[cfg(not(windows))]
88 fn atomic_rename(temp_path: &Path, final_path: &Path) -> Result<()> {
89 fs::rename(temp_path, final_path)
91 .map_err(|e| CascadeError::config(format!("Failed to finalize file write: {e}")))?;
92 Ok(())
93 }
94
95 pub fn write_bytes(path: &Path, data: &[u8]) -> Result<()> {
97 with_concurrent_file_lock(path, || {
98 let temp_path = path.with_extension("tmp");
99
100 fs::write(&temp_path, data).map_err(|e| {
101 CascadeError::config(format!("Failed to write temporary file: {e}"))
102 })?;
103
104 atomic_rename(&temp_path, path)
105 })
106 }
107}
108
109pub mod path_validation {
111 use super::*;
112 use std::path::PathBuf;
113
114 pub fn validate_config_path(path: &Path, base_dir: &Path) -> Result<PathBuf> {
117 if !path.exists() {
119 let canonical_base = base_dir.canonicalize().map_err(|e| {
121 CascadeError::config(format!("Invalid base directory '{base_dir:?}': {e}"))
122 })?;
123
124 let mut check_path = path.to_path_buf();
126
127 while !check_path.exists() && check_path.parent().is_some() {
129 check_path = check_path.parent().unwrap().to_path_buf();
130 }
131
132 if check_path.exists() {
133 let canonical_check = check_path.canonicalize().map_err(|e| {
134 CascadeError::config(format!("Cannot validate path security: {e}"))
135 })?;
136
137 if !canonical_check.starts_with(&canonical_base) {
138 return Err(CascadeError::config(format!(
139 "Path '{path:?}' would be outside allowed directory '{canonical_base:?}'"
140 )));
141 }
142 }
143
144 Ok(path.to_path_buf())
146 } else {
147 let canonical_path = path
149 .canonicalize()
150 .map_err(|e| CascadeError::config(format!("Invalid path '{path:?}': {e}")))?;
151
152 let canonical_base = base_dir.canonicalize().map_err(|e| {
153 CascadeError::config(format!("Invalid base directory '{base_dir:?}': {e}"))
154 })?;
155
156 if !canonical_path.starts_with(&canonical_base) {
157 return Err(CascadeError::config(format!(
158 "Path '{canonical_path:?}' is outside allowed directory '{canonical_base:?}'"
159 )));
160 }
161
162 Ok(canonical_path)
163 }
164 }
165
166 pub fn sanitize_filename(name: &str) -> String {
168 name.chars()
169 .map(|c| match c {
170 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => c,
171 _ => '_',
172 })
173 .collect()
174 }
175}
176
177pub mod async_ops {
179 use super::*;
180 use tokio::task;
181
182 pub async fn run_git_operation<F, R>(operation: F) -> Result<R>
184 where
185 F: FnOnce() -> Result<R> + Send + 'static,
186 R: Send + 'static,
187 {
188 task::spawn_blocking(operation)
189 .await
190 .map_err(|e| CascadeError::config(format!("Background task failed: {e}")))?
191 }
192
193 pub async fn run_file_operation<F, R>(operation: F) -> Result<R>
195 where
196 F: FnOnce() -> Result<R> + Send + 'static,
197 R: Send + 'static,
198 {
199 task::spawn_blocking(operation)
200 .await
201 .map_err(|e| CascadeError::config(format!("File operation failed: {e}")))?
202 }
203}
204
205pub mod file_locking {
207 use super::*;
208 use std::fs::{File, OpenOptions};
209 use std::path::Path;
210 use std::time::{Duration, Instant};
211
212 pub struct FileLock {
214 _file: File,
215 lock_path: std::path::PathBuf,
216 }
217
218 impl FileLock {
219 #[cfg(windows)]
221 const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[cfg(windows)]
223 const RETRY_INTERVAL: Duration = Duration::from_millis(100); #[cfg(not(windows))]
226 const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); #[cfg(not(windows))]
228 const RETRY_INTERVAL: Duration = Duration::from_millis(50); pub fn acquire_with_timeout(file_path: &Path, timeout: Duration) -> Result<Self> {
232 let lock_path = file_path.with_extension("lock");
233 let start_time = Instant::now();
234
235 loop {
236 match Self::try_acquire(&lock_path) {
237 Ok(lock) => return Ok(lock),
238 Err(e) => {
239 if start_time.elapsed() >= timeout {
240 return Err(CascadeError::config(format!(
241 "Timeout waiting for lock on {file_path:?} after {}ms (platform: {}): {e}",
242 timeout.as_millis(),
243 if cfg!(windows) { "windows" } else { "unix" }
244 )));
245 }
246 std::thread::sleep(Self::RETRY_INTERVAL);
247 }
248 }
249 }
250 }
251
252 pub fn try_acquire(lock_path: &Path) -> Result<Self> {
254 let file = Self::create_lock_file(lock_path)?;
256
257 Ok(Self {
258 _file: file,
259 lock_path: lock_path.to_path_buf(),
260 })
261 }
262
263 #[cfg(windows)]
265 fn create_lock_file(lock_path: &Path) -> Result<File> {
266 OpenOptions::new()
268 .write(true)
269 .create_new(true)
270 .open(lock_path)
271 .map_err(|e| {
272 match e.kind() {
274 std::io::ErrorKind::AlreadyExists => {
275 CascadeError::config(format!(
276 "Lock file {lock_path:?} already exists - another process may be accessing the file"
277 ))
278 }
279 std::io::ErrorKind::PermissionDenied => {
280 CascadeError::config(format!(
281 "Permission denied creating lock file {lock_path:?} - check file permissions"
282 ))
283 }
284 _ => CascadeError::config(format!(
285 "Failed to acquire lock {lock_path:?} on Windows: {e}"
286 ))
287 }
288 })
289 }
290
291 #[cfg(not(windows))]
292 fn create_lock_file(lock_path: &Path) -> Result<File> {
293 OpenOptions::new()
295 .write(true)
296 .create_new(true)
297 .open(lock_path)
298 .map_err(|e| {
299 CascadeError::config(format!("Failed to acquire lock {lock_path:?}: {e}"))
300 })
301 }
302
303 pub fn acquire(file_path: &Path) -> Result<Self> {
305 Self::acquire_with_timeout(file_path, Self::DEFAULT_TIMEOUT)
306 }
307
308 pub fn acquire_aggressive(file_path: &Path) -> Result<Self> {
310 let timeout = if cfg!(windows) {
311 Duration::from_secs(15) } else {
313 Duration::from_secs(8) };
315 Self::acquire_with_timeout(file_path, timeout)
316 }
317 }
318
319 impl Drop for FileLock {
320 fn drop(&mut self) {
321 let _ = std::fs::remove_file(&self.lock_path);
323 }
324 }
325
326 pub fn with_file_lock<F, R>(file_path: &Path, operation: F) -> Result<R>
328 where
329 F: FnOnce() -> Result<R>,
330 {
331 let _lock = FileLock::acquire(file_path)?;
332 operation()
333 }
334
335 pub async fn with_file_lock_async<F, Fut, R>(file_path: &Path, operation: F) -> Result<R>
337 where
338 F: FnOnce() -> Fut,
339 Fut: std::future::Future<Output = Result<R>>,
340 {
341 let file_path = file_path.to_path_buf();
342 let _lock = tokio::task::spawn_blocking(move || FileLock::acquire(&file_path))
343 .await
344 .map_err(|e| CascadeError::config(format!("Lock task failed: {e}")))?;
345
346 operation().await
347 }
348}