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