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