1#![allow(clippy::needless_pass_by_value)]
3
4use std::path::Path;
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8
9use crate::file_error::file_too_large_error;
10pub use crate::file_error::{
11 is_file_too_large_error, is_not_regular_file_error, is_symlink_not_allowed_error,
12};
13use crate::path::parent_dir;
14use crate::temp::sibling_temp_path;
15use crate::types::FileMeta;
16
17const WRITE_ATOMIC_TEMP_ATTEMPTS: usize = 16;
18
19pub type AsyncFile = tokio::fs::File;
21
22pub async fn create_parent_dir(path: &Path) -> AppResult<()> {
24 if let Some(parent) = parent_dir(path) {
25 super::dir::create_all(parent).await?;
26 }
27 Ok(())
28}
29
30pub async fn exists(path: &Path) -> AppResult<bool> {
32 exists_from_metadata(path, tokio::fs::symlink_metadata(path).await)
33}
34
35fn exists_from_metadata(
36 path: &Path,
37 result: std::io::Result<std::fs::Metadata>,
38) -> AppResult<bool> {
39 match result {
40 Ok(metadata) => Ok(metadata.is_file() && !metadata.file_type().is_symlink()),
41 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
42 Err(error) => Err(inspect_file_error(path, error)),
43 }
44}
45
46pub async fn metadata(path: &Path) -> AppResult<FileMeta> {
48 let metadata = tokio::fs::symlink_metadata(path)
49 .await
50 .map_err(|error| inspect_file_error(path, error))?;
51 Ok(FileMeta {
52 path: path.to_path_buf(),
53 len: metadata.len(),
54 created: metadata.created().ok(),
55 modified: metadata.modified().ok(),
56 is_file: metadata.is_file(),
57 is_dir: metadata.is_dir(),
58 is_symlink: metadata.file_type().is_symlink(),
59 })
60}
61
62pub async fn read(path: &Path) -> AppResult<Vec<u8>> {
64 tokio::fs::read(path)
65 .await
66 .map_err(|error| read_file_error(path, error))
67}
68
69pub async fn read_string(path: &Path) -> AppResult<String> {
71 tokio::fs::read_to_string(path)
72 .await
73 .map_err(|error| read_file_error(path, error))
74}
75
76pub async fn read_bounded(path: &Path, max_bytes: u64) -> AppResult<Vec<u8>> {
78 let file = open_no_follow_regular(path).await?;
79 let metadata = file
80 .metadata()
81 .await
82 .map_err(|error| inspect_file_error(path, error))?;
83 if metadata.is_file() && metadata.len() > max_bytes {
84 return Err(file_too_large_error(path, metadata.len(), max_bytes));
85 }
86
87 let capacity = metadata.len().min(max_bytes).try_into().unwrap_or(0);
88 let mut bytes = Vec::with_capacity(capacity);
89 file.take(max_bytes.saturating_add(1))
90 .read_to_end(&mut bytes)
91 .await
92 .map_err(|error| read_file_error(path, error))?;
93 if bytes.len() as u64 > max_bytes {
94 return Err(file_too_large_error(path, bytes.len() as u64, max_bytes));
95 }
96 Ok(bytes)
97}
98
99pub async fn read_string_bounded(path: &Path, max_bytes: u64) -> AppResult<String> {
101 let bytes = read_bounded(path, max_bytes).await?;
102 String::from_utf8(bytes).map_err(|error| {
103 AppError::new(
104 ErrorCode::InvalidInput,
105 format!("file '{}' is not valid UTF-8: {error}", path.display()),
106 )
107 })
108}
109
110pub async fn write(path: &Path, bytes: impl AsRef<[u8]>) -> AppResult<()> {
112 create_parent_dir(path).await?;
113 tokio::fs::write(path, bytes)
114 .await
115 .map_err(|error| write_file_error(path, error))
116}
117
118pub async fn open(path: &Path) -> AppResult<AsyncFile> {
120 tokio::fs::File::open(path)
121 .await
122 .map_err(|error| open_file_error(path, error))
123}
124
125pub async fn open_no_follow_regular(path: &Path) -> AppResult<AsyncFile> {
127 let path = path.to_path_buf();
128 let file = tokio::task::spawn_blocking({
129 let path = path.clone();
130 move || crate::sync_io::file::open_no_follow_regular(&path)
131 })
132 .await
133 .map_err(|error| {
134 AppError::new(
135 ErrorCode::Internal,
136 format!(
137 "failed to join no-follow file open task for '{}': {error}",
138 path.display()
139 ),
140 )
141 .with_cause(error)
142 })??;
143
144 Ok(AsyncFile::from_std(file))
145}
146
147pub async fn create(path: &Path) -> AppResult<AsyncFile> {
149 create_parent_dir(path).await?;
150 tokio::fs::File::create(path)
151 .await
152 .map_err(|error| create_file_error(path, error))
153}
154
155pub async fn persist_temp_file(temp_path: &Path, dest: &Path) -> AppResult<()> {
161 tokio::fs::rename(temp_path, dest)
162 .await
163 .map_err(|error| rename_file_error(temp_path, dest, error))
164}
165
166pub async fn copy(from: &Path, to: &Path) -> AppResult<u64> {
168 create_parent_dir(to).await?;
169 tokio::fs::copy(from, to)
170 .await
171 .map_err(|error| copy_file_error(from, to, error))
172}
173
174pub async fn rename(from: &Path, to: &Path) -> AppResult<()> {
176 create_parent_dir(to).await?;
177 tokio::fs::rename(from, to)
178 .await
179 .map_err(|error| rename_file_error(from, to, error))
180}
181
182pub async fn move_file(from: &Path, to: &Path) -> AppResult<()> {
187 create_parent_dir(to).await?;
188 move_file_after_rename(from, to, tokio::fs::rename(from, to).await).await
189}
190
191async fn move_file_after_rename(
192 from: &Path,
193 to: &Path,
194 result: std::io::Result<()>,
195) -> AppResult<()> {
196 match result {
197 Ok(()) => Ok(()),
198 Err(error) if is_cross_device_error(&error) => {
199 copy(from, to).await?;
200 remove(from).await
201 }
202 Err(error) => Err(move_file_error(from, to, error)),
203 }
204}
205
206pub async fn remove(path: &Path) -> AppResult<()> {
208 tokio::fs::remove_file(path)
209 .await
210 .map_err(|error| remove_file_error(path, error))
211}
212
213fn is_cross_device_error(error: &std::io::Error) -> bool {
214 #[cfg(unix)]
215 {
216 error.raw_os_error() == Some(libc::EXDEV)
217 }
218 #[cfg(not(unix))]
219 {
220 error.kind() == std::io::ErrorKind::CrossesDevices
221 }
222}
223
224pub async fn remove_if_exists(path: &Path) -> AppResult<bool> {
226 match tokio::fs::remove_file(path).await {
227 Ok(()) => Ok(true),
228 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
229 Err(error) => Err(remove_file_error(path, error)),
230 }
231}
232
233pub async fn write_atomic(
239 dest: &Path,
240 bytes: impl AsRef<[u8]>,
241 temp_prefix: &str,
242) -> AppResult<()> {
243 write_atomic_with_attempts(dest, bytes, temp_prefix, WRITE_ATOMIC_TEMP_ATTEMPTS, false).await
244}
245
246pub async fn write_atomic_replace(
252 dest: &Path,
253 bytes: impl AsRef<[u8]>,
254 temp_prefix: &str,
255) -> AppResult<()> {
256 write_atomic_with_attempts(dest, bytes, temp_prefix, WRITE_ATOMIC_TEMP_ATTEMPTS, true).await
257}
258
259async fn write_atomic_with_attempts(
260 dest: &Path,
261 bytes: impl AsRef<[u8]>,
262 temp_prefix: &str,
263 attempts: usize,
264 replace_existing: bool,
265) -> AppResult<()> {
266 create_parent_dir(dest).await?;
267
268 let bytes = bytes.as_ref();
269 for _ in 0..attempts {
270 let temp_path = sibling_temp_path(dest, temp_prefix, ".tmp");
271 let mut temp_file = match tokio::fs::OpenOptions::new()
272 .write(true)
273 .create_new(true)
274 .open(&temp_path)
275 .await
276 {
277 Ok(file) => file,
278 Err(error) if should_retry_temp_open(&error) => continue,
279 Err(error) => return Err(create_temp_file_error(&temp_path, error)),
280 };
281
282 let result = async {
283 temp_file
284 .write_all(bytes)
285 .await
286 .map_err(|error| write_temp_file_error(&temp_path, error))?;
287 temp_file
288 .sync_data()
289 .await
290 .map_err(|error| sync_temp_file_error(&temp_path, error))?;
291 drop(temp_file);
292 persist_temp_file_with_replace(&temp_path, dest, replace_existing).await
293 }
294 .await;
295
296 if result.is_err() {
297 let _ = remove_if_exists(&temp_path).await;
298 }
299
300 return result;
301 }
302
303 Err(unique_temp_file_error(dest, attempts))
304}
305
306async fn persist_temp_file_with_replace(
307 temp_path: &Path,
308 dest: &Path,
309 replace_existing: bool,
310) -> AppResult<()> {
311 #[cfg(windows)]
312 if replace_existing {
313 remove_if_exists(dest).await?;
314 }
315
316 let _ = replace_existing;
317 persist_temp_file(temp_path, dest).await
318}
319
320fn should_retry_temp_open(error: &std::io::Error) -> bool {
321 error.kind() == std::io::ErrorKind::AlreadyExists
322}
323
324fn inspect_file_error(path: &Path, error: std::io::Error) -> AppError {
325 AppError::new(
326 ErrorCode::Internal,
327 format!("failed to inspect file '{}': {error}", path.display()),
328 )
329 .with_cause(error)
330}
331
332fn read_file_error(path: &Path, error: std::io::Error) -> AppError {
333 AppError::new(
334 ErrorCode::Internal,
335 format!("failed to read file '{}': {error}", path.display()),
336 )
337 .with_cause(error)
338}
339
340fn open_file_error(path: &Path, error: std::io::Error) -> AppError {
341 AppError::new(
342 ErrorCode::Internal,
343 format!("failed to open file '{}': {error}", path.display()),
344 )
345 .with_cause(error)
346}
347
348fn create_file_error(path: &Path, error: std::io::Error) -> AppError {
349 AppError::new(
350 ErrorCode::Internal,
351 format!("failed to create file '{}': {error}", path.display()),
352 )
353 .with_cause(error)
354}
355
356fn write_file_error(path: &Path, error: std::io::Error) -> AppError {
357 AppError::new(
358 ErrorCode::Internal,
359 format!("failed to write file '{}': {error}", path.display()),
360 )
361 .with_cause(error)
362}
363
364fn copy_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
365 AppError::new(
366 ErrorCode::Internal,
367 format!(
368 "failed to copy '{}' to '{}': {error}",
369 from.display(),
370 to.display()
371 ),
372 )
373 .with_cause(error)
374}
375
376fn rename_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
377 AppError::new(
378 ErrorCode::Internal,
379 format!(
380 "failed to rename '{}' to '{}': {error}",
381 from.display(),
382 to.display()
383 ),
384 )
385 .with_cause(error)
386}
387
388fn move_file_error(from: &Path, to: &Path, error: std::io::Error) -> AppError {
389 AppError::new(
390 ErrorCode::Internal,
391 format!(
392 "failed to move '{}' to '{}': {error}",
393 from.display(),
394 to.display()
395 ),
396 )
397 .with_cause(error)
398}
399
400fn remove_file_error(path: &Path, error: std::io::Error) -> AppError {
401 AppError::new(
402 ErrorCode::Internal,
403 format!("failed to remove '{}': {error}", path.display()),
404 )
405 .with_cause(error)
406}
407
408fn create_temp_file_error(path: &Path, error: std::io::Error) -> AppError {
409 AppError::new(
410 ErrorCode::Internal,
411 format!("failed to create temp file '{}': {error}", path.display()),
412 )
413 .with_cause(error)
414}
415
416fn write_temp_file_error(path: &Path, error: std::io::Error) -> AppError {
417 AppError::new(
418 ErrorCode::Internal,
419 format!("failed to write temp file '{}': {error}", path.display()),
420 )
421 .with_cause(error)
422}
423
424fn sync_temp_file_error(path: &Path, error: std::io::Error) -> AppError {
425 AppError::new(
426 ErrorCode::Internal,
427 format!("failed to sync temp file '{}': {error}", path.display()),
428 )
429 .with_cause(error)
430}
431
432fn unique_temp_file_error(dest: &Path, attempts: usize) -> AppError {
433 AppError::new(
434 ErrorCode::Internal,
435 format!(
436 "failed to create a unique temp file for '{}' after {attempts} attempts",
437 dest.display()
438 ),
439 )
440}
441
442#[cfg(test)]
443mod tests {
444 use super::{
445 WRITE_ATOMIC_TEMP_ATTEMPTS, copy, copy_file_error, create_parent_dir,
446 create_temp_file_error, exists, exists_from_metadata, inspect_file_error,
447 is_file_too_large_error, is_not_regular_file_error, is_symlink_not_allowed_error, metadata,
448 move_file, move_file_after_rename, move_file_error, open_no_follow_regular,
449 persist_temp_file, persist_temp_file_with_replace, read, read_bounded, read_file_error,
450 read_string, read_string_bounded, remove, remove_file_error, remove_if_exists, rename,
451 rename_file_error, should_retry_temp_open, sync_temp_file_error, unique_temp_file_error,
452 write, write_atomic, write_atomic_replace, write_atomic_with_attempts, write_file_error,
453 write_temp_file_error,
454 };
455 use crate::TempDir;
456
457 #[tokio::test]
458 async fn file_lifecycle() {
459 let root = TempDir::new().unwrap();
460 let path = root.child("a/b.txt").unwrap();
461
462 write(&path, b"hello").await.unwrap();
463 assert!(exists(&path).await.unwrap());
464 assert_eq!(read(&path).await.unwrap(), b"hello");
465 assert_eq!(read_string(&path).await.unwrap(), "hello");
466 assert_eq!(metadata(&path).await.unwrap().len, 5);
467
468 let copy_path = root.child("copy/b.txt").unwrap();
469 assert_eq!(copy(&path, ©_path).await.unwrap(), 5);
470 assert_eq!(read_string(©_path).await.unwrap(), "hello");
471
472 let renamed = root.child("renamed/b.txt").unwrap();
473 rename(©_path, &renamed).await.unwrap();
474 assert!(!exists(©_path).await.unwrap());
475 assert!(exists(&renamed).await.unwrap());
476 assert!(remove_if_exists(&renamed).await.unwrap());
477 assert!(!remove_if_exists(&renamed).await.unwrap());
478 }
479
480 #[tokio::test]
481 async fn create_parent_dir_ignores_paths_without_parent() {
482 create_parent_dir(std::path::Path::new("file.txt"))
483 .await
484 .unwrap();
485 }
486
487 #[tokio::test]
488 async fn file_error_paths_are_reported() {
489 let root = TempDir::new().unwrap();
490 let missing = root.child("missing.txt").unwrap();
491 let dir = root.child("dir").unwrap();
492 crate::async_io::dir::create_all(&dir).await.unwrap();
493 let nested_under_file = root.child("file.txt/child.txt").unwrap();
494 root.write_file("file.txt", b"hello").unwrap();
495
496 assert!(!exists(&missing).await.unwrap());
497 assert!(read(&missing).await.is_err());
498 assert!(read_string(&missing).await.is_err());
499 assert!(write(&nested_under_file, b"nope").await.is_err());
500 assert!(
501 copy(&missing, &root.child("copy.txt").unwrap())
502 .await
503 .is_err()
504 );
505 assert!(
506 rename(&missing, &root.child("renamed.txt").unwrap())
507 .await
508 .is_err()
509 );
510 assert!(remove(&missing).await.is_err());
511 assert!(remove_if_exists(&dir).await.is_err());
512 }
513
514 #[tokio::test]
515 async fn bounded_read_accepts_regular_files_within_limit() {
516 let root = TempDir::new().unwrap();
517 let path = root.write_file("file.txt", b"hello").unwrap();
518
519 assert_eq!(read_bounded(&path, 5).await.unwrap(), b"hello");
520 assert_eq!(read_string_bounded(&path, 5).await.unwrap(), "hello");
521 }
522
523 #[tokio::test]
524 async fn bounded_read_rejects_oversized_files() {
525 let root = TempDir::new().unwrap();
526 let path = root.write_file("file.txt", b"hello").unwrap();
527
528 let error = read_bounded(&path, 4).await.unwrap_err();
529
530 assert!(is_file_too_large_error(&error));
531 }
532
533 #[tokio::test]
534 async fn bounded_read_rejects_directories() {
535 let root = TempDir::new().unwrap();
536
537 let error = read_bounded(root.path(), 1024).await.unwrap_err();
538
539 assert!(is_not_regular_file_error(&error));
540 }
541
542 #[cfg(unix)]
543 #[tokio::test]
544 async fn bounded_read_rejects_final_symlinks() {
545 let root = TempDir::new().unwrap();
546 let target = root.write_file("target.txt", b"hello").unwrap();
547 let link = root.child("link.txt").unwrap();
548 std::os::unix::fs::symlink(&target, &link).unwrap();
549
550 let error = read_bounded(&link, 1024).await.unwrap_err();
551
552 assert!(is_symlink_not_allowed_error(&error));
553 }
554
555 #[tokio::test]
556 async fn no_follow_regular_open_accepts_regular_files() {
557 let root = TempDir::new().unwrap();
558 let path = root.write_file("file.txt", b"hello").unwrap();
559
560 let file = open_no_follow_regular(&path).await.unwrap();
561
562 assert!(file.metadata().await.unwrap().is_file());
563 }
564
565 #[test]
566 fn file_error_builders_include_context() {
567 let from = std::path::Path::new("from.txt");
568 let to = std::path::Path::new("to.txt");
569 let err = || std::io::Error::other("boom");
570
571 assert!(
572 inspect_file_error(from, err())
573 .to_string()
574 .contains("inspect file")
575 );
576 assert!(
577 read_file_error(from, err())
578 .to_string()
579 .contains("read file")
580 );
581 assert!(
582 write_file_error(from, err())
583 .to_string()
584 .contains("write file")
585 );
586 assert!(
587 copy_file_error(from, to, err())
588 .to_string()
589 .contains("copy")
590 );
591 assert!(
592 rename_file_error(from, to, err())
593 .to_string()
594 .contains("rename")
595 );
596 assert!(
597 move_file_error(from, to, err())
598 .to_string()
599 .contains("move")
600 );
601 assert!(
602 remove_file_error(from, err())
603 .to_string()
604 .contains("remove")
605 );
606 assert!(
607 create_temp_file_error(from, err())
608 .to_string()
609 .contains("create temp file")
610 );
611 assert!(
612 write_temp_file_error(from, err())
613 .to_string()
614 .contains("write temp file")
615 );
616 assert!(
617 sync_temp_file_error(from, err())
618 .to_string()
619 .contains("sync temp file")
620 );
621 assert!(
622 unique_temp_file_error(to, WRITE_ATOMIC_TEMP_ATTEMPTS)
623 .to_string()
624 .contains("unique temp file")
625 );
626 assert!(exists_from_metadata(from, Err(err())).is_err());
627 assert!(should_retry_temp_open(&std::io::Error::new(
628 std::io::ErrorKind::AlreadyExists,
629 "exists",
630 )));
631 assert!(!should_retry_temp_open(&err()));
632 }
633
634 #[tokio::test]
635 async fn metadata_reports_symlinks_and_missing_errors() {
636 let root = TempDir::new().unwrap();
637 let missing = root.child("missing.txt").unwrap();
638 assert!(metadata(&missing).await.is_err());
639
640 #[cfg(unix)]
641 {
642 let file = root.write_file("file.txt", b"hello").unwrap();
643 let link = root.child("link.txt").unwrap();
644 std::os::unix::fs::symlink(&file, &link).unwrap();
645 let meta = metadata(&link).await.unwrap();
646 assert_eq!(meta.path, link);
647 assert!(meta.is_symlink);
648 assert!(meta.modified.is_some());
649 }
650 }
651
652 #[tokio::test]
653 async fn persist_and_move_helpers_cover_success_and_errors() {
654 let root = TempDir::new().unwrap();
655 let temp = root.write_file("temp.txt", b"temp").unwrap();
656 let dest = root.child("dest.txt").unwrap();
657 persist_temp_file(&temp, &dest).await.unwrap();
658 assert_eq!(read_string(&dest).await.unwrap(), "temp");
659
660 let missing = root.child("missing.txt").unwrap();
661 assert!(
662 persist_temp_file(&missing, &root.child("other.txt").unwrap())
663 .await
664 .is_err()
665 );
666
667 let moved = root.child("moved.txt").unwrap();
668 move_file(&dest, &moved).await.unwrap();
669 assert_eq!(read_string(&moved).await.unwrap(), "temp");
670 assert!(
671 move_file(&missing, &root.child("nope.txt").unwrap())
672 .await
673 .is_err()
674 );
675
676 #[cfg(unix)]
677 {
678 let source = root.write_file("cross-device.txt", b"temp").unwrap();
679 let target = root.child("cross-device-moved.txt").unwrap();
680 move_file_after_rename(
681 &source,
682 &target,
683 Err(std::io::Error::from_raw_os_error(libc::EXDEV)),
684 )
685 .await
686 .unwrap();
687 assert_eq!(read_string(&target).await.unwrap(), "temp");
688 assert!(!source.exists());
689 }
690 }
691
692 #[tokio::test]
693 async fn atomic_write_creates_parent_dirs() {
694 let root = TempDir::new().unwrap();
695 let path = root.child("nested/file.txt").unwrap();
696
697 write_atomic(&path, b"atomic", "test").await.unwrap();
698
699 assert_eq!(read_string(&path).await.unwrap(), "atomic");
700 }
701
702 #[tokio::test]
703 async fn atomic_replace_overwrites_existing_files() {
704 let root = TempDir::new().unwrap();
705 let path = root.write_file("file.txt", b"old").unwrap();
706
707 write_atomic_replace(&path, b"new", "test").await.unwrap();
708
709 assert_eq!(read_string(&path).await.unwrap(), "new");
710 }
711
712 #[tokio::test]
713 async fn atomic_write_sanitizes_temp_prefix() {
714 let root = TempDir::new().unwrap();
715 let path = root.child("nested/file.txt").unwrap();
716
717 write_atomic(&path, b"atomic", "../escape").await.unwrap();
718
719 assert_eq!(read_string(&path).await.unwrap(), "atomic");
720 assert!(!root.child("escape").unwrap().exists());
721 }
722
723 #[tokio::test]
724 async fn atomic_write_reports_destination_parent_errors() {
725 let root = TempDir::new().unwrap();
726 root.write_file("file.txt", b"hello").unwrap();
727 let path = root.child("file.txt/nested.txt").unwrap();
728
729 assert!(write_atomic(&path, b"atomic", "test").await.is_err());
730 }
731
732 #[tokio::test]
733 async fn atomic_write_reports_persist_and_attempt_errors() {
734 let root = TempDir::new().unwrap();
735 let dest_dir = root.child("dest").unwrap();
736 crate::async_io::dir::create_all(&dest_dir).await.unwrap();
737
738 assert!(write_atomic(&dest_dir, b"atomic", "test").await.is_err());
739 assert!(
740 write_atomic_with_attempts(
741 &root.child("file.txt").unwrap(),
742 b"atomic",
743 "test",
744 0,
745 false
746 )
747 .await
748 .is_err()
749 );
750 assert!(
751 write_atomic(
752 &root.child("too-long.txt").unwrap(),
753 b"atomic",
754 &"x".repeat(300)
755 )
756 .await
757 .is_err()
758 );
759 }
760
761 #[tokio::test]
762 async fn replace_policy_still_rejects_destination_directories() {
763 let root = TempDir::new().unwrap();
764 let temp = root.write_file("temp.txt", b"temp").unwrap();
765 let dest = root.child("dest").unwrap();
766 crate::async_io::dir::create_all(&dest).await.unwrap();
767
768 assert!(
769 persist_temp_file_with_replace(&temp, &dest, true)
770 .await
771 .is_err()
772 );
773 }
774
775 #[cfg(unix)]
776 #[tokio::test]
777 async fn exists_rejects_symlinks_to_files() {
778 let root = TempDir::new().unwrap();
779 let path = root.child("file.txt").unwrap();
780 let link = root.child("link.txt").unwrap();
781 write(&path, b"hello").await.unwrap();
782 std::os::unix::fs::symlink(&path, &link).unwrap();
783
784 assert!(!exists(&link).await.unwrap());
785 }
786}