Skip to main content

turbo_tasks_fs/
lib.rs

1#![feature(arbitrary_self_types)]
2#![feature(arbitrary_self_types_pointers)]
3#![feature(btree_cursors)] // needed for the `InvalidatorMap` and watcher, reduces time complexity
4#![feature(io_error_more)]
5#![feature(min_specialization)]
6// if `normalize_lexically` isn't eventually stabilized, we can copy the implementation from the
7// stdlib into our source tree
8#![feature(normalize_lexically)]
9#![feature(trivial_bounds)]
10// Junction points are used on Windows. We could use a third-party crate for this if the junction
11// API isn't eventually stabilized.
12#![cfg_attr(windows, feature(junction_point))]
13#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
14#![allow(clippy::mutable_key_type)]
15
16pub mod attach;
17pub mod embed;
18pub mod glob;
19mod globset;
20pub mod invalidation;
21mod invalidator_map;
22pub mod json;
23mod mutex_map;
24mod path_map;
25mod read_glob;
26mod retry;
27pub mod rope;
28pub mod source_context;
29pub mod util;
30pub(crate) mod virtual_fs;
31mod watcher;
32
33use std::{
34    borrow::Cow,
35    cmp::{Ordering, min},
36    env,
37    error::Error as StdError,
38    fmt::{self, Debug, Formatter},
39    fs::FileType,
40    future::Future,
41    io::{self, BufRead, BufReader, ErrorKind, Read, Write as _},
42    mem::take,
43    path::{MAIN_SEPARATOR, Path, PathBuf},
44    sync::{Arc, LazyLock, Weak},
45    time::Duration,
46};
47
48use anyhow::{Context, Result, anyhow, bail};
49use auto_hash_map::{AutoMap, AutoSet};
50use bincode::{Decode, Encode};
51use bitflags::bitflags;
52use dunce::simplified;
53use indexmap::IndexSet;
54use jsonc_parser::{ParseOptions, parse_to_serde_value};
55use mime::Mime;
56use rustc_hash::FxHashSet;
57use serde_json::Value;
58use tokio::{
59    runtime::Handle,
60    sync::{RwLock, RwLockReadGuard},
61};
62use tracing::Instrument;
63use turbo_rcstr::{RcStr, rcstr};
64use turbo_tasks::{
65    Completion, Effect, EffectStateStorage, InvalidationReason, NonLocalValue, ReadRef, ResolvedVc,
66    TaskInput, TurboTasksApi, ValueToString, ValueToStringRef, Vc, debug::ValueDebugFormat,
67    emit_effect, mark_session_dependent, parallel, trace::TraceRawVcs, turbo_tasks_weak, turbobail,
68    turbofmt,
69};
70use turbo_tasks_hash::{
71    DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64,
72    hash_xxh3_hash128,
73};
74use turbo_unix_path::{
75    get_parent_path, get_relative_path_to, join_path, normalize_path, sys_to_unix, unix_to_sys,
76};
77
78use crate::{
79    attach::AttachedFileSystem,
80    glob::Glob,
81    invalidation::Write,
82    invalidator_map::InvalidatorMap,
83    json::UnparsableJson,
84    mutex_map::MutexMap,
85    path_map::OrderedPathMapExt,
86    read_glob::{read_glob, track_glob},
87    retry::{can_retry, retry_blocking, retry_blocking_custom},
88    rope::{Rope, RopeReader},
89    util::extract_disk_access,
90    watcher::DiskWatcher,
91};
92pub use crate::{read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem};
93
94/// Validate the path, returning the valid path, a modified-but-now-valid path, or bailing with an
95/// error.
96///
97/// The behaviour of the file system changes depending on the OS, and indeed sometimes the FS
98/// implementation of the OS itself.
99///
100/// - On Windows the limit for normal file paths is 260 characters, a holdover from the DOS days,
101///   but Rust will opportunistically rewrite paths to 'UNC' paths for supported path operations
102///   which can be up to 32767 characters long.
103/// - On macOS, the limit is traditionally 255 characters for the file name and a second limit of
104///   1024 for the entire path (verified by running `getconf PATH_MAX /`).
105/// - On Linux, the limit differs between kernel (and by extension, distro) and filesystem. On most
106///   common file systems (e.g. ext4, btrfs, and xfs), individual file names can be up to 255 bytes
107///   with no hard limit on total path length. [Some legacy POSIX APIs are restricted to the
108///   `PATH_MAX` value of 4096 bytes in `limits.h`, but most applications support longer
109///   paths][PATH_MAX].
110///
111/// For more details, refer to <https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits>.
112///
113/// Realistically, the output path lengths will be the same across all platforms, so we need to set
114/// a conservative limit and be particular about when we decide to bump it. Here we have opted for
115/// 255 characters, because it is the shortest of the three options.
116///
117/// [PATH_MAX]: https://eklitzke.org/path-max-is-tricky
118pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
119    /// Here we check if the path is too long for windows, and if so, attempt to canonicalize it
120    /// to a UNC path.
121    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            /// here we are only going to check if the total length exceeds, or the last segment
140            /// exceeds. This heuristic is primarily to avoid long file names, and it makes the
141            /// operation much cheaper.
142            const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
143            // macOS reports a limit of 1024, but I (@arlyon) have had issues with paths above 1016
144            // so we subtract a bit to be safe. on most linux distros this is likely a lot larger
145            // than 1024, but macOS is *special*
146            const MAX_PATH_LENGTH: usize = 1024 - 8;
147
148            // check the last segment (file name)
149            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    // the semaphore isn't serialized, and we assume the environment variable doesn't change during
202    // runtime, so it's okay to access it in this untracked way.
203    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    // the semaphore isn't serialized, and we assume the environment variable doesn't change during
210    // runtime, so it's okay to access it in this untracked way.
211    static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
212        number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
213            // We write a lot of smallish files where high concurrency will cause metadata
214            // thrashing. So 4 threads is a safe cross platform suitable value.
215            4,
216        )
217    });
218    tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
219}
220
221#[turbo_tasks::value_trait]
222pub trait FileSystem: ValueToString {
223    /// Returns the path to the root of the file system.
224    #[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    /// See [`FileSystemPath::write_symbolic_link_dir`].
237    #[turbo_tasks::function]
238    fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
239    #[turbo_tasks::function]
240    fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
241}
242
243#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
244struct DiskFileSystemInner {
245    pub name: RcStr,
246    pub root: RcStr,
247    #[turbo_tasks(debug_ignore, trace_ignore)]
248    #[bincode(skip)]
249    mutex_map: MutexMap<PathBuf>,
250    #[turbo_tasks(debug_ignore, trace_ignore)]
251    #[bincode(skip)]
252    invalidator_map: InvalidatorMap,
253    #[turbo_tasks(debug_ignore, trace_ignore)]
254    #[bincode(skip)]
255    dir_invalidator_map: InvalidatorMap,
256    /// Lock that makes invalidation atomic. It will keep a write lock during
257    /// watcher invalidation and a read lock during other operations.
258    #[turbo_tasks(debug_ignore, trace_ignore)]
259    #[bincode(skip)]
260    invalidation_lock: RwLock<()>,
261    /// Semaphore to limit the maximum number of concurrent file operations.
262    #[turbo_tasks(debug_ignore, trace_ignore)]
263    #[bincode(skip, default = "create_read_semaphore")]
264    read_semaphore: tokio::sync::Semaphore,
265    /// Semaphore to limit the maximum number of concurrent file operations.
266    #[turbo_tasks(debug_ignore, trace_ignore)]
267    #[bincode(skip, default = "create_write_semaphore")]
268    write_semaphore: tokio::sync::Semaphore,
269
270    #[turbo_tasks(debug_ignore, trace_ignore)]
271    watcher: DiskWatcher,
272    /// Root paths that we do not allow access to from this filesystem.
273    /// Useful for things like output directories to prevent accidental ouroboros situations.
274    denied_paths: Vec<RcStr>,
275    /// Used by invalidators when called from a non-turbo-tasks thread, specifically in the fs
276    /// watcher.
277    #[turbo_tasks(debug_ignore, trace_ignore)]
278    #[bincode(skip, default = "turbo_tasks_weak")]
279    turbo_tasks: Weak<dyn TurboTasksApi>,
280    /// Used by invalidators when called from a non-tokio thread, specifically in the fs watcher.
281    #[turbo_tasks(debug_ignore, trace_ignore)]
282    #[bincode(skip, default = "Handle::current")]
283    tokio_handle: Handle,
284    #[turbo_tasks(debug_ignore, trace_ignore)]
285    #[bincode(skip)]
286    effect_state_storage: EffectStateStorage,
287}
288
289impl DiskFileSystemInner {
290    /// Returns the root as Path
291    fn root_path(&self) -> &Path {
292        // just in case there's a windows unc path prefix we remove it with `dunce`
293        simplified(Path::new(&*self.root))
294    }
295
296    /// Checks if a path is within the denied path
297    /// Returns true if the path should be treated as non-existent
298    ///
299    /// Since denied_paths are guaranteed to be:
300    /// - normalized (no ../ traversals)
301    /// - using unix separators (/)
302    /// - relative to the fs root
303    ///
304    /// We can efficiently check using string operations
305    fn is_path_denied(&self, path: &FileSystemPath) -> bool {
306        let path = &path.path;
307        self.denied_paths.iter().any(|denied_path| {
308            path.starts_with(denied_path.as_str())
309                && (path.len() == denied_path.len()
310                    || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
311        })
312    }
313
314    /// registers the path as an invalidator for the current task,
315    /// has to be called within a turbo-tasks function
316    async fn register_read_invalidator(&self, path: &Path) -> Result<()> {
317        if let Some(invalidator) = turbo_tasks::get_invalidator() {
318            self.invalidator_map.insert(path.to_owned(), invalidator);
319            self.watcher
320                .ensure_watched_file(path, self.root_path())
321                .await?;
322        }
323        Ok(())
324    }
325
326    /// After an effect writes to a path, invalidate any read tasks tracking that path so they
327    /// re-read the updated content. This is necessary because the file watcher may not be active
328    /// (e.g., in tests or build-only scenarios).
329    fn invalidate_from_write(&self, full_path: &Path) {
330        let mut invalidator_map = self.invalidator_map.lock().unwrap();
331        if let Some(invalidators) = invalidator_map.remove(full_path) {
332            let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
333                return;
334            };
335            let _guard = self.tokio_handle.enter();
336            let reason = Write {
337                path: full_path.to_string_lossy().into_owned(),
338            };
339            for invalidator in invalidators {
340                invalidator.invalidate_with_reason(&*turbo_tasks, reason.clone());
341            }
342        }
343    }
344
345    /// registers the path as an invalidator for the current task,
346    /// has to be called within a turbo-tasks function
347    async fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
348        if let Some(invalidator) = turbo_tasks::get_invalidator() {
349            self.dir_invalidator_map
350                .insert(path.to_owned(), invalidator);
351            self.watcher
352                .ensure_watched_dir(path, self.root_path())
353                .await?;
354        }
355        Ok(())
356    }
357
358    async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
359        let lock1 = self.invalidation_lock.read().await;
360        let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
361        PathLockGuard(lock1, lock2)
362    }
363
364    fn invalidate(&self) {
365        let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
366        let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
367            return;
368        };
369        let _guard = self.tokio_handle.enter();
370
371        let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
372        let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
373        let invalidators = invalidator_map
374            .into_iter()
375            .chain(dir_invalidator_map)
376            .flat_map(|(_, invalidators)| invalidators.into_iter())
377            .collect::<Vec<_>>();
378        parallel::for_each_owned(invalidators, |invalidator| {
379            invalidator.invalidate(&*turbo_tasks)
380        });
381    }
382
383    /// Invalidates every tracked file in the filesystem.
384    ///
385    /// Calls the given
386    fn invalidate_with_reason<R: InvalidationReason + Clone>(
387        &self,
388        reason: impl Fn(&Path) -> R + Sync,
389    ) {
390        let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
391        let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
392            return;
393        };
394        let _guard = self.tokio_handle.enter();
395
396        let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
397        let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
398        let invalidators = invalidator_map
399            .into_iter()
400            .chain(dir_invalidator_map)
401            .flat_map(|(path, invalidators)| {
402                let reason_for_path = reason(&path);
403                invalidators
404                    .into_iter()
405                    .map(move |i| (reason_for_path.clone(), i))
406            })
407            .collect::<Vec<_>>();
408        parallel::for_each_owned(invalidators, |(reason, invalidator)| {
409            invalidator.invalidate_with_reason(&*turbo_tasks, reason)
410        });
411    }
412
413    /// Invalidates tracked files/directories for `paths` and their children.
414    /// Also invalidates tracked directory reads for all parent directories to
415    /// account for file creations/deletions under the deferred subtree.
416    fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
417        &self,
418        paths: impl IntoIterator<Item = PathBuf>,
419        reason: impl Fn(&Path) -> R + Sync,
420    ) {
421        let _span =
422            tracing::info_span!("invalidate filesystem paths", name = &*self.root).entered();
423        let Some(turbo_tasks) = self.turbo_tasks.upgrade() else {
424            return;
425        };
426        let _guard = self.tokio_handle.enter();
427
428        let mut invalidator_map = self.invalidator_map.lock().unwrap();
429        let mut dir_invalidator_map = self.dir_invalidator_map.lock().unwrap();
430        let mut invalidators = Vec::new();
431        let mut parent_dirs_to_invalidate = FxHashSet::default();
432
433        for path in paths {
434            let mut current_parent = path.parent();
435            while let Some(parent) = current_parent {
436                parent_dirs_to_invalidate.insert(parent.to_path_buf());
437                current_parent = parent.parent();
438            }
439
440            for (invalidated_path, path_invalidators) in
441                invalidator_map.extract_path_with_children(&path)
442            {
443                let reason_for_path = reason(&invalidated_path);
444                invalidators.extend(
445                    path_invalidators
446                        .into_iter()
447                        .map(|invalidator| (reason_for_path.clone(), invalidator)),
448                );
449            }
450
451            for (invalidated_path, path_invalidators) in
452                dir_invalidator_map.extract_path_with_children(&path)
453            {
454                let reason_for_path = reason(&invalidated_path);
455                invalidators.extend(
456                    path_invalidators
457                        .into_iter()
458                        .map(|invalidator| (reason_for_path.clone(), invalidator)),
459                );
460            }
461        }
462
463        for path in parent_dirs_to_invalidate {
464            if let Some(path_invalidators) = dir_invalidator_map.remove(&path) {
465                let reason_for_path = reason(&path);
466                invalidators.extend(
467                    path_invalidators
468                        .into_iter()
469                        .map(|invalidator| (reason_for_path.clone(), invalidator)),
470                );
471            }
472        }
473
474        drop(invalidator_map);
475        drop(dir_invalidator_map);
476
477        parallel::for_each_owned(invalidators, |(reason, invalidator)| {
478            invalidator.invalidate_with_reason(&*turbo_tasks, reason)
479        });
480    }
481
482    #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
483    async fn start_watching_internal(
484        self: &Arc<Self>,
485        report_invalidation_reason: bool,
486        poll_interval: Option<Duration>,
487    ) -> Result<()> {
488        let root_path = self.root_path().to_path_buf();
489
490        // create the directory for the filesystem on disk, if it doesn't exist
491        retry_blocking(|| std::fs::create_dir_all(&root_path))
492            .instrument(tracing::info_span!("create root directory", name = ?root_path))
493            .concurrency_limited(&self.write_semaphore)
494            .await?;
495
496        self.watcher
497            .start_watching(self.clone(), report_invalidation_reason, poll_interval)
498            .await?;
499
500        Ok(())
501    }
502}
503
504#[derive(Clone, ValueToString)]
505#[value_to_string(self.inner.name)]
506#[turbo_tasks::value(cell = "new", eq = "manual")]
507pub struct DiskFileSystem {
508    inner: Arc<DiskFileSystemInner>,
509}
510
511impl DiskFileSystem {
512    pub fn name(&self) -> &RcStr {
513        &self.inner.name
514    }
515
516    pub fn root(&self) -> &RcStr {
517        &self.inner.root
518    }
519
520    pub fn invalidate(&self) {
521        self.inner.invalidate();
522    }
523
524    pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
525        &self,
526        reason: impl Fn(&Path) -> R + Sync,
527    ) {
528        self.inner.invalidate_with_reason(reason);
529    }
530
531    pub fn invalidate_path_and_children_with_reason<R: InvalidationReason + Clone>(
532        &self,
533        paths: impl IntoIterator<Item = PathBuf>,
534        reason: impl Fn(&Path) -> R + Sync,
535    ) {
536        self.inner
537            .invalidate_path_and_children_with_reason(paths, reason);
538    }
539
540    pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
541        self.inner
542            .start_watching_internal(false, poll_interval)
543            .await
544    }
545
546    pub async fn start_watching_with_invalidation_reason(
547        &self,
548        poll_interval: Option<Duration>,
549    ) -> Result<()> {
550        self.inner
551            .start_watching_internal(true, poll_interval)
552            .await
553    }
554
555    pub async fn stop_watching(&self) {
556        self.inner.watcher.stop_watching().await;
557    }
558
559    /// Try to convert [`Path`] to [`FileSystemPath`]. Return `None` if the file path leaves the
560    /// filesystem root. If no `relative_to` argument is given, it is assumed that the `sys_path` is
561    /// relative to the [`DiskFileSystem`] root.
562    ///
563    /// Attempts to convert absolute paths to paths relative to the filesystem root, though we only
564    /// attempt to do so lexically.
565    ///
566    /// Assumes `self` is the `DiskFileSystem` contained in `vc_self`. This API is a bit awkward
567    /// because:
568    /// - [`Path`]/[`PathBuf`] should not be stored in the filesystem cache, so the function cannot
569    ///   be a [`turbo_tasks::function`].
570    /// - It's a little convenient for this function to be sync.
571    pub fn try_from_sys_path(
572        &self,
573        vc_self: ResolvedVc<DiskFileSystem>,
574        sys_path: &Path,
575        relative_to: Option<&FileSystemPath>,
576    ) -> Option<FileSystemPath> {
577        let vc_self = ResolvedVc::upcast(vc_self);
578
579        let sys_path = simplified(sys_path);
580        let relative_sys_path = if sys_path.is_absolute() {
581            // `normalize_lexically` will return an error if the relative `sys_path` leaves the
582            // DiskFileSystem root
583            let normalized_sys_path = sys_path.normalize_lexically().ok()?;
584            normalized_sys_path
585                .strip_prefix(self.inner.root_path())
586                .ok()?
587                .to_owned()
588        } else if let Some(relative_to) = relative_to {
589            debug_assert_eq!(
590                relative_to.fs, vc_self,
591                "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
592            );
593            let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
594            joined_sys_path.push(sys_path);
595            joined_sys_path.normalize_lexically().ok()?
596        } else {
597            sys_path.normalize_lexically().ok()?
598        };
599
600        Some(FileSystemPath {
601            fs: vc_self,
602            path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
603        })
604    }
605
606    pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
607        let path = self.inner.root_path();
608        if fs_path.path.is_empty() {
609            path.to_path_buf()
610        } else {
611            path.join(&*unix_to_sys(&fs_path.path))
612        }
613    }
614}
615
616#[allow(dead_code, reason = "we need to hold onto the locks")]
617struct PathLockGuard<'a>(
618    #[allow(dead_code)] RwLockReadGuard<'a, ()>,
619    #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
620);
621
622fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
623    if let Ok(rel_path) = path.strip_prefix(root_path) {
624        let path = if MAIN_SEPARATOR != '/' {
625            let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
626            format!("[{name}]/{rel_path}")
627        } else {
628            format!("[{name}]/{}", rel_path.display())
629        };
630        Some(path)
631    } else {
632        None
633    }
634}
635
636impl DiskFileSystem {
637    /// Create a new instance of `DiskFileSystem`.
638    /// # Arguments
639    ///
640    /// * `name` - Name of the filesystem.
641    /// * `root` - Path to the given filesystem's root. Should be
642    ///   [canonicalized][std::fs::canonicalize].
643    pub fn new(name: RcStr, root: Vc<RcStr>) -> Vc<Self> {
644        Self::new_internal(name, root, Vec::new())
645    }
646
647    /// Create a new instance of `DiskFileSystem`.
648    /// # Arguments
649    ///
650    /// * `name` - Name of the filesystem.
651    /// * `root` - Path to the given filesystem's root. Should be
652    ///   [canonicalized][std::fs::canonicalize].
653    /// * `denied_paths` - Paths within this filesystem that are not allowed to be accessed or
654    ///   navigated into.  These must be normalized, non-empty and relative to the fs root.
655    pub fn new_with_denied_paths(
656        name: RcStr,
657        root: Vc<RcStr>,
658        denied_paths: Vec<RcStr>,
659    ) -> Vc<Self> {
660        for denied_path in &denied_paths {
661            debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
662            debug_assert!(
663                normalize_path(denied_path).as_deref() == Some(&**denied_path),
664                "denied_path must be normalized: {denied_path:?}"
665            );
666        }
667        Self::new_internal(name, root, denied_paths)
668    }
669}
670
671#[turbo_tasks::value_impl]
672impl DiskFileSystem {
673    #[turbo_tasks::function]
674    async fn new_internal(
675        name: RcStr,
676        root: Vc<RcStr>,
677        denied_paths: Vec<RcStr>,
678    ) -> Result<Vc<Self>> {
679        let root = root.owned().await?;
680        let instance = DiskFileSystem {
681            inner: Arc::new(DiskFileSystemInner {
682                name,
683                root,
684                mutex_map: Default::default(),
685                invalidation_lock: Default::default(),
686                invalidator_map: InvalidatorMap::new(),
687                dir_invalidator_map: InvalidatorMap::new(),
688                read_semaphore: create_read_semaphore(),
689                write_semaphore: create_write_semaphore(),
690                watcher: DiskWatcher::new(),
691                denied_paths,
692                turbo_tasks: turbo_tasks_weak(),
693                tokio_handle: Handle::current(),
694                effect_state_storage: EffectStateStorage::default(),
695            }),
696        };
697
698        Ok(Self::cell(instance))
699    }
700}
701
702impl Debug for DiskFileSystem {
703    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
704        write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
705    }
706}
707
708#[turbo_tasks::value_impl]
709impl FileSystem for DiskFileSystem {
710    #[turbo_tasks::function(fs)]
711    async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
712        mark_session_dependent();
713
714        // Check if path is denied - if so, treat as NotFound
715        if self.inner.is_path_denied(&fs_path) {
716            return Ok(FileContent::NotFound.cell());
717        }
718        let full_path = self.to_sys_path(&fs_path);
719
720        self.inner.register_read_invalidator(&full_path).await?;
721
722        let _lock = self.inner.lock_path(&full_path).await;
723        let content = match retry_blocking(|| File::from_path(&full_path))
724            .instrument(tracing::info_span!("read file", name = ?full_path))
725            .concurrency_limited(&self.inner.read_semaphore)
726            .await
727        {
728            Ok(file) => FileContent::new(file),
729            Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
730                FileContent::NotFound
731            }
732            // ast-grep-ignore: no-context-format
733            Err(e) => return Err(anyhow!(e).context(format!("reading file {full_path:?}"))),
734        };
735        Ok(content.cell())
736    }
737
738    #[turbo_tasks::function(fs)]
739    async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
740        mark_session_dependent();
741
742        // Check if directory itself is denied - if so, treat as NotFound
743        if self.inner.is_path_denied(&fs_path) {
744            return Ok(RawDirectoryContent::not_found());
745        }
746        let full_path = self.to_sys_path(&fs_path);
747
748        self.inner.register_dir_invalidator(&full_path).await?;
749
750        // we use the sync std function here as it's a lot faster (600%) in node-file-trace
751        let read_dir = match retry_blocking(|| std::fs::read_dir(&full_path))
752            .instrument(tracing::info_span!("read directory", name = ?full_path))
753            .concurrency_limited(&self.inner.read_semaphore)
754            .await
755        {
756            Ok(dir) => dir,
757            Err(e)
758                if e.kind() == ErrorKind::NotFound
759                    || e.kind() == ErrorKind::NotADirectory
760                    || e.kind() == ErrorKind::InvalidFilename =>
761            {
762                return Ok(RawDirectoryContent::not_found());
763            }
764            Err(e) => {
765                // ast-grep-ignore: no-context-format
766                return Err(anyhow!(e).context(format!("reading dir {full_path:?}")));
767            }
768        };
769        let dir_path = fs_path.path.as_str();
770        let denied_entries: FxHashSet<&str> = self
771            .inner
772            .denied_paths
773            .iter()
774            .filter_map(|denied_path| {
775                // If we have a denied path, we need to see if the current directory is a prefix of
776                // the denied path meaning that it is possible that some directory entry needs to be
777                // filtered. we do this first to avoid string manipulation on every
778                // iteration of the directory entries. So while expanding `foo/bar`,
779                // if `foo/bar/baz` is denied, we filter out `baz`.
780                // But if foo/bar/baz/qux is denied we don't filter anything from this level.
781                if denied_path.starts_with(dir_path) {
782                    let denied_path_suffix =
783                        if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
784                            Some(&denied_path[dir_path.len() + 1..])
785                        } else if dir_path.is_empty() {
786                            Some(denied_path.as_str())
787                        } else {
788                            None
789                        };
790                    // if the suffix is `foo/bar` we cannot filter foo from this level
791                    denied_path_suffix.filter(|s| !s.contains('/'))
792                } else {
793                    None
794                }
795            })
796            .collect();
797
798        let entries = read_dir
799            .filter_map(|r| {
800                let e = match r {
801                    Ok(e) => e,
802                    Err(err) => return Some(Err(err.into())),
803                };
804
805                // we filter out any non unicode names
806                let file_name = RcStr::from(e.file_name().to_str()?);
807                // Filter out denied entries
808                if denied_entries.contains(file_name.as_str()) {
809                    return None;
810                }
811
812                let entry = match e.file_type() {
813                    Ok(t) if t.is_file() => RawDirectoryEntry::File,
814                    Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
815                    Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
816                    Ok(_) => RawDirectoryEntry::Other,
817                    Err(err) => return Some(Err(err.into())),
818                };
819
820                Some(anyhow::Ok((file_name, entry)))
821            })
822            .collect::<Result<_>>()
823            .with_context(|| format!("reading directory item in {full_path:?}"))?;
824
825        Ok(RawDirectoryContent::new(entries))
826    }
827
828    #[turbo_tasks::function(fs)]
829    async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
830        mark_session_dependent();
831
832        // Check if path is denied - if so, treat as NotFound
833        if self.inner.is_path_denied(&fs_path) {
834            return Ok(LinkContent::NotFound.cell());
835        }
836        let full_path = self.to_sys_path(&fs_path);
837
838        self.inner.register_read_invalidator(&full_path).await?;
839
840        let _lock = self.inner.lock_path(&full_path).await;
841        let link_path = match retry_blocking(|| std::fs::read_link(&full_path))
842            .instrument(tracing::info_span!("read symlink", name = ?full_path))
843            .concurrency_limited(&self.inner.read_semaphore)
844            .await
845        {
846            Ok(res) => res,
847            Err(_) => return Ok(LinkContent::NotFound.cell()),
848        };
849        let is_link_absolute = link_path.is_absolute();
850
851        let mut file = link_path.clone();
852        if !is_link_absolute {
853            if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
854                normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
855            }) {
856                #[cfg(windows)]
857                {
858                    file = PathBuf::from(normalized_linked_path);
859                }
860                // `normalize_path` stripped the leading `/` of the path
861                // add it back here or the `strip_prefix` will return `Err`
862                #[cfg(not(windows))]
863                {
864                    file = PathBuf::from(format!("/{normalized_linked_path}"));
865                }
866            } else {
867                return Ok(LinkContent::Invalid.cell());
868            }
869        }
870
871        // strip the root from the path, it serves two purpose
872        // 1. ensure the linked path is under the root
873        // 2. strip the root path if the linked path is absolute
874        //
875        // we use `dunce::simplify` to strip a potential UNC prefix on windows, on any
876        // other OS this gets compiled away
877        let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
878
879        let relative_to_root_path = match result {
880            Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
881            Err(_) => return Ok(LinkContent::Invalid.cell()),
882        };
883
884        let (target, file_type) = if is_link_absolute {
885            let target_string = RcStr::from(relative_to_root_path.to_string_lossy());
886            (
887                target_string.clone(),
888                FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
889                    .get_type()
890                    .await?,
891            )
892        } else {
893            let link_path_string_cow = link_path.to_string_lossy();
894            let link_path_unix = RcStr::from(sys_to_unix(&link_path_string_cow));
895            (
896                link_path_unix.clone(),
897                fs_path.parent().join(&link_path_unix)?.get_type().await?,
898            )
899        };
900
901        Ok(LinkContent::Link {
902            target,
903            link_type: {
904                let mut link_type = Default::default();
905                if link_path.is_absolute() {
906                    link_type |= LinkType::ABSOLUTE;
907                }
908                if matches!(&*file_type, FileSystemEntryType::Directory) {
909                    link_type |= LinkType::DIRECTORY;
910                }
911                link_type
912            },
913        }
914        .cell())
915    }
916
917    #[turbo_tasks::function(fs)]
918    async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
919        // You might be tempted to use `mark_session_dependent` here, but
920        // `write` purely declares a side effect and does not need to be reexecuted in the next
921        // session. All side effects are reexecuted in general.
922
923        // Check if path is denied - if so, return an error
924        if self.inner.is_path_denied(&fs_path) {
925            turbobail!("Cannot write to denied path: {fs_path}");
926        }
927        let full_path = self.to_sys_path(&fs_path);
928
929        // Persist the file content so it is stored in the persistent cache.
930        // Since FileContent uses serialization = "hash", persisting it here ensures the full
931        // content is available in the persistent cache (via PersistedFileContent) and does not
932        // require recomputing the content on cache restore — avoiding unnecessary downstream
933        // recomputation.
934        let content = content.persist().await?;
935
936        let inner = self.inner.clone();
937
938        #[derive(TraceRawVcs, NonLocalValue)]
939        struct WriteEffect {
940            full_path: PathBuf,
941            inner: Arc<DiskFileSystemInner>,
942            content: ReadRef<PersistedFileContent>,
943            content_hash: u128,
944        }
945
946        impl Effect for WriteEffect {
947            type Error = AnyhowWrapper;
948            type Value = u128;
949
950            fn key(&self) -> Vec<u8> {
951                self.full_path.as_os_str().as_encoded_bytes().to_vec()
952            }
953
954            fn value(&self) -> &u128 {
955                &self.content_hash
956            }
957
958            fn state_storage(&self) -> &EffectStateStorage {
959                &self.inner.effect_state_storage
960            }
961
962            async fn apply(&self) -> Result<(), AnyhowWrapper> {
963                self.apply_inner().await.map_err(AnyhowWrapper::from)
964            }
965        }
966
967        impl WriteEffect {
968            async fn apply_inner(&self) -> anyhow::Result<()> {
969                let full_path = validate_path_length(&self.full_path)?;
970
971                let _lock = self.inner.lock_path(&full_path).await;
972
973                // We perform an untracked comparison here, so that this write is not dependent
974                // on a read's Vc<FileContent> (and the memory it holds). Our untracked read can
975                // be freed immediately. Given this is an output file, it's unlikely any Turbo
976                // code will need to read the file from disk into a Vc<FileContent>, so we're
977                // not wasting cycles.
978                let compare = self
979                    .content
980                    .streaming_compare(&full_path)
981                    .instrument(tracing::info_span!("read file before write", name = ?full_path))
982                    .concurrency_limited(&self.inner.read_semaphore)
983                    .await?;
984                if compare == FileComparison::Equal {
985                    return Ok(());
986                }
987
988                match &*self.content {
989                    PersistedFileContent::Content(..) => {
990                        let content = self.content.clone();
991                        let full_path = full_path.into_owned();
992                        async {
993                            let do_write = || {
994                                let content = content.clone();
995                                let full_path = full_path.clone();
996                                let span = tracing::info_span!("write file", name = ?full_path);
997                                retry_blocking(move || {
998                                    let mut f = std::fs::File::create(&full_path)?;
999                                    let PersistedFileContent::Content(file) = &*content else {
1000                                        unreachable!()
1001                                    };
1002                                    std::io::copy(&mut file.read(), &mut f)?;
1003                                    #[cfg(unix)]
1004                                    f.set_permissions(file.meta.permissions.into())?;
1005                                    f.flush()?;
1006
1007                                    static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
1008                                        std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
1009                                            .is_some_and(|v| v == "1" || v == "true")
1010                                    });
1011                                    if *WRITE_VERSION {
1012                                        let mut full_path = full_path.clone();
1013                                        let hash = hash_xxh3_hash64(file);
1014                                        let ext = full_path.extension();
1015                                        let ext = if let Some(ext) = ext {
1016                                            format!("{:016x}.{}", hash, ext.to_string_lossy())
1017                                        } else {
1018                                            format!("{hash:016x}")
1019                                        };
1020                                        full_path.set_extension(ext);
1021                                        let mut f = std::fs::File::create(&full_path)?;
1022                                        std::io::copy(&mut file.read(), &mut f)?;
1023                                        #[cfg(unix)]
1024                                        f.set_permissions(file.meta.permissions.into())?;
1025                                        f.flush()?;
1026                                    }
1027                                    Ok::<(), io::Error>(())
1028                                })
1029                                .instrument(span)
1030                            };
1031
1032                            match do_write().await {
1033                                Err(e) if e.kind() == ErrorKind::NotFound => {
1034                                    // The parent directory doesn't exist. Create it and retry once.
1035                                    if let Some(parent) = full_path.parent() {
1036                                        retry_blocking(|| std::fs::create_dir_all(parent))
1037                                            .instrument(tracing::info_span!(
1038                                                "create directory",
1039                                                name = ?parent
1040                                            ))
1041                                            .await
1042                                            .with_context(|| {
1043                                                format!(
1044                                                    "failed to create directory {parent:?} for \
1045                                                     write to {full_path:?}",
1046                                                )
1047                                            })?;
1048                                    }
1049                                    do_write().await.with_context(|| {
1050                                        format!("failed to write to {full_path:?}")
1051                                    })?;
1052                                }
1053                                result => {
1054                                    result.with_context(|| {
1055                                        format!("failed to write to {full_path:?}")
1056                                    })?;
1057                                }
1058                            }
1059                            anyhow::Ok(())
1060                        }
1061                        .concurrency_limited(&self.inner.write_semaphore)
1062                        .await?;
1063                    }
1064                    PersistedFileContent::NotFound => {
1065                        retry_blocking(|| std::fs::remove_file(&full_path))
1066                            .instrument(tracing::info_span!("remove file", name = ?full_path))
1067                            .concurrency_limited(&self.inner.write_semaphore)
1068                            .await
1069                            .or_else(|err| {
1070                                if err.kind() == ErrorKind::NotFound {
1071                                    Ok(())
1072                                } else {
1073                                    Err(err)
1074                                }
1075                            })
1076                            .with_context(|| format!("removing {full_path:?} failed"))?;
1077                    }
1078                }
1079
1080                // Invalidate any read tasks tracking this path so they re-read the new content
1081                self.inner.invalidate_from_write(&self.full_path);
1082
1083                Ok(())
1084            }
1085        }
1086
1087        let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*content));
1088        emit_effect(WriteEffect {
1089            full_path,
1090            inner,
1091            content,
1092            content_hash,
1093        });
1094
1095        Ok(())
1096    }
1097
1098    #[turbo_tasks::function(fs)]
1099    async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
1100        // You might be tempted to use `mark_session_dependent` here, but we purely declare a side
1101        // effect and does not need to be re-executed in the next session. All side effects are
1102        // re-executed in general.
1103
1104        // Check if path is denied - if so, return an error
1105        if self.inner.is_path_denied(&fs_path) {
1106            turbobail!("Cannot write link to denied path: {fs_path}");
1107        }
1108
1109        let content = target.await?;
1110
1111        let full_path = self.to_sys_path(&fs_path);
1112        let inner = self.inner.clone();
1113
1114        #[derive(TraceRawVcs, NonLocalValue)]
1115        struct WriteLinkEffect {
1116            full_path: PathBuf,
1117            inner: Arc<DiskFileSystemInner>,
1118            content: ReadRef<LinkContent>,
1119            content_hash: u128,
1120        }
1121
1122        impl Effect for WriteLinkEffect {
1123            type Error = AnyhowWrapper;
1124            type Value = u128;
1125
1126            fn key(&self) -> Vec<u8> {
1127                self.full_path.as_os_str().as_encoded_bytes().to_vec()
1128            }
1129
1130            fn value(&self) -> &u128 {
1131                &self.content_hash
1132            }
1133
1134            fn state_storage(&self) -> &EffectStateStorage {
1135                &self.inner.effect_state_storage
1136            }
1137
1138            async fn apply(&self) -> Result<(), AnyhowWrapper> {
1139                self.apply_inner().await.map_err(AnyhowWrapper::from)
1140            }
1141        }
1142
1143        impl WriteLinkEffect {
1144            async fn apply_inner(&self) -> anyhow::Result<()> {
1145                let full_path = validate_path_length(&self.full_path)?;
1146
1147                let _lock = self.inner.lock_path(&full_path).await;
1148
1149                enum OsSpecificLinkContent {
1150                    Link {
1151                        #[cfg(windows)]
1152                        is_directory: bool,
1153                        target: PathBuf,
1154                    },
1155                    NotFound,
1156                    Invalid,
1157                }
1158
1159                let os_specific_link_content = match &*self.content {
1160                    LinkContent::Link { target, link_type } => {
1161                        let is_directory = link_type.contains(LinkType::DIRECTORY);
1162                        let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1163                            Path::new(&self.inner.root).join(unix_to_sys(target).as_ref())
1164                        } else {
1165                            let relative_target = PathBuf::from(unix_to_sys(target).as_ref());
1166                            if cfg!(windows) && is_directory {
1167                                // Windows junction points must always be stored as absolute
1168                                full_path
1169                                    .parent()
1170                                    .unwrap_or(&full_path)
1171                                    .join(relative_target)
1172                            } else {
1173                                relative_target
1174                            }
1175                        };
1176                        OsSpecificLinkContent::Link {
1177                            #[cfg(windows)]
1178                            is_directory,
1179                            target: target_path,
1180                        }
1181                    }
1182                    LinkContent::Invalid => OsSpecificLinkContent::Invalid,
1183                    LinkContent::NotFound => OsSpecificLinkContent::NotFound,
1184                };
1185
1186                let old_content = match retry_blocking(|| std::fs::read_link(&full_path))
1187                    .instrument(tracing::info_span!("read symlink before write", name = ?full_path))
1188                    .concurrency_limited(&self.inner.read_semaphore)
1189                    .await
1190                {
1191                    Ok(res) => Some((res.is_absolute(), res)),
1192                    Err(_) => None,
1193                };
1194                let is_equal = match (&os_specific_link_content, &old_content) {
1195                    (
1196                        OsSpecificLinkContent::Link { target, .. },
1197                        Some((old_is_absolute, old_target)),
1198                    ) => target == old_target && target.is_absolute() == *old_is_absolute,
1199                    (OsSpecificLinkContent::NotFound, None) => true,
1200                    _ => false,
1201                };
1202                if is_equal {
1203                    return Ok(());
1204                }
1205
1206                match os_specific_link_content {
1207                    OsSpecificLinkContent::Link {
1208                        target,
1209                        #[cfg(windows)]
1210                        is_directory,
1211                        ..
1212                    } => {
1213                        let full_path = full_path.into_owned();
1214
1215                        #[derive(thiserror::Error, Debug)]
1216                        #[error("{msg}: {source}")]
1217                        struct SymlinkCreationError {
1218                            msg: &'static str,
1219                            #[source]
1220                            source: io::Error,
1221                        }
1222
1223                        let mut has_old_content = old_content.is_some();
1224                        let try_create_link = || {
1225                            if has_old_content {
1226                                // Remove existing symlink before creating a new one. On Unix,
1227                                // symlink(2) fails with EEXIST if the link already exists instead
1228                                // of overwriting it. Windows has similar behavior with junction
1229                                // points.
1230                                remove_symbolic_link_dir_helper(&full_path).map_err(|err| {
1231                                    SymlinkCreationError {
1232                                        msg: "removal of existing symbolic link or junction point \
1233                                              failed",
1234                                        source: err,
1235                                    }
1236                                })?;
1237                                has_old_content = false;
1238                            }
1239                            #[cfg(not(windows))]
1240                            let io_result = std::os::unix::fs::symlink(&target, &full_path);
1241                            #[cfg(windows)]
1242                            let io_result = if is_directory {
1243                                std::os::windows::fs::junction_point(&target, &full_path)
1244                            } else {
1245                                std::os::windows::fs::symlink_file(&target, &full_path)
1246                            };
1247                            io_result.map_err(|err| {
1248                                if err.kind() == ErrorKind::AlreadyExists {
1249                                    // try to remove the symlink on the next iteration of the loop
1250                                    has_old_content = true;
1251                                }
1252                                SymlinkCreationError {
1253                                    msg: "creation of a new symbolic link or junction point failed",
1254                                    source: err,
1255                                }
1256                            })
1257                        };
1258                        fn can_retry_link(err: &SymlinkCreationError) -> bool {
1259                            err.source.kind() == ErrorKind::AlreadyExists || can_retry(&err.source)
1260                        }
1261                        let err_context = || {
1262                            #[cfg(not(windows))]
1263                            let message = format!(
1264                                "failed to create symlink at {full_path:?} pointing to {target:?}"
1265                            );
1266                            #[cfg(windows)]
1267                            let message = if is_directory {
1268                                format!(
1269                                    "failed to create junction point at {full_path:?} pointing to \
1270                                     {target:?}"
1271                                )
1272                            } else {
1273                                format!(
1274                                    "failed to create symlink at {full_path:?} pointing to \
1275                                     {target:?}\n\
1276                                    (Note: creating file symlinks on Windows require developer \
1277                                     mode or admin permissions: \
1278                                     https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode)",
1279                                )
1280                            };
1281                            message
1282                        };
1283                        async {
1284                            let write_result =
1285                                retry_blocking_custom(try_create_link, can_retry_link)
1286                                    .instrument(tracing::info_span!(
1287                                        "write symlink",
1288                                        name = ?full_path,
1289                                        target = ?target,
1290                                    ))
1291                                    .await;
1292
1293                            match write_result {
1294                                Err(ref e) if e.source.kind() == ErrorKind::NotFound => {
1295                                    // Parent directory doesn't exist. Create it and retry once.
1296                                    if let Some(parent) = full_path.parent() {
1297                                        retry_blocking(|| std::fs::create_dir_all(parent))
1298                                            .instrument(tracing::info_span!(
1299                                                "create directory",
1300                                                name = ?parent
1301                                            ))
1302                                            .await
1303                                            .with_context(|| {
1304                                                format!(
1305                                                    "failed to create directory {parent:?} for \
1306                                                     write link to {full_path:?}",
1307                                                )
1308                                            })?;
1309                                    }
1310                                    // After the first attempt, any pre-existing link was already
1311                                    // removed (has_old_content is now false), so just create.
1312                                    retry_blocking_custom(
1313                                        || {
1314                                            #[cfg(not(windows))]
1315                                            let io_result =
1316                                                std::os::unix::fs::symlink(&target, &full_path);
1317                                            #[cfg(windows)]
1318                                            let io_result = if is_directory {
1319                                                std::os::windows::fs::junction_point(
1320                                                    &target, &full_path,
1321                                                )
1322                                            } else {
1323                                                std::os::windows::fs::symlink_file(
1324                                                    &target, &full_path,
1325                                                )
1326                                            };
1327                                            io_result.map_err(|err| SymlinkCreationError {
1328                                                msg: "creation of a new symbolic link or junction \
1329                                                      point failed",
1330                                                source: err,
1331                                            })
1332                                        },
1333                                        |e: &SymlinkCreationError| can_retry(&e.source),
1334                                    )
1335                                    .instrument(tracing::info_span!(
1336                                        "write symlink",
1337                                        name = ?full_path,
1338                                        target = ?target,
1339                                    ))
1340                                    .await
1341                                    .with_context(err_context)?;
1342                                }
1343                                result => result.with_context(err_context)?,
1344                            }
1345                            anyhow::Ok(())
1346                        }
1347                        .concurrency_limited(&self.inner.write_semaphore)
1348                        .await?;
1349                    }
1350                    OsSpecificLinkContent::Invalid => {
1351                        bail!("invalid symlink target: {full_path:?}");
1352                    }
1353                    OsSpecificLinkContent::NotFound => {
1354                        retry_blocking(|| remove_symbolic_link_dir_helper(&full_path))
1355                            .instrument(tracing::info_span!("remove symlink", name = ?full_path))
1356                            .concurrency_limited(&self.inner.write_semaphore)
1357                            .await
1358                            .with_context(|| format!("removing {full_path:?} failed"))?;
1359                    }
1360                }
1361
1362                // Invalidate any read tasks tracking this path so they re-read the new content
1363                self.inner.invalidate_from_write(&self.full_path);
1364
1365                Ok(())
1366            }
1367        }
1368
1369        let content_hash = u128::from_le_bytes(hash_xxh3_hash128(&*content));
1370        emit_effect(WriteLinkEffect {
1371            full_path,
1372            inner,
1373            content,
1374            content_hash,
1375        });
1376        Ok(())
1377    }
1378
1379    #[turbo_tasks::function(fs)]
1380    async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1381        mark_session_dependent();
1382        let full_path = self.to_sys_path(&fs_path);
1383
1384        // Check if path is denied - if so, return an error (metadata shouldn't be readable)
1385        if self.inner.is_path_denied(&fs_path) {
1386            turbobail!("Cannot read metadata from denied path: {fs_path}");
1387        }
1388
1389        self.inner.register_read_invalidator(&full_path).await?;
1390
1391        let _lock = self.inner.lock_path(&full_path).await;
1392        let meta = retry_blocking(|| std::fs::metadata(&full_path))
1393            .instrument(tracing::info_span!("read metadata", name = ?full_path))
1394            .concurrency_limited(&self.inner.read_semaphore)
1395            .await
1396            .with_context(|| format!("reading metadata for {:?}", full_path))?;
1397
1398        Ok(FileMeta::cell(meta.into()))
1399    }
1400}
1401
1402fn remove_symbolic_link_dir_helper(path: &Path) -> io::Result<()> {
1403    let result = if cfg!(windows) {
1404        // Junction points on Windows are treated as directories, and therefore need
1405        // `remove_dir`:
1406        //
1407        // > `RemoveDirectory` can be used to remove a directory junction. Since the target
1408        // > directory and its contents will remain accessible through its canonical path, the
1409        // > target directory itself is not affected by removing a junction which targets it.
1410        //
1411        // -- https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectoryw
1412        //
1413        // However, Next 16.1.0 shipped with symlinks, before we switched to junction links on
1414        // Windows, and `remove_dir` won't work on symlinks. So try to remove it as a directory
1415        // (junction) first, and then fall back to removing it as a file (symlink).
1416        std::fs::remove_dir(path).or_else(|err| {
1417            if err.kind() == ErrorKind::NotADirectory {
1418                std::fs::remove_file(path)
1419            } else {
1420                Err(err)
1421            }
1422        })
1423    } else {
1424        std::fs::remove_file(path)
1425    };
1426    match result {
1427        Ok(()) => Ok(()),
1428        Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
1429        Err(err) => Err(err),
1430    }
1431}
1432
1433#[derive(Debug, Clone, Hash, TaskInput)]
1434#[turbo_tasks::value(shared)]
1435pub struct FileSystemPath {
1436    pub fs: ResolvedVc<Box<dyn FileSystem>>,
1437    pub path: RcStr,
1438}
1439
1440impl ValueToStringRef for FileSystemPath {
1441    async fn to_string_ref(&self) -> Result<RcStr> {
1442        turbofmt!("[{}]/{}", self.fs, self.path).await
1443    }
1444}
1445
1446#[turbo_tasks::value_impl]
1447impl ValueToString for FileSystemPath {
1448    #[turbo_tasks::function]
1449    async fn to_string(&self) -> Result<Vc<RcStr>> {
1450        Ok(Vc::cell(self.to_string_ref().await?))
1451    }
1452}
1453
1454impl FileSystemPath {
1455    pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1456        if self.fs == other.fs && self.path.starts_with(&*other.path) {
1457            if other.path.is_empty() {
1458                true
1459            } else {
1460                self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1461            }
1462        } else {
1463            false
1464        }
1465    }
1466
1467    pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1468        if self.fs == other.fs && self.path.starts_with(&*other.path) {
1469            if other.path.is_empty() {
1470                true
1471            } else {
1472                matches!(
1473                    self.path.as_bytes().get(other.path.len()),
1474                    Some(&b'/') | None
1475                )
1476            }
1477        } else {
1478            false
1479        }
1480    }
1481
1482    pub fn is_root(&self) -> bool {
1483        self.path.is_empty()
1484    }
1485
1486    pub fn is_in_node_modules(&self) -> bool {
1487        self.path.starts_with("node_modules/") || self.path.contains("/node_modules/")
1488    }
1489
1490    /// Returns the path of `inner` relative to `self`.
1491    ///
1492    /// Note: this method always strips the leading `/` from the result.
1493    pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1494        if self.fs != inner.fs {
1495            return None;
1496        }
1497        let path = inner.path.strip_prefix(&*self.path)?;
1498        if self.path.is_empty() {
1499            Some(path)
1500        } else if let Some(stripped) = path.strip_prefix('/') {
1501            Some(stripped)
1502        } else {
1503            None
1504        }
1505    }
1506
1507    pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1508        if self.fs != other.fs {
1509            return None;
1510        }
1511
1512        Some(get_relative_path_to(&self.path, &other.path).into())
1513    }
1514
1515    /// Returns the final component of the FileSystemPath, or an empty string
1516    /// for the root path.
1517    pub fn file_name(&self) -> &str {
1518        let (_, file_name) = self.split_file_name();
1519        file_name
1520    }
1521
1522    /// Returns true if this path has the given extension
1523    ///
1524    /// slightly faster than `self.extension() == Some(extension)` as we can simply match a
1525    /// suffix
1526    pub fn has_extension(&self, extension: &str) -> bool {
1527        debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1528        self.path.ends_with(extension)
1529    }
1530
1531    /// Returns the extension (without a leading `.`)
1532    pub fn extension(&self) -> Option<&str> {
1533        let (_, extension) = self.split_extension();
1534        extension
1535    }
1536
1537    /// Splits the path into two components:
1538    /// 1. The path without the extension;
1539    /// 2. The extension, if any.
1540    fn split_extension(&self) -> (&str, Option<&str>) {
1541        if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1542            if extension.contains('/') ||
1543                // The file name begins with a `.` and has no other `.`s within.
1544                path_before_extension.ends_with('/') || path_before_extension.is_empty()
1545            {
1546                (self.path.as_str(), None)
1547            } else {
1548                (path_before_extension, Some(extension))
1549            }
1550        } else {
1551            (self.path.as_str(), None)
1552        }
1553    }
1554
1555    /// Splits the path into two components:
1556    /// 1. The parent directory, if any;
1557    /// 2. The file name;
1558    fn split_file_name(&self) -> (Option<&str>, &str) {
1559        // Since the path is normalized, we know `parent`, if any, must not be empty.
1560        if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1561            (Some(parent), file_name)
1562        } else {
1563            (None, self.path.as_str())
1564        }
1565    }
1566
1567    /// Splits the path into three components:
1568    /// 1. The parent directory, if any;
1569    /// 2. The file stem;
1570    /// 3. The extension, if any.
1571    fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1572        let (path_before_extension, extension) = self.split_extension();
1573
1574        if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1575            (Some(parent), file_stem, extension)
1576        } else {
1577            (None, path_before_extension, extension)
1578        }
1579    }
1580}
1581
1582#[turbo_tasks::value(transparent)]
1583pub struct FileSystemPathOption(Option<FileSystemPath>);
1584
1585#[turbo_tasks::value_impl]
1586impl FileSystemPathOption {
1587    #[turbo_tasks::function]
1588    pub fn none() -> Vc<Self> {
1589        Vc::cell(None)
1590    }
1591}
1592
1593impl FileSystemPath {
1594    /// Create a new FileSystemPath from a path within a FileSystem. The
1595    /// /-separated path is expected to be already normalized (this is asserted
1596    /// in dev mode).
1597    fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1598        // On Windows, the path must be converted to a unix path before creating. But on
1599        // Unix, backslashes are a valid char in file names, and the path can be
1600        // provided by the user, so we allow it.
1601        debug_assert!(
1602            MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1603            "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1604             '/'",
1605        );
1606        debug_assert!(
1607            normalize_path(&path).as_deref() == Some(&*path),
1608            "path {path} must be normalized",
1609        );
1610        FileSystemPath { fs, path }
1611    }
1612
1613    /// Adds a subpath to the current path. The /-separated `path` argument might contain ".." or
1614    /// "." segments, but it must not leave the root of the filesystem.
1615    pub fn join(&self, path: &str) -> Result<Self> {
1616        if let Some(path) = join_path(&self.path, path) {
1617            Ok(Self::new_normalized(self.fs, path.into()))
1618        } else {
1619            bail!(
1620                "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1621                self.path,
1622                path,
1623            );
1624        }
1625    }
1626
1627    /// Adds a suffix to the filename. `path` must not contain `/`.
1628    pub fn append(&self, path: &str) -> Result<Self> {
1629        if path.contains('/') {
1630            bail!(
1631                "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1632                self.path,
1633                path,
1634            )
1635        }
1636        Ok(Self::new_normalized(
1637            self.fs,
1638            format!("{}{}", self.path, path).into(),
1639        ))
1640    }
1641
1642    /// Adds a suffix to the basename of the file path. `appending` must not contain `/`. The [file
1643    /// extension][FileSystemPath::extension] will stay intact.
1644    pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1645        if appending.contains('/') {
1646            bail!(
1647                "FileSystemPath({:?}).append_to_stem({:?}) must not append '/'",
1648                self.path,
1649                appending,
1650            )
1651        }
1652        if let (path, Some(ext)) = self.split_extension() {
1653            return Ok(Self::new_normalized(
1654                self.fs,
1655                format!("{path}{appending}.{ext}").into(),
1656            ));
1657        }
1658        Ok(Self::new_normalized(
1659            self.fs,
1660            format!("{}{}", self.path, appending).into(),
1661        ))
1662    }
1663
1664    /// Similar to [FileSystemPath::join], but returns an [`Option`] that will be [`None`] when the
1665    /// joined path would leave the filesystem root.
1666    #[allow(clippy::needless_borrow)] // for windows build
1667    pub fn try_join(&self, path: &str) -> Option<FileSystemPath> {
1668        // TODO(PACK-3279): Remove this once we do not produce invalid paths at the first place.
1669        #[cfg(target_os = "windows")]
1670        let path = path.replace('\\', "/");
1671
1672        join_path(&self.path, &path).map(|p| Self::new_normalized(self.fs, RcStr::from(p)))
1673    }
1674
1675    /// Similar to [FileSystemPath::try_join], but returns [`None`] when the new path would leave
1676    /// the current path (not just the filesystem root). This is useful for preventing access
1677    /// outside of a directory.
1678    pub fn try_join_inside(&self, path: &str) -> Option<FileSystemPath> {
1679        if let Some(p) = join_path(&self.path, path)
1680            && p.starts_with(&*self.path)
1681        {
1682            return Some(Self::new_normalized(self.fs, RcStr::from(p)));
1683        }
1684        None
1685    }
1686
1687    /// DETERMINISM: Result is in random order. Either sort the result or do not depend on the
1688    /// order.
1689    pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1690        read_glob(self.clone(), glob)
1691    }
1692
1693    // Tracks all files and directories matching the glob using the filesystem watcher. Follows
1694    // symlinks as though they were part of the original hierarchy. The returned [`Vc`] will be
1695    // invalidated if a file or directory changes.
1696    pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1697        track_glob(self.clone(), glob, include_dot_files)
1698    }
1699
1700    pub fn root(&self) -> Vc<Self> {
1701        self.fs().root()
1702    }
1703}
1704
1705impl FileSystemPath {
1706    pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1707        *self.fs
1708    }
1709
1710    pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1711        self.is_inside_ref(other)
1712    }
1713
1714    pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1715        self.is_inside_or_equal_ref(other)
1716    }
1717
1718    /// Creates a new [`FileSystemPath`] like `self` but with the given
1719    /// extension.
1720    pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1721        let (path_without_extension, _) = self.split_extension();
1722        Self::new_normalized(
1723            self.fs,
1724            // Like `Path::with_extension` and `PathBuf::set_extension`, if the extension is empty,
1725            // we remove the extension altogether.
1726            match extension.is_empty() {
1727                true => path_without_extension.into(),
1728                false => format!("{path_without_extension}.{extension}").into(),
1729            },
1730        )
1731    }
1732
1733    /// Extracts the stem (non-extension) portion of self.file_name.
1734    ///
1735    /// The stem is:
1736    ///
1737    /// * [`None`], if there is no file name;
1738    /// * The entire file name if there is no embedded `.`;
1739    /// * The entire file name if the file name begins with `.` and has no other `.`s within;
1740    /// * Otherwise, the portion of the file name before the final `.`
1741    pub fn file_stem(&self) -> Option<&str> {
1742        let (_, file_stem, _) = self.split_file_stem_extension();
1743        if file_stem.is_empty() {
1744            return None;
1745        }
1746        Some(file_stem)
1747    }
1748}
1749
1750impl std::fmt::Display for FileSystemPath {
1751    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1752        f.write_str(&self.path)
1753    }
1754}
1755
1756#[turbo_tasks::function]
1757pub async fn rebase(
1758    fs_path: FileSystemPath,
1759    old_base: FileSystemPath,
1760    new_base: FileSystemPath,
1761) -> Result<Vc<FileSystemPath>> {
1762    let new_path;
1763    if old_base.path.is_empty() {
1764        if new_base.path.is_empty() {
1765            new_path = fs_path.path.clone();
1766        } else {
1767            new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1768        }
1769    } else {
1770        let base_path = [&old_base.path, "/"].concat();
1771        if !fs_path.path.starts_with(&base_path) {
1772            turbobail!(
1773                "rebasing {fs_path} from {old_base} onto {new_base} doesn't work because it's not \
1774                 part of the source path",
1775            );
1776        }
1777        if new_base.path.is_empty() {
1778            new_path = [&fs_path.path[base_path.len()..]].concat().into();
1779        } else {
1780            new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1781                .concat()
1782                .into();
1783        }
1784    }
1785    Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1786}
1787
1788// Not turbo-tasks functions, only delegating
1789impl FileSystemPath {
1790    pub fn read(&self) -> Vc<FileContent> {
1791        self.fs().read(self.clone())
1792    }
1793
1794    pub fn read_link(&self) -> Vc<LinkContent> {
1795        self.fs().read_link(self.clone())
1796    }
1797
1798    pub fn read_json(&self) -> Vc<FileJsonContent> {
1799        self.fs().read(self.clone()).parse_json()
1800    }
1801
1802    pub fn read_json5(&self) -> Vc<FileJsonContent> {
1803        self.fs().read(self.clone()).parse_json5()
1804    }
1805
1806    /// Reads content of a directory.
1807    ///
1808    /// DETERMINISM: Result is in random order. Either sort result or do not
1809    /// depend on the order.
1810    pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1811        self.fs().raw_read_dir(self.clone())
1812    }
1813
1814    pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1815        self.fs().write(self.clone(), content)
1816    }
1817
1818    /// Creates a symbolic link to a directory on *nix platforms, or a directory junction point on
1819    /// Windows.
1820    ///
1821    /// [Windows supports symbolic links][windows-symlink], but they [can require elevated
1822    /// privileges][windows-privileges] if "developer mode" is not enabled, so we can't safely use
1823    /// them. Using junction points [matches the behavior of pnpm][pnpm-windows].
1824    ///
1825    /// This only supports directories because Windows junction points are incompatible with files.
1826    /// To ensure compatibility, this will return an error if the target is a file, even on
1827    /// platforms with full symlink support.
1828    ///
1829    /// **We intentionally do not provide an API for symlinking a file**, as we cannot support that
1830    /// on all Windows configurations.
1831    ///
1832    /// [windows-symlink]: https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/
1833    /// [windows-privileges]: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/create-symbolic-links
1834    /// [pnpm-windows]: https://pnpm.io/faq#does-it-work-on-windows
1835    pub fn write_symbolic_link_dir(&self, target: Vc<LinkContent>) -> Vc<()> {
1836        self.fs().write_link(self.clone(), target)
1837    }
1838
1839    pub fn metadata(&self) -> Vc<FileMeta> {
1840        self.fs().metadata(self.clone())
1841    }
1842
1843    // Returns the realpath to the file, resolving all symlinks and reporting an error if the path
1844    // is invalid.
1845    pub async fn realpath(&self) -> Result<FileSystemPath> {
1846        let result = &(*self.realpath_with_links().await?);
1847        match &result.path_result {
1848            Ok(path) => Ok(path.clone()),
1849            Err(error) => bail!("{}", error.as_error_message(self, result).await?),
1850        }
1851    }
1852
1853    pub fn rebase(
1854        fs_path: FileSystemPath,
1855        old_base: FileSystemPath,
1856        new_base: FileSystemPath,
1857    ) -> Vc<FileSystemPath> {
1858        rebase(fs_path, old_base, new_base)
1859    }
1860}
1861
1862impl FileSystemPath {
1863    /// Reads content of a directory.
1864    ///
1865    /// DETERMINISM: Result is in random order. Either sort result or do not
1866    /// depend on the order.
1867    pub fn read_dir(&self) -> Vc<DirectoryContent> {
1868        read_dir(self.clone())
1869    }
1870
1871    pub fn parent(&self) -> FileSystemPath {
1872        let path = &self.path;
1873        if path.is_empty() {
1874            return self.clone();
1875        }
1876        FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1877    }
1878
1879    // It is important that get_type uses read_dir and not stat/metadata.
1880    // - `get_type` is called very very often during resolving and stat would
1881    // make it 1 syscall per call, whereas read_dir would make it 1 syscall per
1882    // directory.
1883    // - `metadata` allows you to use the "wrong" casing on
1884    // case-insensitive filesystems, while read_dir gives you the "correct"
1885    // casing. We want to enforce "correct" casing to avoid broken builds on
1886    // Vercel deployments (case-sensitive).
1887    pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1888        get_type(self.clone())
1889    }
1890
1891    pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1892        realpath_with_links(self.clone())
1893    }
1894}
1895
1896#[derive(Clone, Debug)]
1897#[turbo_tasks::value(shared)]
1898pub struct RealPathResult {
1899    pub path_result: Result<FileSystemPath, RealPathResultError>,
1900    pub symlinks: Vec<FileSystemPath>,
1901}
1902
1903/// Errors that can occur when resolving a path with symlinks.
1904/// Many of these can be transient conditions that might happen when package managers are running.
1905#[derive(Debug, Clone, Hash, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
1906pub enum RealPathResultError {
1907    TooManySymlinks,
1908    CycleDetected,
1909    Invalid,
1910    NotFound,
1911}
1912
1913impl RealPathResultError {
1914    /// Formats the error message
1915    pub async fn as_error_message(
1916        &self,
1917        orig: &FileSystemPath,
1918        result: &RealPathResult,
1919    ) -> Result<RcStr> {
1920        Ok(match self {
1921            RealPathResultError::TooManySymlinks => {
1922                let len = result.symlinks.len();
1923                turbofmt!("Symlink {orig} leads to too many other symlinks ({len} links)").await?
1924            }
1925            RealPathResultError::CycleDetected => {
1926                // symlinks is Vec<FileSystemPath> — format with Debug since
1927                // turbofmt can't resolve a whole Vec asynchronously.
1928                let symlinks_dbg = format!(
1929                    "{:?}",
1930                    result.symlinks.iter().map(|s| &s.path).collect::<Vec<_>>()
1931                );
1932                turbofmt!("Symlink {orig} is in a symlink loop: {symlinks_dbg}").await?
1933            }
1934            RealPathResultError::Invalid => {
1935                turbofmt!("Symlink {orig} is invalid, it points out of the filesystem root").await?
1936            }
1937            RealPathResultError::NotFound => {
1938                turbofmt!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1939                    .await?
1940            }
1941        })
1942    }
1943}
1944
1945#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
1946#[turbo_tasks::value(shared)]
1947pub enum Permissions {
1948    Readable,
1949    #[default]
1950    Writable,
1951    Executable,
1952}
1953
1954// Only handle the permissions on unix platform for now
1955
1956#[cfg(unix)]
1957impl From<Permissions> for std::fs::Permissions {
1958    fn from(perm: Permissions) -> Self {
1959        use std::os::unix::fs::PermissionsExt;
1960        match perm {
1961            Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1962            Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1963            Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1964        }
1965    }
1966}
1967
1968#[cfg(unix)]
1969impl From<std::fs::Permissions> for Permissions {
1970    fn from(perm: std::fs::Permissions) -> Self {
1971        use std::os::unix::fs::PermissionsExt;
1972        if perm.readonly() {
1973            Permissions::Readable
1974        } else {
1975            // https://github.com/fitzgen/is_executable/blob/master/src/lib.rs#L96
1976            if perm.mode() & 0o111 != 0 {
1977                Permissions::Executable
1978            } else {
1979                Permissions::Writable
1980            }
1981        }
1982    }
1983}
1984
1985#[cfg(not(unix))]
1986impl From<std::fs::Permissions> for Permissions {
1987    fn from(_: std::fs::Permissions) -> Self {
1988        Permissions::default()
1989    }
1990}
1991
1992#[turbo_tasks::value(shared, serialization = "hash")]
1993#[derive(Clone, Debug, PartialOrd, Ord)]
1994pub enum FileContent {
1995    Content(File),
1996    NotFound,
1997}
1998
1999impl From<File> for FileContent {
2000    fn from(file: File) -> Self {
2001        FileContent::Content(file)
2002    }
2003}
2004
2005/// A persisted version of [`FileContent`] that stores the full file content in the task cache.
2006///
2007/// [`FileContent`] uses `serialization = "hash"`, so only a hash is kept in the persistent cache.
2008/// When reading the file content back from the cache, the hash is compared to detect changes, but
2009/// the actual data is not available. `PersistedFileContent` provides the full data so that
2010/// [`DiskFileSystem::write`] can retrieve it without re-reading from disk.
2011#[turbo_tasks::value(shared)]
2012#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
2013pub enum PersistedFileContent {
2014    Content(File),
2015    NotFound,
2016}
2017
2018impl PersistedFileContent {
2019    /// Performs a comparison of self's data against a disk file's streamed read.
2020    async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
2021        let old_file =
2022            extract_disk_access(retry_blocking(|| std::fs::File::open(path)).await, path)?;
2023        let Some(old_file) = old_file else {
2024            return Ok(match self {
2025                PersistedFileContent::NotFound => FileComparison::Equal,
2026                _ => FileComparison::Create,
2027            });
2028        };
2029        // We know old file exists, does the new file?
2030        let PersistedFileContent::Content(new_file) = self else {
2031            return Ok(FileComparison::NotEqual);
2032        };
2033
2034        let old_meta = extract_disk_access(retry_blocking(|| old_file.metadata()).await, path)?;
2035        let Some(old_meta) = old_meta else {
2036            // If we failed to get meta, then the old file has been deleted between the
2037            // handle open. In which case, we just pretend the file never existed.
2038            return Ok(FileComparison::Create);
2039        };
2040        // If the meta is different, we need to rewrite the file to update it.
2041        if new_file.meta != old_meta.into() {
2042            return Ok(FileComparison::NotEqual);
2043        }
2044
2045        // So meta matches, and we have a file handle. Let's stream the contents to see
2046        // if they match.
2047        let mut new_contents = new_file.read();
2048        let mut old_contents = BufReader::new(old_file);
2049        Ok(loop {
2050            let new_chunk = new_contents.fill_buf()?;
2051            let Ok(old_chunk) = old_contents.fill_buf() else {
2052                break FileComparison::NotEqual;
2053            };
2054
2055            let len = min(new_chunk.len(), old_chunk.len());
2056            if len == 0 {
2057                if new_chunk.len() == old_chunk.len() {
2058                    break FileComparison::Equal;
2059                } else {
2060                    break FileComparison::NotEqual;
2061                }
2062            }
2063
2064            if new_chunk[0..len] != old_chunk[0..len] {
2065                break FileComparison::NotEqual;
2066            }
2067
2068            new_contents.consume(len);
2069            old_contents.consume(len);
2070        })
2071    }
2072}
2073
2074#[derive(Clone, Debug, Eq, PartialEq)]
2075enum FileComparison {
2076    Create,
2077    Equal,
2078    NotEqual,
2079}
2080
2081bitflags! {
2082  #[derive(
2083    Default,
2084    TraceRawVcs,
2085    NonLocalValue,
2086    DeterministicHash,
2087    Encode,
2088    Decode,
2089  )]
2090  pub struct LinkType: u8 {
2091      const DIRECTORY = 0b00000001;
2092      const ABSOLUTE = 0b00000010;
2093  }
2094}
2095
2096/// The contents of a symbolic link. On Windows, this may be a junction point.
2097///
2098/// When reading, we treat symbolic links and junction points on Windows as equivalent. When
2099/// creating a new link, we always create junction points, because symlink creation may fail if
2100/// Windows "developer mode" is not enabled and we're running in an unprivileged environment.
2101#[turbo_tasks::value(shared)]
2102#[derive(Debug, DeterministicHash)]
2103pub enum LinkContent {
2104    /// A valid symbolic link pointing to `target`.
2105    ///
2106    /// When reading a relative link, the target is raw value read from the link.
2107    ///
2108    /// When reading an absolute link, the target is stripped of the root path while reading. This
2109    /// ensures we don't store absolute paths inside of the persistent cache.
2110    ///
2111    /// We don't use the [`FileSystemPath`] to store the target, because the [`FileSystemPath`] is
2112    /// always normalized. In [`FileSystemPath::write_symbolic_link_dir`] we need to compare
2113    /// `target` with the value returned by [`std::fs::read_link`].
2114    Link {
2115        target: RcStr,
2116        link_type: LinkType,
2117    },
2118    // Invalid means the link is invalid it points out of the filesystem root
2119    Invalid,
2120    // The target was not found
2121    NotFound,
2122}
2123
2124#[turbo_tasks::value(shared)]
2125#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
2126pub struct File {
2127    #[turbo_tasks(debug_ignore)]
2128    content: Rope,
2129    meta: FileMeta,
2130}
2131
2132impl File {
2133    /// Reads a [File] from the given path
2134    fn from_path(p: &Path) -> io::Result<Self> {
2135        let mut file = std::fs::File::open(p)?;
2136        let metadata = file.metadata()?;
2137
2138        let mut output = Vec::with_capacity(metadata.len() as usize);
2139        file.read_to_end(&mut output)?;
2140
2141        Ok(File {
2142            meta: metadata.into(),
2143            content: Rope::from(output),
2144        })
2145    }
2146
2147    /// Creates a [File] from raw bytes.
2148    fn from_bytes(content: Vec<u8>) -> Self {
2149        File {
2150            meta: FileMeta::default(),
2151            content: Rope::from(content),
2152        }
2153    }
2154
2155    /// Creates a [File] from a rope.
2156    fn from_rope(content: Rope) -> Self {
2157        File {
2158            meta: FileMeta::default(),
2159            content,
2160        }
2161    }
2162
2163    /// Returns the content type associated with this file.
2164    pub fn content_type(&self) -> Option<&Mime> {
2165        self.meta.content_type.as_ref()
2166    }
2167
2168    /// Sets the content type associated with this file.
2169    pub fn with_content_type(mut self, content_type: Mime) -> Self {
2170        self.meta.content_type = Some(content_type);
2171        self
2172    }
2173
2174    /// Returns a Read/AsyncRead/Stream/Iterator to access the File's contents.
2175    pub fn read(&self) -> RopeReader<'_> {
2176        self.content.read()
2177    }
2178}
2179
2180impl Debug for File {
2181    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2182        f.debug_struct("File")
2183            .field("meta", &self.meta)
2184            .field("content (hash)", &hash_xxh3_hash64(&self.content))
2185            .finish()
2186    }
2187}
2188
2189impl From<RcStr> for File {
2190    fn from(s: RcStr) -> Self {
2191        s.into_owned().into()
2192    }
2193}
2194
2195impl From<String> for File {
2196    fn from(s: String) -> Self {
2197        File::from_bytes(s.into_bytes())
2198    }
2199}
2200
2201impl From<ReadRef<RcStr>> for File {
2202    fn from(s: ReadRef<RcStr>) -> Self {
2203        File::from_bytes(s.as_bytes().to_vec())
2204    }
2205}
2206
2207impl From<&str> for File {
2208    fn from(s: &str) -> Self {
2209        File::from_bytes(s.as_bytes().to_vec())
2210    }
2211}
2212
2213impl From<Vec<u8>> for File {
2214    fn from(bytes: Vec<u8>) -> Self {
2215        File::from_bytes(bytes)
2216    }
2217}
2218
2219impl From<&[u8]> for File {
2220    fn from(bytes: &[u8]) -> Self {
2221        File::from_bytes(bytes.to_vec())
2222    }
2223}
2224
2225impl From<ReadRef<Rope>> for File {
2226    fn from(rope: ReadRef<Rope>) -> Self {
2227        File::from_rope(ReadRef::into_owned(rope))
2228    }
2229}
2230
2231impl From<Rope> for File {
2232    fn from(rope: Rope) -> Self {
2233        File::from_rope(rope)
2234    }
2235}
2236
2237impl File {
2238    pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
2239        Self {
2240            meta,
2241            content: Rope::from(content),
2242        }
2243    }
2244
2245    /// Returns the associated [FileMeta] of this file.
2246    pub fn meta(&self) -> &FileMeta {
2247        &self.meta
2248    }
2249
2250    /// Returns the immutable contents of this file.
2251    pub fn content(&self) -> &Rope {
2252        &self.content
2253    }
2254}
2255
2256#[turbo_tasks::value(shared)]
2257#[derive(Debug, Clone, Default)]
2258pub struct FileMeta {
2259    // Size of the file
2260    // len: u64,
2261    permissions: Permissions,
2262    #[bincode(with = "turbo_bincode::mime_option")]
2263    #[turbo_tasks(trace_ignore)]
2264    content_type: Option<Mime>,
2265}
2266
2267impl Ord for FileMeta {
2268    fn cmp(&self, other: &Self) -> Ordering {
2269        self.permissions
2270            .cmp(&other.permissions)
2271            .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2272    }
2273}
2274
2275impl PartialOrd for FileMeta {
2276    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2277        Some(self.cmp(other))
2278    }
2279}
2280
2281impl From<std::fs::Metadata> for FileMeta {
2282    fn from(meta: std::fs::Metadata) -> Self {
2283        let permissions = meta.permissions().into();
2284
2285        Self {
2286            permissions,
2287            content_type: None,
2288        }
2289    }
2290}
2291
2292impl DeterministicHash for FileMeta {
2293    fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2294        self.permissions.deterministic_hash(state);
2295        if let Some(content_type) = &self.content_type {
2296            content_type.to_string().deterministic_hash(state);
2297        }
2298    }
2299}
2300
2301impl FileContent {
2302    pub fn new(file: File) -> Self {
2303        FileContent::Content(file)
2304    }
2305
2306    pub fn is_content(&self) -> bool {
2307        matches!(self, FileContent::Content(_))
2308    }
2309
2310    pub fn as_content(&self) -> Option<&File> {
2311        match self {
2312            FileContent::Content(file) => Some(file),
2313            FileContent::NotFound => None,
2314        }
2315    }
2316
2317    pub fn parse_json_ref(&self) -> FileJsonContent {
2318        match self {
2319            FileContent::Content(file) => {
2320                let content = file.content.clone().into_bytes();
2321                let de = &mut serde_json::Deserializer::from_slice(&content);
2322                match serde_path_to_error::deserialize(de) {
2323                    Ok(data) => FileJsonContent::Content(data),
2324                    Err(e) => FileJsonContent::Unparsable(Box::new(
2325                        UnparsableJson::from_serde_path_to_error(e),
2326                    )),
2327                }
2328            }
2329            FileContent::NotFound => FileJsonContent::NotFound,
2330        }
2331    }
2332
2333    pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2334        match self {
2335            FileContent::Content(file) => match file.content.to_str() {
2336                Ok(string) => match parse_to_serde_value(
2337                    &string,
2338                    &ParseOptions {
2339                        allow_comments: true,
2340                        allow_trailing_commas: true,
2341                        allow_loose_object_property_names: false,
2342                    },
2343                ) {
2344                    Ok(data) => match data {
2345                        Some(value) => FileJsonContent::Content(value),
2346                        None => FileJsonContent::unparsable(rcstr!(
2347                            "text content doesn't contain any json data"
2348                        )),
2349                    },
2350                    Err(e) => FileJsonContent::Unparsable(Box::new(
2351                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
2352                    )),
2353                },
2354                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2355            },
2356            FileContent::NotFound => FileJsonContent::NotFound,
2357        }
2358    }
2359
2360    pub fn parse_json5_ref(&self) -> FileJsonContent {
2361        match self {
2362            FileContent::Content(file) => match file.content.to_str() {
2363                Ok(string) => match parse_to_serde_value(
2364                    &string,
2365                    &ParseOptions {
2366                        allow_comments: true,
2367                        allow_trailing_commas: true,
2368                        allow_loose_object_property_names: true,
2369                    },
2370                ) {
2371                    Ok(data) => match data {
2372                        Some(value) => FileJsonContent::Content(value),
2373                        None => FileJsonContent::unparsable(rcstr!(
2374                            "text content doesn't contain any json data"
2375                        )),
2376                    },
2377                    Err(e) => FileJsonContent::Unparsable(Box::new(
2378                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
2379                    )),
2380                },
2381                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2382            },
2383            FileContent::NotFound => FileJsonContent::NotFound,
2384        }
2385    }
2386
2387    pub fn lines_ref(&self) -> FileLinesContent {
2388        match self {
2389            FileContent::Content(file) => match file.content.to_str() {
2390                Ok(string) => {
2391                    let mut bytes_offset = 0;
2392                    FileLinesContent::Lines(
2393                        string
2394                            .split('\n')
2395                            .map(|l| {
2396                                let line = FileLine {
2397                                    content: l.to_string(),
2398                                    bytes_offset,
2399                                };
2400                                bytes_offset += (l.len() + 1) as u32;
2401                                line
2402                            })
2403                            .collect(),
2404                    )
2405                }
2406                Err(_) => FileLinesContent::Unparsable,
2407            },
2408            FileContent::NotFound => FileLinesContent::NotFound,
2409        }
2410    }
2411}
2412
2413#[turbo_tasks::value_impl]
2414impl FileContent {
2415    #[turbo_tasks::function]
2416    pub fn len(&self) -> Result<Vc<Option<u64>>> {
2417        Ok(Vc::cell(match self {
2418            FileContent::Content(file) => Some(file.content.len() as u64),
2419            FileContent::NotFound => None,
2420        }))
2421    }
2422
2423    #[turbo_tasks::function]
2424    pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2425        Ok(self.parse_json_ref().cell())
2426    }
2427
2428    #[turbo_tasks::function]
2429    pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
2430        self.parse_json_with_comments_ref().cell()
2431    }
2432
2433    #[turbo_tasks::function]
2434    pub fn parse_json5(&self) -> Vc<FileJsonContent> {
2435        self.parse_json5_ref().cell()
2436    }
2437
2438    #[turbo_tasks::function]
2439    pub fn lines(&self) -> Vc<FileLinesContent> {
2440        self.lines_ref().cell()
2441    }
2442
2443    #[turbo_tasks::function]
2444    pub async fn hash(&self) -> Result<Vc<u64>> {
2445        Ok(Vc::cell(hash_xxh3_hash64(self)))
2446    }
2447
2448    /// Converts this [`FileContent`] into a [`PersistedFileContent`] by cloning.
2449    ///
2450    /// Use this in contexts where the full file content must be serialized to the persistent
2451    /// task cache (e.g., in [`DiskFileSystem::write`]).
2452    #[turbo_tasks::function]
2453    pub fn persist(&self) -> Vc<PersistedFileContent> {
2454        match self {
2455            FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
2456            FileContent::NotFound => PersistedFileContent::NotFound.cell(),
2457        }
2458    }
2459
2460    /// Compared to [FileContent::hash], this hashes only the bytes of the file content and
2461    /// nothing else, returning `None` if the file does not exist.
2462    ///
2463    /// If `salt` is non-empty it is written into the hasher before the file bytes in a single
2464    /// pass. An empty salt produces the same result as hashing without a prefix.
2465    #[turbo_tasks::function]
2466    pub async fn content_hash(
2467        &self,
2468        salt: Vc<RcStr>,
2469        algorithm: HashAlgorithm,
2470    ) -> Result<Vc<Option<RcStr>>> {
2471        match self {
2472            FileContent::Content(file) => Ok(Vc::cell(Some(
2473                deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
2474            ))),
2475            FileContent::NotFound => Ok(Vc::cell(None)),
2476        }
2477    }
2478}
2479
2480/// A file's content interpreted as a JSON value.
2481#[turbo_tasks::value(shared, serialization = "skip")]
2482pub enum FileJsonContent {
2483    Content(Value),
2484    Unparsable(Box<UnparsableJson>),
2485    NotFound,
2486}
2487
2488#[turbo_tasks::value_impl]
2489impl ValueToString for FileJsonContent {
2490    /// Returns the JSON file content as a UTF-8 string.
2491    ///
2492    /// This operation will only succeed if the file contents are a valid JSON
2493    /// value.
2494    #[turbo_tasks::function]
2495    fn to_string(&self) -> Result<Vc<RcStr>> {
2496        match self {
2497            FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2498            FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2499            FileJsonContent::NotFound => bail!("File not found"),
2500        }
2501    }
2502}
2503
2504#[turbo_tasks::value_impl]
2505impl FileJsonContent {
2506    #[turbo_tasks::function]
2507    pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2508        match &*self.await? {
2509            FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2510            FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
2511            FileJsonContent::NotFound => bail!("File not found"),
2512        }
2513    }
2514}
2515impl FileJsonContent {
2516    pub fn unparsable(message: RcStr) -> Self {
2517        FileJsonContent::Unparsable(Box::new(UnparsableJson {
2518            message,
2519            path: None,
2520            start_location: None,
2521            end_location: None,
2522        }))
2523    }
2524
2525    pub fn unparsable_with_message(message: RcStr) -> Self {
2526        FileJsonContent::Unparsable(Box::new(UnparsableJson {
2527            message,
2528            path: None,
2529            start_location: None,
2530            end_location: None,
2531        }))
2532    }
2533}
2534
2535#[derive(Debug, PartialEq, Eq)]
2536pub struct FileLine {
2537    pub content: String,
2538    pub bytes_offset: u32,
2539}
2540
2541impl FileLine {
2542    pub fn len(&self) -> usize {
2543        self.content.len()
2544    }
2545
2546    #[must_use]
2547    pub fn is_empty(&self) -> bool {
2548        self.len() == 0
2549    }
2550}
2551
2552#[turbo_tasks::value(shared, serialization = "skip")]
2553pub enum FileLinesContent {
2554    Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2555    Unparsable,
2556    NotFound,
2557}
2558
2559#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2560pub enum RawDirectoryEntry {
2561    File,
2562    Directory,
2563    Symlink,
2564    // Other just means 'not a file, directory, or symlink'
2565    Other,
2566}
2567
2568#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2569pub enum DirectoryEntry {
2570    File(FileSystemPath),
2571    Directory(FileSystemPath),
2572    Symlink(FileSystemPath),
2573    Other(FileSystemPath),
2574    Error(RcStr),
2575}
2576
2577impl DirectoryEntry {
2578    /// Handles the `DirectoryEntry::Symlink` variant by checking the symlink target
2579    /// type and replacing it with `DirectoryEntry::File` or
2580    /// `DirectoryEntry::Directory`.
2581    pub async fn resolve_symlink(self) -> Result<Self> {
2582        if let DirectoryEntry::Symlink(symlink) = &self {
2583            let result = &*symlink.realpath_with_links().await?;
2584            let real_path = match &result.path_result {
2585                Ok(path) => path,
2586                Err(error) => {
2587                    return Ok(DirectoryEntry::Error(
2588                        error.as_error_message(symlink, result).await?,
2589                    ));
2590                }
2591            };
2592            Ok(match *real_path.get_type().await? {
2593                FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2594                FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2595                // Happens if the link is to a non-existent file
2596                FileSystemEntryType::NotFound => DirectoryEntry::Error(
2597                    turbofmt!("Symlink {symlink} points at {real_path} which does not exist")
2598                        .await?,
2599                ),
2600                // This is caused by eventual consistency
2601                FileSystemEntryType::Symlink => turbobail!(
2602                    "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2603                ),
2604                _ => self,
2605            })
2606        } else {
2607            Ok(self)
2608        }
2609    }
2610
2611    pub fn path(self) -> Option<FileSystemPath> {
2612        match self {
2613            DirectoryEntry::File(path)
2614            | DirectoryEntry::Directory(path)
2615            | DirectoryEntry::Symlink(path)
2616            | DirectoryEntry::Other(path) => Some(path),
2617            DirectoryEntry::Error(_) => None,
2618        }
2619    }
2620}
2621
2622#[turbo_tasks::value]
2623#[derive(Hash, Clone, Copy, Debug)]
2624pub enum FileSystemEntryType {
2625    NotFound,
2626    File,
2627    Directory,
2628    Symlink,
2629    /// These would be things like named pipes, sockets, etc.
2630    Other,
2631    Error,
2632}
2633
2634impl From<FileType> for FileSystemEntryType {
2635    fn from(file_type: FileType) -> Self {
2636        match file_type {
2637            t if t.is_dir() => FileSystemEntryType::Directory,
2638            t if t.is_file() => FileSystemEntryType::File,
2639            t if t.is_symlink() => FileSystemEntryType::Symlink,
2640            _ => FileSystemEntryType::Other,
2641        }
2642    }
2643}
2644
2645impl From<DirectoryEntry> for FileSystemEntryType {
2646    fn from(entry: DirectoryEntry) -> Self {
2647        FileSystemEntryType::from(&entry)
2648    }
2649}
2650
2651impl From<&DirectoryEntry> for FileSystemEntryType {
2652    fn from(entry: &DirectoryEntry) -> Self {
2653        match entry {
2654            DirectoryEntry::File(_) => FileSystemEntryType::File,
2655            DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2656            DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2657            DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2658            DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2659        }
2660    }
2661}
2662
2663impl From<RawDirectoryEntry> for FileSystemEntryType {
2664    fn from(entry: RawDirectoryEntry) -> Self {
2665        FileSystemEntryType::from(&entry)
2666    }
2667}
2668
2669impl From<&RawDirectoryEntry> for FileSystemEntryType {
2670    fn from(entry: &RawDirectoryEntry) -> Self {
2671        match entry {
2672            RawDirectoryEntry::File => FileSystemEntryType::File,
2673            RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2674            RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2675            RawDirectoryEntry::Other => FileSystemEntryType::Other,
2676        }
2677    }
2678}
2679
2680#[turbo_tasks::value]
2681#[derive(Debug)]
2682pub enum RawDirectoryContent {
2683    // The entry keys are the directory relative file names
2684    // e.g. for `/bar/foo`, it will be `foo`
2685    Entries(AutoMap<RcStr, RawDirectoryEntry>),
2686    NotFound,
2687}
2688
2689impl RawDirectoryContent {
2690    pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2691        Self::cell(RawDirectoryContent::Entries(entries))
2692    }
2693
2694    pub fn not_found() -> Vc<Self> {
2695        Self::cell(RawDirectoryContent::NotFound)
2696    }
2697}
2698
2699#[turbo_tasks::value]
2700#[derive(Debug)]
2701pub enum DirectoryContent {
2702    Entries(AutoMap<RcStr, DirectoryEntry>),
2703    NotFound,
2704}
2705
2706impl DirectoryContent {
2707    pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2708        Self::cell(DirectoryContent::Entries(entries))
2709    }
2710
2711    pub fn not_found() -> Vc<Self> {
2712        Self::cell(DirectoryContent::NotFound)
2713    }
2714}
2715
2716#[derive(ValueToString)]
2717#[value_to_string("null")]
2718#[turbo_tasks::value(shared)]
2719pub struct NullFileSystem;
2720
2721#[turbo_tasks::value_impl]
2722impl FileSystem for NullFileSystem {
2723    #[turbo_tasks::function]
2724    fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2725        FileContent::NotFound.cell()
2726    }
2727
2728    #[turbo_tasks::function]
2729    fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2730        LinkContent::NotFound.cell()
2731    }
2732
2733    #[turbo_tasks::function]
2734    fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2735        RawDirectoryContent::not_found()
2736    }
2737
2738    #[turbo_tasks::function]
2739    fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) {}
2740
2741    #[turbo_tasks::function]
2742    fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) {}
2743
2744    #[turbo_tasks::function]
2745    fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2746        FileMeta::default().cell()
2747    }
2748}
2749
2750pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2751    loop {
2752        if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2753            path = fs.get_inner_fs_path(path).owned().await?;
2754            continue;
2755        }
2756
2757        if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2758            let sys_path = fs.await?.to_sys_path(&path);
2759            return Ok(Some(sys_path));
2760        }
2761
2762        return Ok(None);
2763    }
2764}
2765
2766#[turbo_tasks::function]
2767async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2768    let fs = path.fs().to_resolved().await?;
2769    match &*fs.raw_read_dir(path.clone()).await? {
2770        RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2771        RawDirectoryContent::Entries(entries) => {
2772            let mut normalized_entries = AutoMap::new();
2773            let dir_path = &path.path;
2774            for (name, entry) in entries {
2775                // Construct the path directly instead of going through `join`.
2776                // We do not need to normalize since the `name` is guaranteed to be a simple
2777                // path segment.
2778                let path = if dir_path.is_empty() {
2779                    name.clone()
2780                } else {
2781                    RcStr::from(format!("{dir_path}/{name}"))
2782                };
2783
2784                let entry_path = FileSystemPath::new_normalized(fs, path);
2785                let entry = match entry {
2786                    RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2787                    RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2788                    RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2789                    RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2790                };
2791                normalized_entries.insert(name.clone(), entry);
2792            }
2793            Ok(DirectoryContent::new(normalized_entries))
2794        }
2795    }
2796}
2797
2798#[turbo_tasks::function]
2799async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2800    if path.is_root() {
2801        return Ok(FileSystemEntryType::Directory.cell());
2802    }
2803    let parent = path.parent();
2804    let dir_content = parent.raw_read_dir().await?;
2805    match &*dir_content {
2806        RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2807        RawDirectoryContent::Entries(entries) => {
2808            let (_, file_name) = path.split_file_name();
2809            if let Some(entry) = entries.get(file_name) {
2810                Ok(FileSystemEntryType::from(entry).cell())
2811            } else {
2812                Ok(FileSystemEntryType::NotFound.cell())
2813            }
2814        }
2815    }
2816}
2817
2818#[turbo_tasks::function]
2819async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2820    let mut current_path = path;
2821    let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2822    let mut visited: AutoSet<RcStr> = AutoSet::new();
2823    let mut error = RealPathResultError::TooManySymlinks;
2824    // Pick some arbitrary symlink depth limit... similar to the ELOOP logic for realpath(3).
2825    // SYMLOOP_MAX is 40 for Linux: https://unix.stackexchange.com/q/721724
2826    for _i in 0..40 {
2827        if current_path.is_root() {
2828            // fast path
2829            return Ok(RealPathResult {
2830                path_result: Ok(current_path),
2831                symlinks: symlinks.into_iter().collect(),
2832            }
2833            .cell());
2834        }
2835
2836        if !visited.insert(current_path.path.clone()) {
2837            error = RealPathResultError::CycleDetected;
2838            break; // we detected a cycle
2839        }
2840
2841        // see if a parent segment of the path is a symlink and resolve that first
2842        let parent = current_path.parent();
2843        let parent_result = parent.realpath_with_links().owned().await?;
2844        let basename = current_path
2845            .path
2846            .rsplit_once('/')
2847            .map_or(current_path.path.as_str(), |(_, name)| name);
2848        symlinks.extend(parent_result.symlinks);
2849        let parent_path = match parent_result.path_result {
2850            Ok(path) => {
2851                if path != parent {
2852                    current_path = path.join(basename)?;
2853                }
2854                path
2855            }
2856            Err(parent_error) => {
2857                error = parent_error;
2858                break;
2859            }
2860        };
2861
2862        // use `get_type` before trying `read_link`, as there's a good chance of a cache hit on
2863        // `get_type`, and `read_link` isn't the common codepath.
2864        if !matches!(
2865            *current_path.get_type().await?,
2866            FileSystemEntryType::Symlink
2867        ) {
2868            return Ok(RealPathResult {
2869                path_result: Ok(current_path),
2870                symlinks: symlinks.into_iter().collect(), // convert set to vec
2871            }
2872            .cell());
2873        }
2874
2875        match &*current_path.read_link().await? {
2876            LinkContent::Link { target, link_type } => {
2877                symlinks.insert(current_path.clone());
2878                current_path = if link_type.contains(LinkType::ABSOLUTE) {
2879                    current_path.root().owned().await?
2880                } else {
2881                    parent_path
2882                }
2883                .join(target)?;
2884            }
2885            LinkContent::NotFound => {
2886                error = RealPathResultError::NotFound;
2887                break;
2888            }
2889            LinkContent::Invalid => {
2890                error = RealPathResultError::Invalid;
2891                break;
2892            }
2893        }
2894    }
2895
2896    // Too many attempts or detected a cycle, we bailed out!
2897    //
2898    // TODO: There's no proper way to indicate an non-turbo-tasks error here, so just return the
2899    // original path and all the symlinks we followed.
2900    //
2901    // Returning the followed symlinks is still important, even if there is an error! Otherwise
2902    // we may never notice if the symlink loop is fixed.
2903    Ok(RealPathResult {
2904        path_result: Err(error),
2905        symlinks: symlinks.into_iter().collect(),
2906    }
2907    .cell())
2908}
2909
2910/// Wrapper to convert [`anyhow::Error`] to `impl std::error::Error` for use in [`Effect::apply`].
2911// TODO(bgw): use a structured error type instead of anyhow for write/write_link
2912#[derive(TraceRawVcs, NonLocalValue)]
2913pub(crate) struct AnyhowWrapper(anyhow::Error);
2914
2915impl fmt::Display for AnyhowWrapper {
2916    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2917        fmt::Display::fmt(&self.0, f)
2918    }
2919}
2920
2921impl fmt::Debug for AnyhowWrapper {
2922    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2923        fmt::Debug::fmt(&self.0, f)
2924    }
2925}
2926
2927impl StdError for AnyhowWrapper {
2928    fn source(&self) -> Option<&(dyn StdError + 'static)> {
2929        self.0.source()
2930    }
2931}
2932
2933impl From<anyhow::Error> for AnyhowWrapper {
2934    fn from(err: anyhow::Error) -> Self {
2935        AnyhowWrapper(err)
2936    }
2937}
2938
2939#[cfg(test)]
2940mod tests {
2941    use turbo_rcstr::rcstr;
2942    use turbo_tasks::{Effects, OperationVc, Vc, take_effects};
2943    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2944
2945    use super::*;
2946
2947    #[turbo_tasks::function(operation)]
2948    async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
2949        let _ = op.resolve().strongly_consistent().await?;
2950        Ok(take_effects(op).await?.cell())
2951    }
2952
2953    #[test]
2954    fn test_get_relative_path_to() {
2955        assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2956        assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2957        assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2958        assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2959        assert_eq!(
2960            get_relative_path_to("a/b/c", "c/b/a").as_str(),
2961            "../../../c/b/a"
2962        );
2963        assert_eq!(
2964            get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2965            "../../../c/b/a"
2966        );
2967    }
2968
2969    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2970    async fn with_extension() {
2971        turbo_tasks_testing::VcStorage::with(async {
2972            let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2973                .to_resolved()
2974                .await?;
2975
2976            let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2977
2978            let path_json = path_txt.with_extension("json");
2979            assert_eq!(&*path_json.path, "foo/bar.json");
2980
2981            let path_no_ext = path_txt.with_extension("");
2982            assert_eq!(&*path_no_ext.path, "foo/bar");
2983
2984            let path_new_ext = path_no_ext.with_extension("json");
2985            assert_eq!(&*path_new_ext.path, "foo/bar.json");
2986
2987            let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2988
2989            let path_no_slash_json = path_no_slash_txt.with_extension("json");
2990            assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2991
2992            let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2993            assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2994
2995            let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2996            assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2997
2998            anyhow::Ok(())
2999        })
3000        .await
3001        .unwrap()
3002    }
3003
3004    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3005    async fn file_stem() {
3006        turbo_tasks_testing::VcStorage::with(async {
3007            let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
3008                .to_resolved()
3009                .await?;
3010
3011            let path = FileSystemPath::new_normalized(fs, rcstr!(""));
3012            assert_eq!(path.file_stem(), None);
3013
3014            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
3015            assert_eq!(path.file_stem(), Some("bar"));
3016
3017            let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
3018            assert_eq!(path.file_stem(), Some("bar"));
3019
3020            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
3021            assert_eq!(path.file_stem(), Some("bar"));
3022
3023            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
3024            assert_eq!(path.file_stem(), Some(".bar"));
3025
3026            anyhow::Ok(())
3027        })
3028        .await
3029        .unwrap()
3030    }
3031
3032    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3033    async fn test_try_from_sys_path() {
3034        let sys_root = if cfg!(windows) {
3035            Path::new(r"C:\fake\root")
3036        } else {
3037            Path::new(r"/fake/root")
3038        };
3039
3040        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3041            BackendOptions::default(),
3042            noop_backing_storage(),
3043        ));
3044        tt.run_once(async {
3045            assert_try_from_sys_path_operation(RcStr::from(sys_root.to_str().unwrap()))
3046                .read_strongly_consistent()
3047                .await?;
3048
3049            anyhow::Ok(())
3050        })
3051        .await
3052        .unwrap();
3053    }
3054
3055    #[turbo_tasks::function(operation)]
3056    async fn assert_try_from_sys_path_operation(sys_root: RcStr) -> anyhow::Result<()> {
3057        let sys_root = Path::new(sys_root.as_str());
3058        let fs_vc = DiskFileSystem::new(
3059            rcstr!("temp"),
3060            Vc::cell(RcStr::from(sys_root.to_str().unwrap())),
3061        )
3062        .to_resolved()
3063        .await?;
3064        let fs = fs_vc.await?;
3065        let fs_root_path = fs_vc.root().await?;
3066
3067        assert_eq!(
3068            fs.try_from_sys_path(
3069                fs_vc,
3070                &Path::new("relative").join("directory"),
3071                /* relative_to */ None,
3072            )
3073            .unwrap()
3074            .path,
3075            "relative/directory"
3076        );
3077
3078        assert_eq!(
3079            fs.try_from_sys_path(
3080                fs_vc,
3081                &sys_root
3082                    .join("absolute")
3083                    .join("directory")
3084                    .join("..")
3085                    .join("normalized_path"),
3086                /* relative_to */ Some(&fs_root_path.join("ignored").unwrap()),
3087            )
3088            .unwrap()
3089            .path,
3090            "absolute/normalized_path"
3091        );
3092
3093        assert_eq!(
3094            fs.try_from_sys_path(
3095                fs_vc,
3096                Path::new("child"),
3097                /* relative_to */ Some(&fs_root_path.join("parent").unwrap()),
3098            )
3099            .unwrap()
3100            .path,
3101            "parent/child"
3102        );
3103
3104        assert_eq!(
3105            fs.try_from_sys_path(
3106                fs_vc,
3107                &Path::new("..").join("parallel_dir"),
3108                /* relative_to */ Some(&fs_root_path.join("parent").unwrap()),
3109            )
3110            .unwrap()
3111            .path,
3112            "parallel_dir"
3113        );
3114
3115        assert_eq!(
3116            fs.try_from_sys_path(
3117                fs_vc,
3118                &Path::new("relative")
3119                    .join("..")
3120                    .join("..")
3121                    .join("leaves_root"),
3122                /* relative_to */ None,
3123            ),
3124            None
3125        );
3126
3127        assert_eq!(
3128            fs.try_from_sys_path(
3129                fs_vc,
3130                &sys_root
3131                    .join("absolute")
3132                    .join("..")
3133                    .join("..")
3134                    .join("leaves_root"),
3135                /* relative_to */ None,
3136            ),
3137            None
3138        );
3139
3140        Ok(())
3141    }
3142
3143    #[cfg(test)]
3144    mod symlink_tests {
3145        use std::{
3146            fs::{File, create_dir_all, read_to_string},
3147            io::Write,
3148        };
3149
3150        use rand::{RngExt, SeedableRng};
3151        use turbo_rcstr::{RcStr, rcstr};
3152        use turbo_tasks::{ResolvedVc, Vc};
3153        use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3154
3155        use super::extract_effects_operation;
3156        use crate::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
3157
3158        #[turbo_tasks::function(operation)]
3159        async fn test_write_link_effect_operation(
3160            fs: ResolvedVc<DiskFileSystem>,
3161            path: FileSystemPath,
3162            target: RcStr,
3163        ) -> anyhow::Result<()> {
3164            let write_file = |f| {
3165                fs.write_link(
3166                    f,
3167                    LinkContent::Link {
3168                        target: format!("{target}/data.txt").into(),
3169                        link_type: LinkType::empty(),
3170                    }
3171                    .cell(),
3172                )
3173            };
3174            // Write it twice (same content)
3175            write_file(path.join("symlink-file")?).await?;
3176            write_file(path.join("symlink-file")?).await?;
3177
3178            let write_dir = |f| {
3179                fs.write_link(
3180                    f,
3181                    LinkContent::Link {
3182                        target: target.clone(),
3183                        link_type: LinkType::DIRECTORY,
3184                    }
3185                    .cell(),
3186                )
3187            };
3188            // Write it twice (same content)
3189            write_dir(path.join("symlink-dir")?).await?;
3190            write_dir(path.join("symlink-dir")?).await?;
3191
3192            Ok(())
3193        }
3194
3195        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3196        async fn test_write_link() {
3197            let scratch = tempfile::tempdir().unwrap();
3198            let path = scratch.path().to_owned();
3199
3200            create_dir_all(path.join("subdir-a")).unwrap();
3201            File::create_new(path.join("subdir-a/data.txt"))
3202                .unwrap()
3203                .write_all(b"foo")
3204                .unwrap();
3205            create_dir_all(path.join("subdir-b")).unwrap();
3206            File::create_new(path.join("subdir-b/data.txt"))
3207                .unwrap()
3208                .write_all(b"bar")
3209                .unwrap();
3210            let root = path.to_str().unwrap().into();
3211
3212            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3213                BackendOptions::default(),
3214                noop_backing_storage(),
3215            ));
3216
3217            tt.run_once(async move {
3218                let fs = disk_file_system_operation(root)
3219                    .resolve()
3220                    .strongly_consistent()
3221                    .await?;
3222                let root_path = disk_file_system_root(fs);
3223
3224                extract_effects_operation(test_write_link_effect_operation(
3225                    fs,
3226                    root_path.clone(),
3227                    rcstr!("subdir-a"),
3228                ))
3229                .read_strongly_consistent()
3230                .await?
3231                .apply()
3232                .await?;
3233
3234                assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
3235                assert_eq!(
3236                    read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3237                    "foo"
3238                );
3239
3240                // Write the same links again but with different targets
3241                extract_effects_operation(test_write_link_effect_operation(
3242                    fs,
3243                    root_path,
3244                    rcstr!("subdir-b"),
3245                ))
3246                .read_strongly_consistent()
3247                .await?
3248                .apply()
3249                .await?;
3250
3251                assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
3252                assert_eq!(
3253                    read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
3254                    "bar"
3255                );
3256
3257                anyhow::Ok(())
3258            })
3259            .await
3260            .unwrap();
3261        }
3262
3263        const STRESS_ITERATIONS: usize = 100;
3264        const STRESS_PARALLELISM: usize = 8;
3265        const STRESS_TARGET_COUNT: usize = 20;
3266        const STRESS_SYMLINK_COUNT: usize = 16;
3267
3268        #[turbo_tasks::function(operation)]
3269        fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
3270            DiskFileSystem::new(rcstr!("test"), Vc::cell(fs_root))
3271        }
3272
3273        fn disk_file_system_root(fs: ResolvedVc<DiskFileSystem>) -> FileSystemPath {
3274            FileSystemPath {
3275                fs: ResolvedVc::upcast(fs),
3276                path: rcstr!(""),
3277            }
3278        }
3279
3280        #[turbo_tasks::function(operation)]
3281        async fn write_symlink_stress_batch(
3282            fs: ResolvedVc<DiskFileSystem>,
3283            symlinks_dir: FileSystemPath,
3284            updates: Vec<(usize, usize)>,
3285        ) -> anyhow::Result<()> {
3286            use turbo_tasks::TryJoinIterExt;
3287
3288            updates
3289                .into_iter()
3290                .map(|(symlink_idx, target_idx)| {
3291                    let target = RcStr::from(format!("../_targets/{target_idx}"));
3292                    let symlink_path = symlinks_dir.join(&symlink_idx.to_string()).unwrap();
3293                    async move {
3294                        fs.write_link(
3295                            symlink_path,
3296                            LinkContent::Link {
3297                                target,
3298                                link_type: LinkType::DIRECTORY,
3299                            }
3300                            .cell(),
3301                        )
3302                        .await
3303                    }
3304                })
3305                .try_join()
3306                .await?;
3307            Ok(())
3308        }
3309
3310        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3311        async fn test_symlink_stress() {
3312            let scratch = tempfile::tempdir().unwrap();
3313            let path = scratch.path().to_owned();
3314
3315            let targets_dir = path.join("_targets");
3316            create_dir_all(&targets_dir).unwrap();
3317            for i in 0..STRESS_TARGET_COUNT {
3318                create_dir_all(targets_dir.join(i.to_string())).unwrap();
3319            }
3320            create_dir_all(path.join("_symlinks")).unwrap();
3321
3322            let root = RcStr::from(path.to_str().unwrap());
3323
3324            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3325                BackendOptions::default(),
3326                noop_backing_storage(),
3327            ));
3328
3329            tt.run_once(async move {
3330                let fs = disk_file_system_operation(root)
3331                    .resolve()
3332                    .strongly_consistent()
3333                    .await?;
3334                let root_path = disk_file_system_root(fs);
3335                let symlinks_dir = root_path.join("_symlinks")?;
3336
3337                let initial_updates: Vec<(usize, usize)> =
3338                    (0..STRESS_SYMLINK_COUNT).map(|i| (i, 0)).collect();
3339                extract_effects_operation(write_symlink_stress_batch(
3340                    fs,
3341                    symlinks_dir.clone(),
3342                    initial_updates,
3343                ))
3344                .read_strongly_consistent()
3345                .await?
3346                .apply()
3347                .await?;
3348
3349                let mut rng = rand::rngs::SmallRng::seed_from_u64(0);
3350                for _ in 0..STRESS_ITERATIONS {
3351                    let mut updates_map = rustc_hash::FxHashMap::default();
3352                    for _ in 0..STRESS_PARALLELISM {
3353                        let symlink_idx = rng.random_range(0..STRESS_SYMLINK_COUNT);
3354                        let target_idx = rng.random_range(0..STRESS_TARGET_COUNT);
3355                        updates_map.insert(symlink_idx, target_idx);
3356                    }
3357                    let updates: Vec<(usize, usize)> = updates_map.into_iter().collect();
3358
3359                    extract_effects_operation(write_symlink_stress_batch(
3360                        fs,
3361                        symlinks_dir.clone(),
3362                        updates,
3363                    ))
3364                    .read_strongly_consistent()
3365                    .await?
3366                    .apply()
3367                    .await?;
3368                }
3369
3370                anyhow::Ok(())
3371            })
3372            .await
3373            .unwrap();
3374
3375            tt.stop_and_wait().await;
3376        }
3377    }
3378
3379    // Tests helpers for denied_path tests
3380    #[cfg(test)]
3381    mod denied_path_tests {
3382        use std::{
3383            fs::{File, create_dir_all, read_to_string},
3384            io::Write,
3385            path::Path,
3386        };
3387
3388        use turbo_rcstr::{RcStr, rcstr};
3389        use turbo_tasks::{Effects, Vc, take_effects};
3390        use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
3391
3392        use crate::{
3393            DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
3394            FileSystemPath,
3395            glob::{Glob, GlobOptions},
3396        };
3397
3398        /// Helper to set up a test filesystem with denied_path
3399        /// Creates the filesystem structure on disk and returns paths
3400        fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
3401            let scratch = tempfile::tempdir().unwrap();
3402            let path = scratch.path();
3403
3404            // Create standard test structure:
3405            // /allowed_file.txt
3406            // /allowed_dir/file.txt
3407            // /other_file.txt
3408            // /denied_dir/secret.txt
3409            // /denied_dir/nested/deep.txt
3410            File::create_new(path.join("allowed_file.txt"))
3411                .unwrap()
3412                .write_all(b"allowed content")
3413                .unwrap();
3414
3415            create_dir_all(path.join("allowed_dir")).unwrap();
3416            File::create_new(path.join("allowed_dir/file.txt"))
3417                .unwrap()
3418                .write_all(b"allowed dir content")
3419                .unwrap();
3420
3421            File::create_new(path.join("other_file.txt"))
3422                .unwrap()
3423                .write_all(b"other content")
3424                .unwrap();
3425
3426            create_dir_all(path.join("denied_dir/nested")).unwrap();
3427            File::create_new(path.join("denied_dir/secret.txt"))
3428                .unwrap()
3429                .write_all(b"secret content")
3430                .unwrap();
3431            File::create_new(path.join("denied_dir/nested/deep.txt"))
3432                .unwrap()
3433                .write_all(b"deep secret")
3434                .unwrap();
3435
3436            let root = RcStr::from(path.to_str().unwrap());
3437            // denied_path should be relative to root, using unix separators
3438            let denied_path = rcstr!("denied_dir");
3439
3440            (scratch, root, denied_path)
3441        }
3442
3443        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3444        async fn test_denied_path_read() {
3445            #[turbo_tasks::function(operation)]
3446            async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3447                let fs = DiskFileSystem::new_with_denied_paths(
3448                    rcstr!("test"),
3449                    Vc::cell(root),
3450                    vec![denied_path],
3451                );
3452                let root_path = fs.root().await?;
3453
3454                // Test 1: Reading allowed file should work
3455                let allowed_file = root_path.join("allowed_file.txt")?;
3456                let content = allowed_file.read().await?;
3457                assert!(
3458                    matches!(&*content, FileContent::Content(_)),
3459                    "allowed file should be readable"
3460                );
3461
3462                // Test 2: Direct read of denied file should return NotFound
3463                let denied_file = root_path.join("denied_dir/secret.txt")?;
3464                let content = denied_file.read().await?;
3465                assert!(
3466                    matches!(&*content, FileContent::NotFound),
3467                    "denied file should return NotFound, got {:?}",
3468                    content
3469                );
3470
3471                // Test 3: Reading nested denied file should return NotFound
3472                let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
3473                let content = nested_denied.read().await?;
3474                assert!(
3475                    matches!(&*content, FileContent::NotFound),
3476                    "nested denied file should return NotFound"
3477                );
3478
3479                // Test 4: Reading the denied directory itself should return NotFound
3480                let denied_dir = root_path.join("denied_dir")?;
3481                let content = denied_dir.read().await?;
3482                assert!(
3483                    matches!(&*content, FileContent::NotFound),
3484                    "denied directory should return NotFound"
3485                );
3486
3487                Ok(())
3488            }
3489
3490            let (_scratch, root, denied_path) = setup_test_fs();
3491            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3492                BackendOptions::default(),
3493                noop_backing_storage(),
3494            ));
3495            tt.run_once(async {
3496                test_operation(root, denied_path)
3497                    .read_strongly_consistent()
3498                    .await?;
3499
3500                anyhow::Ok(())
3501            })
3502            .await
3503            .unwrap();
3504        }
3505
3506        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3507        async fn test_denied_path_read_dir() {
3508            #[turbo_tasks::function(operation)]
3509            async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3510                let fs = DiskFileSystem::new_with_denied_paths(
3511                    rcstr!("test"),
3512                    Vc::cell(root),
3513                    vec![denied_path],
3514                );
3515                let root_path = fs.root().await?;
3516
3517                // Test: read_dir on root should not include denied_dir
3518                let dir_content = root_path.read_dir().await?;
3519                match &*dir_content {
3520                    DirectoryContent::Entries(entries) => {
3521                        assert!(
3522                            entries.contains_key(&rcstr!("allowed_dir")),
3523                            "allowed_dir should be visible"
3524                        );
3525                        assert!(
3526                            entries.contains_key(&rcstr!("other_file.txt")),
3527                            "other_file.txt should be visible"
3528                        );
3529                        assert!(
3530                            entries.contains_key(&rcstr!("allowed_file.txt")),
3531                            "allowed_file.txt should be visible"
3532                        );
3533                        assert!(
3534                            !entries.contains_key(&rcstr!("denied_dir")),
3535                            "denied_dir should NOT be visible in read_dir"
3536                        );
3537                    }
3538                    DirectoryContent::NotFound => panic!("root directory should exist"),
3539                }
3540
3541                // Test: read_dir on denied_dir should return NotFound
3542                let denied_dir = root_path.join("denied_dir")?;
3543                let dir_content = denied_dir.read_dir().await?;
3544                assert!(
3545                    matches!(&*dir_content, DirectoryContent::NotFound),
3546                    "denied_dir read_dir should return NotFound"
3547                );
3548
3549                Ok(())
3550            }
3551
3552            let (_scratch, root, denied_path) = setup_test_fs();
3553            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3554                BackendOptions::default(),
3555                noop_backing_storage(),
3556            ));
3557            tt.run_once(async {
3558                test_operation(root, denied_path)
3559                    .read_strongly_consistent()
3560                    .await?;
3561
3562                anyhow::Ok(())
3563            })
3564            .await
3565            .unwrap();
3566        }
3567
3568        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3569        async fn test_denied_path_read_glob() {
3570            #[turbo_tasks::function(operation)]
3571            async fn test_operation(root: RcStr, denied_path: RcStr) -> anyhow::Result<()> {
3572                let fs = DiskFileSystem::new_with_denied_paths(
3573                    rcstr!("test"),
3574                    Vc::cell(root),
3575                    vec![denied_path],
3576                );
3577                let root_path = fs.root().await?;
3578
3579                // Test: read_glob with ** should not reveal denied files
3580                let glob_result = root_path
3581                    .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3582                    .await?;
3583
3584                // Check top level results
3585                assert!(
3586                    glob_result.results.contains_key("allowed_file.txt"),
3587                    "allowed_file.txt should be found"
3588                );
3589                assert!(
3590                    glob_result.results.contains_key("other_file.txt"),
3591                    "other_file.txt should be found"
3592                );
3593                assert!(
3594                    !glob_result.results.contains_key("denied_dir"),
3595                    "denied_dir should NOT appear in glob results"
3596                );
3597
3598                // Check that denied_dir doesn't appear in inner results
3599                assert!(
3600                    !glob_result.inner.contains_key("denied_dir"),
3601                    "denied_dir should NOT appear in glob inner results"
3602                );
3603
3604                // Verify allowed_dir is present (to ensure we're not filtering everything)
3605                assert!(
3606                    glob_result.inner.contains_key("allowed_dir"),
3607                    "allowed_dir directory should be present"
3608                );
3609                let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3610                assert!(
3611                    sub_inner.results.contains_key("file.txt"),
3612                    "allowed_dir/file.txt should be found"
3613                );
3614
3615                Ok(())
3616            }
3617
3618            let (_scratch, root, denied_path) = setup_test_fs();
3619            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3620                BackendOptions::default(),
3621                noop_backing_storage(),
3622            ));
3623            tt.run_once(async {
3624                test_operation(root, denied_path)
3625                    .read_strongly_consistent()
3626                    .await?;
3627
3628                anyhow::Ok(())
3629            })
3630            .await
3631            .unwrap();
3632        }
3633
3634        #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3635        async fn test_denied_path_write() {
3636            #[turbo_tasks::function(operation)]
3637            async fn write_file_operation(
3638                path: FileSystemPath,
3639                contents: RcStr,
3640            ) -> anyhow::Result<()> {
3641                path.write(
3642                    FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3643                        .cell(),
3644                )
3645                .await?;
3646                Ok(())
3647            }
3648
3649            /// Writes the allowed file and captures effects to be applied at
3650            /// the top level.
3651            #[turbo_tasks::function(operation)]
3652            async fn write_allowed_file_operation(
3653                root: RcStr,
3654                denied_path: RcStr,
3655                file_path: RcStr,
3656                contents: RcStr,
3657            ) -> anyhow::Result<Vc<Effects>> {
3658                let fs = DiskFileSystem::new_with_denied_paths(
3659                    rcstr!("test"),
3660                    Vc::cell(root),
3661                    vec![denied_path],
3662                );
3663                let root_path = fs.root().await?;
3664                let allowed_file = root_path.join(&file_path)?;
3665                let write_op = write_file_operation(allowed_file, contents);
3666                write_op.read_strongly_consistent().await?;
3667                Ok(take_effects(write_op).await?.cell())
3668            }
3669
3670            #[turbo_tasks::function(operation)]
3671            async fn test_denied_writes_operation(
3672                root: RcStr,
3673                denied_path: RcStr,
3674                denied_file: RcStr,
3675                nested_denied_file: RcStr,
3676            ) -> anyhow::Result<()> {
3677                let fs = DiskFileSystem::new_with_denied_paths(
3678                    rcstr!("test"),
3679                    Vc::cell(root),
3680                    vec![denied_path],
3681                );
3682                let root_path = fs.root().await?;
3683
3684                let path = root_path.join(&denied_file)?;
3685                let result = write_file_operation(path, rcstr!("forbidden"))
3686                    .read_strongly_consistent()
3687                    .await;
3688                assert!(
3689                    result.is_err(),
3690                    "writing to denied path should return an error"
3691                );
3692
3693                let path = root_path.join(&nested_denied_file)?;
3694                let result = write_file_operation(path, rcstr!("nested"))
3695                    .read_strongly_consistent()
3696                    .await;
3697                assert!(
3698                    result.is_err(),
3699                    "writing to nested denied path should return an error"
3700                );
3701
3702                Ok(())
3703            }
3704
3705            let (_scratch, root, denied_path) = setup_test_fs();
3706            let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3707                BackendOptions::default(),
3708                noop_backing_storage(),
3709            ));
3710            tt.run_once(async {
3711                const ALLOWED_FILE: &str = "allowed_dir/new_file.txt";
3712                const TEST_CONTENT: &str = "test content";
3713
3714                // Test 1: Writing to allowed directory should work
3715                let effects = write_allowed_file_operation(
3716                    root.clone(),
3717                    denied_path.clone(),
3718                    RcStr::from(ALLOWED_FILE),
3719                    RcStr::from(TEST_CONTENT),
3720                )
3721                .read_strongly_consistent()
3722                .await?;
3723                effects.apply().await?;
3724
3725                // Verify the file was written to disk
3726                let content = read_to_string(Path::new(root.as_str()).join(ALLOWED_FILE))?;
3727                assert_eq!(content, TEST_CONTENT, "allowed file write should succeed");
3728
3729                // Tests 2 & 3: Writing to denied paths should fail
3730                test_denied_writes_operation(
3731                    root,
3732                    denied_path,
3733                    RcStr::from("denied_dir/forbidden.txt"),
3734                    RcStr::from("denied_dir/nested/file.txt"),
3735                )
3736                .read_strongly_consistent()
3737                .await?;
3738
3739                anyhow::Ok(())
3740            })
3741            .await
3742            .unwrap();
3743        }
3744    }
3745}