1use std::{
4 env,
5 ffi::OsString,
6 fmt::{self, Debug, Formatter},
7 future::Future,
8 io::{self, ErrorKind, Write as _},
9 mem::take,
10 path::{MAIN_SEPARATOR, Path, PathBuf},
11 sync::{Arc, LazyLock, Weak},
12 time::Duration,
13};
14
15use anyhow::{Context, Result, anyhow, bail};
16use async_trait::async_trait;
17use bincode::{Decode, Encode};
18#[cfg(windows)]
19use omnipath::WinPathExt;
20use rustc_hash::FxHashSet;
21use smallvec::SmallVec;
22use tokio::{
23 runtime::Handle,
24 sync::{RwLock, RwLockReadGuard},
25};
26use tracing::Instrument;
27use turbo_rcstr::RcStr;
28use turbo_tasks::{
29 CapturedEffect, Effect, EffectExt, EffectStateStorage, InvalidationReason, NonLocalValue,
30 ReadRef, ResolvedVc, TurboTasksApi, ValueToString, Vc, debug::ValueDebugFormat, parallel,
31 trace::TraceRawVcs, turbo_tasks_weak, turbobail,
32};
33use turbo_tasks_hash::{hash_xxh3_hash64, hash_xxh3_hash128};
34use turbo_unix_path::{normalize_path, sys_to_unix, unix_to_sys};
35
36use crate::{
37 AnyhowWrapper, File, FileComparison, FileContent, FileMeta, FileSystem, FileSystemEntryType,
38 FileSystemPath, LinkContent, LinkType, PersistedFileContent, RawDirectoryContent,
39 RawDirectoryEntry,
40 invalidation::Write,
41 invalidator_map::InvalidatorMap,
42 mutex_map::MutexMap,
43 path_map::OrderedPathMapExt,
44 retry::{can_retry, retry_blocking, retry_blocking_custom},
45 watcher::DiskWatcher,
46};
47
48pub fn validate_path_length(path: &Path) -> io::Result<()> {
73 fn error(name_or_path: &str, len: usize, limit: usize) -> io::Error {
74 io::Error::new(
75 io::ErrorKind::InvalidFilename,
76 format!("file {name_or_path} is too long ({len}) exceeds filesystem limit of {limit}"),
77 )
78 }
79 if cfg!(windows) {
80 debug_assert!(
82 matches!(
83 path.components().next(),
84 Some(std::path::Component::Prefix(prefix)) if prefix.kind().is_verbatim()
85 ),
86 "expected a verbatim path, got {path:?}",
87 );
88
89 const MAX_VERBATIM_PATH_LENGTH_WINDOWS: usize = 32_767 - 100;
94 let len = path.as_os_str().len();
95 if len > MAX_VERBATIM_PATH_LENGTH_WINDOWS {
96 return Err(error("path", len, MAX_VERBATIM_PATH_LENGTH_WINDOWS));
97 }
98 }
99
100 if cfg!(unix) {
101 const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
105
106 let name_len = path
109 .file_name()
110 .map(|n| n.as_encoded_bytes().len())
111 .unwrap_or(0);
112 if name_len > MAX_FILE_NAME_LENGTH_UNIX {
113 return Err(error("name", name_len, MAX_FILE_NAME_LENGTH_UNIX));
114 }
115
116 if cfg!(target_os = "macos") {
120 #[cfg(unix)]
121 {
122 use std::os::unix::ffi::OsStrExt;
123 const MAX_PATH_LENGTH: usize = 1024 - 8;
127 let path_len = path.as_os_str().as_bytes().len();
128 if path_len > MAX_PATH_LENGTH {
129 return Err(error("path", path_len, MAX_PATH_LENGTH));
130 }
131 }
132 }
133 }
134
135 Ok(())
137}
138
139pub fn canonicalize_to_rcstr(path: &Path) -> io::Result<RcStr> {
145 fs_err::canonicalize(path)?
146 .into_string()
147 .map(RcStr::from)
148 .map_err(|p| {
149 io::Error::new(
150 ErrorKind::InvalidFilename,
151 anyhow!("canonicalized path {p:?} is not valid unicode"),
152 )
153 })
154}
155
156trait ConcurrencyLimitedExt {
157 type Output;
158 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
159}
160
161impl<F, R> ConcurrencyLimitedExt for F
162where
163 F: Future<Output = R>,
164{
165 type Output = R;
166 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
167 let _permit = semaphore.acquire().await;
168 self.await
169 }
170}
171
172fn number_env_var(name: &'static str) -> Option<usize> {
173 env::var(name)
174 .ok()
175 .filter(|val| !val.is_empty())
176 .map(|val| match val.parse() {
177 Ok(n) => n,
178 Err(err) => panic!("{name} must be a valid integer: {err}"),
179 })
180 .filter(|val| *val != 0)
181}
182
183fn create_read_semaphore() -> tokio::sync::Semaphore {
184 static TURBO_ENGINE_READ_CONCURRENCY: LazyLock<usize> =
187 LazyLock::new(|| number_env_var("TURBO_ENGINE_READ_CONCURRENCY").unwrap_or(64));
188 tokio::sync::Semaphore::new(*TURBO_ENGINE_READ_CONCURRENCY)
189}
190
191fn create_write_semaphore() -> tokio::sync::Semaphore {
192 static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
195 number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
196 4,
199 )
200 });
201 tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
202}
203
204#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
205pub(crate) struct DiskFileSystemInner {
206 pub name: RcStr,
207 root: RcStr,
215 #[turbo_tasks(debug_ignore, trace_ignore)]
216 #[bincode(skip)]
217 mutex_map: MutexMap<Arc<PathBuf>>,
218 #[turbo_tasks(debug_ignore, trace_ignore)]
219 #[bincode(skip)]
220 pub(crate) invalidator_map: InvalidatorMap,
221 #[turbo_tasks(debug_ignore, trace_ignore)]
222 #[bincode(skip)]
223 pub(crate) dir_invalidator_map: InvalidatorMap,
224 #[turbo_tasks(debug_ignore, trace_ignore)]
227 #[bincode(skip)]
228 pub(crate) invalidation_lock: RwLock<()>,
229 #[turbo_tasks(debug_ignore, trace_ignore)]
231 #[bincode(skip, default = "create_read_semaphore")]
232 read_semaphore: tokio::sync::Semaphore,
233 #[turbo_tasks(debug_ignore, trace_ignore)]
235 #[bincode(skip, default = "create_write_semaphore")]
236 write_semaphore: tokio::sync::Semaphore,
237
238 #[turbo_tasks(debug_ignore, trace_ignore)]
239 pub(crate) watcher: DiskWatcher,
240 denied_paths: Vec<RcStr>,
243 #[turbo_tasks(debug_ignore, trace_ignore)]
246 #[bincode(skip, default = "turbo_tasks_weak")]
247 pub(crate) turbo_tasks: Weak<dyn TurboTasksApi>,
248 #[turbo_tasks(debug_ignore, trace_ignore)]
250 #[bincode(skip, default = "Handle::current")]
251 pub(crate) tokio_handle: Handle,
252 #[turbo_tasks(debug_ignore, trace_ignore)]
253 #[bincode(skip)]
254 effect_state_storage: EffectStateStorage,
255}
256
257impl DiskFileSystemInner {
258 pub(crate) fn root_path(&self) -> &Path {
259 Path::new(&*self.root)
260 }
261
262 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
272 let path = &path.path;
273 self.denied_paths.iter().any(|denied_path| {
274 path.starts_with(denied_path.as_str())
275 && (path.len() == denied_path.len()
276 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
277 })
278 }
279
280 async fn register_read_invalidator(&self, path: &Arc<PathBuf>) -> Result<()> {
283 if let Some(invalidator) = turbo_tasks::get_invalidator() {
284 self.invalidator_map.insert(path.clone(), invalidator);
285 self.watcher
286 .ensure_watched_file(path, self.root_path())
287 .await?;
288 }
289 Ok(())
290 }
291
292 fn invalidate_from_write(&self, full_path: &Path) {
296 let mut invalidator_map = self.invalidator_map.lock().unwrap();
297 if let Some(invalidators) = invalidator_map.remove(full_path) {
298 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
299 return;
300 };
301 let _guard = self.tokio_handle.enter();
302 let reason = Write {
303 path: full_path.to_string_lossy().into_owned(),
304 };
305 for invalidator in invalidators {
306 invalidator.invalidate_with_reason(&*turbo_tasks, reason.clone());
307 }
308 }
309 }
310
311 async fn register_dir_invalidator(&self, path: &Arc<PathBuf>) -> Result<()> {
314 if let Some(invalidator) = turbo_tasks::get_invalidator() {
315 self.dir_invalidator_map.insert(path.clone(), invalidator);
316 self.watcher
317 .ensure_watched_dir(path, self.root_path())
318 .await?;
319 }
320 Ok(())
321 }
322
323 async fn lock_path(&self, full_path: Arc<PathBuf>) -> PathLockGuard<'_> {
324 let lock1 = self.invalidation_lock.read().await;
325 let lock2 = self.mutex_map.lock(full_path).await;
326 PathLockGuard(lock1, lock2)
327 }
328
329 pub(crate) fn invalidate(&self) {
330 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
331 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
332 return;
333 };
334 let _guard = self.tokio_handle.enter();
335
336 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
337 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
338 let invalidators = invalidator_map
339 .into_iter()
340 .chain(dir_invalidator_map)
341 .flat_map(|(_, invalidators)| invalidators.into_iter())
342 .collect::<Vec<_>>();
343 parallel::for_each_owned(invalidators, |invalidator| {
344 invalidator.invalidate(&*turbo_tasks)
345 });
346 }
347
348 pub(crate) fn invalidate_with_reason<R: InvalidationReason + Clone>(
352 &self,
353 reason: impl Fn(&Path) -> R + Sync,
354 ) {
355 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
356 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
357 return;
358 };
359 let _guard = self.tokio_handle.enter();
360
361 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
362 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
363 let invalidators = invalidator_map
364 .into_iter()
365 .chain(dir_invalidator_map)
366 .flat_map(|(path, invalidators)| {
367 let reason_for_path = reason(&path);
368 invalidators
369 .into_iter()
370 .map(move |i| (reason_for_path.clone(), i))
371 })
372 .collect::<Vec<_>>();
373 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
374 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
375 });
376 }
377
378 fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
382 &self,
383 paths: impl IntoIterator<Item = PathBuf>,
384 reason: impl Fn(&Path) -> R + Sync,
385 ) {
386 let _span =
387 tracing::info_span!("invalidate filesystem paths", name = &*self.root).entered();
388 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
389 return;
390 };
391 let _guard = self.tokio_handle.enter();
392
393 let mut invalidator_map = self.invalidator_map.lock().unwrap();
394 let mut dir_invalidator_map = self.dir_invalidator_map.lock().unwrap();
395 let mut invalidators = Vec::new();
396 let mut parent_dirs_to_invalidate = FxHashSet::default();
397
398 for path in paths {
399 let mut current_parent = path.parent();
400 while let Some(parent) = current_parent {
401 parent_dirs_to_invalidate.insert(parent.to_path_buf());
402 current_parent = parent.parent();
403 }
404
405 for (invalidated_path, path_invalidators) in
406 invalidator_map.extract_path_with_children(&path)
407 {
408 let reason_for_path = reason(&invalidated_path);
409 invalidators.extend(
410 path_invalidators
411 .into_iter()
412 .map(|invalidator| (reason_for_path.clone(), invalidator)),
413 );
414 }
415
416 for (invalidated_path, path_invalidators) in
417 dir_invalidator_map.extract_path_with_children(&path)
418 {
419 let reason_for_path = reason(&invalidated_path);
420 invalidators.extend(
421 path_invalidators
422 .into_iter()
423 .map(|invalidator| (reason_for_path.clone(), invalidator)),
424 );
425 }
426 }
427
428 for path in parent_dirs_to_invalidate {
429 if let Some(path_invalidators) = dir_invalidator_map.remove(path.as_path()) {
430 let reason_for_path = reason(&path);
431 invalidators.extend(
432 path_invalidators
433 .into_iter()
434 .map(|invalidator| (reason_for_path.clone(), invalidator)),
435 );
436 }
437 }
438
439 drop(invalidator_map);
440 drop(dir_invalidator_map);
441
442 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
443 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
444 });
445 }
446
447 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
448 async fn start_watching_internal(
449 self: &Arc<Self>,
450 report_invalidation_reason: bool,
451 poll_interval: Option<Duration>,
452 ) -> Result<()> {
453 let root_path = self.root_path().to_path_buf();
454
455 retry_blocking(|| std::fs::create_dir_all(&root_path))
457 .instrument(tracing::info_span!("create root directory", name = ?root_path))
458 .concurrency_limited(&self.write_semaphore)
459 .await?;
460
461 self.watcher
462 .start_watching(self.clone(), report_invalidation_reason, poll_interval)
463 .await?;
464
465 Ok(())
466 }
467}
468
469#[derive(Clone, ValueToString)]
475#[value_to_string(self.inner.name)]
476#[turbo_tasks::value(cell = "new", eq = "manual", evict = "never")]
477pub struct DiskFileSystem {
478 inner: Arc<DiskFileSystemInner>,
479}
480
481impl DiskFileSystem {
482 pub fn name(&self) -> &RcStr {
483 &self.inner.name
484 }
485
486 pub fn root(&self) -> &RcStr {
487 &self.inner.root
488 }
489
490 pub fn invalidate(&self) {
491 self.inner.invalidate();
492 }
493
494 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
495 &self,
496 reason: impl Fn(&Path) -> R + Sync,
497 ) {
498 self.inner.invalidate_with_reason(reason);
499 }
500
501 pub fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
502 &self,
503 paths: impl IntoIterator<Item = PathBuf>,
504 reason: impl Fn(&Path) -> R + Sync,
505 ) {
506 self.inner
507 .invalidate_path_and_children_with_reason(paths, reason);
508 }
509
510 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
511 self.inner
512 .start_watching_internal(false, poll_interval)
513 .await
514 }
515
516 pub async fn start_watching_with_invalidation_reason(
517 &self,
518 poll_interval: Option<Duration>,
519 ) -> Result<()> {
520 self.inner
521 .start_watching_internal(true, poll_interval)
522 .await
523 }
524
525 pub async fn stop_watching(&self) {
526 self.inner.watcher.stop_watching().await;
527 }
528
529 pub fn try_from_sys_path(
542 &self,
543 vc_self: ResolvedVc<DiskFileSystem>,
544 sys_path: &Path,
545 relative_to: Option<&FileSystemPath>,
546 ) -> Option<FileSystemPath> {
547 let vc_self = ResolvedVc::upcast(vc_self);
548
549 let relative_sys_path = if sys_path.is_absolute() {
550 #[cfg(not(windows))]
553 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
554
555 #[cfg(windows)]
558 let normalized_sys_path =
559 crate::windows::to_verbatim_with_case_folded_disk(sys_path).ok()?;
560
561 normalized_sys_path
562 .strip_prefix(self.inner.root_path())
563 .ok()?
564 .to_owned()
565 } else {
566 let root_sys_path = self.inner.root_path();
574 let relative_to_sys_path = if let Some(relative_to) = relative_to {
575 debug_assert_eq!(
576 relative_to.fs, vc_self,
577 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
578 );
579 root_sys_path
580 .join(Path::new(&*unix_to_sys(&relative_to.path)))
581 .join(sys_path)
582 } else {
583 root_sys_path.join(sys_path)
584 };
585 relative_to_sys_path
586 .normalize_lexically()
587 .ok()?
588 .strip_prefix(root_sys_path)
589 .ok()?
590 .to_owned()
591 };
592
593 Some(FileSystemPath {
594 fs: vc_self,
595 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
596 })
597 }
598
599 pub fn to_sys_path_raw(&self, fs_path: &FileSystemPath) -> PathBuf {
611 let sys_root = self.inner.root_path();
612 if fs_path.path.is_empty() {
613 sys_root.to_path_buf()
614 } else {
615 sys_root.join(&*unix_to_sys(&fs_path.path))
616 }
617 }
618
619 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
627 let sys_path = self.to_sys_path_raw(fs_path);
628
629 #[cfg(windows)]
630 return sys_path.to_winuser_path().unwrap_or(sys_path);
631 #[cfg(not(windows))]
632 return sys_path;
633 }
634
635 async fn resolve_link_target_ancestry_slow_path(
650 &self,
651 vc_self: ResolvedVc<Self>,
652 target_sys_path: &Path,
653 ) -> Result<Option<FileSystemPath>> {
654 #[turbo_tasks::value(transparent)]
655 struct OptionRcStr(Option<RcStr>);
656
657 #[turbo_tasks::function(fs, session_dependent)]
660 async fn canonicalize_untracked(sys_path: RcStr) -> Vc<OptionRcStr> {
661 Vc::cell(
662 retry_blocking(|| canonicalize_to_rcstr(Path::new(&*sys_path)))
663 .await
664 .ok(),
665 )
666 }
667
668 let root_sys_path = self.inner.root_path();
669
670 let ancestors: SmallVec<[&Path; 8]> = target_sys_path.ancestors().collect();
675 for prefix in ancestors.into_iter().rev().skip(1) {
676 let Some(prefix_str) = prefix.to_str() else {
677 return Ok(None);
679 };
680 let Some(canonical) = canonicalize_untracked(RcStr::from(prefix_str))
681 .owned()
682 .await?
683 else {
684 return Ok(None);
685 };
686 let canonical = Path::new(canonical.as_str());
687 if canonical.starts_with(root_sys_path) {
688 let rest = target_sys_path
691 .strip_prefix(prefix)
692 .expect("`ancestors` yields prefixes of `target_sys_path`");
693 return Ok(self.try_from_sys_path(vc_self, &canonical.join(rest), None));
694 }
695 }
696
697 Ok(None)
699 }
700}
701
702#[allow(dead_code, reason = "we need to hold onto the locks")]
703struct PathLockGuard<'a>(
704 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
705 #[allow(dead_code)] crate::mutex_map::MutexMapGuard<'a, Arc<PathBuf>>,
706);
707
708pub(crate) fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
709 if let Ok(rel_path) = path.strip_prefix(root_path) {
710 let path = if MAIN_SEPARATOR != '/' {
711 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
712 format!("[{name}]/{rel_path}")
713 } else {
714 format!("[{name}]/{}", rel_path.display())
715 };
716 Some(path)
717 } else {
718 None
719 }
720}
721
722impl DiskFileSystem {
723 pub fn new(name: RcStr, root: Vc<RcStr>) -> Vc<Self> {
730 Self::new_internal(name, root, Vec::new())
731 }
732
733 pub fn new_with_denied_paths(
738 name: RcStr,
739 root: Vc<RcStr>,
740 denied_paths: Vec<RcStr>,
741 ) -> Vc<Self> {
742 for denied_path in &denied_paths {
743 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
744 debug_assert!(
745 normalize_path(denied_path).as_deref() == Some(&**denied_path),
746 "denied_path must be normalized: {denied_path:?}"
747 );
748 }
749 Self::new_internal(name, root, denied_paths)
750 }
751}
752
753#[turbo_tasks::value_impl]
754impl DiskFileSystem {
755 #[turbo_tasks::function]
756 async fn new_internal(
757 name: RcStr,
758 root: Vc<RcStr>,
759 denied_paths: Vec<RcStr>,
760 ) -> Result<Vc<Self>> {
761 let root = root.owned().await?;
762 let instance = DiskFileSystem {
763 inner: Arc::new(DiskFileSystemInner {
764 name,
765 root,
766 mutex_map: Default::default(),
767 invalidation_lock: Default::default(),
768 invalidator_map: InvalidatorMap::new(),
769 dir_invalidator_map: InvalidatorMap::new(),
770 read_semaphore: create_read_semaphore(),
771 write_semaphore: create_write_semaphore(),
772 watcher: DiskWatcher::new(),
773 denied_paths,
774 turbo_tasks: turbo_tasks_weak(),
775 tokio_handle: Handle::current(),
776 effect_state_storage: EffectStateStorage::default(),
777 }),
778 };
779
780 Ok(Self::cell(instance))
781 }
782}
783
784impl Debug for DiskFileSystem {
785 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
786 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
787 }
788}
789
790#[turbo_tasks::value_impl]
791impl FileSystem for DiskFileSystem {
792 #[turbo_tasks::function(fs, session_dependent)]
793 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
794 if self.inner.is_path_denied(&fs_path) {
796 return Ok(FileContent::NotFound.cell());
797 }
798 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
799
800 self.inner.register_read_invalidator(&full_path).await?;
801
802 let _lock = self.inner.lock_path(full_path.clone()).await;
803 let content = match retry_blocking(|| File::from_path(&full_path))
804 .instrument(tracing::info_span!("read file", name = ?full_path))
805 .concurrency_limited(&self.inner.read_semaphore)
806 .await
807 {
808 Ok(file) => FileContent::new(file),
809 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
810 FileContent::NotFound
811 }
812 Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
814 };
815 Ok(content.cell())
816 }
817
818 #[turbo_tasks::function(fs, session_dependent)]
819 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
820 if self.inner.is_path_denied(&fs_path) {
822 return Ok(RawDirectoryContent::not_found());
823 }
824 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
825
826 self.inner.register_dir_invalidator(&full_path).await?;
827
828 let read_dir = match retry_blocking(|| std::fs::read_dir(&*full_path))
830 .instrument(tracing::info_span!("read directory", name = ?full_path))
831 .concurrency_limited(&self.inner.read_semaphore)
832 .await
833 {
834 Ok(dir) => dir,
835 Err(e)
836 if e.kind() == ErrorKind::NotFound
837 || e.kind() == ErrorKind::NotADirectory
838 || e.kind() == ErrorKind::InvalidFilename =>
839 {
840 return Ok(RawDirectoryContent::not_found());
841 }
842 Err(e) => {
843 return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
845 }
846 };
847 let dir_path = fs_path.path.as_str();
848 let denied_entries: FxHashSet<&str> = self
849 .inner
850 .denied_paths
851 .iter()
852 .filter_map(|denied_path| {
853 if denied_path.starts_with(dir_path) {
860 let denied_path_suffix =
861 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
862 Some(&denied_path[dir_path.len() + 1..])
863 } else if dir_path.is_empty() {
864 Some(denied_path.as_str())
865 } else {
866 None
867 };
868 denied_path_suffix.filter(|s| !s.contains('/'))
870 } else {
871 None
872 }
873 })
874 .collect();
875
876 let entries = read_dir
877 .filter_map(|r| {
878 let e = match r {
879 Ok(e) => e,
880 Err(err) => return Some(Err(err.into())),
881 };
882
883 let file_name = RcStr::from(e.file_name().to_str()?);
885 if denied_entries.contains(file_name.as_str()) {
887 return None;
888 }
889
890 let entry = match e.file_type() {
891 Ok(t) if t.is_file() => RawDirectoryEntry::File,
892 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
893 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
894 Ok(_) => RawDirectoryEntry::Other,
895 Err(err) => return Some(Err(err.into())),
896 };
897
898 Some(anyhow::Ok((file_name, entry)))
899 })
900 .collect::<Result<_>>()
901 .with_context(|| format!("reading directory item in {full_path:?}"))?;
902
903 Ok(RawDirectoryContent::new(entries))
904 }
905
906 #[turbo_tasks::function(fs, session_dependent)]
907 async fn read_link(self: ResolvedVc<Self>, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
908 let this = self.await?;
909 let inner = &this.inner;
910 if inner.is_path_denied(&fs_path) {
911 return Ok(LinkContent::NotFound.cell());
912 }
913 let full_link_path = Arc::new(this.to_sys_path_raw(&fs_path));
914
915 inner.register_read_invalidator(&full_link_path).await?;
916
917 let _lock = inner.lock_path(full_link_path.clone()).await;
918 let target_sys_path = match retry_blocking(|| std::fs::read_link(&**full_link_path))
919 .instrument(tracing::info_span!("read symlink", name = ?full_link_path))
920 .concurrency_limited(&inner.read_semaphore)
921 .await
922 {
923 Ok(res) => res,
924 Err(_) => return Ok(LinkContent::NotFound.cell()),
925 };
926
927 let link_parent = fs_path.parent();
931 let mut target_fs_path = this.try_from_sys_path(self, &target_sys_path, Some(&link_parent));
933
934 if target_fs_path.is_none() && target_sys_path.is_absolute() {
939 target_fs_path = this
940 .resolve_link_target_ancestry_slow_path(self, &target_sys_path)
941 .await?;
942 }
943
944 let Some(target_fs_path) = target_fs_path else {
945 return Ok(LinkContent::Invalid.cell());
948 };
949
950 let mut link_type = LinkType::default();
951 let file_type = target_fs_path.get_type().await?;
955 if matches!(&*file_type, FileSystemEntryType::Directory) {
956 link_type |= LinkType::DIRECTORY;
957 }
958
959 let target;
960 if target_sys_path.is_absolute() {
961 target = target_fs_path.path;
963 link_type |= LinkType::ABSOLUTE;
964 } else {
965 let target_str = target_sys_path.to_str().with_context(|| {
967 format!("symlink target {target_sys_path:?} is not valid unicode")
968 })?;
969 target = RcStr::from(sys_to_unix(target_str));
970 };
971
972 Ok(LinkContent::Link { target, link_type }.cell())
973 }
974
975 #[turbo_tasks::function(fs)]
976 async fn write(
977 self: ResolvedVc<Self>,
978 fs_path: FileSystemPath,
979 content: ResolvedVc<FileContent>,
980 ) -> Result<()> {
981 let this = self.await?;
982 if this.inner.is_path_denied(&fs_path) {
988 turbobail!("Cannot write to denied path: {fs_path}");
989 }
990 let full_path = this.to_sys_path_raw(&fs_path);
991
992 validate_path_length(&full_path)?;
995
996 let content = content.persist().to_resolved().await?;
1002 let content_hash = hash_xxh3_hash128(&*content.await?);
1003
1004 #[turbo_tasks::value(eq = "manual", cell = "new")]
1005 struct WriteEffect {
1006 full_path: Arc<PathBuf>,
1007 fs: ResolvedVc<DiskFileSystem>,
1008 content: ResolvedVc<PersistedFileContent>,
1009 content_hash: u128,
1010 }
1011
1012 #[async_trait]
1013 #[turbo_tasks::value_impl]
1014 impl Effect for WriteEffect {
1015 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
1016 let inner = (*self.fs).untracked().await?.inner.clone();
1019
1020 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
1027 let content = if inner
1028 .effect_state_storage
1029 .matches_applied(&key_bytes, self.content_hash)
1030 {
1031 None
1032 } else {
1033 Some((*self.content).untracked().await?)
1037 };
1038 Ok(Box::new(CapturedWriteEffect {
1039 full_path: self.full_path.clone(),
1040 inner,
1041 content,
1042 content_hash: self.content_hash,
1043 }) as Box<dyn CapturedEffect>)
1044 }
1045 }
1046
1047 #[derive(TraceRawVcs, NonLocalValue, Clone)]
1048 struct CapturedWriteEffect {
1049 full_path: Arc<PathBuf>,
1050 inner: Arc<DiskFileSystemInner>,
1051 content: Option<ReadRef<PersistedFileContent>>,
1052 content_hash: u128,
1053 }
1054
1055 #[async_trait]
1056 impl CapturedEffect for CapturedWriteEffect {
1057 fn key(&self) -> Box<[u8]> {
1058 self.full_path.as_os_str().as_encoded_bytes().into()
1059 }
1060
1061 fn value_hash(&self) -> u128 {
1062 self.content_hash
1063 }
1064
1065 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1066 let body = self.content.as_ref().map(|content| {
1067 || async { self.apply_inner(content).await.map_err(AnyhowWrapper::from) }
1068 });
1069 self.inner
1070 .effect_state_storage
1071 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1072 .await
1073 }
1074 }
1075
1076 impl CapturedWriteEffect {
1077 async fn apply_inner(
1078 &self,
1079 content: &ReadRef<PersistedFileContent>,
1080 ) -> anyhow::Result<()> {
1081 let full_path = &self.full_path;
1082
1083 let _lock = self.inner.lock_path(full_path.clone()).await;
1084
1085 let compare = content
1091 .streaming_compare(full_path)
1092 .instrument(tracing::info_span!(
1093 "read file before write",
1094 name = ?full_path,
1095 ))
1096 .concurrency_limited(&self.inner.read_semaphore)
1097 .await?;
1098 if compare == FileComparison::Equal {
1099 return Ok(());
1100 }
1101
1102 match &**content {
1103 PersistedFileContent::Content(..) => {
1104 let content = content.clone();
1105
1106 let mut missing_parent_dir = false;
1107 let do_write = || {
1108 if missing_parent_dir && let Some(parent) = full_path.parent() {
1109 std::fs::create_dir_all(parent)?;
1110 missing_parent_dir = false;
1111 }
1112 let mut f = std::fs::File::create(&**full_path).inspect_err(|err| {
1113 if err.kind() == ErrorKind::NotFound {
1114 missing_parent_dir = true;
1116 }
1117 })?;
1118 let PersistedFileContent::Content(file) = &*content else {
1119 unreachable!()
1120 };
1121 std::io::copy(&mut file.read(), &mut f)?;
1122 #[cfg(unix)]
1123 f.set_permissions(file.meta.permissions.into())?;
1124 f.flush()?;
1125
1126 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
1127 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
1128 .is_some_and(|v| v == "1" || v == "true")
1129 });
1130 if *WRITE_VERSION {
1131 let mut full_path = (**full_path).clone();
1132 let hash = hash_xxh3_hash64(file);
1133 let orig_ext = full_path.extension();
1134 let mut ext = OsString::from(format!("{hash:016x}"));
1135 if let Some(orig_ext) = orig_ext {
1136 ext.push(".");
1137 ext.push(orig_ext);
1138 }
1139 full_path.set_extension(ext);
1140 validate_path_length(&full_path)?;
1141 let mut f = std::fs::File::create(&*full_path)?;
1142 std::io::copy(&mut file.read(), &mut f)?;
1143 #[cfg(unix)]
1144 f.set_permissions(file.meta.permissions.into())?;
1145 f.flush()?;
1146 }
1147 Ok::<(), io::Error>(())
1148 };
1149 fn can_retry_write(err: &io::Error) -> bool {
1150 err.kind() == ErrorKind::NotFound || can_retry(err)
1151 }
1152 retry_blocking_custom(do_write, can_retry_write)
1153 .instrument(tracing::info_span!("write file", name = ?full_path))
1154 .concurrency_limited(&self.inner.write_semaphore)
1155 .await
1156 .with_context(|| format!("failed to write to {full_path:?}"))?;
1157 }
1158 PersistedFileContent::NotFound => {
1159 retry_blocking(|| std::fs::remove_file(&**full_path))
1160 .instrument(tracing::info_span!("remove file", name = ?full_path))
1161 .concurrency_limited(&self.inner.write_semaphore)
1162 .await
1163 .or_else(|err| {
1164 if err.kind() == ErrorKind::NotFound {
1165 Ok(())
1166 } else {
1167 Err(err)
1168 }
1169 })
1170 .with_context(|| format!("removing {full_path:?} failed"))?;
1171 }
1172 }
1173
1174 self.inner.invalidate_from_write(&self.full_path);
1176
1177 Ok(())
1178 }
1179 }
1180
1181 WriteEffect {
1182 full_path: Arc::new(full_path),
1183 fs: self,
1184 content,
1185 content_hash,
1186 }
1187 .resolved_cell()
1188 .emit();
1189
1190 Ok(())
1191 }
1192
1193 #[turbo_tasks::function(fs)]
1194 async fn write_link(
1195 self: ResolvedVc<Self>,
1196 fs_path: FileSystemPath,
1197 target: ResolvedVc<LinkContent>,
1198 ) -> Result<()> {
1199 let this = self.await?;
1204 if this.inner.is_path_denied(&fs_path) {
1206 turbobail!("Cannot write link to denied path: {fs_path}");
1207 }
1208 let full_path = this.to_sys_path_raw(&fs_path);
1209
1210 validate_path_length(&full_path)?;
1211
1212 let content_hash = hash_xxh3_hash128(&*target.await?);
1213
1214 #[turbo_tasks::value(eq = "manual", cell = "new")]
1215 struct WriteLinkEffect {
1216 full_path: Arc<PathBuf>,
1217 fs: ResolvedVc<DiskFileSystem>,
1218 target: ResolvedVc<LinkContent>,
1219 content_hash: u128,
1220 }
1221
1222 #[async_trait]
1223 #[turbo_tasks::value_impl]
1224 impl Effect for WriteLinkEffect {
1225 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
1226 let inner = (*self.fs).untracked().await?.inner.clone();
1227
1228 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
1231 let content = if inner
1232 .effect_state_storage
1233 .matches_applied(&key_bytes, self.content_hash)
1234 {
1235 None
1236 } else {
1237 Some((*self.target).untracked().await?)
1239 };
1240 Ok(Box::new(CapturedWriteLinkEffect {
1241 full_path: self.full_path.clone(),
1242 inner,
1243 content,
1244 content_hash: self.content_hash,
1245 }) as Box<dyn CapturedEffect>)
1246 }
1247 }
1248
1249 #[derive(TraceRawVcs, NonLocalValue, Clone)]
1251 struct CapturedWriteLinkEffect {
1252 full_path: Arc<PathBuf>,
1253 inner: Arc<DiskFileSystemInner>,
1254 content: Option<ReadRef<LinkContent>>,
1255 content_hash: u128,
1256 }
1257
1258 #[async_trait]
1259 impl CapturedEffect for CapturedWriteLinkEffect {
1260 fn key(&self) -> Box<[u8]> {
1261 self.full_path.as_os_str().as_encoded_bytes().into()
1262 }
1263
1264 fn value_hash(&self) -> u128 {
1265 self.content_hash
1266 }
1267
1268 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1269 let body = self.content.as_ref().map(|content| {
1270 || async { self.apply_inner(content).await.map_err(AnyhowWrapper::from) }
1271 });
1272 self.inner
1273 .effect_state_storage
1274 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1275 .await
1276 }
1277 }
1278
1279 impl CapturedWriteLinkEffect {
1280 async fn apply_inner(&self, content: &ReadRef<LinkContent>) -> anyhow::Result<()> {
1281 let full_path = self.full_path.clone();
1282
1283 let _lock = self.inner.lock_path(full_path.clone()).await;
1284
1285 enum OsSpecificLinkContent {
1286 Link {
1287 #[cfg(windows)]
1288 is_directory: bool,
1289 target: PathBuf,
1290 },
1291 NotFound,
1292 Invalid,
1293 }
1294
1295 let os_specific_link_content = match &**content {
1296 LinkContent::Link { target, link_type } => {
1297 let is_directory = link_type.contains(LinkType::DIRECTORY);
1298 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1299 self.inner.root_path().join(unix_to_sys(target).as_ref())
1300 } else {
1301 let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1302 if cfg!(windows) && is_directory {
1303 full_path
1305 .parent()
1306 .unwrap_or(&full_path)
1307 .join(relative_target)
1308 } else {
1309 relative_target
1310 }
1311 };
1312 OsSpecificLinkContent::Link {
1313 #[cfg(windows)]
1314 is_directory,
1315 target: target_path,
1316 }
1317 }
1318 LinkContent::Invalid => OsSpecificLinkContent::Invalid,
1319 LinkContent::NotFound => OsSpecificLinkContent::NotFound,
1320 };
1321
1322 let old_content = match retry_blocking(|| std::fs::read_link(&**full_path))
1323 .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1324 .concurrency_limited(&self.inner.read_semaphore)
1325 .await
1326 {
1327 Ok(res) => Some((res.is_absolute(), res)),
1328 Err(_) => None,
1329 };
1330 let is_equal = match (&os_specific_link_content, &old_content) {
1331 (
1332 OsSpecificLinkContent::Link { target, .. },
1333 Some((old_is_absolute, old_target)),
1334 ) => target == old_target && target.is_absolute() == *old_is_absolute,
1335 (OsSpecificLinkContent::NotFound, None) => true,
1336 _ => false,
1337 };
1338 if is_equal {
1339 return Ok(());
1340 }
1341
1342 match os_specific_link_content {
1343 OsSpecificLinkContent::Link {
1344 target,
1345 #[cfg(windows)]
1346 is_directory,
1347 ..
1348 } => {
1349 #[derive(thiserror::Error, Debug)]
1350 #[error("{msg}: {source}")]
1351 struct SymlinkCreationError {
1352 msg: &'static str,
1353 #[source]
1354 source: io::Error,
1355 }
1356
1357 let mut missing_parent_dir = false;
1358 let mut has_old_content = old_content.is_some();
1359 let try_create_link = || {
1360 if missing_parent_dir && let Some(parent) = full_path.parent() {
1361 std::fs::create_dir_all(parent).map_err(|err| {
1362 SymlinkCreationError {
1363 msg: "failed to create directory",
1364 source: err,
1365 }
1366 })?;
1367 missing_parent_dir = false;
1368 }
1369 if has_old_content {
1370 remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1375 SymlinkCreationError {
1376 msg: "removal of existing symbolic link or junction point \
1377 failed",
1378 source: err,
1379 }
1380 })?;
1381 has_old_content = false;
1382 }
1383 #[cfg(not(windows))]
1384 let io_result = std::os::unix::fs::symlink(&target, &**full_path);
1385 #[cfg(windows)]
1386 let io_result = if is_directory {
1387 std::os::windows::fs::junction_point(&target, &**full_path)
1388 } else {
1389 std::os::windows::fs::symlink_file(&target, &**full_path)
1390 };
1391 io_result.map_err(|err| {
1392 match err.kind() {
1393 ErrorKind::NotFound => {
1394 missing_parent_dir = true;
1396 }
1397 ErrorKind::AlreadyExists => {
1398 has_old_content = true;
1400 }
1401 _ => {}
1402 }
1403 SymlinkCreationError {
1404 msg: "creation of a new symbolic link or junction point failed",
1405 source: err,
1406 }
1407 })
1408 };
1409 fn can_retry_link(err: &SymlinkCreationError) -> bool {
1410 matches!(
1411 err.source.kind(),
1412 ErrorKind::NotFound | ErrorKind::AlreadyExists
1413 ) || can_retry(&err.source)
1414 }
1415 let err_context = || {
1416 #[cfg(not(windows))]
1417 let message = format!(
1418 "failed to create symlink at {full_path:?} pointing to {target:?}"
1419 );
1420 #[cfg(windows)]
1421 let message = if is_directory {
1422 format!(
1423 "failed to create junction point at {full_path:?} pointing to \
1424 {target:?}"
1425 )
1426 } else {
1427 format!(
1428 "failed to create symlink at {full_path:?} pointing to \
1429 {target:?}\n\
1430 (Note: creating file symlinks on Windows require developer \
1431 mode or admin permissions: \
1432 https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1433 )
1434 };
1435 message
1436 };
1437 retry_blocking_custom(try_create_link, can_retry_link)
1438 .instrument(tracing::info_span!(
1439 "write symlink",
1440 name = ?full_path,
1441 target = ?target,
1442 ))
1443 .concurrency_limited(&self.inner.write_semaphore)
1444 .await
1445 .with_context(err_context)?;
1446 }
1447 OsSpecificLinkContent::Invalid => {
1448 bail!("invalid symlink target: {full_path:?}");
1449 }
1450 OsSpecificLinkContent::NotFound => {
1451 retry_blocking(|| remove_symbolic_link_dir_helper(&full_path))
1452 .instrument(tracing::info_span!("remove symlink", name = ?full_path))
1453 .concurrency_limited(&self.inner.write_semaphore)
1454 .await
1455 .with_context(|| format!("removing {full_path:?} failed"))?;
1456 }
1457 }
1458
1459 self.inner.invalidate_from_write(&self.full_path);
1461
1462 Ok(())
1463 }
1464 }
1465
1466 WriteLinkEffect {
1467 full_path: Arc::new(full_path),
1468 fs: self,
1469 target,
1470 content_hash,
1471 }
1472 .resolved_cell()
1473 .emit();
1474 Ok(())
1475 }
1476
1477 #[turbo_tasks::function(fs, session_dependent)]
1478 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1479 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
1480
1481 if self.inner.is_path_denied(&fs_path) {
1483 turbobail!("Cannot read metadata from denied path: {fs_path}");
1484 }
1485
1486 self.inner.register_read_invalidator(&full_path).await?;
1487
1488 let _lock = self.inner.lock_path(full_path.clone()).await;
1489 let meta = retry_blocking(|| std::fs::metadata(&**full_path))
1490 .instrument(tracing::info_span!("read metadata", name = ?full_path))
1491 .concurrency_limited(&self.inner.read_semaphore)
1492 .await
1493 .with_context(|| format!("reading metadata for {:?}", full_path))?;
1494
1495 Ok(FileMeta::cell(meta.into()))
1496 }
1497}
1498
1499fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1500 let result = if cfg!(windows) {
1501 std::fs::remove_dir(path).or_else(|err| {
1514 if err.kind() == ErrorKind::NotADirectory {
1515 std::fs::remove_file(path)
1516 } else {
1517 Err(err)
1518 }
1519 })
1520 } else {
1521 std::fs::remove_file(path)
1522 };
1523 match result {
1524 Ok(()) => Ok(()),
1525 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1526 Err(err) => Err(err),
1527 }
1528}
1529
1530#[cfg(test)]
1531mod tests {
1532 use turbo_rcstr::rcstr;
1533 use turbo_tasks::{Effects, OperationVc, Vc, take_effects};
1534 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1535
1536 use super::*;
1537
1538 #[turbo_tasks::function(operation, root)]
1539 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
1540 let _ = op.resolve().strongly_consistent().await?;
1541 Ok(take_effects(op).await?.cell())
1542 }
1543
1544 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1545 async fn test_try_from_sys_path() {
1546 let sys_root = if cfg!(windows) {
1547 Path::new(r"C:\fake\root")
1548 } else {
1549 Path::new(r"/fake/root")
1550 };
1551
1552 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1553 BackendOptions::default(),
1554 noop_backing_storage(),
1555 ));
1556 tt.run_once(async {
1557 assert_try_from_sys_path_operation(RcStr::from(sys_root.to_str().unwrap()))
1558 .read_strongly_consistent()
1559 .await?;
1560
1561 anyhow::Ok(())
1562 })
1563 .await
1564 .unwrap();
1565 }
1566
1567 #[turbo_tasks::function(operation, root)]
1568 async fn assert_try_from_sys_path_operation(sys_root: RcStr) -> anyhow::Result<()> {
1569 let sys_root = Path::new(sys_root.as_str());
1570 let fs_vc = DiskFileSystem::new(
1571 rcstr!("temp"),
1572 Vc::cell(RcStr::from(sys_root.to_str().unwrap())),
1573 )
1574 .to_resolved()
1575 .await?;
1576 let fs = fs_vc.await?;
1577 let fs_root_path = fs_vc.root().await?;
1578
1579 assert_eq!(
1580 fs.try_from_sys_path(
1581 fs_vc,
1582 &Path::new("relative").join("directory"),
1583 None,
1584 )
1585 .unwrap()
1586 .path,
1587 "relative/directory"
1588 );
1589
1590 assert_eq!(
1591 fs.try_from_sys_path(
1592 fs_vc,
1593 &sys_root
1594 .join("absolute")
1595 .join("directory")
1596 .join("..")
1597 .join("normalized_path"),
1598 Some(&fs_root_path.join("ignored").unwrap()),
1599 )
1600 .unwrap()
1601 .path,
1602 "absolute/normalized_path"
1603 );
1604
1605 assert_eq!(
1606 fs.try_from_sys_path(
1607 fs_vc,
1608 Path::new("child"),
1609 Some(&fs_root_path.join("parent").unwrap()),
1610 )
1611 .unwrap()
1612 .path,
1613 "parent/child"
1614 );
1615
1616 assert_eq!(
1617 fs.try_from_sys_path(
1618 fs_vc,
1619 &Path::new("..").join("parallel_dir"),
1620 Some(&fs_root_path.join("parent").unwrap()),
1621 )
1622 .unwrap()
1623 .path,
1624 "parallel_dir"
1625 );
1626
1627 assert_eq!(
1628 fs.try_from_sys_path(
1629 fs_vc,
1630 &Path::new("relative")
1631 .join("..")
1632 .join("..")
1633 .join("leaves_root"),
1634 None,
1635 ),
1636 None
1637 );
1638
1639 assert_eq!(
1640 fs.try_from_sys_path(
1641 fs_vc,
1642 &sys_root
1643 .join("absolute")
1644 .join("..")
1645 .join("..")
1646 .join("leaves_root"),
1647 None,
1648 ),
1649 None
1650 );
1651
1652 Ok(())
1653 }
1654
1655 #[cfg(test)]
1656 mod symlink_tests {
1657 use std::{
1658 fs::{File, create_dir_all, read_to_string},
1659 io::Write,
1660 };
1661
1662 use rand::{RngExt, SeedableRng};
1663 use turbo_rcstr::{RcStr, rcstr};
1664 use turbo_tasks::{ResolvedVc, Vc, read_strongly_consistent_and_apply_effects};
1665 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1666
1667 use super::extract_effects_operation;
1668 use crate::{
1669 DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType,
1670 canonicalize_to_rcstr,
1671 };
1672
1673 #[turbo_tasks::function(operation, root)]
1674 async fn test_write_link_effect_operation(
1675 fs: ResolvedVc<DiskFileSystem>,
1676 path: FileSystemPath,
1677 target: RcStr,
1678 ) -> anyhow::Result<()> {
1679 let write_file = |f| {
1680 fs.write_link(
1681 f,
1682 LinkContent::Link {
1683 target: format!("{target}/data.txt").into(),
1684 link_type: LinkType::empty(),
1685 }
1686 .cell(),
1687 )
1688 };
1689 write_file(path.join("symlink-file")?).await?;
1691 write_file(path.join("symlink-file")?).await?;
1692
1693 let write_dir = |f| {
1694 fs.write_link(
1695 f,
1696 LinkContent::Link {
1697 target: target.clone(),
1698 link_type: LinkType::DIRECTORY,
1699 }
1700 .cell(),
1701 )
1702 };
1703 write_dir(path.join("symlink-dir")?).await?;
1705 write_dir(path.join("symlink-dir")?).await?;
1706
1707 Ok(())
1708 }
1709
1710 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1711 async fn test_write_link() {
1712 let scratch = tempfile::tempdir().unwrap();
1713 let path = scratch.path().to_owned();
1714
1715 create_dir_all(path.join("subdir-a")).unwrap();
1716 File::create_new(path.join("subdir-a/data.txt"))
1717 .unwrap()
1718 .write_all(b"foo")
1719 .unwrap();
1720 create_dir_all(path.join("subdir-b")).unwrap();
1721 File::create_new(path.join("subdir-b/data.txt"))
1722 .unwrap()
1723 .write_all(b"bar")
1724 .unwrap();
1725 let root = path.to_str().unwrap().into();
1726
1727 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1728 BackendOptions::default(),
1729 noop_backing_storage(),
1730 ));
1731
1732 tt.run_once(async move {
1733 let fs = disk_file_system_operation(root)
1734 .resolve()
1735 .strongly_consistent()
1736 .await?;
1737 let root_path = disk_file_system_root(fs);
1738
1739 read_strongly_consistent_and_apply_effects(
1740 extract_effects_operation(test_write_link_effect_operation(
1741 fs,
1742 root_path.clone(),
1743 rcstr!("subdir-a"),
1744 )),
1745 |e| e,
1746 )
1747 .await?;
1748
1749 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
1750 assert_eq!(
1751 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
1752 "foo"
1753 );
1754
1755 read_strongly_consistent_and_apply_effects(
1757 extract_effects_operation(test_write_link_effect_operation(
1758 fs,
1759 root_path,
1760 rcstr!("subdir-b"),
1761 )),
1762 |e| e,
1763 )
1764 .await?;
1765
1766 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
1767 assert_eq!(
1768 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
1769 "bar"
1770 );
1771
1772 anyhow::Ok(())
1773 })
1774 .await
1775 .unwrap();
1776 }
1777
1778 #[turbo_tasks::function(operation, root)]
1782 async fn assert_read_relative_symlink_operation(
1783 fs: ResolvedVc<DiskFileSystem>,
1784 root_path: FileSystemPath,
1785 ) -> anyhow::Result<()> {
1786 let sibling = fs.read_link(root_path.join("sub/link-sibling")?).await?;
1788 assert_eq!(
1789 *sibling,
1790 LinkContent::Link {
1791 target: rcstr!("foo.txt"),
1792 link_type: LinkType::empty(),
1793 }
1794 );
1795
1796 let parent = fs.read_link(root_path.join("sub/link-parent")?).await?;
1798 assert_eq!(
1799 *parent,
1800 LinkContent::Link {
1801 target: rcstr!("../root.txt"),
1802 link_type: LinkType::empty(),
1803 }
1804 );
1805
1806 Ok(())
1807 }
1808
1809 #[cfg(unix)]
1810 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1811 async fn test_read_relative_symlink() {
1812 use std::os::unix::fs::symlink;
1813
1814 let scratch = tempfile::tempdir().unwrap();
1815 let path = scratch.path().to_owned();
1816
1817 create_dir_all(path.join("sub")).unwrap();
1822 File::create_new(path.join("root.txt"))
1823 .unwrap()
1824 .write_all(b"root")
1825 .unwrap();
1826 File::create_new(path.join("sub/foo.txt"))
1827 .unwrap()
1828 .write_all(b"foo")
1829 .unwrap();
1830 symlink("foo.txt", path.join("sub/link-sibling")).unwrap();
1831 symlink("../root.txt", path.join("sub/link-parent")).unwrap();
1832
1833 let root: RcStr = path.to_str().unwrap().into();
1834
1835 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1836 BackendOptions::default(),
1837 noop_backing_storage(),
1838 ));
1839
1840 tt.run_once(async move {
1841 let fs = disk_file_system_operation(root)
1842 .resolve()
1843 .strongly_consistent()
1844 .await?;
1845 let root_path = disk_file_system_root(fs);
1846
1847 assert_read_relative_symlink_operation(fs, root_path)
1848 .read_strongly_consistent()
1849 .await?;
1850
1851 anyhow::Ok(())
1852 })
1853 .await
1854 .unwrap();
1855 }
1856
1857 #[cfg(unix)]
1858 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1859 async fn test_read_absolute_symlink_slow_path() {
1860 use std::os::unix::fs::symlink;
1861
1862 let scratch = tempfile::tempdir().unwrap();
1863 let path = scratch.path().to_owned();
1864
1865 let real_root = path.join("real-root");
1871 create_dir_all(&real_root).unwrap();
1872 File::create_new(real_root.join("foo.txt"))
1873 .unwrap()
1874 .write_all(b"foo")
1875 .unwrap();
1876 File::create_new(path.join("outside.txt"))
1877 .unwrap()
1878 .write_all(b"outside")
1879 .unwrap();
1880 symlink(&real_root, path.join("alias")).unwrap();
1881 symlink(path.join("alias/foo.txt"), real_root.join("link-via-alias")).unwrap();
1882 symlink(path.join("outside.txt"), real_root.join("link-outside")).unwrap();
1883
1884 let root = canonicalize_to_rcstr(&real_root).unwrap();
1888
1889 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1890 BackendOptions::default(),
1891 noop_backing_storage(),
1892 ));
1893
1894 #[turbo_tasks::function(operation, root)]
1899 async fn assert_read_absolute_symlink_slow_path_operation(
1900 fs: ResolvedVc<DiskFileSystem>,
1901 root_path: FileSystemPath,
1902 ) -> anyhow::Result<()> {
1903 let via_alias = fs.read_link(root_path.join("link-via-alias")?).await?;
1905 assert_eq!(
1906 *via_alias,
1907 LinkContent::Link {
1908 target: rcstr!("foo.txt"),
1909 link_type: LinkType::ABSOLUTE,
1910 }
1911 );
1912
1913 let outside = fs.read_link(root_path.join("link-outside")?).await?;
1915 assert_eq!(*outside, LinkContent::Invalid);
1916
1917 Ok(())
1918 }
1919
1920 tt.run_once(async move {
1921 let fs = disk_file_system_operation(root)
1922 .resolve()
1923 .strongly_consistent()
1924 .await?;
1925 let root_path = disk_file_system_root(fs);
1926
1927 assert_read_absolute_symlink_slow_path_operation(fs, root_path)
1928 .read_strongly_consistent()
1929 .await?;
1930
1931 anyhow::Ok(())
1932 })
1933 .await
1934 .unwrap();
1935 }
1936
1937 const STRESS_ITERATIONS: usize = 100;
1938 const STRESS_PARALLELISM: usize = 8;
1939 const STRESS_TARGET_COUNT: usize = 20;
1940 const STRESS_SYMLINK_COUNT: usize = 16;
1941
1942 #[turbo_tasks::function(operation, root)]
1943 fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
1944 DiskFileSystem::new(rcstr!("test"), Vc::cell(fs_root))
1945 }
1946
1947 fn disk_file_system_root(fs: ResolvedVc<DiskFileSystem>) -> FileSystemPath {
1948 FileSystemPath {
1949 fs: ResolvedVc::upcast(fs),
1950 path: rcstr!(""),
1951 }
1952 }
1953
1954 #[turbo_tasks::function(operation, root)]
1955 async fn write_symlink_stress_batch(
1956 fs: ResolvedVc<DiskFileSystem>,
1957 symlinks_dir: FileSystemPath,
1958 updates: Vec<(usize, usize)>,
1959 ) -> anyhow::Result<()> {
1960 use turbo_tasks::TryJoinIterExt;
1961
1962 updates
1963 .into_iter()
1964 .map(|(symlink_idx, target_idx)| {
1965 let target = RcStr::from(format!("../_targets/{target_idx}"));
1966 let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
1967 async move {
1968 fs.write_link(
1969 symlink_path,
1970 LinkContent::Link {
1971 target,
1972 link_type: LinkType::DIRECTORY,
1973 }
1974 .cell(),
1975 )
1976 .await
1977 }
1978 })
1979 .try_join()
1980 .await?;
1981 Ok(())
1982 }
1983
1984 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1985 async fn test_symlink_stress() {
1986 let scratch = tempfile::tempdir().unwrap();
1987 let path = scratch.path().to_owned();
1988
1989 let targets_dir = path.join("_targets");
1990 create_dir_all(&targets_dir).unwrap();
1991 for i in 0..STRESS_TARGET_COUNT {
1992 create_dir_all(targets_dir.join(i.to_string())).unwrap();
1993 }
1994 create_dir_all(path.join("_symlinks")).unwrap();
1995
1996 let root = RcStr::from(path.to_str().unwrap());
1997
1998 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1999 BackendOptions::default(),
2000 noop_backing_storage(),
2001 ));
2002
2003 tt.run_once(async move {
2004 let fs = disk_file_system_operation(root)
2005 .resolve()
2006 .strongly_consistent()
2007 .await?;
2008 let root_path = disk_file_system_root(fs);
2009 let symlinks_dir = root_path.join("_symlinks")?;
2010
2011 let initial_updates: Vec<(usize, usize)> =
2012 (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
2013 read_strongly_consistent_and_apply_effects(
2014 extract_effects_operation(write_symlink_stress_batch(
2015 fs,
2016 symlinks_dir.clone(),
2017 initial_updates,
2018 )),
2019 |e| e,
2020 )
2021 .await?;
2022
2023 let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
2024 for _ in 0..STRESS_ITERATIONS {
2025 let mut updates_map = rustc_hash::FxHashMap::default();
2026 for _ in 0..STRESS_PARALLELISM {
2027 let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
2028 let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
2029 updates_map.insert(symlink_idx, target_idx);
2030 }
2031 let updates: Vec<(usize, usize)> = updates_map.into_iter().collect();
2032
2033 read_strongly_consistent_and_apply_effects(
2034 extract_effects_operation(write_symlink_stress_batch(
2035 fs,
2036 symlinks_dir.clone(),
2037 updates,
2038 )),
2039 |e| e,
2040 )
2041 .await?;
2042 }
2043
2044 anyhow::Ok(())
2045 })
2046 .await
2047 .unwrap();
2048
2049 tt.stop_and_wait().await;
2050 }
2051 }
2052
2053 #[cfg(test)]
2055 mod denied_path_tests {
2056 use std::{
2057 fs::{File, create_dir_all, read_to_string},
2058 io::Write,
2059 path::Path,
2060 };
2061
2062 use turbo_rcstr::{RcStr, rcstr};
2063 use turbo_tasks::{Effects, Vc, read_strongly_consistent_and_apply_effects, take_effects};
2064 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2065
2066 use crate::{
2067 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
2068 FileSystemPath,
2069 glob::{Glob, GlobOptions},
2070 };
2071
2072 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
2075 let scratch = tempfile::tempdir().unwrap();
2076 let path = scratch.path();
2077
2078 File::create_new(path.join("allowed_file.txt"))
2085 .unwrap()
2086 .write_all(b"allowed content")
2087 .unwrap();
2088
2089 create_dir_all(path.join("allowed_dir")).unwrap();
2090 File::create_new(path.join("allowed_dir/file.txt"))
2091 .unwrap()
2092 .write_all(b"allowed dir content")
2093 .unwrap();
2094
2095 File::create_new(path.join("other_file.txt"))
2096 .unwrap()
2097 .write_all(b"other content")
2098 .unwrap();
2099
2100 create_dir_all(path.join("denied_dir/nested")).unwrap();
2101 File::create_new(path.join("denied_dir/secret.txt"))
2102 .unwrap()
2103 .write_all(b"secret content")
2104 .unwrap();
2105 File::create_new(path.join("denied_dir/nested/deep.txt"))
2106 .unwrap()
2107 .write_all(b"deep secret")
2108 .unwrap();
2109
2110 let root = RcStr::from(path.to_str().unwrap());
2111 let denied_path = rcstr!("denied_dir");
2113
2114 (scratch, root, denied_path)
2115 }
2116
2117 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2118 async fn test_denied_path_read() {
2119 #[turbo_tasks::function(operation, root)]
2120 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2121 let fs = DiskFileSystem::new_with_denied_paths(
2122 rcstr!("test"),
2123 Vc::cell(root),
2124 vec![denied_path],
2125 );
2126 let root_path = fs.root().await?;
2127
2128 let allowed_file = root_path.join("allowed_file.txt")?;
2130 let content = allowed_file.read().await?;
2131 assert!(
2132 matches!(&*content, FileContent::Content(_)),
2133 "allowed file should be readable"
2134 );
2135
2136 let denied_file = root_path.join("denied_dir/secret.txt")?;
2138 let content = denied_file.read().await?;
2139 assert!(
2140 matches!(&*content, FileContent::NotFound),
2141 "denied file should return NotFound, got {:?}",
2142 content
2143 );
2144
2145 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
2147 let content = nested_denied.read().await?;
2148 assert!(
2149 matches!(&*content, FileContent::NotFound),
2150 "nested denied file should return NotFound"
2151 );
2152
2153 let denied_dir = root_path.join("denied_dir")?;
2155 let content = denied_dir.read().await?;
2156 assert!(
2157 matches!(&*content, FileContent::NotFound),
2158 "denied directory should return NotFound"
2159 );
2160
2161 Ok(())
2162 }
2163
2164 let (_scratch, root, denied_path) = setup_test_fs();
2165 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2166 BackendOptions::default(),
2167 noop_backing_storage(),
2168 ));
2169 tt.run_once(async {
2170 test_operation(root, denied_path)
2171 .read_strongly_consistent()
2172 .await?;
2173
2174 anyhow::Ok(())
2175 })
2176 .await
2177 .unwrap();
2178 }
2179
2180 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2181 async fn test_denied_path_read_dir() {
2182 #[turbo_tasks::function(operation, root)]
2183 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2184 let fs = DiskFileSystem::new_with_denied_paths(
2185 rcstr!("test"),
2186 Vc::cell(root),
2187 vec![denied_path],
2188 );
2189 let root_path = fs.root().await?;
2190
2191 let dir_content = root_path.read_dir().await?;
2193 match &*dir_content {
2194 DirectoryContent::Entries(entries) => {
2195 assert!(
2196 entries.contains_key(&rcstr!("allowed_dir")),
2197 "allowed_dir should be visible"
2198 );
2199 assert!(
2200 entries.contains_key(&rcstr!("other_file.txt")),
2201 "other_file.txt should be visible"
2202 );
2203 assert!(
2204 entries.contains_key(&rcstr!("allowed_file.txt")),
2205 "allowed_file.txt should be visible"
2206 );
2207 assert!(
2208 !entries.contains_key(&rcstr!("denied_dir")),
2209 "denied_dir should NOT be visible in read_dir"
2210 );
2211 }
2212 DirectoryContent::NotFound => panic!("root directory should exist"),
2213 }
2214
2215 let denied_dir = root_path.join("denied_dir")?;
2217 let dir_content = denied_dir.read_dir().await?;
2218 assert!(
2219 matches!(&*dir_content, DirectoryContent::NotFound),
2220 "denied_dir read_dir should return NotFound"
2221 );
2222
2223 Ok(())
2224 }
2225
2226 let (_scratch, root, denied_path) = setup_test_fs();
2227 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2228 BackendOptions::default(),
2229 noop_backing_storage(),
2230 ));
2231 tt.run_once(async {
2232 test_operation(root, denied_path)
2233 .read_strongly_consistent()
2234 .await?;
2235
2236 anyhow::Ok(())
2237 })
2238 .await
2239 .unwrap();
2240 }
2241
2242 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2243 async fn test_denied_path_read_glob() {
2244 #[turbo_tasks::function(operation, root)]
2245 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2246 let fs = DiskFileSystem::new_with_denied_paths(
2247 rcstr!("test"),
2248 Vc::cell(root),
2249 vec![denied_path],
2250 );
2251 let root_path = fs.root().await?;
2252
2253 let glob_result = root_path
2255 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
2256 .await?;
2257
2258 assert!(
2260 glob_result.results.contains_key("allowed_file.txt"),
2261 "allowed_file.txt should be found"
2262 );
2263 assert!(
2264 glob_result.results.contains_key("other_file.txt"),
2265 "other_file.txt should be found"
2266 );
2267 assert!(
2268 !glob_result.results.contains_key("denied_dir"),
2269 "denied_dir should NOT appear in glob results"
2270 );
2271
2272 assert!(
2274 !glob_result.inner.contains_key("denied_dir"),
2275 "denied_dir should NOT appear in glob inner results"
2276 );
2277
2278 assert!(
2280 glob_result.inner.contains_key("allowed_dir"),
2281 "allowed_dir directory should be present"
2282 );
2283 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
2284 assert!(
2285 sub_inner.results.contains_key("file.txt"),
2286 "allowed_dir/file.txt should be found"
2287 );
2288
2289 Ok(())
2290 }
2291
2292 let (_scratch, root, denied_path) = setup_test_fs();
2293 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2294 BackendOptions::default(),
2295 noop_backing_storage(),
2296 ));
2297 tt.run_once(async {
2298 test_operation(root, denied_path)
2299 .read_strongly_consistent()
2300 .await?;
2301
2302 anyhow::Ok(())
2303 })
2304 .await
2305 .unwrap();
2306 }
2307
2308 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2309 async fn test_denied_path_write() {
2310 #[turbo_tasks::function(operation, root)]
2311 async fn write_file_operation(
2312 path: FileSystemPath,
2313 contents: RcStr,
2314 ) -> anyhow::Result<()> {
2315 path.write(
2316 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
2317 .cell(),
2318 )
2319 .await?;
2320 Ok(())
2321 }
2322
2323 #[turbo_tasks::function(operation, root)]
2326 async fn write_allowed_file_operation(
2327 root: RcStr,
2328 denied_path: RcStr,
2329 file_path: RcStr,
2330 contents: RcStr,
2331 ) -> anyhow::Result<Vc<Effects>> {
2332 let fs = DiskFileSystem::new_with_denied_paths(
2333 rcstr!("test"),
2334 Vc::cell(root),
2335 vec![denied_path],
2336 );
2337 let root_path = fs.root().await?;
2338 let allowed_file = root_path.join(&file_path)?;
2339 let write_op = write_file_operation(allowed_file, contents);
2340 write_op.read_strongly_consistent().await?;
2341 Ok(take_effects(write_op).await?.cell())
2342 }
2343
2344 #[turbo_tasks::function(operation, root)]
2345 async fn test_denied_writes_operation(
2346 root: RcStr,
2347 denied_path: RcStr,
2348 denied_file: RcStr,
2349 nested_denied_file: RcStr,
2350 ) -> anyhow::Result<()> {
2351 let fs = DiskFileSystem::new_with_denied_paths(
2352 rcstr!("test"),
2353 Vc::cell(root),
2354 vec![denied_path],
2355 );
2356 let root_path = fs.root().await?;
2357
2358 let path = root_path.join(&denied_file)?;
2359 let result = write_file_operation(path, rcstr!("forbidden"))
2360 .read_strongly_consistent()
2361 .await;
2362 assert!(
2363 result.is_err(),
2364 "writing to denied path should return an error"
2365 );
2366
2367 let path = root_path.join(&nested_denied_file)?;
2368 let result = write_file_operation(path, rcstr!("nested"))
2369 .read_strongly_consistent()
2370 .await;
2371 assert!(
2372 result.is_err(),
2373 "writing to nested denied path should return an error"
2374 );
2375
2376 Ok(())
2377 }
2378
2379 let (_scratch, root, denied_path) = setup_test_fs();
2380 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2381 BackendOptions::default(),
2382 noop_backing_storage(),
2383 ));
2384 tt.run_once(async {
2385 const ALLOWED_FILE: &str = "allowed_dir/new_file.txt";
2386 const TEST_CONTENT: &str = "test content";
2387
2388 let effects_op = write_allowed_file_operation(
2390 root.clone(),
2391 denied_path.clone(),
2392 RcStr::from(ALLOWED_FILE),
2393 RcStr::from(TEST_CONTENT),
2394 );
2395 read_strongly_consistent_and_apply_effects(effects_op, |e| e).await?;
2396
2397 let content = read_to_string(Path::new(root.as_str()).join(ALLOWED_FILE))?;
2399 assert_eq!(content, TEST_CONTENT, "allowed file write should succeed");
2400
2401 test_denied_writes_operation(
2403 root,
2404 denied_path,
2405 RcStr::from("denied_dir/forbidden.txt"),
2406 RcStr::from("denied_dir/nested/file.txt"),
2407 )
2408 .read_strongly_consistent()
2409 .await?;
2410
2411 anyhow::Ok(())
2412 })
2413 .await
2414 .unwrap();
2415 }
2416 }
2417}