turbo_tasks_fs/
lib.rs

1#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
2#![feature(btree_cursors)] // needed for the `InvalidatorMap` and watcher, reduces time complexity
3#![feature(trivial_bounds)]
4#![feature(min_specialization)]
5#![feature(iter_advance_by)]
6#![feature(io_error_more)]
7#![feature(round_char_boundary)]
8#![feature(arbitrary_self_types)]
9#![feature(arbitrary_self_types_pointers)]
10#![allow(clippy::mutable_key_type)]
11
12pub mod attach;
13pub mod embed;
14pub mod glob;
15mod globset;
16pub mod invalidation;
17mod invalidator_map;
18pub mod json;
19mod mutex_map;
20mod path_map;
21mod read_glob;
22mod retry;
23pub mod rope;
24pub mod source_context;
25pub mod util;
26pub(crate) mod virtual_fs;
27mod watcher;
28
29use std::{
30    borrow::Cow,
31    cmp::{Ordering, min},
32    fmt::{self, Debug, Display, Formatter},
33    fs::FileType,
34    future::Future,
35    io::{self, BufRead, BufReader, ErrorKind, Read},
36    mem::take,
37    path::{MAIN_SEPARATOR, Path, PathBuf},
38    sync::{Arc, LazyLock},
39    time::Duration,
40};
41
42use anyhow::{Context, Result, anyhow, bail};
43use auto_hash_map::{AutoMap, AutoSet};
44use bitflags::bitflags;
45use dunce::simplified;
46use indexmap::IndexSet;
47use jsonc_parser::{ParseOptions, parse_to_serde_value};
48use mime::Mime;
49use rustc_hash::FxHashSet;
50use serde::{Deserialize, Serialize};
51use serde_json::Value;
52use tokio::sync::{RwLock, RwLockReadGuard};
53use tracing::Instrument;
54use turbo_rcstr::{RcStr, rcstr};
55use turbo_tasks::{
56    ApplyEffectsContext, Completion, InvalidationReason, Invalidator, NonLocalValue, ReadRef,
57    ResolvedVc, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, effect,
58    mark_session_dependent, mark_stateful, parallel, trace::TraceRawVcs,
59};
60use turbo_tasks_hash::{DeterministicHash, DeterministicHasher, hash_xxh3_hash64};
61use turbo_unix_path::{
62    get_parent_path, get_relative_path_to, join_path, normalize_path, sys_to_unix, unix_to_sys,
63};
64
65use crate::{
66    attach::AttachedFileSystem,
67    glob::Glob,
68    invalidation::Write,
69    invalidator_map::{InvalidatorMap, WriteContent},
70    json::UnparsableJson,
71    mutex_map::MutexMap,
72    read_glob::{read_glob, track_glob},
73    retry::retry_blocking,
74    rope::{Rope, RopeReader},
75    util::extract_disk_access,
76    watcher::DiskWatcher,
77};
78pub use crate::{read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem};
79
80/// A (somewhat arbitrary) filename limit that we should try to keep output file names below.
81///
82/// For the sake of consistency, this is a fixed constant that is likely to be safe across all
83/// platforms.
84///
85/// Different operating systems have different limits on file name and file path. See
86/// [`validate_path_length`] for details. Because this only accounts for a single path segment, and
87/// not the total path length, this cannot not guarantee a full file path is safe.
88///
89/// To ensure file names are kept within this limit, call
90/// [`FileSystemPath::truncate_file_name_with_hash`].
91pub const MAX_SAFE_FILE_NAME_LENGTH: usize = 200;
92
93/// Validate the path, returning the valid path, a modified-but-now-valid path, or bailing with an
94/// error.
95///
96/// The behaviour of the file system changes depending on the OS, and indeed sometimes the FS
97/// implementation of the OS itself.
98///
99/// - On Windows the limit for normal file paths is 260 characters, a holdover from the DOS days,
100///   but Rust will opportunistically rewrite paths to 'UNC' paths for supported path operations
101///   which can be up to 32767 characters long.
102/// - On macOS, the limit is traditionally 255 characters for the file name and a second limit of
103///   1024 for the entire path (verified by running `getconf PATH_MAX /`).
104/// - On Linux, the limit differs between kernel (and by extension, distro) and filesystem. On most
105///   common file systems (e.g. ext4, btrfs, and xfs), individual file names can be up to 255 bytes
106///   with no hard limit on total path length. [Some legacy POSIX APIs are restricted to the
107///   `PATH_MAX` value of 4096 bytes in `limits.h`, but most applications support longer
108///   paths][PATH_MAX].
109///
110/// For more details, refer to <https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits>.
111///
112/// Realistically, the output path lengths will be the same across all platforms, so we need to set
113/// a conservative limit and be particular about when we decide to bump it. Here we have opted for
114/// 255 characters, because it is the shortest of the three options.
115///
116/// [PATH_MAX]: https://eklitzke.org/path-max-is-tricky
117pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
118    /// Here we check if the path is too long for windows, and if so, attempt to canonicalize it
119    /// to a UNC path.
120    #[cfg(target_family = "windows")]
121    fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
122        const MAX_PATH_LENGTH_WINDOWS: usize = 260;
123        const UNC_PREFIX: &str = "\\\\?\\";
124
125        if path.starts_with(UNC_PREFIX) {
126            return Ok(path.into());
127        }
128
129        if path.as_os_str().len() > MAX_PATH_LENGTH_WINDOWS {
130            let new_path = std::fs::canonicalize(path)
131                .map_err(|_| anyhow!("file is too long, and could not be normalized"))?;
132            return Ok(new_path.into());
133        }
134
135        Ok(path.into())
136    }
137
138    /// Here we are only going to check if the total length exceeds, or the last segment exceeds.
139    /// This heuristic is primarily to avoid long file names, and it makes the operation much
140    /// cheaper.
141    #[cfg(not(target_family = "windows"))]
142    fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
143        const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
144        // macOS reports a limit of 1024, but I (@arlyon) have had issues with paths above 1016
145        // so we subtract a bit to be safe. on most linux distros this is likely a lot larger than
146        // 1024, but macOS is *special*
147        const MAX_PATH_LENGTH: usize = 1024 - 8;
148
149        // check the last segment (file name)
150        if path
151            .file_name()
152            .map(|n| n.as_encoded_bytes().len())
153            .unwrap_or(0)
154            > MAX_FILE_NAME_LENGTH_UNIX
155        {
156            anyhow::bail!(
157                "file name is too long (exceeds {} bytes)",
158                MAX_FILE_NAME_LENGTH_UNIX
159            );
160        }
161
162        if path.as_os_str().len() > MAX_PATH_LENGTH {
163            anyhow::bail!("path is too long (exceeds {} bytes)", MAX_PATH_LENGTH);
164        }
165
166        Ok(path.into())
167    }
168
169    validate_path_length_inner(path).with_context(|| {
170        format!(
171            "path length for file {} exceeds max length of filesystem",
172            path.to_string_lossy()
173        )
174    })
175}
176
177trait ConcurrencyLimitedExt {
178    type Output;
179    async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
180}
181
182impl<F, R> ConcurrencyLimitedExt for F
183where
184    F: Future<Output = R>,
185{
186    type Output = R;
187    async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
188        let _permit = semaphore.acquire().await;
189        self.await
190    }
191}
192
193fn create_semaphore() -> tokio::sync::Semaphore {
194    tokio::sync::Semaphore::new(256)
195}
196
197#[turbo_tasks::value_trait]
198pub trait FileSystem: ValueToString {
199    /// Returns the path to the root of the file system.
200    #[turbo_tasks::function]
201    fn root(self: ResolvedVc<Self>) -> Vc<FileSystemPath> {
202        FileSystemPath::new_normalized(self, RcStr::default()).cell()
203    }
204    #[turbo_tasks::function]
205    fn read(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileContent>;
206    #[turbo_tasks::function]
207    fn read_link(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<LinkContent>;
208    #[turbo_tasks::function]
209    fn raw_read_dir(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<RawDirectoryContent>;
210    #[turbo_tasks::function]
211    fn write(self: Vc<Self>, fs_path: FileSystemPath, content: Vc<FileContent>) -> Vc<()>;
212    #[turbo_tasks::function]
213    fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
214    #[turbo_tasks::function]
215    fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
216}
217
218#[derive(Default)]
219struct DiskFileSystemApplyContext {
220    /// A cache of already created directories to avoid creating them multiple times.
221    created_directories: FxHashSet<PathBuf>,
222}
223
224#[derive(Serialize, Deserialize, TraceRawVcs, ValueDebugFormat, NonLocalValue)]
225struct DiskFileSystemInner {
226    pub name: RcStr,
227    pub root: RcStr,
228    #[turbo_tasks(debug_ignore, trace_ignore)]
229    #[serde(skip)]
230    mutex_map: MutexMap<PathBuf>,
231    #[turbo_tasks(debug_ignore, trace_ignore)]
232    #[serde(skip)]
233    invalidator_map: InvalidatorMap,
234    #[turbo_tasks(debug_ignore, trace_ignore)]
235    #[serde(skip)]
236    dir_invalidator_map: InvalidatorMap,
237    /// Lock that makes invalidation atomic. It will keep a write lock during
238    /// watcher invalidation and a read lock during other operations.
239    #[turbo_tasks(debug_ignore, trace_ignore)]
240    #[serde(skip)]
241    invalidation_lock: RwLock<()>,
242    /// Semaphore to limit the maximum number of concurrent file operations.
243    #[turbo_tasks(debug_ignore, trace_ignore)]
244    #[serde(skip, default = "create_semaphore")]
245    semaphore: tokio::sync::Semaphore,
246
247    #[turbo_tasks(debug_ignore, trace_ignore)]
248    watcher: DiskWatcher,
249}
250
251impl DiskFileSystemInner {
252    /// Returns the root as Path
253    fn root_path(&self) -> &Path {
254        simplified(Path::new(&*self.root))
255    }
256
257    /// registers the path as an invalidator for the current task,
258    /// has to be called within a turbo-tasks function
259    fn register_read_invalidator(&self, path: &Path) -> Result<()> {
260        let invalidator = turbo_tasks::get_invalidator();
261        self.invalidator_map
262            .insert(path.to_owned(), invalidator, None);
263        self.watcher.ensure_watched_file(path, self.root_path())?;
264        Ok(())
265    }
266
267    /// registers the path as an invalidator for the current task,
268    /// has to be called within a turbo-tasks function. It removes and returns
269    /// the current list of invalidators.
270    fn register_write_invalidator(
271        &self,
272        path: &Path,
273        invalidator: Invalidator,
274        write_content: WriteContent,
275    ) -> Result<Vec<(Invalidator, Option<WriteContent>)>> {
276        let mut invalidator_map = self.invalidator_map.lock().unwrap();
277        let invalidators = invalidator_map.entry(path.to_owned()).or_default();
278        let old_invalidators = invalidators
279            .extract_if(|i, old_write_content| {
280                i == &invalidator
281                    || old_write_content
282                        .as_ref()
283                        .is_none_or(|old| old != &write_content)
284            })
285            .filter(|(i, _)| i != &invalidator)
286            .collect::<Vec<_>>();
287        invalidators.insert(invalidator, Some(write_content));
288        drop(invalidator_map);
289        self.watcher.ensure_watched_file(path, self.root_path())?;
290        Ok(old_invalidators)
291    }
292
293    /// registers the path as an invalidator for the current task,
294    /// has to be called within a turbo-tasks function
295    fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
296        let invalidator = turbo_tasks::get_invalidator();
297        self.dir_invalidator_map
298            .insert(path.to_owned(), invalidator, None);
299        self.watcher.ensure_watched_dir(path, self.root_path())?;
300        Ok(())
301    }
302
303    async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
304        let lock1 = self.invalidation_lock.read().await;
305        let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
306        PathLockGuard(lock1, lock2)
307    }
308
309    fn invalidate(&self) {
310        let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
311        let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
312        let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
313        let invalidators = invalidator_map
314            .into_iter()
315            .chain(dir_invalidator_map)
316            .flat_map(|(_, invalidators)| invalidators.into_keys())
317            .collect::<Vec<_>>();
318        parallel::for_each_owned(invalidators, |invalidator| invalidator.invalidate());
319    }
320
321    /// Invalidates every tracked file in the filesystem.
322    ///
323    /// Calls the given
324    fn invalidate_with_reason<R: InvalidationReason + Clone>(
325        &self,
326        reason: impl Fn(&Path) -> R + Sync,
327    ) {
328        let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
329        let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
330        let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
331        let invalidators = invalidator_map
332            .into_iter()
333            .chain(dir_invalidator_map)
334            .flat_map(|(path, invalidators)| {
335                let reason_for_path = reason(&path);
336                invalidators
337                    .into_keys()
338                    .map(move |i| (reason_for_path.clone(), i))
339            })
340            .collect::<Vec<_>>();
341        parallel::for_each_owned(invalidators, |(reason, invalidator)| {
342            invalidator.invalidate_with_reason(reason)
343        });
344    }
345
346    fn invalidate_from_write(
347        &self,
348        full_path: &Path,
349        invalidators: Vec<(Invalidator, Option<WriteContent>)>,
350    ) {
351        if !invalidators.is_empty() {
352            if let Some(path) = format_absolute_fs_path(full_path, &self.name, self.root_path()) {
353                if invalidators.len() == 1 {
354                    let (invalidator, _) = invalidators.into_iter().next().unwrap();
355                    invalidator.invalidate_with_reason(Write { path });
356                } else {
357                    invalidators.into_iter().for_each(|(invalidator, _)| {
358                        invalidator.invalidate_with_reason(Write { path: path.clone() });
359                    });
360                }
361            } else {
362                invalidators.into_iter().for_each(|(invalidator, _)| {
363                    invalidator.invalidate();
364                });
365            }
366        }
367    }
368
369    #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
370    async fn start_watching_internal(
371        self: &Arc<Self>,
372        report_invalidation_reason: bool,
373        poll_interval: Option<Duration>,
374    ) -> Result<()> {
375        let root_path = self.root_path().to_path_buf();
376
377        // create the directory for the filesystem on disk, if it doesn't exist
378        retry_blocking(root_path.clone(), move |path| {
379            let _tracing =
380                tracing::info_span!("create root directory", name = display(path.display()))
381                    .entered();
382
383            std::fs::create_dir_all(path)
384        })
385        .concurrency_limited(&self.semaphore)
386        .await?;
387
388        self.watcher
389            .start_watching(self.clone(), report_invalidation_reason, poll_interval)?;
390
391        Ok(())
392    }
393
394    async fn create_directory(self: &Arc<Self>, directory: &Path) -> Result<()> {
395        let already_created = ApplyEffectsContext::with_or_insert_with(
396            DiskFileSystemApplyContext::default,
397            |fs_context| fs_context.created_directories.contains(directory),
398        );
399        if !already_created {
400            let func = |p: &Path| std::fs::create_dir_all(p);
401            retry_blocking(directory.to_path_buf(), func)
402                .concurrency_limited(&self.semaphore)
403                .instrument(tracing::info_span!(
404                    "create directory",
405                    name = display(directory.display())
406                ))
407                .await?;
408            ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| {
409                fs_context
410                    .created_directories
411                    .insert(directory.to_path_buf())
412            });
413        }
414        Ok(())
415    }
416}
417
418#[turbo_tasks::value(cell = "new", eq = "manual")]
419pub struct DiskFileSystem {
420    inner: Arc<DiskFileSystemInner>,
421}
422
423impl DiskFileSystem {
424    pub fn name(&self) -> &RcStr {
425        &self.inner.name
426    }
427
428    pub fn root(&self) -> &RcStr {
429        &self.inner.root
430    }
431
432    pub fn invalidate(&self) {
433        self.inner.invalidate();
434    }
435
436    pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
437        &self,
438        reason: impl Fn(&Path) -> R + Sync,
439    ) {
440        self.inner.invalidate_with_reason(reason);
441    }
442
443    pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
444        self.inner
445            .start_watching_internal(false, poll_interval)
446            .await
447    }
448
449    pub async fn start_watching_with_invalidation_reason(
450        &self,
451        poll_interval: Option<Duration>,
452    ) -> Result<()> {
453        self.inner
454            .start_watching_internal(true, poll_interval)
455            .await
456    }
457
458    pub fn stop_watching(&self) {
459        self.inner.watcher.stop_watching();
460    }
461
462    pub fn to_sys_path(&self, fs_path: FileSystemPath) -> Result<PathBuf> {
463        // just in case there's a windows unc path prefix we remove it with `dunce`
464        let path = self.inner.root_path();
465        Ok(if fs_path.path.is_empty() {
466            path.to_path_buf()
467        } else {
468            path.join(&*unix_to_sys(&fs_path.path))
469        })
470    }
471}
472
473#[allow(dead_code, reason = "we need to hold onto the locks")]
474struct PathLockGuard<'a>(
475    #[allow(dead_code)] RwLockReadGuard<'a, ()>,
476    #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
477);
478
479fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
480    if let Ok(rel_path) = path.strip_prefix(root_path) {
481        let path = if MAIN_SEPARATOR != '/' {
482            let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
483            format!("[{name}]/{rel_path}")
484        } else {
485            format!("[{name}]/{}", rel_path.display())
486        };
487        Some(path)
488    } else {
489        None
490    }
491}
492
493#[turbo_tasks::value_impl]
494impl DiskFileSystem {
495    /// Create a new instance of `DiskFileSystem`.
496    /// # Arguments
497    ///
498    /// * `name` - Name of the filesystem.
499    /// * `root` - Path to the given filesystem's root. Should be
500    ///   [canonicalized][std::fs::canonicalize].
501    #[turbo_tasks::function]
502    pub fn new(name: RcStr, root: RcStr) -> Result<Vc<Self>> {
503        mark_stateful();
504
505        let instance = DiskFileSystem {
506            inner: Arc::new(DiskFileSystemInner {
507                name,
508                root,
509                mutex_map: Default::default(),
510                invalidation_lock: Default::default(),
511                invalidator_map: InvalidatorMap::new(),
512                dir_invalidator_map: InvalidatorMap::new(),
513                semaphore: create_semaphore(),
514                watcher: DiskWatcher::new(),
515            }),
516        };
517
518        Ok(Self::cell(instance))
519    }
520}
521
522impl Debug for DiskFileSystem {
523    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
524        write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
525    }
526}
527
528#[turbo_tasks::value_impl]
529impl FileSystem for DiskFileSystem {
530    #[turbo_tasks::function(fs)]
531    async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
532        mark_session_dependent();
533        let full_path = self.to_sys_path(fs_path)?;
534        self.inner.register_read_invalidator(&full_path)?;
535
536        let _lock = self.inner.lock_path(&full_path).await;
537        let content = match retry_blocking(full_path.clone(), |path: &Path| File::from_path(path))
538            .concurrency_limited(&self.inner.semaphore)
539            .instrument(tracing::info_span!(
540                "read file",
541                name = display(full_path.display())
542            ))
543            .await
544        {
545            Ok(file) => FileContent::new(file),
546            Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
547                FileContent::NotFound
548            }
549            Err(e) => {
550                bail!(anyhow!(e).context(format!("reading file {}", full_path.display())))
551            }
552        };
553        Ok(content.cell())
554    }
555
556    #[turbo_tasks::function(fs)]
557    async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
558        mark_session_dependent();
559        let full_path = self.to_sys_path(fs_path)?;
560        self.inner.register_dir_invalidator(&full_path)?;
561
562        // we use the sync std function here as it's a lot faster (600%) in
563        // node-file-trace
564        let read_dir = match retry_blocking(full_path.clone(), |path| {
565            let _span =
566                tracing::info_span!("read directory", name = display(path.display())).entered();
567            std::fs::read_dir(path)
568        })
569        .concurrency_limited(&self.inner.semaphore)
570        .await
571        {
572            Ok(dir) => dir,
573            Err(e)
574                if e.kind() == ErrorKind::NotFound
575                    || e.kind() == ErrorKind::NotADirectory
576                    || e.kind() == ErrorKind::InvalidFilename =>
577            {
578                return Ok(RawDirectoryContent::not_found());
579            }
580            Err(e) => {
581                bail!(anyhow!(e).context(format!("reading dir {}", full_path.display())))
582            }
583        };
584
585        let entries = read_dir
586            .filter_map(|r| {
587                let e = match r {
588                    Ok(e) => e,
589                    Err(err) => return Some(Err(err.into())),
590                };
591
592                // we filter out any non unicode names
593                let file_name = e.file_name().to_str()?.into();
594
595                let entry = match e.file_type() {
596                    Ok(t) if t.is_file() => RawDirectoryEntry::File,
597                    Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
598                    Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
599                    Ok(_) => RawDirectoryEntry::Other,
600                    Err(err) => return Some(Err(err.into())),
601                };
602
603                Some(anyhow::Ok((file_name, entry)))
604            })
605            .collect::<Result<_>>()
606            .with_context(|| format!("reading directory item in {}", full_path.display()))?;
607
608        Ok(RawDirectoryContent::new(entries))
609    }
610
611    #[turbo_tasks::function(fs)]
612    async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
613        mark_session_dependent();
614        let full_path = self.to_sys_path(fs_path.clone())?;
615        self.inner.register_read_invalidator(&full_path)?;
616
617        let _lock = self.inner.lock_path(&full_path).await;
618        let link_path =
619            match retry_blocking(full_path.clone(), |path: &Path| std::fs::read_link(path))
620                .concurrency_limited(&self.inner.semaphore)
621                .instrument(tracing::info_span!(
622                    "read symlink",
623                    name = display(full_path.display())
624                ))
625                .await
626            {
627                Ok(res) => res,
628                Err(_) => return Ok(LinkContent::NotFound.cell()),
629            };
630        let is_link_absolute = link_path.is_absolute();
631
632        let mut file = link_path.clone();
633        if !is_link_absolute {
634            if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
635                normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
636            }) {
637                #[cfg(target_family = "windows")]
638                {
639                    file = PathBuf::from(normalized_linked_path);
640                }
641                // `normalize_path` stripped the leading `/` of the path
642                // add it back here or the `strip_prefix` will return `Err`
643                #[cfg(not(target_family = "windows"))]
644                {
645                    file = PathBuf::from(format!("/{normalized_linked_path}"));
646                }
647            } else {
648                return Ok(LinkContent::Invalid.cell());
649            }
650        }
651
652        // strip the root from the path, it serves two purpose
653        // 1. ensure the linked path is under the root
654        // 2. strip the root path if the linked path is absolute
655        //
656        // we use `dunce::simplify` to strip a potential UNC prefix on windows, on any
657        // other OS this gets compiled away
658        let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
659
660        let relative_to_root_path = match result {
661            Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
662            Err(_) => return Ok(LinkContent::Invalid.cell()),
663        };
664
665        let (target, file_type) = if is_link_absolute {
666            let target_string: RcStr = relative_to_root_path.to_string_lossy().into();
667            (
668                target_string.clone(),
669                FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
670                    .get_type()
671                    .await?,
672            )
673        } else {
674            let link_path_string_cow = link_path.to_string_lossy();
675            let link_path_unix: RcStr = sys_to_unix(&link_path_string_cow).into();
676            (
677                link_path_unix.clone(),
678                fs_path.parent().join(&link_path_unix)?.get_type().await?,
679            )
680        };
681
682        Ok(LinkContent::Link {
683            target,
684            link_type: {
685                let mut link_type = Default::default();
686                if link_path.is_absolute() {
687                    link_type |= LinkType::ABSOLUTE;
688                }
689                if matches!(&*file_type, FileSystemEntryType::Directory) {
690                    link_type |= LinkType::DIRECTORY;
691                }
692                link_type
693            },
694        }
695        .cell())
696    }
697
698    #[turbo_tasks::function(fs)]
699    async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
700        // You might be tempted to use `mark_session_dependent` here, but
701        // `write` purely declares a side effect and does not need to be reexecuted in the next
702        // session. All side effects are reexecuted in general.
703
704        let full_path = self.to_sys_path(fs_path)?;
705        let content = content.await?;
706        let inner = self.inner.clone();
707        let invalidator = turbo_tasks::get_invalidator();
708
709        effect(async move {
710            let full_path = validate_path_length(&full_path)?;
711
712            let _lock = inner.lock_path(&full_path).await;
713
714            // Track the file, so that we will rewrite it if it ever changes.
715            let old_invalidators = inner.register_write_invalidator(
716                &full_path,
717                invalidator,
718                WriteContent::File(content.clone()),
719            )?;
720
721            // We perform an untracked comparison here, so that this write is not dependent
722            // on a read's Vc<FileContent> (and the memory it holds). Our untracked read can
723            // be freed immediately. Given this is an output file, it's unlikely any Turbo
724            // code will need to read the file from disk into a Vc<FileContent>, so we're
725            // not wasting cycles.
726            let compare = content
727                .streaming_compare(&full_path)
728                .concurrency_limited(&inner.semaphore)
729                .instrument(tracing::info_span!(
730                    "read file before write",
731                    name = display(full_path.display())
732                ))
733                .await?;
734            if compare == FileComparison::Equal {
735                if !old_invalidators.is_empty() {
736                    for (invalidator, write_content) in old_invalidators {
737                        inner.invalidator_map.insert(
738                            full_path.clone().into_owned(),
739                            invalidator,
740                            write_content,
741                        );
742                    }
743                }
744                return Ok(());
745            }
746
747            match &*content {
748                FileContent::Content(..) => {
749                    let create_directory = compare == FileComparison::Create;
750                    if create_directory && let Some(parent) = full_path.parent() {
751                        inner.create_directory(parent).await.with_context(|| {
752                            format!(
753                                "failed to create directory {} for write to {}",
754                                parent.display(),
755                                full_path.display()
756                            )
757                        })?;
758                    }
759
760                    let full_path_to_write = full_path.clone();
761                    let content = content.clone();
762                    retry_blocking(full_path_to_write.into_owned(), move |full_path| {
763                        use std::io::Write;
764
765                        let mut f = std::fs::File::create(full_path)?;
766                        let FileContent::Content(file) = &*content else {
767                            unreachable!()
768                        };
769                        std::io::copy(&mut file.read(), &mut f)?;
770                        #[cfg(target_family = "unix")]
771                        f.set_permissions(file.meta.permissions.into())?;
772                        f.flush()?;
773
774                        static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
775                            std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
776                                .is_some_and(|v| v == "1" || v == "true")
777                        });
778                        if *WRITE_VERSION {
779                            let mut full_path = full_path.to_owned();
780                            let hash = hash_xxh3_hash64(file);
781                            let ext = full_path.extension();
782                            let ext = if let Some(ext) = ext {
783                                format!("{:016x}.{}", hash, ext.to_string_lossy())
784                            } else {
785                                format!("{hash:016x}")
786                            };
787                            full_path.set_extension(ext);
788                            let mut f = std::fs::File::create(&full_path)?;
789                            std::io::copy(&mut file.read(), &mut f)?;
790                            #[cfg(target_family = "unix")]
791                            f.set_permissions(file.meta.permissions.into())?;
792                            f.flush()?;
793                        }
794                        Ok::<(), io::Error>(())
795                    })
796                    .concurrency_limited(&inner.semaphore)
797                    .instrument(tracing::info_span!(
798                        "write file",
799                        name = display(full_path.display())
800                    ))
801                    .await
802                    .with_context(|| format!("failed to write to {}", full_path.display()))?;
803                }
804                FileContent::NotFound => {
805                    retry_blocking(full_path.clone().into_owned(), |path| {
806                        std::fs::remove_file(path)
807                    })
808                    .concurrency_limited(&inner.semaphore)
809                    .instrument(tracing::info_span!(
810                        "remove file",
811                        name = display(full_path.display())
812                    ))
813                    .await
814                    .or_else(|err| {
815                        if err.kind() == ErrorKind::NotFound {
816                            Ok(())
817                        } else {
818                            Err(err)
819                        }
820                    })
821                    .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
822                }
823            }
824
825            inner.invalidate_from_write(&full_path, old_invalidators);
826
827            Ok(())
828        });
829
830        Ok(())
831    }
832
833    #[turbo_tasks::function(fs)]
834    async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
835        // You might be tempted to use `mark_session_dependent` here, but
836        // `write_link` purely declares a side effect and does not need to be reexecuted in the next
837        // session. All side effects are reexecuted in general.
838
839        let full_path = self.to_sys_path(fs_path)?;
840        let content = target.await?;
841        let inner = self.inner.clone();
842        let invalidator = turbo_tasks::get_invalidator();
843
844        effect(async move {
845            let full_path = validate_path_length(&full_path)?;
846
847            let _lock = inner.lock_path(&full_path).await;
848
849            let old_invalidators = inner.register_write_invalidator(
850                &full_path,
851                invalidator,
852                WriteContent::Link(content.clone()),
853            )?;
854
855            // TODO(sokra) preform a untracked read here, register an invalidator and get
856            // all existing invalidators
857            let old_content = match retry_blocking(full_path.clone().into_owned(), |path| {
858                std::fs::read_link(path)
859            })
860            .concurrency_limited(&inner.semaphore)
861            .instrument(tracing::info_span!(
862                "read symlink before write",
863                name = display(full_path.display())
864            ))
865            .await
866            {
867                Ok(res) => Some((res.is_absolute(), res)),
868                Err(_) => None,
869            };
870            let is_equal = match (&*content, &old_content) {
871                (LinkContent::Link { target, link_type }, Some((old_is_absolute, old_target))) => {
872                    Path::new(&**target) == old_target
873                        && link_type.contains(LinkType::ABSOLUTE) == *old_is_absolute
874                }
875                (LinkContent::NotFound, None) => true,
876                _ => false,
877            };
878            if is_equal {
879                if !old_invalidators.is_empty() {
880                    for (invalidator, write_content) in old_invalidators {
881                        inner.invalidator_map.insert(
882                            full_path.clone().into_owned(),
883                            invalidator,
884                            write_content,
885                        );
886                    }
887                }
888                return Ok(());
889            }
890
891            match &*content {
892                LinkContent::Link { target, link_type } => {
893                    let create_directory = old_content.is_none();
894                    if create_directory && let Some(parent) = full_path.parent() {
895                        inner.create_directory(parent).await.with_context(|| {
896                            format!(
897                                "failed to create directory {} for write link to {}",
898                                parent.display(),
899                                full_path.display()
900                            )
901                        })?;
902                    }
903
904                    let link_type = *link_type;
905                    let target_path = if link_type.contains(LinkType::ABSOLUTE) {
906                        Path::new(&inner.root).join(unix_to_sys(target).as_ref())
907                    } else {
908                        PathBuf::from(unix_to_sys(target).as_ref())
909                    };
910                    let full_path = full_path.into_owned();
911                    retry_blocking(target_path, move |target_path| {
912                        let _span = tracing::info_span!(
913                            "write symlink",
914                            name = display(target_path.display())
915                        )
916                        .entered();
917                        // we use the sync std method here because `symlink` is fast
918                        // if we put it into a task, it will be slower
919                        #[cfg(not(target_family = "windows"))]
920                        {
921                            std::os::unix::fs::symlink(target_path, &full_path)
922                        }
923                        #[cfg(target_family = "windows")]
924                        {
925                            if link_type.contains(LinkType::DIRECTORY) {
926                                std::os::windows::fs::symlink_dir(target_path, &full_path)
927                            } else {
928                                std::os::windows::fs::symlink_file(target_path, &full_path)
929                            }
930                        }
931                    })
932                    .await
933                    .with_context(|| format!("create symlink to {target}"))?;
934                }
935                LinkContent::Invalid => {
936                    anyhow::bail!("invalid symlink target: {}", full_path.display())
937                }
938                LinkContent::NotFound => {
939                    retry_blocking(full_path.clone().into_owned(), |path| {
940                        std::fs::remove_file(path)
941                    })
942                    .concurrency_limited(&inner.semaphore)
943                    .await
944                    .or_else(|err| {
945                        if err.kind() == ErrorKind::NotFound {
946                            Ok(())
947                        } else {
948                            Err(err)
949                        }
950                    })
951                    .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
952                }
953            }
954
955            Ok(())
956        });
957        Ok(())
958    }
959
960    #[turbo_tasks::function(fs)]
961    async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
962        mark_session_dependent();
963        let full_path = self.to_sys_path(fs_path)?;
964        self.inner.register_read_invalidator(&full_path)?;
965
966        let _lock = self.inner.lock_path(&full_path).await;
967        let meta = retry_blocking(full_path.clone(), |path| std::fs::metadata(path))
968            .concurrency_limited(&self.inner.semaphore)
969            .instrument(tracing::info_span!(
970                "read metadata",
971                name = display(full_path.display())
972            ))
973            .await
974            .with_context(|| format!("reading metadata for {}", full_path.display()))?;
975
976        Ok(FileMeta::cell(meta.into()))
977    }
978}
979
980#[turbo_tasks::value_impl]
981impl ValueToString for DiskFileSystem {
982    #[turbo_tasks::function]
983    fn to_string(&self) -> Vc<RcStr> {
984        Vc::cell(self.inner.name.clone())
985    }
986}
987
988#[turbo_tasks::value(shared)]
989#[derive(Debug, Clone, Hash, TaskInput)]
990pub struct FileSystemPath {
991    pub fs: ResolvedVc<Box<dyn FileSystem>>,
992    pub path: RcStr,
993}
994
995impl FileSystemPath {
996    /// Mimics `ValueToString::to_string`.
997    pub fn value_to_string(&self) -> Vc<RcStr> {
998        value_to_string(self.clone())
999    }
1000}
1001
1002#[turbo_tasks::function]
1003async fn value_to_string(path: FileSystemPath) -> Result<Vc<RcStr>> {
1004    Ok(Vc::cell(
1005        format!("[{}]/{}", path.fs.to_string().await?, path.path).into(),
1006    ))
1007}
1008
1009impl FileSystemPath {
1010    pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1011        if self.fs == other.fs && self.path.starts_with(&*other.path) {
1012            if other.path.is_empty() {
1013                true
1014            } else {
1015                self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1016            }
1017        } else {
1018            false
1019        }
1020    }
1021
1022    pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1023        if self.fs == other.fs && self.path.starts_with(&*other.path) {
1024            if other.path.is_empty() {
1025                true
1026            } else {
1027                matches!(
1028                    self.path.as_bytes().get(other.path.len()),
1029                    Some(&b'/') | None
1030                )
1031            }
1032        } else {
1033            false
1034        }
1035    }
1036
1037    pub fn is_root(&self) -> bool {
1038        self.path.is_empty()
1039    }
1040
1041    /// Returns the path of `inner` relative to `self`.
1042    ///
1043    /// Note: this method always strips the leading `/` from the result.
1044    pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1045        if self.fs != inner.fs {
1046            return None;
1047        }
1048        let path = inner.path.strip_prefix(&*self.path)?;
1049        if self.path.is_empty() {
1050            Some(path)
1051        } else if let Some(stripped) = path.strip_prefix('/') {
1052            Some(stripped)
1053        } else {
1054            None
1055        }
1056    }
1057
1058    pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1059        if self.fs != other.fs {
1060            return None;
1061        }
1062
1063        Some(get_relative_path_to(&self.path, &other.path).into())
1064    }
1065
1066    /// Returns the final component of the FileSystemPath, or an empty string
1067    /// for the root path.
1068    pub fn file_name(&self) -> &str {
1069        let (_, file_name) = self.split_file_name();
1070        file_name
1071    }
1072
1073    /// Returns true if this path has the given extension
1074    ///
1075    /// slightly faster than `self.extension_ref() == Some(extension)` as we can simply match a
1076    /// suffix
1077    pub fn has_extension(&self, extension: &str) -> bool {
1078        debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1079        self.path.ends_with(extension)
1080    }
1081
1082    /// Returns the extension (without a leading `.`)
1083    pub fn extension_ref(&self) -> Option<&str> {
1084        let (_, extension) = self.split_extension();
1085        extension
1086    }
1087
1088    /// Splits the path into two components:
1089    /// 1. The path without the extension;
1090    /// 2. The extension, if any.
1091    fn split_extension(&self) -> (&str, Option<&str>) {
1092        if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1093            if extension.contains('/') ||
1094                // The file name begins with a `.` and has no other `.`s within.
1095                path_before_extension.ends_with('/') || path_before_extension.is_empty()
1096            {
1097                (self.path.as_str(), None)
1098            } else {
1099                (path_before_extension, Some(extension))
1100            }
1101        } else {
1102            (self.path.as_str(), None)
1103        }
1104    }
1105
1106    /// Splits the path into two components:
1107    /// 1. The parent directory, if any;
1108    /// 2. The file name;
1109    fn split_file_name(&self) -> (Option<&str>, &str) {
1110        // Since the path is normalized, we know `parent`, if any, must not be empty.
1111        if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1112            (Some(parent), file_name)
1113        } else {
1114            (None, self.path.as_str())
1115        }
1116    }
1117
1118    /// Splits the path into three components:
1119    /// 1. The parent directory, if any;
1120    /// 2. The file stem;
1121    /// 3. The extension, if any.
1122    fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1123        let (path_before_extension, extension) = self.split_extension();
1124
1125        if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1126            (Some(parent), file_stem, extension)
1127        } else {
1128            (None, path_before_extension, extension)
1129        }
1130    }
1131}
1132
1133#[turbo_tasks::value(transparent)]
1134pub struct FileSystemPathOption(Option<FileSystemPath>);
1135
1136#[turbo_tasks::value_impl]
1137impl FileSystemPathOption {
1138    #[turbo_tasks::function]
1139    pub fn none() -> Vc<Self> {
1140        Vc::cell(None)
1141    }
1142}
1143
1144impl FileSystemPath {
1145    /// Create a new FileSystemPath from a path within a FileSystem. The
1146    /// /-separated path is expected to be already normalized (this is asserted
1147    /// in dev mode).
1148    fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1149        // On Windows, the path must be converted to a unix path before creating. But on
1150        // Unix, backslashes are a valid char in file names, and the path can be
1151        // provided by the user, so we allow it.
1152        debug_assert!(
1153            MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1154            "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1155             '/'",
1156        );
1157        debug_assert!(
1158            normalize_path(&path).as_deref() == Some(&*path),
1159            "path {path} must be normalized",
1160        );
1161        FileSystemPath { fs, path }
1162    }
1163
1164    /// Adds a subpath to the current path. The /-separate path argument might
1165    /// contain ".." or "." segments, but it must not leave the root of the
1166    /// filesystem.
1167    pub fn join(&self, path: &str) -> Result<Self> {
1168        if let Some(path) = join_path(&self.path, path) {
1169            Ok(Self::new_normalized(self.fs, path.into()))
1170        } else {
1171            bail!(
1172                "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1173                self.path,
1174                path
1175            );
1176        }
1177    }
1178
1179    /// Adds a suffix to the filename. [path] must not contain `/`.
1180    pub fn append(&self, path: &str) -> Result<Self> {
1181        if path.contains('/') {
1182            bail!(
1183                "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1184                self.path,
1185                path
1186            )
1187        }
1188        Ok(Self::new_normalized(
1189            self.fs,
1190            format!("{}{}", self.path, path).into(),
1191        ))
1192    }
1193
1194    /// Adds a suffix to the basename of the filename. [appending] must not
1195    /// contain `/`. Extension will stay intact.
1196    pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1197        if appending.contains('/') {
1198            bail!(
1199                "FileSystemPath(\"{}\").append_to_stem(\"{}\") must not append '/'",
1200                self.path,
1201                appending
1202            )
1203        }
1204        if let (path, Some(ext)) = self.split_extension() {
1205            return Ok(Self::new_normalized(
1206                self.fs,
1207                format!("{path}{appending}.{ext}").into(),
1208            ));
1209        }
1210        Ok(Self::new_normalized(
1211            self.fs,
1212            format!("{}{}", self.path, appending).into(),
1213        ))
1214    }
1215
1216    /// Similar to [FileSystemPath::join], but returns an Option that will be
1217    /// None when the joined path would leave the filesystem root.
1218    #[allow(clippy::needless_borrow)] // for windows build
1219    pub fn try_join(&self, path: &str) -> Result<Option<FileSystemPath>> {
1220        // TODO(PACK-3279): Remove this once we do not produce invalid paths at the first place.
1221        #[cfg(target_os = "windows")]
1222        let path = path.replace('\\', "/");
1223
1224        if let Some(path) = join_path(&self.path, &path) {
1225            Ok(Some(Self::new_normalized(self.fs, path.into())))
1226        } else {
1227            Ok(None)
1228        }
1229    }
1230
1231    /// Similar to [FileSystemPath::join], but returns an Option that will be
1232    /// None when the joined path would leave the current path.
1233    pub fn try_join_inside(&self, path: &str) -> Result<Option<FileSystemPath>> {
1234        if let Some(path) = join_path(&self.path, path)
1235            && path.starts_with(&*self.path)
1236        {
1237            return Ok(Some(Self::new_normalized(self.fs, path.into())));
1238        }
1239        Ok(None)
1240    }
1241
1242    /// DETERMINISM: Result is in random order. Either sort result or do not depend
1243    /// on the order.
1244    pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1245        read_glob(self.clone(), glob)
1246    }
1247
1248    // Tracks all files and directories matching the glob
1249    // Follows symlinks as though they were part of the original hierarchy.
1250    pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1251        track_glob(self.clone(), glob, include_dot_files)
1252    }
1253
1254    pub fn root(&self) -> Vc<Self> {
1255        self.fs().root()
1256    }
1257}
1258
1259impl FileSystemPath {
1260    pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1261        *self.fs
1262    }
1263
1264    pub fn extension(&self) -> &str {
1265        self.extension_ref().unwrap_or_default()
1266    }
1267
1268    pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1269        self.is_inside_ref(other)
1270    }
1271
1272    pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1273        self.is_inside_or_equal_ref(other)
1274    }
1275
1276    /// Creates a new [`FileSystemPath`] like `self` but with the given
1277    /// extension.
1278    pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1279        let (path_without_extension, _) = self.split_extension();
1280        Self::new_normalized(
1281            self.fs,
1282            // Like `Path::with_extension` and `PathBuf::set_extension`, if the extension is empty,
1283            // we remove the extension altogether.
1284            match extension.is_empty() {
1285                true => path_without_extension.into(),
1286                false => format!("{path_without_extension}.{extension}").into(),
1287            },
1288        )
1289    }
1290
1291    /// Extracts the stem (non-extension) portion of self.file_name.
1292    ///
1293    /// The stem is:
1294    ///
1295    /// * [`None`], if there is no file name;
1296    /// * The entire file name if there is no embedded `.`;
1297    /// * The entire file name if the file name begins with `.` and has no other `.`s within;
1298    /// * Otherwise, the portion of the file name before the final `.`
1299    pub fn file_stem(&self) -> Option<&str> {
1300        let (_, file_stem, _) = self.split_file_stem_extension();
1301        if file_stem.is_empty() {
1302            return None;
1303        }
1304        Some(file_stem)
1305    }
1306}
1307
1308impl Display for FileSystemPath {
1309    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1310        write!(f, "{}", self.path)
1311    }
1312}
1313
1314#[turbo_tasks::function]
1315pub async fn rebase(
1316    fs_path: FileSystemPath,
1317    old_base: FileSystemPath,
1318    new_base: FileSystemPath,
1319) -> Result<Vc<FileSystemPath>> {
1320    let new_path;
1321    if old_base.path.is_empty() {
1322        if new_base.path.is_empty() {
1323            new_path = fs_path.path.clone();
1324        } else {
1325            new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1326        }
1327    } else {
1328        let base_path = [&old_base.path, "/"].concat();
1329        if !fs_path.path.starts_with(&base_path) {
1330            bail!(
1331                "rebasing {} from {} onto {} doesn't work because it's not part of the source path",
1332                fs_path.to_string(),
1333                old_base.to_string(),
1334                new_base.to_string()
1335            );
1336        }
1337        if new_base.path.is_empty() {
1338            new_path = [&fs_path.path[base_path.len()..]].concat().into();
1339        } else {
1340            new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1341                .concat()
1342                .into();
1343        }
1344    }
1345    Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1346}
1347
1348// Not turbo-tasks functions, only delegating
1349impl FileSystemPath {
1350    pub fn read(&self) -> Vc<FileContent> {
1351        self.fs().read(self.clone())
1352    }
1353
1354    pub fn read_link(&self) -> Vc<LinkContent> {
1355        self.fs().read_link(self.clone())
1356    }
1357
1358    pub fn read_json(&self) -> Vc<FileJsonContent> {
1359        self.fs().read(self.clone()).parse_json()
1360    }
1361
1362    pub fn read_json5(&self) -> Vc<FileJsonContent> {
1363        self.fs().read(self.clone()).parse_json5()
1364    }
1365
1366    /// Reads content of a directory.
1367    ///
1368    /// DETERMINISM: Result is in random order. Either sort result or do not
1369    /// depend on the order.
1370    pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1371        self.fs().raw_read_dir(self.clone())
1372    }
1373
1374    pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1375        self.fs().write(self.clone(), content)
1376    }
1377
1378    pub fn write_link(&self, target: Vc<LinkContent>) -> Vc<()> {
1379        self.fs().write_link(self.clone(), target)
1380    }
1381
1382    pub fn metadata(&self) -> Vc<FileMeta> {
1383        self.fs().metadata(self.clone())
1384    }
1385
1386    pub fn realpath(&self) -> Vc<FileSystemPath> {
1387        self.realpath_with_links().path()
1388    }
1389
1390    pub fn rebase(
1391        fs_path: FileSystemPath,
1392        old_base: FileSystemPath,
1393        new_base: FileSystemPath,
1394    ) -> Vc<FileSystemPath> {
1395        rebase(fs_path, old_base, new_base)
1396    }
1397}
1398
1399impl FileSystemPath {
1400    /// Reads content of a directory.
1401    ///
1402    /// DETERMINISM: Result is in random order. Either sort result or do not
1403    /// depend on the order.
1404    pub fn read_dir(&self) -> Vc<DirectoryContent> {
1405        read_dir(self.clone())
1406    }
1407
1408    pub fn parent(&self) -> FileSystemPath {
1409        let path = &self.path;
1410        if path.is_empty() {
1411            return self.clone();
1412        }
1413        FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1414    }
1415
1416    // It is important that get_type uses read_dir and not stat/metadata.
1417    // - `get_type` is called very very often during resolving and stat would
1418    // make it 1 syscall per call, whereas read_dir would make it 1 syscall per
1419    // directory.
1420    // - `metadata` allows you to use the "wrong" casing on
1421    // case-insensitive filesystems, while read_dir gives you the "correct"
1422    // casing. We want to enforce "correct" casing to avoid broken builds on
1423    // Vercel deployments (case-sensitive).
1424    pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1425        get_type(self.clone())
1426    }
1427
1428    pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1429        realpath_with_links(self.clone())
1430    }
1431}
1432
1433#[turbo_tasks::value_impl]
1434impl ValueToString for FileSystemPath {
1435    #[turbo_tasks::function]
1436    async fn to_string(&self) -> Result<Vc<RcStr>> {
1437        Ok(Vc::cell(
1438            format!("[{}]/{}", self.fs.to_string().await?, self.path).into(),
1439        ))
1440    }
1441}
1442
1443#[derive(Clone, Debug)]
1444#[turbo_tasks::value(shared)]
1445pub struct RealPathResult {
1446    pub path: FileSystemPath,
1447    pub symlinks: Vec<FileSystemPath>,
1448}
1449
1450#[turbo_tasks::value_impl]
1451impl RealPathResult {
1452    #[turbo_tasks::function]
1453    pub fn path(&self) -> Vc<FileSystemPath> {
1454        self.path.clone().cell()
1455    }
1456}
1457
1458#[derive(Clone, Copy, Debug, DeterministicHash, PartialOrd, Ord)]
1459#[turbo_tasks::value(shared)]
1460pub enum Permissions {
1461    Readable,
1462    Writable,
1463    Executable,
1464}
1465
1466impl Default for Permissions {
1467    fn default() -> Self {
1468        Self::Writable
1469    }
1470}
1471
1472// Only handle the permissions on unix platform for now
1473
1474#[cfg(target_family = "unix")]
1475impl From<Permissions> for std::fs::Permissions {
1476    fn from(perm: Permissions) -> Self {
1477        use std::os::unix::fs::PermissionsExt;
1478        match perm {
1479            Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1480            Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1481            Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1482        }
1483    }
1484}
1485
1486#[cfg(target_family = "unix")]
1487impl From<std::fs::Permissions> for Permissions {
1488    fn from(perm: std::fs::Permissions) -> Self {
1489        use std::os::unix::fs::PermissionsExt;
1490        if perm.readonly() {
1491            Permissions::Readable
1492        } else {
1493            // https://github.com/fitzgen/is_executable/blob/master/src/lib.rs#L96
1494            if perm.mode() & 0o111 != 0 {
1495                Permissions::Executable
1496            } else {
1497                Permissions::Writable
1498            }
1499        }
1500    }
1501}
1502
1503#[cfg(not(target_family = "unix"))]
1504impl From<std::fs::Permissions> for Permissions {
1505    fn from(_: std::fs::Permissions) -> Self {
1506        Permissions::default()
1507    }
1508}
1509
1510#[turbo_tasks::value(shared)]
1511#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
1512pub enum FileContent {
1513    Content(File),
1514    NotFound,
1515}
1516
1517impl From<File> for FileContent {
1518    fn from(file: File) -> Self {
1519        FileContent::Content(file)
1520    }
1521}
1522
1523impl From<File> for Vc<FileContent> {
1524    fn from(file: File) -> Self {
1525        FileContent::Content(file).cell()
1526    }
1527}
1528
1529#[derive(Clone, Debug, Eq, PartialEq)]
1530enum FileComparison {
1531    Create,
1532    Equal,
1533    NotEqual,
1534}
1535
1536impl FileContent {
1537    /// Performs a comparison of self's data against a disk file's streamed
1538    /// read.
1539    async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
1540        let old_file = extract_disk_access(
1541            retry_blocking(path.to_path_buf(), |path| std::fs::File::open(path)).await,
1542            path,
1543        )?;
1544        let Some(old_file) = old_file else {
1545            return Ok(match self {
1546                FileContent::NotFound => FileComparison::Equal,
1547                _ => FileComparison::Create,
1548            });
1549        };
1550        // We know old file exists, does the new file?
1551        let FileContent::Content(new_file) = self else {
1552            return Ok(FileComparison::NotEqual);
1553        };
1554
1555        let old_meta = extract_disk_access(
1556            retry_blocking(path.to_path_buf(), {
1557                let file_for_metadata = old_file.try_clone()?;
1558                move |_| file_for_metadata.metadata()
1559            })
1560            .await,
1561            path,
1562        )?;
1563        let Some(old_meta) = old_meta else {
1564            // If we failed to get meta, then the old file has been deleted between the
1565            // handle open. In which case, we just pretend the file never
1566            // existed.
1567            return Ok(FileComparison::Create);
1568        };
1569        // If the meta is different, we need to rewrite the file to update it.
1570        if new_file.meta != old_meta.into() {
1571            return Ok(FileComparison::NotEqual);
1572        }
1573
1574        // So meta matches, and we have a file handle. Let's stream the contents to see
1575        // if they match.
1576        let mut new_contents = new_file.read();
1577        let mut old_contents = BufReader::new(old_file);
1578        Ok(loop {
1579            let new_chunk = new_contents.fill_buf()?;
1580            let Ok(old_chunk) = old_contents.fill_buf() else {
1581                break FileComparison::NotEqual;
1582            };
1583
1584            let len = min(new_chunk.len(), old_chunk.len());
1585            if len == 0 {
1586                if new_chunk.len() == old_chunk.len() {
1587                    break FileComparison::Equal;
1588                } else {
1589                    break FileComparison::NotEqual;
1590                }
1591            }
1592
1593            if new_chunk[0..len] != old_chunk[0..len] {
1594                break FileComparison::NotEqual;
1595            }
1596
1597            new_contents.consume(len);
1598            old_contents.consume(len);
1599        })
1600    }
1601}
1602
1603bitflags! {
1604  #[derive(Default, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
1605  pub struct LinkType: u8 {
1606      const DIRECTORY = 0b00000001;
1607      const ABSOLUTE = 0b00000010;
1608  }
1609}
1610
1611#[turbo_tasks::value(shared)]
1612#[derive(Debug)]
1613pub enum LinkContent {
1614    // for the relative link, the target is raw value read from the link
1615    // for the absolute link, the target is stripped of the root path while reading
1616    // We don't use the `FileSystemPath` here for now, because the `FileSystemPath` is always
1617    // normalized, which means in `fn write_link` we couldn't restore the raw value of the file
1618    // link because there is only **dist** path in `fn write_link`, and we need the raw path if
1619    // we want to restore the link value in `fn write_link`
1620    Link { target: RcStr, link_type: LinkType },
1621    Invalid,
1622    NotFound,
1623}
1624
1625#[turbo_tasks::value(shared)]
1626#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
1627pub struct File {
1628    #[turbo_tasks(debug_ignore)]
1629    content: Rope,
1630    meta: FileMeta,
1631}
1632
1633impl File {
1634    /// Reads a [File] from the given path
1635    fn from_path(p: &Path) -> io::Result<Self> {
1636        let mut file = std::fs::File::open(p)?;
1637        let metadata = file.metadata()?;
1638
1639        let mut output = Vec::with_capacity(metadata.len() as usize);
1640        file.read_to_end(&mut output)?;
1641
1642        Ok(File {
1643            meta: metadata.into(),
1644            content: Rope::from(output),
1645        })
1646    }
1647
1648    /// Creates a [File] from raw bytes.
1649    fn from_bytes(content: Vec<u8>) -> Self {
1650        File {
1651            meta: FileMeta::default(),
1652            content: Rope::from(content),
1653        }
1654    }
1655
1656    /// Creates a [File] from a rope.
1657    fn from_rope(content: Rope) -> Self {
1658        File {
1659            meta: FileMeta::default(),
1660            content,
1661        }
1662    }
1663
1664    /// Returns the content type associated with this file.
1665    pub fn content_type(&self) -> Option<&Mime> {
1666        self.meta.content_type.as_ref()
1667    }
1668
1669    /// Sets the content type associated with this file.
1670    pub fn with_content_type(mut self, content_type: Mime) -> Self {
1671        self.meta.content_type = Some(content_type);
1672        self
1673    }
1674
1675    /// Returns a Read/AsyncRead/Stream/Iterator to access the File's contents.
1676    pub fn read(&self) -> RopeReader {
1677        self.content.read()
1678    }
1679}
1680
1681impl Debug for File {
1682    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1683        f.debug_struct("File")
1684            .field("meta", &self.meta)
1685            .field("content (hash)", &hash_xxh3_hash64(&self.content))
1686            .finish()
1687    }
1688}
1689
1690impl From<RcStr> for File {
1691    fn from(s: RcStr) -> Self {
1692        s.into_owned().into()
1693    }
1694}
1695
1696impl From<String> for File {
1697    fn from(s: String) -> Self {
1698        File::from_bytes(s.into_bytes())
1699    }
1700}
1701
1702impl From<ReadRef<RcStr>> for File {
1703    fn from(s: ReadRef<RcStr>) -> Self {
1704        File::from_bytes(s.as_bytes().to_vec())
1705    }
1706}
1707
1708impl From<&str> for File {
1709    fn from(s: &str) -> Self {
1710        File::from_bytes(s.as_bytes().to_vec())
1711    }
1712}
1713
1714impl From<Vec<u8>> for File {
1715    fn from(bytes: Vec<u8>) -> Self {
1716        File::from_bytes(bytes)
1717    }
1718}
1719
1720impl From<&[u8]> for File {
1721    fn from(bytes: &[u8]) -> Self {
1722        File::from_bytes(bytes.to_vec())
1723    }
1724}
1725
1726impl From<ReadRef<Rope>> for File {
1727    fn from(rope: ReadRef<Rope>) -> Self {
1728        File::from_rope(ReadRef::into_owned(rope))
1729    }
1730}
1731
1732impl From<Rope> for File {
1733    fn from(rope: Rope) -> Self {
1734        File::from_rope(rope)
1735    }
1736}
1737
1738impl File {
1739    pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
1740        Self {
1741            meta,
1742            content: Rope::from(content),
1743        }
1744    }
1745
1746    /// Returns the associated [FileMeta] of this file.
1747    pub fn meta(&self) -> &FileMeta {
1748        &self.meta
1749    }
1750
1751    /// Returns the immutable contents of this file.
1752    pub fn content(&self) -> &Rope {
1753        &self.content
1754    }
1755}
1756
1757mod mime_option_serde {
1758    use std::{fmt, str::FromStr};
1759
1760    use mime::Mime;
1761    use serde::{Deserializer, Serializer, de};
1762
1763    pub fn serialize<S>(mime: &Option<Mime>, serializer: S) -> Result<S::Ok, S::Error>
1764    where
1765        S: Serializer,
1766    {
1767        if let Some(mime) = mime {
1768            serializer.serialize_str(mime.as_ref())
1769        } else {
1770            serializer.serialize_str("")
1771        }
1772    }
1773
1774    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Mime>, D::Error>
1775    where
1776        D: Deserializer<'de>,
1777    {
1778        struct Visitor;
1779
1780        impl de::Visitor<'_> for Visitor {
1781            type Value = Option<Mime>;
1782
1783            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1784                formatter.write_str("a valid MIME type or empty string")
1785            }
1786
1787            fn visit_str<E>(self, value: &str) -> Result<Option<Mime>, E>
1788            where
1789                E: de::Error,
1790            {
1791                if value.is_empty() {
1792                    Ok(None)
1793                } else {
1794                    Mime::from_str(value)
1795                        .map(Some)
1796                        .map_err(|e| E::custom(format!("{e}")))
1797                }
1798            }
1799        }
1800
1801        deserializer.deserialize_str(Visitor)
1802    }
1803}
1804
1805#[turbo_tasks::value(shared)]
1806#[derive(Debug, Clone, Default)]
1807pub struct FileMeta {
1808    // Size of the file
1809    // len: u64,
1810    permissions: Permissions,
1811    #[serde(with = "mime_option_serde")]
1812    #[turbo_tasks(trace_ignore)]
1813    content_type: Option<Mime>,
1814}
1815
1816impl Ord for FileMeta {
1817    fn cmp(&self, other: &Self) -> Ordering {
1818        self.permissions
1819            .cmp(&other.permissions)
1820            .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
1821    }
1822}
1823
1824impl PartialOrd for FileMeta {
1825    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1826        Some(self.cmp(other))
1827    }
1828}
1829
1830impl From<std::fs::Metadata> for FileMeta {
1831    fn from(meta: std::fs::Metadata) -> Self {
1832        let permissions = meta.permissions().into();
1833
1834        Self {
1835            permissions,
1836            content_type: None,
1837        }
1838    }
1839}
1840
1841impl DeterministicHash for FileMeta {
1842    fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
1843        self.permissions.deterministic_hash(state);
1844        if let Some(content_type) = &self.content_type {
1845            content_type.to_string().deterministic_hash(state);
1846        }
1847    }
1848}
1849
1850impl FileContent {
1851    pub fn new(file: File) -> Self {
1852        FileContent::Content(file)
1853    }
1854
1855    pub fn is_content(&self) -> bool {
1856        matches!(self, FileContent::Content(_))
1857    }
1858
1859    pub fn as_content(&self) -> Option<&File> {
1860        match self {
1861            FileContent::Content(file) => Some(file),
1862            FileContent::NotFound => None,
1863        }
1864    }
1865
1866    pub fn parse_json_ref(&self) -> FileJsonContent {
1867        match self {
1868            FileContent::Content(file) => {
1869                let content = file.content.clone().into_bytes();
1870                let de = &mut serde_json::Deserializer::from_slice(&content);
1871                match serde_path_to_error::deserialize(de) {
1872                    Ok(data) => FileJsonContent::Content(data),
1873                    Err(e) => FileJsonContent::Unparsable(Box::new(
1874                        UnparsableJson::from_serde_path_to_error(e),
1875                    )),
1876                }
1877            }
1878            FileContent::NotFound => FileJsonContent::NotFound,
1879        }
1880    }
1881
1882    pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
1883        match self {
1884            FileContent::Content(file) => match file.content.to_str() {
1885                Ok(string) => match parse_to_serde_value(
1886                    &string,
1887                    &ParseOptions {
1888                        allow_comments: true,
1889                        allow_trailing_commas: true,
1890                        allow_loose_object_property_names: false,
1891                    },
1892                ) {
1893                    Ok(data) => match data {
1894                        Some(value) => FileJsonContent::Content(value),
1895                        None => FileJsonContent::unparsable(rcstr!(
1896                            "text content doesn't contain any json data"
1897                        )),
1898                    },
1899                    Err(e) => FileJsonContent::Unparsable(Box::new(
1900                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
1901                    )),
1902                },
1903                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
1904            },
1905            FileContent::NotFound => FileJsonContent::NotFound,
1906        }
1907    }
1908
1909    pub fn parse_json5_ref(&self) -> FileJsonContent {
1910        match self {
1911            FileContent::Content(file) => match file.content.to_str() {
1912                Ok(string) => match parse_to_serde_value(
1913                    &string,
1914                    &ParseOptions {
1915                        allow_comments: true,
1916                        allow_trailing_commas: true,
1917                        allow_loose_object_property_names: true,
1918                    },
1919                ) {
1920                    Ok(data) => match data {
1921                        Some(value) => FileJsonContent::Content(value),
1922                        None => FileJsonContent::unparsable(rcstr!(
1923                            "text content doesn't contain any json data"
1924                        )),
1925                    },
1926                    Err(e) => FileJsonContent::Unparsable(Box::new(
1927                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
1928                    )),
1929                },
1930                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
1931            },
1932            FileContent::NotFound => FileJsonContent::NotFound,
1933        }
1934    }
1935
1936    pub fn lines_ref(&self) -> FileLinesContent {
1937        match self {
1938            FileContent::Content(file) => match file.content.to_str() {
1939                Ok(string) => {
1940                    let mut bytes_offset = 0;
1941                    FileLinesContent::Lines(
1942                        string
1943                            .split('\n')
1944                            .map(|l| {
1945                                let line = FileLine {
1946                                    content: l.to_string(),
1947                                    bytes_offset,
1948                                };
1949                                bytes_offset += (l.len() + 1) as u32;
1950                                line
1951                            })
1952                            .collect(),
1953                    )
1954                }
1955                Err(_) => FileLinesContent::Unparsable,
1956            },
1957            FileContent::NotFound => FileLinesContent::NotFound,
1958        }
1959    }
1960}
1961
1962#[turbo_tasks::value_impl]
1963impl FileContent {
1964    #[turbo_tasks::function]
1965    pub fn len(&self) -> Result<Vc<Option<u64>>> {
1966        Ok(Vc::cell(match self {
1967            FileContent::Content(file) => Some(file.content.len() as u64),
1968            FileContent::NotFound => None,
1969        }))
1970    }
1971
1972    #[turbo_tasks::function]
1973    pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
1974        Ok(self.parse_json_ref().into())
1975    }
1976
1977    #[turbo_tasks::function]
1978    pub async fn parse_json_with_comments(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
1979        let this = self.await?;
1980        Ok(this.parse_json_with_comments_ref().into())
1981    }
1982
1983    #[turbo_tasks::function]
1984    pub async fn parse_json5(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
1985        let this = self.await?;
1986        Ok(this.parse_json5_ref().into())
1987    }
1988
1989    #[turbo_tasks::function]
1990    pub async fn lines(self: Vc<Self>) -> Result<Vc<FileLinesContent>> {
1991        let this = self.await?;
1992        Ok(this.lines_ref().into())
1993    }
1994
1995    #[turbo_tasks::function]
1996    pub async fn hash(self: Vc<Self>) -> Result<Vc<u64>> {
1997        Ok(Vc::cell(hash_xxh3_hash64(&self.await?)))
1998    }
1999}
2000
2001/// A file's content interpreted as a JSON value.
2002#[turbo_tasks::value(shared, serialization = "none")]
2003pub enum FileJsonContent {
2004    Content(Value),
2005    Unparsable(Box<UnparsableJson>),
2006    NotFound,
2007}
2008
2009#[turbo_tasks::value_impl]
2010impl ValueToString for FileJsonContent {
2011    /// Returns the JSON file content as a UTF-8 string.
2012    ///
2013    /// This operation will only succeed if the file contents are a valid JSON
2014    /// value.
2015    #[turbo_tasks::function]
2016    fn to_string(&self) -> Result<Vc<RcStr>> {
2017        match self {
2018            FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2019            FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2020            FileJsonContent::NotFound => Err(anyhow!("File not found")),
2021        }
2022    }
2023}
2024
2025#[turbo_tasks::value_impl]
2026impl FileJsonContent {
2027    #[turbo_tasks::function]
2028    pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2029        match &*self.await? {
2030            FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2031            FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2032            FileJsonContent::NotFound => Err(anyhow!("File not found")),
2033        }
2034    }
2035}
2036impl FileJsonContent {
2037    pub fn unparsable(message: RcStr) -> Self {
2038        FileJsonContent::Unparsable(Box::new(UnparsableJson {
2039            message,
2040            path: None,
2041            start_location: None,
2042            end_location: None,
2043        }))
2044    }
2045
2046    pub fn unparsable_with_message(message: RcStr) -> Self {
2047        FileJsonContent::Unparsable(Box::new(UnparsableJson {
2048            message,
2049            path: None,
2050            start_location: None,
2051            end_location: None,
2052        }))
2053    }
2054}
2055
2056#[derive(Debug, PartialEq, Eq)]
2057pub struct FileLine {
2058    pub content: String,
2059    pub bytes_offset: u32,
2060}
2061
2062#[turbo_tasks::value(shared, serialization = "none")]
2063pub enum FileLinesContent {
2064    Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2065    Unparsable,
2066    NotFound,
2067}
2068
2069#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
2070pub enum RawDirectoryEntry {
2071    File,
2072    Directory,
2073    Symlink,
2074    Other,
2075    Error,
2076}
2077
2078#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
2079pub enum DirectoryEntry {
2080    File(FileSystemPath),
2081    Directory(FileSystemPath),
2082    Symlink(FileSystemPath),
2083    Other(FileSystemPath),
2084    Error,
2085}
2086
2087impl DirectoryEntry {
2088    /// Handles the `DirectoryEntry::Symlink` variant by checking the symlink target
2089    /// type and replacing it with `DirectoryEntry::File` or
2090    /// `DirectoryEntry::Directory`.
2091    pub async fn resolve_symlink(self) -> Result<Self> {
2092        if let DirectoryEntry::Symlink(symlink) = &self {
2093            let real_path = symlink.realpath().owned().await?;
2094            match *real_path.get_type().await? {
2095                FileSystemEntryType::Directory => Ok(DirectoryEntry::Directory(real_path)),
2096                FileSystemEntryType::File => Ok(DirectoryEntry::File(real_path)),
2097                _ => Ok(self),
2098            }
2099        } else {
2100            Ok(self)
2101        }
2102    }
2103
2104    pub fn path(self) -> Option<FileSystemPath> {
2105        match self {
2106            DirectoryEntry::File(path)
2107            | DirectoryEntry::Directory(path)
2108            | DirectoryEntry::Symlink(path)
2109            | DirectoryEntry::Other(path) => Some(path),
2110            DirectoryEntry::Error => None,
2111        }
2112    }
2113}
2114
2115#[turbo_tasks::value]
2116#[derive(Hash, Clone, Copy, Debug)]
2117pub enum FileSystemEntryType {
2118    NotFound,
2119    File,
2120    Directory,
2121    Symlink,
2122    Other,
2123    Error,
2124}
2125
2126impl From<FileType> for FileSystemEntryType {
2127    fn from(file_type: FileType) -> Self {
2128        match file_type {
2129            t if t.is_dir() => FileSystemEntryType::Directory,
2130            t if t.is_file() => FileSystemEntryType::File,
2131            t if t.is_symlink() => FileSystemEntryType::Symlink,
2132            _ => FileSystemEntryType::Other,
2133        }
2134    }
2135}
2136
2137impl From<DirectoryEntry> for FileSystemEntryType {
2138    fn from(entry: DirectoryEntry) -> Self {
2139        FileSystemEntryType::from(&entry)
2140    }
2141}
2142
2143impl From<&DirectoryEntry> for FileSystemEntryType {
2144    fn from(entry: &DirectoryEntry) -> Self {
2145        match entry {
2146            DirectoryEntry::File(_) => FileSystemEntryType::File,
2147            DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2148            DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2149            DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2150            DirectoryEntry::Error => FileSystemEntryType::Error,
2151        }
2152    }
2153}
2154
2155impl From<RawDirectoryEntry> for FileSystemEntryType {
2156    fn from(entry: RawDirectoryEntry) -> Self {
2157        FileSystemEntryType::from(&entry)
2158    }
2159}
2160
2161impl From<&RawDirectoryEntry> for FileSystemEntryType {
2162    fn from(entry: &RawDirectoryEntry) -> Self {
2163        match entry {
2164            RawDirectoryEntry::File => FileSystemEntryType::File,
2165            RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2166            RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2167            RawDirectoryEntry::Other => FileSystemEntryType::Other,
2168            RawDirectoryEntry::Error => FileSystemEntryType::Error,
2169        }
2170    }
2171}
2172
2173#[turbo_tasks::value]
2174#[derive(Debug)]
2175pub enum RawDirectoryContent {
2176    // The entry keys are the directory relative file names
2177    // e.g. for `/bar/foo`, it will be `foo`
2178    Entries(AutoMap<RcStr, RawDirectoryEntry>),
2179    NotFound,
2180}
2181
2182impl RawDirectoryContent {
2183    pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2184        Self::cell(RawDirectoryContent::Entries(entries))
2185    }
2186
2187    pub fn not_found() -> Vc<Self> {
2188        Self::cell(RawDirectoryContent::NotFound)
2189    }
2190}
2191
2192#[turbo_tasks::value]
2193#[derive(Debug)]
2194pub enum DirectoryContent {
2195    Entries(AutoMap<RcStr, DirectoryEntry>),
2196    NotFound,
2197}
2198
2199impl DirectoryContent {
2200    pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2201        Self::cell(DirectoryContent::Entries(entries))
2202    }
2203
2204    pub fn not_found() -> Vc<Self> {
2205        Self::cell(DirectoryContent::NotFound)
2206    }
2207}
2208
2209#[turbo_tasks::value(shared)]
2210pub struct NullFileSystem;
2211
2212#[turbo_tasks::value_impl]
2213impl FileSystem for NullFileSystem {
2214    #[turbo_tasks::function]
2215    fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2216        FileContent::NotFound.cell()
2217    }
2218
2219    #[turbo_tasks::function]
2220    fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2221        LinkContent::NotFound.into()
2222    }
2223
2224    #[turbo_tasks::function]
2225    fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2226        RawDirectoryContent::not_found()
2227    }
2228
2229    #[turbo_tasks::function]
2230    fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) -> Vc<()> {
2231        Vc::default()
2232    }
2233
2234    #[turbo_tasks::function]
2235    fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) -> Vc<()> {
2236        Vc::default()
2237    }
2238
2239    #[turbo_tasks::function]
2240    fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2241        FileMeta::default().cell()
2242    }
2243}
2244
2245#[turbo_tasks::value_impl]
2246impl ValueToString for NullFileSystem {
2247    #[turbo_tasks::function]
2248    fn to_string(&self) -> Vc<RcStr> {
2249        Vc::cell(rcstr!("null"))
2250    }
2251}
2252
2253pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2254    loop {
2255        if let Some(fs) = Vc::try_resolve_downcast_type::<AttachedFileSystem>(path.fs()).await? {
2256            path = fs.get_inner_fs_path(path).owned().await?;
2257            continue;
2258        }
2259
2260        if let Some(fs) = Vc::try_resolve_downcast_type::<DiskFileSystem>(path.fs()).await? {
2261            let sys_path = fs.await?.to_sys_path(path)?;
2262            return Ok(Some(sys_path));
2263        }
2264
2265        return Ok(None);
2266    }
2267}
2268
2269#[turbo_tasks::function]
2270async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2271    let fs = path.fs().to_resolved().await?;
2272    match &*fs.raw_read_dir(path.clone()).await? {
2273        RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2274        RawDirectoryContent::Entries(entries) => {
2275            let mut normalized_entries = AutoMap::new();
2276            let dir_path = &path.path;
2277            for (name, entry) in entries {
2278                // Construct the path directly instead of going through `join`.
2279                // We do not need to normalize since the `name` is guaranteed to be a simple
2280                // path segment.
2281                let path = if dir_path.is_empty() {
2282                    name.clone()
2283                } else {
2284                    RcStr::from(format!("{dir_path}/{name}"))
2285                };
2286
2287                let entry_path = FileSystemPath::new_normalized(fs, path);
2288                let entry = match entry {
2289                    RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2290                    RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2291                    RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2292                    RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2293                    RawDirectoryEntry::Error => DirectoryEntry::Error,
2294                };
2295                normalized_entries.insert(name.clone(), entry);
2296            }
2297            Ok(DirectoryContent::new(normalized_entries))
2298        }
2299    }
2300}
2301
2302#[turbo_tasks::function]
2303async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2304    if path.is_root() {
2305        return Ok(FileSystemEntryType::Directory.cell());
2306    }
2307    let parent = path.parent();
2308    let dir_content = parent.raw_read_dir().await?;
2309    match &*dir_content {
2310        RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2311        RawDirectoryContent::Entries(entries) => {
2312            let (_, file_name) = path.split_file_name();
2313            if let Some(entry) = entries.get(file_name) {
2314                Ok(FileSystemEntryType::from(entry).cell())
2315            } else {
2316                Ok(FileSystemEntryType::NotFound.cell())
2317            }
2318        }
2319    }
2320}
2321
2322#[turbo_tasks::function]
2323async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2324    let mut current_vc = path.clone();
2325    let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2326    let mut visited: AutoSet<RcStr> = AutoSet::new();
2327    // Pick some arbitrary symlink depth limit... similar to the ELOOP logic for realpath(3).
2328    // SYMLOOP_MAX is 40 for Linux: https://unix.stackexchange.com/q/721724
2329    for _i in 0..40 {
2330        let current = current_vc.clone();
2331        if current.is_root() {
2332            // fast path
2333            return Ok(RealPathResult {
2334                path: current_vc,
2335                symlinks: symlinks.into_iter().collect(),
2336            }
2337            .cell());
2338        }
2339
2340        if !visited.insert(current.path.clone()) {
2341            break; // we detected a cycle
2342        }
2343
2344        // see if a parent segment of the path is a symlink and resolve that first
2345        let parent = current_vc.parent();
2346        let parent_result = parent.realpath_with_links().owned().await?;
2347        let basename = current
2348            .path
2349            .rsplit_once('/')
2350            .map_or(current.path.as_str(), |(_, name)| name);
2351        if parent_result.path != parent {
2352            current_vc = parent_result.path.join(basename)?;
2353        }
2354        symlinks.extend(parent_result.symlinks);
2355
2356        // use `get_type` before trying `read_link`, as there's a good chance of a cache hit on
2357        // `get_type`, and `read_link` isn't the common codepath.
2358        if !matches!(*current_vc.get_type().await?, FileSystemEntryType::Symlink) {
2359            return Ok(RealPathResult {
2360                path: current_vc,
2361                symlinks: symlinks.into_iter().collect(), // convert set to vec
2362            }
2363            .cell());
2364        }
2365
2366        if let LinkContent::Link { target, link_type } = &*current_vc.read_link().await? {
2367            symlinks.insert(current_vc.clone());
2368            current_vc = if link_type.contains(LinkType::ABSOLUTE) {
2369                current_vc.root().owned().await?
2370            } else {
2371                parent_result.path
2372            }
2373            .join(target)?;
2374        } else {
2375            // get_type() and read_link() might disagree temporarily due to turbo-tasks
2376            // eventual consistency or if the file gets invalidated before the directory does
2377            return Ok(RealPathResult {
2378                path: current_vc,
2379                symlinks: symlinks.into_iter().collect(), // convert set to vec
2380            }
2381            .cell());
2382        }
2383    }
2384
2385    // Too many attempts or detected a cycle, we bailed out!
2386    //
2387    // TODO: There's no proper way to indicate an non-turbo-tasks error here, so just return the
2388    // original path and all the symlinks we followed.
2389    //
2390    // Returning the followed symlinks is still important, even if there is an error! Otherwise
2391    // we may never notice if the symlink loop is fixed.
2392    Ok(RealPathResult {
2393        path,
2394        symlinks: symlinks.into_iter().collect(),
2395    }
2396    .cell())
2397}
2398
2399pub fn register() {
2400    turbo_tasks::register();
2401    include!(concat!(env!("OUT_DIR"), "/register.rs"));
2402}
2403
2404#[cfg(test)]
2405mod tests {
2406    use turbo_rcstr::rcstr;
2407
2408    use super::*;
2409
2410    #[test]
2411    fn test_get_relative_path_to() {
2412        assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2413        assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2414        assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2415        assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2416        assert_eq!(
2417            get_relative_path_to("a/b/c", "c/b/a").as_str(),
2418            "../../../c/b/a"
2419        );
2420        assert_eq!(
2421            get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2422            "../../../c/b/a"
2423        );
2424    }
2425
2426    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2427    async fn with_extension() {
2428        crate::register();
2429
2430        turbo_tasks_testing::VcStorage::with(async {
2431            let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2432                .to_resolved()
2433                .await?;
2434
2435            let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2436
2437            let path_json = path_txt.with_extension("json");
2438            assert_eq!(&*path_json.path, "foo/bar.json");
2439
2440            let path_no_ext = path_txt.with_extension("");
2441            assert_eq!(&*path_no_ext.path, "foo/bar");
2442
2443            let path_new_ext = path_no_ext.with_extension("json");
2444            assert_eq!(&*path_new_ext.path, "foo/bar.json");
2445
2446            let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2447
2448            let path_no_slash_json = path_no_slash_txt.with_extension("json");
2449            assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2450
2451            let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2452            assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2453
2454            let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2455            assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2456
2457            anyhow::Ok(())
2458        })
2459        .await
2460        .unwrap()
2461    }
2462
2463    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2464    async fn file_stem() {
2465        crate::register();
2466
2467        turbo_tasks_testing::VcStorage::with(async {
2468            let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2469                .to_resolved()
2470                .await?;
2471
2472            let path = FileSystemPath::new_normalized(fs, rcstr!(""));
2473            assert_eq!(path.file_stem(), None);
2474
2475            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2476            assert_eq!(path.file_stem(), Some("bar"));
2477
2478            let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2479            assert_eq!(path.file_stem(), Some("bar"));
2480
2481            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
2482            assert_eq!(path.file_stem(), Some("bar"));
2483
2484            let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
2485            assert_eq!(path.file_stem(), Some(".bar"));
2486
2487            anyhow::Ok(())
2488        })
2489        .await
2490        .unwrap()
2491    }
2492}