1use std::fs::{File, OpenOptions};
6use std::io::Read as _;
7#[cfg(unix)]
8use std::os::unix::fs::OpenOptionsExt as _;
9use std::path::{Path, PathBuf};
10
11use rskit_errors::{AppError, AppResult, ErrorCode};
12
13use crate::types::FileMeta;
14
15use crate::file_error::{file_too_large_error, not_regular_file_error, symlink_not_allowed_error};
16pub use crate::file_error::{
17 is_file_too_large_error, is_not_regular_file_error, is_symlink_not_allowed_error,
18};
19use crate::path::parent_dir;
20use crate::temp::sibling_temp_path;
21
22const WRITE_ATOMIC_TEMP_ATTEMPTS: usize = 16;
23
24pub fn create_parent_dir(path: &Path) -> AppResult<()> {
26 if let Some(parent) = parent_dir(path) {
27 std::fs::create_dir_all(parent).map_err(create_parent_dirs_error)?;
28 }
29 Ok(())
30}
31
32pub fn open(path: &Path) -> AppResult<File> {
34 File::open(path).map_err(|error| open_file_error(path, error))
35}
36
37pub fn create(path: &Path) -> AppResult<File> {
39 create_parent_dir(path)?;
40 File::create(path).map_err(|error| create_file_error(path, error))
41}
42
43pub fn exists(path: &Path) -> AppResult<bool> {
45 match std::fs::symlink_metadata(path) {
46 Ok(metadata) => Ok(metadata.is_file() && !metadata.file_type().is_symlink()),
47 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
48 Err(error) => Err(inspect_file_error(path, error)),
49 }
50}
51
52pub fn open_no_follow_regular(path: &Path) -> AppResult<File> {
54 let file = open_no_follow(path)?;
55 let metadata = file
56 .metadata()
57 .map_err(|error| inspect_file_error(path, error))?;
58 if !metadata.is_file() {
59 return Err(not_regular_file_error(path));
60 }
61 Ok(file)
62}
63
64#[cfg(unix)]
65fn open_no_follow(path: &Path) -> AppResult<File> {
66 OpenOptions::new()
67 .read(true)
68 .custom_flags(libc::O_NOFOLLOW)
69 .open(path)
70 .map_err(|error| open_file_error(path, error))
71}
72
73#[cfg(not(unix))]
74fn open_no_follow(path: &Path) -> AppResult<File> {
75 let metadata =
76 std::fs::symlink_metadata(path).map_err(|error| inspect_file_error(path, error))?;
77 if metadata.file_type().is_symlink() {
78 return Err(
79 symlink_not_allowed_error(path).with_cause(std::io::Error::other("path is a symlink"))
80 );
81 }
82 open(path)
83}
84
85pub fn read(path: &Path) -> AppResult<Vec<u8>> {
87 std::fs::read(path).map_err(|error| read_file_error(path, error))
88}
89
90pub fn read_string(path: &Path) -> AppResult<String> {
92 std::fs::read_to_string(path).map_err(|error| read_file_error(path, error))
93}
94
95pub fn read_bounded(path: &Path, max_bytes: u64) -> AppResult<Vec<u8>> {
97 let mut file = open_no_follow_regular(path)?;
98 read_bounded_from_file(path, max_bytes, &mut file)
99}
100
101pub fn read_string_bounded(path: &Path, max_bytes: u64) -> AppResult<String> {
103 let bytes = read_bounded(path, max_bytes)?;
104 String::from_utf8(bytes).map_err(|error| {
105 AppError::new(
106 ErrorCode::InvalidInput,
107 format!("file '{}' is not valid UTF-8: {error}", path.display()),
108 )
109 })
110}
111
112fn read_bounded_from_file(path: &Path, max_bytes: u64, file: &mut File) -> AppResult<Vec<u8>> {
113 let metadata = file
114 .metadata()
115 .map_err(|error| inspect_file_error(path, error))?;
116 if metadata.is_file() && metadata.len() > max_bytes {
117 return Err(file_too_large_error(path, metadata.len(), max_bytes));
118 }
119
120 let capacity = metadata.len().min(max_bytes).try_into().unwrap_or(0);
121 let mut bytes = Vec::with_capacity(capacity);
122 file.by_ref()
123 .take(max_bytes.saturating_add(1))
124 .read_to_end(&mut bytes)
125 .map_err(|error| read_file_error(path, error))?;
126 if bytes.len() as u64 > max_bytes {
127 return Err(file_too_large_error(path, bytes.len() as u64, max_bytes));
128 }
129 Ok(bytes)
130}
131
132pub fn write(path: &Path, bytes: impl AsRef<[u8]>) -> AppResult<()> {
134 create_parent_dir(path)?;
135 std::fs::write(path, bytes).map_err(|error| write_file_error(path, error))
136}
137
138pub fn copy(from: &Path, to: &Path) -> AppResult<u64> {
140 create_parent_dir(to)?;
141 std::fs::copy(from, to).map_err(|error| copy_file_error(from, to, error))
142}
143
144pub fn rename(from: &Path, to: &Path) -> AppResult<()> {
146 create_parent_dir(to)?;
147 std::fs::rename(from, to).map_err(|error| rename_file_error(from, to, error))
148}
149
150pub fn move_file(from: &Path, to: &Path) -> AppResult<()> {
152 create_parent_dir(to)?;
153 match std::fs::rename(from, to) {
154 Ok(()) => Ok(()),
155 Err(error) if is_cross_device_error(&error) => {
156 copy(from, to)?;
157 remove(from)
158 }
159 Err(error) => Err(move_file_error(from, to, error)),
160 }
161}
162
163pub fn remove(path: &Path) -> AppResult<()> {
165 std::fs::remove_file(path).map_err(|error| remove_file_error(path, error))
166}
167
168pub fn remove_if_exists(path: &Path) -> AppResult<bool> {
170 match std::fs::remove_file(path) {
171 Ok(()) => Ok(true),
172 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
173 Err(error) => Err(remove_file_error(path, error)),
174 }
175}
176
177pub fn metadata(path: &Path) -> AppResult<FileMeta> {
179 let metadata =
180 std::fs::symlink_metadata(path).map_err(|error| inspect_file_error(path, error))?;
181 Ok(FileMeta {
182 path: path.to_path_buf(),
183 len: metadata.len(),
184 created: metadata.created().ok(),
185 modified: metadata.modified().ok(),
186 is_file: metadata.is_file(),
187 is_dir: metadata.is_dir(),
188 is_symlink: metadata.file_type().is_symlink(),
189 })
190}
191
192pub fn write_atomic(dest: &Path, bytes: impl AsRef<[u8]>, temp_prefix: &str) -> AppResult<()> {
194 write_atomic_with_attempts(dest, bytes, temp_prefix, WRITE_ATOMIC_TEMP_ATTEMPTS, false)
195}
196
197pub fn write_atomic_replace(
203 dest: &Path,
204 bytes: impl AsRef<[u8]>,
205 temp_prefix: &str,
206) -> AppResult<()> {
207 write_atomic_with_attempts(dest, bytes, temp_prefix, WRITE_ATOMIC_TEMP_ATTEMPTS, true)
208}
209
210fn write_atomic_with_attempts(
211 dest: &Path,
212 bytes: impl AsRef<[u8]>,
213 temp_prefix: &str,
214 attempts: usize,
215 replace_existing: bool,
216) -> AppResult<()> {
217 create_parent_dir(dest)?;
218 let bytes = bytes.as_ref();
219
220 for _ in 0..attempts {
221 let temp_path = sibling_temp_path(dest, temp_prefix, ".tmp");
222 let mut temp_file = match OpenOptions::new()
223 .write(true)
224 .create_new(true)
225 .open(&temp_path)
226 {
227 Ok(file) => file,
228 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
229 Err(error) => return Err(create_file_error(&temp_path, error)),
230 };
231
232 let result = (|| {
233 use std::io::Write as _;
234 temp_file
235 .write_all(bytes)
236 .map_err(|error| write_file_error(&temp_path, error))?;
237 temp_file
238 .sync_data()
239 .map_err(|error| sync_file_error(&temp_path, error))?;
240 drop(temp_file);
241 persist_temp_file_with_replace(&temp_path, dest, replace_existing)
242 })();
243
244 if result.is_err() {
245 let _ = remove_if_exists(&temp_path);
246 }
247 return result;
248 }
249
250 Err(AppError::new(
251 ErrorCode::Internal,
252 format!(
253 "failed to create a unique temp file for '{}' after {attempts} attempts",
254 dest.display()
255 ),
256 ))
257}
258
259fn persist_temp_file_with_replace(
260 temp_path: &Path,
261 dest: &Path,
262 replace_existing: bool,
263) -> AppResult<()> {
264 #[cfg(windows)]
265 if replace_existing {
266 remove_if_exists(dest)?;
267 }
268
269 let _ = replace_existing;
270 rename(temp_path, dest)
271}
272
273pub fn canonicalize(path: &Path) -> AppResult<PathBuf> {
275 std::fs::canonicalize(path).map_err(|error| {
276 AppError::new(
277 ErrorCode::Internal,
278 format!("failed to canonicalize '{}': {error}", path.display()),
279 )
280 })
281}
282
283fn is_cross_device_error(error: &std::io::Error) -> bool {
284 #[cfg(unix)]
285 {
286 error.raw_os_error() == Some(libc::EXDEV)
287 }
288 #[cfg(not(unix))]
289 {
290 error.kind() == std::io::ErrorKind::CrossesDevices
291 }
292}
293
294fn create_parent_dirs_error(error: std::io::Error) -> AppError {
295 AppError::new(
296 ErrorCode::Internal,
297 format!("failed to create parent dirs: {error}"),
298 )
299 .with_cause(error)
300}
301
302fn inspect_file_error(path: &Path, error: std::io::Error) -> AppError {
303 AppError::new(
304 ErrorCode::Internal,
305 format!("failed to inspect file '{}': {error}", path.display()),
306 )
307 .with_cause(error)
308}
309
310fn open_file_error(path: &Path, error: std::io::Error) -> AppError {
311 if is_symlink_open_error(&error) {
312 return symlink_not_allowed_error(path).with_cause(error);
313 }
314
315 AppError::new(
316 ErrorCode::Internal,
317 format!("failed to open file '{}': {error}", path.display()),
318 )
319 .with_cause(error)
320}
321
322fn is_symlink_open_error(error: &std::io::Error) -> bool {
323 #[cfg(unix)]
324 {
325 error.raw_os_error() == Some(libc::ELOOP)
326 }
327 #[cfg(not(unix))]
328 {
329 false
330 }
331}
332
333fn create_file_error(path: &Path, error: std::io::Error) -> AppError {
334 AppError::new(
335 ErrorCode::Internal,
336 format!("failed to create file '{}': {error}", path.display()),
337 )
338 .with_cause(error)
339}
340
341fn read_file_error(path: &Path, error: std::io::Error) -> AppError {
342 AppError::new(
343 ErrorCode::Internal,
344 format!("failed to read file '{}': {error}", path.display()),
345 )
346 .with_cause(error)
347}
348
349fn write_file_error(path: &Path, error: std::io::Error) -> AppError {
350 AppError::new(
351 ErrorCode::Internal,
352 format!("failed to write file '{}': {error}", path.display()),
353 )
354 .with_cause(error)
355}
356
357fn copy_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
358 AppError::new(
359 ErrorCode::Internal,
360 format!(
361 "failed to copy '{}' to '{}': {error}",
362 from.display(),
363 to.display()
364 ),
365 )
366 .with_cause(error)
367}
368
369fn rename_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
370 AppError::new(
371 ErrorCode::Internal,
372 format!(
373 "failed to rename '{}' to '{}': {error}",
374 from.display(),
375 to.display()
376 ),
377 )
378 .with_cause(error)
379}
380
381fn move_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
382 AppError::new(
383 ErrorCode::Internal,
384 format!(
385 "failed to move '{}' to '{}': {error}",
386 from.display(),
387 to.display()
388 ),
389 )
390 .with_cause(error)
391}
392
393fn remove_file_error(path: &Path, error: std::io::Error) -> AppError {
394 AppError::new(
395 ErrorCode::Internal,
396 format!("failed to remove '{}': {error}", path.display()),
397 )
398 .with_cause(error)
399}
400
401fn sync_file_error(path: &Path, error: std::io::Error) -> AppError {
402 AppError::new(
403 ErrorCode::Internal,
404 format!("failed to sync file '{}': {error}", path.display()),
405 )
406 .with_cause(error)
407}
408
409#[cfg(test)]
410mod tests {
411 use super::{
412 is_file_too_large_error, is_not_regular_file_error, is_symlink_not_allowed_error,
413 persist_temp_file_with_replace, read_bounded, read_string, read_string_bounded,
414 write_atomic_replace,
415 };
416
417 use crate::TempDir;
418
419 #[test]
420 fn bounded_read_accepts_regular_files_within_limit() {
421 let root = TempDir::new().unwrap();
422 let path = root.write_file("file.txt", b"hello").unwrap();
423
424 assert_eq!(read_bounded(&path, 5).unwrap(), b"hello");
425 assert_eq!(read_string_bounded(&path, 5).unwrap(), "hello");
426 }
427
428 #[test]
429 fn bounded_read_rejects_oversized_files() {
430 let root = TempDir::new().unwrap();
431 let path = root.write_file("file.txt", b"hello").unwrap();
432
433 let error = read_bounded(&path, 4).unwrap_err();
434
435 assert!(is_file_too_large_error(&error));
436 }
437
438 #[test]
439 fn bounded_read_rejects_directories() {
440 let root = TempDir::new().unwrap();
441
442 let error = read_bounded(root.path(), 1024).unwrap_err();
443
444 assert!(is_not_regular_file_error(&error));
445 }
446
447 #[cfg(unix)]
448 #[test]
449 fn bounded_read_rejects_final_symlinks() {
450 let root = TempDir::new().unwrap();
451 let target = root.write_file("target.txt", b"hello").unwrap();
452 let link = root.child("link.txt").unwrap();
453 std::os::unix::fs::symlink(&target, &link).unwrap();
454
455 let error = read_bounded(&link, 1024).unwrap_err();
456
457 assert!(is_symlink_not_allowed_error(&error));
458 }
459
460 #[test]
461 fn atomic_replace_overwrites_existing_files() {
462 let root = TempDir::new().unwrap();
463 let path = root.write_file("file.txt", b"old").unwrap();
464
465 write_atomic_replace(&path, b"new", "test").unwrap();
466
467 assert_eq!(read_string(&path).unwrap(), "new");
468 }
469
470 #[test]
471 fn replace_policy_still_rejects_destination_directories() {
472 let root = TempDir::new().unwrap();
473 let temp = root.write_file("temp.txt", b"temp").unwrap();
474 let dest = root.child("dest").unwrap();
475 std::fs::create_dir_all(&dest).unwrap();
476
477 assert!(persist_temp_file_with_replace(&temp, &dest, true).is_err());
478 }
479}