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::{Component, MAIN_SEPARATOR, Path, PathBuf, Prefix},
11 sync::{Arc, LazyLock, Weak},
12};
13
14use anyhow::{Context, Result, anyhow};
15use async_trait::async_trait;
16use bincode::{Decode, Encode};
17#[cfg(windows)]
18use omnipath::WinPathExt;
19use rustc_hash::FxHashSet;
20use smallvec::SmallVec;
21use tokio::{
22 runtime::Handle,
23 sync::{RwLock, RwLockReadGuard},
24};
25use tracing::Instrument;
26use turbo_rcstr::{RcStr, rcstr};
27use turbo_tasks::{
28 CapturedEffect, Effect, EffectExt, EffectStateStorage, InvalidationReason, NonLocalValue,
29 ReadRef, ResolvedVc, TurboTasksApi, ValueToString, Vc, debug::ValueDebugFormat, parallel,
30 trace::TraceRawVcs, turbo_tasks_weak, turbobail,
31};
32use turbo_tasks_hash::{hash_xxh3_hash64, hash_xxh3_hash128};
33use turbo_unix_path::{normalize_path, sys_to_unix, unix_to_sys};
34
35#[cfg(windows)]
36use crate::windows::{is_link_junction_point, to_verbatim_with_case_folded_disk};
37use crate::{
38 AnyhowWrapper, File, FileComparison, FileContent, FileMeta, FileSystem, FileSystemPath,
39 LinkContent, LinkTarget, PersistedFileContent, RawDirectoryContent, RawDirectoryEntry,
40 WriteLinkContent, WriteLinkTarget, WriteLinkTargetType,
41 invalidation::Write,
42 invalidator_map::InvalidatorMap,
43 mutex_map::MutexMap,
44 path_map::OrderedPathMapExt,
45 retry::{can_retry, retry_blocking, retry_blocking_custom},
46 watcher::{DiskWatcher, DiskWatcherConfig},
47};
48
49pub fn validate_path_length(path: &Path) -> io::Result<()> {
74 fn error(name_or_path: &str, len: usize, limit: usize) -> io::Error {
75 io::Error::new(
76 io::ErrorKind::InvalidFilename,
77 format!("file {name_or_path} is too long ({len}) exceeds filesystem limit of {limit}"),
78 )
79 }
80 if cfg!(windows) {
81 debug_assert!(
83 matches!(
84 path.components().next(),
85 Some(std::path::Component::Prefix(prefix)) if prefix.kind().is_verbatim()
86 ),
87 "expected a verbatim path, got {path:?}",
88 );
89
90 const MAX_VERBATIM_PATH_LENGTH_WINDOWS: usize = 32_767 - 100;
95 let len = path.as_os_str().len();
96 if len > MAX_VERBATIM_PATH_LENGTH_WINDOWS {
97 return Err(error("path", len, MAX_VERBATIM_PATH_LENGTH_WINDOWS));
98 }
99 }
100
101 if cfg!(unix) {
102 const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
106
107 let name_len = path
110 .file_name()
111 .map(|n| n.as_encoded_bytes().len())
112 .unwrap_or(0);
113 if name_len > MAX_FILE_NAME_LENGTH_UNIX {
114 return Err(error("name", name_len, MAX_FILE_NAME_LENGTH_UNIX));
115 }
116
117 if cfg!(target_os = "macos") {
121 #[cfg(unix)]
122 {
123 use std::os::unix::ffi::OsStrExt;
124 const MAX_PATH_LENGTH: usize = 1024 - 8;
128 let path_len = path.as_os_str().as_bytes().len();
129 if path_len > MAX_PATH_LENGTH {
130 return Err(error("path", path_len, MAX_PATH_LENGTH));
131 }
132 }
133 }
134 }
135
136 Ok(())
138}
139
140pub fn canonicalize_to_rcstr(path: &Path) -> io::Result<RcStr> {
146 fs_err::canonicalize(path)?
147 .into_string()
148 .map(RcStr::from)
149 .map_err(|p| {
150 io::Error::new(
151 ErrorKind::InvalidFilename,
152 anyhow!("canonicalized path {p:?} is not valid unicode"),
153 )
154 })
155}
156
157trait ConcurrencyLimitedExt {
158 type Output;
159 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
160}
161
162impl<F, R> ConcurrencyLimitedExt for F
163where
164 F: Future<Output = R>,
165{
166 type Output = R;
167 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
168 let _permit = semaphore.acquire().await;
169 self.await
170 }
171}
172
173fn number_env_var(name: &'static str) -> Option<usize> {
174 env::var(name)
175 .ok()
176 .filter(|val| !val.is_empty())
177 .map(|val| match val.parse() {
178 Ok(n) => n,
179 Err(err) => panic!("{name} must be a valid integer: {err}"),
180 })
181 .filter(|val| *val != 0)
182}
183
184fn create_read_semaphore() -> tokio::sync::Semaphore {
185 static TURBO_ENGINE_READ_CONCURRENCY: LazyLock<usize> =
188 LazyLock::new(|| number_env_var("TURBO_ENGINE_READ_CONCURRENCY").unwrap_or(64));
189 tokio::sync::Semaphore::new(*TURBO_ENGINE_READ_CONCURRENCY)
190}
191
192fn create_write_semaphore() -> tokio::sync::Semaphore {
193 static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
196 number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
197 4,
200 )
201 });
202 tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
203}
204
205#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
206pub(crate) struct DiskFileSystemInner {
207 pub name: RcStr,
208 root: RcStr,
216 #[turbo_tasks(debug_ignore, trace_ignore)]
217 #[bincode(skip)]
218 mutex_map: MutexMap<Arc<PathBuf>>,
219 #[turbo_tasks(debug_ignore, trace_ignore)]
220 #[bincode(skip)]
221 pub(crate) invalidator_map: InvalidatorMap,
222 #[turbo_tasks(debug_ignore, trace_ignore)]
223 #[bincode(skip)]
224 pub(crate) dir_invalidator_map: InvalidatorMap,
225 #[turbo_tasks(debug_ignore, trace_ignore)]
228 #[bincode(skip)]
229 pub(crate) invalidation_lock: RwLock<()>,
230 #[turbo_tasks(debug_ignore, trace_ignore)]
232 #[bincode(skip, default = "create_read_semaphore")]
233 read_semaphore: tokio::sync::Semaphore,
234 #[turbo_tasks(debug_ignore, trace_ignore)]
236 #[bincode(skip, default = "create_write_semaphore")]
237 write_semaphore: tokio::sync::Semaphore,
238
239 #[turbo_tasks(debug_ignore, trace_ignore)]
240 pub(crate) watcher: DiskWatcher,
241 denied_paths: Vec<RcStr>,
244 #[turbo_tasks(debug_ignore, trace_ignore)]
247 #[bincode(skip, default = "turbo_tasks_weak")]
248 pub(crate) turbo_tasks: Weak<dyn TurboTasksApi>,
249 #[turbo_tasks(debug_ignore, trace_ignore)]
251 #[bincode(skip, default = "Handle::current")]
252 pub(crate) tokio_handle: Handle,
253 #[turbo_tasks(debug_ignore, trace_ignore)]
254 #[bincode(skip)]
255 effect_state_storage: EffectStateStorage,
256}
257
258impl DiskFileSystemInner {
259 pub(crate) fn root_path(&self) -> &Path {
260 Path::new(&*self.root)
261 }
262
263 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
273 let path = &path.path;
274 self.denied_paths.iter().any(|denied_path| {
275 path.starts_with(denied_path.as_str())
276 && (path.len() == denied_path.len()
277 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
278 })
279 }
280
281 async fn register_read_invalidator(&self, path: &Arc<PathBuf>) -> Result<()> {
284 if let Some(invalidator) = turbo_tasks::get_invalidator() {
285 self.invalidator_map.insert(path.clone(), invalidator);
286 self.watcher
287 .ensure_watched_file(path, self.root_path())
288 .await?;
289 }
290 Ok(())
291 }
292
293 fn invalidate_from_write(&self, full_path: &Path) {
297 let mut invalidator_map = self.invalidator_map.lock().unwrap();
298 if let Some(invalidators) = invalidator_map.remove(full_path) {
299 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
300 return;
301 };
302 let _guard = self.tokio_handle.enter();
303 let reason = Write {
304 path: full_path.to_string_lossy().into_owned(),
305 };
306 for invalidator in invalidators {
307 invalidator.invalidate_with_reason(&*turbo_tasks, reason.clone());
308 }
309 }
310 }
311
312 async fn register_dir_invalidator(&self, path: &Arc<PathBuf>) -> Result<()> {
315 if let Some(invalidator) = turbo_tasks::get_invalidator() {
316 self.dir_invalidator_map.insert(path.clone(), invalidator);
317 self.watcher
318 .ensure_watched_dir(path, self.root_path())
319 .await?;
320 }
321 Ok(())
322 }
323
324 async fn lock_path(&self, full_path: Arc<PathBuf>) -> PathLockGuard<'_> {
325 let lock1 = self.invalidation_lock.read().await;
326 let lock2 = self.mutex_map.lock(full_path).await;
327 PathLockGuard(lock1, lock2)
328 }
329
330 pub(crate) fn invalidate(&self) {
331 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
332 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
333 return;
334 };
335 let _guard = self.tokio_handle.enter();
336
337 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
338 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
339 let invalidators = invalidator_map
340 .into_iter()
341 .chain(dir_invalidator_map)
342 .flat_map(|(_, invalidators)| invalidators.into_iter())
343 .collect::<Vec<_>>();
344 parallel::for_each_owned(invalidators, |invalidator| {
345 invalidator.invalidate(&*turbo_tasks)
346 });
347 }
348
349 pub(crate) fn invalidate_with_reason<R: InvalidationReason + Clone>(
353 &self,
354 reason: impl Fn(&Path) -> R + Sync,
355 ) {
356 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
357 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
358 return;
359 };
360 let _guard = self.tokio_handle.enter();
361
362 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
363 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
364 let invalidators = invalidator_map
365 .into_iter()
366 .chain(dir_invalidator_map)
367 .flat_map(|(path, invalidators)| {
368 let reason_for_path = reason(&path);
369 invalidators
370 .into_iter()
371 .map(move |i| (reason_for_path.clone(), i))
372 })
373 .collect::<Vec<_>>();
374 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
375 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
376 });
377 }
378
379 fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
383 &self,
384 paths: impl IntoIterator<Item = PathBuf>,
385 reason: impl Fn(&Path) -> R + Sync,
386 ) {
387 let _span =
388 tracing::info_span!("invalidate filesystem paths", name = &*self.root).entered();
389 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
390 return;
391 };
392 let _guard = self.tokio_handle.enter();
393
394 let mut invalidator_map = self.invalidator_map.lock().unwrap();
395 let mut dir_invalidator_map = self.dir_invalidator_map.lock().unwrap();
396 let mut invalidators = Vec::new();
397 let mut parent_dirs_to_invalidate = FxHashSet::default();
398
399 for path in paths {
400 let mut current_parent = path.parent();
401 while let Some(parent) = current_parent {
402 parent_dirs_to_invalidate.insert(parent.to_path_buf());
403 current_parent = parent.parent();
404 }
405
406 for (invalidated_path, path_invalidators) in
407 invalidator_map.extract_path_with_children(&path)
408 {
409 let reason_for_path = reason(&invalidated_path);
410 invalidators.extend(
411 path_invalidators
412 .into_iter()
413 .map(|invalidator| (reason_for_path.clone(), invalidator)),
414 );
415 }
416
417 for (invalidated_path, path_invalidators) in
418 dir_invalidator_map.extract_path_with_children(&path)
419 {
420 let reason_for_path = reason(&invalidated_path);
421 invalidators.extend(
422 path_invalidators
423 .into_iter()
424 .map(|invalidator| (reason_for_path.clone(), invalidator)),
425 );
426 }
427 }
428
429 for path in parent_dirs_to_invalidate {
430 if let Some(path_invalidators) = dir_invalidator_map.remove(path.as_path()) {
431 let reason_for_path = reason(&path);
432 invalidators.extend(
433 path_invalidators
434 .into_iter()
435 .map(|invalidator| (reason_for_path.clone(), invalidator)),
436 );
437 }
438 }
439
440 drop(invalidator_map);
441 drop(dir_invalidator_map);
442
443 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
444 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
445 });
446 }
447
448 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
449 async fn start_watching_internal(self: &Arc<Self>) -> Result<()> {
450 let root_path = self.root_path().to_path_buf();
451
452 retry_blocking(|| std::fs::create_dir_all(&root_path))
454 .instrument(tracing::info_span!("create root directory", name = ?root_path))
455 .concurrency_limited(&self.write_semaphore)
456 .await?;
457
458 DiskWatcher::start_watching(self.clone()).await?;
459
460 Ok(())
461 }
462}
463
464#[derive(Clone, ValueToString)]
470#[value_to_string(self.inner.name)]
471#[turbo_tasks::value(cell = "new", eq = "manual", evict = "never")]
472pub struct DiskFileSystem {
473 inner: Arc<DiskFileSystemInner>,
474}
475
476impl DiskFileSystem {
477 pub fn name(&self) -> &RcStr {
478 &self.inner.name
479 }
480
481 pub fn root(&self) -> &RcStr {
482 &self.inner.root
483 }
484
485 #[cfg(debug_assertions)]
486 async fn ensure_path_is_realpath(&self, operation: &str, path: &Path) -> Result<()> {
487 if let Ok(realpath) = retry_blocking(|| fs_err::canonicalize(path))
488 .instrument(tracing::info_span!("realpath for filesystem read", name = ?path))
489 .concurrency_limited(&self.inner.read_semaphore)
490 .await
491 && realpath != path
492 {
493 anyhow::bail!(
494 "{operation} called with unresolved path {path:?}; resolve it to {realpath:?} \
495 first"
496 );
497 }
498 Ok(())
499 }
500
501 pub fn invalidate(&self) {
502 self.inner.invalidate();
503 }
504
505 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
506 &self,
507 reason: impl Fn(&Path) -> R + Sync,
508 ) {
509 self.inner.invalidate_with_reason(reason);
510 }
511
512 pub fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
513 &self,
514 paths: impl IntoIterator<Item = PathBuf>,
515 reason: impl Fn(&Path) -> R + Sync,
516 ) {
517 self.inner
518 .invalidate_path_and_children_with_reason(paths, reason);
519 }
520
521 pub async fn start_watching(&self) -> Result<()> {
522 self.inner.start_watching_internal().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 = to_verbatim_with_case_folded_disk(sys_path).ok()?;
559
560 normalized_sys_path
561 .strip_prefix(self.inner.root_path())
562 .ok()?
563 .to_owned()
564 } else {
565 let root_sys_path = self.inner.root_path();
573 let relative_to_sys_path = if let Some(relative_to) = relative_to {
574 debug_assert_eq!(
575 relative_to.fs, vc_self,
576 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
577 );
578 root_sys_path
579 .join(Path::new(&*unix_to_sys(&relative_to.path)))
580 .join(sys_path)
581 } else {
582 root_sys_path.join(sys_path)
583 };
584 relative_to_sys_path
585 .normalize_lexically()
586 .ok()?
587 .strip_prefix(root_sys_path)
588 .ok()?
589 .to_owned()
590 };
591
592 Some(FileSystemPath {
593 fs: vc_self,
594 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
595 })
596 }
597
598 pub fn to_sys_path_raw(&self, fs_path: &FileSystemPath) -> PathBuf {
610 let sys_root = self.inner.root_path();
611 if fs_path.path.is_empty() {
612 sys_root.to_path_buf()
613 } else {
614 sys_root.join(&*unix_to_sys(&fs_path.path))
615 }
616 }
617
618 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
626 let sys_path = self.to_sys_path_raw(fs_path);
627
628 #[cfg(windows)]
629 return sys_path.to_winuser_path().unwrap_or(sys_path);
630 #[cfg(not(windows))]
631 return sys_path;
632 }
633
634 async fn resolve_link_target_ancestry_slow_path(
649 &self,
650 vc_self: ResolvedVc<Self>,
651 target_sys_path: &Path,
652 ) -> Result<Option<FileSystemPath>> {
653 #[turbo_tasks::value(transparent)]
654 struct OptionRcStr(Option<RcStr>);
655
656 #[turbo_tasks::function(fs, session_dependent)]
659 async fn canonicalize_untracked(sys_path: RcStr) -> Vc<OptionRcStr> {
660 Vc::cell(
661 retry_blocking(|| canonicalize_to_rcstr(Path::new(&*sys_path)))
662 .await
663 .ok(),
664 )
665 }
666
667 let root_sys_path = self.inner.root_path();
668
669 let ancestors: SmallVec<[&Path; 8]> = target_sys_path.ancestors().collect();
674 for prefix in ancestors.into_iter().rev().skip(1) {
675 let Some(prefix_str) = prefix.to_str() else {
676 return Ok(None);
678 };
679 let Some(canonical) = canonicalize_untracked(RcStr::from(prefix_str))
680 .owned()
681 .await?
682 else {
683 return Ok(None);
684 };
685 let canonical = Path::new(canonical.as_str());
686 if canonical.starts_with(root_sys_path) {
687 let rest = target_sys_path
690 .strip_prefix(prefix)
691 .expect("`ancestors` yields prefixes of `target_sys_path`");
692 return Ok(self.try_from_sys_path(vc_self, &canonical.join(rest), None));
693 }
694 }
695
696 Ok(None)
698 }
699}
700
701#[allow(dead_code, reason = "we need to hold onto the locks")]
702struct PathLockGuard<'a>(
703 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
704 #[allow(dead_code)] crate::mutex_map::MutexMapGuard<'a, Arc<PathBuf>>,
705);
706
707pub(crate) fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
708 if let Ok(rel_path) = path.strip_prefix(root_path) {
709 let path = if MAIN_SEPARATOR != '/' {
710 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
711 format!("[{name}]/{rel_path}")
712 } else {
713 format!("[{name}]/{}", rel_path.display())
714 };
715 Some(path)
716 } else {
717 None
718 }
719}
720
721impl DiskFileSystem {
722 pub fn new(name: RcStr, root: Vc<RcStr>) -> Vc<Self> {
730 Self::new_internal(name, root, Vec::new(), DiskWatcherConfig::default())
731 }
732
733 pub fn new_with_options(
744 name: RcStr,
745 root: Vc<RcStr>,
746 denied_paths: Vec<RcStr>,
747 watcher_config: DiskWatcherConfig,
748 ) -> Vc<Self> {
749 for denied_path in &denied_paths {
750 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
751 debug_assert!(
752 normalize_path(denied_path).as_deref() == Some(&**denied_path),
753 "denied_path must be normalized: {denied_path:?}"
754 );
755 }
756 Self::new_internal(name, root, denied_paths, watcher_config)
757 }
758}
759
760#[turbo_tasks::value_impl]
761impl DiskFileSystem {
762 #[turbo_tasks::function]
763 async fn new_internal(
764 name: RcStr,
765 root: Vc<RcStr>,
766 denied_paths: Vec<RcStr>,
767 watcher_config: DiskWatcherConfig,
768 ) -> Result<Vc<Self>> {
769 let root = root.owned().await?;
770 let instance = DiskFileSystem {
771 inner: Arc::new(DiskFileSystemInner {
772 name,
773 root,
774 mutex_map: Default::default(),
775 invalidation_lock: Default::default(),
776 invalidator_map: InvalidatorMap::new(),
777 dir_invalidator_map: InvalidatorMap::new(),
778 read_semaphore: create_read_semaphore(),
779 write_semaphore: create_write_semaphore(),
780 watcher: DiskWatcher::new(watcher_config),
781 denied_paths,
782 turbo_tasks: turbo_tasks_weak(),
783 tokio_handle: Handle::current(),
784 effect_state_storage: EffectStateStorage::default(),
785 }),
786 };
787
788 Ok(Self::cell(instance))
789 }
790}
791
792impl Debug for DiskFileSystem {
793 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
794 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
795 }
796}
797
798#[turbo_tasks::value_impl]
799impl FileSystem for DiskFileSystem {
800 #[turbo_tasks::function(fs, session_dependent)]
801 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
802 if self.inner.is_path_denied(&fs_path) {
804 return Ok(FileContent::NotFound.cell());
805 }
806 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
807
808 self.inner.register_read_invalidator(&full_path).await?;
809
810 let _lock = self.inner.lock_path(full_path.clone()).await;
811 let content = match retry_blocking(|| File::from_path(&full_path))
812 .instrument(tracing::info_span!("read file", name = ?full_path))
813 .concurrency_limited(&self.inner.read_semaphore)
814 .await
815 {
816 Ok(file) => {
817 #[cfg(debug_assertions)]
818 self.ensure_path_is_realpath("read_file", &full_path)
819 .await?;
820 FileContent::new(file)
821 }
822 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
823 FileContent::NotFound
824 }
825 Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
827 };
828 Ok(content.cell())
829 }
830
831 #[turbo_tasks::function(fs, session_dependent)]
832 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
833 if self.inner.is_path_denied(&fs_path) {
835 return Ok(RawDirectoryContent::not_found());
836 }
837 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
838
839 self.inner.register_dir_invalidator(&full_path).await?;
840
841 let read_dir = match retry_blocking(|| std::fs::read_dir(&*full_path))
843 .instrument(tracing::info_span!("read directory", name = ?full_path))
844 .concurrency_limited(&self.inner.read_semaphore)
845 .await
846 {
847 Ok(dir) => {
848 #[cfg(debug_assertions)]
849 self.ensure_path_is_realpath("read_dir", &full_path).await?;
850 dir
851 }
852 Err(e)
853 if e.kind() == ErrorKind::NotFound
854 || e.kind() == ErrorKind::NotADirectory
855 || e.kind() == ErrorKind::InvalidFilename =>
856 {
857 return Ok(RawDirectoryContent::not_found());
858 }
859 Err(e) => {
860 return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
862 }
863 };
864 let dir_path = fs_path.path.as_str();
865 let denied_entries: FxHashSet<&str> = self
866 .inner
867 .denied_paths
868 .iter()
869 .filter_map(|denied_path| {
870 if denied_path.starts_with(dir_path) {
877 let denied_path_suffix =
878 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
879 Some(&denied_path[dir_path.len() + 1..])
880 } else if dir_path.is_empty() {
881 Some(denied_path.as_str())
882 } else {
883 None
884 };
885 denied_path_suffix.filter(|s| !s.contains('/'))
887 } else {
888 None
889 }
890 })
891 .collect();
892
893 let entries = read_dir
894 .filter_map(|r| {
895 let e = match r {
896 Ok(e) => e,
897 Err(err) => return Some(Err(err.into())),
898 };
899
900 let file_name = RcStr::from(e.file_name().to_str()?);
902 if denied_entries.contains(file_name.as_str()) {
904 return None;
905 }
906
907 let entry = match e.file_type() {
908 Ok(t) if t.is_file() => RawDirectoryEntry::File,
909 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
910 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
911 Ok(_) => RawDirectoryEntry::Other,
912 Err(err) => return Some(Err(err.into())),
913 };
914
915 Some(anyhow::Ok((file_name, entry)))
916 })
917 .collect::<Result<_>>()
918 .with_context(|| format!("reading directory item in {full_path:?}"))?;
919
920 Ok(RawDirectoryContent::new(entries))
921 }
922
923 #[turbo_tasks::function(fs, session_dependent)]
924 async fn read_link(self: ResolvedVc<Self>, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
925 let this = self.await?;
926 let inner = &this.inner;
927 if inner.is_path_denied(&fs_path) {
928 return Ok(LinkContent::Invalid {
929 reason: rcstr!("access to the symlink path is denied"),
930 }
931 .cell());
932 }
933 let full_link_path = Arc::new(this.to_sys_path_raw(&fs_path));
934
935 inner.register_read_invalidator(&full_link_path).await?;
936
937 let _lock = inner.lock_path(full_link_path.clone()).await;
938 let mut target_sys_path = match retry_blocking(|| std::fs::read_link(&**full_link_path))
939 .instrument(tracing::info_span!("read symlink", name = ?full_link_path))
940 .concurrency_limited(&inner.read_semaphore)
941 .await
942 {
943 Ok(res) => res,
944 Err(err) if err.kind() == ErrorKind::NotFound => {
945 return Ok(LinkContent::NotFound.cell());
946 }
947 Err(err) => {
948 return Ok(LinkContent::Invalid {
949 reason: RcStr::from(err.to_string()),
950 }
951 .cell());
952 }
953 };
954
955 if cfg!(windows) && target_sys_path.has_root() && !target_sys_path.is_absolute() {
956 let mut absolute_target = inner.root_path().to_path_buf();
959 absolute_target.push(target_sys_path);
960 target_sys_path = absolute_target;
961 }
962
963 let target = if target_sys_path.is_absolute() {
964 let mut target_fs_path = this.try_from_sys_path(self, &target_sys_path, None);
967
968 if target_fs_path.is_none() {
973 target_fs_path = this
974 .resolve_link_target_ancestry_slow_path(self, &target_sys_path)
975 .await?;
976 }
977
978 let Some(target_fs_path) = target_fs_path else {
979 return Ok(LinkContent::Invalid {
982 reason: rcstr!(
983 "the symlink target leaves the filesystem root or its parent directory \
984 could not be resolved"
985 ),
986 }
987 .cell());
988 };
989 LinkTarget::Absolute {
991 resolved: target_fs_path,
992 }
993 } else {
994 if cfg!(windows)
995 && let Some(Component::Prefix(target_prefix)) = target_sys_path.components().next()
996 {
997 let Prefix::Disk(target_drive) = target_prefix.kind() else {
1001 unreachable!(
1002 "path is relative, but contains a prefix that should form an absolute path"
1003 );
1004 };
1005 let root_drive = match inner.root_path().components().next() {
1006 Some(Component::Prefix(root_prefix)) => match root_prefix.kind() {
1007 Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => Some(drive),
1008 _ => None,
1009 },
1010 _ => None,
1011 };
1012 if root_drive
1013 .is_none_or(|root_drive| !target_drive.eq_ignore_ascii_case(&root_drive))
1014 {
1015 return Ok(LinkContent::Invalid {
1016 reason: rcstr!(
1017 "the symlink target uses a different drive than the filesystem root"
1018 ),
1019 }
1020 .cell());
1021 }
1022
1023 target_sys_path = target_sys_path.components().skip(1).collect();
1024 }
1025
1026 let Some(target_str) = target_sys_path.to_str() else {
1029 return Ok(LinkContent::Invalid {
1030 reason: RcStr::from(format!(
1031 "the symlink target {target_sys_path:?} is not valid unicode"
1032 )),
1033 }
1034 .cell());
1035 };
1036 let raw = RcStr::from(sys_to_unix(target_str));
1037
1038 let Some(resolved) = fs_path.parent().try_join(&raw) else {
1044 return Ok(LinkContent::Invalid {
1045 reason: rcstr!("the symlink target leaves the filesystem root"),
1046 }
1047 .cell());
1048 };
1049 LinkTarget::Relative { raw, resolved }
1050 };
1051
1052 Ok(LinkContent::Link { target }.cell())
1053 }
1054
1055 #[turbo_tasks::function(fs, session_dependent)]
1056 async fn is_junction_point(&self, fs_path: FileSystemPath) -> Result<Vc<bool>> {
1057 #[cfg(windows)]
1058 {
1059 if self.inner.is_path_denied(&fs_path) {
1060 return Ok(Vc::cell(false));
1061 }
1062 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
1063 self.inner.register_read_invalidator(&full_path).await?;
1064
1065 let _lock = self.inner.lock_path(full_path.clone()).await;
1066 let is_junction_point = retry_blocking(|| is_link_junction_point(&full_path))
1067 .instrument(tracing::info_span!("read junction point", name = ?full_path))
1068 .concurrency_limited(&self.inner.read_semaphore)
1069 .await
1070 .with_context(|| format!("checking junction point {full_path:?}"))?;
1071 Ok(Vc::cell(is_junction_point))
1072 }
1073 #[cfg(not(windows))]
1074 {
1075 let _ = fs_path;
1076 Ok(Vc::cell(false))
1077 }
1078 }
1079
1080 #[turbo_tasks::function(fs)]
1081 async fn write(
1082 self: ResolvedVc<Self>,
1083 fs_path: FileSystemPath,
1084 content: ResolvedVc<FileContent>,
1085 ) -> Result<()> {
1086 let this = self.await?;
1087 if this.inner.is_path_denied(&fs_path) {
1093 turbobail!("Cannot write to denied path: {fs_path}");
1094 }
1095 let full_path = this.to_sys_path_raw(&fs_path);
1096
1097 validate_path_length(&full_path)?;
1100
1101 let content = content.persist().to_resolved().await?;
1107 let content_hash = hash_xxh3_hash128(&*content.await?);
1108
1109 #[turbo_tasks::value(eq = "manual", cell = "new")]
1110 struct WriteEffect {
1111 full_path: Arc<PathBuf>,
1112 fs: ResolvedVc<DiskFileSystem>,
1113 content: ResolvedVc<PersistedFileContent>,
1114 content_hash: u128,
1115 }
1116
1117 #[async_trait]
1118 #[turbo_tasks::value_impl]
1119 impl Effect for WriteEffect {
1120 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
1121 let inner = (*self.fs).untracked().await?.inner.clone();
1124
1125 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
1132 let content = if inner
1133 .effect_state_storage
1134 .matches_applied(&key_bytes, self.content_hash)
1135 {
1136 None
1137 } else {
1138 Some((*self.content).untracked().await?)
1142 };
1143 Ok(Box::new(CapturedWriteEffect {
1144 full_path: self.full_path.clone(),
1145 inner,
1146 content,
1147 content_hash: self.content_hash,
1148 }) as Box<dyn CapturedEffect>)
1149 }
1150 }
1151
1152 #[derive(TraceRawVcs, NonLocalValue, Clone)]
1153 struct CapturedWriteEffect {
1154 full_path: Arc<PathBuf>,
1155 inner: Arc<DiskFileSystemInner>,
1156 content: Option<ReadRef<PersistedFileContent>>,
1157 content_hash: u128,
1158 }
1159
1160 #[async_trait]
1161 impl CapturedEffect for CapturedWriteEffect {
1162 fn key(&self) -> Box<[u8]> {
1163 self.full_path.as_os_str().as_encoded_bytes().into()
1164 }
1165
1166 fn value_hash(&self) -> u128 {
1167 self.content_hash
1168 }
1169
1170 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1171 let body = self.content.as_ref().map(|content| {
1172 async || self.apply_inner(content).await.map_err(AnyhowWrapper::from)
1173 });
1174 self.inner
1175 .effect_state_storage
1176 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1177 .await
1178 }
1179 }
1180
1181 impl CapturedWriteEffect {
1182 async fn apply_inner(
1183 &self,
1184 content: &ReadRef<PersistedFileContent>,
1185 ) -> anyhow::Result<()> {
1186 let full_path = &self.full_path;
1187
1188 let _lock = self.inner.lock_path(full_path.clone()).await;
1189
1190 let compare = content
1196 .streaming_compare(full_path)
1197 .instrument(tracing::info_span!(
1198 "read file before write",
1199 name = ?full_path,
1200 ))
1201 .concurrency_limited(&self.inner.read_semaphore)
1202 .await?;
1203 if compare == FileComparison::Equal {
1204 return Ok(());
1205 }
1206
1207 match &**content {
1208 PersistedFileContent::Content(..) => {
1209 let content = content.clone();
1210
1211 let mut missing_parent_dir = false;
1212 let do_write = || {
1213 if missing_parent_dir && let Some(parent) = full_path.parent() {
1214 std::fs::create_dir_all(parent)?;
1215 missing_parent_dir = false;
1216 }
1217 let mut f = std::fs::File::create(&**full_path).inspect_err(|err| {
1218 if err.kind() == ErrorKind::NotFound {
1219 missing_parent_dir = true;
1221 }
1222 })?;
1223 let PersistedFileContent::Content(file) = &*content else {
1224 unreachable!()
1225 };
1226 std::io::copy(&mut file.read(), &mut f)?;
1227 #[cfg(unix)]
1228 f.set_permissions(file.meta.permissions.into())?;
1229 f.flush()?;
1230
1231 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
1232 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
1233 .is_some_and(|v| v == "1" || v == "true")
1234 });
1235 if *WRITE_VERSION {
1236 let mut full_path = (**full_path).clone();
1237 let hash = hash_xxh3_hash64(file);
1238 let orig_ext = full_path.extension();
1239 let mut ext = OsString::from(format!("{hash:016x}"));
1240 if let Some(orig_ext) = orig_ext {
1241 ext.push(".");
1242 ext.push(orig_ext);
1243 }
1244 full_path.set_extension(ext);
1245 validate_path_length(&full_path)?;
1246 let mut f = std::fs::File::create(&*full_path)?;
1247 std::io::copy(&mut file.read(), &mut f)?;
1248 #[cfg(unix)]
1249 f.set_permissions(file.meta.permissions.into())?;
1250 f.flush()?;
1251 }
1252 Ok::<(), io::Error>(())
1253 };
1254 fn can_retry_write(err: &io::Error) -> bool {
1255 err.kind() == ErrorKind::NotFound || can_retry(err)
1256 }
1257 retry_blocking_custom(do_write, can_retry_write)
1258 .instrument(tracing::info_span!("write file", name = ?full_path))
1259 .concurrency_limited(&self.inner.write_semaphore)
1260 .await
1261 .with_context(|| format!("failed to write to {full_path:?}"))?;
1262 }
1263 PersistedFileContent::NotFound => {
1264 retry_blocking(|| std::fs::remove_file(&**full_path))
1265 .instrument(tracing::info_span!("remove file", name = ?full_path))
1266 .concurrency_limited(&self.inner.write_semaphore)
1267 .await
1268 .or_else(|err| {
1269 if err.kind() == ErrorKind::NotFound {
1270 Ok(())
1271 } else {
1272 Err(err)
1273 }
1274 })
1275 .with_context(|| format!("removing {full_path:?} failed"))?;
1276 }
1277 }
1278
1279 self.inner.invalidate_from_write(&self.full_path);
1281
1282 Ok(())
1283 }
1284 }
1285
1286 WriteEffect {
1287 full_path: Arc::new(full_path),
1288 fs: self,
1289 content,
1290 content_hash,
1291 }
1292 .resolved_cell()
1293 .emit();
1294
1295 Ok(())
1296 }
1297
1298 #[turbo_tasks::function(fs)]
1299 async fn write_link(
1300 self: ResolvedVc<Self>,
1301 fs_path: FileSystemPath,
1302 target: ResolvedVc<WriteLinkContent>,
1303 ) -> Result<()> {
1304 let this = self.await?;
1309 if this.inner.is_path_denied(&fs_path) {
1311 turbobail!("Cannot write link to denied path: {fs_path}");
1312 }
1313 let full_path = this.to_sys_path_raw(&fs_path);
1314
1315 validate_path_length(&full_path)?;
1316
1317 let content_hash = hash_xxh3_hash128(&*target.await?);
1318
1319 #[turbo_tasks::value(eq = "manual", cell = "new")]
1320 struct WriteLinkEffect {
1321 full_path: Arc<PathBuf>,
1322 fs: ResolvedVc<DiskFileSystem>,
1323 target: ResolvedVc<WriteLinkContent>,
1324 content_hash: u128,
1325 }
1326
1327 #[async_trait]
1328 #[turbo_tasks::value_impl]
1329 impl Effect for WriteLinkEffect {
1330 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
1331 let inner = (*self.fs).untracked().await?.inner.clone();
1332
1333 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
1336 let content = if inner
1337 .effect_state_storage
1338 .matches_applied(&key_bytes, self.content_hash)
1339 {
1340 None
1341 } else {
1342 Some((*self.target).untracked().await?)
1344 };
1345 Ok(Box::new(CapturedWriteLinkEffect {
1346 full_path: self.full_path.clone(),
1347 inner,
1348 content,
1349 content_hash: self.content_hash,
1350 }) as Box<dyn CapturedEffect>)
1351 }
1352 }
1353
1354 #[derive(TraceRawVcs, NonLocalValue, Clone)]
1356 struct CapturedWriteLinkEffect {
1357 full_path: Arc<PathBuf>,
1358 inner: Arc<DiskFileSystemInner>,
1359 content: Option<ReadRef<WriteLinkContent>>,
1360 content_hash: u128,
1361 }
1362
1363 #[async_trait]
1364 impl CapturedEffect for CapturedWriteLinkEffect {
1365 fn key(&self) -> Box<[u8]> {
1366 self.full_path.as_os_str().as_encoded_bytes().into()
1367 }
1368
1369 fn value_hash(&self) -> u128 {
1370 self.content_hash
1371 }
1372
1373 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1374 let body = self.content.as_ref().map(|content| {
1375 async || self.apply_inner(content).await.map_err(AnyhowWrapper::from)
1376 });
1377 self.inner
1378 .effect_state_storage
1379 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1380 .await
1381 }
1382 }
1383
1384 impl CapturedWriteLinkEffect {
1385 async fn apply_inner(&self, content: &ReadRef<WriteLinkContent>) -> anyhow::Result<()> {
1386 let full_path = self.full_path.clone();
1387
1388 let _lock = self.inner.lock_path(full_path.clone()).await;
1389
1390 let WriteLinkContent {
1391 target,
1392 target_type,
1393 } = &**content;
1394 let is_directory =
1395 matches!(target_type, WriteLinkTargetType::DirectoryOrJunctionPoint);
1396 let target = match target {
1397 WriteLinkTarget::Absolute(target) => {
1398 self.inner.root_path().join(unix_to_sys(target).as_ref())
1399 }
1400 WriteLinkTarget::Relative(target) => {
1401 let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1402 if cfg!(windows) && is_directory {
1403 full_path
1405 .parent()
1406 .unwrap_or(&full_path)
1407 .join(relative_target)
1408 } else {
1409 relative_target
1410 }
1411 }
1412 };
1413
1414 let old_content = match retry_blocking(|| std::fs::read_link(&**full_path))
1415 .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1416 .concurrency_limited(&self.inner.read_semaphore)
1417 .await
1418 {
1419 Ok(res) => Some((res.is_absolute(), res)),
1420 Err(_) => None,
1421 };
1422 #[cfg(not(windows))]
1423 let is_equal = match &old_content {
1424 Some((old_is_absolute, old_target)) => {
1425 target == *old_target && target.is_absolute() == *old_is_absolute
1426 }
1427 None => false,
1428 };
1429 #[cfg(windows)]
1430 let is_equal = match &old_content {
1431 Some((old_is_absolute, old_target)) => {
1432 target == *old_target
1433 && target.is_absolute() == *old_is_absolute
1434 && is_link_junction_point(&full_path).ok() == Some(is_directory)
1435 }
1436 None => false,
1437 };
1438 if is_equal {
1439 return Ok(());
1440 }
1441
1442 #[derive(thiserror::Error, Debug)]
1443 #[error("{msg}: {source}")]
1444 struct SymlinkCreationError {
1445 msg: &'static str,
1446 #[source]
1447 source: io::Error,
1448 }
1449
1450 let mut missing_parent_dir = false;
1451 let mut has_old_content = old_content.is_some();
1452 let try_create_link = || {
1453 if missing_parent_dir && let Some(parent) = full_path.parent() {
1454 std::fs::create_dir_all(parent).map_err(|err| SymlinkCreationError {
1455 msg: "failed to create directory",
1456 source: err,
1457 })?;
1458 missing_parent_dir = false;
1459 }
1460 if has_old_content {
1461 remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1466 SymlinkCreationError {
1467 msg: "removal of existing symbolic link or junction point failed",
1468 source: err,
1469 }
1470 })?;
1471 has_old_content = false;
1472 }
1473 #[cfg(all(not(windows), not(target_os = "wasi")))]
1474 let io_result = std::os::unix::fs::symlink(&target, &**full_path);
1475 #[cfg(target_os = "wasi")]
1476 let io_result = std::os::wasi::fs::symlink_path(&target, &**full_path);
1477 #[cfg(windows)]
1478 let io_result = if is_directory {
1479 std::os::windows::fs::junction_point(&target, &**full_path)
1480 } else {
1481 std::os::windows::fs::symlink_file(&target, &**full_path)
1482 };
1483 io_result.map_err(|err| {
1484 match err.kind() {
1485 ErrorKind::NotFound => {
1486 missing_parent_dir = true;
1488 }
1489 ErrorKind::AlreadyExists => {
1490 has_old_content = true;
1492 }
1493 _ => {}
1494 }
1495 SymlinkCreationError {
1496 msg: "creation of a new symbolic link or junction point failed",
1497 source: err,
1498 }
1499 })
1500 };
1501 fn can_retry_link(err: &SymlinkCreationError) -> bool {
1502 matches!(
1503 err.source.kind(),
1504 ErrorKind::NotFound | ErrorKind::AlreadyExists
1505 ) || can_retry(&err.source)
1506 }
1507 let err_context = || {
1508 #[cfg(not(windows))]
1509 let message =
1510 format!("failed to create symlink at {full_path:?} pointing to {target:?}");
1511 #[cfg(windows)]
1512 let message = if is_directory {
1513 format!(
1514 "failed to create junction point at {full_path:?} pointing to \
1515 {target:?}"
1516 )
1517 } else {
1518 format!(
1519 "failed to create symlink at {full_path:?} pointing to \
1520 {target:?}\n\
1521 (Note: creating file symlinks on Windows require developer \
1522 mode or admin permissions: \
1523 https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1524 )
1525 };
1526 message
1527 };
1528 retry_blocking_custom(try_create_link, can_retry_link)
1529 .instrument(tracing::info_span!(
1530 "write symlink",
1531 name = ?full_path,
1532 target = ?target,
1533 ))
1534 .concurrency_limited(&self.inner.write_semaphore)
1535 .await
1536 .with_context(err_context)?;
1537
1538 self.inner.invalidate_from_write(&self.full_path);
1540
1541 Ok(())
1542 }
1543 }
1544
1545 WriteLinkEffect {
1546 full_path: Arc::new(full_path),
1547 fs: self,
1548 target,
1549 content_hash,
1550 }
1551 .resolved_cell()
1552 .emit();
1553 Ok(())
1554 }
1555
1556 #[turbo_tasks::function(fs, session_dependent)]
1557 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1558 let full_path = Arc::new(self.to_sys_path_raw(&fs_path));
1559
1560 if self.inner.is_path_denied(&fs_path) {
1562 turbobail!("Cannot read metadata from denied path: {fs_path}");
1563 }
1564
1565 self.inner.register_read_invalidator(&full_path).await?;
1566
1567 let _lock = self.inner.lock_path(full_path.clone()).await;
1568 let meta = retry_blocking(|| std::fs::metadata(&**full_path))
1569 .instrument(tracing::info_span!("read metadata", name = ?full_path))
1570 .concurrency_limited(&self.inner.read_semaphore)
1571 .await
1572 .with_context(|| format!("reading metadata for {:?}", full_path))?;
1573
1574 Ok(FileMeta::cell(meta.into()))
1575 }
1576}
1577
1578fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1579 let result = if cfg!(windows) {
1580 std::fs::remove_dir(path).or_else(|err| {
1593 if err.kind() == ErrorKind::NotADirectory {
1594 std::fs::remove_file(path)
1595 } else {
1596 Err(err)
1597 }
1598 })
1599 } else {
1600 std::fs::remove_file(path)
1601 };
1602 match result {
1603 Ok(()) => Ok(()),
1604 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1605 Err(err) => Err(err),
1606 }
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611 use turbo_rcstr::rcstr;
1612 use turbo_tasks::{Effects, OperationVc, Vc, take_effects};
1613 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1614
1615 use super::*;
1616
1617 #[turbo_tasks::function(operation, root)]
1618 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
1619 let _ = op.resolve().strongly_consistent().await?;
1620 Ok(take_effects(op).await?.cell())
1621 }
1622
1623 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1624 async fn test_try_from_sys_path() {
1625 let sys_root = if cfg!(windows) {
1626 Path::new(r"C:\fake\root")
1627 } else {
1628 Path::new(r"/fake/root")
1629 };
1630
1631 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1632 BackendOptions::default(),
1633 noop_backing_storage(),
1634 ));
1635 tt.run_once(async {
1636 assert_try_from_sys_path_operation(RcStr::from(sys_root.to_str().unwrap()))
1637 .read_strongly_consistent()
1638 .await?;
1639
1640 anyhow::Ok(())
1641 })
1642 .await
1643 .unwrap();
1644 }
1645
1646 #[turbo_tasks::function(operation, root)]
1647 async fn assert_try_from_sys_path_operation(sys_root: RcStr) -> anyhow::Result<()> {
1648 let sys_root = Path::new(sys_root.as_str());
1649 let fs_vc = DiskFileSystem::new(
1650 rcstr!("temp"),
1651 Vc::cell(RcStr::from(sys_root.to_str().unwrap())),
1652 )
1653 .to_resolved()
1654 .await?;
1655 let fs = fs_vc.await?;
1656 let fs_root_path = fs_vc.root().await?;
1657
1658 assert_eq!(
1659 fs.try_from_sys_path(
1660 fs_vc,
1661 &Path::new("relative").join("directory"),
1662 None,
1663 )
1664 .unwrap()
1665 .path,
1666 "relative/directory"
1667 );
1668
1669 assert_eq!(
1670 fs.try_from_sys_path(
1671 fs_vc,
1672 &sys_root
1673 .join("absolute")
1674 .join("directory")
1675 .join("..")
1676 .join("normalized_path"),
1677 Some(&fs_root_path.join("ignored").unwrap()),
1678 )
1679 .unwrap()
1680 .path,
1681 "absolute/normalized_path"
1682 );
1683
1684 assert_eq!(
1685 fs.try_from_sys_path(
1686 fs_vc,
1687 Path::new("child"),
1688 Some(&fs_root_path.join("parent").unwrap()),
1689 )
1690 .unwrap()
1691 .path,
1692 "parent/child"
1693 );
1694
1695 assert_eq!(
1696 fs.try_from_sys_path(
1697 fs_vc,
1698 &Path::new("..").join("parallel_dir"),
1699 Some(&fs_root_path.join("parent").unwrap()),
1700 )
1701 .unwrap()
1702 .path,
1703 "parallel_dir"
1704 );
1705
1706 assert_eq!(
1707 fs.try_from_sys_path(
1708 fs_vc,
1709 &Path::new("relative")
1710 .join("..")
1711 .join("..")
1712 .join("leaves_root"),
1713 None,
1714 ),
1715 None
1716 );
1717
1718 assert_eq!(
1719 fs.try_from_sys_path(
1720 fs_vc,
1721 &sys_root
1722 .join("absolute")
1723 .join("..")
1724 .join("..")
1725 .join("leaves_root"),
1726 None,
1727 ),
1728 None
1729 );
1730
1731 Ok(())
1732 }
1733
1734 #[cfg(test)]
1735 mod symlink_tests {
1736 use std::{
1737 fs::{File, create_dir_all, read_to_string},
1738 io::Write,
1739 };
1740
1741 use rand::{RngExt, SeedableRng};
1742 use turbo_rcstr::{RcStr, rcstr};
1743 use turbo_tasks::{ResolvedVc, Vc, read_strongly_consistent_and_apply_effects};
1744 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1745
1746 use super::extract_effects_operation;
1747 #[cfg(all(unix, debug_assertions))]
1748 use crate::{DirectoryContent, FileContent, RawDirectoryContent};
1749 use crate::{
1750 DiskFileSystem, FileSystem, FileSystemEntryType, FileSystemPath, LinkContent,
1751 LinkTarget, RealPathErrorType, WriteLinkContent, WriteLinkTarget, WriteLinkTargetType,
1752 canonicalize_to_rcstr,
1753 };
1754
1755 #[turbo_tasks::function(operation, root)]
1756 async fn test_write_link_effect_operation(
1757 fs: ResolvedVc<DiskFileSystem>,
1758 path: FileSystemPath,
1759 target: RcStr,
1760 ) -> anyhow::Result<()> {
1761 let write_file = |f| {
1762 fs.write_link(
1763 f,
1764 WriteLinkContent {
1765 target: WriteLinkTarget::Relative(format!("{target}/data.txt").into()),
1766 target_type: WriteLinkTargetType::FileNonPortable,
1767 }
1768 .cell(),
1769 )
1770 };
1771 write_file(path.join("symlink-file")?).await?;
1773 write_file(path.join("symlink-file")?).await?;
1774
1775 let write_dir = |f| {
1776 fs.write_link(
1777 f,
1778 WriteLinkContent {
1779 target: WriteLinkTarget::Relative(target.clone()),
1780 target_type: WriteLinkTargetType::DirectoryOrJunctionPoint,
1781 }
1782 .cell(),
1783 )
1784 };
1785 write_dir(path.join("symlink-dir")?).await?;
1787 write_dir(path.join("symlink-dir")?).await?;
1788
1789 Ok(())
1790 }
1791
1792 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1793 async fn test_write_link() {
1794 let scratch = tempfile::tempdir().unwrap();
1795 let path = scratch.path().to_owned();
1796
1797 create_dir_all(path.join("subdir-a")).unwrap();
1798 File::create_new(path.join("subdir-a/data.txt"))
1799 .unwrap()
1800 .write_all(b"foo")
1801 .unwrap();
1802 create_dir_all(path.join("subdir-b")).unwrap();
1803 File::create_new(path.join("subdir-b/data.txt"))
1804 .unwrap()
1805 .write_all(b"bar")
1806 .unwrap();
1807 let root = path.to_str().unwrap().into();
1808
1809 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1810 BackendOptions::default(),
1811 noop_backing_storage(),
1812 ));
1813
1814 tt.run_once(async move {
1815 let fs = disk_file_system_operation(root)
1816 .resolve()
1817 .strongly_consistent()
1818 .await?;
1819 let root_path = disk_file_system_root(fs);
1820
1821 read_strongly_consistent_and_apply_effects(
1822 extract_effects_operation(test_write_link_effect_operation(
1823 fs,
1824 root_path.clone(),
1825 rcstr!("subdir-a"),
1826 )),
1827 |e| e,
1828 )
1829 .await?;
1830
1831 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
1832 assert_eq!(
1833 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
1834 "foo"
1835 );
1836
1837 read_strongly_consistent_and_apply_effects(
1839 extract_effects_operation(test_write_link_effect_operation(
1840 fs,
1841 root_path,
1842 rcstr!("subdir-b"),
1843 )),
1844 |e| e,
1845 )
1846 .await?;
1847
1848 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
1849 assert_eq!(
1850 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
1851 "bar"
1852 );
1853
1854 anyhow::Ok(())
1855 })
1856 .await
1857 .unwrap();
1858 }
1859
1860 #[turbo_tasks::function(operation, root)]
1864 async fn assert_read_relative_symlink_operation(
1865 fs: ResolvedVc<DiskFileSystem>,
1866 root_path: FileSystemPath,
1867 ) -> anyhow::Result<()> {
1868 let sibling = fs.read_link(root_path.join("sub/link-sibling")?).await?;
1870 assert_eq!(
1871 *sibling,
1872 LinkContent::Link {
1873 target: LinkTarget::Relative {
1874 raw: rcstr!("foo.txt"),
1875 resolved: root_path.join("sub/foo.txt")?,
1876 },
1877 }
1878 );
1879
1880 let parent = fs.read_link(root_path.join("sub/link-parent")?).await?;
1882 assert_eq!(
1883 *parent,
1884 LinkContent::Link {
1885 target: LinkTarget::Relative {
1886 raw: rcstr!("../root.txt"),
1887 resolved: root_path.join("root.txt")?,
1888 },
1889 }
1890 );
1891
1892 Ok(())
1893 }
1894
1895 #[cfg(all(unix, debug_assertions))]
1896 #[turbo_tasks::function(operation, root)]
1897 async fn assert_read_realpath_operation(root_path: FileSystemPath) -> anyhow::Result<()> {
1898 let unresolved_dir = root_path.join("alias/child")?;
1899 let resolved_dir = unresolved_dir
1900 .realpath()
1901 .await?
1902 .expect("the linked directory should resolve");
1903
1904 assert_ne!(unresolved_dir, resolved_dir);
1905 let error = unresolved_dir
1906 .read_dir()
1907 .await
1908 .expect_err("a directory read through a symlinked parent must be rejected");
1909 let message = format!("{error:#}");
1910 assert!(message.contains("alias/child"));
1911 assert!(message.contains("real/child"));
1912 assert!(matches!(
1913 &*resolved_dir.read_dir().await?,
1914 DirectoryContent::Entries(entries) if entries.contains_key(&rcstr!("data.txt"))
1915 ));
1916
1917 assert!(matches!(
1918 &*root_path.join("file-alias")?.raw_read_dir().await?,
1919 RawDirectoryContent::NotFound
1920 ));
1921
1922 let unresolved_file = unresolved_dir.join("data.txt")?;
1923 let resolved_file = unresolved_file
1924 .realpath()
1925 .await?
1926 .expect("the linked file should resolve");
1927 assert_ne!(unresolved_file, resolved_file);
1928 let error = unresolved_file
1929 .read()
1930 .await
1931 .expect_err("a file read through a symlinked parent must be rejected");
1932 let message = format!("{error:#}");
1933 assert!(message.contains("alias/child/data.txt"));
1934 assert!(message.contains("real/child/data.txt"));
1935 assert!(matches!(
1936 &*resolved_file.read().await?,
1937 FileContent::Content(_)
1938 ));
1939
1940 Ok(())
1941 }
1942
1943 #[cfg(all(unix, debug_assertions))]
1944 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1945 async fn test_reads_require_realpath() {
1946 use std::os::unix::fs::symlink;
1947
1948 let scratch = tempfile::tempdir().unwrap();
1949 let path = scratch.path().to_owned();
1950 create_dir_all(path.join("real/child")).unwrap();
1951 File::create_new(path.join("real/child/data.txt")).unwrap();
1952 symlink("real", path.join("alias")).unwrap();
1953 symlink("real/child/data.txt", path.join("file-alias")).unwrap();
1954
1955 let root = canonicalize_to_rcstr(&path).unwrap();
1956 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1957 BackendOptions::default(),
1958 noop_backing_storage(),
1959 ));
1960
1961 tt.run_once(async move {
1962 let fs = disk_file_system_operation(root)
1963 .resolve()
1964 .strongly_consistent()
1965 .await?;
1966 let root_path = disk_file_system_root(fs);
1967 assert_read_realpath_operation(root_path)
1968 .read_strongly_consistent()
1969 .await?;
1970 anyhow::Ok(())
1971 })
1972 .await
1973 .unwrap();
1974 }
1975
1976 #[cfg(unix)]
1979 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1980 async fn test_dangling_symlink() {
1981 use std::os::unix::fs::symlink;
1982
1983 let scratch = tempfile::tempdir().unwrap();
1984 let path = scratch.path().to_owned();
1985 create_dir_all(path.join("sub")).unwrap();
1986 create_dir_all(path.join("target-dir")).unwrap();
1987 symlink("missing.txt", path.join("sub/link-dangling")).unwrap();
1988 symlink("link-dangling", path.join("sub/link-chain")).unwrap();
1989 symlink("../target-dir", path.join("sub/link-dir")).unwrap();
1990
1991 let root = canonicalize_to_rcstr(&path).unwrap();
1992
1993 #[turbo_tasks::function(operation, root)]
1994 async fn assert_operation(
1995 fs: ResolvedVc<DiskFileSystem>,
1996 root_path: FileSystemPath,
1997 ) -> anyhow::Result<()> {
1998 let link_path = root_path.join("sub/link-dangling")?;
1999
2000 let link = fs.read_link(link_path.clone()).await?;
2002 let LinkContent::Link { target } = &*link else {
2003 anyhow::bail!("expected a valid link, got {link:?}");
2004 };
2005 assert_eq!(
2006 *target,
2007 LinkTarget::Relative {
2008 raw: rcstr!("missing.txt"),
2009 resolved: root_path.join("sub/missing.txt")?,
2010 }
2011 );
2012 assert_eq!(target.target_type().await?, FileSystemEntryType::NotFound,);
2013
2014 let result = link_path.realpath_with_links().await?;
2016 assert!(matches!(
2017 result.path_result.as_ref().unwrap_err().kind(),
2018 RealPathErrorType::NotFound
2019 ));
2020
2021 let chain_path = root_path.join("sub/link-chain")?;
2023 let result = chain_path.realpath_with_links().await?;
2024 assert!(matches!(
2025 result.path_result.as_ref().unwrap_err().kind(),
2026 RealPathErrorType::NotFound
2027 ));
2028
2029 let missing_in_linked_dir = root_path.join("sub/link-dir/package.json")?;
2031 let result = missing_in_linked_dir.realpath_with_links().await?;
2032 assert!(matches!(
2033 result.path_result.as_ref().unwrap_err().kind(),
2034 RealPathErrorType::NotFound
2035 ));
2036
2037 let missing = root_path.join("sub/missing.txt")?;
2039 let result = missing.realpath_with_links().await?;
2040 assert!(matches!(
2041 result.path_result.as_ref().unwrap_err().kind(),
2042 RealPathErrorType::NotFound
2043 ));
2044
2045 Ok(())
2046 }
2047
2048 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2049 BackendOptions::default(),
2050 noop_backing_storage(),
2051 ));
2052
2053 tt.run_once(async move {
2054 let fs = disk_file_system_operation(root)
2055 .resolve()
2056 .strongly_consistent()
2057 .await?;
2058
2059 assert_operation(fs, disk_file_system_root(fs))
2060 .read_strongly_consistent()
2061 .await?;
2062
2063 anyhow::Ok(())
2064 })
2065 .await
2066 .unwrap();
2067
2068 tt.stop_and_wait().await;
2069 }
2070
2071 #[cfg(unix)]
2072 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2073 async fn test_link_target_resolved_type_through_chain() {
2074 use std::os::unix::fs::symlink;
2075
2076 let scratch = tempfile::tempdir().unwrap();
2077 let path = scratch.path().to_owned();
2078 create_dir_all(path.join("target-dir")).unwrap();
2079 File::create_new(path.join("target-file")).unwrap();
2080 symlink("target-dir", path.join("dir-inner")).unwrap();
2081 symlink("dir-inner", path.join("dir-outer")).unwrap();
2082 symlink("target-file", path.join("file-inner")).unwrap();
2083 symlink("file-inner", path.join("file-outer")).unwrap();
2084 symlink("../outside", path.join("invalid-inner")).unwrap();
2085 symlink("invalid-inner", path.join("invalid-outer")).unwrap();
2086
2087 let root = canonicalize_to_rcstr(&path).unwrap();
2088
2089 #[turbo_tasks::function(operation, root)]
2090 async fn assert_operation(
2091 fs: ResolvedVc<DiskFileSystem>,
2092 root_path: FileSystemPath,
2093 ) -> anyhow::Result<()> {
2094 for (input_path, expected_output) in [
2095 ("dir-outer", FileSystemEntryType::Directory),
2096 ("file-outer", FileSystemEntryType::File),
2097 ("invalid-outer", FileSystemEntryType::Error),
2098 ] {
2099 let link = fs.read_link(root_path.join(input_path)?).await?;
2100 let LinkContent::Link { target } = &*link else {
2101 anyhow::bail!("expected a valid link, got {link:?}");
2102 };
2103 assert_eq!(target.resolved_type().await?, expected_output);
2104 }
2105
2106 Ok(())
2107 }
2108
2109 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2110 BackendOptions::default(),
2111 noop_backing_storage(),
2112 ));
2113
2114 tt.run_once(async move {
2115 let fs = disk_file_system_operation(root)
2116 .resolve()
2117 .strongly_consistent()
2118 .await?;
2119
2120 assert_operation(fs, disk_file_system_root(fs))
2121 .read_strongly_consistent()
2122 .await?;
2123
2124 anyhow::Ok(())
2125 })
2126 .await
2127 .unwrap();
2128
2129 tt.stop_and_wait().await;
2130 }
2131
2132 #[cfg(unix)]
2137 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2138 async fn test_read_escaping_relative_symlink() {
2139 use std::os::unix::fs::symlink;
2140
2141 let scratch = tempfile::tempdir().unwrap();
2142 let path = scratch.path().join("the-root");
2144 create_dir_all(path.join("sub")).unwrap();
2145 File::create_new(path.join("root.txt"))
2146 .unwrap()
2147 .write_all(b"root")
2148 .unwrap();
2149 symlink("../../the-root/root.txt", path.join("sub/link-reentrant")).unwrap();
2151 create_dir_all(scratch.path().join("sibling")).unwrap();
2153 File::create_new(scratch.path().join("sibling/root.txt"))
2154 .unwrap()
2155 .write_all(b"sibling")
2156 .unwrap();
2157 symlink("../../sibling/root.txt", path.join("sub/link-sideways")).unwrap();
2158 symlink("../root.txt", path.join("sub/link-inside")).unwrap();
2160 File::create_new(path.join("sub/a\\b.txt"))
2162 .unwrap()
2163 .write_all(b"backslash")
2164 .unwrap();
2165 symlink("a\\b.txt", path.join("sub/link-backslash")).unwrap();
2166
2167 let root = canonicalize_to_rcstr(&path).unwrap();
2168
2169 #[turbo_tasks::function(operation, root)]
2170 async fn assert_operation(
2171 fs: ResolvedVc<DiskFileSystem>,
2172 root_path: FileSystemPath,
2173 ) -> anyhow::Result<()> {
2174 let reentrant = fs.read_link(root_path.join("sub/link-reentrant")?).await?;
2178 assert!(matches!(
2179 &*reentrant,
2180 LinkContent::Invalid { reason }
2181 if reason == "the symlink target leaves the filesystem root"
2182 ));
2183
2184 let sideways = fs.read_link(root_path.join("sub/link-sideways")?).await?;
2187 assert!(matches!(
2188 &*sideways,
2189 LinkContent::Invalid{reason}
2190 if reason == "the symlink target leaves the filesystem root"
2191 ));
2192
2193 let backslash_path = root_path.join("sub/link-backslash")?;
2197 let backslash = fs.read_link(backslash_path.clone()).await?;
2198 assert_eq!(
2199 *backslash,
2200 LinkContent::Link {
2201 target: LinkTarget::Relative {
2202 raw: rcstr!("a\\b.txt"),
2203 resolved: root_path.join("sub/a\\b.txt")?,
2204 },
2205 }
2206 );
2207
2208 let inside_path = root_path.join("sub/link-inside")?;
2210 let inside = fs.read_link(inside_path.clone()).await?;
2211 let LinkContent::Link { target } = &*inside else {
2212 anyhow::bail!("expected a valid link, got {inside:?}");
2213 };
2214 assert_eq!(
2215 *target,
2216 LinkTarget::Relative {
2217 raw: rcstr!("../root.txt"),
2218 resolved: root_path.join("root.txt")?,
2219 }
2220 );
2221 assert_eq!(target.target_type().await?, FileSystemEntryType::File,);
2222
2223 Ok(())
2224 }
2225
2226 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2227 BackendOptions::default(),
2228 noop_backing_storage(),
2229 ));
2230 tt.run_once(async move {
2231 let fs = disk_file_system_operation(root)
2232 .resolve()
2233 .strongly_consistent()
2234 .await?;
2235 let root_path = disk_file_system_root(fs);
2236
2237 assert_operation(fs, root_path)
2238 .read_strongly_consistent()
2239 .await?;
2240
2241 anyhow::Ok(())
2242 })
2243 .await
2244 .unwrap();
2245
2246 tt.stop_and_wait().await;
2247 }
2248
2249 #[cfg(unix)]
2250 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2251 async fn test_read_relative_symlink() {
2252 use std::os::unix::fs::symlink;
2253
2254 let scratch = tempfile::tempdir().unwrap();
2255 let path = scratch.path().to_owned();
2256
2257 create_dir_all(path.join("sub")).unwrap();
2262 File::create_new(path.join("root.txt"))
2263 .unwrap()
2264 .write_all(b"root")
2265 .unwrap();
2266 File::create_new(path.join("sub/foo.txt"))
2267 .unwrap()
2268 .write_all(b"foo")
2269 .unwrap();
2270 symlink("foo.txt", path.join("sub/link-sibling")).unwrap();
2271 symlink("../root.txt", path.join("sub/link-parent")).unwrap();
2272
2273 let root: RcStr = path.to_str().unwrap().into();
2274
2275 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2276 BackendOptions::default(),
2277 noop_backing_storage(),
2278 ));
2279
2280 tt.run_once(async move {
2281 let fs = disk_file_system_operation(root)
2282 .resolve()
2283 .strongly_consistent()
2284 .await?;
2285 let root_path = disk_file_system_root(fs);
2286
2287 assert_read_relative_symlink_operation(fs, root_path)
2288 .read_strongly_consistent()
2289 .await?;
2290
2291 anyhow::Ok(())
2292 })
2293 .await
2294 .unwrap();
2295 }
2296
2297 #[cfg(unix)]
2298 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2299 async fn test_read_absolute_symlink_slow_path() {
2300 use std::os::unix::fs::symlink;
2301
2302 let scratch = tempfile::tempdir().unwrap();
2303 let path = scratch.path().to_owned();
2304
2305 let real_root = path.join("real-root");
2311 create_dir_all(&real_root).unwrap();
2312 File::create_new(real_root.join("foo.txt"))
2313 .unwrap()
2314 .write_all(b"foo")
2315 .unwrap();
2316 File::create_new(path.join("outside.txt"))
2317 .unwrap()
2318 .write_all(b"outside")
2319 .unwrap();
2320 symlink(&real_root, path.join("alias")).unwrap();
2321 symlink(path.join("alias/foo.txt"), real_root.join("link-via-alias")).unwrap();
2322 symlink(path.join("outside.txt"), real_root.join("link-outside")).unwrap();
2323
2324 let root = canonicalize_to_rcstr(&real_root).unwrap();
2328
2329 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2330 BackendOptions::default(),
2331 noop_backing_storage(),
2332 ));
2333
2334 #[turbo_tasks::function(operation, root)]
2339 async fn assert_read_absolute_symlink_slow_path_operation(
2340 fs: ResolvedVc<DiskFileSystem>,
2341 root_path: FileSystemPath,
2342 ) -> anyhow::Result<()> {
2343 let via_alias = fs.read_link(root_path.join("link-via-alias")?).await?;
2345 assert_eq!(
2346 *via_alias,
2347 LinkContent::Link {
2348 target: LinkTarget::Absolute {
2349 resolved: root_path.join("foo.txt")?,
2350 },
2351 }
2352 );
2353
2354 let outside = fs.read_link(root_path.join("link-outside")?).await?;
2356 assert!(matches!(
2357 &*outside,
2358 LinkContent::Invalid { reason}
2359 if reason.contains("leaves the filesystem root")
2360 ));
2361
2362 Ok(())
2363 }
2364
2365 tt.run_once(async move {
2366 let fs = disk_file_system_operation(root)
2367 .resolve()
2368 .strongly_consistent()
2369 .await?;
2370 let root_path = disk_file_system_root(fs);
2371
2372 assert_read_absolute_symlink_slow_path_operation(fs, root_path)
2373 .read_strongly_consistent()
2374 .await?;
2375
2376 anyhow::Ok(())
2377 })
2378 .await
2379 .unwrap();
2380 }
2381
2382 const STRESS_ITERATIONS: usize = 100;
2383 const STRESS_PARALLELISM: usize = 8;
2384 const STRESS_TARGET_COUNT: usize = 20;
2385 const STRESS_SYMLINK_COUNT: usize = 16;
2386
2387 #[turbo_tasks::function(operation, root)]
2388 fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
2389 DiskFileSystem::new(rcstr!("test"), Vc::cell(fs_root))
2390 }
2391
2392 fn disk_file_system_root(fs: ResolvedVc<DiskFileSystem>) -> FileSystemPath {
2393 FileSystemPath {
2394 fs: ResolvedVc::upcast(fs),
2395 path: rcstr!(""),
2396 }
2397 }
2398
2399 #[turbo_tasks::function(operation, root)]
2400 async fn write_symlink_stress_batch(
2401 fs: ResolvedVc<DiskFileSystem>,
2402 symlinks_dir: FileSystemPath,
2403 updates: Vec<(usize, usize)>,
2404 ) -> anyhow::Result<()> {
2405 use turbo_tasks::TryJoinIterExt;
2406
2407 updates
2408 .into_iter()
2409 .map(|(symlink_idx, target_idx)| {
2410 let target = RcStr::from(format!("../_targets/{target_idx}"));
2411 let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
2412 async move {
2413 fs.write_link(
2414 symlink_path,
2415 WriteLinkContent {
2416 target: WriteLinkTarget::Relative(target),
2417 target_type: WriteLinkTargetType::DirectoryOrJunctionPoint,
2418 }
2419 .cell(),
2420 )
2421 .await
2422 }
2423 })
2424 .try_join()
2425 .await?;
2426 Ok(())
2427 }
2428
2429 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2430 async fn test_symlink_stress() {
2431 let scratch = tempfile::tempdir().unwrap();
2432 let path = scratch.path().to_owned();
2433
2434 let targets_dir = path.join("_targets");
2435 create_dir_all(&targets_dir).unwrap();
2436 for i in 0..STRESS_TARGET_COUNT {
2437 create_dir_all(targets_dir.join(i.to_string())).unwrap();
2438 }
2439 create_dir_all(path.join("_symlinks")).unwrap();
2440
2441 let root = RcStr::from(path.to_str().unwrap());
2442
2443 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2444 BackendOptions::default(),
2445 noop_backing_storage(),
2446 ));
2447
2448 tt.run_once(async move {
2449 let fs = disk_file_system_operation(root)
2450 .resolve()
2451 .strongly_consistent()
2452 .await?;
2453 let root_path = disk_file_system_root(fs);
2454 let symlinks_dir = root_path.join("_symlinks")?;
2455
2456 let initial_updates: Vec<(usize, usize)> =
2457 (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
2458 read_strongly_consistent_and_apply_effects(
2459 extract_effects_operation(write_symlink_stress_batch(
2460 fs,
2461 symlinks_dir.clone(),
2462 initial_updates,
2463 )),
2464 |e| e,
2465 )
2466 .await?;
2467
2468 let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
2469 for _ in 0..STRESS_ITERATIONS {
2470 let mut updates_map = rustc_hash::FxHashMap::default();
2471 for _ in 0..STRESS_PARALLELISM {
2472 let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
2473 let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
2474 updates_map.insert(symlink_idx, target_idx);
2475 }
2476 let updates: Vec<(usize, usize)> = updates_map.into_iter().collect();
2477
2478 read_strongly_consistent_and_apply_effects(
2479 extract_effects_operation(write_symlink_stress_batch(
2480 fs,
2481 symlinks_dir.clone(),
2482 updates,
2483 )),
2484 |e| e,
2485 )
2486 .await?;
2487 }
2488
2489 anyhow::Ok(())
2490 })
2491 .await
2492 .unwrap();
2493
2494 tt.stop_and_wait().await;
2495 }
2496 }
2497
2498 #[cfg(test)]
2500 mod denied_path_tests {
2501 use std::{
2502 fs::{File, create_dir_all, read_to_string},
2503 io::Write,
2504 path::Path,
2505 };
2506
2507 use turbo_rcstr::{RcStr, rcstr};
2508 use turbo_tasks::{Effects, Vc, read_strongly_consistent_and_apply_effects, take_effects};
2509 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2510
2511 use crate::{
2512 DirectoryContent, DiskFileSystem, DiskWatcherConfig, File as TurboFile, FileContent,
2513 FileSystem, FileSystemPath,
2514 glob::{Glob, GlobOptions},
2515 };
2516
2517 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
2520 let scratch = tempfile::tempdir().unwrap();
2521 let path = scratch.path();
2522
2523 File::create_new(path.join("allowed_file.txt"))
2530 .unwrap()
2531 .write_all(b"allowed content")
2532 .unwrap();
2533
2534 create_dir_all(path.join("allowed_dir")).unwrap();
2535 File::create_new(path.join("allowed_dir/file.txt"))
2536 .unwrap()
2537 .write_all(b"allowed dir content")
2538 .unwrap();
2539
2540 File::create_new(path.join("other_file.txt"))
2541 .unwrap()
2542 .write_all(b"other content")
2543 .unwrap();
2544
2545 create_dir_all(path.join("denied_dir/nested")).unwrap();
2546 File::create_new(path.join("denied_dir/secret.txt"))
2547 .unwrap()
2548 .write_all(b"secret content")
2549 .unwrap();
2550 File::create_new(path.join("denied_dir/nested/deep.txt"))
2551 .unwrap()
2552 .write_all(b"deep secret")
2553 .unwrap();
2554
2555 let root = RcStr::from(path.to_str().unwrap());
2556 let denied_path = rcstr!("denied_dir");
2558
2559 (scratch, root, denied_path)
2560 }
2561
2562 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2563 async fn test_denied_path_read() {
2564 #[turbo_tasks::function(operation, root)]
2565 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2566 let fs = DiskFileSystem::new_with_options(
2567 rcstr!("test"),
2568 Vc::cell(root),
2569 vec![denied_path],
2570 DiskWatcherConfig::default(),
2571 );
2572 let root_path = fs.root().await?;
2573
2574 let allowed_file = root_path.join("allowed_file.txt")?;
2576 let content = allowed_file.read().await?;
2577 assert!(
2578 matches!(&*content, FileContent::Content(_)),
2579 "allowed file should be readable"
2580 );
2581
2582 let denied_file = root_path.join("denied_dir/secret.txt")?;
2584 let content = denied_file.read().await?;
2585 assert!(
2586 matches!(&*content, FileContent::NotFound),
2587 "denied file should return NotFound, got {:?}",
2588 content
2589 );
2590
2591 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
2593 let content = nested_denied.read().await?;
2594 assert!(
2595 matches!(&*content, FileContent::NotFound),
2596 "nested denied file should return NotFound"
2597 );
2598
2599 let denied_dir = root_path.join("denied_dir")?;
2601 let content = denied_dir.read().await?;
2602 assert!(
2603 matches!(&*content, FileContent::NotFound),
2604 "denied directory should return NotFound"
2605 );
2606
2607 Ok(())
2608 }
2609
2610 let (_scratch, root, denied_path) = setup_test_fs();
2611 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2612 BackendOptions::default(),
2613 noop_backing_storage(),
2614 ));
2615 tt.run_once(async {
2616 test_operation(root, denied_path)
2617 .read_strongly_consistent()
2618 .await?;
2619
2620 anyhow::Ok(())
2621 })
2622 .await
2623 .unwrap();
2624 }
2625
2626 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2627 async fn test_denied_path_read_dir() {
2628 #[turbo_tasks::function(operation, root)]
2629 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2630 let fs = DiskFileSystem::new_with_options(
2631 rcstr!("test"),
2632 Vc::cell(root),
2633 vec![denied_path],
2634 DiskWatcherConfig::default(),
2635 );
2636 let root_path = fs.root().await?;
2637
2638 let dir_content = root_path.read_dir().await?;
2640 match &*dir_content {
2641 DirectoryContent::Entries(entries) => {
2642 assert!(
2643 entries.contains_key(&rcstr!("allowed_dir")),
2644 "allowed_dir should be visible"
2645 );
2646 assert!(
2647 entries.contains_key(&rcstr!("other_file.txt")),
2648 "other_file.txt should be visible"
2649 );
2650 assert!(
2651 entries.contains_key(&rcstr!("allowed_file.txt")),
2652 "allowed_file.txt should be visible"
2653 );
2654 assert!(
2655 !entries.contains_key(&rcstr!("denied_dir")),
2656 "denied_dir should NOT be visible in read_dir"
2657 );
2658 }
2659 DirectoryContent::NotFound => panic!("root directory should exist"),
2660 }
2661
2662 let denied_dir = root_path.join("denied_dir")?;
2664 let dir_content = denied_dir.read_dir().await?;
2665 assert!(
2666 matches!(&*dir_content, DirectoryContent::NotFound),
2667 "denied_dir read_dir should return NotFound"
2668 );
2669
2670 Ok(())
2671 }
2672
2673 let (_scratch, root, denied_path) = setup_test_fs();
2674 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2675 BackendOptions::default(),
2676 noop_backing_storage(),
2677 ));
2678 tt.run_once(async {
2679 test_operation(root, denied_path)
2680 .read_strongly_consistent()
2681 .await?;
2682
2683 anyhow::Ok(())
2684 })
2685 .await
2686 .unwrap();
2687 }
2688
2689 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2690 async fn test_denied_path_read_glob() {
2691 #[turbo_tasks::function(operation, root)]
2692 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
2693 let fs = DiskFileSystem::new_with_options(
2694 rcstr!("test"),
2695 Vc::cell(root),
2696 vec![denied_path],
2697 DiskWatcherConfig::default(),
2698 );
2699 let root_path = fs.root().await?;
2700
2701 let glob_result = root_path
2703 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
2704 .await?;
2705
2706 assert!(
2708 glob_result.results.contains_key("allowed_file.txt"),
2709 "allowed_file.txt should be found"
2710 );
2711 assert!(
2712 glob_result.results.contains_key("other_file.txt"),
2713 "other_file.txt should be found"
2714 );
2715 assert!(
2716 !glob_result.results.contains_key("denied_dir"),
2717 "denied_dir should NOT appear in glob results"
2718 );
2719
2720 assert!(
2722 !glob_result.inner.contains_key("denied_dir"),
2723 "denied_dir should NOT appear in glob inner results"
2724 );
2725
2726 assert!(
2728 glob_result.inner.contains_key("allowed_dir"),
2729 "allowed_dir directory should be present"
2730 );
2731 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
2732 assert!(
2733 sub_inner.results.contains_key("file.txt"),
2734 "allowed_dir/file.txt should be found"
2735 );
2736
2737 Ok(())
2738 }
2739
2740 let (_scratch, root, denied_path) = setup_test_fs();
2741 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2742 BackendOptions::default(),
2743 noop_backing_storage(),
2744 ));
2745 tt.run_once(async {
2746 test_operation(root, denied_path)
2747 .read_strongly_consistent()
2748 .await?;
2749
2750 anyhow::Ok(())
2751 })
2752 .await
2753 .unwrap();
2754 }
2755
2756 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2757 async fn test_denied_path_write() {
2758 #[turbo_tasks::function(operation, root)]
2759 async fn write_file_operation(
2760 path: FileSystemPath,
2761 contents: RcStr,
2762 ) -> anyhow::Result<()> {
2763 path.write(
2764 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
2765 .cell(),
2766 )
2767 .await?;
2768 Ok(())
2769 }
2770
2771 #[turbo_tasks::function(operation, root)]
2774 async fn write_allowed_file_operation(
2775 root: RcStr,
2776 denied_path: RcStr,
2777 file_path: RcStr,
2778 contents: RcStr,
2779 ) -> anyhow::Result<Vc<Effects>> {
2780 let fs = DiskFileSystem::new_with_options(
2781 rcstr!("test"),
2782 Vc::cell(root),
2783 vec![denied_path],
2784 DiskWatcherConfig::default(),
2785 );
2786 let root_path = fs.root().await?;
2787 let allowed_file = root_path.join(&file_path)?;
2788 let write_op = write_file_operation(allowed_file, contents);
2789 write_op.read_strongly_consistent().await?;
2790 Ok(take_effects(write_op).await?.cell())
2791 }
2792
2793 #[turbo_tasks::function(operation, root)]
2794 async fn test_denied_writes_operation(
2795 root: RcStr,
2796 denied_path: RcStr,
2797 denied_file: RcStr,
2798 nested_denied_file: RcStr,
2799 ) -> anyhow::Result<()> {
2800 let fs = DiskFileSystem::new_with_options(
2801 rcstr!("test"),
2802 Vc::cell(root),
2803 vec![denied_path],
2804 DiskWatcherConfig::default(),
2805 );
2806 let root_path = fs.root().await?;
2807
2808 let path = root_path.join(&denied_file)?;
2809 let result = write_file_operation(path, rcstr!("forbidden"))
2810 .read_strongly_consistent()
2811 .await;
2812 assert!(
2813 result.is_err(),
2814 "writing to denied path should return an error"
2815 );
2816
2817 let path = root_path.join(&nested_denied_file)?;
2818 let result = write_file_operation(path, rcstr!("nested"))
2819 .read_strongly_consistent()
2820 .await;
2821 assert!(
2822 result.is_err(),
2823 "writing to nested denied path should return an error"
2824 );
2825
2826 Ok(())
2827 }
2828
2829 let (_scratch, root, denied_path) = setup_test_fs();
2830 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2831 BackendOptions::default(),
2832 noop_backing_storage(),
2833 ));
2834 tt.run_once(async {
2835 const ALLOWED_FILE: &str = "allowed_dir/new_file.txt";
2836 const TEST_CONTENT: &str = "test content";
2837
2838 let effects_op = write_allowed_file_operation(
2840 root.clone(),
2841 denied_path.clone(),
2842 RcStr::from(ALLOWED_FILE),
2843 RcStr::from(TEST_CONTENT),
2844 );
2845 read_strongly_consistent_and_apply_effects(effects_op, |e| e).await?;
2846
2847 let content = read_to_string(Path::new(root.as_str()).join(ALLOWED_FILE))?;
2849 assert_eq!(content, TEST_CONTENT, "allowed file write should succeed");
2850
2851 test_denied_writes_operation(
2853 root,
2854 denied_path,
2855 RcStr::from("denied_dir/forbidden.txt"),
2856 RcStr::from("denied_dir/nested/file.txt"),
2857 )
2858 .read_strongly_consistent()
2859 .await?;
2860
2861 anyhow::Ok(())
2862 })
2863 .await
2864 .unwrap();
2865 }
2866 }
2867}