Skip to main content

turbo_tasks_fs/
lib.rs

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