1#![feature(arbitrary_self_types)]
2#![feature(arbitrary_self_types_pointers)]
3#![feature(btree_cursors)] #![feature(io_error_more)]
5#![feature(iter_advance_by)]
6#![feature(min_specialization)]
7#![feature(normalize_lexically)]
10#![feature(trivial_bounds)]
11#![feature(downcast_unchecked)]
12#![cfg_attr(windows, feature(junction_point))]
15#![allow(clippy::needless_return)] #![allow(clippy::mutable_key_type)]
17
18pub mod attach;
19pub mod embed;
20pub mod glob;
21mod globset;
22pub mod invalidation;
23mod invalidator_map;
24pub mod json;
25mod mutex_map;
26mod path_map;
27mod read_glob;
28mod retry;
29pub mod rope;
30pub mod source_context;
31pub mod util;
32pub(crate) mod virtual_fs;
33mod watcher;
34
35use std::{
36 borrow::Cow,
37 cmp::{Ordering, min},
38 env,
39 fmt::{self, Debug, Display, Formatter},
40 fs::FileType,
41 future::Future,
42 io::{self, BufRead, BufReader, ErrorKind, Read, Write as _},
43 mem::take,
44 path::{MAIN_SEPARATOR, Path, PathBuf},
45 sync::{Arc, LazyLock, Weak},
46 time::Duration,
47};
48
49use anyhow::{Context, Result, anyhow, bail};
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 ApplyEffectsContext, Completion, InvalidationReason, Invalidator, NonLocalValue, ReadRef,
67 ResolvedVc, TaskInput, TurboTasksApi, ValueToString, Vc, debug::ValueDebugFormat, effect,
68 mark_session_dependent, parallel, trace::TraceRawVcs, turbo_tasks_weak,
69};
70use turbo_tasks_hash::{DeterministicHash, DeterministicHasher, hash_xxh3_hash64};
71use turbo_unix_path::{
72 get_parent_path, get_relative_path_to, join_path, normalize_path, sys_to_unix, unix_to_sys,
73};
74
75use crate::{
76 attach::AttachedFileSystem,
77 glob::Glob,
78 invalidation::Write,
79 invalidator_map::{InvalidatorMap, WriteContent},
80 json::UnparsableJson,
81 mutex_map::MutexMap,
82 read_glob::{read_glob, track_glob},
83 retry::{can_retry, retry_blocking, retry_blocking_custom},
84 rope::{Rope, RopeReader},
85 util::extract_disk_access,
86 watcher::DiskWatcher,
87};
88pub use crate::{read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem};
89
90pub const MAX_SAFE_FILE_NAME_LENGTH: usize = 200;
102
103pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
128 fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
131 if cfg!(windows) {
132 const MAX_PATH_LENGTH_WINDOWS: usize = 260;
133 const UNC_PREFIX: &str = "\\\\?\\";
134
135 if path.starts_with(UNC_PREFIX) {
136 return Ok(path.into());
137 }
138
139 if path.as_os_str().len() > MAX_PATH_LENGTH_WINDOWS {
140 let new_path = std::fs::canonicalize(path).map_err(|err| {
141 anyhow!(err).context("file is too long, and could not be normalized")
142 })?;
143 return Ok(new_path.into());
144 }
145
146 Ok(path.into())
147 } else {
148 const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
152 const MAX_PATH_LENGTH: usize = 1024 - 8;
156
157 if path
159 .file_name()
160 .map(|n| n.as_encoded_bytes().len())
161 .unwrap_or(0)
162 > MAX_FILE_NAME_LENGTH_UNIX
163 {
164 anyhow::bail!(
165 "file name is too long (exceeds {} bytes)",
166 MAX_FILE_NAME_LENGTH_UNIX,
167 );
168 }
169
170 if path.as_os_str().len() > MAX_PATH_LENGTH {
171 anyhow::bail!("path is too long (exceeds {MAX_PATH_LENGTH} bytes)");
172 }
173
174 Ok(path.into())
175 }
176 }
177
178 validate_path_length_inner(path)
179 .with_context(|| format!("path length for file {path:?} exceeds max length of filesystem"))
180}
181
182trait ConcurrencyLimitedExt {
183 type Output;
184 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
185}
186
187impl<F, R> ConcurrencyLimitedExt for F
188where
189 F: Future<Output = R>,
190{
191 type Output = R;
192 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
193 let _permit = semaphore.acquire().await;
194 self.await
195 }
196}
197
198fn number_env_var(name: &'static str) -> Option<usize> {
199 env::var(name)
200 .ok()
201 .filter(|val| !val.is_empty())
202 .map(|val| match val.parse() {
203 Ok(n) => n,
204 Err(err) => panic!("{name} must be a valid integer: {err}"),
205 })
206 .filter(|val| *val != 0)
207}
208
209fn create_read_semaphore() -> tokio::sync::Semaphore {
210 static TURBO_ENGINE_READ_CONCURRENCY: LazyLock<usize> =
213 LazyLock::new(|| number_env_var("TURBO_ENGINE_READ_CONCURRENCY").unwrap_or(64));
214 tokio::sync::Semaphore::new(*TURBO_ENGINE_READ_CONCURRENCY)
215}
216
217fn create_write_semaphore() -> tokio::sync::Semaphore {
218 static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
221 number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
222 4,
225 )
226 });
227 tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
228}
229
230#[turbo_tasks::value_trait]
231pub trait FileSystem: ValueToString {
232 #[turbo_tasks::function]
234 fn root(self: ResolvedVc<Self>) -> Vc<FileSystemPath> {
235 FileSystemPath::new_normalized(self, RcStr::default()).cell()
236 }
237 #[turbo_tasks::function]
238 fn read(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileContent>;
239 #[turbo_tasks::function]
240 fn read_link(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<LinkContent>;
241 #[turbo_tasks::function]
242 fn raw_read_dir(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<RawDirectoryContent>;
243 #[turbo_tasks::function]
244 fn write(self: Vc<Self>, fs_path: FileSystemPath, content: Vc<FileContent>) -> Vc<()>;
245 #[turbo_tasks::function]
247 fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
248 #[turbo_tasks::function]
249 fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
250}
251
252#[derive(Default)]
253struct DiskFileSystemApplyContext {
254 created_directories: FxHashSet<PathBuf>,
256}
257
258#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
259struct DiskFileSystemInner {
260 pub name: RcStr,
261 pub root: RcStr,
262 #[turbo_tasks(debug_ignore, trace_ignore)]
263 #[bincode(skip)]
264 mutex_map: MutexMap<PathBuf>,
265 #[turbo_tasks(debug_ignore, trace_ignore)]
266 #[bincode(skip)]
267 invalidator_map: InvalidatorMap,
268 #[turbo_tasks(debug_ignore, trace_ignore)]
269 #[bincode(skip)]
270 dir_invalidator_map: InvalidatorMap,
271 #[turbo_tasks(debug_ignore, trace_ignore)]
274 #[bincode(skip)]
275 invalidation_lock: RwLock<()>,
276 #[turbo_tasks(debug_ignore, trace_ignore)]
278 #[bincode(skip, default = "create_read_semaphore")]
279 read_semaphore: tokio::sync::Semaphore,
280 #[turbo_tasks(debug_ignore, trace_ignore)]
282 #[bincode(skip, default = "create_write_semaphore")]
283 write_semaphore: tokio::sync::Semaphore,
284
285 #[turbo_tasks(debug_ignore, trace_ignore)]
286 watcher: DiskWatcher,
287 denied_paths: Vec<RcStr>,
290 #[turbo_tasks(debug_ignore, trace_ignore)]
293 #[bincode(skip, default = "turbo_tasks_weak")]
294 turbo_tasks: Weak<dyn TurboTasksApi>,
295 #[turbo_tasks(debug_ignore, trace_ignore)]
297 #[bincode(skip, default = "Handle::current")]
298 tokio_handle: Handle,
299}
300
301impl DiskFileSystemInner {
302 fn root_path(&self) -> &Path {
304 simplified(Path::new(&*self.root))
306 }
307
308 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
318 let path = &path.path;
319 self.denied_paths.iter().any(|denied_path| {
320 path.starts_with(denied_path.as_str())
321 && (path.len() == denied_path.len()
322 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
323 })
324 }
325
326 fn register_read_invalidator(&self, path: &Path) -> Result<()> {
329 if let Some(invalidator) = turbo_tasks::get_invalidator() {
330 self.invalidator_map
331 .insert(path.to_owned(), invalidator, None);
332 self.watcher.ensure_watched_file(path, self.root_path())?;
333 }
334 Ok(())
335 }
336
337 fn register_write_invalidator(
341 &self,
342 path: &Path,
343 invalidator: Invalidator,
344 write_content: WriteContent,
345 ) -> Result<Vec<(Invalidator, Option<WriteContent>)>> {
346 let mut invalidator_map = self.invalidator_map.lock().unwrap();
347 let invalidators = invalidator_map.entry(path.to_owned()).or_default();
348 let old_invalidators = invalidators
349 .extract_if(|i, old_write_content| {
350 i == &invalidator
351 || old_write_content
352 .as_ref()
353 .is_none_or(|old| old != &write_content)
354 })
355 .filter(|(i, _)| i != &invalidator)
356 .collect::<Vec<_>>();
357 invalidators.insert(invalidator, Some(write_content));
358 drop(invalidator_map);
359 self.watcher.ensure_watched_file(path, self.root_path())?;
360 Ok(old_invalidators)
361 }
362
363 fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
366 if let Some(invalidator) = turbo_tasks::get_invalidator() {
367 self.dir_invalidator_map
368 .insert(path.to_owned(), invalidator, None);
369 self.watcher.ensure_watched_dir(path, self.root_path())?;
370 }
371 Ok(())
372 }
373
374 async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
375 let lock1 = self.invalidation_lock.read().await;
376 let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
377 PathLockGuard(lock1, lock2)
378 }
379
380 fn invalidate(&self) {
381 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
382 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
383 return;
384 };
385 let _guard = self.tokio_handle.enter();
386
387 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
388 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
389 let invalidators = invalidator_map
390 .into_iter()
391 .chain(dir_invalidator_map)
392 .flat_map(|(_, invalidators)| invalidators.into_keys())
393 .collect::<Vec<_>>();
394 parallel::for_each_owned(invalidators, |invalidator| {
395 invalidator.invalidate(&*turbo_tasks)
396 });
397 }
398
399 fn invalidate_with_reason<R: InvalidationReason + Clone>(
403 &self,
404 reason: impl Fn(&Path) -> R + Sync,
405 ) {
406 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
407 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
408 return;
409 };
410 let _guard = self.tokio_handle.enter();
411
412 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
413 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
414 let invalidators = invalidator_map
415 .into_iter()
416 .chain(dir_invalidator_map)
417 .flat_map(|(path, invalidators)| {
418 let reason_for_path = reason(&path);
419 invalidators
420 .into_keys()
421 .map(move |i| (reason_for_path.clone(), i))
422 })
423 .collect::<Vec<_>>();
424 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
425 invalidator.invalidate_with_reason(&*turbo_tasks, reason)
426 });
427 }
428
429 fn invalidate_from_write(
430 &self,
431 full_path: &Path,
432 invalidators: Vec<(Invalidator, Option<WriteContent>)>,
433 ) {
434 if !invalidators.is_empty() {
435 let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
436 return;
437 };
438 let _guard = self.tokio_handle.enter();
439
440 if let Some(path) = format_absolute_fs_path(full_path, &self.name, self.root_path()) {
441 if invalidators.len() == 1 {
442 let (invalidator, _) = invalidators.into_iter().next().unwrap();
443 invalidator.invalidate_with_reason(&*turbo_tasks, Write { path });
444 } else {
445 invalidators.into_iter().for_each(|(invalidator, _)| {
446 invalidator
447 .invalidate_with_reason(&*turbo_tasks, Write { path: path.clone() });
448 });
449 }
450 } else {
451 invalidators.into_iter().for_each(|(invalidator, _)| {
452 invalidator.invalidate(&*turbo_tasks);
453 });
454 }
455 }
456 }
457
458 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
459 async fn start_watching_internal(
460 self: &Arc<Self>,
461 report_invalidation_reason: bool,
462 poll_interval: Option<Duration>,
463 ) -> Result<()> {
464 let root_path = self.root_path().to_path_buf();
465
466 retry_blocking(|| std::fs::create_dir_all(&root_path))
468 .instrument(tracing::info_span!("create root directory", name = ?root_path))
469 .concurrency_limited(&self.write_semaphore)
470 .await?;
471
472 self.watcher
473 .start_watching(self.clone(), report_invalidation_reason, poll_interval)?;
474
475 Ok(())
476 }
477
478 async fn create_directory(self: &Arc<Self>, directory: &Path) -> Result<()> {
479 let already_created = ApplyEffectsContext::with_or_insert_with(
480 DiskFileSystemApplyContext::default,
481 |fs_context| fs_context.created_directories.contains(directory),
482 );
483 if !already_created {
484 retry_blocking(|| std::fs::create_dir_all(directory))
485 .instrument(tracing::info_span!("create directory", name = ?directory))
486 .concurrency_limited(&self.write_semaphore)
487 .await?;
488 ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| {
489 fs_context
490 .created_directories
491 .insert(directory.to_path_buf())
492 });
493 }
494 Ok(())
495 }
496}
497
498#[turbo_tasks::value(cell = "new", eq = "manual")]
499pub struct DiskFileSystem {
500 inner: Arc<DiskFileSystemInner>,
501}
502
503impl DiskFileSystem {
504 pub fn name(&self) -> &RcStr {
505 &self.inner.name
506 }
507
508 pub fn root(&self) -> &RcStr {
509 &self.inner.root
510 }
511
512 pub fn invalidate(&self) {
513 self.inner.invalidate();
514 }
515
516 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
517 &self,
518 reason: impl Fn(&Path) -> R + Sync,
519 ) {
520 self.inner.invalidate_with_reason(reason);
521 }
522
523 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
524 self.inner
525 .start_watching_internal(false, poll_interval)
526 .await
527 }
528
529 pub async fn start_watching_with_invalidation_reason(
530 &self,
531 poll_interval: Option<Duration>,
532 ) -> Result<()> {
533 self.inner
534 .start_watching_internal(true, poll_interval)
535 .await
536 }
537
538 pub fn stop_watching(&self) {
539 self.inner.watcher.stop_watching();
540 }
541
542 pub fn try_from_sys_path(
555 &self,
556 vc_self: ResolvedVc<DiskFileSystem>,
557 sys_path: &Path,
558 relative_to: Option<&FileSystemPath>,
559 ) -> Option<FileSystemPath> {
560 let vc_self = ResolvedVc::upcast(vc_self);
561
562 let sys_path = simplified(sys_path);
563 let relative_sys_path = if sys_path.is_absolute() {
564 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
567 normalized_sys_path
568 .strip_prefix(self.inner.root_path())
569 .ok()?
570 .to_owned()
571 } else if let Some(relative_to) = relative_to {
572 debug_assert_eq!(
573 relative_to.fs, vc_self,
574 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
575 );
576 let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
577 joined_sys_path.push(sys_path);
578 joined_sys_path.normalize_lexically().ok()?
579 } else {
580 sys_path.normalize_lexically().ok()?
581 };
582
583 Some(FileSystemPath {
584 fs: vc_self,
585 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
586 })
587 }
588
589 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
590 let path = self.inner.root_path();
591 if fs_path.path.is_empty() {
592 path.to_path_buf()
593 } else {
594 path.join(&*unix_to_sys(&fs_path.path))
595 }
596 }
597}
598
599#[allow(dead_code, reason = "we need to hold onto the locks")]
600struct PathLockGuard<'a>(
601 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
602 #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
603);
604
605fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
606 if let Ok(rel_path) = path.strip_prefix(root_path) {
607 let path = if MAIN_SEPARATOR != '/' {
608 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
609 format!("[{name}]/{rel_path}")
610 } else {
611 format!("[{name}]/{}", rel_path.display())
612 };
613 Some(path)
614 } else {
615 None
616 }
617}
618
619impl DiskFileSystem {
620 pub fn new(name: RcStr, root: RcStr) -> Vc<Self> {
627 Self::new_internal(name, root, Vec::new())
628 }
629
630 pub fn new_with_denied_paths(name: RcStr, root: RcStr, denied_paths: Vec<RcStr>) -> Vc<Self> {
639 for denied_path in &denied_paths {
640 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
641 debug_assert!(
642 normalize_path(denied_path).as_deref() == Some(&**denied_path),
643 "denied_path must be normalized: {denied_path:?}"
644 );
645 }
646 Self::new_internal(name, root, denied_paths)
647 }
648}
649
650#[turbo_tasks::value_impl]
651impl DiskFileSystem {
652 #[turbo_tasks::function]
653 fn new_internal(name: RcStr, root: RcStr, denied_paths: Vec<RcStr>) -> Vc<Self> {
654 let instance = DiskFileSystem {
655 inner: Arc::new(DiskFileSystemInner {
656 name,
657 root,
658 mutex_map: Default::default(),
659 invalidation_lock: Default::default(),
660 invalidator_map: InvalidatorMap::new(),
661 dir_invalidator_map: InvalidatorMap::new(),
662 read_semaphore: create_read_semaphore(),
663 write_semaphore: create_write_semaphore(),
664 watcher: DiskWatcher::new(),
665 denied_paths,
666 turbo_tasks: turbo_tasks_weak(),
667 tokio_handle: Handle::current(),
668 }),
669 };
670
671 Self::cell(instance)
672 }
673}
674
675impl Debug for DiskFileSystem {
676 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
677 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
678 }
679}
680
681#[turbo_tasks::value_impl]
682impl FileSystem for DiskFileSystem {
683 #[turbo_tasks::function(fs)]
684 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
685 mark_session_dependent();
686
687 if self.inner.is_path_denied(&fs_path) {
689 return Ok(FileContent::NotFound.cell());
690 }
691 let full_path = self.to_sys_path(&fs_path);
692
693 self.inner.register_read_invalidator(&full_path)?;
694
695 let _lock = self.inner.lock_path(&full_path).await;
696 let content = match retry_blocking(|| File::from_path(&full_path))
697 .instrument(tracing::info_span!("read file", name = ?full_path))
698 .concurrency_limited(&self.inner.read_semaphore)
699 .await
700 {
701 Ok(file) => FileContent::new(file),
702 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
703 FileContent::NotFound
704 }
705 Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
706 };
707 Ok(content.cell())
708 }
709
710 #[turbo_tasks::function(fs)]
711 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
712 mark_session_dependent();
713
714 if self.inner.is_path_denied(&fs_path) {
716 return Ok(RawDirectoryContent::not_found());
717 }
718 let full_path = self.to_sys_path(&fs_path);
719
720 self.inner.register_dir_invalidator(&full_path)?;
721
722 let read_dir = match retry_blocking(|| std::fs::read_dir(&full_path))
724 .instrument(tracing::info_span!("read directory", name = ?full_path))
725 .concurrency_limited(&self.inner.read_semaphore)
726 .await
727 {
728 Ok(dir) => dir,
729 Err(e)
730 if e.kind() == ErrorKind::NotFound
731 || e.kind() == ErrorKind::NotADirectory
732 || e.kind() == ErrorKind::InvalidFilename =>
733 {
734 return Ok(RawDirectoryContent::not_found());
735 }
736 Err(e) => {
737 return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
738 }
739 };
740 let dir_path = fs_path.path.as_str();
741 let denied_entries: FxHashSet<&str> = self
742 .inner
743 .denied_paths
744 .iter()
745 .filter_map(|denied_path| {
746 if denied_path.starts_with(dir_path) {
753 let denied_path_suffix =
754 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
755 Some(&denied_path[dir_path.len() + 1..])
756 } else if dir_path.is_empty() {
757 Some(denied_path.as_str())
758 } else {
759 None
760 };
761 denied_path_suffix.filter(|s| !s.contains('/'))
763 } else {
764 None
765 }
766 })
767 .collect();
768
769 let entries = read_dir
770 .filter_map(|r| {
771 let e = match r {
772 Ok(e) => e,
773 Err(err) => return Some(Err(err.into())),
774 };
775
776 let file_name = RcStr::from(e.file_name().to_str()?);
778 if denied_entries.contains(file_name.as_str()) {
780 return None;
781 }
782
783 let entry = match e.file_type() {
784 Ok(t) if t.is_file() => RawDirectoryEntry::File,
785 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
786 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
787 Ok(_) => RawDirectoryEntry::Other,
788 Err(err) => return Some(Err(err.into())),
789 };
790
791 Some(anyhow::Ok((file_name, entry)))
792 })
793 .collect::<Result<_>>()
794 .with_context(|| format!("reading directory item in {full_path:?}"))?;
795
796 Ok(RawDirectoryContent::new(entries))
797 }
798
799 #[turbo_tasks::function(fs)]
800 async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
801 mark_session_dependent();
802
803 if self.inner.is_path_denied(&fs_path) {
805 return Ok(LinkContent::NotFound.cell());
806 }
807 let full_path = self.to_sys_path(&fs_path);
808
809 self.inner.register_read_invalidator(&full_path)?;
810
811 let _lock = self.inner.lock_path(&full_path).await;
812 let link_path = match retry_blocking(|| std::fs::read_link(&full_path))
813 .instrument(tracing::info_span!("read symlink", name = ?full_path))
814 .concurrency_limited(&self.inner.read_semaphore)
815 .await
816 {
817 Ok(res) => res,
818 Err(_) => return Ok(LinkContent::NotFound.cell()),
819 };
820 let is_link_absolute = link_path.is_absolute();
821
822 let mut file = link_path.clone();
823 if !is_link_absolute {
824 if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
825 normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
826 }) {
827 #[cfg(windows)]
828 {
829 file = PathBuf::from(normalized_linked_path);
830 }
831 #[cfg(not(windows))]
834 {
835 file = PathBuf::from(format!("/{normalized_linked_path}"));
836 }
837 } else {
838 return Ok(LinkContent::Invalid.cell());
839 }
840 }
841
842 let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
849
850 let relative_to_root_path = match result {
851 Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
852 Err(_) => return Ok(LinkContent::Invalid.cell()),
853 };
854
855 let (target, file_type) = if is_link_absolute {
856 let target_string = RcStr::from(relative_to_root_path.to_string_lossy());
857 (
858 target_string.clone(),
859 FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
860 .get_type()
861 .await?,
862 )
863 } else {
864 let link_path_string_cow = link_path.to_string_lossy();
865 let link_path_unix = RcStr::from(sys_to_unix(&link_path_string_cow));
866 (
867 link_path_unix.clone(),
868 fs_path.parent().join(&link_path_unix)?.get_type().await?,
869 )
870 };
871
872 Ok(LinkContent::Link {
873 target,
874 link_type: {
875 let mut link_type = Default::default();
876 if link_path.is_absolute() {
877 link_type |= LinkType::ABSOLUTE;
878 }
879 if matches!(&*file_type, FileSystemEntryType::Directory) {
880 link_type |= LinkType::DIRECTORY;
881 }
882 link_type
883 },
884 }
885 .cell())
886 }
887
888 #[turbo_tasks::function(fs)]
889 async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
890 if self.inner.is_path_denied(&fs_path) {
896 bail!(
897 "Cannot write to denied path: {}",
898 fs_path.value_to_string().await?
899 );
900 }
901 let full_path = self.to_sys_path(&fs_path);
902
903 let content = content.await?;
904
905 let inner = self.inner.clone();
906 let invalidator = turbo_tasks::get_invalidator();
907
908 effect(async move {
909 let full_path = validate_path_length(&full_path)?;
910
911 let _lock = inner.lock_path(&full_path).await;
912
913 let old_invalidators = invalidator
915 .map(|invalidator| {
916 inner.register_write_invalidator(
917 &full_path,
918 invalidator,
919 WriteContent::File(content.clone()),
920 )
921 })
922 .transpose()?
923 .unwrap_or_default();
924
925 let compare = content
931 .streaming_compare(&full_path)
932 .instrument(tracing::info_span!("read file before write", name = ?full_path))
933 .concurrency_limited(&inner.read_semaphore)
934 .await?;
935 if compare == FileComparison::Equal {
936 if !old_invalidators.is_empty() {
937 for (invalidator, write_content) in old_invalidators {
938 inner.invalidator_map.insert(
939 full_path.clone().into_owned(),
940 invalidator,
941 write_content,
942 );
943 }
944 }
945 return Ok(());
946 }
947
948 match &*content {
949 FileContent::Content(..) => {
950 let create_directory = compare == FileComparison::Create;
951 if create_directory && let Some(parent) = full_path.parent() {
952 inner.create_directory(parent).await.with_context(|| {
953 format!(
954 "failed to create directory {parent:?} for write to {full_path:?}",
955 )
956 })?;
957 }
958
959 let content = content.clone();
960 retry_blocking(|| {
961 let mut f = std::fs::File::create(&full_path)?;
962 let FileContent::Content(file) = &*content else {
963 unreachable!()
964 };
965 std::io::copy(&mut file.read(), &mut f)?;
966 #[cfg(unix)]
967 f.set_permissions(file.meta.permissions.into())?;
968 f.flush()?;
969
970 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
971 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
972 .is_some_and(|v| v == "1" || v == "true")
973 });
974 if *WRITE_VERSION {
975 let mut full_path = full_path.clone().into_owned();
976 let hash = hash_xxh3_hash64(file);
977 let ext = full_path.extension();
978 let ext = if let Some(ext) = ext {
979 format!("{:016x}.{}", hash, ext.to_string_lossy())
980 } else {
981 format!("{hash:016x}")
982 };
983 full_path.set_extension(ext);
984 let mut f = std::fs::File::create(&full_path)?;
985 std::io::copy(&mut file.read(), &mut f)?;
986 #[cfg(unix)]
987 f.set_permissions(file.meta.permissions.into())?;
988 f.flush()?;
989 }
990 Ok::<(), io::Error>(())
991 })
992 .instrument(tracing::info_span!("write file", name = ?full_path))
993 .concurrency_limited(&inner.write_semaphore)
994 .await
995 .with_context(|| format!("failed to write to {full_path:?}"))?;
996 }
997 FileContent::NotFound => {
998 retry_blocking(|| std::fs::remove_file(&full_path))
999 .instrument(tracing::info_span!("remove file", name = ?full_path))
1000 .concurrency_limited(&inner.write_semaphore)
1001 .await
1002 .or_else(|err| {
1003 if err.kind() == ErrorKind::NotFound {
1004 Ok(())
1005 } else {
1006 Err(err)
1007 }
1008 })
1009 .with_context(|| format!("removing {full_path:?} failed"))?;
1010 }
1011 }
1012
1013 inner.invalidate_from_write(&full_path, old_invalidators);
1014
1015 Ok(())
1016 });
1017
1018 Ok(())
1019 }
1020
1021 #[turbo_tasks::function(fs)]
1022 async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
1023 if self.inner.is_path_denied(&fs_path) {
1029 bail!(
1030 "Cannot write link to denied path: {}",
1031 fs_path.value_to_string().await?
1032 );
1033 }
1034
1035 let content = target.await?;
1036
1037 let full_path = self.to_sys_path(&fs_path);
1038 let inner = self.inner.clone();
1039 let invalidator = turbo_tasks::get_invalidator();
1040
1041 effect(async move {
1042 let full_path = validate_path_length(&full_path)?;
1043
1044 let _lock = inner.lock_path(&full_path).await;
1045
1046 let old_invalidators = invalidator
1047 .map(|invalidator| {
1048 inner.register_write_invalidator(
1049 &full_path,
1050 invalidator,
1051 WriteContent::Link(content.clone()),
1052 )
1053 })
1054 .transpose()?
1055 .unwrap_or_default();
1056
1057 enum OsSpecificLinkContent {
1058 Link {
1059 #[cfg(windows)]
1060 is_directory: bool,
1061 target: PathBuf,
1062 },
1063 NotFound,
1064 Invalid,
1065 }
1066
1067 let os_specific_link_content = match &*content {
1068 LinkContent::Link { target, link_type } => {
1069 let is_directory = link_type.contains(LinkType::DIRECTORY);
1070 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1071 Path::new(&inner.root).join(unix_to_sys(target).as_ref())
1072 } else {
1073 let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1074 if cfg!(windows) && is_directory {
1075 full_path
1077 .parent()
1078 .unwrap_or(&full_path)
1079 .join(relative_target)
1080 } else {
1081 relative_target
1082 }
1083 };
1084 OsSpecificLinkContent::Link {
1085 #[cfg(windows)]
1086 is_directory,
1087 target: target_path,
1088 }
1089 }
1090 LinkContent::Invalid => OsSpecificLinkContent::Invalid,
1091 LinkContent::NotFound => OsSpecificLinkContent::NotFound,
1092 };
1093
1094 let old_content = match retry_blocking(|| std::fs::read_link(&full_path))
1095 .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1096 .concurrency_limited(&inner.read_semaphore)
1097 .await
1098 {
1099 Ok(res) => Some((res.is_absolute(), res)),
1100 Err(_) => None,
1101 };
1102 let is_equal = match (&os_specific_link_content, &old_content) {
1103 (
1104 OsSpecificLinkContent::Link { target, .. },
1105 Some((old_is_absolute, old_target)),
1106 ) => target == old_target && target.is_absolute() == *old_is_absolute,
1107 (OsSpecificLinkContent::NotFound, None) => true,
1108 _ => false,
1109 };
1110 if is_equal {
1111 if !old_invalidators.is_empty() {
1112 for (invalidator, write_content) in old_invalidators {
1113 inner.invalidator_map.insert(
1114 full_path.clone().into_owned(),
1115 invalidator,
1116 write_content,
1117 );
1118 }
1119 }
1120 return Ok(());
1121 }
1122
1123 match os_specific_link_content {
1124 OsSpecificLinkContent::Link {
1125 target,
1126 #[cfg(windows)]
1127 is_directory,
1128 ..
1129 } => {
1130 let full_path = full_path.into_owned();
1131
1132 let create_directory = old_content.is_none();
1133 if create_directory && let Some(parent) = full_path.parent() {
1134 inner.create_directory(parent).await.with_context(|| {
1135 format!(
1136 "failed to create directory {parent:?} for write link to \
1137 {full_path:?}",
1138 )
1139 })?;
1140 }
1141
1142 #[derive(thiserror::Error, Debug)]
1143 #[error("{msg}: {source}")]
1144 struct SymlinkCreationError {
1145 msg: &'static str,
1146 #[source]
1147 source: io::Error,
1148 }
1149
1150 let mut has_old_content = old_content.is_some();
1151 let try_create_link = || {
1152 if has_old_content {
1153 remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1157 SymlinkCreationError {
1158 msg: "removal of existing symbolic link or junction point \
1159 failed",
1160 source: err,
1161 }
1162 })?;
1163 has_old_content = false;
1164 }
1165 #[cfg(not(windows))]
1166 let io_result = std::os::unix::fs::symlink(&target, &full_path);
1167 #[cfg(windows)]
1168 let io_result = if is_directory {
1169 std::os::windows::fs::junction_point(&target, &full_path)
1170 } else {
1171 std::os::windows::fs::symlink_file(&target, &full_path)
1172 };
1173 io_result.map_err(|err| {
1174 if err.kind() == ErrorKind::AlreadyExists {
1175 has_old_content = true;
1177 }
1178 SymlinkCreationError {
1179 msg: "creation of a new symbolic link or junction point failed",
1180 source: err,
1181 }
1182 })
1183 };
1184 fn can_retry_link(err: &SymlinkCreationError) -> bool {
1185 err.source.kind() == ErrorKind::AlreadyExists || can_retry(&err.source)
1186 }
1187 let err_context = || {
1188 #[cfg(not(windows))]
1189 let message = format!(
1190 "failed to create symlink at {full_path:?} pointing to {target:?}"
1191 );
1192 #[cfg(windows)]
1193 let message = if is_directory {
1194 format!(
1195 "failed to create junction point at {full_path:?} pointing to \
1196 {target:?}"
1197 )
1198 } else {
1199 format!(
1200 "failed to create symlink at {full_path:?} pointing to {target:?}\n\
1201 (Note: creating file symlinks on Windows require developer mode or \
1202 admin permissions: \
1203 https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1204 )
1205 };
1206 message
1207 };
1208 retry_blocking_custom(try_create_link, can_retry_link)
1209 .instrument(tracing::info_span!(
1210 "write symlink",
1211 name = ?full_path,
1212 target = ?target,
1213 ))
1214 .concurrency_limited(&inner.write_semaphore)
1215 .await
1216 .with_context(err_context)?;
1217 }
1218 OsSpecificLinkContent::Invalid => {
1219 bail!("invalid symlink target: {full_path:?}")
1220 }
1221 OsSpecificLinkContent::NotFound => {
1222 retry_blocking(|| remove_symbolic_link_dir_helper(&full_path))
1223 .instrument(tracing::info_span!("remove symlink", name = ?full_path))
1224 .concurrency_limited(&inner.write_semaphore)
1225 .await
1226 .with_context(|| format!("removing {full_path:?} failed"))?;
1227 }
1228 }
1229
1230 Ok(())
1231 });
1232 Ok(())
1233 }
1234
1235 #[turbo_tasks::function(fs)]
1236 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1237 mark_session_dependent();
1238 let full_path = self.to_sys_path(&fs_path);
1239
1240 if self.inner.is_path_denied(&fs_path) {
1242 bail!(
1243 "Cannot read metadata from denied path: {}",
1244 fs_path.value_to_string().await?
1245 );
1246 }
1247
1248 self.inner.register_read_invalidator(&full_path)?;
1249
1250 let _lock = self.inner.lock_path(&full_path).await;
1251 let meta = retry_blocking(|| std::fs::metadata(&full_path))
1252 .instrument(tracing::info_span!("read metadata", name = ?full_path))
1253 .concurrency_limited(&self.inner.read_semaphore)
1254 .await
1255 .with_context(|| format!("reading metadata for {:?}", full_path))?;
1256
1257 Ok(FileMeta::cell(meta.into()))
1258 }
1259}
1260
1261fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1262 let result = if cfg!(windows) {
1263 std::fs::remove_dir(path).or_else(|err| {
1276 if err.kind() == ErrorKind::NotADirectory {
1277 std::fs::remove_file(path)
1278 } else {
1279 Err(err)
1280 }
1281 })
1282 } else {
1283 std::fs::remove_file(path)
1284 };
1285 match result {
1286 Ok(()) => Ok(()),
1287 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1288 Err(err) => Err(err),
1289 }
1290}
1291
1292#[turbo_tasks::value_impl]
1293impl ValueToString for DiskFileSystem {
1294 #[turbo_tasks::function]
1295 fn to_string(&self) -> Vc<RcStr> {
1296 Vc::cell(self.inner.name.clone())
1297 }
1298}
1299
1300#[turbo_tasks::value(shared)]
1301#[derive(Debug, Clone, Hash, TaskInput)]
1302pub struct FileSystemPath {
1303 pub fs: ResolvedVc<Box<dyn FileSystem>>,
1304 pub path: RcStr,
1305}
1306
1307impl FileSystemPath {
1308 pub fn value_to_string(&self) -> Vc<RcStr> {
1310 value_to_string(self.clone())
1311 }
1312}
1313
1314#[turbo_tasks::function]
1315async fn value_to_string(path: FileSystemPath) -> Result<Vc<RcStr>> {
1316 Ok(Vc::cell(
1317 format!("[{}]/{}", path.fs.to_string().await?, path.path).into(),
1318 ))
1319}
1320
1321impl FileSystemPath {
1322 pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1323 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1324 if other.path.is_empty() {
1325 true
1326 } else {
1327 self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1328 }
1329 } else {
1330 false
1331 }
1332 }
1333
1334 pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1335 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1336 if other.path.is_empty() {
1337 true
1338 } else {
1339 matches!(
1340 self.path.as_bytes().get(other.path.len()),
1341 Some(&b'/') | None
1342 )
1343 }
1344 } else {
1345 false
1346 }
1347 }
1348
1349 pub fn is_root(&self) -> bool {
1350 self.path.is_empty()
1351 }
1352
1353 pub fn is_in_node_modules(&self) -> bool {
1354 self.path.starts_with("node_modules/") || self.path.contains("/node_modules/")
1355 }
1356
1357 pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1361 if self.fs != inner.fs {
1362 return None;
1363 }
1364 let path = inner.path.strip_prefix(&*self.path)?;
1365 if self.path.is_empty() {
1366 Some(path)
1367 } else if let Some(stripped) = path.strip_prefix('/') {
1368 Some(stripped)
1369 } else {
1370 None
1371 }
1372 }
1373
1374 pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1375 if self.fs != other.fs {
1376 return None;
1377 }
1378
1379 Some(get_relative_path_to(&self.path, &other.path).into())
1380 }
1381
1382 pub fn file_name(&self) -> &str {
1385 let (_, file_name) = self.split_file_name();
1386 file_name
1387 }
1388
1389 pub fn has_extension(&self, extension: &str) -> bool {
1394 debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1395 self.path.ends_with(extension)
1396 }
1397
1398 pub fn extension_ref(&self) -> Option<&str> {
1400 let (_, extension) = self.split_extension();
1401 extension
1402 }
1403
1404 fn split_extension(&self) -> (&str, Option<&str>) {
1408 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1409 if extension.contains('/') ||
1410 path_before_extension.ends_with('/') || path_before_extension.is_empty()
1412 {
1413 (self.path.as_str(), None)
1414 } else {
1415 (path_before_extension, Some(extension))
1416 }
1417 } else {
1418 (self.path.as_str(), None)
1419 }
1420 }
1421
1422 fn split_file_name(&self) -> (Option<&str>, &str) {
1426 if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1428 (Some(parent), file_name)
1429 } else {
1430 (None, self.path.as_str())
1431 }
1432 }
1433
1434 fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1439 let (path_before_extension, extension) = self.split_extension();
1440
1441 if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1442 (Some(parent), file_stem, extension)
1443 } else {
1444 (None, path_before_extension, extension)
1445 }
1446 }
1447}
1448
1449#[turbo_tasks::value(transparent)]
1450pub struct FileSystemPathOption(Option<FileSystemPath>);
1451
1452#[turbo_tasks::value_impl]
1453impl FileSystemPathOption {
1454 #[turbo_tasks::function]
1455 pub fn none() -> Vc<Self> {
1456 Vc::cell(None)
1457 }
1458}
1459
1460impl FileSystemPath {
1461 fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1465 debug_assert!(
1469 MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1470 "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1471 '/'",
1472 );
1473 debug_assert!(
1474 normalize_path(&path).as_deref() == Some(&*path),
1475 "path {path} must be normalized",
1476 );
1477 FileSystemPath { fs, path }
1478 }
1479
1480 pub fn join(&self, path: &str) -> Result<Self> {
1484 if let Some(path) = join_path(&self.path, path) {
1485 Ok(Self::new_normalized(self.fs, path.into()))
1486 } else {
1487 bail!(
1488 "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1489 self.path,
1490 path,
1491 );
1492 }
1493 }
1494
1495 pub fn append(&self, path: &str) -> Result<Self> {
1497 if path.contains('/') {
1498 bail!(
1499 "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1500 self.path,
1501 path,
1502 )
1503 }
1504 Ok(Self::new_normalized(
1505 self.fs,
1506 format!("{}{}", self.path, path).into(),
1507 ))
1508 }
1509
1510 pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1513 if appending.contains('/') {
1514 bail!(
1515 "FileSystemPath(\"{}\").append_to_stem(\"{}\") must not append '/'",
1516 self.path,
1517 appending,
1518 )
1519 }
1520 if let (path, Some(ext)) = self.split_extension() {
1521 return Ok(Self::new_normalized(
1522 self.fs,
1523 format!("{path}{appending}.{ext}").into(),
1524 ));
1525 }
1526 Ok(Self::new_normalized(
1527 self.fs,
1528 format!("{}{}", self.path, appending).into(),
1529 ))
1530 }
1531
1532 #[allow(clippy::needless_borrow)] pub fn try_join(&self, path: &str) -> Option<FileSystemPath> {
1536 #[cfg(target_os = "windows")]
1538 let path = path.replace('\\', "/");
1539
1540 join_path(&self.path, &path).map(|p| Self::new_normalized(self.fs, RcStr::from(p)))
1541 }
1542
1543 pub fn try_join_inside(&self, path: &str) -> Option<FileSystemPath> {
1547 if let Some(p) = join_path(&self.path, path)
1548 && p.starts_with(&*self.path)
1549 {
1550 return Some(Self::new_normalized(self.fs, RcStr::from(p)));
1551 }
1552 None
1553 }
1554
1555 pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1558 read_glob(self.clone(), glob)
1559 }
1560
1561 pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1564 track_glob(self.clone(), glob, include_dot_files)
1565 }
1566
1567 pub fn root(&self) -> Vc<Self> {
1568 self.fs().root()
1569 }
1570}
1571
1572impl FileSystemPath {
1573 pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1574 *self.fs
1575 }
1576
1577 pub fn extension(&self) -> &str {
1578 self.extension_ref().unwrap_or_default()
1579 }
1580
1581 pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1582 self.is_inside_ref(other)
1583 }
1584
1585 pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1586 self.is_inside_or_equal_ref(other)
1587 }
1588
1589 pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1592 let (path_without_extension, _) = self.split_extension();
1593 Self::new_normalized(
1594 self.fs,
1595 match extension.is_empty() {
1598 true => path_without_extension.into(),
1599 false => format!("{path_without_extension}.{extension}").into(),
1600 },
1601 )
1602 }
1603
1604 pub fn file_stem(&self) -> Option<&str> {
1613 let (_, file_stem, _) = self.split_file_stem_extension();
1614 if file_stem.is_empty() {
1615 return None;
1616 }
1617 Some(file_stem)
1618 }
1619}
1620
1621impl Display for FileSystemPath {
1622 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1623 write!(f, "{}", self.path)
1624 }
1625}
1626
1627#[turbo_tasks::function]
1628pub async fn rebase(
1629 fs_path: FileSystemPath,
1630 old_base: FileSystemPath,
1631 new_base: FileSystemPath,
1632) -> Result<Vc<FileSystemPath>> {
1633 let new_path;
1634 if old_base.path.is_empty() {
1635 if new_base.path.is_empty() {
1636 new_path = fs_path.path.clone();
1637 } else {
1638 new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1639 }
1640 } else {
1641 let base_path = [&old_base.path, "/"].concat();
1642 if !fs_path.path.starts_with(&base_path) {
1643 bail!(
1644 "rebasing {fs_path} from {old_base} onto {new_base} doesn't work because it's not \
1645 part of the source path",
1646 );
1647 }
1648 if new_base.path.is_empty() {
1649 new_path = [&fs_path.path[base_path.len()..]].concat().into();
1650 } else {
1651 new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1652 .concat()
1653 .into();
1654 }
1655 }
1656 Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1657}
1658
1659impl FileSystemPath {
1661 pub fn read(&self) -> Vc<FileContent> {
1662 self.fs().read(self.clone())
1663 }
1664
1665 pub fn read_link(&self) -> Vc<LinkContent> {
1666 self.fs().read_link(self.clone())
1667 }
1668
1669 pub fn read_json(&self) -> Vc<FileJsonContent> {
1670 self.fs().read(self.clone()).parse_json()
1671 }
1672
1673 pub fn read_json5(&self) -> Vc<FileJsonContent> {
1674 self.fs().read(self.clone()).parse_json5()
1675 }
1676
1677 pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1682 self.fs().raw_read_dir(self.clone())
1683 }
1684
1685 pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1686 self.fs().write(self.clone(), content)
1687 }
1688
1689 pub fn write_symbolic_link_dir(&self, target: Vc<LinkContent>) -> Vc<()> {
1707 self.fs().write_link(self.clone(), target)
1708 }
1709
1710 pub fn metadata(&self) -> Vc<FileMeta> {
1711 self.fs().metadata(self.clone())
1712 }
1713
1714 pub async fn realpath(&self) -> Result<FileSystemPath> {
1717 let result = &(*self.realpath_with_links().await?);
1718 match &result.path_result {
1719 Ok(path) => Ok(path.clone()),
1720 Err(error) => Err(anyhow::anyhow!(error.as_error_message(self, result))),
1721 }
1722 }
1723
1724 pub fn rebase(
1725 fs_path: FileSystemPath,
1726 old_base: FileSystemPath,
1727 new_base: FileSystemPath,
1728 ) -> Vc<FileSystemPath> {
1729 rebase(fs_path, old_base, new_base)
1730 }
1731}
1732
1733impl FileSystemPath {
1734 pub fn read_dir(&self) -> Vc<DirectoryContent> {
1739 read_dir(self.clone())
1740 }
1741
1742 pub fn parent(&self) -> FileSystemPath {
1743 let path = &self.path;
1744 if path.is_empty() {
1745 return self.clone();
1746 }
1747 FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1748 }
1749
1750 pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1759 get_type(self.clone())
1760 }
1761
1762 pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1763 realpath_with_links(self.clone())
1764 }
1765}
1766
1767#[turbo_tasks::value_impl]
1768impl ValueToString for FileSystemPath {
1769 #[turbo_tasks::function]
1770 fn to_string(&self) -> Vc<RcStr> {
1771 self.value_to_string()
1772 }
1773}
1774
1775#[derive(Clone, Debug)]
1776#[turbo_tasks::value(shared)]
1777pub struct RealPathResult {
1778 pub path_result: Result<FileSystemPath, RealPathResultError>,
1779 pub symlinks: Vec<FileSystemPath>,
1780}
1781
1782#[derive(Debug, Clone, Hash, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
1785pub enum RealPathResultError {
1786 TooManySymlinks,
1787 CycleDetected,
1788 Invalid,
1789 NotFound,
1790}
1791
1792impl RealPathResultError {
1793 pub fn as_error_message(&self, orig: &FileSystemPath, result: &RealPathResult) -> String {
1795 match self {
1796 RealPathResultError::TooManySymlinks => format!(
1797 "Symlink {orig} leads to too many other symlinks ({len} links)",
1798 len = result.symlinks.len()
1799 ),
1800 RealPathResultError::CycleDetected => {
1801 format!("Symlink {orig} is in a symlink loop: {:?}", result.symlinks)
1802 }
1803 RealPathResultError::Invalid => {
1804 format!("Symlink {orig} is invalid, it points out of the filesystem root")
1805 }
1806 RealPathResultError::NotFound => {
1807 format!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1808 }
1809 }
1810 }
1811}
1812
1813#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
1814#[turbo_tasks::value(shared)]
1815pub enum Permissions {
1816 Readable,
1817 #[default]
1818 Writable,
1819 Executable,
1820}
1821
1822#[cfg(unix)]
1825impl From<Permissions> for std::fs::Permissions {
1826 fn from(perm: Permissions) -> Self {
1827 use std::os::unix::fs::PermissionsExt;
1828 match perm {
1829 Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1830 Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1831 Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1832 }
1833 }
1834}
1835
1836#[cfg(unix)]
1837impl From<std::fs::Permissions> for Permissions {
1838 fn from(perm: std::fs::Permissions) -> Self {
1839 use std::os::unix::fs::PermissionsExt;
1840 if perm.readonly() {
1841 Permissions::Readable
1842 } else {
1843 if perm.mode() & 0o111 != 0 {
1845 Permissions::Executable
1846 } else {
1847 Permissions::Writable
1848 }
1849 }
1850 }
1851}
1852
1853#[cfg(not(unix))]
1854impl From<std::fs::Permissions> for Permissions {
1855 fn from(_: std::fs::Permissions) -> Self {
1856 Permissions::default()
1857 }
1858}
1859
1860#[turbo_tasks::value(shared)]
1861#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
1862pub enum FileContent {
1863 Content(File),
1864 NotFound,
1865}
1866
1867impl From<File> for FileContent {
1868 fn from(file: File) -> Self {
1869 FileContent::Content(file)
1870 }
1871}
1872
1873#[derive(Clone, Debug, Eq, PartialEq)]
1874enum FileComparison {
1875 Create,
1876 Equal,
1877 NotEqual,
1878}
1879
1880impl FileContent {
1881 async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
1884 let old_file =
1885 extract_disk_access(retry_blocking(|| std::fs::File::open(path)).await, path)?;
1886 let Some(old_file) = old_file else {
1887 return Ok(match self {
1888 FileContent::NotFound => FileComparison::Equal,
1889 _ => FileComparison::Create,
1890 });
1891 };
1892 let FileContent::Content(new_file) = self else {
1894 return Ok(FileComparison::NotEqual);
1895 };
1896
1897 let old_meta = extract_disk_access(retry_blocking(|| old_file.metadata()).await, path)?;
1898 let Some(old_meta) = old_meta else {
1899 return Ok(FileComparison::Create);
1903 };
1904 if new_file.meta != old_meta.into() {
1906 return Ok(FileComparison::NotEqual);
1907 }
1908
1909 let mut new_contents = new_file.read();
1912 let mut old_contents = BufReader::new(old_file);
1913 Ok(loop {
1914 let new_chunk = new_contents.fill_buf()?;
1915 let Ok(old_chunk) = old_contents.fill_buf() else {
1916 break FileComparison::NotEqual;
1917 };
1918
1919 let len = min(new_chunk.len(), old_chunk.len());
1920 if len == 0 {
1921 if new_chunk.len() == old_chunk.len() {
1922 break FileComparison::Equal;
1923 } else {
1924 break FileComparison::NotEqual;
1925 }
1926 }
1927
1928 if new_chunk[0..len] != old_chunk[0..len] {
1929 break FileComparison::NotEqual;
1930 }
1931
1932 new_contents.consume(len);
1933 old_contents.consume(len);
1934 })
1935 }
1936}
1937
1938bitflags! {
1939 #[derive(
1940 Default,
1941 TraceRawVcs,
1942 NonLocalValue,
1943 DeterministicHash,
1944 Encode,
1945 Decode,
1946 )]
1947 pub struct LinkType: u8 {
1948 const DIRECTORY = 0b00000001;
1949 const ABSOLUTE = 0b00000010;
1950 }
1951}
1952
1953#[turbo_tasks::value(shared)]
1959#[derive(Debug)]
1960pub enum LinkContent {
1961 Link {
1972 target: RcStr,
1973 link_type: LinkType,
1974 },
1975 Invalid,
1977 NotFound,
1979}
1980
1981#[turbo_tasks::value(shared)]
1982#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
1983pub struct File {
1984 #[turbo_tasks(debug_ignore)]
1985 content: Rope,
1986 meta: FileMeta,
1987}
1988
1989impl File {
1990 fn from_path(p: &Path) -> io::Result<Self> {
1992 let mut file = std::fs::File::open(p)?;
1993 let metadata = file.metadata()?;
1994
1995 let mut output = Vec::with_capacity(metadata.len() as usize);
1996 file.read_to_end(&mut output)?;
1997
1998 Ok(File {
1999 meta: metadata.into(),
2000 content: Rope::from(output),
2001 })
2002 }
2003
2004 fn from_bytes(content: Vec<u8>) -> Self {
2006 File {
2007 meta: FileMeta::default(),
2008 content: Rope::from(content),
2009 }
2010 }
2011
2012 fn from_rope(content: Rope) -> Self {
2014 File {
2015 meta: FileMeta::default(),
2016 content,
2017 }
2018 }
2019
2020 pub fn content_type(&self) -> Option<&Mime> {
2022 self.meta.content_type.as_ref()
2023 }
2024
2025 pub fn with_content_type(mut self, content_type: Mime) -> Self {
2027 self.meta.content_type = Some(content_type);
2028 self
2029 }
2030
2031 pub fn read(&self) -> RopeReader<'_> {
2033 self.content.read()
2034 }
2035}
2036
2037impl Debug for File {
2038 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2039 f.debug_struct("File")
2040 .field("meta", &self.meta)
2041 .field("content (hash)", &hash_xxh3_hash64(&self.content))
2042 .finish()
2043 }
2044}
2045
2046impl From<RcStr> for File {
2047 fn from(s: RcStr) -> Self {
2048 s.into_owned().into()
2049 }
2050}
2051
2052impl From<String> for File {
2053 fn from(s: String) -> Self {
2054 File::from_bytes(s.into_bytes())
2055 }
2056}
2057
2058impl From<ReadRef<RcStr>> for File {
2059 fn from(s: ReadRef<RcStr>) -> Self {
2060 File::from_bytes(s.as_bytes().to_vec())
2061 }
2062}
2063
2064impl From<&str> for File {
2065 fn from(s: &str) -> Self {
2066 File::from_bytes(s.as_bytes().to_vec())
2067 }
2068}
2069
2070impl From<Vec<u8>> for File {
2071 fn from(bytes: Vec<u8>) -> Self {
2072 File::from_bytes(bytes)
2073 }
2074}
2075
2076impl From<&[u8]> for File {
2077 fn from(bytes: &[u8]) -> Self {
2078 File::from_bytes(bytes.to_vec())
2079 }
2080}
2081
2082impl From<ReadRef<Rope>> for File {
2083 fn from(rope: ReadRef<Rope>) -> Self {
2084 File::from_rope(ReadRef::into_owned(rope))
2085 }
2086}
2087
2088impl From<Rope> for File {
2089 fn from(rope: Rope) -> Self {
2090 File::from_rope(rope)
2091 }
2092}
2093
2094impl File {
2095 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
2096 Self {
2097 meta,
2098 content: Rope::from(content),
2099 }
2100 }
2101
2102 pub fn meta(&self) -> &FileMeta {
2104 &self.meta
2105 }
2106
2107 pub fn content(&self) -> &Rope {
2109 &self.content
2110 }
2111}
2112
2113#[turbo_tasks::value(shared)]
2114#[derive(Debug, Clone, Default)]
2115pub struct FileMeta {
2116 permissions: Permissions,
2119 #[bincode(with = "turbo_bincode::mime_option")]
2120 #[turbo_tasks(trace_ignore)]
2121 content_type: Option<Mime>,
2122}
2123
2124impl Ord for FileMeta {
2125 fn cmp(&self, other: &Self) -> Ordering {
2126 self.permissions
2127 .cmp(&other.permissions)
2128 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2129 }
2130}
2131
2132impl PartialOrd for FileMeta {
2133 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2134 Some(self.cmp(other))
2135 }
2136}
2137
2138impl From<std::fs::Metadata> for FileMeta {
2139 fn from(meta: std::fs::Metadata) -> Self {
2140 let permissions = meta.permissions().into();
2141
2142 Self {
2143 permissions,
2144 content_type: None,
2145 }
2146 }
2147}
2148
2149impl DeterministicHash for FileMeta {
2150 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2151 self.permissions.deterministic_hash(state);
2152 if let Some(content_type) = &self.content_type {
2153 content_type.to_string().deterministic_hash(state);
2154 }
2155 }
2156}
2157
2158impl FileContent {
2159 pub fn new(file: File) -> Self {
2160 FileContent::Content(file)
2161 }
2162
2163 pub fn is_content(&self) -> bool {
2164 matches!(self, FileContent::Content(_))
2165 }
2166
2167 pub fn as_content(&self) -> Option<&File> {
2168 match self {
2169 FileContent::Content(file) => Some(file),
2170 FileContent::NotFound => None,
2171 }
2172 }
2173
2174 pub fn parse_json_ref(&self) -> FileJsonContent {
2175 match self {
2176 FileContent::Content(file) => {
2177 let content = file.content.clone().into_bytes();
2178 let de = &mut serde_json::Deserializer::from_slice(&content);
2179 match serde_path_to_error::deserialize(de) {
2180 Ok(data) => FileJsonContent::Content(data),
2181 Err(e) => FileJsonContent::Unparsable(Box::new(
2182 UnparsableJson::from_serde_path_to_error(e),
2183 )),
2184 }
2185 }
2186 FileContent::NotFound => FileJsonContent::NotFound,
2187 }
2188 }
2189
2190 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2191 match self {
2192 FileContent::Content(file) => match file.content.to_str() {
2193 Ok(string) => match parse_to_serde_value(
2194 &string,
2195 &ParseOptions {
2196 allow_comments: true,
2197 allow_trailing_commas: true,
2198 allow_loose_object_property_names: false,
2199 },
2200 ) {
2201 Ok(data) => match data {
2202 Some(value) => FileJsonContent::Content(value),
2203 None => FileJsonContent::unparsable(rcstr!(
2204 "text content doesn't contain any json data"
2205 )),
2206 },
2207 Err(e) => FileJsonContent::Unparsable(Box::new(
2208 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2209 )),
2210 },
2211 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2212 },
2213 FileContent::NotFound => FileJsonContent::NotFound,
2214 }
2215 }
2216
2217 pub fn parse_json5_ref(&self) -> FileJsonContent {
2218 match self {
2219 FileContent::Content(file) => match file.content.to_str() {
2220 Ok(string) => match parse_to_serde_value(
2221 &string,
2222 &ParseOptions {
2223 allow_comments: true,
2224 allow_trailing_commas: true,
2225 allow_loose_object_property_names: true,
2226 },
2227 ) {
2228 Ok(data) => match data {
2229 Some(value) => FileJsonContent::Content(value),
2230 None => FileJsonContent::unparsable(rcstr!(
2231 "text content doesn't contain any json data"
2232 )),
2233 },
2234 Err(e) => FileJsonContent::Unparsable(Box::new(
2235 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2236 )),
2237 },
2238 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2239 },
2240 FileContent::NotFound => FileJsonContent::NotFound,
2241 }
2242 }
2243
2244 pub fn lines_ref(&self) -> FileLinesContent {
2245 match self {
2246 FileContent::Content(file) => match file.content.to_str() {
2247 Ok(string) => {
2248 let mut bytes_offset = 0;
2249 FileLinesContent::Lines(
2250 string
2251 .split('\n')
2252 .map(|l| {
2253 let line = FileLine {
2254 content: l.to_string(),
2255 bytes_offset,
2256 };
2257 bytes_offset += (l.len() + 1) as u32;
2258 line
2259 })
2260 .collect(),
2261 )
2262 }
2263 Err(_) => FileLinesContent::Unparsable,
2264 },
2265 FileContent::NotFound => FileLinesContent::NotFound,
2266 }
2267 }
2268}
2269
2270#[turbo_tasks::value_impl]
2271impl FileContent {
2272 #[turbo_tasks::function]
2273 pub fn len(&self) -> Result<Vc<Option<u64>>> {
2274 Ok(Vc::cell(match self {
2275 FileContent::Content(file) => Some(file.content.len() as u64),
2276 FileContent::NotFound => None,
2277 }))
2278 }
2279
2280 #[turbo_tasks::function]
2281 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2282 Ok(self.parse_json_ref().cell())
2283 }
2284
2285 #[turbo_tasks::function]
2286 pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
2287 self.parse_json_with_comments_ref().cell()
2288 }
2289
2290 #[turbo_tasks::function]
2291 pub fn parse_json5(&self) -> Vc<FileJsonContent> {
2292 self.parse_json5_ref().cell()
2293 }
2294
2295 #[turbo_tasks::function]
2296 pub fn lines(&self) -> Vc<FileLinesContent> {
2297 self.lines_ref().cell()
2298 }
2299
2300 #[turbo_tasks::function]
2301 pub async fn hash(self: Vc<Self>) -> Result<Vc<u64>> {
2302 Ok(Vc::cell(hash_xxh3_hash64(&self.await?)))
2303 }
2304}
2305
2306#[turbo_tasks::value(shared, serialization = "none")]
2308pub enum FileJsonContent {
2309 Content(Value),
2310 Unparsable(Box<UnparsableJson>),
2311 NotFound,
2312}
2313
2314#[turbo_tasks::value_impl]
2315impl ValueToString for FileJsonContent {
2316 #[turbo_tasks::function]
2321 fn to_string(&self) -> Result<Vc<RcStr>> {
2322 match self {
2323 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2324 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2325 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2326 }
2327 }
2328}
2329
2330#[turbo_tasks::value_impl]
2331impl FileJsonContent {
2332 #[turbo_tasks::function]
2333 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2334 match &*self.await? {
2335 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2336 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2337 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2338 }
2339 }
2340}
2341impl FileJsonContent {
2342 pub fn unparsable(message: RcStr) -> Self {
2343 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2344 message,
2345 path: None,
2346 start_location: None,
2347 end_location: None,
2348 }))
2349 }
2350
2351 pub fn unparsable_with_message(message: RcStr) -> Self {
2352 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2353 message,
2354 path: None,
2355 start_location: None,
2356 end_location: None,
2357 }))
2358 }
2359}
2360
2361#[derive(Debug, PartialEq, Eq)]
2362pub struct FileLine {
2363 pub content: String,
2364 pub bytes_offset: u32,
2365}
2366
2367impl FileLine {
2368 pub fn len(&self) -> usize {
2369 self.content.len()
2370 }
2371
2372 #[must_use]
2373 pub fn is_empty(&self) -> bool {
2374 self.len() == 0
2375 }
2376}
2377
2378#[turbo_tasks::value(shared, serialization = "none")]
2379pub enum FileLinesContent {
2380 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2381 Unparsable,
2382 NotFound,
2383}
2384
2385#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2386pub enum RawDirectoryEntry {
2387 File,
2388 Directory,
2389 Symlink,
2390 Other,
2392}
2393
2394#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2395pub enum DirectoryEntry {
2396 File(FileSystemPath),
2397 Directory(FileSystemPath),
2398 Symlink(FileSystemPath),
2399 Other(FileSystemPath),
2400 Error(RcStr),
2401}
2402
2403impl DirectoryEntry {
2404 pub async fn resolve_symlink(self) -> Result<Self> {
2408 if let DirectoryEntry::Symlink(symlink) = &self {
2409 let result = &*symlink.realpath_with_links().await?;
2410 let real_path = match &result.path_result {
2411 Ok(path) => path,
2412 Err(error) => {
2413 return Ok(DirectoryEntry::Error(
2414 error.as_error_message(symlink, result).into(),
2415 ));
2416 }
2417 };
2418 Ok(match *real_path.get_type().await? {
2419 FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2420 FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2421 FileSystemEntryType::NotFound => DirectoryEntry::Error(
2423 format!("Symlink {symlink} points at {real_path} which does not exist").into(),
2424 ),
2425 FileSystemEntryType::Symlink => bail!(
2427 "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2428 ),
2429 _ => self,
2430 })
2431 } else {
2432 Ok(self)
2433 }
2434 }
2435
2436 pub fn path(self) -> Option<FileSystemPath> {
2437 match self {
2438 DirectoryEntry::File(path)
2439 | DirectoryEntry::Directory(path)
2440 | DirectoryEntry::Symlink(path)
2441 | DirectoryEntry::Other(path) => Some(path),
2442 DirectoryEntry::Error(_) => None,
2443 }
2444 }
2445}
2446
2447#[turbo_tasks::value]
2448#[derive(Hash, Clone, Copy, Debug)]
2449pub enum FileSystemEntryType {
2450 NotFound,
2451 File,
2452 Directory,
2453 Symlink,
2454 Other,
2456 Error,
2457}
2458
2459impl From<FileType> for FileSystemEntryType {
2460 fn from(file_type: FileType) -> Self {
2461 match file_type {
2462 t if t.is_dir() => FileSystemEntryType::Directory,
2463 t if t.is_file() => FileSystemEntryType::File,
2464 t if t.is_symlink() => FileSystemEntryType::Symlink,
2465 _ => FileSystemEntryType::Other,
2466 }
2467 }
2468}
2469
2470impl From<DirectoryEntry> for FileSystemEntryType {
2471 fn from(entry: DirectoryEntry) -> Self {
2472 FileSystemEntryType::from(&entry)
2473 }
2474}
2475
2476impl From<&DirectoryEntry> for FileSystemEntryType {
2477 fn from(entry: &DirectoryEntry) -> Self {
2478 match entry {
2479 DirectoryEntry::File(_) => FileSystemEntryType::File,
2480 DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2481 DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2482 DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2483 DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2484 }
2485 }
2486}
2487
2488impl From<RawDirectoryEntry> for FileSystemEntryType {
2489 fn from(entry: RawDirectoryEntry) -> Self {
2490 FileSystemEntryType::from(&entry)
2491 }
2492}
2493
2494impl From<&RawDirectoryEntry> for FileSystemEntryType {
2495 fn from(entry: &RawDirectoryEntry) -> Self {
2496 match entry {
2497 RawDirectoryEntry::File => FileSystemEntryType::File,
2498 RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2499 RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2500 RawDirectoryEntry::Other => FileSystemEntryType::Other,
2501 }
2502 }
2503}
2504
2505#[turbo_tasks::value]
2506#[derive(Debug)]
2507pub enum RawDirectoryContent {
2508 Entries(AutoMap<RcStr, RawDirectoryEntry>),
2511 NotFound,
2512}
2513
2514impl RawDirectoryContent {
2515 pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2516 Self::cell(RawDirectoryContent::Entries(entries))
2517 }
2518
2519 pub fn not_found() -> Vc<Self> {
2520 Self::cell(RawDirectoryContent::NotFound)
2521 }
2522}
2523
2524#[turbo_tasks::value]
2525#[derive(Debug)]
2526pub enum DirectoryContent {
2527 Entries(AutoMap<RcStr, DirectoryEntry>),
2528 NotFound,
2529}
2530
2531impl DirectoryContent {
2532 pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2533 Self::cell(DirectoryContent::Entries(entries))
2534 }
2535
2536 pub fn not_found() -> Vc<Self> {
2537 Self::cell(DirectoryContent::NotFound)
2538 }
2539}
2540
2541#[turbo_tasks::value(shared)]
2542pub struct NullFileSystem;
2543
2544#[turbo_tasks::value_impl]
2545impl FileSystem for NullFileSystem {
2546 #[turbo_tasks::function]
2547 fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2548 FileContent::NotFound.cell()
2549 }
2550
2551 #[turbo_tasks::function]
2552 fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2553 LinkContent::NotFound.cell()
2554 }
2555
2556 #[turbo_tasks::function]
2557 fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2558 RawDirectoryContent::not_found()
2559 }
2560
2561 #[turbo_tasks::function]
2562 fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) -> Vc<()> {
2563 Vc::default()
2564 }
2565
2566 #[turbo_tasks::function]
2567 fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) -> Vc<()> {
2568 Vc::default()
2569 }
2570
2571 #[turbo_tasks::function]
2572 fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2573 FileMeta::default().cell()
2574 }
2575}
2576
2577#[turbo_tasks::value_impl]
2578impl ValueToString for NullFileSystem {
2579 #[turbo_tasks::function]
2580 fn to_string(&self) -> Vc<RcStr> {
2581 Vc::cell(rcstr!("null"))
2582 }
2583}
2584
2585pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2586 loop {
2587 if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2588 path = fs.get_inner_fs_path(path).owned().await?;
2589 continue;
2590 }
2591
2592 if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2593 let sys_path = fs.await?.to_sys_path(&path);
2594 return Ok(Some(sys_path));
2595 }
2596
2597 return Ok(None);
2598 }
2599}
2600
2601#[turbo_tasks::function]
2602async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2603 let fs = path.fs().to_resolved().await?;
2604 match &*fs.raw_read_dir(path.clone()).await? {
2605 RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2606 RawDirectoryContent::Entries(entries) => {
2607 let mut normalized_entries = AutoMap::new();
2608 let dir_path = &path.path;
2609 for (name, entry) in entries {
2610 let path = if dir_path.is_empty() {
2614 name.clone()
2615 } else {
2616 RcStr::from(format!("{dir_path}/{name}"))
2617 };
2618
2619 let entry_path = FileSystemPath::new_normalized(fs, path);
2620 let entry = match entry {
2621 RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2622 RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2623 RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2624 RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2625 };
2626 normalized_entries.insert(name.clone(), entry);
2627 }
2628 Ok(DirectoryContent::new(normalized_entries))
2629 }
2630 }
2631}
2632
2633#[turbo_tasks::function]
2634async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2635 if path.is_root() {
2636 return Ok(FileSystemEntryType::Directory.cell());
2637 }
2638 let parent = path.parent();
2639 let dir_content = parent.raw_read_dir().await?;
2640 match &*dir_content {
2641 RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2642 RawDirectoryContent::Entries(entries) => {
2643 let (_, file_name) = path.split_file_name();
2644 if let Some(entry) = entries.get(file_name) {
2645 Ok(FileSystemEntryType::from(entry).cell())
2646 } else {
2647 Ok(FileSystemEntryType::NotFound.cell())
2648 }
2649 }
2650 }
2651}
2652
2653#[turbo_tasks::function]
2654async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2655 let mut current_path = path;
2656 let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2657 let mut visited: AutoSet<RcStr> = AutoSet::new();
2658 let mut error = RealPathResultError::TooManySymlinks;
2659 for _i in 0..40 {
2662 if current_path.is_root() {
2663 return Ok(RealPathResult {
2665 path_result: Ok(current_path),
2666 symlinks: symlinks.into_iter().collect(),
2667 }
2668 .cell());
2669 }
2670
2671 if !visited.insert(current_path.path.clone()) {
2672 error = RealPathResultError::CycleDetected;
2673 break; }
2675
2676 let parent = current_path.parent();
2678 let parent_result = parent.realpath_with_links().owned().await?;
2679 let basename = current_path
2680 .path
2681 .rsplit_once('/')
2682 .map_or(current_path.path.as_str(), |(_, name)| name);
2683 symlinks.extend(parent_result.symlinks);
2684 let parent_path = match parent_result.path_result {
2685 Ok(path) => {
2686 if path != parent {
2687 current_path = path.join(basename)?;
2688 }
2689 path
2690 }
2691 Err(parent_error) => {
2692 error = parent_error;
2693 break;
2694 }
2695 };
2696
2697 if !matches!(
2700 *current_path.get_type().await?,
2701 FileSystemEntryType::Symlink
2702 ) {
2703 return Ok(RealPathResult {
2704 path_result: Ok(current_path),
2705 symlinks: symlinks.into_iter().collect(), }
2707 .cell());
2708 }
2709
2710 match &*current_path.read_link().await? {
2711 LinkContent::Link { target, link_type } => {
2712 symlinks.insert(current_path.clone());
2713 current_path = if link_type.contains(LinkType::ABSOLUTE) {
2714 current_path.root().owned().await?
2715 } else {
2716 parent_path
2717 }
2718 .join(target)?;
2719 }
2720 LinkContent::NotFound => {
2721 error = RealPathResultError::NotFound;
2722 break;
2723 }
2724 LinkContent::Invalid => {
2725 error = RealPathResultError::Invalid;
2726 break;
2727 }
2728 }
2729 }
2730
2731 Ok(RealPathResult {
2739 path_result: Err(error),
2740 symlinks: symlinks.into_iter().collect(),
2741 }
2742 .cell())
2743}
2744
2745#[cfg(test)]
2746mod tests {
2747 use turbo_rcstr::rcstr;
2748 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2749
2750 use super::*;
2751
2752 #[test]
2753 fn test_get_relative_path_to() {
2754 assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2755 assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2756 assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2757 assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2758 assert_eq!(
2759 get_relative_path_to("a/b/c", "c/b/a").as_str(),
2760 "../../../c/b/a"
2761 );
2762 assert_eq!(
2763 get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2764 "../../../c/b/a"
2765 );
2766 }
2767
2768 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2769 async fn with_extension() {
2770 turbo_tasks_testing::VcStorage::with(async {
2771 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2772 .to_resolved()
2773 .await?;
2774
2775 let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2776
2777 let path_json = path_txt.with_extension("json");
2778 assert_eq!(&*path_json.path, "foo/bar.json");
2779
2780 let path_no_ext = path_txt.with_extension("");
2781 assert_eq!(&*path_no_ext.path, "foo/bar");
2782
2783 let path_new_ext = path_no_ext.with_extension("json");
2784 assert_eq!(&*path_new_ext.path, "foo/bar.json");
2785
2786 let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2787
2788 let path_no_slash_json = path_no_slash_txt.with_extension("json");
2789 assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2790
2791 let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2792 assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2793
2794 let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2795 assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2796
2797 anyhow::Ok(())
2798 })
2799 .await
2800 .unwrap()
2801 }
2802
2803 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2804 async fn file_stem() {
2805 turbo_tasks_testing::VcStorage::with(async {
2806 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2807 .to_resolved()
2808 .await?;
2809
2810 let path = FileSystemPath::new_normalized(fs, rcstr!(""));
2811 assert_eq!(path.file_stem(), None);
2812
2813 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2814 assert_eq!(path.file_stem(), Some("bar"));
2815
2816 let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2817 assert_eq!(path.file_stem(), Some("bar"));
2818
2819 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
2820 assert_eq!(path.file_stem(), Some("bar"));
2821
2822 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
2823 assert_eq!(path.file_stem(), Some(".bar"));
2824
2825 anyhow::Ok(())
2826 })
2827 .await
2828 .unwrap()
2829 }
2830
2831 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2832 async fn test_try_from_sys_path() {
2833 let sys_root = if cfg!(windows) {
2834 Path::new(r"C:\fake\root")
2835 } else {
2836 Path::new(r"/fake/root")
2837 };
2838
2839 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2840 BackendOptions::default(),
2841 noop_backing_storage(),
2842 ));
2843 tt.run_once(async {
2844 let fs_vc =
2845 DiskFileSystem::new(rcstr!("temp"), RcStr::from(sys_root.to_str().unwrap()))
2846 .to_resolved()
2847 .await?;
2848 let fs = fs_vc.await?;
2849 let fs_root_path = fs_vc.root().await?;
2850
2851 assert_eq!(
2852 fs.try_from_sys_path(
2853 fs_vc,
2854 &Path::new("relative").join("directory"),
2855 None,
2856 )
2857 .unwrap()
2858 .path,
2859 "relative/directory"
2860 );
2861
2862 assert_eq!(
2863 fs.try_from_sys_path(
2864 fs_vc,
2865 &sys_root
2866 .join("absolute")
2867 .join("directory")
2868 .join("..")
2869 .join("normalized_path"),
2870 Some(&fs_root_path.join("ignored").unwrap()),
2871 )
2872 .unwrap()
2873 .path,
2874 "absolute/normalized_path"
2875 );
2876
2877 assert_eq!(
2878 fs.try_from_sys_path(
2879 fs_vc,
2880 Path::new("child"),
2881 Some(&fs_root_path.join("parent").unwrap()),
2882 )
2883 .unwrap()
2884 .path,
2885 "parent/child"
2886 );
2887
2888 assert_eq!(
2889 fs.try_from_sys_path(
2890 fs_vc,
2891 &Path::new("..").join("parallel_dir"),
2892 Some(&fs_root_path.join("parent").unwrap()),
2893 )
2894 .unwrap()
2895 .path,
2896 "parallel_dir"
2897 );
2898
2899 assert_eq!(
2900 fs.try_from_sys_path(
2901 fs_vc,
2902 &Path::new("relative")
2903 .join("..")
2904 .join("..")
2905 .join("leaves_root"),
2906 None,
2907 ),
2908 None
2909 );
2910
2911 assert_eq!(
2912 fs.try_from_sys_path(
2913 fs_vc,
2914 &sys_root
2915 .join("absolute")
2916 .join("..")
2917 .join("..")
2918 .join("leaves_root"),
2919 None,
2920 ),
2921 None
2922 );
2923
2924 anyhow::Ok(())
2925 })
2926 .await
2927 .unwrap();
2928 }
2929
2930 #[cfg(test)]
2931 mod symlink_tests {
2932 use std::{
2933 fs::{File, create_dir_all, read_to_string},
2934 io::Write,
2935 };
2936
2937 use rand::{Rng, SeedableRng};
2938 use turbo_rcstr::{RcStr, rcstr};
2939 use turbo_tasks::{ResolvedVc, Vc, apply_effects};
2940 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2941
2942 use crate::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
2943
2944 #[turbo_tasks::function(operation)]
2945 async fn test_write_link_effect(
2946 fs: ResolvedVc<DiskFileSystem>,
2947 path: FileSystemPath,
2948 target: RcStr,
2949 ) -> anyhow::Result<()> {
2950 let write_file = |f| {
2951 fs.write_link(
2952 f,
2953 LinkContent::Link {
2954 target: format!("{target}/data.txt").into(),
2955 link_type: LinkType::empty(),
2956 }
2957 .cell(),
2958 )
2959 };
2960 write_file(path.join("symlink-file")?).await?;
2962 write_file(path.join("symlink-file")?).await?;
2963
2964 let write_dir = |f| {
2965 fs.write_link(
2966 f,
2967 LinkContent::Link {
2968 target: target.clone(),
2969 link_type: LinkType::DIRECTORY,
2970 }
2971 .cell(),
2972 )
2973 };
2974 write_dir(path.join("symlink-dir")?).await?;
2976 write_dir(path.join("symlink-dir")?).await?;
2977
2978 Ok(())
2979 }
2980
2981 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2982 async fn test_write_link() {
2983 let scratch = tempfile::tempdir().unwrap();
2984 let path = scratch.path().to_owned();
2985
2986 create_dir_all(path.join("subdir-a")).unwrap();
2987 File::create_new(path.join("subdir-a/data.txt"))
2988 .unwrap()
2989 .write_all(b"foo")
2990 .unwrap();
2991 create_dir_all(path.join("subdir-b")).unwrap();
2992 File::create_new(path.join("subdir-b/data.txt"))
2993 .unwrap()
2994 .write_all(b"bar")
2995 .unwrap();
2996 let root = path.to_str().unwrap().into();
2997
2998 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2999 BackendOptions::default(),
3000 noop_backing_storage(),
3001 ));
3002
3003 tt.run_once(async move {
3004 let fs = DiskFileSystem::new(rcstr!("test"), root)
3005 .to_resolved()
3006 .await?;
3007 let root_path = fs.root().owned().await?;
3008
3009 let write_result =
3010 test_write_link_effect(fs, root_path.clone(), rcstr!("subdir-a"));
3011 write_result.read_strongly_consistent().await?;
3012 apply_effects(write_result).await?;
3013
3014 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
3015 assert_eq!(
3016 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3017 "foo"
3018 );
3019
3020 let write_result = test_write_link_effect(fs, root_path, rcstr!("subdir-b"));
3022 write_result.read_strongly_consistent().await?;
3023 apply_effects(write_result).await?;
3024
3025 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
3026 assert_eq!(
3027 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3028 "bar"
3029 );
3030
3031 anyhow::Ok(())
3032 })
3033 .await
3034 .unwrap();
3035 }
3036
3037 const STRESS_ITERATIONS: usize = 100;
3038 const STRESS_PARALLELISM: usize = 8;
3039 const STRESS_TARGET_COUNT: usize = 20;
3040 const STRESS_SYMLINK_COUNT: usize = 16;
3041
3042 #[turbo_tasks::function(operation)]
3043 fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
3044 DiskFileSystem::new(rcstr!("test"), fs_root)
3045 }
3046
3047 #[turbo_tasks::function(operation)]
3048 fn disk_file_system_root_operation(fs: ResolvedVc<DiskFileSystem>) -> Vc<FileSystemPath> {
3049 fs.root()
3050 }
3051
3052 #[turbo_tasks::function(operation)]
3053 async fn write_symlink_stress_batch(
3054 fs: ResolvedVc<DiskFileSystem>,
3055 symlinks_dir: FileSystemPath,
3056 updates: Vec<(usize, usize)>,
3057 ) -> anyhow::Result<()> {
3058 use turbo_tasks::TryJoinIterExt;
3059
3060 updates
3061 .into_iter()
3062 .map(|(symlink_idx, target_idx)| {
3063 let target = RcStr::from(format!("../_targets/{target_idx}"));
3064 let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
3065 async move {
3066 fs.write_link(
3067 symlink_path,
3068 LinkContent::Link {
3069 target,
3070 link_type: LinkType::DIRECTORY,
3071 }
3072 .cell(),
3073 )
3074 .await
3075 }
3076 })
3077 .try_join()
3078 .await?;
3079 Ok(())
3080 }
3081
3082 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3083 async fn test_symlink_stress() {
3084 let scratch = tempfile::tempdir().unwrap();
3085 let path = scratch.path().to_owned();
3086
3087 let targets_dir = path.join("_targets");
3088 create_dir_all(&targets_dir).unwrap();
3089 for i in 0..STRESS_TARGET_COUNT {
3090 create_dir_all(targets_dir.join(i.to_string())).unwrap();
3091 }
3092 create_dir_all(path.join("_symlinks")).unwrap();
3093
3094 let root = RcStr::from(path.to_str().unwrap());
3095
3096 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3097 BackendOptions::default(),
3098 noop_backing_storage(),
3099 ));
3100
3101 tt.run_once(async move {
3102 let fs = disk_file_system_operation(root)
3103 .resolve_strongly_consistent()
3104 .await?;
3105 let root_path = disk_file_system_root_operation(fs)
3106 .resolve_strongly_consistent()
3107 .await?
3108 .owned()
3109 .await?;
3110 let symlinks_dir = root_path.join("_symlinks")?;
3111
3112 let initial_updates: Vec<(usize, usize)> =
3113 (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
3114 let initial_op =
3115 write_symlink_stress_batch(fs, symlinks_dir.clone(), initial_updates);
3116 initial_op.read_strongly_consistent().await?;
3117 apply_effects(initial_op).await?;
3118
3119 let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
3120 for _ in 0..STRESS_ITERATIONS {
3121 let updates: Vec<(usize, usize)> = (0..STRESS_PARALLELISM)
3122 .map(|_| {
3123 let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
3124 let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
3125 (symlink_idx, target_idx)
3126 })
3127 .collect();
3128
3129 let write_op = write_symlink_stress_batch(fs, symlinks_dir.clone(), updates);
3130 write_op.read_strongly_consistent().await?;
3131 apply_effects(write_op).await?;
3132 }
3133
3134 anyhow::Ok(())
3135 })
3136 .await
3137 .unwrap();
3138
3139 tt.stop_and_wait().await;
3140 }
3141 }
3142
3143 #[cfg(test)]
3145 mod denied_path_tests {
3146 use std::{
3147 fs::{File, create_dir_all},
3148 io::Write,
3149 };
3150
3151 use turbo_rcstr::{RcStr, rcstr};
3152 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3153
3154 use crate::{
3155 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
3156 FileSystemPath,
3157 glob::{Glob, GlobOptions},
3158 };
3159
3160 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
3163 let scratch = tempfile::tempdir().unwrap();
3164 let path = scratch.path();
3165
3166 File::create_new(path.join("allowed_file.txt"))
3173 .unwrap()
3174 .write_all(b"allowed content")
3175 .unwrap();
3176
3177 create_dir_all(path.join("allowed_dir")).unwrap();
3178 File::create_new(path.join("allowed_dir/file.txt"))
3179 .unwrap()
3180 .write_all(b"allowed dir content")
3181 .unwrap();
3182
3183 File::create_new(path.join("other_file.txt"))
3184 .unwrap()
3185 .write_all(b"other content")
3186 .unwrap();
3187
3188 create_dir_all(path.join("denied_dir/nested")).unwrap();
3189 File::create_new(path.join("denied_dir/secret.txt"))
3190 .unwrap()
3191 .write_all(b"secret content")
3192 .unwrap();
3193 File::create_new(path.join("denied_dir/nested/deep.txt"))
3194 .unwrap()
3195 .write_all(b"deep secret")
3196 .unwrap();
3197
3198 let root = RcStr::from(path.to_str().unwrap());
3199 let denied_path = rcstr!("denied_dir");
3201
3202 (scratch, root, denied_path)
3203 }
3204
3205 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3206 async fn test_denied_path_read() {
3207 let (_scratch, root, denied_path) = setup_test_fs();
3208 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3209 BackendOptions::default(),
3210 noop_backing_storage(),
3211 ));
3212
3213 tt.run_once(async {
3214 let fs =
3215 DiskFileSystem::new_with_denied_paths(rcstr!("test"), root, vec![denied_path]);
3216 let root_path = fs.root().await?;
3217
3218 let allowed_file = root_path.join("allowed_file.txt")?;
3220 let content = allowed_file.read().await?;
3221 assert!(
3222 matches!(&*content, FileContent::Content(_)),
3223 "allowed file should be readable"
3224 );
3225
3226 let denied_file = root_path.join("denied_dir/secret.txt")?;
3228 let content = denied_file.read().await?;
3229 assert!(
3230 matches!(&*content, FileContent::NotFound),
3231 "denied file should return NotFound, got {:?}",
3232 content
3233 );
3234
3235 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
3237 let content = nested_denied.read().await?;
3238 assert!(
3239 matches!(&*content, FileContent::NotFound),
3240 "nested denied file should return NotFound"
3241 );
3242
3243 let denied_dir = root_path.join("denied_dir")?;
3245 let content = denied_dir.read().await?;
3246 assert!(
3247 matches!(&*content, FileContent::NotFound),
3248 "denied directory should return NotFound"
3249 );
3250
3251 anyhow::Ok(())
3252 })
3253 .await
3254 .unwrap();
3255 }
3256
3257 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3258 async fn test_denied_path_read_dir() {
3259 let (_scratch, root, denied_path) = setup_test_fs();
3260 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3261 BackendOptions::default(),
3262 noop_backing_storage(),
3263 ));
3264
3265 tt.run_once(async {
3266 let fs =
3267 DiskFileSystem::new_with_denied_paths(rcstr!("test"), root, vec![denied_path]);
3268 let root_path = fs.root().await?;
3269
3270 let dir_content = root_path.read_dir().await?;
3272 match &*dir_content {
3273 DirectoryContent::Entries(entries) => {
3274 assert!(
3275 entries.contains_key(&rcstr!("allowed_dir")),
3276 "allowed_dir should be visible"
3277 );
3278 assert!(
3279 entries.contains_key(&rcstr!("other_file.txt")),
3280 "other_file.txt should be visible"
3281 );
3282 assert!(
3283 entries.contains_key(&rcstr!("allowed_file.txt")),
3284 "allowed_file.txt should be visible"
3285 );
3286 assert!(
3287 !entries.contains_key(&rcstr!("denied_dir")),
3288 "denied_dir should NOT be visible in read_dir"
3289 );
3290 }
3291 DirectoryContent::NotFound => panic!("root directory should exist"),
3292 }
3293
3294 let denied_dir = root_path.join("denied_dir")?;
3296 let dir_content = denied_dir.read_dir().await?;
3297 assert!(
3298 matches!(&*dir_content, DirectoryContent::NotFound),
3299 "denied_dir read_dir should return NotFound"
3300 );
3301
3302 anyhow::Ok(())
3303 })
3304 .await
3305 .unwrap();
3306 }
3307
3308 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3309 async fn test_denied_path_read_glob() {
3310 let (_scratch, root, denied_path) = setup_test_fs();
3311 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3312 BackendOptions::default(),
3313 noop_backing_storage(),
3314 ));
3315
3316 tt.run_once(async {
3317 let fs =
3318 DiskFileSystem::new_with_denied_paths(rcstr!("test"), root, vec![denied_path]);
3319 let root_path = fs.root().await?;
3320
3321 let glob_result = root_path
3323 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3324 .await?;
3325
3326 assert!(
3328 glob_result.results.contains_key("allowed_file.txt"),
3329 "allowed_file.txt should be found"
3330 );
3331 assert!(
3332 glob_result.results.contains_key("other_file.txt"),
3333 "other_file.txt should be found"
3334 );
3335 assert!(
3336 !glob_result.results.contains_key("denied_dir"),
3337 "denied_dir should NOT appear in glob results"
3338 );
3339
3340 assert!(
3342 !glob_result.inner.contains_key("denied_dir"),
3343 "denied_dir should NOT appear in glob inner results"
3344 );
3345
3346 assert!(
3348 glob_result.inner.contains_key("allowed_dir"),
3349 "allowed_dir directory should be present"
3350 );
3351 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3352 assert!(
3353 sub_inner.results.contains_key("file.txt"),
3354 "allowed_dir/file.txt should be found"
3355 );
3356
3357 anyhow::Ok(())
3358 })
3359 .await
3360 .unwrap();
3361 }
3362
3363 #[turbo_tasks::function(operation)]
3364 async fn write_file(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
3365 path.write(
3366 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3367 .cell(),
3368 )
3369 .await?;
3370 Ok(())
3371 }
3372
3373 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3374 async fn test_denied_path_write() {
3375 use turbo_tasks::apply_effects;
3376
3377 let (_scratch, root, denied_path) = setup_test_fs();
3378 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3379 BackendOptions::default(),
3380 noop_backing_storage(),
3381 ));
3382
3383 tt.run_once(async {
3384 let fs =
3385 DiskFileSystem::new_with_denied_paths(rcstr!("test"), root, vec![denied_path]);
3386 let root_path = fs.root().await?;
3387
3388 let allowed_file = root_path.join("allowed_dir/new_file.txt")?;
3390 let write_result = write_file(allowed_file.clone(), rcstr!("test content"));
3391 write_result.read_strongly_consistent().await?;
3392 apply_effects(write_result).await?;
3393
3394 let read_content = allowed_file.read().await?;
3396 assert!(
3397 matches!(&*read_content, FileContent::Content(_)),
3398 "allowed file write should succeed"
3399 );
3400
3401 let denied_file = root_path.join("denied_dir/forbidden.txt")?;
3403 let write_result = write_file(denied_file, rcstr!("forbidden"));
3404 let result = write_result.read_strongly_consistent().await;
3405 assert!(
3406 result.is_err(),
3407 "writing to denied path should return an error"
3408 );
3409
3410 let nested_denied = root_path.join("denied_dir/nested/file.txt")?;
3412 let write_result = write_file(nested_denied, rcstr!("nested"));
3413 let result = write_result.read_strongly_consistent().await;
3414 assert!(
3415 result.is_err(),
3416 "writing to nested denied path should return an error"
3417 );
3418
3419 anyhow::Ok(())
3420 })
3421 .await
3422 .unwrap();
3423 }
3424 }
3425}