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