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