1#![allow(clippy::needless_return)] #![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
79pub const MAX_SAFE_FILE_NAME_LENGTH: usize = 200;
91
92pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
117 #[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 #[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 const MAX_PATH_LENGTH: usize = 1024 - 8;
147
148 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 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 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 #[turbo_tasks(debug_ignore, trace_ignore)]
232 #[serde(skip)]
233 invalidation_lock: RwLock<()>,
234 #[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 fn root_path(&self) -> &Path {
246 simplified(Path::new(&*self.root))
247 }
248
249 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 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 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 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 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 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 #[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 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 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 #[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 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 let old_invalidators = inner.register_write_invalidator(
730 &full_path,
731 invalidator,
732 WriteContent::File(content.clone()),
733 )?;
734
735 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 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 #[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 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 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 fn split_extension(&self) -> (&str, Option<&str>) {
1103 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1104 if extension.contains('/') ||
1105 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 fn split_file_name(&self) -> (Option<&str>, &str) {
1121 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 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 #[turbo_tasks::function]
1161 fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Vc<Self> {
1162 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 #[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 #[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 #[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 #[turbo_tasks::function]
1238 pub async fn try_join(&self, path: RcStr) -> Result<Vc<FileSystemPathOption>> {
1239 #[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 #[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 #[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 #[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 match extension.is_empty() {
1312 true => path_without_extension.into(),
1313 false => format!("{path_without_extension}.{extension}").into(),
1314 },
1315 )
1316 }
1317
1318 #[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
1379impl 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 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 #[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 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 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 for _i in 0..40 {
1523 let current = current_vc.await?;
1524 if current.is_root() {
1525 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; }
1536
1537 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 if !matches!(*current_vc.get_type().await?, FileSystemEntryType::Symlink) {
1556 return Ok(RealPathResult {
1557 path: current_vc,
1558 symlinks: symlinks.into_iter().collect(), }
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 return Ok(RealPathResult {
1577 path: current_vc,
1578 symlinks: symlinks.into_iter().collect(), }
1580 .cell());
1581 }
1582 }
1583
1584 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#[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 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 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 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 return Ok(FileComparison::Create);
1724 };
1725 if new_file.meta != old_meta.into() {
1727 return Ok(FileComparison::NotEqual);
1728 }
1729
1730 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 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 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 fn from_bytes(content: Vec<u8>) -> Self {
1806 File {
1807 meta: FileMeta::default(),
1808 content: Rope::from(content),
1809 }
1810 }
1811
1812 fn from_rope(content: Rope) -> Self {
1814 File {
1815 meta: FileMeta::default(),
1816 content,
1817 }
1818 }
1819
1820 pub fn content_type(&self) -> Option<&Mime> {
1822 self.meta.content_type.as_ref()
1823 }
1824
1825 pub fn with_content_type(mut self, content_type: Mime) -> Self {
1827 self.meta.content_type = Some(content_type);
1828 self
1829 }
1830
1831 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 pub fn meta(&self) -> &FileMeta {
1904 &self.meta
1905 }
1906
1907 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 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#[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 #[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 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 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}