turbo_tasks_fs/
lib.rs

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