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