1#![feature(arbitrary_self_types)]
2#![feature(arbitrary_self_types_pointers)]
3#![feature(btree_cursors)] #![feature(io_error_more)]
5#![feature(min_specialization)]
6#![feature(normalize_lexically)]
9#![feature(trivial_bounds)]
10#![cfg_attr(windows, feature(junction_point))]
13#![allow(clippy::needless_return)] #![allow(clippy::mutable_key_type)]
15
16pub mod attach;
17pub mod embed;
18pub mod glob;
19mod globset;
20pub mod invalidation;
21mod invalidator_map;
22pub mod json;
23mod mutex_map;
24mod path_map;
25mod read_glob;
26mod retry;
27pub mod rope;
28pub mod source_context;
29pub mod util;
30pub(crate) mod virtual_fs;
31mod watcher;
32
33use std::{
34 borrow::Cow,
35 cmp::{Ordering, min},
36 env,
37 error::Error as StdError,
38 fmt::{self, Debug, Formatter},
39 fs::FileType,
40 future::Future,
41 io::{self, BufRead, BufReader, ErrorKind, Read, Write as _},
42 mem::take,
43 path::{MAIN_SEPARATOR, Path, PathBuf},
44 sync::{Arc, LazyLock, Weak},
45 time::Duration,
46};
47
48use anyhow::{Context, Result, anyhow, bail};
49use auto_hash_map::{AutoMap, AutoSet};
50use bincode::{Decode, Encode};
51use bitflags::bitflags;
52use dunce::simplified;
53use indexmap::IndexSet;
54use jsonc_parser::{ParseOptions, parse_to_serde_value};
55use mime::Mime;
56use rustc_hash::FxHashSet;
57use serde_json::Value;
58use tokio::{
59 runtime::Handle,
60 sync::{RwLock, RwLockReadGuard},
61};
62use tracing::Instrument;
63use turbo_rcstr::{RcStr, rcstr};
64use turbo_tasks::{
65 Completion, Effect, EffectStateStorage, InvalidationReason, NonLocalValue, ReadRef, ResolvedVc,
66 TaskInput, TurboTasksApi, ValueToString, ValueToStringRef, Vc, debug::ValueDebugFormat,
67 emit_effect, mark_session_dependent, parallel, trace::TraceRawVcs, turbo_tasks_weak, turbobail,
68 turbofmt,
69};
70use turbo_tasks_hash::{
71 DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64,
72 hash_xxh3_hash128,
73};
74use turbo_unix_path::{
75 get_parent_path, get_relative_path_to, join_path, normalize_path, sys_to_unix, unix_to_sys,
76};
77
78use crate::{
79 attach::AttachedFileSystem,
80 glob::Glob,
81 invalidation::Write,
82 invalidator_map::InvalidatorMap,
83 json::UnparsableJson,
84 mutex_map::MutexMap,
85 path_map::OrderedPathMapExt,
86 read_glob::{read_glob, track_glob},
87 retry::{can_retry, retry_blocking, retry_blocking_custom},
88 rope::{Rope, RopeReader},
89 util::extract_disk_access,
90 watcher::DiskWatcher,
91};
92pub use crate::{read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem};
93
94pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
119 fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
122 if cfg!(windows) {
123 const MAX_PATH_LENGTH_WINDOWS: usize = 260;
124 const UNC_PREFIX: &str = "\\\\?\\";
125
126 if path.starts_with(UNC_PREFIX) {
127 return Ok(path.into());
128 }
129
130 if path.as_os_str().len() > MAX_PATH_LENGTH_WINDOWS {
131 let new_path = std::fs::canonicalize(path).map_err(|err| {
132 anyhow!(err).context("file is too long, and could not be normalized")
133 })?;
134 return Ok(new_path.into());
135 }
136
137 Ok(path.into())
138 } else {
139 const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
143 const MAX_PATH_LENGTH: usize = 1024 - 8;
147
148 if path
150 .file_name()
151 .map(|n| n.as_encoded_bytes().len())
152 .unwrap_or(0)
153 > MAX_FILE_NAME_LENGTH_UNIX
154 {
155 anyhow::bail!(
156 "file name is too long (exceeds {} bytes)",
157 MAX_FILE_NAME_LENGTH_UNIX,
158 );
159 }
160
161 if path.as_os_str().len() > MAX_PATH_LENGTH {
162 anyhow::bail!("path is too long (exceeds {MAX_PATH_LENGTH} bytes)");
163 }
164
165 Ok(path.into())
166 }
167 }
168
169 validate_path_length_inner(path)
170 .with_context(|| format!("path length for file {path:?} exceeds max length of filesystem"))
171}
172
173trait ConcurrencyLimitedExt {
174 type Output;
175 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
176}
177
178impl<F, R> ConcurrencyLimitedExt for F
179where
180 F: Future<Output = R>,
181{
182 type Output = R;
183 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
184 let _permit = semaphore.acquire().await;
185 self.await
186 }
187}
188
189fn number_env_var(name: &'static str) -> Option<usize> {
190 env::var(name)
191 .ok()
192 .filter(|val| !val.is_empty())
193 .map(|val| match val.parse() {
194 Ok(n) => n,
195 Err(err) => panic!("{name} must be a valid integer: {err}"),
196 })
197 .filter(|val| *val != 0)
198}
199
200fn create_read_semaphore() -> tokio::sync::Semaphore {
201 static TURBO_ENGINE_READ_CONCURRENCY: LazyLock<usize> =
204 LazyLock::new(|| number_env_var("TURBO_ENGINE_READ_CONCURRENCY").unwrap_or(64));
205 tokio::sync::Semaphore::new(*TURBO_ENGINE_READ_CONCURRENCY)
206}
207
208fn create_write_semaphore() -> tokio::sync::Semaphore {
209 static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
212 number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
213 4,
216 )
217 });
218 tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
219}
220
221#[turbo_tasks::value_trait]
222pub trait FileSystem: ValueToString {
223 #[turbo_tasks::function]
225 fn root(self: ResolvedVc<Self>) -> Vc<FileSystemPath> {
226 FileSystemPath::new_normalized(self, RcStr::default()).cell()
227 }
228 #[turbo_tasks::function]
229 fn read(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileContent>;
230 #[turbo_tasks::function]
231 fn read_link(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<LinkContent>;
232 #[turbo_tasks::function]
233 fn raw_read_dir(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<RawDirectoryContent>;
234 #[turbo_tasks::function]
235 fn write(self: Vc<Self>, fs_path: FileSystemPath, content: Vc<FileContent>) -> Vc<()>;
236 #[turbo_tasks::function]
238 fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
239 #[turbo_tasks::function]
240 fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
241}
242
243#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
244struct DiskFileSystemInner {
245 pub name: RcStr,
246 pub root: RcStr,
247 #[turbo_tasks(debug_ignore, trace_ignore)]
248 #[bincode(skip)]
249 mutex_map: MutexMap<PathBuf>,
250 #[turbo_tasks(debug_ignore, trace_ignore)]
251 #[bincode(skip)]
252 invalidator_map: InvalidatorMap,
253 #[turbo_tasks(debug_ignore, trace_ignore)]
254 #[bincode(skip)]
255 dir_invalidator_map: InvalidatorMap,
256 #[turbo_tasks(debug_ignore, trace_ignore)]
259 #[bincode(skip)]
260 invalidation_lock: RwLock<()>,
261 #[turbo_tasks(debug_ignore, trace_ignore)]
263 #[bincode(skip, default = "create_read_semaphore")]
264 read_semaphore: tokio::sync::Semaphore,
265 #[turbo_tasks(debug_ignore, trace_ignore)]
267 #[bincode(skip, default = "create_write_semaphore")]
268 write_semaphore: tokio::sync::Semaphore,
269
270 #[turbo_tasks(debug_ignore, trace_ignore)]
271 watcher: DiskWatcher,
272 denied_paths: Vec<RcStr>,
275 #[turbo_tasks(debug_ignore, trace_ignore)]
278 #[bincode(skip, default = "turbo_tasks_weak")]
279 turbo_tasks: Weak<dyn TurboTasksApi>,
280 #[turbo_tasks(debug_ignore, trace_ignore)]
282 #[bincode(skip, default = "Handle::current")]
283 tokio_handle: Handle,
284 #[turbo_tasks(debug_ignore, trace_ignore)]
285 #[bincode(skip)]
286 effect_state_storage: EffectStateStorage,
287}
288
289impl DiskFileSystemInner {
290 fn root_path(&self) -> &Path {
292 simplified(Path::new(&*self.root))
294 }
295
296 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
306 let path = &path.path;
307 self.denied_paths.iter().any(|denied_path| {
308 path.starts_with(denied_path.as_str())
309 && (path.len() == denied_path.len()
310 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
311 })
312 }
313
314 async fn register_read_invalidator(&self, path: &Path) -> Result<()> {
317 if let Some(invalidator) = turbo_tasks::get_invalidator() {
318 self.invalidator_map.insert(path.to_owned(), invalidator);
319 self.watcher
320 .ensure_watched_file(path, self.root_path())
321 .await?;
322 }
323 Ok(())
324 }
325
326 fn invalidate_from_write(&self, full_path: &Path) {
330 let mut invalidator_map = self.invalidator_map.lock().unwrap();
331 if let Some(invalidators) = invalidator_map.remove(full_path) {
332 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
333 return;
334 };
335 let _guard = self.tokio_handle.enter();
336 let reason = Write {
337 path: full_path.to_string_lossy().into_owned(),
338 };
339 for invalidator in invalidators {
340 invalidator.invalidate_with_reason(&*turbo_tasks, reason.clone());
341 }
342 }
343 }
344
345 async fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
348 if let Some(invalidator) = turbo_tasks::get_invalidator() {
349 self.dir_invalidator_map
350 .insert(path.to_owned(), invalidator);
351 self.watcher
352 .ensure_watched_dir(path, self.root_path())
353 .await?;
354 }
355 Ok(())
356 }
357
358 async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
359 let lock1 = self.invalidation_lock.read().await;
360 let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
361 PathLockGuard(lock1, lock2)
362 }
363
364 fn invalidate(&self) {
365 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
366 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
367 return;
368 };
369 let _guard = self.tokio_handle.enter();
370
371 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
372 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
373 let invalidators = invalidator_map
374 .into_iter()
375 .chain(dir_invalidator_map)
376 .flat_map(|(_, invalidators)| invalidators.into_iter())
377 .collect::<Vec<_>>();
378 parallel::for_each_owned(invalidators, |invalidator| {
379 invalidator.invalidate(&*turbo_tasks)
380 });
381 }
382
383 fn invalidate_with_reason<R: InvalidationReason + Clone>(
387 &self,
388 reason: impl Fn(&Path) -> R + Sync,
389 ) {
390 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
391 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
392 return;
393 };
394 let _guard = self.tokio_handle.enter();
395
396 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
397 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
398 let invalidators = invalidator_map
399 .into_iter()
400 .chain(dir_invalidator_map)
401 .flat_map(|(path, invalidators)| {
402 let reason_for_path = reason(&path);
403 invalidators
404 .into_iter()
405 .map(move |i| (reason_for_path.clone(), i))
406 })
407 .collect::<Vec<_>>();
408 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
409 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
410 });
411 }
412
413 fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
417 &self,
418 paths: impl IntoIterator<Item = PathBuf>,
419 reason: impl Fn(&Path) -> R + Sync,
420 ) {
421 let _span =
422 tracing::info_span!("invalidate filesystem paths", name = &*self.root).entered();
423 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
424 return;
425 };
426 let _guard = self.tokio_handle.enter();
427
428 let mut invalidator_map = self.invalidator_map.lock().unwrap();
429 let mut dir_invalidator_map = self.dir_invalidator_map.lock().unwrap();
430 let mut invalidators = Vec::new();
431 let mut parent_dirs_to_invalidate = FxHashSet::default();
432
433 for path in paths {
434 let mut current_parent = path.parent();
435 while let Some(parent) = current_parent {
436 parent_dirs_to_invalidate.insert(parent.to_path_buf());
437 current_parent = parent.parent();
438 }
439
440 for (invalidated_path, path_invalidators) in
441 invalidator_map.extract_path_with_children(&path)
442 {
443 let reason_for_path = reason(&invalidated_path);
444 invalidators.extend(
445 path_invalidators
446 .into_iter()
447 .map(|invalidator| (reason_for_path.clone(), invalidator)),
448 );
449 }
450
451 for (invalidated_path, path_invalidators) in
452 dir_invalidator_map.extract_path_with_children(&path)
453 {
454 let reason_for_path = reason(&invalidated_path);
455 invalidators.extend(
456 path_invalidators
457 .into_iter()
458 .map(|invalidator| (reason_for_path.clone(), invalidator)),
459 );
460 }
461 }
462
463 for path in parent_dirs_to_invalidate {
464 if let Some(path_invalidators) = dir_invalidator_map.remove(&path) {
465 let reason_for_path = reason(&path);
466 invalidators.extend(
467 path_invalidators
468 .into_iter()
469 .map(|invalidator| (reason_for_path.clone(), invalidator)),
470 );
471 }
472 }
473
474 drop(invalidator_map);
475 drop(dir_invalidator_map);
476
477 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
478 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
479 });
480 }
481
482 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
483 async fn start_watching_internal(
484 self: &Arc<Self>,
485 report_invalidation_reason: bool,
486 poll_interval: Option<Duration>,
487 ) -> Result<()> {
488 let root_path = self.root_path().to_path_buf();
489
490 retry_blocking(|| std::fs::create_dir_all(&root_path))
492 .instrument(tracing::info_span!("create root directory", name = ?root_path))
493 .concurrency_limited(&self.write_semaphore)
494 .await?;
495
496 self.watcher
497 .start_watching(self.clone(), report_invalidation_reason, poll_interval)
498 .await?;
499
500 Ok(())
501 }
502}
503
504#[derive(Clone, ValueToString)]
505#[value_to_string(self.inner.name)]
506#[turbo_tasks::value(cell = "new", eq = "manual")]
507pub struct DiskFileSystem {
508 inner: Arc<DiskFileSystemInner>,
509}
510
511impl DiskFileSystem {
512 pub fn name(&self) -> &RcStr {
513 &self.inner.name
514 }
515
516 pub fn root(&self) -> &RcStr {
517 &self.inner.root
518 }
519
520 pub fn invalidate(&self) {
521 self.inner.invalidate();
522 }
523
524 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
525 &self,
526 reason: impl Fn(&Path) -> R + Sync,
527 ) {
528 self.inner.invalidate_with_reason(reason);
529 }
530
531 pub fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
532 &self,
533 paths: impl IntoIterator<Item = PathBuf>,
534 reason: impl Fn(&Path) -> R + Sync,
535 ) {
536 self.inner
537 .invalidate_path_and_children_with_reason(paths, reason);
538 }
539
540 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
541 self.inner
542 .start_watching_internal(false, poll_interval)
543 .await
544 }
545
546 pub async fn start_watching_with_invalidation_reason(
547 &self,
548 poll_interval: Option<Duration>,
549 ) -> Result<()> {
550 self.inner
551 .start_watching_internal(true, poll_interval)
552 .await
553 }
554
555 pub async fn stop_watching(&self) {
556 self.inner.watcher.stop_watching().await;
557 }
558
559 pub fn try_from_sys_path(
572 &self,
573 vc_self: ResolvedVc<DiskFileSystem>,
574 sys_path: &Path,
575 relative_to: Option<&FileSystemPath>,
576 ) -> Option<FileSystemPath> {
577 let vc_self = ResolvedVc::upcast(vc_self);
578
579 let sys_path = simplified(sys_path);
580 let relative_sys_path = if sys_path.is_absolute() {
581 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
584 normalized_sys_path
585 .strip_prefix(self.inner.root_path())
586 .ok()?
587 .to_owned()
588 } else if let Some(relative_to) = relative_to {
589 debug_assert_eq!(
590 relative_to.fs, vc_self,
591 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
592 );
593 let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
594 joined_sys_path.push(sys_path);
595 joined_sys_path.normalize_lexically().ok()?
596 } else {
597 sys_path.normalize_lexically().ok()?
598 };
599
600 Some(FileSystemPath {
601 fs: vc_self,
602 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
603 })
604 }
605
606 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
607 let path = self.inner.root_path();
608 if fs_path.path.is_empty() {
609 path.to_path_buf()
610 } else {
611 path.join(&*unix_to_sys(&fs_path.path))
612 }
613 }
614}
615
616#[allow(dead_code, reason = "we need to hold onto the locks")]
617struct PathLockGuard<'a>(
618 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
619 #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
620);
621
622fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
623 if let Ok(rel_path) = path.strip_prefix(root_path) {
624 let path = if MAIN_SEPARATOR != '/' {
625 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
626 format!("[{name}]/{rel_path}")
627 } else {
628 format!("[{name}]/{}", rel_path.display())
629 };
630 Some(path)
631 } else {
632 None
633 }
634}
635
636impl DiskFileSystem {
637 pub fn new(name: RcStr, root: Vc<RcStr>) -> Vc<Self> {
644 Self::new_internal(name, root, Vec::new())
645 }
646
647 pub fn new_with_denied_paths(
656 name: RcStr,
657 root: Vc<RcStr>,
658 denied_paths: Vec<RcStr>,
659 ) -> Vc<Self> {
660 for denied_path in &denied_paths {
661 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
662 debug_assert!(
663 normalize_path(denied_path).as_deref() == Some(&**denied_path),
664 "denied_path must be normalized: {denied_path:?}"
665 );
666 }
667 Self::new_internal(name, root, denied_paths)
668 }
669}
670
671#[turbo_tasks::value_impl]
672impl DiskFileSystem {
673 #[turbo_tasks::function]
674 async fn new_internal(
675 name: RcStr,
676 root: Vc<RcStr>,
677 denied_paths: Vec<RcStr>,
678 ) -> Result<Vc<Self>> {
679 let root = root.owned().await?;
680 let instance = DiskFileSystem {
681 inner: Arc::new(DiskFileSystemInner {
682 name,
683 root,
684 mutex_map: Default::default(),
685 invalidation_lock: Default::default(),
686 invalidator_map: InvalidatorMap::new(),
687 dir_invalidator_map: InvalidatorMap::new(),
688 read_semaphore: create_read_semaphore(),
689 write_semaphore: create_write_semaphore(),
690 watcher: DiskWatcher::new(),
691 denied_paths,
692 turbo_tasks: turbo_tasks_weak(),
693 tokio_handle: Handle::current(),
694 effect_state_storage: EffectStateStorage::default(),
695 }),
696 };
697
698 Ok(Self::cell(instance))
699 }
700}
701
702impl Debug for DiskFileSystem {
703 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
704 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
705 }
706}
707
708#[turbo_tasks::value_impl]
709impl FileSystem for DiskFileSystem {
710 #[turbo_tasks::function(fs)]
711 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
712 mark_session_dependent();
713
714 if self.inner.is_path_denied(&fs_path) {
716 return Ok(FileContent::NotFound.cell());
717 }
718 let full_path = self.to_sys_path(&fs_path);
719
720 self.inner.register_read_invalidator(&full_path).await?;
721
722 let _lock = self.inner.lock_path(&full_path).await;
723 let content = match retry_blocking(|| File::from_path(&full_path))
724 .instrument(tracing::info_span!("read file", name = ?full_path))
725 .concurrency_limited(&self.inner.read_semaphore)
726 .await
727 {
728 Ok(file) => FileContent::new(file),
729 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
730 FileContent::NotFound
731 }
732 Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
734 };
735 Ok(content.cell())
736 }
737
738 #[turbo_tasks::function(fs)]
739 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
740 mark_session_dependent();
741
742 if self.inner.is_path_denied(&fs_path) {
744 return Ok(RawDirectoryContent::not_found());
745 }
746 let full_path = self.to_sys_path(&fs_path);
747
748 self.inner.register_dir_invalidator(&full_path).await?;
749
750 let read_dir = match retry_blocking(|| std::fs::read_dir(&full_path))
752 .instrument(tracing::info_span!("read directory", name = ?full_path))
753 .concurrency_limited(&self.inner.read_semaphore)
754 .await
755 {
756 Ok(dir) => dir,
757 Err(e)
758 if e.kind() == ErrorKind::NotFound
759 || e.kind() == ErrorKind::NotADirectory
760 || e.kind() == ErrorKind::InvalidFilename =>
761 {
762 return Ok(RawDirectoryContent::not_found());
763 }
764 Err(e) => {
765 return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
767 }
768 };
769 let dir_path = fs_path.path.as_str();
770 let denied_entries: FxHashSet<&str> = self
771 .inner
772 .denied_paths
773 .iter()
774 .filter_map(|denied_path| {
775 if denied_path.starts_with(dir_path) {
782 let denied_path_suffix =
783 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
784 Some(&denied_path[dir_path.len() + 1..])
785 } else if dir_path.is_empty() {
786 Some(denied_path.as_str())
787 } else {
788 None
789 };
790 denied_path_suffix.filter(|s| !s.contains('/'))
792 } else {
793 None
794 }
795 })
796 .collect();
797
798 let entries = read_dir
799 .filter_map(|r| {
800 let e = match r {
801 Ok(e) => e,
802 Err(err) => return Some(Err(err.into())),
803 };
804
805 let file_name = RcStr::from(e.file_name().to_str()?);
807 if denied_entries.contains(file_name.as_str()) {
809 return None;
810 }
811
812 let entry = match e.file_type() {
813 Ok(t) if t.is_file() => RawDirectoryEntry::File,
814 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
815 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
816 Ok(_) => RawDirectoryEntry::Other,
817 Err(err) => return Some(Err(err.into())),
818 };
819
820 Some(anyhow::Ok((file_name, entry)))
821 })
822 .collect::<Result<_>>()
823 .with_context(|| format!("reading directory item in {full_path:?}"))?;
824
825 Ok(RawDirectoryContent::new(entries))
826 }
827
828 #[turbo_tasks::function(fs)]
829 async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
830 mark_session_dependent();
831
832 if self.inner.is_path_denied(&fs_path) {
834 return Ok(LinkContent::NotFound.cell());
835 }
836 let full_path = self.to_sys_path(&fs_path);
837
838 self.inner.register_read_invalidator(&full_path).await?;
839
840 let _lock = self.inner.lock_path(&full_path).await;
841 let link_path = match retry_blocking(|| std::fs::read_link(&full_path))
842 .instrument(tracing::info_span!("read symlink", name = ?full_path))
843 .concurrency_limited(&self.inner.read_semaphore)
844 .await
845 {
846 Ok(res) => res,
847 Err(_) => return Ok(LinkContent::NotFound.cell()),
848 };
849 let is_link_absolute = link_path.is_absolute();
850
851 let mut file = link_path.clone();
852 if !is_link_absolute {
853 if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
854 normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
855 }) {
856 #[cfg(windows)]
857 {
858 file = PathBuf::from(normalized_linked_path);
859 }
860 #[cfg(not(windows))]
863 {
864 file = PathBuf::from(format!("/{normalized_linked_path}"));
865 }
866 } else {
867 return Ok(LinkContent::Invalid.cell());
868 }
869 }
870
871 let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
878
879 let relative_to_root_path = match result {
880 Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
881 Err(_) => return Ok(LinkContent::Invalid.cell()),
882 };
883
884 let (target, file_type) = if is_link_absolute {
885 let target_string = RcStr::from(relative_to_root_path.to_string_lossy());
886 (
887 target_string.clone(),
888 FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
889 .get_type()
890 .await?,
891 )
892 } else {
893 let link_path_string_cow = link_path.to_string_lossy();
894 let link_path_unix = RcStr::from(sys_to_unix(&link_path_string_cow));
895 (
896 link_path_unix.clone(),
897 fs_path.parent().join(&link_path_unix)?.get_type().await?,
898 )
899 };
900
901 Ok(LinkContent::Link {
902 target,
903 link_type: {
904 let mut link_type = Default::default();
905 if link_path.is_absolute() {
906 link_type |= LinkType::ABSOLUTE;
907 }
908 if matches!(&*file_type, FileSystemEntryType::Directory) {
909 link_type |= LinkType::DIRECTORY;
910 }
911 link_type
912 },
913 }
914 .cell())
915 }
916
917 #[turbo_tasks::function(fs)]
918 async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
919 if self.inner.is_path_denied(&fs_path) {
925 turbobail!("Cannot write to denied path: {fs_path}");
926 }
927 let full_path = self.to_sys_path(&fs_path);
928
929 let content = content.persist().await?;
935
936 let inner = self.inner.clone();
937
938 #[derive(TraceRawVcs, NonLocalValue)]
939 struct WriteEffect {
940 full_path: PathBuf,
941 inner: Arc<DiskFileSystemInner>,
942 content: ReadRef<PersistedFileContent>,
943 content_hash: u128,
944 }
945
946 impl Effect for WriteEffect {
947 type Error = AnyhowWrapper;
948 type Value = u128;
949
950 fn key(&self) -> Vec<u8> {
951 self.full_path.as_os_str().as_encoded_bytes().to_vec()
952 }
953
954 fn value(&self) -> &u128 {
955 &self.content_hash
956 }
957
958 fn state_storage(&self) -> &EffectStateStorage {
959 &self.inner.effect_state_storage
960 }
961
962 async fn apply(&self) -> Result<(), AnyhowWrapper> {
963 self.apply_inner().await.map_err(AnyhowWrapper::from)
964 }
965 }
966
967 impl WriteEffect {
968 async fn apply_inner(&self) -> anyhow::Result<()> {
969 let full_path = validate_path_length(&self.full_path)?;
970
971 let _lock = self.inner.lock_path(&full_path).await;
972
973 let compare = self
979 .content
980 .streaming_compare(&full_path)
981 .instrument(tracing::info_span!("read file before write", name = ?full_path))
982 .concurrency_limited(&self.inner.read_semaphore)
983 .await?;
984 if compare == FileComparison::Equal {
985 return Ok(());
986 }
987
988 match &*self.content {
989 PersistedFileContent::Content(..) => {
990 let content = self.content.clone();
991 let full_path = full_path.into_owned();
992 async {
993 let do_write = || {
994 let content = content.clone();
995 let full_path = full_path.clone();
996 let span = tracing::info_span!("write file", name = ?full_path);
997 retry_blocking(move || {
998 let mut f = std::fs::File::create(&full_path)?;
999 let PersistedFileContent::Content(file) = &*content else {
1000 unreachable!()
1001 };
1002 std::io::copy(&mut file.read(), &mut f)?;
1003 #[cfg(unix)]
1004 f.set_permissions(file.meta.permissions.into())?;
1005 f.flush()?;
1006
1007 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
1008 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
1009 .is_some_and(|v| v == "1" || v == "true")
1010 });
1011 if *WRITE_VERSION {
1012 let mut full_path = full_path.clone();
1013 let hash = hash_xxh3_hash64(file);
1014 let ext = full_path.extension();
1015 let ext = if let Some(ext) = ext {
1016 format!("{:016x}.{}", hash, ext.to_string_lossy())
1017 } else {
1018 format!("{hash:016x}")
1019 };
1020 full_path.set_extension(ext);
1021 let mut f = std::fs::File::create(&full_path)?;
1022 std::io::copy(&mut file.read(), &mut f)?;
1023 #[cfg(unix)]
1024 f.set_permissions(file.meta.permissions.into())?;
1025 f.flush()?;
1026 }
1027 Ok::<(), io::Error>(())
1028 })
1029 .instrument(span)
1030 };
1031
1032 match do_write().await {
1033 Err(e) if e.kind() == ErrorKind::NotFound => {
1034 if let Some(parent) = full_path.parent() {
1036 retry_blocking(|| std::fs::create_dir_all(parent))
1037 .instrument(tracing::info_span!(
1038 "create directory",
1039 name = ?parent
1040 ))
1041 .await
1042 .with_context(|| {
1043 format!(
1044 "failed to create directory {parent:?} for \
1045 write to {full_path:?}",
1046 )
1047 })?;
1048 }
1049 do_write().await.with_context(|| {
1050 format!("failed to write to {full_path:?}")
1051 })?;
1052 }
1053 result => {
1054 result.with_context(|| {
1055 format!("failed to write to {full_path:?}")
1056 })?;
1057 }
1058 }
1059 anyhow::Ok(())
1060 }
1061 .concurrency_limited(&self.inner.write_semaphore)
1062 .await?;
1063 }
1064 PersistedFileContent::NotFound => {
1065 retry_blocking(|| std::fs::remove_file(&full_path))
1066 .instrument(tracing::info_span!("remove file", name = ?full_path))
1067 .concurrency_limited(&self.inner.write_semaphore)
1068 .await
1069 .or_else(|err| {
1070 if err.kind() == ErrorKind::NotFound {
1071 Ok(())
1072 } else {
1073 Err(err)
1074 }
1075 })
1076 .with_context(|| format!("removing {full_path:?} failed"))?;
1077 }
1078 }
1079
1080 self.inner.invalidate_from_write(&self.full_path);
1082
1083 Ok(())
1084 }
1085 }
1086
1087 let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*content));
1088 emit_effect(WriteEffect {
1089 full_path,
1090 inner,
1091 content,
1092 content_hash,
1093 });
1094
1095 Ok(())
1096 }
1097
1098 #[turbo_tasks::function(fs)]
1099 async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
1100 if self.inner.is_path_denied(&fs_path) {
1106 turbobail!("Cannot write link to denied path: {fs_path}");
1107 }
1108
1109 let content = target.await?;
1110
1111 let full_path = self.to_sys_path(&fs_path);
1112 let inner = self.inner.clone();
1113
1114 #[derive(TraceRawVcs, NonLocalValue)]
1115 struct WriteLinkEffect {
1116 full_path: PathBuf,
1117 inner: Arc<DiskFileSystemInner>,
1118 content: ReadRef<LinkContent>,
1119 content_hash: u128,
1120 }
1121
1122 impl Effect for WriteLinkEffect {
1123 type Error = AnyhowWrapper;
1124 type Value = u128;
1125
1126 fn key(&self) -> Vec<u8> {
1127 self.full_path.as_os_str().as_encoded_bytes().to_vec()
1128 }
1129
1130 fn value(&self) -> &u128 {
1131 &self.content_hash
1132 }
1133
1134 fn state_storage(&self) -> &EffectStateStorage {
1135 &self.inner.effect_state_storage
1136 }
1137
1138 async fn apply(&self) -> Result<(), AnyhowWrapper> {
1139 self.apply_inner().await.map_err(AnyhowWrapper::from)
1140 }
1141 }
1142
1143 impl WriteLinkEffect {
1144 async fn apply_inner(&self) -> anyhow::Result<()> {
1145 let full_path = validate_path_length(&self.full_path)?;
1146
1147 let _lock = self.inner.lock_path(&full_path).await;
1148
1149 enum OsSpecificLinkContent {
1150 Link {
1151 #[cfg(windows)]
1152 is_directory: bool,
1153 target: PathBuf,
1154 },
1155 NotFound,
1156 Invalid,
1157 }
1158
1159 let os_specific_link_content = match &*self.content {
1160 LinkContent::Link { target, link_type } => {
1161 let is_directory = link_type.contains(LinkType::DIRECTORY);
1162 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1163 Path::new(&self.inner.root).join(unix_to_sys(target).as_ref())
1164 } else {
1165 let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1166 if cfg!(windows) && is_directory {
1167 full_path
1169 .parent()
1170 .unwrap_or(&full_path)
1171 .join(relative_target)
1172 } else {
1173 relative_target
1174 }
1175 };
1176 OsSpecificLinkContent::Link {
1177 #[cfg(windows)]
1178 is_directory,
1179 target: target_path,
1180 }
1181 }
1182 LinkContent::Invalid => OsSpecificLinkContent::Invalid,
1183 LinkContent::NotFound => OsSpecificLinkContent::NotFound,
1184 };
1185
1186 let old_content = match retry_blocking(|| std::fs::read_link(&full_path))
1187 .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1188 .concurrency_limited(&self.inner.read_semaphore)
1189 .await
1190 {
1191 Ok(res) => Some((res.is_absolute(), res)),
1192 Err(_) => None,
1193 };
1194 let is_equal = match (&os_specific_link_content, &old_content) {
1195 (
1196 OsSpecificLinkContent::Link { target, .. },
1197 Some((old_is_absolute, old_target)),
1198 ) => target == old_target && target.is_absolute() == *old_is_absolute,
1199 (OsSpecificLinkContent::NotFound, None) => true,
1200 _ => false,
1201 };
1202 if is_equal {
1203 return Ok(());
1204 }
1205
1206 match os_specific_link_content {
1207 OsSpecificLinkContent::Link {
1208 target,
1209 #[cfg(windows)]
1210 is_directory,
1211 ..
1212 } => {
1213 let full_path = full_path.into_owned();
1214
1215 #[derive(thiserror::Error, Debug)]
1216 #[error("{msg}: {source}")]
1217 struct SymlinkCreationError {
1218 msg: &'static str,
1219 #[source]
1220 source: io::Error,
1221 }
1222
1223 let mut has_old_content = old_content.is_some();
1224 let try_create_link = || {
1225 if has_old_content {
1226 remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1231 SymlinkCreationError {
1232 msg: "removal of existing symbolic link or junction point \
1233 failed",
1234 source: err,
1235 }
1236 })?;
1237 has_old_content = false;
1238 }
1239 #[cfg(not(windows))]
1240 let io_result = std::os::unix::fs::symlink(&target, &full_path);
1241 #[cfg(windows)]
1242 let io_result = if is_directory {
1243 std::os::windows::fs::junction_point(&target, &full_path)
1244 } else {
1245 std::os::windows::fs::symlink_file(&target, &full_path)
1246 };
1247 io_result.map_err(|err| {
1248 if err.kind() == ErrorKind::AlreadyExists {
1249 has_old_content = true;
1251 }
1252 SymlinkCreationError {
1253 msg: "creation of a new symbolic link or junction point failed",
1254 source: err,
1255 }
1256 })
1257 };
1258 fn can_retry_link(err: &SymlinkCreationError) -> bool {
1259 err.source.kind() == ErrorKind::AlreadyExists || can_retry(&err.source)
1260 }
1261 let err_context = || {
1262 #[cfg(not(windows))]
1263 let message = format!(
1264 "failed to create symlink at {full_path:?} pointing to {target:?}"
1265 );
1266 #[cfg(windows)]
1267 let message = if is_directory {
1268 format!(
1269 "failed to create junction point at {full_path:?} pointing to \
1270 {target:?}"
1271 )
1272 } else {
1273 format!(
1274 "failed to create symlink at {full_path:?} pointing to \
1275 {target:?}\n\
1276 (Note: creating file symlinks on Windows require developer \
1277 mode or admin permissions: \
1278 https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1279 )
1280 };
1281 message
1282 };
1283 async {
1284 let write_result =
1285 retry_blocking_custom(try_create_link, can_retry_link)
1286 .instrument(tracing::info_span!(
1287 "write symlink",
1288 name = ?full_path,
1289 target = ?target,
1290 ))
1291 .await;
1292
1293 match write_result {
1294 Err(ref e) if e.source.kind() == ErrorKind::NotFound => {
1295 if let Some(parent) = full_path.parent() {
1297 retry_blocking(|| std::fs::create_dir_all(parent))
1298 .instrument(tracing::info_span!(
1299 "create directory",
1300 name = ?parent
1301 ))
1302 .await
1303 .with_context(|| {
1304 format!(
1305 "failed to create directory {parent:?} for \
1306 write link to {full_path:?}",
1307 )
1308 })?;
1309 }
1310 retry_blocking_custom(
1313 || {
1314 #[cfg(not(windows))]
1315 let io_result =
1316 std::os::unix::fs::symlink(&target, &full_path);
1317 #[cfg(windows)]
1318 let io_result = if is_directory {
1319 std::os::windows::fs::junction_point(
1320 &target, &full_path,
1321 )
1322 } else {
1323 std::os::windows::fs::symlink_file(
1324 &target, &full_path,
1325 )
1326 };
1327 io_result.map_err(|err| SymlinkCreationError {
1328 msg: "creation of a new symbolic link or junction \
1329 point failed",
1330 source: err,
1331 })
1332 },
1333 |e: &SymlinkCreationError| can_retry(&e.source),
1334 )
1335 .instrument(tracing::info_span!(
1336 "write symlink",
1337 name = ?full_path,
1338 target = ?target,
1339 ))
1340 .await
1341 .with_context(err_context)?;
1342 }
1343 result => result.with_context(err_context)?,
1344 }
1345 anyhow::Ok(())
1346 }
1347 .concurrency_limited(&self.inner.write_semaphore)
1348 .await?;
1349 }
1350 OsSpecificLinkContent::Invalid => {
1351 bail!("invalid symlink target: {full_path:?}");
1352 }
1353 OsSpecificLinkContent::NotFound => {
1354 retry_blocking(|| remove_symbolic_link_dir_helper(&full_path))
1355 .instrument(tracing::info_span!("remove symlink", name = ?full_path))
1356 .concurrency_limited(&self.inner.write_semaphore)
1357 .await
1358 .with_context(|| format!("removing {full_path:?} failed"))?;
1359 }
1360 }
1361
1362 self.inner.invalidate_from_write(&self.full_path);
1364
1365 Ok(())
1366 }
1367 }
1368
1369 let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*content));
1370 emit_effect(WriteLinkEffect {
1371 full_path,
1372 inner,
1373 content,
1374 content_hash,
1375 });
1376 Ok(())
1377 }
1378
1379 #[turbo_tasks::function(fs)]
1380 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1381 mark_session_dependent();
1382 let full_path = self.to_sys_path(&fs_path);
1383
1384 if self.inner.is_path_denied(&fs_path) {
1386 turbobail!("Cannot read metadata from denied path: {fs_path}");
1387 }
1388
1389 self.inner.register_read_invalidator(&full_path).await?;
1390
1391 let _lock = self.inner.lock_path(&full_path).await;
1392 let meta = retry_blocking(|| std::fs::metadata(&full_path))
1393 .instrument(tracing::info_span!("read metadata", name = ?full_path))
1394 .concurrency_limited(&self.inner.read_semaphore)
1395 .await
1396 .with_context(|| format!("reading metadata for {:?}", full_path))?;
1397
1398 Ok(FileMeta::cell(meta.into()))
1399 }
1400}
1401
1402fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1403 let result = if cfg!(windows) {
1404 std::fs::remove_dir(path).or_else(|err| {
1417 if err.kind() == ErrorKind::NotADirectory {
1418 std::fs::remove_file(path)
1419 } else {
1420 Err(err)
1421 }
1422 })
1423 } else {
1424 std::fs::remove_file(path)
1425 };
1426 match result {
1427 Ok(()) => Ok(()),
1428 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1429 Err(err) => Err(err),
1430 }
1431}
1432
1433#[derive(Debug, Clone, Hash, TaskInput)]
1434#[turbo_tasks::value(shared)]
1435pub struct FileSystemPath {
1436 pub fs: ResolvedVc<Box<dyn FileSystem>>,
1437 pub path: RcStr,
1438}
1439
1440impl ValueToStringRef for FileSystemPath {
1441 async fn to_string_ref(&self) -> Result<RcStr> {
1442 turbofmt!("[{}]/{}", self.fs, self.path).await
1443 }
1444}
1445
1446#[turbo_tasks::value_impl]
1447impl ValueToString for FileSystemPath {
1448 #[turbo_tasks::function]
1449 async fn to_string(&self) -> Result<Vc<RcStr>> {
1450 Ok(Vc::cell(self.to_string_ref().await?))
1451 }
1452}
1453
1454impl FileSystemPath {
1455 pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1456 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1457 if other.path.is_empty() {
1458 true
1459 } else {
1460 self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1461 }
1462 } else {
1463 false
1464 }
1465 }
1466
1467 pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1468 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1469 if other.path.is_empty() {
1470 true
1471 } else {
1472 matches!(
1473 self.path.as_bytes().get(other.path.len()),
1474 Some(&b'/') | None
1475 )
1476 }
1477 } else {
1478 false
1479 }
1480 }
1481
1482 pub fn is_root(&self) -> bool {
1483 self.path.is_empty()
1484 }
1485
1486 pub fn is_in_node_modules(&self) -> bool {
1487 self.path.starts_with("node_modules/") || self.path.contains("/node_modules/")
1488 }
1489
1490 pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1494 if self.fs != inner.fs {
1495 return None;
1496 }
1497 let path = inner.path.strip_prefix(&*self.path)?;
1498 if self.path.is_empty() {
1499 Some(path)
1500 } else if let Some(stripped) = path.strip_prefix('/') {
1501 Some(stripped)
1502 } else {
1503 None
1504 }
1505 }
1506
1507 pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1508 if self.fs != other.fs {
1509 return None;
1510 }
1511
1512 Some(get_relative_path_to(&self.path, &other.path).into())
1513 }
1514
1515 pub fn file_name(&self) -> &str {
1518 let (_, file_name) = self.split_file_name();
1519 file_name
1520 }
1521
1522 pub fn has_extension(&self, extension: &str) -> bool {
1527 debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1528 self.path.ends_with(extension)
1529 }
1530
1531 pub fn extension(&self) -> Option<&str> {
1533 let (_, extension) = self.split_extension();
1534 extension
1535 }
1536
1537 fn split_extension(&self) -> (&str, Option<&str>) {
1541 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1542 if extension.contains('/') ||
1543 path_before_extension.ends_with('/') || path_before_extension.is_empty()
1545 {
1546 (self.path.as_str(), None)
1547 } else {
1548 (path_before_extension, Some(extension))
1549 }
1550 } else {
1551 (self.path.as_str(), None)
1552 }
1553 }
1554
1555 fn split_file_name(&self) -> (Option<&str>, &str) {
1559 if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1561 (Some(parent), file_name)
1562 } else {
1563 (None, self.path.as_str())
1564 }
1565 }
1566
1567 fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1572 let (path_before_extension, extension) = self.split_extension();
1573
1574 if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1575 (Some(parent), file_stem, extension)
1576 } else {
1577 (None, path_before_extension, extension)
1578 }
1579 }
1580}
1581
1582#[turbo_tasks::value(transparent)]
1583pub struct FileSystemPathOption(Option<FileSystemPath>);
1584
1585#[turbo_tasks::value_impl]
1586impl FileSystemPathOption {
1587 #[turbo_tasks::function]
1588 pub fn none() -> Vc<Self> {
1589 Vc::cell(None)
1590 }
1591}
1592
1593impl FileSystemPath {
1594 fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1598 debug_assert!(
1602 MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1603 "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1604 '/'",
1605 );
1606 debug_assert!(
1607 normalize_path(&path).as_deref() == Some(&*path),
1608 "path {path} must be normalized",
1609 );
1610 FileSystemPath { fs, path }
1611 }
1612
1613 pub fn join(&self, path: &str) -> Result<Self> {
1616 if let Some(path) = join_path(&self.path, path) {
1617 Ok(Self::new_normalized(self.fs, path.into()))
1618 } else {
1619 bail!(
1620 "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1621 self.path,
1622 path,
1623 );
1624 }
1625 }
1626
1627 pub fn append(&self, path: &str) -> Result<Self> {
1629 if path.contains('/') {
1630 bail!(
1631 "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1632 self.path,
1633 path,
1634 )
1635 }
1636 Ok(Self::new_normalized(
1637 self.fs,
1638 format!("{}{}", self.path, path).into(),
1639 ))
1640 }
1641
1642 pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1645 if appending.contains('/') {
1646 bail!(
1647 "FileSystemPath({:?}).append_to_stem({:?}) must not append '/'",
1648 self.path,
1649 appending,
1650 )
1651 }
1652 if let (path, Some(ext)) = self.split_extension() {
1653 return Ok(Self::new_normalized(
1654 self.fs,
1655 format!("{path}{appending}.{ext}").into(),
1656 ));
1657 }
1658 Ok(Self::new_normalized(
1659 self.fs,
1660 format!("{}{}", self.path, appending).into(),
1661 ))
1662 }
1663
1664 #[allow(clippy::needless_borrow)] pub fn try_join(&self, path: &str) -> Option<FileSystemPath> {
1668 #[cfg(target_os = "windows")]
1670 let path = path.replace('\\', "/");
1671
1672 join_path(&self.path, &path).map(|p| Self::new_normalized(self.fs, RcStr::from(p)))
1673 }
1674
1675 pub fn try_join_inside(&self, path: &str) -> Option<FileSystemPath> {
1679 if let Some(p) = join_path(&self.path, path)
1680 && p.starts_with(&*self.path)
1681 {
1682 return Some(Self::new_normalized(self.fs, RcStr::from(p)));
1683 }
1684 None
1685 }
1686
1687 pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1690 read_glob(self.clone(), glob)
1691 }
1692
1693 pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1697 track_glob(self.clone(), glob, include_dot_files)
1698 }
1699
1700 pub fn root(&self) -> Vc<Self> {
1701 self.fs().root()
1702 }
1703}
1704
1705impl FileSystemPath {
1706 pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1707 *self.fs
1708 }
1709
1710 pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1711 self.is_inside_ref(other)
1712 }
1713
1714 pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1715 self.is_inside_or_equal_ref(other)
1716 }
1717
1718 pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1721 let (path_without_extension, _) = self.split_extension();
1722 Self::new_normalized(
1723 self.fs,
1724 match extension.is_empty() {
1727 true => path_without_extension.into(),
1728 false => format!("{path_without_extension}.{extension}").into(),
1729 },
1730 )
1731 }
1732
1733 pub fn file_stem(&self) -> Option<&str> {
1742 let (_, file_stem, _) = self.split_file_stem_extension();
1743 if file_stem.is_empty() {
1744 return None;
1745 }
1746 Some(file_stem)
1747 }
1748}
1749
1750impl std::fmt::Display for FileSystemPath {
1751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1752 f.write_str(&self.path)
1753 }
1754}
1755
1756#[turbo_tasks::function]
1757pub async fn rebase(
1758 fs_path: FileSystemPath,
1759 old_base: FileSystemPath,
1760 new_base: FileSystemPath,
1761) -> Result<Vc<FileSystemPath>> {
1762 let new_path;
1763 if old_base.path.is_empty() {
1764 if new_base.path.is_empty() {
1765 new_path = fs_path.path.clone();
1766 } else {
1767 new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1768 }
1769 } else {
1770 let base_path = [&old_base.path, "/"].concat();
1771 if !fs_path.path.starts_with(&base_path) {
1772 turbobail!(
1773 "rebasing {fs_path} from {old_base} onto {new_base} doesn't work because it's not \
1774 part of the source path",
1775 );
1776 }
1777 if new_base.path.is_empty() {
1778 new_path = [&fs_path.path[base_path.len()..]].concat().into();
1779 } else {
1780 new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1781 .concat()
1782 .into();
1783 }
1784 }
1785 Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1786}
1787
1788impl FileSystemPath {
1790 pub fn read(&self) -> Vc<FileContent> {
1791 self.fs().read(self.clone())
1792 }
1793
1794 pub fn read_link(&self) -> Vc<LinkContent> {
1795 self.fs().read_link(self.clone())
1796 }
1797
1798 pub fn read_json(&self) -> Vc<FileJsonContent> {
1799 self.fs().read(self.clone()).parse_json()
1800 }
1801
1802 pub fn read_json5(&self) -> Vc<FileJsonContent> {
1803 self.fs().read(self.clone()).parse_json5()
1804 }
1805
1806 pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1811 self.fs().raw_read_dir(self.clone())
1812 }
1813
1814 pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1815 self.fs().write(self.clone(), content)
1816 }
1817
1818 pub fn write_symbolic_link_dir(&self, target: Vc<LinkContent>) -> Vc<()> {
1836 self.fs().write_link(self.clone(), target)
1837 }
1838
1839 pub fn metadata(&self) -> Vc<FileMeta> {
1840 self.fs().metadata(self.clone())
1841 }
1842
1843 pub async fn realpath(&self) -> Result<FileSystemPath> {
1846 let result = &(*self.realpath_with_links().await?);
1847 match &result.path_result {
1848 Ok(path) => Ok(path.clone()),
1849 Err(error) => bail!("{}", error.as_error_message(self, result).await?),
1850 }
1851 }
1852
1853 pub fn rebase(
1854 fs_path: FileSystemPath,
1855 old_base: FileSystemPath,
1856 new_base: FileSystemPath,
1857 ) -> Vc<FileSystemPath> {
1858 rebase(fs_path, old_base, new_base)
1859 }
1860}
1861
1862impl FileSystemPath {
1863 pub fn read_dir(&self) -> Vc<DirectoryContent> {
1868 read_dir(self.clone())
1869 }
1870
1871 pub fn parent(&self) -> FileSystemPath {
1872 let path = &self.path;
1873 if path.is_empty() {
1874 return self.clone();
1875 }
1876 FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1877 }
1878
1879 pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1888 get_type(self.clone())
1889 }
1890
1891 pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1892 realpath_with_links(self.clone())
1893 }
1894}
1895
1896#[derive(Clone, Debug)]
1897#[turbo_tasks::value(shared)]
1898pub struct RealPathResult {
1899 pub path_result: Result<FileSystemPath, RealPathResultError>,
1900 pub symlinks: Vec<FileSystemPath>,
1901}
1902
1903#[derive(Debug, Clone, Hash, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
1906pub enum RealPathResultError {
1907 TooManySymlinks,
1908 CycleDetected,
1909 Invalid,
1910 NotFound,
1911}
1912
1913impl RealPathResultError {
1914 pub async fn as_error_message(
1916 &self,
1917 orig: &FileSystemPath,
1918 result: &RealPathResult,
1919 ) -> Result<RcStr> {
1920 Ok(match self {
1921 RealPathResultError::TooManySymlinks => {
1922 let len = result.symlinks.len();
1923 turbofmt!("Symlink {orig} leads to too many other symlinks ({len} links)").await?
1924 }
1925 RealPathResultError::CycleDetected => {
1926 let symlinks_dbg = format!(
1929 "{:?}",
1930 result.symlinks.iter().map(|s| &s.path).collect::<Vec<_>>()
1931 );
1932 turbofmt!("Symlink {orig} is in a symlink loop: {symlinks_dbg}").await?
1933 }
1934 RealPathResultError::Invalid => {
1935 turbofmt!("Symlink {orig} is invalid, it points out of the filesystem root").await?
1936 }
1937 RealPathResultError::NotFound => {
1938 turbofmt!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1939 .await?
1940 }
1941 })
1942 }
1943}
1944
1945#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
1946#[turbo_tasks::value(shared)]
1947pub enum Permissions {
1948 Readable,
1949 #[default]
1950 Writable,
1951 Executable,
1952}
1953
1954#[cfg(unix)]
1957impl From<Permissions> for std::fs::Permissions {
1958 fn from(perm: Permissions) -> Self {
1959 use std::os::unix::fs::PermissionsExt;
1960 match perm {
1961 Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1962 Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1963 Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1964 }
1965 }
1966}
1967
1968#[cfg(unix)]
1969impl From<std::fs::Permissions> for Permissions {
1970 fn from(perm: std::fs::Permissions) -> Self {
1971 use std::os::unix::fs::PermissionsExt;
1972 if perm.readonly() {
1973 Permissions::Readable
1974 } else {
1975 if perm.mode() & 0o111 != 0 {
1977 Permissions::Executable
1978 } else {
1979 Permissions::Writable
1980 }
1981 }
1982 }
1983}
1984
1985#[cfg(not(unix))]
1986impl From<std::fs::Permissions> for Permissions {
1987 fn from(_: std::fs::Permissions) -> Self {
1988 Permissions::default()
1989 }
1990}
1991
1992#[turbo_tasks::value(shared, serialization = "hash")]
1993#[derive(Clone, Debug, PartialOrd, Ord)]
1994pub enum FileContent {
1995 Content(File),
1996 NotFound,
1997}
1998
1999impl From<File> for FileContent {
2000 fn from(file: File) -> Self {
2001 FileContent::Content(file)
2002 }
2003}
2004
2005#[turbo_tasks::value(shared)]
2012#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
2013pub enum PersistedFileContent {
2014 Content(File),
2015 NotFound,
2016}
2017
2018impl PersistedFileContent {
2019 async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
2021 let old_file =
2022 extract_disk_access(retry_blocking(|| std::fs::File::open(path)).await, path)?;
2023 let Some(old_file) = old_file else {
2024 return Ok(match self {
2025 PersistedFileContent::NotFound => FileComparison::Equal,
2026 _ => FileComparison::Create,
2027 });
2028 };
2029 let PersistedFileContent::Content(new_file) = self else {
2031 return Ok(FileComparison::NotEqual);
2032 };
2033
2034 let old_meta = extract_disk_access(retry_blocking(|| old_file.metadata()).await, path)?;
2035 let Some(old_meta) = old_meta else {
2036 return Ok(FileComparison::Create);
2039 };
2040 if new_file.meta != old_meta.into() {
2042 return Ok(FileComparison::NotEqual);
2043 }
2044
2045 let mut new_contents = new_file.read();
2048 let mut old_contents = BufReader::new(old_file);
2049 Ok(loop {
2050 let new_chunk = new_contents.fill_buf()?;
2051 let Ok(old_chunk) = old_contents.fill_buf() else {
2052 break FileComparison::NotEqual;
2053 };
2054
2055 let len = min(new_chunk.len(), old_chunk.len());
2056 if len == 0 {
2057 if new_chunk.len() == old_chunk.len() {
2058 break FileComparison::Equal;
2059 } else {
2060 break FileComparison::NotEqual;
2061 }
2062 }
2063
2064 if new_chunk[0..len] != old_chunk[0..len] {
2065 break FileComparison::NotEqual;
2066 }
2067
2068 new_contents.consume(len);
2069 old_contents.consume(len);
2070 })
2071 }
2072}
2073
2074#[derive(Clone, Debug, Eq, PartialEq)]
2075enum FileComparison {
2076 Create,
2077 Equal,
2078 NotEqual,
2079}
2080
2081bitflags! {
2082 #[derive(
2083 Default,
2084 TraceRawVcs,
2085 NonLocalValue,
2086 DeterministicHash,
2087 Encode,
2088 Decode,
2089 )]
2090 pub struct LinkType: u8 {
2091 const DIRECTORY = 0b00000001;
2092 const ABSOLUTE = 0b00000010;
2093 }
2094}
2095
2096#[turbo_tasks::value(shared)]
2102#[derive(Debug, DeterministicHash)]
2103pub enum LinkContent {
2104 Link {
2115 target: RcStr,
2116 link_type: LinkType,
2117 },
2118 Invalid,
2120 NotFound,
2122}
2123
2124#[turbo_tasks::value(shared)]
2125#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
2126pub struct File {
2127 #[turbo_tasks(debug_ignore)]
2128 content: Rope,
2129 meta: FileMeta,
2130}
2131
2132impl File {
2133 fn from_path(p: &Path) -> io::Result<Self> {
2135 let mut file = std::fs::File::open(p)?;
2136 let metadata = file.metadata()?;
2137
2138 let mut output = Vec::with_capacity(metadata.len() as usize);
2139 file.read_to_end(&mut output)?;
2140
2141 Ok(File {
2142 meta: metadata.into(),
2143 content: Rope::from(output),
2144 })
2145 }
2146
2147 fn from_bytes(content: Vec<u8>) -> Self {
2149 File {
2150 meta: FileMeta::default(),
2151 content: Rope::from(content),
2152 }
2153 }
2154
2155 fn from_rope(content: Rope) -> Self {
2157 File {
2158 meta: FileMeta::default(),
2159 content,
2160 }
2161 }
2162
2163 pub fn content_type(&self) -> Option<&Mime> {
2165 self.meta.content_type.as_ref()
2166 }
2167
2168 pub fn with_content_type(mut self, content_type: Mime) -> Self {
2170 self.meta.content_type = Some(content_type);
2171 self
2172 }
2173
2174 pub fn read(&self) -> RopeReader<'_> {
2176 self.content.read()
2177 }
2178}
2179
2180impl Debug for File {
2181 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2182 f.debug_struct("File")
2183 .field("meta", &self.meta)
2184 .field("content (hash)", &hash_xxh3_hash64(&self.content))
2185 .finish()
2186 }
2187}
2188
2189impl From<RcStr> for File {
2190 fn from(s: RcStr) -> Self {
2191 s.into_owned().into()
2192 }
2193}
2194
2195impl From<String> for File {
2196 fn from(s: String) -> Self {
2197 File::from_bytes(s.into_bytes())
2198 }
2199}
2200
2201impl From<ReadRef<RcStr>> for File {
2202 fn from(s: ReadRef<RcStr>) -> Self {
2203 File::from_bytes(s.as_bytes().to_vec())
2204 }
2205}
2206
2207impl From<&str> for File {
2208 fn from(s: &str) -> Self {
2209 File::from_bytes(s.as_bytes().to_vec())
2210 }
2211}
2212
2213impl From<Vec<u8>> for File {
2214 fn from(bytes: Vec<u8>) -> Self {
2215 File::from_bytes(bytes)
2216 }
2217}
2218
2219impl From<&[u8]> for File {
2220 fn from(bytes: &[u8]) -> Self {
2221 File::from_bytes(bytes.to_vec())
2222 }
2223}
2224
2225impl From<ReadRef<Rope>> for File {
2226 fn from(rope: ReadRef<Rope>) -> Self {
2227 File::from_rope(ReadRef::into_owned(rope))
2228 }
2229}
2230
2231impl From<Rope> for File {
2232 fn from(rope: Rope) -> Self {
2233 File::from_rope(rope)
2234 }
2235}
2236
2237impl File {
2238 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
2239 Self {
2240 meta,
2241 content: Rope::from(content),
2242 }
2243 }
2244
2245 pub fn meta(&self) -> &FileMeta {
2247 &self.meta
2248 }
2249
2250 pub fn content(&self) -> &Rope {
2252 &self.content
2253 }
2254}
2255
2256#[turbo_tasks::value(shared)]
2257#[derive(Debug, Clone, Default)]
2258pub struct FileMeta {
2259 permissions: Permissions,
2262 #[bincode(with = "turbo_bincode::mime_option")]
2263 #[turbo_tasks(trace_ignore)]
2264 content_type: Option<Mime>,
2265}
2266
2267impl Ord for FileMeta {
2268 fn cmp(&self, other: &Self) -> Ordering {
2269 self.permissions
2270 .cmp(&other.permissions)
2271 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2272 }
2273}
2274
2275impl PartialOrd for FileMeta {
2276 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2277 Some(self.cmp(other))
2278 }
2279}
2280
2281impl From<std::fs::Metadata> for FileMeta {
2282 fn from(meta: std::fs::Metadata) -> Self {
2283 let permissions = meta.permissions().into();
2284
2285 Self {
2286 permissions,
2287 content_type: None,
2288 }
2289 }
2290}
2291
2292impl DeterministicHash for FileMeta {
2293 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2294 self.permissions.deterministic_hash(state);
2295 if let Some(content_type) = &self.content_type {
2296 content_type.to_string().deterministic_hash(state);
2297 }
2298 }
2299}
2300
2301impl FileContent {
2302 pub fn new(file: File) -> Self {
2303 FileContent::Content(file)
2304 }
2305
2306 pub fn is_content(&self) -> bool {
2307 matches!(self, FileContent::Content(_))
2308 }
2309
2310 pub fn as_content(&self) -> Option<&File> {
2311 match self {
2312 FileContent::Content(file) => Some(file),
2313 FileContent::NotFound => None,
2314 }
2315 }
2316
2317 pub fn parse_json_ref(&self) -> FileJsonContent {
2318 match self {
2319 FileContent::Content(file) => {
2320 let content = file.content.clone().into_bytes();
2321 let de = &mut serde_json::Deserializer::from_slice(&content);
2322 match serde_path_to_error::deserialize(de) {
2323 Ok(data) => FileJsonContent::Content(data),
2324 Err(e) => FileJsonContent::Unparsable(Box::new(
2325 UnparsableJson::from_serde_path_to_error(e),
2326 )),
2327 }
2328 }
2329 FileContent::NotFound => FileJsonContent::NotFound,
2330 }
2331 }
2332
2333 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2334 match self {
2335 FileContent::Content(file) => match file.content.to_str() {
2336 Ok(string) => match parse_to_serde_value(
2337 &string,
2338 &ParseOptions {
2339 allow_comments: true,
2340 allow_trailing_commas: true,
2341 allow_loose_object_property_names: false,
2342 },
2343 ) {
2344 Ok(data) => match data {
2345 Some(value) => FileJsonContent::Content(value),
2346 None => FileJsonContent::unparsable(rcstr!(
2347 "text content doesn't contain any json data"
2348 )),
2349 },
2350 Err(e) => FileJsonContent::Unparsable(Box::new(
2351 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2352 )),
2353 },
2354 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2355 },
2356 FileContent::NotFound => FileJsonContent::NotFound,
2357 }
2358 }
2359
2360 pub fn parse_json5_ref(&self) -> FileJsonContent {
2361 match self {
2362 FileContent::Content(file) => match file.content.to_str() {
2363 Ok(string) => match parse_to_serde_value(
2364 &string,
2365 &ParseOptions {
2366 allow_comments: true,
2367 allow_trailing_commas: true,
2368 allow_loose_object_property_names: true,
2369 },
2370 ) {
2371 Ok(data) => match data {
2372 Some(value) => FileJsonContent::Content(value),
2373 None => FileJsonContent::unparsable(rcstr!(
2374 "text content doesn't contain any json data"
2375 )),
2376 },
2377 Err(e) => FileJsonContent::Unparsable(Box::new(
2378 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2379 )),
2380 },
2381 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2382 },
2383 FileContent::NotFound => FileJsonContent::NotFound,
2384 }
2385 }
2386
2387 pub fn lines_ref(&self) -> FileLinesContent {
2388 match self {
2389 FileContent::Content(file) => match file.content.to_str() {
2390 Ok(string) => {
2391 let mut bytes_offset = 0;
2392 FileLinesContent::Lines(
2393 string
2394 .split('\n')
2395 .map(|l| {
2396 let line = FileLine {
2397 content: l.to_string(),
2398 bytes_offset,
2399 };
2400 bytes_offset += (l.len() + 1) as u32;
2401 line
2402 })
2403 .collect(),
2404 )
2405 }
2406 Err(_) => FileLinesContent::Unparsable,
2407 },
2408 FileContent::NotFound => FileLinesContent::NotFound,
2409 }
2410 }
2411}
2412
2413#[turbo_tasks::value_impl]
2414impl FileContent {
2415 #[turbo_tasks::function]
2416 pub fn len(&self) -> Result<Vc<Option<u64>>> {
2417 Ok(Vc::cell(match self {
2418 FileContent::Content(file) => Some(file.content.len() as u64),
2419 FileContent::NotFound => None,
2420 }))
2421 }
2422
2423 #[turbo_tasks::function]
2424 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2425 Ok(self.parse_json_ref().cell())
2426 }
2427
2428 #[turbo_tasks::function]
2429 pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
2430 self.parse_json_with_comments_ref().cell()
2431 }
2432
2433 #[turbo_tasks::function]
2434 pub fn parse_json5(&self) -> Vc<FileJsonContent> {
2435 self.parse_json5_ref().cell()
2436 }
2437
2438 #[turbo_tasks::function]
2439 pub fn lines(&self) -> Vc<FileLinesContent> {
2440 self.lines_ref().cell()
2441 }
2442
2443 #[turbo_tasks::function]
2444 pub async fn hash(&self) -> Result<Vc<u64>> {
2445 Ok(Vc::cell(hash_xxh3_hash64(self)))
2446 }
2447
2448 #[turbo_tasks::function]
2453 pub fn persist(&self) -> Vc<PersistedFileContent> {
2454 match self {
2455 FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
2456 FileContent::NotFound => PersistedFileContent::NotFound.cell(),
2457 }
2458 }
2459
2460 #[turbo_tasks::function]
2466 pub async fn content_hash(
2467 &self,
2468 salt: Vc<RcStr>,
2469 algorithm: HashAlgorithm,
2470 ) -> Result<Vc<Option<RcStr>>> {
2471 match self {
2472 FileContent::Content(file) => Ok(Vc::cell(Some(
2473 deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
2474 ))),
2475 FileContent::NotFound => Ok(Vc::cell(None)),
2476 }
2477 }
2478}
2479
2480#[turbo_tasks::value(shared, serialization = "skip")]
2482pub enum FileJsonContent {
2483 Content(Value),
2484 Unparsable(Box<UnparsableJson>),
2485 NotFound,
2486}
2487
2488#[turbo_tasks::value_impl]
2489impl ValueToString for FileJsonContent {
2490 #[turbo_tasks::function]
2495 fn to_string(&self) -> Result<Vc<RcStr>> {
2496 match self {
2497 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2498 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2499 FileJsonContent::NotFound => bail!("File not found"),
2500 }
2501 }
2502}
2503
2504#[turbo_tasks::value_impl]
2505impl FileJsonContent {
2506 #[turbo_tasks::function]
2507 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2508 match &*self.await? {
2509 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2510 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2511 FileJsonContent::NotFound => bail!("File not found"),
2512 }
2513 }
2514}
2515impl FileJsonContent {
2516 pub fn unparsable(message: RcStr) -> Self {
2517 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2518 message,
2519 path: None,
2520 start_location: None,
2521 end_location: None,
2522 }))
2523 }
2524
2525 pub fn unparsable_with_message(message: RcStr) -> Self {
2526 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2527 message,
2528 path: None,
2529 start_location: None,
2530 end_location: None,
2531 }))
2532 }
2533}
2534
2535#[derive(Debug, PartialEq, Eq)]
2536pub struct FileLine {
2537 pub content: String,
2538 pub bytes_offset: u32,
2539}
2540
2541impl FileLine {
2542 pub fn len(&self) -> usize {
2543 self.content.len()
2544 }
2545
2546 #[must_use]
2547 pub fn is_empty(&self) -> bool {
2548 self.len() == 0
2549 }
2550}
2551
2552#[turbo_tasks::value(shared, serialization = "skip")]
2553pub enum FileLinesContent {
2554 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2555 Unparsable,
2556 NotFound,
2557}
2558
2559#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2560pub enum RawDirectoryEntry {
2561 File,
2562 Directory,
2563 Symlink,
2564 Other,
2566}
2567
2568#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2569pub enum DirectoryEntry {
2570 File(FileSystemPath),
2571 Directory(FileSystemPath),
2572 Symlink(FileSystemPath),
2573 Other(FileSystemPath),
2574 Error(RcStr),
2575}
2576
2577impl DirectoryEntry {
2578 pub async fn resolve_symlink(self) -> Result<Self> {
2582 if let DirectoryEntry::Symlink(symlink) = &self {
2583 let result = &*symlink.realpath_with_links().await?;
2584 let real_path = match &result.path_result {
2585 Ok(path) => path,
2586 Err(error) => {
2587 return Ok(DirectoryEntry::Error(
2588 error.as_error_message(symlink, result).await?,
2589 ));
2590 }
2591 };
2592 Ok(match *real_path.get_type().await? {
2593 FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2594 FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2595 FileSystemEntryType::NotFound => DirectoryEntry::Error(
2597 turbofmt!("Symlink {symlink} points at {real_path} which does not exist")
2598 .await?,
2599 ),
2600 FileSystemEntryType::Symlink => turbobail!(
2602 "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2603 ),
2604 _ => self,
2605 })
2606 } else {
2607 Ok(self)
2608 }
2609 }
2610
2611 pub fn path(self) -> Option<FileSystemPath> {
2612 match self {
2613 DirectoryEntry::File(path)
2614 | DirectoryEntry::Directory(path)
2615 | DirectoryEntry::Symlink(path)
2616 | DirectoryEntry::Other(path) => Some(path),
2617 DirectoryEntry::Error(_) => None,
2618 }
2619 }
2620}
2621
2622#[turbo_tasks::value]
2623#[derive(Hash, Clone, Copy, Debug)]
2624pub enum FileSystemEntryType {
2625 NotFound,
2626 File,
2627 Directory,
2628 Symlink,
2629 Other,
2631 Error,
2632}
2633
2634impl From<FileType> for FileSystemEntryType {
2635 fn from(file_type: FileType) -> Self {
2636 match file_type {
2637 t if t.is_dir() => FileSystemEntryType::Directory,
2638 t if t.is_file() => FileSystemEntryType::File,
2639 t if t.is_symlink() => FileSystemEntryType::Symlink,
2640 _ => FileSystemEntryType::Other,
2641 }
2642 }
2643}
2644
2645impl From<DirectoryEntry> for FileSystemEntryType {
2646 fn from(entry: DirectoryEntry) -> Self {
2647 FileSystemEntryType::from(&entry)
2648 }
2649}
2650
2651impl From<&DirectoryEntry> for FileSystemEntryType {
2652 fn from(entry: &DirectoryEntry) -> Self {
2653 match entry {
2654 DirectoryEntry::File(_) => FileSystemEntryType::File,
2655 DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2656 DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2657 DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2658 DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2659 }
2660 }
2661}
2662
2663impl From<RawDirectoryEntry> for FileSystemEntryType {
2664 fn from(entry: RawDirectoryEntry) -> Self {
2665 FileSystemEntryType::from(&entry)
2666 }
2667}
2668
2669impl From<&RawDirectoryEntry> for FileSystemEntryType {
2670 fn from(entry: &RawDirectoryEntry) -> Self {
2671 match entry {
2672 RawDirectoryEntry::File => FileSystemEntryType::File,
2673 RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2674 RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2675 RawDirectoryEntry::Other => FileSystemEntryType::Other,
2676 }
2677 }
2678}
2679
2680#[turbo_tasks::value]
2681#[derive(Debug)]
2682pub enum RawDirectoryContent {
2683 Entries(AutoMap<RcStr, RawDirectoryEntry>),
2686 NotFound,
2687}
2688
2689impl RawDirectoryContent {
2690 pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2691 Self::cell(RawDirectoryContent::Entries(entries))
2692 }
2693
2694 pub fn not_found() -> Vc<Self> {
2695 Self::cell(RawDirectoryContent::NotFound)
2696 }
2697}
2698
2699#[turbo_tasks::value]
2700#[derive(Debug)]
2701pub enum DirectoryContent {
2702 Entries(AutoMap<RcStr, DirectoryEntry>),
2703 NotFound,
2704}
2705
2706impl DirectoryContent {
2707 pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2708 Self::cell(DirectoryContent::Entries(entries))
2709 }
2710
2711 pub fn not_found() -> Vc<Self> {
2712 Self::cell(DirectoryContent::NotFound)
2713 }
2714}
2715
2716#[derive(ValueToString)]
2717#[value_to_string("null")]
2718#[turbo_tasks::value(shared)]
2719pub struct NullFileSystem;
2720
2721#[turbo_tasks::value_impl]
2722impl FileSystem for NullFileSystem {
2723 #[turbo_tasks::function]
2724 fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2725 FileContent::NotFound.cell()
2726 }
2727
2728 #[turbo_tasks::function]
2729 fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2730 LinkContent::NotFound.cell()
2731 }
2732
2733 #[turbo_tasks::function]
2734 fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2735 RawDirectoryContent::not_found()
2736 }
2737
2738 #[turbo_tasks::function]
2739 fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) {}
2740
2741 #[turbo_tasks::function]
2742 fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) {}
2743
2744 #[turbo_tasks::function]
2745 fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2746 FileMeta::default().cell()
2747 }
2748}
2749
2750pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2751 loop {
2752 if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2753 path = fs.get_inner_fs_path(path).owned().await?;
2754 continue;
2755 }
2756
2757 if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2758 let sys_path = fs.await?.to_sys_path(&path);
2759 return Ok(Some(sys_path));
2760 }
2761
2762 return Ok(None);
2763 }
2764}
2765
2766#[turbo_tasks::function]
2767async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2768 let fs = path.fs().to_resolved().await?;
2769 match &*fs.raw_read_dir(path.clone()).await? {
2770 RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2771 RawDirectoryContent::Entries(entries) => {
2772 let mut normalized_entries = AutoMap::new();
2773 let dir_path = &path.path;
2774 for (name, entry) in entries {
2775 let path = if dir_path.is_empty() {
2779 name.clone()
2780 } else {
2781 RcStr::from(format!("{dir_path}/{name}"))
2782 };
2783
2784 let entry_path = FileSystemPath::new_normalized(fs, path);
2785 let entry = match entry {
2786 RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2787 RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2788 RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2789 RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2790 };
2791 normalized_entries.insert(name.clone(), entry);
2792 }
2793 Ok(DirectoryContent::new(normalized_entries))
2794 }
2795 }
2796}
2797
2798#[turbo_tasks::function]
2799async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2800 if path.is_root() {
2801 return Ok(FileSystemEntryType::Directory.cell());
2802 }
2803 let parent = path.parent();
2804 let dir_content = parent.raw_read_dir().await?;
2805 match &*dir_content {
2806 RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2807 RawDirectoryContent::Entries(entries) => {
2808 let (_, file_name) = path.split_file_name();
2809 if let Some(entry) = entries.get(file_name) {
2810 Ok(FileSystemEntryType::from(entry).cell())
2811 } else {
2812 Ok(FileSystemEntryType::NotFound.cell())
2813 }
2814 }
2815 }
2816}
2817
2818#[turbo_tasks::function]
2819async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2820 let mut current_path = path;
2821 let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2822 let mut visited: AutoSet<RcStr> = AutoSet::new();
2823 let mut error = RealPathResultError::TooManySymlinks;
2824 for _i in 0..40 {
2827 if current_path.is_root() {
2828 return Ok(RealPathResult {
2830 path_result: Ok(current_path),
2831 symlinks: symlinks.into_iter().collect(),
2832 }
2833 .cell());
2834 }
2835
2836 if !visited.insert(current_path.path.clone()) {
2837 error = RealPathResultError::CycleDetected;
2838 break; }
2840
2841 let parent = current_path.parent();
2843 let parent_result = parent.realpath_with_links().owned().await?;
2844 let basename = current_path
2845 .path
2846 .rsplit_once('/')
2847 .map_or(current_path.path.as_str(), |(_, name)| name);
2848 symlinks.extend(parent_result.symlinks);
2849 let parent_path = match parent_result.path_result {
2850 Ok(path) => {
2851 if path != parent {
2852 current_path = path.join(basename)?;
2853 }
2854 path
2855 }
2856 Err(parent_error) => {
2857 error = parent_error;
2858 break;
2859 }
2860 };
2861
2862 if !matches!(
2865 *current_path.get_type().await?,
2866 FileSystemEntryType::Symlink
2867 ) {
2868 return Ok(RealPathResult {
2869 path_result: Ok(current_path),
2870 symlinks: symlinks.into_iter().collect(), }
2872 .cell());
2873 }
2874
2875 match &*current_path.read_link().await? {
2876 LinkContent::Link { target, link_type } => {
2877 symlinks.insert(current_path.clone());
2878 current_path = if link_type.contains(LinkType::ABSOLUTE) {
2879 current_path.root().owned().await?
2880 } else {
2881 parent_path
2882 }
2883 .join(target)?;
2884 }
2885 LinkContent::NotFound => {
2886 error = RealPathResultError::NotFound;
2887 break;
2888 }
2889 LinkContent::Invalid => {
2890 error = RealPathResultError::Invalid;
2891 break;
2892 }
2893 }
2894 }
2895
2896 Ok(RealPathResult {
2904 path_result: Err(error),
2905 symlinks: symlinks.into_iter().collect(),
2906 }
2907 .cell())
2908}
2909
2910#[derive(TraceRawVcs, NonLocalValue)]
2913pub(crate) struct AnyhowWrapper(anyhow::Error);
2914
2915impl fmt::Display for AnyhowWrapper {
2916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2917 fmt::Display::fmt(&self.0, f)
2918 }
2919}
2920
2921impl fmt::Debug for AnyhowWrapper {
2922 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2923 fmt::Debug::fmt(&self.0, f)
2924 }
2925}
2926
2927impl StdError for AnyhowWrapper {
2928 fn source(&self) -> Option<&(dyn StdError + 'static)> {
2929 self.0.source()
2930 }
2931}
2932
2933impl From<anyhow::Error> for AnyhowWrapper {
2934 fn from(err: anyhow::Error) -> Self {
2935 AnyhowWrapper(err)
2936 }
2937}
2938
2939#[cfg(test)]
2940mod tests {
2941 use turbo_rcstr::rcstr;
2942 use turbo_tasks::{Effects, OperationVc, Vc, take_effects};
2943 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2944
2945 use super::*;
2946
2947 #[turbo_tasks::function(operation)]
2948 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
2949 let _ = op.resolve().strongly_consistent().await?;
2950 Ok(take_effects(op).await?.cell())
2951 }
2952
2953 #[test]
2954 fn test_get_relative_path_to() {
2955 assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2956 assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2957 assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2958 assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2959 assert_eq!(
2960 get_relative_path_to("a/b/c", "c/b/a").as_str(),
2961 "../../../c/b/a"
2962 );
2963 assert_eq!(
2964 get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2965 "../../../c/b/a"
2966 );
2967 }
2968
2969 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2970 async fn with_extension() {
2971 turbo_tasks_testing::VcStorage::with(async {
2972 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2973 .to_resolved()
2974 .await?;
2975
2976 let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2977
2978 let path_json = path_txt.with_extension("json");
2979 assert_eq!(&*path_json.path, "foo/bar.json");
2980
2981 let path_no_ext = path_txt.with_extension("");
2982 assert_eq!(&*path_no_ext.path, "foo/bar");
2983
2984 let path_new_ext = path_no_ext.with_extension("json");
2985 assert_eq!(&*path_new_ext.path, "foo/bar.json");
2986
2987 let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2988
2989 let path_no_slash_json = path_no_slash_txt.with_extension("json");
2990 assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2991
2992 let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2993 assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2994
2995 let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2996 assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2997
2998 anyhow::Ok(())
2999 })
3000 .await
3001 .unwrap()
3002 }
3003
3004 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3005 async fn file_stem() {
3006 turbo_tasks_testing::VcStorage::with(async {
3007 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
3008 .to_resolved()
3009 .await?;
3010
3011 let path = FileSystemPath::new_normalized(fs, rcstr!(""));
3012 assert_eq!(path.file_stem(), None);
3013
3014 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
3015 assert_eq!(path.file_stem(), Some("bar"));
3016
3017 let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
3018 assert_eq!(path.file_stem(), Some("bar"));
3019
3020 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
3021 assert_eq!(path.file_stem(), Some("bar"));
3022
3023 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
3024 assert_eq!(path.file_stem(), Some(".bar"));
3025
3026 anyhow::Ok(())
3027 })
3028 .await
3029 .unwrap()
3030 }
3031
3032 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3033 async fn test_try_from_sys_path() {
3034 let sys_root = if cfg!(windows) {
3035 Path::new(r"C:\fake\root")
3036 } else {
3037 Path::new(r"/fake/root")
3038 };
3039
3040 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3041 BackendOptions::default(),
3042 noop_backing_storage(),
3043 ));
3044 tt.run_once(async {
3045 assert_try_from_sys_path_operation(RcStr::from(sys_root.to_str().unwrap()))
3046 .read_strongly_consistent()
3047 .await?;
3048
3049 anyhow::Ok(())
3050 })
3051 .await
3052 .unwrap();
3053 }
3054
3055 #[turbo_tasks::function(operation)]
3056 async fn assert_try_from_sys_path_operation(sys_root: RcStr) -> anyhow::Result<()> {
3057 let sys_root = Path::new(sys_root.as_str());
3058 let fs_vc = DiskFileSystem::new(
3059 rcstr!("temp"),
3060 Vc::cell(RcStr::from(sys_root.to_str().unwrap())),
3061 )
3062 .to_resolved()
3063 .await?;
3064 let fs = fs_vc.await?;
3065 let fs_root_path = fs_vc.root().await?;
3066
3067 assert_eq!(
3068 fs.try_from_sys_path(
3069 fs_vc,
3070 &Path::new("relative").join("directory"),
3071 None,
3072 )
3073 .unwrap()
3074 .path,
3075 "relative/directory"
3076 );
3077
3078 assert_eq!(
3079 fs.try_from_sys_path(
3080 fs_vc,
3081 &sys_root
3082 .join("absolute")
3083 .join("directory")
3084 .join("..")
3085 .join("normalized_path"),
3086 Some(&fs_root_path.join("ignored").unwrap()),
3087 )
3088 .unwrap()
3089 .path,
3090 "absolute/normalized_path"
3091 );
3092
3093 assert_eq!(
3094 fs.try_from_sys_path(
3095 fs_vc,
3096 Path::new("child"),
3097 Some(&fs_root_path.join("parent").unwrap()),
3098 )
3099 .unwrap()
3100 .path,
3101 "parent/child"
3102 );
3103
3104 assert_eq!(
3105 fs.try_from_sys_path(
3106 fs_vc,
3107 &Path::new("..").join("parallel_dir"),
3108 Some(&fs_root_path.join("parent").unwrap()),
3109 )
3110 .unwrap()
3111 .path,
3112 "parallel_dir"
3113 );
3114
3115 assert_eq!(
3116 fs.try_from_sys_path(
3117 fs_vc,
3118 &Path::new("relative")
3119 .join("..")
3120 .join("..")
3121 .join("leaves_root"),
3122 None,
3123 ),
3124 None
3125 );
3126
3127 assert_eq!(
3128 fs.try_from_sys_path(
3129 fs_vc,
3130 &sys_root
3131 .join("absolute")
3132 .join("..")
3133 .join("..")
3134 .join("leaves_root"),
3135 None,
3136 ),
3137 None
3138 );
3139
3140 Ok(())
3141 }
3142
3143 #[cfg(test)]
3144 mod symlink_tests {
3145 use std::{
3146 fs::{File, create_dir_all, read_to_string},
3147 io::Write,
3148 };
3149
3150 use rand::{RngExt, SeedableRng};
3151 use turbo_rcstr::{RcStr, rcstr};
3152 use turbo_tasks::{ResolvedVc, Vc};
3153 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3154
3155 use super::extract_effects_operation;
3156 use crate::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
3157
3158 #[turbo_tasks::function(operation)]
3159 async fn test_write_link_effect_operation(
3160 fs: ResolvedVc<DiskFileSystem>,
3161 path: FileSystemPath,
3162 target: RcStr,
3163 ) -> anyhow::Result<()> {
3164 let write_file = |f| {
3165 fs.write_link(
3166 f,
3167 LinkContent::Link {
3168 target: format!("{target}/data.txt").into(),
3169 link_type: LinkType::empty(),
3170 }
3171 .cell(),
3172 )
3173 };
3174 write_file(path.join("symlink-file")?).await?;
3176 write_file(path.join("symlink-file")?).await?;
3177
3178 let write_dir = |f| {
3179 fs.write_link(
3180 f,
3181 LinkContent::Link {
3182 target: target.clone(),
3183 link_type: LinkType::DIRECTORY,
3184 }
3185 .cell(),
3186 )
3187 };
3188 write_dir(path.join("symlink-dir")?).await?;
3190 write_dir(path.join("symlink-dir")?).await?;
3191
3192 Ok(())
3193 }
3194
3195 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3196 async fn test_write_link() {
3197 let scratch = tempfile::tempdir().unwrap();
3198 let path = scratch.path().to_owned();
3199
3200 create_dir_all(path.join("subdir-a")).unwrap();
3201 File::create_new(path.join("subdir-a/data.txt"))
3202 .unwrap()
3203 .write_all(b"foo")
3204 .unwrap();
3205 create_dir_all(path.join("subdir-b")).unwrap();
3206 File::create_new(path.join("subdir-b/data.txt"))
3207 .unwrap()
3208 .write_all(b"bar")
3209 .unwrap();
3210 let root = path.to_str().unwrap().into();
3211
3212 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3213 BackendOptions::default(),
3214 noop_backing_storage(),
3215 ));
3216
3217 tt.run_once(async move {
3218 let fs = disk_file_system_operation(root)
3219 .resolve()
3220 .strongly_consistent()
3221 .await?;
3222 let root_path = disk_file_system_root(fs);
3223
3224 extract_effects_operation(test_write_link_effect_operation(
3225 fs,
3226 root_path.clone(),
3227 rcstr!("subdir-a"),
3228 ))
3229 .read_strongly_consistent()
3230 .await?
3231 .apply()
3232 .await?;
3233
3234 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
3235 assert_eq!(
3236 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3237 "foo"
3238 );
3239
3240 extract_effects_operation(test_write_link_effect_operation(
3242 fs,
3243 root_path,
3244 rcstr!("subdir-b"),
3245 ))
3246 .read_strongly_consistent()
3247 .await?
3248 .apply()
3249 .await?;
3250
3251 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
3252 assert_eq!(
3253 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3254 "bar"
3255 );
3256
3257 anyhow::Ok(())
3258 })
3259 .await
3260 .unwrap();
3261 }
3262
3263 const STRESS_ITERATIONS: usize = 100;
3264 const STRESS_PARALLELISM: usize = 8;
3265 const STRESS_TARGET_COUNT: usize = 20;
3266 const STRESS_SYMLINK_COUNT: usize = 16;
3267
3268 #[turbo_tasks::function(operation)]
3269 fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
3270 DiskFileSystem::new(rcstr!("test"), Vc::cell(fs_root))
3271 }
3272
3273 fn disk_file_system_root(fs: ResolvedVc<DiskFileSystem>) -> FileSystemPath {
3274 FileSystemPath {
3275 fs: ResolvedVc::upcast(fs),
3276 path: rcstr!(""),
3277 }
3278 }
3279
3280 #[turbo_tasks::function(operation)]
3281 async fn write_symlink_stress_batch(
3282 fs: ResolvedVc<DiskFileSystem>,
3283 symlinks_dir: FileSystemPath,
3284 updates: Vec<(usize, usize)>,
3285 ) -> anyhow::Result<()> {
3286 use turbo_tasks::TryJoinIterExt;
3287
3288 updates
3289 .into_iter()
3290 .map(|(symlink_idx, target_idx)| {
3291 let target = RcStr::from(format!("../_targets/{target_idx}"));
3292 let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
3293 async move {
3294 fs.write_link(
3295 symlink_path,
3296 LinkContent::Link {
3297 target,
3298 link_type: LinkType::DIRECTORY,
3299 }
3300 .cell(),
3301 )
3302 .await
3303 }
3304 })
3305 .try_join()
3306 .await?;
3307 Ok(())
3308 }
3309
3310 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3311 async fn test_symlink_stress() {
3312 let scratch = tempfile::tempdir().unwrap();
3313 let path = scratch.path().to_owned();
3314
3315 let targets_dir = path.join("_targets");
3316 create_dir_all(&targets_dir).unwrap();
3317 for i in 0..STRESS_TARGET_COUNT {
3318 create_dir_all(targets_dir.join(i.to_string())).unwrap();
3319 }
3320 create_dir_all(path.join("_symlinks")).unwrap();
3321
3322 let root = RcStr::from(path.to_str().unwrap());
3323
3324 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3325 BackendOptions::default(),
3326 noop_backing_storage(),
3327 ));
3328
3329 tt.run_once(async move {
3330 let fs = disk_file_system_operation(root)
3331 .resolve()
3332 .strongly_consistent()
3333 .await?;
3334 let root_path = disk_file_system_root(fs);
3335 let symlinks_dir = root_path.join("_symlinks")?;
3336
3337 let initial_updates: Vec<(usize, usize)> =
3338 (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
3339 extract_effects_operation(write_symlink_stress_batch(
3340 fs,
3341 symlinks_dir.clone(),
3342 initial_updates,
3343 ))
3344 .read_strongly_consistent()
3345 .await?
3346 .apply()
3347 .await?;
3348
3349 let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
3350 for _ in 0..STRESS_ITERATIONS {
3351 let mut updates_map = rustc_hash::FxHashMap::default();
3352 for _ in 0..STRESS_PARALLELISM {
3353 let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
3354 let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
3355 updates_map.insert(symlink_idx, target_idx);
3356 }
3357 let updates: Vec<(usize, usize)> = updates_map.into_iter().collect();
3358
3359 extract_effects_operation(write_symlink_stress_batch(
3360 fs,
3361 symlinks_dir.clone(),
3362 updates,
3363 ))
3364 .read_strongly_consistent()
3365 .await?
3366 .apply()
3367 .await?;
3368 }
3369
3370 anyhow::Ok(())
3371 })
3372 .await
3373 .unwrap();
3374
3375 tt.stop_and_wait().await;
3376 }
3377 }
3378
3379 #[cfg(test)]
3381 mod denied_path_tests {
3382 use std::{
3383 fs::{File, create_dir_all, read_to_string},
3384 io::Write,
3385 path::Path,
3386 };
3387
3388 use turbo_rcstr::{RcStr, rcstr};
3389 use turbo_tasks::{Effects, Vc, take_effects};
3390 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3391
3392 use crate::{
3393 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
3394 FileSystemPath,
3395 glob::{Glob, GlobOptions},
3396 };
3397
3398 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
3401 let scratch = tempfile::tempdir().unwrap();
3402 let path = scratch.path();
3403
3404 File::create_new(path.join("allowed_file.txt"))
3411 .unwrap()
3412 .write_all(b"allowed content")
3413 .unwrap();
3414
3415 create_dir_all(path.join("allowed_dir")).unwrap();
3416 File::create_new(path.join("allowed_dir/file.txt"))
3417 .unwrap()
3418 .write_all(b"allowed dir content")
3419 .unwrap();
3420
3421 File::create_new(path.join("other_file.txt"))
3422 .unwrap()
3423 .write_all(b"other content")
3424 .unwrap();
3425
3426 create_dir_all(path.join("denied_dir/nested")).unwrap();
3427 File::create_new(path.join("denied_dir/secret.txt"))
3428 .unwrap()
3429 .write_all(b"secret content")
3430 .unwrap();
3431 File::create_new(path.join("denied_dir/nested/deep.txt"))
3432 .unwrap()
3433 .write_all(b"deep secret")
3434 .unwrap();
3435
3436 let root = RcStr::from(path.to_str().unwrap());
3437 let denied_path = rcstr!("denied_dir");
3439
3440 (scratch, root, denied_path)
3441 }
3442
3443 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3444 async fn test_denied_path_read() {
3445 #[turbo_tasks::function(operation)]
3446 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3447 let fs = DiskFileSystem::new_with_denied_paths(
3448 rcstr!("test"),
3449 Vc::cell(root),
3450 vec![denied_path],
3451 );
3452 let root_path = fs.root().await?;
3453
3454 let allowed_file = root_path.join("allowed_file.txt")?;
3456 let content = allowed_file.read().await?;
3457 assert!(
3458 matches!(&*content, FileContent::Content(_)),
3459 "allowed file should be readable"
3460 );
3461
3462 let denied_file = root_path.join("denied_dir/secret.txt")?;
3464 let content = denied_file.read().await?;
3465 assert!(
3466 matches!(&*content, FileContent::NotFound),
3467 "denied file should return NotFound, got {:?}",
3468 content
3469 );
3470
3471 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
3473 let content = nested_denied.read().await?;
3474 assert!(
3475 matches!(&*content, FileContent::NotFound),
3476 "nested denied file should return NotFound"
3477 );
3478
3479 let denied_dir = root_path.join("denied_dir")?;
3481 let content = denied_dir.read().await?;
3482 assert!(
3483 matches!(&*content, FileContent::NotFound),
3484 "denied directory should return NotFound"
3485 );
3486
3487 Ok(())
3488 }
3489
3490 let (_scratch, root, denied_path) = setup_test_fs();
3491 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3492 BackendOptions::default(),
3493 noop_backing_storage(),
3494 ));
3495 tt.run_once(async {
3496 test_operation(root, denied_path)
3497 .read_strongly_consistent()
3498 .await?;
3499
3500 anyhow::Ok(())
3501 })
3502 .await
3503 .unwrap();
3504 }
3505
3506 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3507 async fn test_denied_path_read_dir() {
3508 #[turbo_tasks::function(operation)]
3509 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3510 let fs = DiskFileSystem::new_with_denied_paths(
3511 rcstr!("test"),
3512 Vc::cell(root),
3513 vec![denied_path],
3514 );
3515 let root_path = fs.root().await?;
3516
3517 let dir_content = root_path.read_dir().await?;
3519 match &*dir_content {
3520 DirectoryContent::Entries(entries) => {
3521 assert!(
3522 entries.contains_key(&rcstr!("allowed_dir")),
3523 "allowed_dir should be visible"
3524 );
3525 assert!(
3526 entries.contains_key(&rcstr!("other_file.txt")),
3527 "other_file.txt should be visible"
3528 );
3529 assert!(
3530 entries.contains_key(&rcstr!("allowed_file.txt")),
3531 "allowed_file.txt should be visible"
3532 );
3533 assert!(
3534 !entries.contains_key(&rcstr!("denied_dir")),
3535 "denied_dir should NOT be visible in read_dir"
3536 );
3537 }
3538 DirectoryContent::NotFound => panic!("root directory should exist"),
3539 }
3540
3541 let denied_dir = root_path.join("denied_dir")?;
3543 let dir_content = denied_dir.read_dir().await?;
3544 assert!(
3545 matches!(&*dir_content, DirectoryContent::NotFound),
3546 "denied_dir read_dir should return NotFound"
3547 );
3548
3549 Ok(())
3550 }
3551
3552 let (_scratch, root, denied_path) = setup_test_fs();
3553 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3554 BackendOptions::default(),
3555 noop_backing_storage(),
3556 ));
3557 tt.run_once(async {
3558 test_operation(root, denied_path)
3559 .read_strongly_consistent()
3560 .await?;
3561
3562 anyhow::Ok(())
3563 })
3564 .await
3565 .unwrap();
3566 }
3567
3568 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3569 async fn test_denied_path_read_glob() {
3570 #[turbo_tasks::function(operation)]
3571 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3572 let fs = DiskFileSystem::new_with_denied_paths(
3573 rcstr!("test"),
3574 Vc::cell(root),
3575 vec![denied_path],
3576 );
3577 let root_path = fs.root().await?;
3578
3579 let glob_result = root_path
3581 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3582 .await?;
3583
3584 assert!(
3586 glob_result.results.contains_key("allowed_file.txt"),
3587 "allowed_file.txt should be found"
3588 );
3589 assert!(
3590 glob_result.results.contains_key("other_file.txt"),
3591 "other_file.txt should be found"
3592 );
3593 assert!(
3594 !glob_result.results.contains_key("denied_dir"),
3595 "denied_dir should NOT appear in glob results"
3596 );
3597
3598 assert!(
3600 !glob_result.inner.contains_key("denied_dir"),
3601 "denied_dir should NOT appear in glob inner results"
3602 );
3603
3604 assert!(
3606 glob_result.inner.contains_key("allowed_dir"),
3607 "allowed_dir directory should be present"
3608 );
3609 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3610 assert!(
3611 sub_inner.results.contains_key("file.txt"),
3612 "allowed_dir/file.txt should be found"
3613 );
3614
3615 Ok(())
3616 }
3617
3618 let (_scratch, root, denied_path) = setup_test_fs();
3619 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3620 BackendOptions::default(),
3621 noop_backing_storage(),
3622 ));
3623 tt.run_once(async {
3624 test_operation(root, denied_path)
3625 .read_strongly_consistent()
3626 .await?;
3627
3628 anyhow::Ok(())
3629 })
3630 .await
3631 .unwrap();
3632 }
3633
3634 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3635 async fn test_denied_path_write() {
3636 #[turbo_tasks::function(operation)]
3637 async fn write_file_operation(
3638 path: FileSystemPath,
3639 contents: RcStr,
3640 ) -> anyhow::Result<()> {
3641 path.write(
3642 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3643 .cell(),
3644 )
3645 .await?;
3646 Ok(())
3647 }
3648
3649 #[turbo_tasks::function(operation)]
3652 async fn write_allowed_file_operation(
3653 root: RcStr,
3654 denied_path: RcStr,
3655 file_path: RcStr,
3656 contents: RcStr,
3657 ) -> anyhow::Result<Vc<Effects>> {
3658 let fs = DiskFileSystem::new_with_denied_paths(
3659 rcstr!("test"),
3660 Vc::cell(root),
3661 vec![denied_path],
3662 );
3663 let root_path = fs.root().await?;
3664 let allowed_file = root_path.join(&file_path)?;
3665 let write_op = write_file_operation(allowed_file, contents);
3666 write_op.read_strongly_consistent().await?;
3667 Ok(take_effects(write_op).await?.cell())
3668 }
3669
3670 #[turbo_tasks::function(operation)]
3671 async fn test_denied_writes_operation(
3672 root: RcStr,
3673 denied_path: RcStr,
3674 denied_file: RcStr,
3675 nested_denied_file: RcStr,
3676 ) -> anyhow::Result<()> {
3677 let fs = DiskFileSystem::new_with_denied_paths(
3678 rcstr!("test"),
3679 Vc::cell(root),
3680 vec![denied_path],
3681 );
3682 let root_path = fs.root().await?;
3683
3684 let path = root_path.join(&denied_file)?;
3685 let result = write_file_operation(path, rcstr!("forbidden"))
3686 .read_strongly_consistent()
3687 .await;
3688 assert!(
3689 result.is_err(),
3690 "writing to denied path should return an error"
3691 );
3692
3693 let path = root_path.join(&nested_denied_file)?;
3694 let result = write_file_operation(path, rcstr!("nested"))
3695 .read_strongly_consistent()
3696 .await;
3697 assert!(
3698 result.is_err(),
3699 "writing to nested denied path should return an error"
3700 );
3701
3702 Ok(())
3703 }
3704
3705 let (_scratch, root, denied_path) = setup_test_fs();
3706 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3707 BackendOptions::default(),
3708 noop_backing_storage(),
3709 ));
3710 tt.run_once(async {
3711 const ALLOWED_FILE: &str = "allowed_dir/new_file.txt";
3712 const TEST_CONTENT: &str = "test content";
3713
3714 let effects = write_allowed_file_operation(
3716 root.clone(),
3717 denied_path.clone(),
3718 RcStr::from(ALLOWED_FILE),
3719 RcStr::from(TEST_CONTENT),
3720 )
3721 .read_strongly_consistent()
3722 .await?;
3723 effects.apply().await?;
3724
3725 let content = read_to_string(Path::new(root.as_str()).join(ALLOWED_FILE))?;
3727 assert_eq!(content, TEST_CONTENT, "allowed file write should succeed");
3728
3729 test_denied_writes_operation(
3731 root,
3732 denied_path,
3733 RcStr::from("denied_dir/forbidden.txt"),
3734 RcStr::from("denied_dir/nested/file.txt"),
3735 )
3736 .read_strongly_consistent()
3737 .await?;
3738
3739 anyhow::Ok(())
3740 })
3741 .await
3742 .unwrap();
3743 }
3744 }
3745}