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