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 async_trait::async_trait;
50use auto_hash_map::{AutoMap, AutoSet};
51use bincode::{Decode, Encode};
52use bitflags::bitflags;
53use dunce::simplified;
54use indexmap::IndexSet;
55use jsonc_parser::{ParseOptions, parse_to_serde_value};
56use mime::Mime;
57use rustc_hash::FxHashSet;
58use serde_json::Value;
59use tokio::{
60 runtime::Handle,
61 sync::{RwLock, RwLockReadGuard},
62};
63use tracing::Instrument;
64use turbo_rcstr::{RcStr, rcstr};
65use turbo_tasks::{
66 CapturedEffect, Completion, Effect, EffectExt, EffectStateStorage, InvalidationReason,
67 NonLocalValue, ReadRef, ResolvedVc, TurboTasksApi, ValueToString, ValueToStringRef, Vc,
68 debug::ValueDebugFormat, parallel, trace::TraceRawVcs, turbo_tasks_weak, turbobail, 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_unchecked(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)]
510#[value_to_string(self.inner.name)]
511#[turbo_tasks::value(cell = "new", eq = "manual", evict = "never")]
512pub struct DiskFileSystem {
513 inner: Arc<DiskFileSystemInner>,
514}
515
516impl DiskFileSystem {
517 pub fn name(&self) -> &RcStr {
518 &self.inner.name
519 }
520
521 pub fn root(&self) -> &RcStr {
522 &self.inner.root
523 }
524
525 pub fn invalidate(&self) {
526 self.inner.invalidate();
527 }
528
529 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
530 &self,
531 reason: impl Fn(&Path) -> R + Sync,
532 ) {
533 self.inner.invalidate_with_reason(reason);
534 }
535
536 pub fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
537 &self,
538 paths: impl IntoIterator<Item = PathBuf>,
539 reason: impl Fn(&Path) -> R + Sync,
540 ) {
541 self.inner
542 .invalidate_path_and_children_with_reason(paths, reason);
543 }
544
545 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
546 self.inner
547 .start_watching_internal(false, poll_interval)
548 .await
549 }
550
551 pub async fn start_watching_with_invalidation_reason(
552 &self,
553 poll_interval: Option<Duration>,
554 ) -> Result<()> {
555 self.inner
556 .start_watching_internal(true, poll_interval)
557 .await
558 }
559
560 pub async fn stop_watching(&self) {
561 self.inner.watcher.stop_watching().await;
562 }
563
564 pub fn try_from_sys_path(
577 &self,
578 vc_self: ResolvedVc<DiskFileSystem>,
579 sys_path: &Path,
580 relative_to: Option<&FileSystemPath>,
581 ) -> Option<FileSystemPath> {
582 let vc_self = ResolvedVc::upcast(vc_self);
583
584 let sys_path = simplified(sys_path);
585 let relative_sys_path = if sys_path.is_absolute() {
586 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
589 normalized_sys_path
590 .strip_prefix(self.inner.root_path())
591 .ok()?
592 .to_owned()
593 } else if let Some(relative_to) = relative_to {
594 debug_assert_eq!(
595 relative_to.fs, vc_self,
596 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
597 );
598 let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
599 joined_sys_path.push(sys_path);
600 joined_sys_path.normalize_lexically().ok()?
601 } else {
602 sys_path.normalize_lexically().ok()?
603 };
604
605 Some(FileSystemPath {
606 fs: vc_self,
607 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
608 })
609 }
610
611 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
612 let path = self.inner.root_path();
613 if fs_path.path.is_empty() {
614 path.to_path_buf()
615 } else {
616 path.join(&*unix_to_sys(&fs_path.path))
617 }
618 }
619}
620
621#[allow(dead_code, reason = "we need to hold onto the locks")]
622struct PathLockGuard<'a>(
623 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
624 #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
625);
626
627fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
628 if let Ok(rel_path) = path.strip_prefix(root_path) {
629 let path = if MAIN_SEPARATOR != '/' {
630 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
631 format!("[{name}]/{rel_path}")
632 } else {
633 format!("[{name}]/{}", rel_path.display())
634 };
635 Some(path)
636 } else {
637 None
638 }
639}
640
641impl DiskFileSystem {
642 pub fn new(name: RcStr, root: Vc<RcStr>) -> Vc<Self> {
649 Self::new_internal(name, root, Vec::new())
650 }
651
652 pub fn new_with_denied_paths(
661 name: RcStr,
662 root: Vc<RcStr>,
663 denied_paths: Vec<RcStr>,
664 ) -> Vc<Self> {
665 for denied_path in &denied_paths {
666 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
667 debug_assert!(
668 normalize_path(denied_path).as_deref() == Some(&**denied_path),
669 "denied_path must be normalized: {denied_path:?}"
670 );
671 }
672 Self::new_internal(name, root, denied_paths)
673 }
674}
675
676#[turbo_tasks::value_impl]
677impl DiskFileSystem {
678 #[turbo_tasks::function]
679 async fn new_internal(
680 name: RcStr,
681 root: Vc<RcStr>,
682 denied_paths: Vec<RcStr>,
683 ) -> Result<Vc<Self>> {
684 let root = root.owned().await?;
685 let instance = DiskFileSystem {
686 inner: Arc::new(DiskFileSystemInner {
687 name,
688 root,
689 mutex_map: Default::default(),
690 invalidation_lock: Default::default(),
691 invalidator_map: InvalidatorMap::new(),
692 dir_invalidator_map: InvalidatorMap::new(),
693 read_semaphore: create_read_semaphore(),
694 write_semaphore: create_write_semaphore(),
695 watcher: DiskWatcher::new(),
696 denied_paths,
697 turbo_tasks: turbo_tasks_weak(),
698 tokio_handle: Handle::current(),
699 effect_state_storage: EffectStateStorage::default(),
700 }),
701 };
702
703 Ok(Self::cell(instance))
704 }
705}
706
707impl Debug for DiskFileSystem {
708 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
709 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
710 }
711}
712
713#[turbo_tasks::value_impl]
714impl FileSystem for DiskFileSystem {
715 #[turbo_tasks::function(fs, session_dependent)]
716 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
717 if self.inner.is_path_denied(&fs_path) {
719 return Ok(FileContent::NotFound.cell());
720 }
721 let full_path = self.to_sys_path(&fs_path);
722
723 self.inner.register_read_invalidator(&full_path).await?;
724
725 let _lock = self.inner.lock_path(&full_path).await;
726 let content = match retry_blocking(|| File::from_path(&full_path))
727 .instrument(tracing::info_span!("read file", name = ?full_path))
728 .concurrency_limited(&self.inner.read_semaphore)
729 .await
730 {
731 Ok(file) => FileContent::new(file),
732 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
733 FileContent::NotFound
734 }
735 Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
737 };
738 Ok(content.cell())
739 }
740
741 #[turbo_tasks::function(fs, session_dependent)]
742 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
743 if self.inner.is_path_denied(&fs_path) {
745 return Ok(RawDirectoryContent::not_found());
746 }
747 let full_path = self.to_sys_path(&fs_path);
748
749 self.inner.register_dir_invalidator(&full_path).await?;
750
751 let read_dir = match retry_blocking(|| std::fs::read_dir(&full_path))
753 .instrument(tracing::info_span!("read directory", name = ?full_path))
754 .concurrency_limited(&self.inner.read_semaphore)
755 .await
756 {
757 Ok(dir) => dir,
758 Err(e)
759 if e.kind() == ErrorKind::NotFound
760 || e.kind() == ErrorKind::NotADirectory
761 || e.kind() == ErrorKind::InvalidFilename =>
762 {
763 return Ok(RawDirectoryContent::not_found());
764 }
765 Err(e) => {
766 return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
768 }
769 };
770 let dir_path = fs_path.path.as_str();
771 let denied_entries: FxHashSet<&str> = self
772 .inner
773 .denied_paths
774 .iter()
775 .filter_map(|denied_path| {
776 if denied_path.starts_with(dir_path) {
783 let denied_path_suffix =
784 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
785 Some(&denied_path[dir_path.len() + 1..])
786 } else if dir_path.is_empty() {
787 Some(denied_path.as_str())
788 } else {
789 None
790 };
791 denied_path_suffix.filter(|s| !s.contains('/'))
793 } else {
794 None
795 }
796 })
797 .collect();
798
799 let entries = read_dir
800 .filter_map(|r| {
801 let e = match r {
802 Ok(e) => e,
803 Err(err) => return Some(Err(err.into())),
804 };
805
806 let file_name = RcStr::from(e.file_name().to_str()?);
808 if denied_entries.contains(file_name.as_str()) {
810 return None;
811 }
812
813 let entry = match e.file_type() {
814 Ok(t) if t.is_file() => RawDirectoryEntry::File,
815 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
816 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
817 Ok(_) => RawDirectoryEntry::Other,
818 Err(err) => return Some(Err(err.into())),
819 };
820
821 Some(anyhow::Ok((file_name, entry)))
822 })
823 .collect::<Result<_>>()
824 .with_context(|| format!("reading directory item in {full_path:?}"))?;
825
826 Ok(RawDirectoryContent::new(entries))
827 }
828
829 #[turbo_tasks::function(fs, session_dependent)]
830 async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
831 if self.inner.is_path_denied(&fs_path) {
833 return Ok(LinkContent::NotFound.cell());
834 }
835 let full_path = self.to_sys_path(&fs_path);
836
837 self.inner.register_read_invalidator(&full_path).await?;
838
839 let _lock = self.inner.lock_path(&full_path).await;
840 let link_path = match retry_blocking(|| std::fs::read_link(&full_path))
841 .instrument(tracing::info_span!("read symlink", name = ?full_path))
842 .concurrency_limited(&self.inner.read_semaphore)
843 .await
844 {
845 Ok(res) => res,
846 Err(_) => return Ok(LinkContent::NotFound.cell()),
847 };
848 let is_link_absolute = link_path.is_absolute();
849
850 let mut file = link_path.clone();
851 if !is_link_absolute {
852 if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
853 normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
854 }) {
855 #[cfg(windows)]
856 {
857 file = PathBuf::from(normalized_linked_path);
858 }
859 #[cfg(not(windows))]
862 {
863 file = PathBuf::from(format!("/{normalized_linked_path}"));
864 }
865 } else {
866 return Ok(LinkContent::Invalid.cell());
867 }
868 }
869
870 let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
877
878 let relative_to_root_path = match result {
879 Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
880 Err(_) => return Ok(LinkContent::Invalid.cell()),
881 };
882
883 let (target, file_type) = if is_link_absolute {
884 let target_string = RcStr::from(relative_to_root_path.to_string_lossy());
885 (
886 target_string.clone(),
887 FileSystemPath::new_normalized_unchecked(
888 fs_path.fs().to_resolved().await?,
889 target_string,
890 )
891 .get_type()
892 .await?,
893 )
894 } else {
895 let link_path_string_cow = link_path.to_string_lossy();
896 let link_path_unix = RcStr::from(sys_to_unix(&link_path_string_cow));
897 (
898 link_path_unix.clone(),
899 fs_path.parent().join(&link_path_unix)?.get_type().await?,
900 )
901 };
902
903 Ok(LinkContent::Link {
904 target,
905 link_type: {
906 let mut link_type = Default::default();
907 if link_path.is_absolute() {
908 link_type |= LinkType::ABSOLUTE;
909 }
910 if matches!(&*file_type, FileSystemEntryType::Directory) {
911 link_type |= LinkType::DIRECTORY;
912 }
913 link_type
914 },
915 }
916 .cell())
917 }
918
919 #[turbo_tasks::function(fs)]
920 async fn write(
921 self: ResolvedVc<Self>,
922 fs_path: FileSystemPath,
923 content: ResolvedVc<FileContent>,
924 ) -> Result<()> {
925 let this = self.await?;
926 if this.inner.is_path_denied(&fs_path) {
932 turbobail!("Cannot write to denied path: {fs_path}");
933 }
934 let full_path = this.to_sys_path(&fs_path);
935
936 let content = content.persist().to_resolved().await?;
942 let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*content.await?));
943
944 #[turbo_tasks::value(eq = "manual", cell = "new")]
945 struct WriteEffect {
946 full_path: Arc<PathBuf>,
947 fs: ResolvedVc<DiskFileSystem>,
948 content: ResolvedVc<PersistedFileContent>,
949 content_hash: u128,
950 }
951
952 #[async_trait]
953 #[turbo_tasks::value_impl]
954 impl Effect for WriteEffect {
955 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
956 let inner = (*self.fs).untracked().await?.inner.clone();
959
960 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
967 let content = if inner
968 .effect_state_storage
969 .matches_applied(&key_bytes, self.content_hash)
970 {
971 None
972 } else {
973 Some((*self.content).untracked().await?)
977 };
978 Ok(Box::new(CapturedWriteEffect {
979 full_path: self.full_path.clone(),
980 inner,
981 content,
982 content_hash: self.content_hash,
983 }) as Box<dyn CapturedEffect>)
984 }
985 }
986
987 #[derive(TraceRawVcs, NonLocalValue, Clone)]
988 struct CapturedWriteEffect {
989 full_path: Arc<PathBuf>,
990 inner: Arc<DiskFileSystemInner>,
991 content: Option<ReadRef<PersistedFileContent>>,
992 content_hash: u128,
993 }
994
995 #[async_trait]
996 impl CapturedEffect for CapturedWriteEffect {
997 fn key(&self) -> Box<[u8]> {
998 self.full_path.as_os_str().as_encoded_bytes().into()
999 }
1000
1001 fn value_hash(&self) -> u128 {
1002 self.content_hash
1003 }
1004
1005 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1006 let body = self.content.as_ref().map(|content| {
1007 || async { self.apply_inner(content).await.map_err(AnyhowWrapper::from) }
1008 });
1009 self.inner
1010 .effect_state_storage
1011 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1012 .await
1013 }
1014 }
1015
1016 impl CapturedWriteEffect {
1017 async fn apply_inner(
1018 &self,
1019 content: &ReadRef<PersistedFileContent>,
1020 ) -> anyhow::Result<()> {
1021 let full_path = validate_path_length(&self.full_path)?;
1022
1023 let _lock = self.inner.lock_path(&full_path).await;
1024
1025 let compare = content
1031 .streaming_compare(&full_path)
1032 .instrument(tracing::info_span!("read file before write", name = ?full_path))
1033 .concurrency_limited(&self.inner.read_semaphore)
1034 .await?;
1035 if compare == FileComparison::Equal {
1036 return Ok(());
1037 }
1038
1039 match &**content {
1040 PersistedFileContent::Content(..) => {
1041 let content = content.clone();
1042 let full_path = full_path.into_owned();
1043 async {
1044 let do_write = || {
1045 let content = content.clone();
1046 let full_path = full_path.clone();
1047 let span = tracing::info_span!("write file", name = ?full_path);
1048 retry_blocking(move || {
1049 let mut f = std::fs::File::create(&full_path)?;
1050 let PersistedFileContent::Content(file) = &*content else {
1051 unreachable!()
1052 };
1053 std::io::copy(&mut file.read(), &mut f)?;
1054 #[cfg(unix)]
1055 f.set_permissions(file.meta.permissions.into())?;
1056 f.flush()?;
1057
1058 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
1059 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
1060 .is_some_and(|v| v == "1" || v == "true")
1061 });
1062 if *WRITE_VERSION {
1063 let mut full_path = full_path.clone();
1064 let hash = hash_xxh3_hash64(file);
1065 let ext = full_path.extension();
1066 let ext = if let Some(ext) = ext {
1067 format!("{:016x}.{}", hash, ext.to_string_lossy())
1068 } else {
1069 format!("{hash:016x}")
1070 };
1071 full_path.set_extension(ext);
1072 let mut f = std::fs::File::create(&full_path)?;
1073 std::io::copy(&mut file.read(), &mut f)?;
1074 #[cfg(unix)]
1075 f.set_permissions(file.meta.permissions.into())?;
1076 f.flush()?;
1077 }
1078 Ok::<(), io::Error>(())
1079 })
1080 .instrument(span)
1081 };
1082
1083 match do_write().await {
1084 Err(e) if e.kind() == ErrorKind::NotFound => {
1085 if let Some(parent) = full_path.parent() {
1087 retry_blocking(|| std::fs::create_dir_all(parent))
1088 .instrument(tracing::info_span!(
1089 "create directory",
1090 name = ?parent
1091 ))
1092 .await
1093 .with_context(|| {
1094 format!(
1095 "failed to create directory {parent:?} for \
1096 write to {full_path:?}",
1097 )
1098 })?;
1099 }
1100 do_write().await.with_context(|| {
1101 format!("failed to write to {full_path:?}")
1102 })?;
1103 }
1104 result => {
1105 result.with_context(|| {
1106 format!("failed to write to {full_path:?}")
1107 })?;
1108 }
1109 }
1110 anyhow::Ok(())
1111 }
1112 .concurrency_limited(&self.inner.write_semaphore)
1113 .await?;
1114 }
1115 PersistedFileContent::NotFound => {
1116 retry_blocking(|| std::fs::remove_file(&full_path))
1117 .instrument(tracing::info_span!("remove file", name = ?full_path))
1118 .concurrency_limited(&self.inner.write_semaphore)
1119 .await
1120 .or_else(|err| {
1121 if err.kind() == ErrorKind::NotFound {
1122 Ok(())
1123 } else {
1124 Err(err)
1125 }
1126 })
1127 .with_context(|| format!("removing {full_path:?} failed"))?;
1128 }
1129 }
1130
1131 self.inner.invalidate_from_write(&self.full_path);
1133
1134 Ok(())
1135 }
1136 }
1137
1138 WriteEffect {
1139 full_path: Arc::new(full_path),
1140 fs: self,
1141 content,
1142 content_hash,
1143 }
1144 .resolved_cell()
1145 .emit();
1146
1147 Ok(())
1148 }
1149
1150 #[turbo_tasks::function(fs)]
1151 async fn write_link(
1152 self: ResolvedVc<Self>,
1153 fs_path: FileSystemPath,
1154 target: ResolvedVc<LinkContent>,
1155 ) -> Result<()> {
1156 let this = self.await?;
1161 if this.inner.is_path_denied(&fs_path) {
1163 turbobail!("Cannot write link to denied path: {fs_path}");
1164 }
1165 let full_path = this.to_sys_path(&fs_path);
1166
1167 let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*target.await?));
1168
1169 #[turbo_tasks::value(eq = "manual", cell = "new")]
1170 struct WriteLinkEffect {
1171 full_path: Arc<PathBuf>,
1172 fs: ResolvedVc<DiskFileSystem>,
1173 target: ResolvedVc<LinkContent>,
1174 content_hash: u128,
1175 }
1176
1177 #[async_trait]
1178 #[turbo_tasks::value_impl]
1179 impl Effect for WriteLinkEffect {
1180 async fn capture(&self) -> Result<Box<dyn CapturedEffect>> {
1181 let inner = (*self.fs).untracked().await?.inner.clone();
1182
1183 let key_bytes: Box<[u8]> = self.full_path.as_os_str().as_encoded_bytes().into();
1186 let content = if inner
1187 .effect_state_storage
1188 .matches_applied(&key_bytes, self.content_hash)
1189 {
1190 None
1191 } else {
1192 Some((*self.target).untracked().await?)
1194 };
1195 Ok(Box::new(CapturedWriteLinkEffect {
1196 full_path: self.full_path.clone(),
1197 inner,
1198 content,
1199 content_hash: self.content_hash,
1200 }) as Box<dyn CapturedEffect>)
1201 }
1202 }
1203
1204 #[derive(TraceRawVcs, NonLocalValue, Clone)]
1206 struct CapturedWriteLinkEffect {
1207 full_path: Arc<PathBuf>,
1208 inner: Arc<DiskFileSystemInner>,
1209 content: Option<ReadRef<LinkContent>>,
1210 content_hash: u128,
1211 }
1212
1213 #[async_trait]
1214 impl CapturedEffect for CapturedWriteLinkEffect {
1215 fn key(&self) -> Box<[u8]> {
1216 self.full_path.as_os_str().as_encoded_bytes().into()
1217 }
1218
1219 fn value_hash(&self) -> u128 {
1220 self.content_hash
1221 }
1222
1223 async fn apply(&self) -> Result<(), turbo_tasks::ApplyError> {
1224 let body = self.content.as_ref().map(|content| {
1225 || async { self.apply_inner(content).await.map_err(AnyhowWrapper::from) }
1226 });
1227 self.inner
1228 .effect_state_storage
1229 .run_apply::<AnyhowWrapper, _, _>(self.key(), self.content_hash, body)
1230 .await
1231 }
1232 }
1233
1234 impl CapturedWriteLinkEffect {
1235 async fn apply_inner(&self, content: &ReadRef<LinkContent>) -> anyhow::Result<()> {
1236 let full_path = validate_path_length(&self.full_path)?;
1237
1238 let _lock = self.inner.lock_path(&full_path).await;
1239
1240 enum OsSpecificLinkContent {
1241 Link {
1242 #[cfg(windows)]
1243 is_directory: bool,
1244 target: PathBuf,
1245 },
1246 NotFound,
1247 Invalid,
1248 }
1249
1250 let os_specific_link_content = match &**content {
1251 LinkContent::Link { target, link_type } => {
1252 let is_directory = link_type.contains(LinkType::DIRECTORY);
1253 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1254 Path::new(&self.inner.root).join(unix_to_sys(target).as_ref())
1255 } else {
1256 let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1257 if cfg!(windows) && is_directory {
1258 full_path
1260 .parent()
1261 .unwrap_or(&full_path)
1262 .join(relative_target)
1263 } else {
1264 relative_target
1265 }
1266 };
1267 OsSpecificLinkContent::Link {
1268 #[cfg(windows)]
1269 is_directory,
1270 target: target_path,
1271 }
1272 }
1273 LinkContent::Invalid => OsSpecificLinkContent::Invalid,
1274 LinkContent::NotFound => OsSpecificLinkContent::NotFound,
1275 };
1276
1277 let old_content = match retry_blocking(|| std::fs::read_link(&full_path))
1278 .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1279 .concurrency_limited(&self.inner.read_semaphore)
1280 .await
1281 {
1282 Ok(res) => Some((res.is_absolute(), res)),
1283 Err(_) => None,
1284 };
1285 let is_equal = match (&os_specific_link_content, &old_content) {
1286 (
1287 OsSpecificLinkContent::Link { target, .. },
1288 Some((old_is_absolute, old_target)),
1289 ) => target == old_target && target.is_absolute() == *old_is_absolute,
1290 (OsSpecificLinkContent::NotFound, None) => true,
1291 _ => false,
1292 };
1293 if is_equal {
1294 return Ok(());
1295 }
1296
1297 match os_specific_link_content {
1298 OsSpecificLinkContent::Link {
1299 target,
1300 #[cfg(windows)]
1301 is_directory,
1302 ..
1303 } => {
1304 let full_path = full_path.into_owned();
1305
1306 #[derive(thiserror::Error, Debug)]
1307 #[error("{msg}: {source}")]
1308 struct SymlinkCreationError {
1309 msg: &'static str,
1310 #[source]
1311 source: io::Error,
1312 }
1313
1314 let mut has_old_content = old_content.is_some();
1315 let try_create_link = || {
1316 if has_old_content {
1317 remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1322 SymlinkCreationError {
1323 msg: "removal of existing symbolic link or junction point \
1324 failed",
1325 source: err,
1326 }
1327 })?;
1328 has_old_content = false;
1329 }
1330 #[cfg(not(windows))]
1331 let io_result = std::os::unix::fs::symlink(&target, &full_path);
1332 #[cfg(windows)]
1333 let io_result = if is_directory {
1334 std::os::windows::fs::junction_point(&target, &full_path)
1335 } else {
1336 std::os::windows::fs::symlink_file(&target, &full_path)
1337 };
1338 io_result.map_err(|err| {
1339 if err.kind() == ErrorKind::AlreadyExists {
1340 has_old_content = true;
1342 }
1343 SymlinkCreationError {
1344 msg: "creation of a new symbolic link or junction point failed",
1345 source: err,
1346 }
1347 })
1348 };
1349 fn can_retry_link(err: &SymlinkCreationError) -> bool {
1350 err.source.kind() == ErrorKind::AlreadyExists || can_retry(&err.source)
1351 }
1352 let err_context = || {
1353 #[cfg(not(windows))]
1354 let message = format!(
1355 "failed to create symlink at {full_path:?} pointing to {target:?}"
1356 );
1357 #[cfg(windows)]
1358 let message = if is_directory {
1359 format!(
1360 "failed to create junction point at {full_path:?} pointing to \
1361 {target:?}"
1362 )
1363 } else {
1364 format!(
1365 "failed to create symlink at {full_path:?} pointing to \
1366 {target:?}\n\
1367 (Note: creating file symlinks on Windows require developer \
1368 mode or admin permissions: \
1369 https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1370 )
1371 };
1372 message
1373 };
1374 async {
1375 let write_result =
1376 retry_blocking_custom(try_create_link, can_retry_link)
1377 .instrument(tracing::info_span!(
1378 "write symlink",
1379 name = ?full_path,
1380 target = ?target,
1381 ))
1382 .await;
1383
1384 match write_result {
1385 Err(ref e) if e.source.kind() == ErrorKind::NotFound => {
1386 if let Some(parent) = full_path.parent() {
1388 retry_blocking(|| std::fs::create_dir_all(parent))
1389 .instrument(tracing::info_span!(
1390 "create directory",
1391 name = ?parent
1392 ))
1393 .await
1394 .with_context(|| {
1395 format!(
1396 "failed to create directory {parent:?} for \
1397 write link to {full_path:?}",
1398 )
1399 })?;
1400 }
1401 retry_blocking_custom(
1404 || {
1405 #[cfg(not(windows))]
1406 let io_result =
1407 std::os::unix::fs::symlink(&target, &full_path);
1408 #[cfg(windows)]
1409 let io_result = if is_directory {
1410 std::os::windows::fs::junction_point(
1411 &target, &full_path,
1412 )
1413 } else {
1414 std::os::windows::fs::symlink_file(
1415 &target, &full_path,
1416 )
1417 };
1418 io_result.map_err(|err| SymlinkCreationError {
1419 msg: "creation of a new symbolic link or junction \
1420 point failed",
1421 source: err,
1422 })
1423 },
1424 |e: &SymlinkCreationError| can_retry(&e.source),
1425 )
1426 .instrument(tracing::info_span!(
1427 "write symlink",
1428 name = ?full_path,
1429 target = ?target,
1430 ))
1431 .await
1432 .with_context(err_context)?;
1433 }
1434 result => result.with_context(err_context)?,
1435 }
1436 anyhow::Ok(())
1437 }
1438 .concurrency_limited(&self.inner.write_semaphore)
1439 .await?;
1440 }
1441 OsSpecificLinkContent::Invalid => {
1442 bail!("invalid symlink target: {full_path:?}");
1443 }
1444 OsSpecificLinkContent::NotFound => {
1445 retry_blocking(|| remove_symbolic_link_dir_helper(&full_path))
1446 .instrument(tracing::info_span!("remove symlink", name = ?full_path))
1447 .concurrency_limited(&self.inner.write_semaphore)
1448 .await
1449 .with_context(|| format!("removing {full_path:?} failed"))?;
1450 }
1451 }
1452
1453 self.inner.invalidate_from_write(&self.full_path);
1455
1456 Ok(())
1457 }
1458 }
1459
1460 WriteLinkEffect {
1461 full_path: Arc::new(full_path),
1462 fs: self,
1463 target,
1464 content_hash,
1465 }
1466 .resolved_cell()
1467 .emit();
1468 Ok(())
1469 }
1470
1471 #[turbo_tasks::function(fs, session_dependent)]
1472 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1473 let full_path = self.to_sys_path(&fs_path);
1474
1475 if self.inner.is_path_denied(&fs_path) {
1477 turbobail!("Cannot read metadata from denied path: {fs_path}");
1478 }
1479
1480 self.inner.register_read_invalidator(&full_path).await?;
1481
1482 let _lock = self.inner.lock_path(&full_path).await;
1483 let meta = retry_blocking(|| std::fs::metadata(&full_path))
1484 .instrument(tracing::info_span!("read metadata", name = ?full_path))
1485 .concurrency_limited(&self.inner.read_semaphore)
1486 .await
1487 .with_context(|| format!("reading metadata for {:?}", full_path))?;
1488
1489 Ok(FileMeta::cell(meta.into()))
1490 }
1491}
1492
1493fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1494 let result = if cfg!(windows) {
1495 std::fs::remove_dir(path).or_else(|err| {
1508 if err.kind() == ErrorKind::NotADirectory {
1509 std::fs::remove_file(path)
1510 } else {
1511 Err(err)
1512 }
1513 })
1514 } else {
1515 std::fs::remove_file(path)
1516 };
1517 match result {
1518 Ok(()) => Ok(()),
1519 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1520 Err(err) => Err(err),
1521 }
1522}
1523
1524#[derive(Debug, Clone, Hash)]
1525#[turbo_tasks::value(shared, task_input)]
1526pub struct FileSystemPath {
1527 pub fs: ResolvedVc<Box<dyn FileSystem>>,
1528 pub path: RcStr,
1529}
1530
1531impl ValueToStringRef for FileSystemPath {
1532 async fn to_string_ref(&self) -> Result<RcStr> {
1533 turbofmt!("[{}]/{}", self.fs, self.path).await
1534 }
1535}
1536
1537#[turbo_tasks::value_impl]
1538impl ValueToString for FileSystemPath {
1539 #[turbo_tasks::function]
1540 async fn to_string(&self) -> Result<Vc<RcStr>> {
1541 Ok(Vc::cell(self.to_string_ref().await?))
1542 }
1543}
1544
1545impl FileSystemPath {
1546 pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1547 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1548 if other.path.is_empty() {
1549 true
1550 } else {
1551 self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1552 }
1553 } else {
1554 false
1555 }
1556 }
1557
1558 pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1559 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1560 if other.path.is_empty() {
1561 true
1562 } else {
1563 matches!(
1564 self.path.as_bytes().get(other.path.len()),
1565 Some(&b'/') | None
1566 )
1567 }
1568 } else {
1569 false
1570 }
1571 }
1572
1573 pub fn is_root(&self) -> bool {
1574 self.path.is_empty()
1575 }
1576
1577 pub fn is_in_node_modules(&self) -> bool {
1578 self.path.starts_with("node_modules/") || self.path.contains("/node_modules/")
1579 }
1580
1581 pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1585 if self.fs != inner.fs {
1586 return None;
1587 }
1588 let path = inner.path.strip_prefix(&*self.path)?;
1589 if self.path.is_empty() {
1590 Some(path)
1591 } else if let Some(stripped) = path.strip_prefix('/') {
1592 Some(stripped)
1593 } else {
1594 None
1595 }
1596 }
1597
1598 pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1599 if self.fs != other.fs {
1600 return None;
1601 }
1602
1603 Some(get_relative_path_to(&self.path, &other.path).into())
1604 }
1605
1606 pub fn file_name(&self) -> &str {
1609 let (_, file_name) = self.split_file_name();
1610 file_name
1611 }
1612
1613 pub fn has_extension(&self, extension: &str) -> bool {
1618 debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1619 self.path.ends_with(extension)
1620 }
1621
1622 pub fn extension(&self) -> Option<&str> {
1624 let (_, extension) = self.split_extension();
1625 extension
1626 }
1627
1628 fn split_extension(&self) -> (&str, Option<&str>) {
1632 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1633 if extension.contains('/') ||
1634 path_before_extension.ends_with('/') || path_before_extension.is_empty()
1636 {
1637 (self.path.as_str(), None)
1638 } else {
1639 (path_before_extension, Some(extension))
1640 }
1641 } else {
1642 (self.path.as_str(), None)
1643 }
1644 }
1645
1646 fn split_file_name(&self) -> (Option<&str>, &str) {
1650 if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1652 (Some(parent), file_name)
1653 } else {
1654 (None, self.path.as_str())
1655 }
1656 }
1657
1658 fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1663 let (path_before_extension, extension) = self.split_extension();
1664
1665 if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1666 (Some(parent), file_stem, extension)
1667 } else {
1668 (None, path_before_extension, extension)
1669 }
1670 }
1671}
1672
1673#[turbo_tasks::value(transparent)]
1674pub struct FileSystemPathOption(Option<FileSystemPath>);
1675
1676#[turbo_tasks::value_impl]
1677impl FileSystemPathOption {
1678 #[turbo_tasks::function]
1679 pub fn none() -> Vc<Self> {
1680 Vc::cell(None)
1681 }
1682}
1683
1684impl FileSystemPath {
1685 pub fn new_normalized_unchecked(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1689 debug_assert!(
1693 MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1694 "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1695 '/'",
1696 );
1697 debug_assert!(
1698 normalize_path(&path).as_deref() == Some(&*path),
1699 "path {path} must be normalized",
1700 );
1701 FileSystemPath { fs, path }
1702 }
1703
1704 pub fn join(&self, path: &str) -> Result<Self> {
1707 if let Some(path) = join_path(&self.path, path) {
1708 Ok(Self::new_normalized_unchecked(self.fs, path.into()))
1709 } else {
1710 bail!(
1711 "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1712 self.path,
1713 path,
1714 );
1715 }
1716 }
1717
1718 pub fn append(&self, path: &str) -> Result<Self> {
1720 if path.contains('/') {
1721 bail!(
1722 "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1723 self.path,
1724 path,
1725 )
1726 }
1727 Ok(Self::new_normalized_unchecked(
1728 self.fs,
1729 format!("{}{}", self.path, path).into(),
1730 ))
1731 }
1732
1733 pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1736 if appending.contains('/') {
1737 bail!(
1738 "FileSystemPath({:?}).append_to_stem({:?}) must not append '/'",
1739 self.path,
1740 appending,
1741 )
1742 }
1743 if let (path, Some(ext)) = self.split_extension() {
1744 return Ok(Self::new_normalized_unchecked(
1745 self.fs,
1746 format!("{path}{appending}.{ext}").into(),
1747 ));
1748 }
1749 Ok(Self::new_normalized_unchecked(
1750 self.fs,
1751 format!("{}{}", self.path, appending).into(),
1752 ))
1753 }
1754
1755 #[allow(clippy::needless_borrow)] pub fn try_join(&self, path: &str) -> Option<FileSystemPath> {
1759 #[cfg(target_os = "windows")]
1761 let path = path.replace('\\', "/");
1762
1763 join_path(&self.path, &path)
1764 .map(|p| Self::new_normalized_unchecked(self.fs, RcStr::from(p)))
1765 }
1766
1767 pub fn try_join_inside(&self, path: &str) -> Option<FileSystemPath> {
1771 if let Some(p) = join_path(&self.path, path)
1772 && p.starts_with(&*self.path)
1773 {
1774 return Some(Self::new_normalized_unchecked(self.fs, RcStr::from(p)));
1775 }
1776 None
1777 }
1778
1779 pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1782 read_glob(self.clone(), glob)
1783 }
1784
1785 pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1789 track_glob(self.clone(), glob, include_dot_files)
1790 }
1791
1792 pub fn root(&self) -> Vc<Self> {
1793 self.fs().root()
1794 }
1795}
1796
1797impl FileSystemPath {
1798 pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1799 *self.fs
1800 }
1801
1802 pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1803 self.is_inside_ref(other)
1804 }
1805
1806 pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1807 self.is_inside_or_equal_ref(other)
1808 }
1809
1810 pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1813 let (path_without_extension, _) = self.split_extension();
1814 Self::new_normalized_unchecked(
1815 self.fs,
1816 match extension.is_empty() {
1819 true => path_without_extension.into(),
1820 false => format!("{path_without_extension}.{extension}").into(),
1821 },
1822 )
1823 }
1824
1825 pub fn file_stem(&self) -> Option<&str> {
1834 let (_, file_stem, _) = self.split_file_stem_extension();
1835 if file_stem.is_empty() {
1836 return None;
1837 }
1838 Some(file_stem)
1839 }
1840}
1841
1842impl std::fmt::Display for FileSystemPath {
1843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1844 f.write_str(&self.path)
1845 }
1846}
1847
1848#[turbo_tasks::function]
1849pub async fn rebase(
1850 fs_path: FileSystemPath,
1851 old_base: FileSystemPath,
1852 new_base: FileSystemPath,
1853) -> Result<Vc<FileSystemPath>> {
1854 let new_path;
1855 if old_base.path.is_empty() {
1856 if new_base.path.is_empty() {
1857 new_path = fs_path.path.clone();
1858 } else {
1859 new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1860 }
1861 } else {
1862 let base_path = [&old_base.path, "/"].concat();
1863 if !fs_path.path.starts_with(&base_path) {
1864 turbobail!(
1865 "rebasing {fs_path} from {old_base} onto {new_base} doesn't work because it's not \
1866 part of the source path",
1867 );
1868 }
1869 if new_base.path.is_empty() {
1870 new_path = [&fs_path.path[base_path.len()..]].concat().into();
1871 } else {
1872 new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1873 .concat()
1874 .into();
1875 }
1876 }
1877 Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1878}
1879
1880impl FileSystemPath {
1882 pub fn read(&self) -> Vc<FileContent> {
1883 self.fs().read(self.clone())
1884 }
1885
1886 pub fn read_link(&self) -> Vc<LinkContent> {
1887 self.fs().read_link(self.clone())
1888 }
1889
1890 pub fn read_json(&self) -> Vc<FileJsonContent> {
1891 self.fs().read(self.clone()).parse_json()
1892 }
1893
1894 pub fn read_json5(&self) -> Vc<FileJsonContent> {
1895 self.fs().read(self.clone()).parse_json5()
1896 }
1897
1898 pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1903 self.fs().raw_read_dir(self.clone())
1904 }
1905
1906 pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1907 self.fs().write(self.clone(), content)
1908 }
1909
1910 pub fn write_symbolic_link_dir(&self, target: Vc<LinkContent>) -> Vc<()> {
1928 self.fs().write_link(self.clone(), target)
1929 }
1930
1931 pub fn metadata(&self) -> Vc<FileMeta> {
1932 self.fs().metadata(self.clone())
1933 }
1934
1935 pub async fn realpath(&self) -> Result<FileSystemPath> {
1938 let result = &(*self.realpath_with_links().await?);
1939 match &result.path_result {
1940 Ok(path) => Ok(path.clone()),
1941 Err(error) => bail!("{}", error.as_error_message(self, result).await?),
1942 }
1943 }
1944
1945 pub fn rebase(
1946 fs_path: FileSystemPath,
1947 old_base: FileSystemPath,
1948 new_base: FileSystemPath,
1949 ) -> Vc<FileSystemPath> {
1950 rebase(fs_path, old_base, new_base)
1951 }
1952}
1953
1954impl FileSystemPath {
1955 pub fn read_dir(&self) -> Vc<DirectoryContent> {
1960 read_dir(self.clone())
1961 }
1962
1963 pub fn parent(&self) -> FileSystemPath {
1964 let path = &self.path;
1965 if path.is_empty() {
1966 return self.clone();
1967 }
1968 FileSystemPath::new_normalized_unchecked(self.fs, RcStr::from(get_parent_path(path)))
1969 }
1970
1971 pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1980 get_type(self.clone())
1981 }
1982
1983 pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1984 realpath_with_links(self.clone())
1985 }
1986}
1987
1988#[derive(Clone, Debug)]
1989#[turbo_tasks::value(shared)]
1990pub struct RealPathResult {
1991 pub path_result: Result<FileSystemPath, RealPathResultError>,
1992 pub symlinks: Vec<FileSystemPath>,
1993}
1994
1995#[derive(Debug, Clone, Hash, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
1998pub enum RealPathResultError {
1999 TooManySymlinks,
2000 CycleDetected,
2001 Invalid,
2002 NotFound,
2003}
2004
2005impl RealPathResultError {
2006 pub async fn as_error_message(
2008 &self,
2009 orig: &FileSystemPath,
2010 result: &RealPathResult,
2011 ) -> Result<RcStr> {
2012 Ok(match self {
2013 RealPathResultError::TooManySymlinks => {
2014 let len = result.symlinks.len();
2015 turbofmt!("Symlink {orig} leads to too many other symlinks ({len} links)").await?
2016 }
2017 RealPathResultError::CycleDetected => {
2018 let symlinks_dbg = format!(
2021 "{:?}",
2022 result.symlinks.iter().map(|s| &s.path).collect::<Vec<_>>()
2023 );
2024 turbofmt!("Symlink {orig} is in a symlink loop: {symlinks_dbg}").await?
2025 }
2026 RealPathResultError::Invalid => {
2027 turbofmt!("Symlink {orig} is invalid, it points out of the filesystem root").await?
2028 }
2029 RealPathResultError::NotFound => {
2030 turbofmt!("Symlink {orig} is invalid, it points at a file that doesn't exist")
2031 .await?
2032 }
2033 })
2034 }
2035}
2036
2037#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
2038#[turbo_tasks::value(shared)]
2039pub enum Permissions {
2040 Readable,
2041 #[default]
2042 Writable,
2043 Executable,
2044}
2045
2046#[cfg(unix)]
2049impl From<Permissions> for std::fs::Permissions {
2050 fn from(perm: Permissions) -> Self {
2051 use std::os::unix::fs::PermissionsExt;
2052 match perm {
2053 Permissions::Readable => std::fs::Permissions::from_mode(0o444),
2054 Permissions::Writable => std::fs::Permissions::from_mode(0o664),
2055 Permissions::Executable => std::fs::Permissions::from_mode(0o755),
2056 }
2057 }
2058}
2059
2060#[cfg(unix)]
2061impl From<std::fs::Permissions> for Permissions {
2062 fn from(perm: std::fs::Permissions) -> Self {
2063 use std::os::unix::fs::PermissionsExt;
2064 if perm.readonly() {
2065 Permissions::Readable
2066 } else {
2067 if perm.mode() & 0o111 != 0 {
2069 Permissions::Executable
2070 } else {
2071 Permissions::Writable
2072 }
2073 }
2074 }
2075}
2076
2077#[cfg(not(unix))]
2078impl From<std::fs::Permissions> for Permissions {
2079 fn from(_: std::fs::Permissions) -> Self {
2080 Permissions::default()
2081 }
2082}
2083
2084#[turbo_tasks::value(shared, serialization = "hash")]
2085#[derive(Clone, Debug, PartialOrd, Ord)]
2086pub enum FileContent {
2087 Content(File),
2088 NotFound,
2089}
2090
2091impl From<File> for FileContent {
2092 fn from(file: File) -> Self {
2093 FileContent::Content(file)
2094 }
2095}
2096
2097#[turbo_tasks::value(shared)]
2104#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
2105pub enum PersistedFileContent {
2106 Content(File),
2107 NotFound,
2108}
2109
2110impl PersistedFileContent {
2111 async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
2113 let old_file =
2114 extract_disk_access(retry_blocking(|| std::fs::File::open(path)).await, path)?;
2115 let Some(old_file) = old_file else {
2116 return Ok(match self {
2117 PersistedFileContent::NotFound => FileComparison::Equal,
2118 _ => FileComparison::Create,
2119 });
2120 };
2121 let PersistedFileContent::Content(new_file) = self else {
2123 return Ok(FileComparison::NotEqual);
2124 };
2125
2126 let old_meta = extract_disk_access(retry_blocking(|| old_file.metadata()).await, path)?;
2127 let Some(old_meta) = old_meta else {
2128 return Ok(FileComparison::Create);
2131 };
2132 if new_file.meta != old_meta.into() {
2134 return Ok(FileComparison::NotEqual);
2135 }
2136
2137 let mut new_contents = new_file.read();
2140 let mut old_contents = BufReader::new(old_file);
2141 Ok(loop {
2142 let new_chunk = new_contents.fill_buf()?;
2143 let Ok(old_chunk) = old_contents.fill_buf() else {
2144 break FileComparison::NotEqual;
2145 };
2146
2147 let len = min(new_chunk.len(), old_chunk.len());
2148 if len == 0 {
2149 if new_chunk.len() == old_chunk.len() {
2150 break FileComparison::Equal;
2151 } else {
2152 break FileComparison::NotEqual;
2153 }
2154 }
2155
2156 if new_chunk[0..len] != old_chunk[0..len] {
2157 break FileComparison::NotEqual;
2158 }
2159
2160 new_contents.consume(len);
2161 old_contents.consume(len);
2162 })
2163 }
2164}
2165
2166#[derive(Clone, Debug, Eq, PartialEq)]
2167enum FileComparison {
2168 Create,
2169 Equal,
2170 NotEqual,
2171}
2172
2173bitflags! {
2174 #[derive(
2175 Default,
2176 TraceRawVcs,
2177 NonLocalValue,
2178 DeterministicHash,
2179 Encode,
2180 Decode,
2181 )]
2182 pub struct LinkType: u8 {
2183 const DIRECTORY = 0b00000001;
2184 const ABSOLUTE = 0b00000010;
2185 }
2186}
2187
2188#[turbo_tasks::value(shared)]
2194#[derive(Debug, DeterministicHash)]
2195pub enum LinkContent {
2196 Link {
2207 target: RcStr,
2208 link_type: LinkType,
2209 },
2210 Invalid,
2212 NotFound,
2214}
2215
2216#[turbo_tasks::value(shared)]
2217#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
2218pub struct File {
2219 #[turbo_tasks(debug_ignore)]
2220 content: Rope,
2221 meta: FileMeta,
2222}
2223
2224impl File {
2225 fn from_path(p: &Path) -> io::Result<Self> {
2227 let mut file = std::fs::File::open(p)?;
2228 let metadata = file.metadata()?;
2229
2230 let mut output = Vec::with_capacity(metadata.len() as usize);
2231 file.read_to_end(&mut output)?;
2232
2233 Ok(File {
2234 meta: metadata.into(),
2235 content: Rope::from(output),
2236 })
2237 }
2238
2239 fn from_bytes(content: Vec<u8>) -> Self {
2241 File {
2242 meta: FileMeta::default(),
2243 content: Rope::from(content),
2244 }
2245 }
2246
2247 fn from_rope(content: Rope) -> Self {
2249 File {
2250 meta: FileMeta::default(),
2251 content,
2252 }
2253 }
2254
2255 pub fn content_type(&self) -> Option<&Mime> {
2257 self.meta.content_type.as_ref()
2258 }
2259
2260 pub fn with_content_type(mut self, content_type: Mime) -> Self {
2262 self.meta.content_type = Some(content_type);
2263 self
2264 }
2265
2266 pub fn read(&self) -> RopeReader<'_> {
2268 self.content.read()
2269 }
2270}
2271
2272impl Debug for File {
2273 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2274 f.debug_struct("File")
2275 .field("meta", &self.meta)
2276 .field("content (hash)", &hash_xxh3_hash64(&self.content))
2277 .finish()
2278 }
2279}
2280
2281impl From<RcStr> for File {
2282 fn from(s: RcStr) -> Self {
2283 s.into_owned().into()
2284 }
2285}
2286
2287impl From<String> for File {
2288 fn from(s: String) -> Self {
2289 File::from_bytes(s.into_bytes())
2290 }
2291}
2292
2293impl From<ReadRef<RcStr>> for File {
2294 fn from(s: ReadRef<RcStr>) -> Self {
2295 File::from_bytes(s.as_bytes().to_vec())
2296 }
2297}
2298
2299impl From<&str> for File {
2300 fn from(s: &str) -> Self {
2301 File::from_bytes(s.as_bytes().to_vec())
2302 }
2303}
2304
2305impl From<Vec<u8>> for File {
2306 fn from(bytes: Vec<u8>) -> Self {
2307 File::from_bytes(bytes)
2308 }
2309}
2310
2311impl From<&[u8]> for File {
2312 fn from(bytes: &[u8]) -> Self {
2313 File::from_bytes(bytes.to_vec())
2314 }
2315}
2316
2317impl From<ReadRef<Rope>> for File {
2318 fn from(rope: ReadRef<Rope>) -> Self {
2319 File::from_rope(ReadRef::into_owned(rope))
2320 }
2321}
2322
2323impl From<Rope> for File {
2324 fn from(rope: Rope) -> Self {
2325 File::from_rope(rope)
2326 }
2327}
2328
2329impl File {
2330 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
2331 Self {
2332 meta,
2333 content: Rope::from(content),
2334 }
2335 }
2336
2337 pub fn meta(&self) -> &FileMeta {
2339 &self.meta
2340 }
2341
2342 pub fn content(&self) -> &Rope {
2344 &self.content
2345 }
2346}
2347
2348#[turbo_tasks::value(shared)]
2349#[derive(Debug, Clone, Default)]
2350pub struct FileMeta {
2351 permissions: Permissions,
2354 #[bincode(with = "turbo_bincode::mime_option")]
2355 #[turbo_tasks(trace_ignore)]
2356 content_type: Option<Mime>,
2357}
2358
2359impl Ord for FileMeta {
2360 fn cmp(&self, other: &Self) -> Ordering {
2361 self.permissions
2362 .cmp(&other.permissions)
2363 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2364 }
2365}
2366
2367impl PartialOrd for FileMeta {
2368 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2369 Some(self.cmp(other))
2370 }
2371}
2372
2373impl From<std::fs::Metadata> for FileMeta {
2374 fn from(meta: std::fs::Metadata) -> Self {
2375 let permissions = meta.permissions().into();
2376
2377 Self {
2378 permissions,
2379 content_type: None,
2380 }
2381 }
2382}
2383
2384impl DeterministicHash for FileMeta {
2385 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2386 self.permissions.deterministic_hash(state);
2387 if let Some(content_type) = &self.content_type {
2388 content_type.to_string().deterministic_hash(state);
2389 }
2390 }
2391}
2392
2393impl FileContent {
2394 pub fn new(file: File) -> Self {
2395 FileContent::Content(file)
2396 }
2397
2398 pub fn is_content(&self) -> bool {
2399 matches!(self, FileContent::Content(_))
2400 }
2401
2402 pub fn as_content(&self) -> Option<&File> {
2403 match self {
2404 FileContent::Content(file) => Some(file),
2405 FileContent::NotFound => None,
2406 }
2407 }
2408
2409 pub fn parse_json_ref(&self) -> FileJsonContent {
2410 match self {
2411 FileContent::Content(file) => {
2412 let content = file.content.clone().into_bytes();
2413 let de = &mut serde_json::Deserializer::from_slice(&content);
2414 match serde_path_to_error::deserialize(de) {
2415 Ok(data) => FileJsonContent::Content(data),
2416 Err(e) => FileJsonContent::Unparsable(Box::new(
2417 UnparsableJson::from_serde_path_to_error(e),
2418 )),
2419 }
2420 }
2421 FileContent::NotFound => FileJsonContent::NotFound,
2422 }
2423 }
2424
2425 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2426 match self {
2427 FileContent::Content(file) => match file.content.to_str() {
2428 Ok(string) => match parse_to_serde_value(
2429 &string,
2430 &ParseOptions {
2431 allow_comments: true,
2432 allow_trailing_commas: true,
2433 allow_loose_object_property_names: false,
2434 },
2435 ) {
2436 Ok(data) => match data {
2437 Some(value) => FileJsonContent::Content(value),
2438 None => FileJsonContent::unparsable(rcstr!(
2439 "text content doesn't contain any json data"
2440 )),
2441 },
2442 Err(e) => FileJsonContent::Unparsable(Box::new(
2443 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2444 )),
2445 },
2446 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2447 },
2448 FileContent::NotFound => FileJsonContent::NotFound,
2449 }
2450 }
2451
2452 pub fn parse_json5_ref(&self) -> FileJsonContent {
2453 match self {
2454 FileContent::Content(file) => match file.content.to_str() {
2455 Ok(string) => match parse_to_serde_value(
2456 &string,
2457 &ParseOptions {
2458 allow_comments: true,
2459 allow_trailing_commas: true,
2460 allow_loose_object_property_names: true,
2461 },
2462 ) {
2463 Ok(data) => match data {
2464 Some(value) => FileJsonContent::Content(value),
2465 None => FileJsonContent::unparsable(rcstr!(
2466 "text content doesn't contain any json data"
2467 )),
2468 },
2469 Err(e) => FileJsonContent::Unparsable(Box::new(
2470 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2471 )),
2472 },
2473 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2474 },
2475 FileContent::NotFound => FileJsonContent::NotFound,
2476 }
2477 }
2478
2479 pub fn lines_ref(&self) -> FileLinesContent {
2480 match self {
2481 FileContent::Content(file) => match file.content.to_str() {
2482 Ok(string) => {
2483 let mut bytes_offset = 0;
2484 FileLinesContent::Lines(
2485 string
2486 .split('\n')
2487 .map(|l| {
2488 let line = FileLine {
2489 content: l.to_string(),
2490 bytes_offset,
2491 };
2492 bytes_offset += (l.len() + 1) as u32;
2493 line
2494 })
2495 .collect(),
2496 )
2497 }
2498 Err(_) => FileLinesContent::Unparsable,
2499 },
2500 FileContent::NotFound => FileLinesContent::NotFound,
2501 }
2502 }
2503}
2504
2505#[turbo_tasks::value_impl]
2506impl FileContent {
2507 #[turbo_tasks::function]
2508 pub fn len(&self) -> Result<Vc<Option<u64>>> {
2509 Ok(Vc::cell(match self {
2510 FileContent::Content(file) => Some(file.content.len() as u64),
2511 FileContent::NotFound => None,
2512 }))
2513 }
2514
2515 #[turbo_tasks::function]
2516 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2517 Ok(self.parse_json_ref().cell())
2518 }
2519
2520 #[turbo_tasks::function]
2521 pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
2522 self.parse_json_with_comments_ref().cell()
2523 }
2524
2525 #[turbo_tasks::function]
2526 pub fn parse_json5(&self) -> Vc<FileJsonContent> {
2527 self.parse_json5_ref().cell()
2528 }
2529
2530 #[turbo_tasks::function]
2531 pub fn lines(&self) -> Vc<FileLinesContent> {
2532 self.lines_ref().cell()
2533 }
2534
2535 #[turbo_tasks::function]
2536 pub fn hash(&self, algorithm: HashAlgorithm) -> Vc<RcStr> {
2537 Vc::cell(RcStr::from(deterministic_hash("", self, algorithm)))
2539 }
2540
2541 #[turbo_tasks::function]
2546 pub fn persist(&self) -> Vc<PersistedFileContent> {
2547 match self {
2548 FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
2549 FileContent::NotFound => PersistedFileContent::NotFound.cell(),
2550 }
2551 }
2552
2553 #[turbo_tasks::function]
2559 pub async fn content_hash(
2560 &self,
2561 salt: Vc<RcStr>,
2562 algorithm: HashAlgorithm,
2563 ) -> Result<Vc<Option<RcStr>>> {
2564 match self {
2565 FileContent::Content(file) => Ok(Vc::cell(Some(
2566 deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
2567 ))),
2568 FileContent::NotFound => Ok(Vc::cell(None)),
2569 }
2570 }
2571}
2572
2573#[turbo_tasks::value(shared, serialization = "skip")]
2575pub enum FileJsonContent {
2576 Content(Value),
2577 Unparsable(Box<UnparsableJson>),
2578 NotFound,
2579}
2580
2581#[turbo_tasks::value_impl]
2582impl ValueToString for FileJsonContent {
2583 #[turbo_tasks::function]
2588 fn to_string(&self) -> Result<Vc<RcStr>> {
2589 match self {
2590 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2591 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2592 FileJsonContent::NotFound => bail!("File not found"),
2593 }
2594 }
2595}
2596
2597#[turbo_tasks::value_impl]
2598impl FileJsonContent {
2599 #[turbo_tasks::function]
2600 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2601 match &*self.await? {
2602 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2603 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2604 FileJsonContent::NotFound => bail!("File not found"),
2605 }
2606 }
2607}
2608impl FileJsonContent {
2609 pub fn unparsable(message: RcStr) -> Self {
2610 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2611 message,
2612 path: None,
2613 start_location: None,
2614 end_location: None,
2615 }))
2616 }
2617
2618 pub fn unparsable_with_message(message: RcStr) -> Self {
2619 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2620 message,
2621 path: None,
2622 start_location: None,
2623 end_location: None,
2624 }))
2625 }
2626}
2627
2628#[derive(Debug, PartialEq, Eq)]
2629pub struct FileLine {
2630 pub content: String,
2631 pub bytes_offset: u32,
2632}
2633
2634impl FileLine {
2635 pub fn len(&self) -> usize {
2636 self.content.len()
2637 }
2638
2639 #[must_use]
2640 pub fn is_empty(&self) -> bool {
2641 self.len() == 0
2642 }
2643}
2644
2645#[turbo_tasks::value(shared, serialization = "skip")]
2646pub enum FileLinesContent {
2647 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2648 Unparsable,
2649 NotFound,
2650}
2651
2652#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2653pub enum RawDirectoryEntry {
2654 File,
2655 Directory,
2656 Symlink,
2657 Other,
2659}
2660
2661#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2662pub enum DirectoryEntry {
2663 File(FileSystemPath),
2664 Directory(FileSystemPath),
2665 Symlink(FileSystemPath),
2666 Other(FileSystemPath),
2667 Error(RcStr),
2668}
2669
2670impl DirectoryEntry {
2671 pub async fn resolve_symlink(self) -> Result<Self> {
2675 if let DirectoryEntry::Symlink(symlink) = &self {
2676 let result = &*symlink.realpath_with_links().await?;
2677 let real_path = match &result.path_result {
2678 Ok(path) => path,
2679 Err(error) => {
2680 return Ok(DirectoryEntry::Error(
2681 error.as_error_message(symlink, result).await?,
2682 ));
2683 }
2684 };
2685 Ok(match *real_path.get_type().await? {
2686 FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2687 FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2688 FileSystemEntryType::NotFound => DirectoryEntry::Error(
2690 turbofmt!("Symlink {symlink} points at {real_path} which does not exist")
2691 .await?,
2692 ),
2693 FileSystemEntryType::Symlink => turbobail!(
2695 "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2696 ),
2697 _ => self,
2698 })
2699 } else {
2700 Ok(self)
2701 }
2702 }
2703
2704 pub fn path(self) -> Option<FileSystemPath> {
2705 match self {
2706 DirectoryEntry::File(path)
2707 | DirectoryEntry::Directory(path)
2708 | DirectoryEntry::Symlink(path)
2709 | DirectoryEntry::Other(path) => Some(path),
2710 DirectoryEntry::Error(_) => None,
2711 }
2712 }
2713}
2714
2715#[turbo_tasks::value]
2716#[derive(Hash, Clone, Copy, Debug)]
2717pub enum FileSystemEntryType {
2718 NotFound,
2719 File,
2720 Directory,
2721 Symlink,
2722 Other,
2724 Error,
2725}
2726
2727impl From<FileType> for FileSystemEntryType {
2728 fn from(file_type: FileType) -> Self {
2729 match file_type {
2730 t if t.is_dir() => FileSystemEntryType::Directory,
2731 t if t.is_file() => FileSystemEntryType::File,
2732 t if t.is_symlink() => FileSystemEntryType::Symlink,
2733 _ => FileSystemEntryType::Other,
2734 }
2735 }
2736}
2737
2738impl From<DirectoryEntry> for FileSystemEntryType {
2739 fn from(entry: DirectoryEntry) -> Self {
2740 FileSystemEntryType::from(&entry)
2741 }
2742}
2743
2744impl From<&DirectoryEntry> for FileSystemEntryType {
2745 fn from(entry: &DirectoryEntry) -> Self {
2746 match entry {
2747 DirectoryEntry::File(_) => FileSystemEntryType::File,
2748 DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2749 DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2750 DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2751 DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2752 }
2753 }
2754}
2755
2756impl From<RawDirectoryEntry> for FileSystemEntryType {
2757 fn from(entry: RawDirectoryEntry) -> Self {
2758 FileSystemEntryType::from(&entry)
2759 }
2760}
2761
2762impl From<&RawDirectoryEntry> for FileSystemEntryType {
2763 fn from(entry: &RawDirectoryEntry) -> Self {
2764 match entry {
2765 RawDirectoryEntry::File => FileSystemEntryType::File,
2766 RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2767 RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2768 RawDirectoryEntry::Other => FileSystemEntryType::Other,
2769 }
2770 }
2771}
2772
2773#[turbo_tasks::value]
2774#[derive(Debug)]
2775pub enum RawDirectoryContent {
2776 Entries(AutoMap<RcStr, RawDirectoryEntry>),
2779 NotFound,
2780}
2781
2782impl RawDirectoryContent {
2783 pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2784 Self::cell(RawDirectoryContent::Entries(entries))
2785 }
2786
2787 pub fn not_found() -> Vc<Self> {
2788 Self::cell(RawDirectoryContent::NotFound)
2789 }
2790}
2791
2792#[turbo_tasks::value]
2793#[derive(Debug)]
2794pub enum DirectoryContent {
2795 Entries(AutoMap<RcStr, DirectoryEntry>),
2796 NotFound,
2797}
2798
2799impl DirectoryContent {
2800 pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2801 Self::cell(DirectoryContent::Entries(entries))
2802 }
2803
2804 pub fn not_found() -> Vc<Self> {
2805 Self::cell(DirectoryContent::NotFound)
2806 }
2807}
2808
2809#[derive(ValueToString)]
2810#[value_to_string("null")]
2811#[turbo_tasks::value(shared)]
2812pub struct NullFileSystem;
2813
2814#[turbo_tasks::value_impl]
2815impl FileSystem for NullFileSystem {
2816 #[turbo_tasks::function]
2817 fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2818 FileContent::NotFound.cell()
2819 }
2820
2821 #[turbo_tasks::function]
2822 fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2823 LinkContent::NotFound.cell()
2824 }
2825
2826 #[turbo_tasks::function]
2827 fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2828 RawDirectoryContent::not_found()
2829 }
2830
2831 #[turbo_tasks::function]
2832 fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) {}
2833
2834 #[turbo_tasks::function]
2835 fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) {}
2836
2837 #[turbo_tasks::function]
2838 fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2839 FileMeta::default().cell()
2840 }
2841}
2842
2843pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2844 loop {
2845 if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2846 path = fs.get_inner_fs_path(path).owned().await?;
2847 continue;
2848 }
2849
2850 if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2851 let sys_path = fs.await?.to_sys_path(&path);
2852 return Ok(Some(sys_path));
2853 }
2854
2855 return Ok(None);
2856 }
2857}
2858
2859#[turbo_tasks::function]
2860async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2861 let fs = path.fs().to_resolved().await?;
2862 match &*fs.raw_read_dir(path.clone()).await? {
2863 RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2864 RawDirectoryContent::Entries(entries) => {
2865 let mut normalized_entries = AutoMap::new();
2866 let dir_path = &path.path;
2867 for (name, entry) in entries {
2868 let path = if dir_path.is_empty() {
2872 name.clone()
2873 } else {
2874 RcStr::from(format!("{dir_path}/{name}"))
2875 };
2876
2877 let entry_path = FileSystemPath::new_normalized_unchecked(fs, path);
2878 let entry = match entry {
2879 RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2880 RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2881 RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2882 RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2883 };
2884 normalized_entries.insert(name.clone(), entry);
2885 }
2886 Ok(DirectoryContent::new(normalized_entries))
2887 }
2888 }
2889}
2890
2891#[turbo_tasks::function]
2892async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2893 if path.is_root() {
2894 return Ok(FileSystemEntryType::Directory.cell());
2895 }
2896 let parent = path.parent();
2897 let dir_content = parent.raw_read_dir().await?;
2898 match &*dir_content {
2899 RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2900 RawDirectoryContent::Entries(entries) => {
2901 let (_, file_name) = path.split_file_name();
2902 if let Some(entry) = entries.get(file_name) {
2903 Ok(FileSystemEntryType::from(entry).cell())
2904 } else {
2905 Ok(FileSystemEntryType::NotFound.cell())
2906 }
2907 }
2908 }
2909}
2910
2911#[turbo_tasks::function]
2912async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2913 let mut current_path = path;
2914 let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2915 let mut visited: AutoSet<RcStr> = AutoSet::new();
2916 let mut error = RealPathResultError::TooManySymlinks;
2917 for _i in 0..40 {
2920 if current_path.is_root() {
2921 return Ok(RealPathResult {
2923 path_result: Ok(current_path),
2924 symlinks: symlinks.into_iter().collect(),
2925 }
2926 .cell());
2927 }
2928
2929 if !visited.insert(current_path.path.clone()) {
2930 error = RealPathResultError::CycleDetected;
2931 break; }
2933
2934 let parent = current_path.parent();
2936 let parent_result = parent.realpath_with_links().owned().await?;
2937 let basename = current_path
2938 .path
2939 .rsplit_once('/')
2940 .map_or(current_path.path.as_str(), |(_, name)| name);
2941 symlinks.extend(parent_result.symlinks);
2942 let parent_path = match parent_result.path_result {
2943 Ok(path) => {
2944 if path != parent {
2945 current_path = path.join(basename)?;
2946 }
2947 path
2948 }
2949 Err(parent_error) => {
2950 error = parent_error;
2951 break;
2952 }
2953 };
2954
2955 if !matches!(
2958 *current_path.get_type().await?,
2959 FileSystemEntryType::Symlink
2960 ) {
2961 return Ok(RealPathResult {
2962 path_result: Ok(current_path),
2963 symlinks: symlinks.into_iter().collect(), }
2965 .cell());
2966 }
2967
2968 match &*current_path.read_link().await? {
2969 LinkContent::Link { target, link_type } => {
2970 symlinks.insert(current_path.clone());
2971 current_path = if link_type.contains(LinkType::ABSOLUTE) {
2972 current_path.root().owned().await?
2973 } else {
2974 parent_path
2975 }
2976 .join(target)?;
2977 }
2978 LinkContent::NotFound => {
2979 error = RealPathResultError::NotFound;
2980 break;
2981 }
2982 LinkContent::Invalid => {
2983 error = RealPathResultError::Invalid;
2984 break;
2985 }
2986 }
2987 }
2988
2989 Ok(RealPathResult {
2997 path_result: Err(error),
2998 symlinks: symlinks.into_iter().collect(),
2999 }
3000 .cell())
3001}
3002
3003#[derive(TraceRawVcs, NonLocalValue)]
3006pub(crate) struct AnyhowWrapper(anyhow::Error);
3007
3008impl fmt::Display for AnyhowWrapper {
3009 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3010 fmt::Display::fmt(&self.0, f)
3011 }
3012}
3013
3014impl fmt::Debug for AnyhowWrapper {
3015 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3016 fmt::Debug::fmt(&self.0, f)
3017 }
3018}
3019
3020impl StdError for AnyhowWrapper {
3021 fn source(&self) -> Option<&(dyn StdError + 'static)> {
3022 self.0.source()
3023 }
3024}
3025
3026impl From<anyhow::Error> for AnyhowWrapper {
3027 fn from(err: anyhow::Error) -> Self {
3028 AnyhowWrapper(err)
3029 }
3030}
3031
3032#[cfg(test)]
3033mod tests {
3034 use turbo_rcstr::rcstr;
3035 use turbo_tasks::{Effects, OperationVc, Vc, take_effects};
3036 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3037
3038 use super::*;
3039
3040 #[turbo_tasks::function(operation, root)]
3041 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
3042 let _ = op.resolve().strongly_consistent().await?;
3043 Ok(take_effects(op).await?.cell())
3044 }
3045
3046 #[test]
3047 fn test_get_relative_path_to() {
3048 assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
3049 assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
3050 assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
3051 assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
3052 assert_eq!(
3053 get_relative_path_to("a/b/c", "c/b/a").as_str(),
3054 "../../../c/b/a"
3055 );
3056 assert_eq!(
3057 get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
3058 "../../../c/b/a"
3059 );
3060 }
3061
3062 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3063 async fn with_extension() {
3064 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3065 BackendOptions::default(),
3066 noop_backing_storage(),
3067 ));
3068 tt.run_once(async move {
3069 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
3070 .to_resolved()
3071 .await?;
3072
3073 let path_txt = FileSystemPath::new_normalized_unchecked(fs, rcstr!("foo/bar.txt"));
3074
3075 let path_json = path_txt.with_extension("json");
3076 assert_eq!(&*path_json.path, "foo/bar.json");
3077
3078 let path_no_ext = path_txt.with_extension("");
3079 assert_eq!(&*path_no_ext.path, "foo/bar");
3080
3081 let path_new_ext = path_no_ext.with_extension("json");
3082 assert_eq!(&*path_new_ext.path, "foo/bar.json");
3083
3084 let path_no_slash_txt = FileSystemPath::new_normalized_unchecked(fs, rcstr!("bar.txt"));
3085
3086 let path_no_slash_json = path_no_slash_txt.with_extension("json");
3087 assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
3088
3089 let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
3090 assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
3091
3092 let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
3093 assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
3094
3095 anyhow::Ok(())
3096 })
3097 .await
3098 .unwrap()
3099 }
3100
3101 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3102 async fn file_stem() {
3103 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3104 BackendOptions::default(),
3105 noop_backing_storage(),
3106 ));
3107 tt.run_once(async move {
3108 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
3109 .to_resolved()
3110 .await?;
3111
3112 let path = FileSystemPath::new_normalized_unchecked(fs, rcstr!(""));
3113 assert_eq!(path.file_stem(), None);
3114
3115 let path = FileSystemPath::new_normalized_unchecked(fs, rcstr!("foo/bar.txt"));
3116 assert_eq!(path.file_stem(), Some("bar"));
3117
3118 let path = FileSystemPath::new_normalized_unchecked(fs, rcstr!("bar.txt"));
3119 assert_eq!(path.file_stem(), Some("bar"));
3120
3121 let path = FileSystemPath::new_normalized_unchecked(fs, rcstr!("foo/bar"));
3122 assert_eq!(path.file_stem(), Some("bar"));
3123
3124 let path = FileSystemPath::new_normalized_unchecked(fs, rcstr!("foo/.bar"));
3125 assert_eq!(path.file_stem(), Some(".bar"));
3126
3127 anyhow::Ok(())
3128 })
3129 .await
3130 .unwrap()
3131 }
3132
3133 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3134 async fn test_try_from_sys_path() {
3135 let sys_root = if cfg!(windows) {
3136 Path::new(r"C:\fake\root")
3137 } else {
3138 Path::new(r"/fake/root")
3139 };
3140
3141 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3142 BackendOptions::default(),
3143 noop_backing_storage(),
3144 ));
3145 tt.run_once(async {
3146 assert_try_from_sys_path_operation(RcStr::from(sys_root.to_str().unwrap()))
3147 .read_strongly_consistent()
3148 .await?;
3149
3150 anyhow::Ok(())
3151 })
3152 .await
3153 .unwrap();
3154 }
3155
3156 #[turbo_tasks::function(operation, root)]
3157 async fn assert_try_from_sys_path_operation(sys_root: RcStr) -> anyhow::Result<()> {
3158 let sys_root = Path::new(sys_root.as_str());
3159 let fs_vc = DiskFileSystem::new(
3160 rcstr!("temp"),
3161 Vc::cell(RcStr::from(sys_root.to_str().unwrap())),
3162 )
3163 .to_resolved()
3164 .await?;
3165 let fs = fs_vc.await?;
3166 let fs_root_path = fs_vc.root().await?;
3167
3168 assert_eq!(
3169 fs.try_from_sys_path(
3170 fs_vc,
3171 &Path::new("relative").join("directory"),
3172 None,
3173 )
3174 .unwrap()
3175 .path,
3176 "relative/directory"
3177 );
3178
3179 assert_eq!(
3180 fs.try_from_sys_path(
3181 fs_vc,
3182 &sys_root
3183 .join("absolute")
3184 .join("directory")
3185 .join("..")
3186 .join("normalized_path"),
3187 Some(&fs_root_path.join("ignored").unwrap()),
3188 )
3189 .unwrap()
3190 .path,
3191 "absolute/normalized_path"
3192 );
3193
3194 assert_eq!(
3195 fs.try_from_sys_path(
3196 fs_vc,
3197 Path::new("child"),
3198 Some(&fs_root_path.join("parent").unwrap()),
3199 )
3200 .unwrap()
3201 .path,
3202 "parent/child"
3203 );
3204
3205 assert_eq!(
3206 fs.try_from_sys_path(
3207 fs_vc,
3208 &Path::new("..").join("parallel_dir"),
3209 Some(&fs_root_path.join("parent").unwrap()),
3210 )
3211 .unwrap()
3212 .path,
3213 "parallel_dir"
3214 );
3215
3216 assert_eq!(
3217 fs.try_from_sys_path(
3218 fs_vc,
3219 &Path::new("relative")
3220 .join("..")
3221 .join("..")
3222 .join("leaves_root"),
3223 None,
3224 ),
3225 None
3226 );
3227
3228 assert_eq!(
3229 fs.try_from_sys_path(
3230 fs_vc,
3231 &sys_root
3232 .join("absolute")
3233 .join("..")
3234 .join("..")
3235 .join("leaves_root"),
3236 None,
3237 ),
3238 None
3239 );
3240
3241 Ok(())
3242 }
3243
3244 #[cfg(test)]
3245 mod symlink_tests {
3246 use std::{
3247 fs::{File, create_dir_all, read_to_string},
3248 io::Write,
3249 };
3250
3251 use rand::{RngExt, SeedableRng};
3252 use turbo_rcstr::{RcStr, rcstr};
3253 use turbo_tasks::{ResolvedVc, Vc, read_strongly_consistent_and_apply_effects};
3254 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3255
3256 use super::extract_effects_operation;
3257 use crate::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
3258
3259 #[turbo_tasks::function(operation, root)]
3260 async fn test_write_link_effect_operation(
3261 fs: ResolvedVc<DiskFileSystem>,
3262 path: FileSystemPath,
3263 target: RcStr,
3264 ) -> anyhow::Result<()> {
3265 let write_file = |f| {
3266 fs.write_link(
3267 f,
3268 LinkContent::Link {
3269 target: format!("{target}/data.txt").into(),
3270 link_type: LinkType::empty(),
3271 }
3272 .cell(),
3273 )
3274 };
3275 write_file(path.join("symlink-file")?).await?;
3277 write_file(path.join("symlink-file")?).await?;
3278
3279 let write_dir = |f| {
3280 fs.write_link(
3281 f,
3282 LinkContent::Link {
3283 target: target.clone(),
3284 link_type: LinkType::DIRECTORY,
3285 }
3286 .cell(),
3287 )
3288 };
3289 write_dir(path.join("symlink-dir")?).await?;
3291 write_dir(path.join("symlink-dir")?).await?;
3292
3293 Ok(())
3294 }
3295
3296 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3297 async fn test_write_link() {
3298 let scratch = tempfile::tempdir().unwrap();
3299 let path = scratch.path().to_owned();
3300
3301 create_dir_all(path.join("subdir-a")).unwrap();
3302 File::create_new(path.join("subdir-a/data.txt"))
3303 .unwrap()
3304 .write_all(b"foo")
3305 .unwrap();
3306 create_dir_all(path.join("subdir-b")).unwrap();
3307 File::create_new(path.join("subdir-b/data.txt"))
3308 .unwrap()
3309 .write_all(b"bar")
3310 .unwrap();
3311 let root = path.to_str().unwrap().into();
3312
3313 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3314 BackendOptions::default(),
3315 noop_backing_storage(),
3316 ));
3317
3318 tt.run_once(async move {
3319 let fs = disk_file_system_operation(root)
3320 .resolve()
3321 .strongly_consistent()
3322 .await?;
3323 let root_path = disk_file_system_root(fs);
3324
3325 read_strongly_consistent_and_apply_effects(
3326 extract_effects_operation(test_write_link_effect_operation(
3327 fs,
3328 root_path.clone(),
3329 rcstr!("subdir-a"),
3330 )),
3331 |e| e,
3332 )
3333 .await?;
3334
3335 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
3336 assert_eq!(
3337 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3338 "foo"
3339 );
3340
3341 read_strongly_consistent_and_apply_effects(
3343 extract_effects_operation(test_write_link_effect_operation(
3344 fs,
3345 root_path,
3346 rcstr!("subdir-b"),
3347 )),
3348 |e| e,
3349 )
3350 .await?;
3351
3352 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
3353 assert_eq!(
3354 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3355 "bar"
3356 );
3357
3358 anyhow::Ok(())
3359 })
3360 .await
3361 .unwrap();
3362 }
3363
3364 const STRESS_ITERATIONS: usize = 100;
3365 const STRESS_PARALLELISM: usize = 8;
3366 const STRESS_TARGET_COUNT: usize = 20;
3367 const STRESS_SYMLINK_COUNT: usize = 16;
3368
3369 #[turbo_tasks::function(operation, root)]
3370 fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
3371 DiskFileSystem::new(rcstr!("test"), Vc::cell(fs_root))
3372 }
3373
3374 fn disk_file_system_root(fs: ResolvedVc<DiskFileSystem>) -> FileSystemPath {
3375 FileSystemPath {
3376 fs: ResolvedVc::upcast(fs),
3377 path: rcstr!(""),
3378 }
3379 }
3380
3381 #[turbo_tasks::function(operation, root)]
3382 async fn write_symlink_stress_batch(
3383 fs: ResolvedVc<DiskFileSystem>,
3384 symlinks_dir: FileSystemPath,
3385 updates: Vec<(usize, usize)>,
3386 ) -> anyhow::Result<()> {
3387 use turbo_tasks::TryJoinIterExt;
3388
3389 updates
3390 .into_iter()
3391 .map(|(symlink_idx, target_idx)| {
3392 let target = RcStr::from(format!("../_targets/{target_idx}"));
3393 let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
3394 async move {
3395 fs.write_link(
3396 symlink_path,
3397 LinkContent::Link {
3398 target,
3399 link_type: LinkType::DIRECTORY,
3400 }
3401 .cell(),
3402 )
3403 .await
3404 }
3405 })
3406 .try_join()
3407 .await?;
3408 Ok(())
3409 }
3410
3411 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3412 async fn test_symlink_stress() {
3413 let scratch = tempfile::tempdir().unwrap();
3414 let path = scratch.path().to_owned();
3415
3416 let targets_dir = path.join("_targets");
3417 create_dir_all(&targets_dir).unwrap();
3418 for i in 0..STRESS_TARGET_COUNT {
3419 create_dir_all(targets_dir.join(i.to_string())).unwrap();
3420 }
3421 create_dir_all(path.join("_symlinks")).unwrap();
3422
3423 let root = RcStr::from(path.to_str().unwrap());
3424
3425 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3426 BackendOptions::default(),
3427 noop_backing_storage(),
3428 ));
3429
3430 tt.run_once(async move {
3431 let fs = disk_file_system_operation(root)
3432 .resolve()
3433 .strongly_consistent()
3434 .await?;
3435 let root_path = disk_file_system_root(fs);
3436 let symlinks_dir = root_path.join("_symlinks")?;
3437
3438 let initial_updates: Vec<(usize, usize)> =
3439 (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
3440 read_strongly_consistent_and_apply_effects(
3441 extract_effects_operation(write_symlink_stress_batch(
3442 fs,
3443 symlinks_dir.clone(),
3444 initial_updates,
3445 )),
3446 |e| e,
3447 )
3448 .await?;
3449
3450 let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
3451 for _ in 0..STRESS_ITERATIONS {
3452 let mut updates_map = rustc_hash::FxHashMap::default();
3453 for _ in 0..STRESS_PARALLELISM {
3454 let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
3455 let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
3456 updates_map.insert(symlink_idx, target_idx);
3457 }
3458 let updates: Vec<(usize, usize)> = updates_map.into_iter().collect();
3459
3460 read_strongly_consistent_and_apply_effects(
3461 extract_effects_operation(write_symlink_stress_batch(
3462 fs,
3463 symlinks_dir.clone(),
3464 updates,
3465 )),
3466 |e| e,
3467 )
3468 .await?;
3469 }
3470
3471 anyhow::Ok(())
3472 })
3473 .await
3474 .unwrap();
3475
3476 tt.stop_and_wait().await;
3477 }
3478 }
3479
3480 #[cfg(test)]
3482 mod denied_path_tests {
3483 use std::{
3484 fs::{File, create_dir_all, read_to_string},
3485 io::Write,
3486 path::Path,
3487 };
3488
3489 use turbo_rcstr::{RcStr, rcstr};
3490 use turbo_tasks::{Effects, Vc, read_strongly_consistent_and_apply_effects, take_effects};
3491 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3492
3493 use crate::{
3494 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
3495 FileSystemPath,
3496 glob::{Glob, GlobOptions},
3497 };
3498
3499 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
3502 let scratch = tempfile::tempdir().unwrap();
3503 let path = scratch.path();
3504
3505 File::create_new(path.join("allowed_file.txt"))
3512 .unwrap()
3513 .write_all(b"allowed content")
3514 .unwrap();
3515
3516 create_dir_all(path.join("allowed_dir")).unwrap();
3517 File::create_new(path.join("allowed_dir/file.txt"))
3518 .unwrap()
3519 .write_all(b"allowed dir content")
3520 .unwrap();
3521
3522 File::create_new(path.join("other_file.txt"))
3523 .unwrap()
3524 .write_all(b"other content")
3525 .unwrap();
3526
3527 create_dir_all(path.join("denied_dir/nested")).unwrap();
3528 File::create_new(path.join("denied_dir/secret.txt"))
3529 .unwrap()
3530 .write_all(b"secret content")
3531 .unwrap();
3532 File::create_new(path.join("denied_dir/nested/deep.txt"))
3533 .unwrap()
3534 .write_all(b"deep secret")
3535 .unwrap();
3536
3537 let root = RcStr::from(path.to_str().unwrap());
3538 let denied_path = rcstr!("denied_dir");
3540
3541 (scratch, root, denied_path)
3542 }
3543
3544 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3545 async fn test_denied_path_read() {
3546 #[turbo_tasks::function(operation, root)]
3547 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3548 let fs = DiskFileSystem::new_with_denied_paths(
3549 rcstr!("test"),
3550 Vc::cell(root),
3551 vec![denied_path],
3552 );
3553 let root_path = fs.root().await?;
3554
3555 let allowed_file = root_path.join("allowed_file.txt")?;
3557 let content = allowed_file.read().await?;
3558 assert!(
3559 matches!(&*content, FileContent::Content(_)),
3560 "allowed file should be readable"
3561 );
3562
3563 let denied_file = root_path.join("denied_dir/secret.txt")?;
3565 let content = denied_file.read().await?;
3566 assert!(
3567 matches!(&*content, FileContent::NotFound),
3568 "denied file should return NotFound, got {:?}",
3569 content
3570 );
3571
3572 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
3574 let content = nested_denied.read().await?;
3575 assert!(
3576 matches!(&*content, FileContent::NotFound),
3577 "nested denied file should return NotFound"
3578 );
3579
3580 let denied_dir = root_path.join("denied_dir")?;
3582 let content = denied_dir.read().await?;
3583 assert!(
3584 matches!(&*content, FileContent::NotFound),
3585 "denied directory should return NotFound"
3586 );
3587
3588 Ok(())
3589 }
3590
3591 let (_scratch, root, denied_path) = setup_test_fs();
3592 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3593 BackendOptions::default(),
3594 noop_backing_storage(),
3595 ));
3596 tt.run_once(async {
3597 test_operation(root, denied_path)
3598 .read_strongly_consistent()
3599 .await?;
3600
3601 anyhow::Ok(())
3602 })
3603 .await
3604 .unwrap();
3605 }
3606
3607 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3608 async fn test_denied_path_read_dir() {
3609 #[turbo_tasks::function(operation, root)]
3610 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3611 let fs = DiskFileSystem::new_with_denied_paths(
3612 rcstr!("test"),
3613 Vc::cell(root),
3614 vec![denied_path],
3615 );
3616 let root_path = fs.root().await?;
3617
3618 let dir_content = root_path.read_dir().await?;
3620 match &*dir_content {
3621 DirectoryContent::Entries(entries) => {
3622 assert!(
3623 entries.contains_key(&rcstr!("allowed_dir")),
3624 "allowed_dir should be visible"
3625 );
3626 assert!(
3627 entries.contains_key(&rcstr!("other_file.txt")),
3628 "other_file.txt should be visible"
3629 );
3630 assert!(
3631 entries.contains_key(&rcstr!("allowed_file.txt")),
3632 "allowed_file.txt should be visible"
3633 );
3634 assert!(
3635 !entries.contains_key(&rcstr!("denied_dir")),
3636 "denied_dir should NOT be visible in read_dir"
3637 );
3638 }
3639 DirectoryContent::NotFound => panic!("root directory should exist"),
3640 }
3641
3642 let denied_dir = root_path.join("denied_dir")?;
3644 let dir_content = denied_dir.read_dir().await?;
3645 assert!(
3646 matches!(&*dir_content, DirectoryContent::NotFound),
3647 "denied_dir read_dir should return NotFound"
3648 );
3649
3650 Ok(())
3651 }
3652
3653 let (_scratch, root, denied_path) = setup_test_fs();
3654 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3655 BackendOptions::default(),
3656 noop_backing_storage(),
3657 ));
3658 tt.run_once(async {
3659 test_operation(root, denied_path)
3660 .read_strongly_consistent()
3661 .await?;
3662
3663 anyhow::Ok(())
3664 })
3665 .await
3666 .unwrap();
3667 }
3668
3669 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3670 async fn test_denied_path_read_glob() {
3671 #[turbo_tasks::function(operation, root)]
3672 async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3673 let fs = DiskFileSystem::new_with_denied_paths(
3674 rcstr!("test"),
3675 Vc::cell(root),
3676 vec![denied_path],
3677 );
3678 let root_path = fs.root().await?;
3679
3680 let glob_result = root_path
3682 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3683 .await?;
3684
3685 assert!(
3687 glob_result.results.contains_key("allowed_file.txt"),
3688 "allowed_file.txt should be found"
3689 );
3690 assert!(
3691 glob_result.results.contains_key("other_file.txt"),
3692 "other_file.txt should be found"
3693 );
3694 assert!(
3695 !glob_result.results.contains_key("denied_dir"),
3696 "denied_dir should NOT appear in glob results"
3697 );
3698
3699 assert!(
3701 !glob_result.inner.contains_key("denied_dir"),
3702 "denied_dir should NOT appear in glob inner results"
3703 );
3704
3705 assert!(
3707 glob_result.inner.contains_key("allowed_dir"),
3708 "allowed_dir directory should be present"
3709 );
3710 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3711 assert!(
3712 sub_inner.results.contains_key("file.txt"),
3713 "allowed_dir/file.txt should be found"
3714 );
3715
3716 Ok(())
3717 }
3718
3719 let (_scratch, root, denied_path) = setup_test_fs();
3720 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3721 BackendOptions::default(),
3722 noop_backing_storage(),
3723 ));
3724 tt.run_once(async {
3725 test_operation(root, denied_path)
3726 .read_strongly_consistent()
3727 .await?;
3728
3729 anyhow::Ok(())
3730 })
3731 .await
3732 .unwrap();
3733 }
3734
3735 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3736 async fn test_denied_path_write() {
3737 #[turbo_tasks::function(operation, root)]
3738 async fn write_file_operation(
3739 path: FileSystemPath,
3740 contents: RcStr,
3741 ) -> anyhow::Result<()> {
3742 path.write(
3743 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3744 .cell(),
3745 )
3746 .await?;
3747 Ok(())
3748 }
3749
3750 #[turbo_tasks::function(operation, root)]
3753 async fn write_allowed_file_operation(
3754 root: RcStr,
3755 denied_path: RcStr,
3756 file_path: RcStr,
3757 contents: RcStr,
3758 ) -> anyhow::Result<Vc<Effects>> {
3759 let fs = DiskFileSystem::new_with_denied_paths(
3760 rcstr!("test"),
3761 Vc::cell(root),
3762 vec![denied_path],
3763 );
3764 let root_path = fs.root().await?;
3765 let allowed_file = root_path.join(&file_path)?;
3766 let write_op = write_file_operation(allowed_file, contents);
3767 write_op.read_strongly_consistent().await?;
3768 Ok(take_effects(write_op).await?.cell())
3769 }
3770
3771 #[turbo_tasks::function(operation, root)]
3772 async fn test_denied_writes_operation(
3773 root: RcStr,
3774 denied_path: RcStr,
3775 denied_file: RcStr,
3776 nested_denied_file: RcStr,
3777 ) -> anyhow::Result<()> {
3778 let fs = DiskFileSystem::new_with_denied_paths(
3779 rcstr!("test"),
3780 Vc::cell(root),
3781 vec![denied_path],
3782 );
3783 let root_path = fs.root().await?;
3784
3785 let path = root_path.join(&denied_file)?;
3786 let result = write_file_operation(path, rcstr!("forbidden"))
3787 .read_strongly_consistent()
3788 .await;
3789 assert!(
3790 result.is_err(),
3791 "writing to denied path should return an error"
3792 );
3793
3794 let path = root_path.join(&nested_denied_file)?;
3795 let result = write_file_operation(path, rcstr!("nested"))
3796 .read_strongly_consistent()
3797 .await;
3798 assert!(
3799 result.is_err(),
3800 "writing to nested denied path should return an error"
3801 );
3802
3803 Ok(())
3804 }
3805
3806 let (_scratch, root, denied_path) = setup_test_fs();
3807 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3808 BackendOptions::default(),
3809 noop_backing_storage(),
3810 ));
3811 tt.run_once(async {
3812 const ALLOWED_FILE: &str = "allowed_dir/new_file.txt";
3813 const TEST_CONTENT: &str = "test content";
3814
3815 let effects_op = write_allowed_file_operation(
3817 root.clone(),
3818 denied_path.clone(),
3819 RcStr::from(ALLOWED_FILE),
3820 RcStr::from(TEST_CONTENT),
3821 );
3822 read_strongly_consistent_and_apply_effects(effects_op, |e| e).await?;
3823
3824 let content = read_to_string(Path::new(root.as_str()).join(ALLOWED_FILE))?;
3826 assert_eq!(content, TEST_CONTENT, "allowed file write should succeed");
3827
3828 test_denied_writes_operation(
3830 root,
3831 denied_path,
3832 RcStr::from("denied_dir/forbidden.txt"),
3833 RcStr::from("denied_dir/nested/file.txt"),
3834 )
3835 .read_strongly_consistent()
3836 .await?;
3837
3838 anyhow::Ok(())
3839 })
3840 .await
3841 .unwrap();
3842 }
3843 }
3844}