1#![feature(arbitrary_self_types)]
2#![feature(arbitrary_self_types_pointers)]
3#![feature(btree_cursors)] #![feature(io_error_more)]
5#![feature(iter_advance_by)]
6#![feature(min_specialization)]
7#![feature(normalize_lexically)]
10#![feature(trivial_bounds)]
11#![allow(clippy::needless_return)] #![allow(clippy::mutable_key_type)]
13
14pub mod attach;
15pub mod embed;
16pub mod glob;
17mod globset;
18pub mod invalidation;
19mod invalidator_map;
20pub mod json;
21mod mutex_map;
22mod path_map;
23mod read_glob;
24mod retry;
25pub mod rope;
26pub mod source_context;
27pub mod util;
28pub(crate) mod virtual_fs;
29mod watcher;
30
31use std::{
32 borrow::Cow,
33 cmp::{Ordering, min},
34 env,
35 fmt::{self, Debug, Display, Formatter},
36 fs::FileType,
37 future::Future,
38 io::{self, BufRead, BufReader, ErrorKind, Read},
39 mem::take,
40 path::{MAIN_SEPARATOR, Path, PathBuf},
41 sync::{Arc, LazyLock},
42 time::Duration,
43};
44
45use anyhow::{Context, Result, anyhow, bail};
46use auto_hash_map::{AutoMap, AutoSet};
47use bitflags::bitflags;
48use dunce::simplified;
49use indexmap::IndexSet;
50use jsonc_parser::{ParseOptions, parse_to_serde_value};
51use mime::Mime;
52use rustc_hash::FxHashSet;
53use serde::{Deserialize, Serialize};
54use serde_json::Value;
55use tokio::sync::{RwLock, RwLockReadGuard};
56use tracing::Instrument;
57use turbo_rcstr::{RcStr, rcstr};
58use turbo_tasks::{
59 ApplyEffectsContext, Completion, InvalidationReason, Invalidator, NonLocalValue, ReadRef,
60 ResolvedVc, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, effect,
61 mark_session_dependent, mark_stateful, parallel, trace::TraceRawVcs,
62};
63use turbo_tasks_hash::{DeterministicHash, DeterministicHasher, hash_xxh3_hash64};
64use turbo_unix_path::{
65 get_parent_path, get_relative_path_to, join_path, normalize_path, sys_to_unix, unix_to_sys,
66};
67
68use crate::{
69 attach::AttachedFileSystem,
70 glob::Glob,
71 invalidation::Write,
72 invalidator_map::{InvalidatorMap, WriteContent},
73 json::UnparsableJson,
74 mutex_map::MutexMap,
75 read_glob::{read_glob, track_glob},
76 retry::retry_blocking,
77 rope::{Rope, RopeReader},
78 util::extract_disk_access,
79 watcher::DiskWatcher,
80};
81pub use crate::{read_glob::ReadGlobResult, virtual_fs::VirtualFileSystem};
82
83pub const MAX_SAFE_FILE_NAME_LENGTH: usize = 200;
95
96pub fn validate_path_length(path: &Path) -> Result<Cow<'_, Path>> {
121 #[cfg(target_family = "windows")]
124 fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
125 const MAX_PATH_LENGTH_WINDOWS: usize = 260;
126 const UNC_PREFIX: &str = "\\\\?\\";
127
128 if path.starts_with(UNC_PREFIX) {
129 return Ok(path.into());
130 }
131
132 if path.as_os_str().len() > MAX_PATH_LENGTH_WINDOWS {
133 let new_path = std::fs::canonicalize(path)
134 .map_err(|_| anyhow!("file is too long, and could not be normalized"))?;
135 return Ok(new_path.into());
136 }
137
138 Ok(path.into())
139 }
140
141 #[cfg(not(target_family = "windows"))]
145 fn validate_path_length_inner(path: &Path) -> Result<Cow<'_, Path>> {
146 const MAX_FILE_NAME_LENGTH_UNIX: usize = 255;
147 const MAX_PATH_LENGTH: usize = 1024 - 8;
151
152 if path
154 .file_name()
155 .map(|n| n.as_encoded_bytes().len())
156 .unwrap_or(0)
157 > MAX_FILE_NAME_LENGTH_UNIX
158 {
159 anyhow::bail!(
160 "file name is too long (exceeds {} bytes)",
161 MAX_FILE_NAME_LENGTH_UNIX
162 );
163 }
164
165 if path.as_os_str().len() > MAX_PATH_LENGTH {
166 anyhow::bail!("path is too long (exceeds {} bytes)", MAX_PATH_LENGTH);
167 }
168
169 Ok(path.into())
170 }
171
172 validate_path_length_inner(path).with_context(|| {
173 format!(
174 "path length for file {} exceeds max length of filesystem",
175 path.to_string_lossy()
176 )
177 })
178}
179
180trait ConcurrencyLimitedExt {
181 type Output;
182 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output;
183}
184
185impl<F, R> ConcurrencyLimitedExt for F
186where
187 F: Future<Output = R>,
188{
189 type Output = R;
190 async fn concurrency_limited(self, semaphore: &tokio::sync::Semaphore) -> Self::Output {
191 let _permit = semaphore.acquire().await;
192 self.await
193 }
194}
195
196fn create_semaphore() -> tokio::sync::Semaphore {
197 static NEXT_TURBOPACK_IO_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
200 env::var("NEXT_TURBOPACK_IO_CONCURRENCY")
201 .ok()
202 .filter(|val| !val.is_empty())
203 .map(|val| {
204 val.parse()
205 .expect("NEXT_TURBOPACK_IO_CONCURRENCY must be a valid integer")
206 })
207 .filter(|val| *val != 0)
208 .unwrap_or(256)
209 });
210 tokio::sync::Semaphore::new(*NEXT_TURBOPACK_IO_CONCURRENCY)
211}
212
213#[turbo_tasks::value_trait]
214pub trait FileSystem: ValueToString {
215 #[turbo_tasks::function]
217 fn root(self: ResolvedVc<Self>) -> Vc<FileSystemPath> {
218 FileSystemPath::new_normalized(self, RcStr::default()).cell()
219 }
220 #[turbo_tasks::function]
221 fn read(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileContent>;
222 #[turbo_tasks::function]
223 fn read_link(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<LinkContent>;
224 #[turbo_tasks::function]
225 fn raw_read_dir(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<RawDirectoryContent>;
226 #[turbo_tasks::function]
227 fn write(self: Vc<Self>, fs_path: FileSystemPath, content: Vc<FileContent>) -> Vc<()>;
228 #[turbo_tasks::function]
229 fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
230 #[turbo_tasks::function]
231 fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
232}
233
234#[derive(Default)]
235struct DiskFileSystemApplyContext {
236 created_directories: FxHashSet<PathBuf>,
238}
239
240#[derive(Serialize, Deserialize, TraceRawVcs, ValueDebugFormat, NonLocalValue)]
241struct DiskFileSystemInner {
242 pub name: RcStr,
243 pub root: RcStr,
244 #[turbo_tasks(debug_ignore, trace_ignore)]
245 #[serde(skip)]
246 mutex_map: MutexMap<PathBuf>,
247 #[turbo_tasks(debug_ignore, trace_ignore)]
248 #[serde(skip)]
249 invalidator_map: InvalidatorMap,
250 #[turbo_tasks(debug_ignore, trace_ignore)]
251 #[serde(skip)]
252 dir_invalidator_map: InvalidatorMap,
253 #[turbo_tasks(debug_ignore, trace_ignore)]
256 #[serde(skip)]
257 invalidation_lock: RwLock<()>,
258 #[turbo_tasks(debug_ignore, trace_ignore)]
260 #[serde(skip, default = "create_semaphore")]
261 semaphore: tokio::sync::Semaphore,
262
263 #[turbo_tasks(debug_ignore, trace_ignore)]
264 watcher: DiskWatcher,
265 denied_path: Option<RcStr>,
268}
269
270impl DiskFileSystemInner {
271 fn root_path(&self) -> &Path {
273 simplified(Path::new(&*self.root))
275 }
276
277 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
287 let Some(denied_path) = &self.denied_path else {
288 return false;
289 };
290 let path = &path.path;
295 path.starts_with(denied_path.as_str())
296 && (path.len() == denied_path.len()
297 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
298 }
299
300 fn register_read_invalidator(&self, path: &Path) -> Result<()> {
303 if let Some(invalidator) = turbo_tasks::get_invalidator() {
304 self.invalidator_map
305 .insert(path.to_owned(), invalidator, None);
306 self.watcher.ensure_watched_file(path, self.root_path())?;
307 }
308 Ok(())
309 }
310
311 fn register_write_invalidator(
315 &self,
316 path: &Path,
317 invalidator: Invalidator,
318 write_content: WriteContent,
319 ) -> Result<Vec<(Invalidator, Option<WriteContent>)>> {
320 let mut invalidator_map = self.invalidator_map.lock().unwrap();
321 let invalidators = invalidator_map.entry(path.to_owned()).or_default();
322 let old_invalidators = invalidators
323 .extract_if(|i, old_write_content| {
324 i == &invalidator
325 || old_write_content
326 .as_ref()
327 .is_none_or(|old| old != &write_content)
328 })
329 .filter(|(i, _)| i != &invalidator)
330 .collect::<Vec<_>>();
331 invalidators.insert(invalidator, Some(write_content));
332 drop(invalidator_map);
333 self.watcher.ensure_watched_file(path, self.root_path())?;
334 Ok(old_invalidators)
335 }
336
337 fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
340 if let Some(invalidator) = turbo_tasks::get_invalidator() {
341 self.dir_invalidator_map
342 .insert(path.to_owned(), invalidator, None);
343 self.watcher.ensure_watched_dir(path, self.root_path())?;
344 }
345 Ok(())
346 }
347
348 async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
349 let lock1 = self.invalidation_lock.read().await;
350 let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
351 PathLockGuard(lock1, lock2)
352 }
353
354 fn invalidate(&self) {
355 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
356 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
357 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
358 let invalidators = invalidator_map
359 .into_iter()
360 .chain(dir_invalidator_map)
361 .flat_map(|(_, invalidators)| invalidators.into_keys())
362 .collect::<Vec<_>>();
363 parallel::for_each_owned(invalidators, |invalidator| invalidator.invalidate());
364 }
365
366 fn invalidate_with_reason<R: InvalidationReason + Clone>(
370 &self,
371 reason: impl Fn(&Path) -> R + Sync,
372 ) {
373 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
374 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
375 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
376 let invalidators = invalidator_map
377 .into_iter()
378 .chain(dir_invalidator_map)
379 .flat_map(|(path, invalidators)| {
380 let reason_for_path = reason(&path);
381 invalidators
382 .into_keys()
383 .map(move |i| (reason_for_path.clone(), i))
384 })
385 .collect::<Vec<_>>();
386 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
387 invalidator.invalidate_with_reason(reason)
388 });
389 }
390
391 fn invalidate_from_write(
392 &self,
393 full_path: &Path,
394 invalidators: Vec<(Invalidator, Option<WriteContent>)>,
395 ) {
396 if !invalidators.is_empty() {
397 if let Some(path) = format_absolute_fs_path(full_path, &self.name, self.root_path()) {
398 if invalidators.len() == 1 {
399 let (invalidator, _) = invalidators.into_iter().next().unwrap();
400 invalidator.invalidate_with_reason(Write { path });
401 } else {
402 invalidators.into_iter().for_each(|(invalidator, _)| {
403 invalidator.invalidate_with_reason(Write { path: path.clone() });
404 });
405 }
406 } else {
407 invalidators.into_iter().for_each(|(invalidator, _)| {
408 invalidator.invalidate();
409 });
410 }
411 }
412 }
413
414 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
415 async fn start_watching_internal(
416 self: &Arc<Self>,
417 report_invalidation_reason: bool,
418 poll_interval: Option<Duration>,
419 ) -> Result<()> {
420 let root_path = self.root_path().to_path_buf();
421
422 retry_blocking(root_path.clone(), move |path| {
424 let _tracing =
425 tracing::info_span!("create root directory", name = display(path.display()))
426 .entered();
427
428 std::fs::create_dir_all(path)
429 })
430 .concurrency_limited(&self.semaphore)
431 .await?;
432
433 self.watcher
434 .start_watching(self.clone(), report_invalidation_reason, poll_interval)?;
435
436 Ok(())
437 }
438
439 async fn create_directory(self: &Arc<Self>, directory: &Path) -> Result<()> {
440 let already_created = ApplyEffectsContext::with_or_insert_with(
441 DiskFileSystemApplyContext::default,
442 |fs_context| fs_context.created_directories.contains(directory),
443 );
444 if !already_created {
445 let func = |p: &Path| std::fs::create_dir_all(p);
446 retry_blocking(directory.to_path_buf(), func)
447 .concurrency_limited(&self.semaphore)
448 .instrument(tracing::info_span!(
449 "create directory",
450 name = display(directory.display())
451 ))
452 .await?;
453 ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| {
454 fs_context
455 .created_directories
456 .insert(directory.to_path_buf())
457 });
458 }
459 Ok(())
460 }
461}
462
463#[turbo_tasks::value(cell = "new", eq = "manual")]
464pub struct DiskFileSystem {
465 inner: Arc<DiskFileSystemInner>,
466}
467
468impl DiskFileSystem {
469 pub fn name(&self) -> &RcStr {
470 &self.inner.name
471 }
472
473 pub fn root(&self) -> &RcStr {
474 &self.inner.root
475 }
476
477 pub fn invalidate(&self) {
478 self.inner.invalidate();
479 }
480
481 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
482 &self,
483 reason: impl Fn(&Path) -> R + Sync,
484 ) {
485 self.inner.invalidate_with_reason(reason);
486 }
487
488 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
489 self.inner
490 .start_watching_internal(false, poll_interval)
491 .await
492 }
493
494 pub async fn start_watching_with_invalidation_reason(
495 &self,
496 poll_interval: Option<Duration>,
497 ) -> Result<()> {
498 self.inner
499 .start_watching_internal(true, poll_interval)
500 .await
501 }
502
503 pub fn stop_watching(&self) {
504 self.inner.watcher.stop_watching();
505 }
506
507 pub fn try_from_sys_path(
520 &self,
521 vc_self: ResolvedVc<DiskFileSystem>,
522 sys_path: &Path,
523 relative_to: Option<&FileSystemPath>,
524 ) -> Option<FileSystemPath> {
525 let vc_self = ResolvedVc::upcast(vc_self);
526
527 let sys_path = simplified(sys_path);
528 let relative_sys_path = if sys_path.is_absolute() {
529 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
532 normalized_sys_path
533 .strip_prefix(self.inner.root_path())
534 .ok()?
535 .to_owned()
536 } else if let Some(relative_to) = relative_to {
537 debug_assert_eq!(
538 relative_to.fs, vc_self,
539 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
540 );
541 let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
542 joined_sys_path.push(sys_path);
543 joined_sys_path.normalize_lexically().ok()?
544 } else {
545 sys_path.normalize_lexically().ok()?
546 };
547
548 Some(FileSystemPath {
549 fs: vc_self,
550 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
551 })
552 }
553
554 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
555 let path = self.inner.root_path();
556 if fs_path.path.is_empty() {
557 path.to_path_buf()
558 } else {
559 path.join(&*unix_to_sys(&fs_path.path))
560 }
561 }
562}
563
564#[allow(dead_code, reason = "we need to hold onto the locks")]
565struct PathLockGuard<'a>(
566 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
567 #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
568);
569
570fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
571 if let Ok(rel_path) = path.strip_prefix(root_path) {
572 let path = if MAIN_SEPARATOR != '/' {
573 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
574 format!("[{name}]/{rel_path}")
575 } else {
576 format!("[{name}]/{}", rel_path.display())
577 };
578 Some(path)
579 } else {
580 None
581 }
582}
583
584impl DiskFileSystem {
585 pub fn new(name: RcStr, root: RcStr) -> Vc<Self> {
592 Self::new_internal(name, root, None)
593 }
594
595 pub fn new_with_denied_path(name: RcStr, root: RcStr, denied_path: RcStr) -> Vc<Self> {
604 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
605 debug_assert!(
606 normalize_path(&denied_path).as_deref() == Some(&*denied_path),
607 "denied_path must be normalized: {denied_path:?}"
608 );
609 Self::new_internal(name, root, Some(denied_path))
610 }
611}
612
613#[turbo_tasks::value_impl]
614impl DiskFileSystem {
615 #[turbo_tasks::function]
616 fn new_internal(name: RcStr, root: RcStr, denied_path: Option<RcStr>) -> Vc<Self> {
617 mark_stateful();
618
619 let instance = DiskFileSystem {
620 inner: Arc::new(DiskFileSystemInner {
621 name,
622 root,
623 mutex_map: Default::default(),
624 invalidation_lock: Default::default(),
625 invalidator_map: InvalidatorMap::new(),
626 dir_invalidator_map: InvalidatorMap::new(),
627 semaphore: create_semaphore(),
628 watcher: DiskWatcher::new(),
629 denied_path,
630 }),
631 };
632
633 Self::cell(instance)
634 }
635}
636
637impl Debug for DiskFileSystem {
638 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
639 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
640 }
641}
642
643#[turbo_tasks::value_impl]
644impl FileSystem for DiskFileSystem {
645 #[turbo_tasks::function(fs)]
646 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
647 mark_session_dependent();
648
649 if self.inner.is_path_denied(&fs_path) {
651 return Ok(FileContent::NotFound.cell());
652 }
653 let full_path = self.to_sys_path(&fs_path);
654
655 self.inner.register_read_invalidator(&full_path)?;
656
657 let _lock = self.inner.lock_path(&full_path).await;
658 let content = match retry_blocking(full_path.clone(), |path: &Path| File::from_path(path))
659 .concurrency_limited(&self.inner.semaphore)
660 .instrument(tracing::info_span!(
661 "read file",
662 name = display(full_path.display())
663 ))
664 .await
665 {
666 Ok(file) => FileContent::new(file),
667 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
668 FileContent::NotFound
669 }
670 Err(e) => {
671 bail!(anyhow!(e).context(format!("reading file {}", full_path.display())))
672 }
673 };
674 Ok(content.cell())
675 }
676
677 #[turbo_tasks::function(fs)]
678 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
679 mark_session_dependent();
680
681 if self.inner.is_path_denied(&fs_path) {
683 return Ok(RawDirectoryContent::not_found());
684 }
685 let full_path = self.to_sys_path(&fs_path);
686
687 self.inner.register_dir_invalidator(&full_path)?;
688
689 let read_dir = match retry_blocking(full_path.clone(), |path| {
692 let _span =
693 tracing::info_span!("read directory", name = display(path.display())).entered();
694 std::fs::read_dir(path)
695 })
696 .concurrency_limited(&self.inner.semaphore)
697 .await
698 {
699 Ok(dir) => dir,
700 Err(e)
701 if e.kind() == ErrorKind::NotFound
702 || e.kind() == ErrorKind::NotADirectory
703 || e.kind() == ErrorKind::InvalidFilename =>
704 {
705 return Ok(RawDirectoryContent::not_found());
706 }
707 Err(e) => {
708 bail!(anyhow!(e).context(format!("reading dir {}", full_path.display())))
709 }
710 };
711 let denied_entry = match self.inner.denied_path.as_ref() {
712 Some(denied_path) => {
713 let dir_path = fs_path.path.as_str();
720 if denied_path.starts_with(dir_path) {
721 let denied_path_suffix =
722 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
723 Some(&denied_path[dir_path.len() + 1..])
724 } else if dir_path.is_empty() {
725 Some(denied_path.as_str())
726 } else {
727 None
728 };
729 denied_path_suffix.filter(|s| !s.contains('/'))
731 } else {
732 None
733 }
734 }
735 None => None,
736 };
737
738 let entries = read_dir
739 .filter_map(|r| {
740 let e = match r {
741 Ok(e) => e,
742 Err(err) => return Some(Err(err.into())),
743 };
744
745 let file_name: RcStr = e.file_name().to_str()?.into();
747 if let Some(denied_name) = denied_entry
749 && denied_name == file_name.as_str()
750 {
751 return None;
752 }
753
754 let entry = match e.file_type() {
755 Ok(t) if t.is_file() => RawDirectoryEntry::File,
756 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
757 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
758 Ok(_) => RawDirectoryEntry::Other,
759 Err(err) => return Some(Err(err.into())),
760 };
761
762 Some(anyhow::Ok((file_name, entry)))
763 })
764 .collect::<Result<_>>()
765 .with_context(|| format!("reading directory item in {}", full_path.display()))?;
766
767 Ok(RawDirectoryContent::new(entries))
768 }
769
770 #[turbo_tasks::function(fs)]
771 async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
772 mark_session_dependent();
773
774 if self.inner.is_path_denied(&fs_path) {
776 return Ok(LinkContent::NotFound.cell());
777 }
778 let full_path = self.to_sys_path(&fs_path);
779
780 self.inner.register_read_invalidator(&full_path)?;
781
782 let _lock = self.inner.lock_path(&full_path).await;
783 let link_path =
784 match retry_blocking(full_path.clone(), |path: &Path| std::fs::read_link(path))
785 .concurrency_limited(&self.inner.semaphore)
786 .instrument(tracing::info_span!(
787 "read symlink",
788 name = display(full_path.display())
789 ))
790 .await
791 {
792 Ok(res) => res,
793 Err(_) => return Ok(LinkContent::NotFound.cell()),
794 };
795 let is_link_absolute = link_path.is_absolute();
796
797 let mut file = link_path.clone();
798 if !is_link_absolute {
799 if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
800 normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
801 }) {
802 #[cfg(target_family = "windows")]
803 {
804 file = PathBuf::from(normalized_linked_path);
805 }
806 #[cfg(not(target_family = "windows"))]
809 {
810 file = PathBuf::from(format!("/{normalized_linked_path}"));
811 }
812 } else {
813 return Ok(LinkContent::Invalid.cell());
814 }
815 }
816
817 let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
824
825 let relative_to_root_path = match result {
826 Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
827 Err(_) => return Ok(LinkContent::Invalid.cell()),
828 };
829
830 let (target, file_type) = if is_link_absolute {
831 let target_string: RcStr = relative_to_root_path.to_string_lossy().into();
832 (
833 target_string.clone(),
834 FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
835 .get_type()
836 .await?,
837 )
838 } else {
839 let link_path_string_cow = link_path.to_string_lossy();
840 let link_path_unix: RcStr = sys_to_unix(&link_path_string_cow).into();
841 (
842 link_path_unix.clone(),
843 fs_path.parent().join(&link_path_unix)?.get_type().await?,
844 )
845 };
846
847 Ok(LinkContent::Link {
848 target,
849 link_type: {
850 let mut link_type = Default::default();
851 if link_path.is_absolute() {
852 link_type |= LinkType::ABSOLUTE;
853 }
854 if matches!(&*file_type, FileSystemEntryType::Directory) {
855 link_type |= LinkType::DIRECTORY;
856 }
857 link_type
858 },
859 }
860 .cell())
861 }
862
863 #[turbo_tasks::function(fs)]
864 async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
865 if self.inner.is_path_denied(&fs_path) {
871 bail!(
872 "Cannot write to denied path: {}",
873 fs_path.value_to_string().await?
874 );
875 }
876 let full_path = self.to_sys_path(&fs_path);
877
878 let content = content.await?;
879
880 let inner = self.inner.clone();
881 let invalidator = turbo_tasks::get_invalidator();
882
883 effect(async move {
884 let full_path = validate_path_length(&full_path)?;
885
886 let _lock = inner.lock_path(&full_path).await;
887
888 let old_invalidators = invalidator
890 .map(|invalidator| {
891 inner.register_write_invalidator(
892 &full_path,
893 invalidator,
894 WriteContent::File(content.clone()),
895 )
896 })
897 .transpose()?
898 .unwrap_or_default();
899
900 let compare = content
906 .streaming_compare(&full_path)
907 .concurrency_limited(&inner.semaphore)
908 .instrument(tracing::info_span!(
909 "read file before write",
910 name = display(full_path.display())
911 ))
912 .await?;
913 if compare == FileComparison::Equal {
914 if !old_invalidators.is_empty() {
915 for (invalidator, write_content) in old_invalidators {
916 inner.invalidator_map.insert(
917 full_path.clone().into_owned(),
918 invalidator,
919 write_content,
920 );
921 }
922 }
923 return Ok(());
924 }
925
926 match &*content {
927 FileContent::Content(..) => {
928 let create_directory = compare == FileComparison::Create;
929 if create_directory && let Some(parent) = full_path.parent() {
930 inner.create_directory(parent).await.with_context(|| {
931 format!(
932 "failed to create directory {} for write to {}",
933 parent.display(),
934 full_path.display()
935 )
936 })?;
937 }
938
939 let full_path_to_write = full_path.clone();
940 let content = content.clone();
941 retry_blocking(full_path_to_write.into_owned(), move |full_path| {
942 use std::io::Write;
943
944 let mut f = std::fs::File::create(full_path)?;
945 let FileContent::Content(file) = &*content else {
946 unreachable!()
947 };
948 std::io::copy(&mut file.read(), &mut f)?;
949 #[cfg(target_family = "unix")]
950 f.set_permissions(file.meta.permissions.into())?;
951 f.flush()?;
952
953 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
954 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
955 .is_some_and(|v| v == "1" || v == "true")
956 });
957 if *WRITE_VERSION {
958 let mut full_path = full_path.to_owned();
959 let hash = hash_xxh3_hash64(file);
960 let ext = full_path.extension();
961 let ext = if let Some(ext) = ext {
962 format!("{:016x}.{}", hash, ext.to_string_lossy())
963 } else {
964 format!("{hash:016x}")
965 };
966 full_path.set_extension(ext);
967 let mut f = std::fs::File::create(&full_path)?;
968 std::io::copy(&mut file.read(), &mut f)?;
969 #[cfg(target_family = "unix")]
970 f.set_permissions(file.meta.permissions.into())?;
971 f.flush()?;
972 }
973 Ok::<(), io::Error>(())
974 })
975 .concurrency_limited(&inner.semaphore)
976 .instrument(tracing::info_span!(
977 "write file",
978 name = display(full_path.display())
979 ))
980 .await
981 .with_context(|| format!("failed to write to {}", full_path.display()))?;
982 }
983 FileContent::NotFound => {
984 retry_blocking(full_path.clone().into_owned(), |path| {
985 std::fs::remove_file(path)
986 })
987 .concurrency_limited(&inner.semaphore)
988 .instrument(tracing::info_span!(
989 "remove file",
990 name = display(full_path.display())
991 ))
992 .await
993 .or_else(|err| {
994 if err.kind() == ErrorKind::NotFound {
995 Ok(())
996 } else {
997 Err(err)
998 }
999 })
1000 .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
1001 }
1002 }
1003
1004 inner.invalidate_from_write(&full_path, old_invalidators);
1005
1006 Ok(())
1007 });
1008
1009 Ok(())
1010 }
1011
1012 #[turbo_tasks::function(fs)]
1013 async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
1014 if self.inner.is_path_denied(&fs_path) {
1020 bail!(
1021 "Cannot write link to denied path: {}",
1022 fs_path.value_to_string().await?
1023 );
1024 }
1025 let full_path = self.to_sys_path(&fs_path);
1026
1027 let content = target.await?;
1028 let inner = self.inner.clone();
1029 let invalidator = turbo_tasks::get_invalidator();
1030
1031 effect(async move {
1032 let full_path = validate_path_length(&full_path)?;
1033
1034 let _lock = inner.lock_path(&full_path).await;
1035
1036 let old_invalidators = invalidator
1037 .map(|invalidator| {
1038 inner.register_write_invalidator(
1039 &full_path,
1040 invalidator,
1041 WriteContent::Link(content.clone()),
1042 )
1043 })
1044 .transpose()?
1045 .unwrap_or_default();
1046
1047 let old_content = match retry_blocking(full_path.clone().into_owned(), |path| {
1050 std::fs::read_link(path)
1051 })
1052 .concurrency_limited(&inner.semaphore)
1053 .instrument(tracing::info_span!(
1054 "read symlink before write",
1055 name = display(full_path.display())
1056 ))
1057 .await
1058 {
1059 Ok(res) => Some((res.is_absolute(), res)),
1060 Err(_) => None,
1061 };
1062 let is_equal = match (&*content, &old_content) {
1063 (LinkContent::Link { target, link_type }, Some((old_is_absolute, old_target))) => {
1064 Path::new(&**target) == old_target
1065 && link_type.contains(LinkType::ABSOLUTE) == *old_is_absolute
1066 }
1067 (LinkContent::NotFound, None) => true,
1068 _ => false,
1069 };
1070 if is_equal {
1071 if !old_invalidators.is_empty() {
1072 for (invalidator, write_content) in old_invalidators {
1073 inner.invalidator_map.insert(
1074 full_path.clone().into_owned(),
1075 invalidator,
1076 write_content,
1077 );
1078 }
1079 }
1080 return Ok(());
1081 }
1082
1083 match &*content {
1084 LinkContent::Link { target, link_type } => {
1085 let create_directory = old_content.is_none();
1086 if create_directory && let Some(parent) = full_path.parent() {
1087 inner.create_directory(parent).await.with_context(|| {
1088 format!(
1089 "failed to create directory {} for write link to {}",
1090 parent.display(),
1091 full_path.display()
1092 )
1093 })?;
1094 }
1095
1096 let link_type = *link_type;
1097 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1098 Path::new(&inner.root).join(unix_to_sys(target).as_ref())
1099 } else {
1100 PathBuf::from(unix_to_sys(target).as_ref())
1101 };
1102 let full_path = full_path.into_owned();
1103 retry_blocking(target_path, move |target_path| {
1104 let _span = tracing::info_span!(
1105 "write symlink",
1106 name = display(target_path.display())
1107 )
1108 .entered();
1109 #[cfg(not(target_family = "windows"))]
1112 {
1113 std::os::unix::fs::symlink(target_path, &full_path)
1114 }
1115 #[cfg(target_family = "windows")]
1116 {
1117 if link_type.contains(LinkType::DIRECTORY) {
1118 std::os::windows::fs::symlink_dir(target_path, &full_path)
1119 } else {
1120 std::os::windows::fs::symlink_file(target_path, &full_path)
1121 }
1122 }
1123 })
1124 .await
1125 .with_context(|| format!("create symlink to {target}"))?;
1126 }
1127 LinkContent::Invalid => {
1128 anyhow::bail!("invalid symlink target: {}", full_path.display())
1129 }
1130 LinkContent::NotFound => {
1131 retry_blocking(full_path.clone().into_owned(), |path| {
1132 std::fs::remove_file(path)
1133 })
1134 .concurrency_limited(&inner.semaphore)
1135 .await
1136 .or_else(|err| {
1137 if err.kind() == ErrorKind::NotFound {
1138 Ok(())
1139 } else {
1140 Err(err)
1141 }
1142 })
1143 .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
1144 }
1145 }
1146
1147 Ok(())
1148 });
1149 Ok(())
1150 }
1151
1152 #[turbo_tasks::function(fs)]
1153 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1154 mark_session_dependent();
1155 let full_path = self.to_sys_path(&fs_path);
1156
1157 if self.inner.is_path_denied(&fs_path) {
1159 bail!(
1160 "Cannot read metadata from denied path: {}",
1161 fs_path.value_to_string().await?
1162 );
1163 }
1164
1165 self.inner.register_read_invalidator(&full_path)?;
1166
1167 let _lock = self.inner.lock_path(&full_path).await;
1168 let meta = retry_blocking(full_path.clone(), |path| std::fs::metadata(path))
1169 .concurrency_limited(&self.inner.semaphore)
1170 .instrument(tracing::info_span!(
1171 "read metadata",
1172 name = display(full_path.display())
1173 ))
1174 .await
1175 .with_context(|| format!("reading metadata for {}", full_path.display()))?;
1176
1177 Ok(FileMeta::cell(meta.into()))
1178 }
1179}
1180
1181#[turbo_tasks::value_impl]
1182impl ValueToString for DiskFileSystem {
1183 #[turbo_tasks::function]
1184 fn to_string(&self) -> Vc<RcStr> {
1185 Vc::cell(self.inner.name.clone())
1186 }
1187}
1188
1189#[turbo_tasks::value(shared)]
1190#[derive(Debug, Clone, Hash, TaskInput)]
1191pub struct FileSystemPath {
1192 pub fs: ResolvedVc<Box<dyn FileSystem>>,
1193 pub path: RcStr,
1194}
1195
1196impl FileSystemPath {
1197 pub fn value_to_string(&self) -> Vc<RcStr> {
1199 value_to_string(self.clone())
1200 }
1201}
1202
1203#[turbo_tasks::function]
1204async fn value_to_string(path: FileSystemPath) -> Result<Vc<RcStr>> {
1205 Ok(Vc::cell(
1206 format!("[{}]/{}", path.fs.to_string().await?, path.path).into(),
1207 ))
1208}
1209
1210impl FileSystemPath {
1211 pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1212 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1213 if other.path.is_empty() {
1214 true
1215 } else {
1216 self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1217 }
1218 } else {
1219 false
1220 }
1221 }
1222
1223 pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1224 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1225 if other.path.is_empty() {
1226 true
1227 } else {
1228 matches!(
1229 self.path.as_bytes().get(other.path.len()),
1230 Some(&b'/') | None
1231 )
1232 }
1233 } else {
1234 false
1235 }
1236 }
1237
1238 pub fn is_root(&self) -> bool {
1239 self.path.is_empty()
1240 }
1241
1242 pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1246 if self.fs != inner.fs {
1247 return None;
1248 }
1249 let path = inner.path.strip_prefix(&*self.path)?;
1250 if self.path.is_empty() {
1251 Some(path)
1252 } else if let Some(stripped) = path.strip_prefix('/') {
1253 Some(stripped)
1254 } else {
1255 None
1256 }
1257 }
1258
1259 pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1260 if self.fs != other.fs {
1261 return None;
1262 }
1263
1264 Some(get_relative_path_to(&self.path, &other.path).into())
1265 }
1266
1267 pub fn file_name(&self) -> &str {
1270 let (_, file_name) = self.split_file_name();
1271 file_name
1272 }
1273
1274 pub fn has_extension(&self, extension: &str) -> bool {
1279 debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1280 self.path.ends_with(extension)
1281 }
1282
1283 pub fn extension_ref(&self) -> Option<&str> {
1285 let (_, extension) = self.split_extension();
1286 extension
1287 }
1288
1289 fn split_extension(&self) -> (&str, Option<&str>) {
1293 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1294 if extension.contains('/') ||
1295 path_before_extension.ends_with('/') || path_before_extension.is_empty()
1297 {
1298 (self.path.as_str(), None)
1299 } else {
1300 (path_before_extension, Some(extension))
1301 }
1302 } else {
1303 (self.path.as_str(), None)
1304 }
1305 }
1306
1307 fn split_file_name(&self) -> (Option<&str>, &str) {
1311 if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1313 (Some(parent), file_name)
1314 } else {
1315 (None, self.path.as_str())
1316 }
1317 }
1318
1319 fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1324 let (path_before_extension, extension) = self.split_extension();
1325
1326 if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1327 (Some(parent), file_stem, extension)
1328 } else {
1329 (None, path_before_extension, extension)
1330 }
1331 }
1332}
1333
1334#[turbo_tasks::value(transparent)]
1335pub struct FileSystemPathOption(Option<FileSystemPath>);
1336
1337#[turbo_tasks::value_impl]
1338impl FileSystemPathOption {
1339 #[turbo_tasks::function]
1340 pub fn none() -> Vc<Self> {
1341 Vc::cell(None)
1342 }
1343}
1344
1345impl FileSystemPath {
1346 fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1350 debug_assert!(
1354 MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1355 "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1356 '/'",
1357 );
1358 debug_assert!(
1359 normalize_path(&path).as_deref() == Some(&*path),
1360 "path {path} must be normalized",
1361 );
1362 FileSystemPath { fs, path }
1363 }
1364
1365 pub fn join(&self, path: &str) -> Result<Self> {
1369 if let Some(path) = join_path(&self.path, path) {
1370 Ok(Self::new_normalized(self.fs, path.into()))
1371 } else {
1372 bail!(
1373 "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1374 self.path,
1375 path
1376 );
1377 }
1378 }
1379
1380 pub fn append(&self, path: &str) -> Result<Self> {
1382 if path.contains('/') {
1383 bail!(
1384 "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1385 self.path,
1386 path
1387 )
1388 }
1389 Ok(Self::new_normalized(
1390 self.fs,
1391 format!("{}{}", self.path, path).into(),
1392 ))
1393 }
1394
1395 pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1398 if appending.contains('/') {
1399 bail!(
1400 "FileSystemPath(\"{}\").append_to_stem(\"{}\") must not append '/'",
1401 self.path,
1402 appending
1403 )
1404 }
1405 if let (path, Some(ext)) = self.split_extension() {
1406 return Ok(Self::new_normalized(
1407 self.fs,
1408 format!("{path}{appending}.{ext}").into(),
1409 ));
1410 }
1411 Ok(Self::new_normalized(
1412 self.fs,
1413 format!("{}{}", self.path, appending).into(),
1414 ))
1415 }
1416
1417 #[allow(clippy::needless_borrow)] pub fn try_join(&self, path: &str) -> Result<Option<FileSystemPath>> {
1421 #[cfg(target_os = "windows")]
1423 let path = path.replace('\\', "/");
1424
1425 if let Some(path) = join_path(&self.path, &path) {
1426 Ok(Some(Self::new_normalized(self.fs, path.into())))
1427 } else {
1428 Ok(None)
1429 }
1430 }
1431
1432 pub fn try_join_inside(&self, path: &str) -> Result<Option<FileSystemPath>> {
1435 if let Some(path) = join_path(&self.path, path)
1436 && path.starts_with(&*self.path)
1437 {
1438 return Ok(Some(Self::new_normalized(self.fs, path.into())));
1439 }
1440 Ok(None)
1441 }
1442
1443 pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1446 read_glob(self.clone(), glob)
1447 }
1448
1449 pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1452 track_glob(self.clone(), glob, include_dot_files)
1453 }
1454
1455 pub fn root(&self) -> Vc<Self> {
1456 self.fs().root()
1457 }
1458}
1459
1460impl FileSystemPath {
1461 pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1462 *self.fs
1463 }
1464
1465 pub fn extension(&self) -> &str {
1466 self.extension_ref().unwrap_or_default()
1467 }
1468
1469 pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1470 self.is_inside_ref(other)
1471 }
1472
1473 pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1474 self.is_inside_or_equal_ref(other)
1475 }
1476
1477 pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1480 let (path_without_extension, _) = self.split_extension();
1481 Self::new_normalized(
1482 self.fs,
1483 match extension.is_empty() {
1486 true => path_without_extension.into(),
1487 false => format!("{path_without_extension}.{extension}").into(),
1488 },
1489 )
1490 }
1491
1492 pub fn file_stem(&self) -> Option<&str> {
1501 let (_, file_stem, _) = self.split_file_stem_extension();
1502 if file_stem.is_empty() {
1503 return None;
1504 }
1505 Some(file_stem)
1506 }
1507}
1508
1509impl Display for FileSystemPath {
1510 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1511 write!(f, "{}", self.path)
1512 }
1513}
1514
1515#[turbo_tasks::function]
1516pub async fn rebase(
1517 fs_path: FileSystemPath,
1518 old_base: FileSystemPath,
1519 new_base: FileSystemPath,
1520) -> Result<Vc<FileSystemPath>> {
1521 let new_path;
1522 if old_base.path.is_empty() {
1523 if new_base.path.is_empty() {
1524 new_path = fs_path.path.clone();
1525 } else {
1526 new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1527 }
1528 } else {
1529 let base_path = [&old_base.path, "/"].concat();
1530 if !fs_path.path.starts_with(&base_path) {
1531 bail!(
1532 "rebasing {} from {} onto {} doesn't work because it's not part of the source path",
1533 fs_path.to_string(),
1534 old_base.to_string(),
1535 new_base.to_string()
1536 );
1537 }
1538 if new_base.path.is_empty() {
1539 new_path = [&fs_path.path[base_path.len()..]].concat().into();
1540 } else {
1541 new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1542 .concat()
1543 .into();
1544 }
1545 }
1546 Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1547}
1548
1549impl FileSystemPath {
1551 pub fn read(&self) -> Vc<FileContent> {
1552 self.fs().read(self.clone())
1553 }
1554
1555 pub fn read_link(&self) -> Vc<LinkContent> {
1556 self.fs().read_link(self.clone())
1557 }
1558
1559 pub fn read_json(&self) -> Vc<FileJsonContent> {
1560 self.fs().read(self.clone()).parse_json()
1561 }
1562
1563 pub fn read_json5(&self) -> Vc<FileJsonContent> {
1564 self.fs().read(self.clone()).parse_json5()
1565 }
1566
1567 pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1572 self.fs().raw_read_dir(self.clone())
1573 }
1574
1575 pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1576 self.fs().write(self.clone(), content)
1577 }
1578
1579 pub fn write_link(&self, target: Vc<LinkContent>) -> Vc<()> {
1580 self.fs().write_link(self.clone(), target)
1581 }
1582
1583 pub fn metadata(&self) -> Vc<FileMeta> {
1584 self.fs().metadata(self.clone())
1585 }
1586
1587 pub async fn realpath(&self) -> Result<FileSystemPath> {
1590 let result = &(*self.realpath_with_links().await?);
1591 match &result.path_result {
1592 Ok(path) => Ok(path.clone()),
1593 Err(error) => Err(anyhow::anyhow!(error.as_error_message(self, result))),
1594 }
1595 }
1596
1597 pub fn rebase(
1598 fs_path: FileSystemPath,
1599 old_base: FileSystemPath,
1600 new_base: FileSystemPath,
1601 ) -> Vc<FileSystemPath> {
1602 rebase(fs_path, old_base, new_base)
1603 }
1604}
1605
1606impl FileSystemPath {
1607 pub fn read_dir(&self) -> Vc<DirectoryContent> {
1612 read_dir(self.clone())
1613 }
1614
1615 pub fn parent(&self) -> FileSystemPath {
1616 let path = &self.path;
1617 if path.is_empty() {
1618 return self.clone();
1619 }
1620 FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1621 }
1622
1623 pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1632 get_type(self.clone())
1633 }
1634
1635 pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1636 realpath_with_links(self.clone())
1637 }
1638}
1639
1640#[turbo_tasks::value_impl]
1641impl ValueToString for FileSystemPath {
1642 #[turbo_tasks::function]
1643 async fn to_string(&self) -> Result<Vc<RcStr>> {
1644 Ok(Vc::cell(
1645 format!("[{}]/{}", self.fs.to_string().await?, self.path).into(),
1646 ))
1647 }
1648}
1649
1650#[derive(Clone, Debug)]
1651#[turbo_tasks::value(shared)]
1652pub struct RealPathResult {
1653 pub path_result: Result<FileSystemPath, RealPathResultError>,
1654 pub symlinks: Vec<FileSystemPath>,
1655}
1656
1657#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, NonLocalValue, TraceRawVcs)]
1660pub enum RealPathResultError {
1661 TooManySymlinks,
1662 CycleDetected,
1663 Invalid,
1664 NotFound,
1665}
1666impl RealPathResultError {
1667 pub fn as_error_message(&self, orig: &FileSystemPath, result: &RealPathResult) -> String {
1669 match self {
1670 RealPathResultError::TooManySymlinks => format!(
1671 "Symlink {orig} leads to too many other symlinks ({len} links)",
1672 len = result.symlinks.len()
1673 ),
1674 RealPathResultError::CycleDetected => {
1675 format!("Symlink {orig} is in a symlink loop: {:?}", result.symlinks)
1676 }
1677 RealPathResultError::Invalid => {
1678 format!("Symlink {orig} is invalid, it points out of the filesystem root")
1679 }
1680 RealPathResultError::NotFound => {
1681 format!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1682 }
1683 }
1684 }
1685}
1686
1687#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
1688#[turbo_tasks::value(shared)]
1689pub enum Permissions {
1690 Readable,
1691 #[default]
1692 Writable,
1693 Executable,
1694}
1695
1696#[cfg(target_family = "unix")]
1699impl From<Permissions> for std::fs::Permissions {
1700 fn from(perm: Permissions) -> Self {
1701 use std::os::unix::fs::PermissionsExt;
1702 match perm {
1703 Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1704 Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1705 Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1706 }
1707 }
1708}
1709
1710#[cfg(target_family = "unix")]
1711impl From<std::fs::Permissions> for Permissions {
1712 fn from(perm: std::fs::Permissions) -> Self {
1713 use std::os::unix::fs::PermissionsExt;
1714 if perm.readonly() {
1715 Permissions::Readable
1716 } else {
1717 if perm.mode() & 0o111 != 0 {
1719 Permissions::Executable
1720 } else {
1721 Permissions::Writable
1722 }
1723 }
1724 }
1725}
1726
1727#[cfg(not(target_family = "unix"))]
1728impl From<std::fs::Permissions> for Permissions {
1729 fn from(_: std::fs::Permissions) -> Self {
1730 Permissions::default()
1731 }
1732}
1733
1734#[turbo_tasks::value(shared)]
1735#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
1736pub enum FileContent {
1737 Content(File),
1738 NotFound,
1739}
1740
1741impl From<File> for FileContent {
1742 fn from(file: File) -> Self {
1743 FileContent::Content(file)
1744 }
1745}
1746
1747impl From<File> for Vc<FileContent> {
1748 fn from(file: File) -> Self {
1749 FileContent::Content(file).cell()
1750 }
1751}
1752
1753#[derive(Clone, Debug, Eq, PartialEq)]
1754enum FileComparison {
1755 Create,
1756 Equal,
1757 NotEqual,
1758}
1759
1760impl FileContent {
1761 async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
1764 let old_file = extract_disk_access(
1765 retry_blocking(path.to_path_buf(), |path| std::fs::File::open(path)).await,
1766 path,
1767 )?;
1768 let Some(old_file) = old_file else {
1769 return Ok(match self {
1770 FileContent::NotFound => FileComparison::Equal,
1771 _ => FileComparison::Create,
1772 });
1773 };
1774 let FileContent::Content(new_file) = self else {
1776 return Ok(FileComparison::NotEqual);
1777 };
1778
1779 let old_meta = extract_disk_access(
1780 retry_blocking(path.to_path_buf(), {
1781 let file_for_metadata = old_file.try_clone()?;
1782 move |_| file_for_metadata.metadata()
1783 })
1784 .await,
1785 path,
1786 )?;
1787 let Some(old_meta) = old_meta else {
1788 return Ok(FileComparison::Create);
1792 };
1793 if new_file.meta != old_meta.into() {
1795 return Ok(FileComparison::NotEqual);
1796 }
1797
1798 let mut new_contents = new_file.read();
1801 let mut old_contents = BufReader::new(old_file);
1802 Ok(loop {
1803 let new_chunk = new_contents.fill_buf()?;
1804 let Ok(old_chunk) = old_contents.fill_buf() else {
1805 break FileComparison::NotEqual;
1806 };
1807
1808 let len = min(new_chunk.len(), old_chunk.len());
1809 if len == 0 {
1810 if new_chunk.len() == old_chunk.len() {
1811 break FileComparison::Equal;
1812 } else {
1813 break FileComparison::NotEqual;
1814 }
1815 }
1816
1817 if new_chunk[0..len] != old_chunk[0..len] {
1818 break FileComparison::NotEqual;
1819 }
1820
1821 new_contents.consume(len);
1822 old_contents.consume(len);
1823 })
1824 }
1825}
1826
1827bitflags! {
1828 #[derive(Default, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
1829 pub struct LinkType: u8 {
1830 const DIRECTORY = 0b00000001;
1831 const ABSOLUTE = 0b00000010;
1832 }
1833}
1834
1835#[turbo_tasks::value(shared)]
1836#[derive(Debug)]
1837pub enum LinkContent {
1838 Link { target: RcStr, link_type: LinkType },
1845 Invalid,
1847 NotFound,
1849}
1850
1851#[turbo_tasks::value(shared)]
1852#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
1853pub struct File {
1854 #[turbo_tasks(debug_ignore)]
1855 content: Rope,
1856 meta: FileMeta,
1857}
1858
1859impl File {
1860 fn from_path(p: &Path) -> io::Result<Self> {
1862 let mut file = std::fs::File::open(p)?;
1863 let metadata = file.metadata()?;
1864
1865 let mut output = Vec::with_capacity(metadata.len() as usize);
1866 file.read_to_end(&mut output)?;
1867
1868 Ok(File {
1869 meta: metadata.into(),
1870 content: Rope::from(output),
1871 })
1872 }
1873
1874 fn from_bytes(content: Vec<u8>) -> Self {
1876 File {
1877 meta: FileMeta::default(),
1878 content: Rope::from(content),
1879 }
1880 }
1881
1882 fn from_rope(content: Rope) -> Self {
1884 File {
1885 meta: FileMeta::default(),
1886 content,
1887 }
1888 }
1889
1890 pub fn content_type(&self) -> Option<&Mime> {
1892 self.meta.content_type.as_ref()
1893 }
1894
1895 pub fn with_content_type(mut self, content_type: Mime) -> Self {
1897 self.meta.content_type = Some(content_type);
1898 self
1899 }
1900
1901 pub fn read(&self) -> RopeReader {
1903 self.content.read()
1904 }
1905}
1906
1907impl Debug for File {
1908 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1909 f.debug_struct("File")
1910 .field("meta", &self.meta)
1911 .field("content (hash)", &hash_xxh3_hash64(&self.content))
1912 .finish()
1913 }
1914}
1915
1916impl From<RcStr> for File {
1917 fn from(s: RcStr) -> Self {
1918 s.into_owned().into()
1919 }
1920}
1921
1922impl From<String> for File {
1923 fn from(s: String) -> Self {
1924 File::from_bytes(s.into_bytes())
1925 }
1926}
1927
1928impl From<ReadRef<RcStr>> for File {
1929 fn from(s: ReadRef<RcStr>) -> Self {
1930 File::from_bytes(s.as_bytes().to_vec())
1931 }
1932}
1933
1934impl From<&str> for File {
1935 fn from(s: &str) -> Self {
1936 File::from_bytes(s.as_bytes().to_vec())
1937 }
1938}
1939
1940impl From<Vec<u8>> for File {
1941 fn from(bytes: Vec<u8>) -> Self {
1942 File::from_bytes(bytes)
1943 }
1944}
1945
1946impl From<&[u8]> for File {
1947 fn from(bytes: &[u8]) -> Self {
1948 File::from_bytes(bytes.to_vec())
1949 }
1950}
1951
1952impl From<ReadRef<Rope>> for File {
1953 fn from(rope: ReadRef<Rope>) -> Self {
1954 File::from_rope(ReadRef::into_owned(rope))
1955 }
1956}
1957
1958impl From<Rope> for File {
1959 fn from(rope: Rope) -> Self {
1960 File::from_rope(rope)
1961 }
1962}
1963
1964impl File {
1965 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
1966 Self {
1967 meta,
1968 content: Rope::from(content),
1969 }
1970 }
1971
1972 pub fn meta(&self) -> &FileMeta {
1974 &self.meta
1975 }
1976
1977 pub fn content(&self) -> &Rope {
1979 &self.content
1980 }
1981}
1982
1983mod mime_option_serde {
1984 use std::{fmt, str::FromStr};
1985
1986 use mime::Mime;
1987 use serde::{Deserializer, Serializer, de};
1988
1989 pub fn serialize<S>(mime: &Option<Mime>, serializer: S) -> Result<S::Ok, S::Error>
1990 where
1991 S: Serializer,
1992 {
1993 if let Some(mime) = mime {
1994 serializer.serialize_str(mime.as_ref())
1995 } else {
1996 serializer.serialize_str("")
1997 }
1998 }
1999
2000 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Mime>, D::Error>
2001 where
2002 D: Deserializer<'de>,
2003 {
2004 struct Visitor;
2005
2006 impl de::Visitor<'_> for Visitor {
2007 type Value = Option<Mime>;
2008
2009 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
2010 formatter.write_str("a valid MIME type or empty string")
2011 }
2012
2013 fn visit_str<E>(self, value: &str) -> Result<Option<Mime>, E>
2014 where
2015 E: de::Error,
2016 {
2017 if value.is_empty() {
2018 Ok(None)
2019 } else {
2020 Mime::from_str(value)
2021 .map(Some)
2022 .map_err(|e| E::custom(format!("{e}")))
2023 }
2024 }
2025 }
2026
2027 deserializer.deserialize_str(Visitor)
2028 }
2029}
2030
2031#[turbo_tasks::value(shared)]
2032#[derive(Debug, Clone, Default)]
2033pub struct FileMeta {
2034 permissions: Permissions,
2037 #[serde(with = "mime_option_serde")]
2038 #[turbo_tasks(trace_ignore)]
2039 content_type: Option<Mime>,
2040}
2041
2042impl Ord for FileMeta {
2043 fn cmp(&self, other: &Self) -> Ordering {
2044 self.permissions
2045 .cmp(&other.permissions)
2046 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2047 }
2048}
2049
2050impl PartialOrd for FileMeta {
2051 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2052 Some(self.cmp(other))
2053 }
2054}
2055
2056impl From<std::fs::Metadata> for FileMeta {
2057 fn from(meta: std::fs::Metadata) -> Self {
2058 let permissions = meta.permissions().into();
2059
2060 Self {
2061 permissions,
2062 content_type: None,
2063 }
2064 }
2065}
2066
2067impl DeterministicHash for FileMeta {
2068 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2069 self.permissions.deterministic_hash(state);
2070 if let Some(content_type) = &self.content_type {
2071 content_type.to_string().deterministic_hash(state);
2072 }
2073 }
2074}
2075
2076impl FileContent {
2077 pub fn new(file: File) -> Self {
2078 FileContent::Content(file)
2079 }
2080
2081 pub fn is_content(&self) -> bool {
2082 matches!(self, FileContent::Content(_))
2083 }
2084
2085 pub fn as_content(&self) -> Option<&File> {
2086 match self {
2087 FileContent::Content(file) => Some(file),
2088 FileContent::NotFound => None,
2089 }
2090 }
2091
2092 pub fn parse_json_ref(&self) -> FileJsonContent {
2093 match self {
2094 FileContent::Content(file) => {
2095 let content = file.content.clone().into_bytes();
2096 let de = &mut serde_json::Deserializer::from_slice(&content);
2097 match serde_path_to_error::deserialize(de) {
2098 Ok(data) => FileJsonContent::Content(data),
2099 Err(e) => FileJsonContent::Unparsable(Box::new(
2100 UnparsableJson::from_serde_path_to_error(e),
2101 )),
2102 }
2103 }
2104 FileContent::NotFound => FileJsonContent::NotFound,
2105 }
2106 }
2107
2108 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2109 match self {
2110 FileContent::Content(file) => match file.content.to_str() {
2111 Ok(string) => match parse_to_serde_value(
2112 &string,
2113 &ParseOptions {
2114 allow_comments: true,
2115 allow_trailing_commas: true,
2116 allow_loose_object_property_names: false,
2117 },
2118 ) {
2119 Ok(data) => match data {
2120 Some(value) => FileJsonContent::Content(value),
2121 None => FileJsonContent::unparsable(rcstr!(
2122 "text content doesn't contain any json data"
2123 )),
2124 },
2125 Err(e) => FileJsonContent::Unparsable(Box::new(
2126 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2127 )),
2128 },
2129 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2130 },
2131 FileContent::NotFound => FileJsonContent::NotFound,
2132 }
2133 }
2134
2135 pub fn parse_json5_ref(&self) -> FileJsonContent {
2136 match self {
2137 FileContent::Content(file) => match file.content.to_str() {
2138 Ok(string) => match parse_to_serde_value(
2139 &string,
2140 &ParseOptions {
2141 allow_comments: true,
2142 allow_trailing_commas: true,
2143 allow_loose_object_property_names: true,
2144 },
2145 ) {
2146 Ok(data) => match data {
2147 Some(value) => FileJsonContent::Content(value),
2148 None => FileJsonContent::unparsable(rcstr!(
2149 "text content doesn't contain any json data"
2150 )),
2151 },
2152 Err(e) => FileJsonContent::Unparsable(Box::new(
2153 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2154 )),
2155 },
2156 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2157 },
2158 FileContent::NotFound => FileJsonContent::NotFound,
2159 }
2160 }
2161
2162 pub fn lines_ref(&self) -> FileLinesContent {
2163 match self {
2164 FileContent::Content(file) => match file.content.to_str() {
2165 Ok(string) => {
2166 let mut bytes_offset = 0;
2167 FileLinesContent::Lines(
2168 string
2169 .split('\n')
2170 .map(|l| {
2171 let line = FileLine {
2172 content: l.to_string(),
2173 bytes_offset,
2174 };
2175 bytes_offset += (l.len() + 1) as u32;
2176 line
2177 })
2178 .collect(),
2179 )
2180 }
2181 Err(_) => FileLinesContent::Unparsable,
2182 },
2183 FileContent::NotFound => FileLinesContent::NotFound,
2184 }
2185 }
2186}
2187
2188#[turbo_tasks::value_impl]
2189impl FileContent {
2190 #[turbo_tasks::function]
2191 pub fn len(&self) -> Result<Vc<Option<u64>>> {
2192 Ok(Vc::cell(match self {
2193 FileContent::Content(file) => Some(file.content.len() as u64),
2194 FileContent::NotFound => None,
2195 }))
2196 }
2197
2198 #[turbo_tasks::function]
2199 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2200 Ok(self.parse_json_ref().into())
2201 }
2202
2203 #[turbo_tasks::function]
2204 pub async fn parse_json_with_comments(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
2205 let this = self.await?;
2206 Ok(this.parse_json_with_comments_ref().into())
2207 }
2208
2209 #[turbo_tasks::function]
2210 pub async fn parse_json5(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
2211 let this = self.await?;
2212 Ok(this.parse_json5_ref().into())
2213 }
2214
2215 #[turbo_tasks::function]
2216 pub async fn lines(self: Vc<Self>) -> Result<Vc<FileLinesContent>> {
2217 let this = self.await?;
2218 Ok(this.lines_ref().into())
2219 }
2220
2221 #[turbo_tasks::function]
2222 pub async fn hash(self: Vc<Self>) -> Result<Vc<u64>> {
2223 Ok(Vc::cell(hash_xxh3_hash64(&self.await?)))
2224 }
2225}
2226
2227#[turbo_tasks::value(shared, serialization = "none")]
2229pub enum FileJsonContent {
2230 Content(Value),
2231 Unparsable(Box<UnparsableJson>),
2232 NotFound,
2233}
2234
2235#[turbo_tasks::value_impl]
2236impl ValueToString for FileJsonContent {
2237 #[turbo_tasks::function]
2242 fn to_string(&self) -> Result<Vc<RcStr>> {
2243 match self {
2244 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2245 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2246 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2247 }
2248 }
2249}
2250
2251#[turbo_tasks::value_impl]
2252impl FileJsonContent {
2253 #[turbo_tasks::function]
2254 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2255 match &*self.await? {
2256 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2257 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2258 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2259 }
2260 }
2261}
2262impl FileJsonContent {
2263 pub fn unparsable(message: RcStr) -> Self {
2264 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2265 message,
2266 path: None,
2267 start_location: None,
2268 end_location: None,
2269 }))
2270 }
2271
2272 pub fn unparsable_with_message(message: RcStr) -> Self {
2273 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2274 message,
2275 path: None,
2276 start_location: None,
2277 end_location: None,
2278 }))
2279 }
2280}
2281
2282#[derive(Debug, PartialEq, Eq)]
2283pub struct FileLine {
2284 pub content: String,
2285 pub bytes_offset: u32,
2286}
2287
2288#[turbo_tasks::value(shared, serialization = "none")]
2289pub enum FileLinesContent {
2290 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2291 Unparsable,
2292 NotFound,
2293}
2294
2295#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
2296pub enum RawDirectoryEntry {
2297 File,
2298 Directory,
2299 Symlink,
2300 Other,
2302}
2303
2304#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
2305pub enum DirectoryEntry {
2306 File(FileSystemPath),
2307 Directory(FileSystemPath),
2308 Symlink(FileSystemPath),
2309 Other(FileSystemPath),
2310 Error(RcStr),
2311}
2312
2313impl DirectoryEntry {
2314 pub async fn resolve_symlink(self) -> Result<Self> {
2318 if let DirectoryEntry::Symlink(symlink) = &self {
2319 let result = &*symlink.realpath_with_links().await?;
2320 let real_path = match &result.path_result {
2321 Ok(path) => path,
2322 Err(error) => {
2323 return Ok(DirectoryEntry::Error(
2324 error.as_error_message(symlink, result).into(),
2325 ));
2326 }
2327 };
2328 Ok(match *real_path.get_type().await? {
2329 FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2330 FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2331 FileSystemEntryType::NotFound => DirectoryEntry::Error(
2333 format!("Symlink {symlink} points at {real_path} which does not exist").into(),
2334 ),
2335 FileSystemEntryType::Symlink => bail!(
2337 "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2338 ),
2339 _ => self,
2340 })
2341 } else {
2342 Ok(self)
2343 }
2344 }
2345
2346 pub fn path(self) -> Option<FileSystemPath> {
2347 match self {
2348 DirectoryEntry::File(path)
2349 | DirectoryEntry::Directory(path)
2350 | DirectoryEntry::Symlink(path)
2351 | DirectoryEntry::Other(path) => Some(path),
2352 DirectoryEntry::Error(_) => None,
2353 }
2354 }
2355}
2356
2357#[turbo_tasks::value]
2358#[derive(Hash, Clone, Copy, Debug)]
2359pub enum FileSystemEntryType {
2360 NotFound,
2361 File,
2362 Directory,
2363 Symlink,
2364 Other,
2366 Error,
2367}
2368
2369impl From<FileType> for FileSystemEntryType {
2370 fn from(file_type: FileType) -> Self {
2371 match file_type {
2372 t if t.is_dir() => FileSystemEntryType::Directory,
2373 t if t.is_file() => FileSystemEntryType::File,
2374 t if t.is_symlink() => FileSystemEntryType::Symlink,
2375 _ => FileSystemEntryType::Other,
2376 }
2377 }
2378}
2379
2380impl From<DirectoryEntry> for FileSystemEntryType {
2381 fn from(entry: DirectoryEntry) -> Self {
2382 FileSystemEntryType::from(&entry)
2383 }
2384}
2385
2386impl From<&DirectoryEntry> for FileSystemEntryType {
2387 fn from(entry: &DirectoryEntry) -> Self {
2388 match entry {
2389 DirectoryEntry::File(_) => FileSystemEntryType::File,
2390 DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2391 DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2392 DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2393 DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2394 }
2395 }
2396}
2397
2398impl From<RawDirectoryEntry> for FileSystemEntryType {
2399 fn from(entry: RawDirectoryEntry) -> Self {
2400 FileSystemEntryType::from(&entry)
2401 }
2402}
2403
2404impl From<&RawDirectoryEntry> for FileSystemEntryType {
2405 fn from(entry: &RawDirectoryEntry) -> Self {
2406 match entry {
2407 RawDirectoryEntry::File => FileSystemEntryType::File,
2408 RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2409 RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2410 RawDirectoryEntry::Other => FileSystemEntryType::Other,
2411 }
2412 }
2413}
2414
2415#[turbo_tasks::value]
2416#[derive(Debug)]
2417pub enum RawDirectoryContent {
2418 Entries(AutoMap<RcStr, RawDirectoryEntry>),
2421 NotFound,
2422}
2423
2424impl RawDirectoryContent {
2425 pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2426 Self::cell(RawDirectoryContent::Entries(entries))
2427 }
2428
2429 pub fn not_found() -> Vc<Self> {
2430 Self::cell(RawDirectoryContent::NotFound)
2431 }
2432}
2433
2434#[turbo_tasks::value]
2435#[derive(Debug)]
2436pub enum DirectoryContent {
2437 Entries(AutoMap<RcStr, DirectoryEntry>),
2438 NotFound,
2439}
2440
2441impl DirectoryContent {
2442 pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2443 Self::cell(DirectoryContent::Entries(entries))
2444 }
2445
2446 pub fn not_found() -> Vc<Self> {
2447 Self::cell(DirectoryContent::NotFound)
2448 }
2449}
2450
2451#[turbo_tasks::value(shared)]
2452pub struct NullFileSystem;
2453
2454#[turbo_tasks::value_impl]
2455impl FileSystem for NullFileSystem {
2456 #[turbo_tasks::function]
2457 fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2458 FileContent::NotFound.cell()
2459 }
2460
2461 #[turbo_tasks::function]
2462 fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2463 LinkContent::NotFound.into()
2464 }
2465
2466 #[turbo_tasks::function]
2467 fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2468 RawDirectoryContent::not_found()
2469 }
2470
2471 #[turbo_tasks::function]
2472 fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) -> Vc<()> {
2473 Vc::default()
2474 }
2475
2476 #[turbo_tasks::function]
2477 fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) -> Vc<()> {
2478 Vc::default()
2479 }
2480
2481 #[turbo_tasks::function]
2482 fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2483 FileMeta::default().cell()
2484 }
2485}
2486
2487#[turbo_tasks::value_impl]
2488impl ValueToString for NullFileSystem {
2489 #[turbo_tasks::function]
2490 fn to_string(&self) -> Vc<RcStr> {
2491 Vc::cell(rcstr!("null"))
2492 }
2493}
2494
2495pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2496 loop {
2497 if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2498 path = fs.get_inner_fs_path(path).owned().await?;
2499 continue;
2500 }
2501
2502 if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2503 let sys_path = fs.await?.to_sys_path(&path);
2504 return Ok(Some(sys_path));
2505 }
2506
2507 return Ok(None);
2508 }
2509}
2510
2511#[turbo_tasks::function]
2512async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2513 let fs = path.fs().to_resolved().await?;
2514 match &*fs.raw_read_dir(path.clone()).await? {
2515 RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2516 RawDirectoryContent::Entries(entries) => {
2517 let mut normalized_entries = AutoMap::new();
2518 let dir_path = &path.path;
2519 for (name, entry) in entries {
2520 let path = if dir_path.is_empty() {
2524 name.clone()
2525 } else {
2526 RcStr::from(format!("{dir_path}/{name}"))
2527 };
2528
2529 let entry_path = FileSystemPath::new_normalized(fs, path);
2530 let entry = match entry {
2531 RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2532 RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2533 RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2534 RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2535 };
2536 normalized_entries.insert(name.clone(), entry);
2537 }
2538 Ok(DirectoryContent::new(normalized_entries))
2539 }
2540 }
2541}
2542
2543#[turbo_tasks::function]
2544async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2545 if path.is_root() {
2546 return Ok(FileSystemEntryType::Directory.cell());
2547 }
2548 let parent = path.parent();
2549 let dir_content = parent.raw_read_dir().await?;
2550 match &*dir_content {
2551 RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2552 RawDirectoryContent::Entries(entries) => {
2553 let (_, file_name) = path.split_file_name();
2554 if let Some(entry) = entries.get(file_name) {
2555 Ok(FileSystemEntryType::from(entry).cell())
2556 } else {
2557 Ok(FileSystemEntryType::NotFound.cell())
2558 }
2559 }
2560 }
2561}
2562
2563#[turbo_tasks::function]
2564async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2565 let mut current_path = path;
2566 let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2567 let mut visited: AutoSet<RcStr> = AutoSet::new();
2568 let mut error = RealPathResultError::TooManySymlinks;
2569 for _i in 0..40 {
2572 if current_path.is_root() {
2573 return Ok(RealPathResult {
2575 path_result: Ok(current_path),
2576 symlinks: symlinks.into_iter().collect(),
2577 }
2578 .cell());
2579 }
2580
2581 if !visited.insert(current_path.path.clone()) {
2582 error = RealPathResultError::CycleDetected;
2583 break; }
2585
2586 let parent = current_path.parent();
2588 let parent_result = parent.realpath_with_links().owned().await?;
2589 let basename = current_path
2590 .path
2591 .rsplit_once('/')
2592 .map_or(current_path.path.as_str(), |(_, name)| name);
2593 symlinks.extend(parent_result.symlinks);
2594 let parent_path = match parent_result.path_result {
2595 Ok(path) => {
2596 if path != parent {
2597 current_path = path.join(basename)?;
2598 }
2599 path
2600 }
2601 Err(parent_error) => {
2602 error = parent_error;
2603 break;
2604 }
2605 };
2606
2607 if !matches!(
2610 *current_path.get_type().await?,
2611 FileSystemEntryType::Symlink
2612 ) {
2613 return Ok(RealPathResult {
2614 path_result: Ok(current_path),
2615 symlinks: symlinks.into_iter().collect(), }
2617 .cell());
2618 }
2619
2620 match &*current_path.read_link().await? {
2621 LinkContent::Link { target, link_type } => {
2622 symlinks.insert(current_path.clone());
2623 current_path = if link_type.contains(LinkType::ABSOLUTE) {
2624 current_path.root().owned().await?
2625 } else {
2626 parent_path
2627 }
2628 .join(target)?;
2629 }
2630 LinkContent::NotFound => {
2631 error = RealPathResultError::NotFound;
2632 break;
2633 }
2634 LinkContent::Invalid => {
2635 error = RealPathResultError::Invalid;
2636 break;
2637 }
2638 }
2639 }
2640
2641 Ok(RealPathResult {
2649 path_result: Err(error),
2650 symlinks: symlinks.into_iter().collect(),
2651 }
2652 .cell())
2653}
2654
2655#[cfg(test)]
2656mod tests {
2657 use turbo_rcstr::rcstr;
2658 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2659
2660 use super::*;
2661
2662 #[test]
2663 fn test_get_relative_path_to() {
2664 assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2665 assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2666 assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2667 assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2668 assert_eq!(
2669 get_relative_path_to("a/b/c", "c/b/a").as_str(),
2670 "../../../c/b/a"
2671 );
2672 assert_eq!(
2673 get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2674 "../../../c/b/a"
2675 );
2676 }
2677
2678 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2679 async fn with_extension() {
2680 turbo_tasks_testing::VcStorage::with(async {
2681 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2682 .to_resolved()
2683 .await?;
2684
2685 let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2686
2687 let path_json = path_txt.with_extension("json");
2688 assert_eq!(&*path_json.path, "foo/bar.json");
2689
2690 let path_no_ext = path_txt.with_extension("");
2691 assert_eq!(&*path_no_ext.path, "foo/bar");
2692
2693 let path_new_ext = path_no_ext.with_extension("json");
2694 assert_eq!(&*path_new_ext.path, "foo/bar.json");
2695
2696 let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2697
2698 let path_no_slash_json = path_no_slash_txt.with_extension("json");
2699 assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2700
2701 let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2702 assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2703
2704 let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2705 assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2706
2707 anyhow::Ok(())
2708 })
2709 .await
2710 .unwrap()
2711 }
2712
2713 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2714 async fn file_stem() {
2715 turbo_tasks_testing::VcStorage::with(async {
2716 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2717 .to_resolved()
2718 .await?;
2719
2720 let path = FileSystemPath::new_normalized(fs, rcstr!(""));
2721 assert_eq!(path.file_stem(), None);
2722
2723 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2724 assert_eq!(path.file_stem(), Some("bar"));
2725
2726 let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2727 assert_eq!(path.file_stem(), Some("bar"));
2728
2729 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
2730 assert_eq!(path.file_stem(), Some("bar"));
2731
2732 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
2733 assert_eq!(path.file_stem(), Some(".bar"));
2734
2735 anyhow::Ok(())
2736 })
2737 .await
2738 .unwrap()
2739 }
2740
2741 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2742 async fn test_try_from_sys_path() {
2743 let sys_root = if cfg!(windows) {
2744 Path::new(r"C:\fake\root")
2745 } else {
2746 Path::new(r"/fake/root")
2747 };
2748
2749 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2750 BackendOptions::default(),
2751 noop_backing_storage(),
2752 ));
2753 tt.run_once(async {
2754 let fs_vc =
2755 DiskFileSystem::new(rcstr!("temp"), RcStr::from(sys_root.to_str().unwrap()))
2756 .to_resolved()
2757 .await?;
2758 let fs = fs_vc.await?;
2759 let fs_root_path = fs_vc.root().await?;
2760
2761 assert_eq!(
2762 fs.try_from_sys_path(
2763 fs_vc,
2764 &Path::new("relative").join("directory"),
2765 None,
2766 )
2767 .unwrap()
2768 .path,
2769 "relative/directory"
2770 );
2771
2772 assert_eq!(
2773 fs.try_from_sys_path(
2774 fs_vc,
2775 &sys_root
2776 .join("absolute")
2777 .join("directory")
2778 .join("..")
2779 .join("normalized_path"),
2780 Some(&fs_root_path.join("ignored").unwrap()),
2781 )
2782 .unwrap()
2783 .path,
2784 "absolute/normalized_path"
2785 );
2786
2787 assert_eq!(
2788 fs.try_from_sys_path(
2789 fs_vc,
2790 Path::new("child"),
2791 Some(&fs_root_path.join("parent").unwrap()),
2792 )
2793 .unwrap()
2794 .path,
2795 "parent/child"
2796 );
2797
2798 assert_eq!(
2799 fs.try_from_sys_path(
2800 fs_vc,
2801 &Path::new("..").join("parallel_dir"),
2802 Some(&fs_root_path.join("parent").unwrap()),
2803 )
2804 .unwrap()
2805 .path,
2806 "parallel_dir"
2807 );
2808
2809 assert_eq!(
2810 fs.try_from_sys_path(
2811 fs_vc,
2812 &Path::new("relative")
2813 .join("..")
2814 .join("..")
2815 .join("leaves_root"),
2816 None,
2817 ),
2818 None
2819 );
2820
2821 assert_eq!(
2822 fs.try_from_sys_path(
2823 fs_vc,
2824 &sys_root
2825 .join("absolute")
2826 .join("..")
2827 .join("..")
2828 .join("leaves_root"),
2829 None,
2830 ),
2831 None
2832 );
2833
2834 anyhow::Ok(())
2835 })
2836 .await
2837 .unwrap();
2838 }
2839
2840 #[cfg(test)]
2842 mod denied_path_tests {
2843 use std::{
2844 fs::{File, create_dir_all},
2845 io::Write,
2846 };
2847
2848 use turbo_rcstr::{RcStr, rcstr};
2849 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2850
2851 use crate::{
2852 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
2853 FileSystemPath,
2854 glob::{Glob, GlobOptions},
2855 };
2856
2857 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
2860 let scratch = tempfile::tempdir().unwrap();
2861 let path = scratch.path();
2862
2863 File::create_new(path.join("allowed_file.txt"))
2870 .unwrap()
2871 .write_all(b"allowed content")
2872 .unwrap();
2873
2874 create_dir_all(path.join("allowed_dir")).unwrap();
2875 File::create_new(path.join("allowed_dir/file.txt"))
2876 .unwrap()
2877 .write_all(b"allowed dir content")
2878 .unwrap();
2879
2880 File::create_new(path.join("other_file.txt"))
2881 .unwrap()
2882 .write_all(b"other content")
2883 .unwrap();
2884
2885 create_dir_all(path.join("denied_dir/nested")).unwrap();
2886 File::create_new(path.join("denied_dir/secret.txt"))
2887 .unwrap()
2888 .write_all(b"secret content")
2889 .unwrap();
2890 File::create_new(path.join("denied_dir/nested/deep.txt"))
2891 .unwrap()
2892 .write_all(b"deep secret")
2893 .unwrap();
2894
2895 let root: RcStr = path.to_str().unwrap().into();
2896 let denied_path: RcStr = rcstr!("denied_dir");
2898
2899 (scratch, root, denied_path)
2900 }
2901
2902 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2903 async fn test_denied_path_read() {
2904 let (_scratch, root, denied_path) = setup_test_fs();
2905 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2906 BackendOptions::default(),
2907 noop_backing_storage(),
2908 ));
2909
2910 tt.run_once(async {
2911 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
2912 let root_path = fs.root().await?;
2913
2914 let allowed_file = root_path.join("allowed_file.txt")?;
2916 let content = allowed_file.read().await?;
2917 assert!(
2918 matches!(&*content, FileContent::Content(_)),
2919 "allowed file should be readable"
2920 );
2921
2922 let denied_file = root_path.join("denied_dir/secret.txt")?;
2924 let content = denied_file.read().await?;
2925 assert!(
2926 matches!(&*content, FileContent::NotFound),
2927 "denied file should return NotFound, got {:?}",
2928 content
2929 );
2930
2931 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
2933 let content = nested_denied.read().await?;
2934 assert!(
2935 matches!(&*content, FileContent::NotFound),
2936 "nested denied file should return NotFound"
2937 );
2938
2939 let denied_dir = root_path.join("denied_dir")?;
2941 let content = denied_dir.read().await?;
2942 assert!(
2943 matches!(&*content, FileContent::NotFound),
2944 "denied directory should return NotFound"
2945 );
2946
2947 anyhow::Ok(())
2948 })
2949 .await
2950 .unwrap();
2951 }
2952
2953 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2954 async fn test_denied_path_read_dir() {
2955 let (_scratch, root, denied_path) = setup_test_fs();
2956 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2957 BackendOptions::default(),
2958 noop_backing_storage(),
2959 ));
2960
2961 tt.run_once(async {
2962 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
2963 let root_path = fs.root().await?;
2964
2965 let dir_content = root_path.read_dir().await?;
2967 match &*dir_content {
2968 DirectoryContent::Entries(entries) => {
2969 assert!(
2970 entries.contains_key(&rcstr!("allowed_dir")),
2971 "allowed_dir should be visible"
2972 );
2973 assert!(
2974 entries.contains_key(&rcstr!("other_file.txt")),
2975 "other_file.txt should be visible"
2976 );
2977 assert!(
2978 entries.contains_key(&rcstr!("allowed_file.txt")),
2979 "allowed_file.txt should be visible"
2980 );
2981 assert!(
2982 !entries.contains_key(&rcstr!("denied_dir")),
2983 "denied_dir should NOT be visible in read_dir"
2984 );
2985 }
2986 DirectoryContent::NotFound => panic!("root directory should exist"),
2987 }
2988
2989 let denied_dir = root_path.join("denied_dir")?;
2991 let dir_content = denied_dir.read_dir().await?;
2992 assert!(
2993 matches!(&*dir_content, DirectoryContent::NotFound),
2994 "denied_dir read_dir should return NotFound"
2995 );
2996
2997 anyhow::Ok(())
2998 })
2999 .await
3000 .unwrap();
3001 }
3002
3003 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3004 async fn test_denied_path_read_glob() {
3005 let (_scratch, root, denied_path) = setup_test_fs();
3006 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3007 BackendOptions::default(),
3008 noop_backing_storage(),
3009 ));
3010
3011 tt.run_once(async {
3012 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3013 let root_path = fs.root().await?;
3014
3015 let glob_result = root_path
3017 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3018 .await?;
3019
3020 assert!(
3022 glob_result.results.contains_key("allowed_file.txt"),
3023 "allowed_file.txt should be found"
3024 );
3025 assert!(
3026 glob_result.results.contains_key("other_file.txt"),
3027 "other_file.txt should be found"
3028 );
3029 assert!(
3030 !glob_result.results.contains_key("denied_dir"),
3031 "denied_dir should NOT appear in glob results"
3032 );
3033
3034 assert!(
3036 !glob_result.inner.contains_key("denied_dir"),
3037 "denied_dir should NOT appear in glob inner results"
3038 );
3039
3040 assert!(
3042 glob_result.inner.contains_key("allowed_dir"),
3043 "allowed_dir directory should be present"
3044 );
3045 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3046 assert!(
3047 sub_inner.results.contains_key("file.txt"),
3048 "allowed_dir/file.txt should be found"
3049 );
3050
3051 anyhow::Ok(())
3052 })
3053 .await
3054 .unwrap();
3055 }
3056
3057 #[turbo_tasks::function(operation)]
3058 async fn write_file(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
3059 path.write(
3060 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3061 .cell(),
3062 )
3063 .await?;
3064 Ok(())
3065 }
3066
3067 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3068 async fn test_denied_path_write() {
3069 use turbo_tasks::apply_effects;
3070
3071 let (_scratch, root, denied_path) = setup_test_fs();
3072 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3073 BackendOptions::default(),
3074 noop_backing_storage(),
3075 ));
3076
3077 tt.run_once(async {
3078 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3079 let root_path = fs.root().await?;
3080
3081 let allowed_file = root_path.join("allowed_dir/new_file.txt")?;
3083 let write_result = write_file(allowed_file.clone(), rcstr!("test content"));
3084 write_result.read_strongly_consistent().await?;
3085 apply_effects(write_result).await?;
3086
3087 let read_content = allowed_file.read().await?;
3089 assert!(
3090 matches!(&*read_content, FileContent::Content(_)),
3091 "allowed file write should succeed"
3092 );
3093
3094 let denied_file = root_path.join("denied_dir/forbidden.txt")?;
3096 let write_result = write_file(denied_file, rcstr!("forbidden"));
3097 let result = write_result.read_strongly_consistent().await;
3098 assert!(
3099 result.is_err(),
3100 "writing to denied path should return an error"
3101 );
3102
3103 let nested_denied = root_path.join("denied_dir/nested/file.txt")?;
3105 let write_result = write_file(nested_denied, rcstr!("nested"));
3106 let result = write_result.read_strongly_consistent().await;
3107 assert!(
3108 result.is_err(),
3109 "writing to nested denied path should return an error"
3110 );
3111
3112 anyhow::Ok(())
3113 })
3114 .await
3115 .unwrap();
3116 }
3117 }
3118}