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