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 bincode::{Decode, Encode};
48use bitflags::bitflags;
49use dunce::simplified;
50use indexmap::IndexSet;
51use jsonc_parser::{ParseOptions, parse_to_serde_value};
52use mime::Mime;
53use rustc_hash::FxHashSet;
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 number_env_var(name: &'static str) -> Option<usize> {
197 env::var(name)
198 .ok()
199 .filter(|val| !val.is_empty())
200 .map(|val| match val.parse() {
201 Ok(n) => n,
202 Err(err) => panic!("{name} must be a valid integer: {err}"),
203 })
204 .filter(|val| *val != 0)
205}
206
207fn create_read_semaphore() -> tokio::sync::Semaphore {
208 static TURBO_ENGINE_READ_CONCURRENCY: LazyLock<usize> =
211 LazyLock::new(|| number_env_var("TURBO_ENGINE_READ_CONCURRENCY").unwrap_or(64));
212 tokio::sync::Semaphore::new(*TURBO_ENGINE_READ_CONCURRENCY)
213}
214
215fn create_write_semaphore() -> tokio::sync::Semaphore {
216 static TURBO_ENGINE_WRITE_CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
219 number_env_var("TURBO_ENGINE_WRITE_CONCURRENCY").unwrap_or(
220 4,
223 )
224 });
225 tokio::sync::Semaphore::new(*TURBO_ENGINE_WRITE_CONCURRENCY)
226}
227
228#[turbo_tasks::value_trait]
229pub trait FileSystem: ValueToString {
230 #[turbo_tasks::function]
232 fn root(self: ResolvedVc<Self>) -> Vc<FileSystemPath> {
233 FileSystemPath::new_normalized(self, RcStr::default()).cell()
234 }
235 #[turbo_tasks::function]
236 fn read(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileContent>;
237 #[turbo_tasks::function]
238 fn read_link(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<LinkContent>;
239 #[turbo_tasks::function]
240 fn raw_read_dir(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<RawDirectoryContent>;
241 #[turbo_tasks::function]
242 fn write(self: Vc<Self>, fs_path: FileSystemPath, content: Vc<FileContent>) -> Vc<()>;
243 #[turbo_tasks::function]
244 fn write_link(self: Vc<Self>, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Vc<()>;
245 #[turbo_tasks::function]
246 fn metadata(self: Vc<Self>, fs_path: FileSystemPath) -> Vc<FileMeta>;
247}
248
249#[derive(Default)]
250struct DiskFileSystemApplyContext {
251 created_directories: FxHashSet<PathBuf>,
253}
254
255#[derive(TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode)]
256struct DiskFileSystemInner {
257 pub name: RcStr,
258 pub root: RcStr,
259 #[turbo_tasks(debug_ignore, trace_ignore)]
260 #[bincode(skip)]
261 mutex_map: MutexMap<PathBuf>,
262 #[turbo_tasks(debug_ignore, trace_ignore)]
263 #[bincode(skip)]
264 invalidator_map: InvalidatorMap,
265 #[turbo_tasks(debug_ignore, trace_ignore)]
266 #[bincode(skip)]
267 dir_invalidator_map: InvalidatorMap,
268 #[turbo_tasks(debug_ignore, trace_ignore)]
271 #[bincode(skip)]
272 invalidation_lock: RwLock<()>,
273 #[turbo_tasks(debug_ignore, trace_ignore)]
275 #[bincode(skip, default = "create_read_semaphore")]
276 read_semaphore: tokio::sync::Semaphore,
277 #[turbo_tasks(debug_ignore, trace_ignore)]
279 #[bincode(skip, default = "create_write_semaphore")]
280 write_semaphore: tokio::sync::Semaphore,
281
282 #[turbo_tasks(debug_ignore, trace_ignore)]
283 watcher: DiskWatcher,
284 denied_path: Option<RcStr>,
287}
288
289impl DiskFileSystemInner {
290 fn root_path(&self) -> &Path {
292 simplified(Path::new(&*self.root))
294 }
295
296 fn is_path_denied(&self, path: &FileSystemPath) -> bool {
306 let Some(denied_path) = &self.denied_path else {
307 return false;
308 };
309 let path = &path.path;
314 path.starts_with(denied_path.as_str())
315 && (path.len() == denied_path.len()
316 || path.as_bytes().get(denied_path.len()) == Some(&b'/'))
317 }
318
319 fn register_read_invalidator(&self, path: &Path) -> Result<()> {
322 if let Some(invalidator) = turbo_tasks::get_invalidator() {
323 self.invalidator_map
324 .insert(path.to_owned(), invalidator, None);
325 self.watcher.ensure_watched_file(path, self.root_path())?;
326 }
327 Ok(())
328 }
329
330 fn register_write_invalidator(
334 &self,
335 path: &Path,
336 invalidator: Invalidator,
337 write_content: WriteContent,
338 ) -> Result<Vec<(Invalidator, Option<WriteContent>)>> {
339 let mut invalidator_map = self.invalidator_map.lock().unwrap();
340 let invalidators = invalidator_map.entry(path.to_owned()).or_default();
341 let old_invalidators = invalidators
342 .extract_if(|i, old_write_content| {
343 i == &invalidator
344 || old_write_content
345 .as_ref()
346 .is_none_or(|old| old != &write_content)
347 })
348 .filter(|(i, _)| i != &invalidator)
349 .collect::<Vec<_>>();
350 invalidators.insert(invalidator, Some(write_content));
351 drop(invalidator_map);
352 self.watcher.ensure_watched_file(path, self.root_path())?;
353 Ok(old_invalidators)
354 }
355
356 fn register_dir_invalidator(&self, path: &Path) -> Result<()> {
359 if let Some(invalidator) = turbo_tasks::get_invalidator() {
360 self.dir_invalidator_map
361 .insert(path.to_owned(), invalidator, None);
362 self.watcher.ensure_watched_dir(path, self.root_path())?;
363 }
364 Ok(())
365 }
366
367 async fn lock_path(&self, full_path: &Path) -> PathLockGuard<'_> {
368 let lock1 = self.invalidation_lock.read().await;
369 let lock2 = self.mutex_map.lock(full_path.to_path_buf()).await;
370 PathLockGuard(lock1, lock2)
371 }
372
373 fn invalidate(&self) {
374 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
375 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
376 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
377 let invalidators = invalidator_map
378 .into_iter()
379 .chain(dir_invalidator_map)
380 .flat_map(|(_, invalidators)| invalidators.into_keys())
381 .collect::<Vec<_>>();
382 parallel::for_each_owned(invalidators, |invalidator| invalidator.invalidate());
383 }
384
385 fn invalidate_with_reason<R: InvalidationReason + Clone>(
389 &self,
390 reason: impl Fn(&Path) -> R + Sync,
391 ) {
392 let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered();
393 let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap());
394 let dir_invalidator_map = take(&mut *self.dir_invalidator_map.lock().unwrap());
395 let invalidators = invalidator_map
396 .into_iter()
397 .chain(dir_invalidator_map)
398 .flat_map(|(path, invalidators)| {
399 let reason_for_path = reason(&path);
400 invalidators
401 .into_keys()
402 .map(move |i| (reason_for_path.clone(), i))
403 })
404 .collect::<Vec<_>>();
405 parallel::for_each_owned(invalidators, |(reason, invalidator)| {
406 invalidator.invalidate_with_reason(reason)
407 });
408 }
409
410 fn invalidate_from_write(
411 &self,
412 full_path: &Path,
413 invalidators: Vec<(Invalidator, Option<WriteContent>)>,
414 ) {
415 if !invalidators.is_empty() {
416 if let Some(path) = format_absolute_fs_path(full_path, &self.name, self.root_path()) {
417 if invalidators.len() == 1 {
418 let (invalidator, _) = invalidators.into_iter().next().unwrap();
419 invalidator.invalidate_with_reason(Write { path });
420 } else {
421 invalidators.into_iter().for_each(|(invalidator, _)| {
422 invalidator.invalidate_with_reason(Write { path: path.clone() });
423 });
424 }
425 } else {
426 invalidators.into_iter().for_each(|(invalidator, _)| {
427 invalidator.invalidate();
428 });
429 }
430 }
431 }
432
433 #[tracing::instrument(level = "info", name = "start filesystem watching", skip_all, fields(path = %self.root))]
434 async fn start_watching_internal(
435 self: &Arc<Self>,
436 report_invalidation_reason: bool,
437 poll_interval: Option<Duration>,
438 ) -> Result<()> {
439 let root_path = self.root_path().to_path_buf();
440
441 retry_blocking(root_path.clone(), move |path| {
443 let _tracing =
444 tracing::info_span!("create root directory", name = display(path.display()))
445 .entered();
446
447 std::fs::create_dir_all(path)
448 })
449 .concurrency_limited(&self.write_semaphore)
450 .await?;
451
452 self.watcher
453 .start_watching(self.clone(), report_invalidation_reason, poll_interval)?;
454
455 Ok(())
456 }
457
458 async fn create_directory(self: &Arc<Self>, directory: &Path) -> Result<()> {
459 let already_created = ApplyEffectsContext::with_or_insert_with(
460 DiskFileSystemApplyContext::default,
461 |fs_context| fs_context.created_directories.contains(directory),
462 );
463 if !already_created {
464 let func = |p: &Path| std::fs::create_dir_all(p);
465 retry_blocking(directory.to_path_buf(), func)
466 .concurrency_limited(&self.write_semaphore)
467 .instrument(tracing::info_span!(
468 "create directory",
469 name = display(directory.display())
470 ))
471 .await?;
472 ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| {
473 fs_context
474 .created_directories
475 .insert(directory.to_path_buf())
476 });
477 }
478 Ok(())
479 }
480}
481
482#[turbo_tasks::value(cell = "new", eq = "manual")]
483pub struct DiskFileSystem {
484 inner: Arc<DiskFileSystemInner>,
485}
486
487impl DiskFileSystem {
488 pub fn name(&self) -> &RcStr {
489 &self.inner.name
490 }
491
492 pub fn root(&self) -> &RcStr {
493 &self.inner.root
494 }
495
496 pub fn invalidate(&self) {
497 self.inner.invalidate();
498 }
499
500 pub fn invalidate_with_reason<R: InvalidationReason + Clone>(
501 &self,
502 reason: impl Fn(&Path) -> R + Sync,
503 ) {
504 self.inner.invalidate_with_reason(reason);
505 }
506
507 pub async fn start_watching(&self, poll_interval: Option<Duration>) -> Result<()> {
508 self.inner
509 .start_watching_internal(false, poll_interval)
510 .await
511 }
512
513 pub async fn start_watching_with_invalidation_reason(
514 &self,
515 poll_interval: Option<Duration>,
516 ) -> Result<()> {
517 self.inner
518 .start_watching_internal(true, poll_interval)
519 .await
520 }
521
522 pub fn stop_watching(&self) {
523 self.inner.watcher.stop_watching();
524 }
525
526 pub fn try_from_sys_path(
539 &self,
540 vc_self: ResolvedVc<DiskFileSystem>,
541 sys_path: &Path,
542 relative_to: Option<&FileSystemPath>,
543 ) -> Option<FileSystemPath> {
544 let vc_self = ResolvedVc::upcast(vc_self);
545
546 let sys_path = simplified(sys_path);
547 let relative_sys_path = if sys_path.is_absolute() {
548 let normalized_sys_path = sys_path.normalize_lexically().ok()?;
551 normalized_sys_path
552 .strip_prefix(self.inner.root_path())
553 .ok()?
554 .to_owned()
555 } else if let Some(relative_to) = relative_to {
556 debug_assert_eq!(
557 relative_to.fs, vc_self,
558 "`relative_to.fs` must match the current `ResolvedVc<DiskFileSystem>`"
559 );
560 let mut joined_sys_path = PathBuf::from(unix_to_sys(&relative_to.path).into_owned());
561 joined_sys_path.push(sys_path);
562 joined_sys_path.normalize_lexically().ok()?
563 } else {
564 sys_path.normalize_lexically().ok()?
565 };
566
567 Some(FileSystemPath {
568 fs: vc_self,
569 path: RcStr::from(sys_to_unix(relative_sys_path.to_str()?)),
570 })
571 }
572
573 pub fn to_sys_path(&self, fs_path: &FileSystemPath) -> PathBuf {
574 let path = self.inner.root_path();
575 if fs_path.path.is_empty() {
576 path.to_path_buf()
577 } else {
578 path.join(&*unix_to_sys(&fs_path.path))
579 }
580 }
581}
582
583#[allow(dead_code, reason = "we need to hold onto the locks")]
584struct PathLockGuard<'a>(
585 #[allow(dead_code)] RwLockReadGuard<'a, ()>,
586 #[allow(dead_code)] mutex_map::MutexMapGuard<'a, PathBuf>,
587);
588
589fn format_absolute_fs_path(path: &Path, name: &str, root_path: &Path) -> Option<String> {
590 if let Ok(rel_path) = path.strip_prefix(root_path) {
591 let path = if MAIN_SEPARATOR != '/' {
592 let rel_path = rel_path.to_string_lossy().replace(MAIN_SEPARATOR, "/");
593 format!("[{name}]/{rel_path}")
594 } else {
595 format!("[{name}]/{}", rel_path.display())
596 };
597 Some(path)
598 } else {
599 None
600 }
601}
602
603impl DiskFileSystem {
604 pub fn new(name: RcStr, root: RcStr) -> Vc<Self> {
611 Self::new_internal(name, root, None)
612 }
613
614 pub fn new_with_denied_path(name: RcStr, root: RcStr, denied_path: RcStr) -> Vc<Self> {
623 debug_assert!(!denied_path.is_empty(), "denied_path must not be empty");
624 debug_assert!(
625 normalize_path(&denied_path).as_deref() == Some(&*denied_path),
626 "denied_path must be normalized: {denied_path:?}"
627 );
628 Self::new_internal(name, root, Some(denied_path))
629 }
630}
631
632#[turbo_tasks::value_impl]
633impl DiskFileSystem {
634 #[turbo_tasks::function]
635 fn new_internal(name: RcStr, root: RcStr, denied_path: Option<RcStr>) -> Vc<Self> {
636 mark_stateful();
637
638 let instance = DiskFileSystem {
639 inner: Arc::new(DiskFileSystemInner {
640 name,
641 root,
642 mutex_map: Default::default(),
643 invalidation_lock: Default::default(),
644 invalidator_map: InvalidatorMap::new(),
645 dir_invalidator_map: InvalidatorMap::new(),
646 read_semaphore: create_read_semaphore(),
647 write_semaphore: create_write_semaphore(),
648 watcher: DiskWatcher::new(),
649 denied_path,
650 }),
651 };
652
653 Self::cell(instance)
654 }
655}
656
657impl Debug for DiskFileSystem {
658 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
659 write!(f, "name: {}, root: {}", self.inner.name, self.inner.root)
660 }
661}
662
663#[turbo_tasks::value_impl]
664impl FileSystem for DiskFileSystem {
665 #[turbo_tasks::function(fs)]
666 async fn read(&self, fs_path: FileSystemPath) -> Result<Vc<FileContent>> {
667 mark_session_dependent();
668
669 if self.inner.is_path_denied(&fs_path) {
671 return Ok(FileContent::NotFound.cell());
672 }
673 let full_path = self.to_sys_path(&fs_path);
674
675 self.inner.register_read_invalidator(&full_path)?;
676
677 let _lock = self.inner.lock_path(&full_path).await;
678 let content = match retry_blocking(full_path.clone(), |path: &Path| File::from_path(path))
679 .concurrency_limited(&self.inner.read_semaphore)
680 .instrument(tracing::info_span!(
681 "read file",
682 name = display(full_path.display())
683 ))
684 .await
685 {
686 Ok(file) => FileContent::new(file),
687 Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => {
688 FileContent::NotFound
689 }
690 Err(e) => {
691 bail!(anyhow!(e).context(format!("reading file {}", full_path.display())))
692 }
693 };
694 Ok(content.cell())
695 }
696
697 #[turbo_tasks::function(fs)]
698 async fn raw_read_dir(&self, fs_path: FileSystemPath) -> Result<Vc<RawDirectoryContent>> {
699 mark_session_dependent();
700
701 if self.inner.is_path_denied(&fs_path) {
703 return Ok(RawDirectoryContent::not_found());
704 }
705 let full_path = self.to_sys_path(&fs_path);
706
707 self.inner.register_dir_invalidator(&full_path)?;
708
709 let read_dir = match retry_blocking(full_path.clone(), |path| {
712 let _span =
713 tracing::info_span!("read directory", name = display(path.display())).entered();
714 std::fs::read_dir(path)
715 })
716 .concurrency_limited(&self.inner.read_semaphore)
717 .await
718 {
719 Ok(dir) => dir,
720 Err(e)
721 if e.kind() == ErrorKind::NotFound
722 || e.kind() == ErrorKind::NotADirectory
723 || e.kind() == ErrorKind::InvalidFilename =>
724 {
725 return Ok(RawDirectoryContent::not_found());
726 }
727 Err(e) => {
728 bail!(anyhow!(e).context(format!("reading dir {}", full_path.display())))
729 }
730 };
731 let denied_entry = match self.inner.denied_path.as_ref() {
732 Some(denied_path) => {
733 let dir_path = fs_path.path.as_str();
740 if denied_path.starts_with(dir_path) {
741 let denied_path_suffix =
742 if denied_path.as_bytes().get(dir_path.len()) == Some(&b'/') {
743 Some(&denied_path[dir_path.len() + 1..])
744 } else if dir_path.is_empty() {
745 Some(denied_path.as_str())
746 } else {
747 None
748 };
749 denied_path_suffix.filter(|s| !s.contains('/'))
751 } else {
752 None
753 }
754 }
755 None => None,
756 };
757
758 let entries = read_dir
759 .filter_map(|r| {
760 let e = match r {
761 Ok(e) => e,
762 Err(err) => return Some(Err(err.into())),
763 };
764
765 let file_name: RcStr = e.file_name().to_str()?.into();
767 if let Some(denied_name) = denied_entry
769 && denied_name == file_name.as_str()
770 {
771 return None;
772 }
773
774 let entry = match e.file_type() {
775 Ok(t) if t.is_file() => RawDirectoryEntry::File,
776 Ok(t) if t.is_dir() => RawDirectoryEntry::Directory,
777 Ok(t) if t.is_symlink() => RawDirectoryEntry::Symlink,
778 Ok(_) => RawDirectoryEntry::Other,
779 Err(err) => return Some(Err(err.into())),
780 };
781
782 Some(anyhow::Ok((file_name, entry)))
783 })
784 .collect::<Result<_>>()
785 .with_context(|| format!("reading directory item in {}", full_path.display()))?;
786
787 Ok(RawDirectoryContent::new(entries))
788 }
789
790 #[turbo_tasks::function(fs)]
791 async fn read_link(&self, fs_path: FileSystemPath) -> Result<Vc<LinkContent>> {
792 mark_session_dependent();
793
794 if self.inner.is_path_denied(&fs_path) {
796 return Ok(LinkContent::NotFound.cell());
797 }
798 let full_path = self.to_sys_path(&fs_path);
799
800 self.inner.register_read_invalidator(&full_path)?;
801
802 let _lock = self.inner.lock_path(&full_path).await;
803 let link_path =
804 match retry_blocking(full_path.clone(), |path: &Path| std::fs::read_link(path))
805 .concurrency_limited(&self.inner.read_semaphore)
806 .instrument(tracing::info_span!(
807 "read symlink",
808 name = display(full_path.display())
809 ))
810 .await
811 {
812 Ok(res) => res,
813 Err(_) => return Ok(LinkContent::NotFound.cell()),
814 };
815 let is_link_absolute = link_path.is_absolute();
816
817 let mut file = link_path.clone();
818 if !is_link_absolute {
819 if let Some(normalized_linked_path) = full_path.parent().and_then(|p| {
820 normalize_path(&sys_to_unix(p.join(&file).to_string_lossy().as_ref()))
821 }) {
822 #[cfg(target_family = "windows")]
823 {
824 file = PathBuf::from(normalized_linked_path);
825 }
826 #[cfg(not(target_family = "windows"))]
829 {
830 file = PathBuf::from(format!("/{normalized_linked_path}"));
831 }
832 } else {
833 return Ok(LinkContent::Invalid.cell());
834 }
835 }
836
837 let result = simplified(&file).strip_prefix(simplified(Path::new(&self.inner.root)));
844
845 let relative_to_root_path = match result {
846 Ok(file) => PathBuf::from(sys_to_unix(&file.to_string_lossy()).as_ref()),
847 Err(_) => return Ok(LinkContent::Invalid.cell()),
848 };
849
850 let (target, file_type) = if is_link_absolute {
851 let target_string: RcStr = relative_to_root_path.to_string_lossy().into();
852 (
853 target_string.clone(),
854 FileSystemPath::new_normalized(fs_path.fs().to_resolved().await?, target_string)
855 .get_type()
856 .await?,
857 )
858 } else {
859 let link_path_string_cow = link_path.to_string_lossy();
860 let link_path_unix: RcStr = sys_to_unix(&link_path_string_cow).into();
861 (
862 link_path_unix.clone(),
863 fs_path.parent().join(&link_path_unix)?.get_type().await?,
864 )
865 };
866
867 Ok(LinkContent::Link {
868 target,
869 link_type: {
870 let mut link_type = Default::default();
871 if link_path.is_absolute() {
872 link_type |= LinkType::ABSOLUTE;
873 }
874 if matches!(&*file_type, FileSystemEntryType::Directory) {
875 link_type |= LinkType::DIRECTORY;
876 }
877 link_type
878 },
879 }
880 .cell())
881 }
882
883 #[turbo_tasks::function(fs)]
884 async fn write(&self, fs_path: FileSystemPath, content: Vc<FileContent>) -> Result<()> {
885 if self.inner.is_path_denied(&fs_path) {
891 bail!(
892 "Cannot write to denied path: {}",
893 fs_path.value_to_string().await?
894 );
895 }
896 let full_path = self.to_sys_path(&fs_path);
897
898 let content = content.await?;
899
900 let inner = self.inner.clone();
901 let invalidator = turbo_tasks::get_invalidator();
902
903 effect(async move {
904 let full_path = validate_path_length(&full_path)?;
905
906 let _lock = inner.lock_path(&full_path).await;
907
908 let old_invalidators = invalidator
910 .map(|invalidator| {
911 inner.register_write_invalidator(
912 &full_path,
913 invalidator,
914 WriteContent::File(content.clone()),
915 )
916 })
917 .transpose()?
918 .unwrap_or_default();
919
920 let compare = content
926 .streaming_compare(&full_path)
927 .concurrency_limited(&inner.read_semaphore)
928 .instrument(tracing::info_span!(
929 "read file before write",
930 name = display(full_path.display())
931 ))
932 .await?;
933 if compare == FileComparison::Equal {
934 if !old_invalidators.is_empty() {
935 for (invalidator, write_content) in old_invalidators {
936 inner.invalidator_map.insert(
937 full_path.clone().into_owned(),
938 invalidator,
939 write_content,
940 );
941 }
942 }
943 return Ok(());
944 }
945
946 match &*content {
947 FileContent::Content(..) => {
948 let create_directory = compare == FileComparison::Create;
949 if create_directory && let Some(parent) = full_path.parent() {
950 inner.create_directory(parent).await.with_context(|| {
951 format!(
952 "failed to create directory {} for write to {}",
953 parent.display(),
954 full_path.display()
955 )
956 })?;
957 }
958
959 let full_path_to_write = full_path.clone();
960 let content = content.clone();
961 retry_blocking(full_path_to_write.into_owned(), move |full_path| {
962 use std::io::Write;
963
964 let mut f = std::fs::File::create(full_path)?;
965 let FileContent::Content(file) = &*content else {
966 unreachable!()
967 };
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 static WRITE_VERSION: LazyLock<bool> = LazyLock::new(|| {
974 std::env::var_os("TURBO_ENGINE_WRITE_VERSION")
975 .is_some_and(|v| v == "1" || v == "true")
976 });
977 if *WRITE_VERSION {
978 let mut full_path = full_path.to_owned();
979 let hash = hash_xxh3_hash64(file);
980 let ext = full_path.extension();
981 let ext = if let Some(ext) = ext {
982 format!("{:016x}.{}", hash, ext.to_string_lossy())
983 } else {
984 format!("{hash:016x}")
985 };
986 full_path.set_extension(ext);
987 let mut f = std::fs::File::create(&full_path)?;
988 std::io::copy(&mut file.read(), &mut f)?;
989 #[cfg(target_family = "unix")]
990 f.set_permissions(file.meta.permissions.into())?;
991 f.flush()?;
992 }
993 Ok::<(), io::Error>(())
994 })
995 .concurrency_limited(&inner.write_semaphore)
996 .instrument(tracing::info_span!(
997 "write file",
998 name = display(full_path.display())
999 ))
1000 .await
1001 .with_context(|| format!("failed to write to {}", full_path.display()))?;
1002 }
1003 FileContent::NotFound => {
1004 retry_blocking(full_path.clone().into_owned(), |path| {
1005 std::fs::remove_file(path)
1006 })
1007 .concurrency_limited(&inner.write_semaphore)
1008 .instrument(tracing::info_span!(
1009 "remove file",
1010 name = display(full_path.display())
1011 ))
1012 .await
1013 .or_else(|err| {
1014 if err.kind() == ErrorKind::NotFound {
1015 Ok(())
1016 } else {
1017 Err(err)
1018 }
1019 })
1020 .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
1021 }
1022 }
1023
1024 inner.invalidate_from_write(&full_path, old_invalidators);
1025
1026 Ok(())
1027 });
1028
1029 Ok(())
1030 }
1031
1032 #[turbo_tasks::function(fs)]
1033 async fn write_link(&self, fs_path: FileSystemPath, target: Vc<LinkContent>) -> Result<()> {
1034 if self.inner.is_path_denied(&fs_path) {
1040 bail!(
1041 "Cannot write link to denied path: {}",
1042 fs_path.value_to_string().await?
1043 );
1044 }
1045 let full_path = self.to_sys_path(&fs_path);
1046
1047 let content = target.await?;
1048 let inner = self.inner.clone();
1049 let invalidator = turbo_tasks::get_invalidator();
1050
1051 effect(async move {
1052 let full_path = validate_path_length(&full_path)?;
1053
1054 let _lock = inner.lock_path(&full_path).await;
1055
1056 let old_invalidators = invalidator
1057 .map(|invalidator| {
1058 inner.register_write_invalidator(
1059 &full_path,
1060 invalidator,
1061 WriteContent::Link(content.clone()),
1062 )
1063 })
1064 .transpose()?
1065 .unwrap_or_default();
1066
1067 let old_content = match retry_blocking(full_path.clone().into_owned(), |path| {
1070 std::fs::read_link(path)
1071 })
1072 .concurrency_limited(&inner.read_semaphore)
1073 .instrument(tracing::info_span!(
1074 "read symlink before write",
1075 name = display(full_path.display())
1076 ))
1077 .await
1078 {
1079 Ok(res) => Some((res.is_absolute(), res)),
1080 Err(_) => None,
1081 };
1082 let is_equal = match (&*content, &old_content) {
1083 (LinkContent::Link { target, link_type }, Some((old_is_absolute, old_target))) => {
1084 Path::new(&**target) == old_target
1085 && link_type.contains(LinkType::ABSOLUTE) == *old_is_absolute
1086 }
1087 (LinkContent::NotFound, None) => true,
1088 _ => false,
1089 };
1090 if is_equal {
1091 if !old_invalidators.is_empty() {
1092 for (invalidator, write_content) in old_invalidators {
1093 inner.invalidator_map.insert(
1094 full_path.clone().into_owned(),
1095 invalidator,
1096 write_content,
1097 );
1098 }
1099 }
1100 return Ok(());
1101 }
1102
1103 match &*content {
1104 LinkContent::Link { target, link_type } => {
1105 let create_directory = old_content.is_none();
1106 if create_directory && let Some(parent) = full_path.parent() {
1107 inner.create_directory(parent).await.with_context(|| {
1108 format!(
1109 "failed to create directory {} for write link to {}",
1110 parent.display(),
1111 full_path.display()
1112 )
1113 })?;
1114 }
1115
1116 let link_type = *link_type;
1117 let target_path = if link_type.contains(LinkType::ABSOLUTE) {
1118 Path::new(&inner.root).join(unix_to_sys(target).as_ref())
1119 } else {
1120 PathBuf::from(unix_to_sys(target).as_ref())
1121 };
1122 let full_path = full_path.into_owned();
1123
1124 if old_content.is_some() {
1125 retry_blocking(full_path.clone(), |path| std::fs::remove_file(path))
1129 .concurrency_limited(&inner.write_semaphore)
1130 .await
1131 .or_else(|err| {
1132 if err.kind() == ErrorKind::NotFound {
1133 Ok(())
1134 } else {
1135 Err(err)
1136 }
1137 })
1138 .with_context(|| {
1139 anyhow!("removing existing symlink {} failed", full_path.display())
1140 })?;
1141 }
1142
1143 retry_blocking(target_path, move |target_path| {
1144 let _span = tracing::info_span!(
1145 "write symlink",
1146 name = display(target_path.display())
1147 )
1148 .entered();
1149 #[cfg(not(target_family = "windows"))]
1152 {
1153 std::os::unix::fs::symlink(target_path, &full_path)
1154 }
1155 #[cfg(target_family = "windows")]
1156 {
1157 if link_type.contains(LinkType::DIRECTORY) {
1158 std::os::windows::fs::symlink_dir(target_path, &full_path)
1159 } else {
1160 std::os::windows::fs::symlink_file(target_path, &full_path)
1161 }
1162 }
1163 })
1164 .await
1165 .with_context(|| format!("create symlink to {target}"))?;
1166 }
1167 LinkContent::Invalid => {
1168 anyhow::bail!("invalid symlink target: {}", full_path.display())
1169 }
1170 LinkContent::NotFound => {
1171 retry_blocking(full_path.clone().into_owned(), |path| {
1172 std::fs::remove_file(path)
1173 })
1174 .concurrency_limited(&inner.write_semaphore)
1175 .await
1176 .or_else(|err| {
1177 if err.kind() == ErrorKind::NotFound {
1178 Ok(())
1179 } else {
1180 Err(err)
1181 }
1182 })
1183 .with_context(|| anyhow!("removing {} failed", full_path.display()))?;
1184 }
1185 }
1186
1187 Ok(())
1188 });
1189 Ok(())
1190 }
1191
1192 #[turbo_tasks::function(fs)]
1193 async fn metadata(&self, fs_path: FileSystemPath) -> Result<Vc<FileMeta>> {
1194 mark_session_dependent();
1195 let full_path = self.to_sys_path(&fs_path);
1196
1197 if self.inner.is_path_denied(&fs_path) {
1199 bail!(
1200 "Cannot read metadata from denied path: {}",
1201 fs_path.value_to_string().await?
1202 );
1203 }
1204
1205 self.inner.register_read_invalidator(&full_path)?;
1206
1207 let _lock = self.inner.lock_path(&full_path).await;
1208 let meta = retry_blocking(full_path.clone(), |path| std::fs::metadata(path))
1209 .concurrency_limited(&self.inner.read_semaphore)
1210 .instrument(tracing::info_span!(
1211 "read metadata",
1212 name = display(full_path.display())
1213 ))
1214 .await
1215 .with_context(|| format!("reading metadata for {}", full_path.display()))?;
1216
1217 Ok(FileMeta::cell(meta.into()))
1218 }
1219}
1220
1221#[turbo_tasks::value_impl]
1222impl ValueToString for DiskFileSystem {
1223 #[turbo_tasks::function]
1224 fn to_string(&self) -> Vc<RcStr> {
1225 Vc::cell(self.inner.name.clone())
1226 }
1227}
1228
1229#[turbo_tasks::value(shared)]
1230#[derive(Debug, Clone, Hash, TaskInput)]
1231pub struct FileSystemPath {
1232 pub fs: ResolvedVc<Box<dyn FileSystem>>,
1233 pub path: RcStr,
1234}
1235
1236impl FileSystemPath {
1237 pub fn value_to_string(&self) -> Vc<RcStr> {
1239 value_to_string(self.clone())
1240 }
1241}
1242
1243#[turbo_tasks::function]
1244async fn value_to_string(path: FileSystemPath) -> Result<Vc<RcStr>> {
1245 Ok(Vc::cell(
1246 format!("[{}]/{}", path.fs.to_string().await?, path.path).into(),
1247 ))
1248}
1249
1250impl FileSystemPath {
1251 pub fn is_inside_ref(&self, other: &FileSystemPath) -> bool {
1252 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1253 if other.path.is_empty() {
1254 true
1255 } else {
1256 self.path.as_bytes().get(other.path.len()) == Some(&b'/')
1257 }
1258 } else {
1259 false
1260 }
1261 }
1262
1263 pub fn is_inside_or_equal_ref(&self, other: &FileSystemPath) -> bool {
1264 if self.fs == other.fs && self.path.starts_with(&*other.path) {
1265 if other.path.is_empty() {
1266 true
1267 } else {
1268 matches!(
1269 self.path.as_bytes().get(other.path.len()),
1270 Some(&b'/') | None
1271 )
1272 }
1273 } else {
1274 false
1275 }
1276 }
1277
1278 pub fn is_root(&self) -> bool {
1279 self.path.is_empty()
1280 }
1281
1282 pub fn is_in_node_modules(&self) -> bool {
1283 self.path.starts_with("node_modules/") || self.path.contains("/node_modules/")
1284 }
1285
1286 pub fn get_path_to<'a>(&self, inner: &'a FileSystemPath) -> Option<&'a str> {
1290 if self.fs != inner.fs {
1291 return None;
1292 }
1293 let path = inner.path.strip_prefix(&*self.path)?;
1294 if self.path.is_empty() {
1295 Some(path)
1296 } else if let Some(stripped) = path.strip_prefix('/') {
1297 Some(stripped)
1298 } else {
1299 None
1300 }
1301 }
1302
1303 pub fn get_relative_path_to(&self, other: &FileSystemPath) -> Option<RcStr> {
1304 if self.fs != other.fs {
1305 return None;
1306 }
1307
1308 Some(get_relative_path_to(&self.path, &other.path).into())
1309 }
1310
1311 pub fn file_name(&self) -> &str {
1314 let (_, file_name) = self.split_file_name();
1315 file_name
1316 }
1317
1318 pub fn has_extension(&self, extension: &str) -> bool {
1323 debug_assert!(!extension.contains('/') && extension.starts_with('.'));
1324 self.path.ends_with(extension)
1325 }
1326
1327 pub fn extension_ref(&self) -> Option<&str> {
1329 let (_, extension) = self.split_extension();
1330 extension
1331 }
1332
1333 fn split_extension(&self) -> (&str, Option<&str>) {
1337 if let Some((path_before_extension, extension)) = self.path.rsplit_once('.') {
1338 if extension.contains('/') ||
1339 path_before_extension.ends_with('/') || path_before_extension.is_empty()
1341 {
1342 (self.path.as_str(), None)
1343 } else {
1344 (path_before_extension, Some(extension))
1345 }
1346 } else {
1347 (self.path.as_str(), None)
1348 }
1349 }
1350
1351 fn split_file_name(&self) -> (Option<&str>, &str) {
1355 if let Some((parent, file_name)) = self.path.rsplit_once('/') {
1357 (Some(parent), file_name)
1358 } else {
1359 (None, self.path.as_str())
1360 }
1361 }
1362
1363 fn split_file_stem_extension(&self) -> (Option<&str>, &str, Option<&str>) {
1368 let (path_before_extension, extension) = self.split_extension();
1369
1370 if let Some((parent, file_stem)) = path_before_extension.rsplit_once('/') {
1371 (Some(parent), file_stem, extension)
1372 } else {
1373 (None, path_before_extension, extension)
1374 }
1375 }
1376}
1377
1378#[turbo_tasks::value(transparent)]
1379pub struct FileSystemPathOption(Option<FileSystemPath>);
1380
1381#[turbo_tasks::value_impl]
1382impl FileSystemPathOption {
1383 #[turbo_tasks::function]
1384 pub fn none() -> Vc<Self> {
1385 Vc::cell(None)
1386 }
1387}
1388
1389impl FileSystemPath {
1390 fn new_normalized(fs: ResolvedVc<Box<dyn FileSystem>>, path: RcStr) -> Self {
1394 debug_assert!(
1398 MAIN_SEPARATOR != '\\' || !path.contains('\\'),
1399 "path {path} must not contain a Windows directory '\\', it must be normalized to Unix \
1400 '/'",
1401 );
1402 debug_assert!(
1403 normalize_path(&path).as_deref() == Some(&*path),
1404 "path {path} must be normalized",
1405 );
1406 FileSystemPath { fs, path }
1407 }
1408
1409 pub fn join(&self, path: &str) -> Result<Self> {
1413 if let Some(path) = join_path(&self.path, path) {
1414 Ok(Self::new_normalized(self.fs, path.into()))
1415 } else {
1416 bail!(
1417 "FileSystemPath(\"{}\").join(\"{}\") leaves the filesystem root",
1418 self.path,
1419 path
1420 );
1421 }
1422 }
1423
1424 pub fn append(&self, path: &str) -> Result<Self> {
1426 if path.contains('/') {
1427 bail!(
1428 "FileSystemPath(\"{}\").append(\"{}\") must not append '/'",
1429 self.path,
1430 path
1431 )
1432 }
1433 Ok(Self::new_normalized(
1434 self.fs,
1435 format!("{}{}", self.path, path).into(),
1436 ))
1437 }
1438
1439 pub fn append_to_stem(&self, appending: &str) -> Result<Self> {
1442 if appending.contains('/') {
1443 bail!(
1444 "FileSystemPath(\"{}\").append_to_stem(\"{}\") must not append '/'",
1445 self.path,
1446 appending
1447 )
1448 }
1449 if let (path, Some(ext)) = self.split_extension() {
1450 return Ok(Self::new_normalized(
1451 self.fs,
1452 format!("{path}{appending}.{ext}").into(),
1453 ));
1454 }
1455 Ok(Self::new_normalized(
1456 self.fs,
1457 format!("{}{}", self.path, appending).into(),
1458 ))
1459 }
1460
1461 #[allow(clippy::needless_borrow)] pub fn try_join(&self, path: &str) -> Option<FileSystemPath> {
1465 #[cfg(target_os = "windows")]
1467 let path = path.replace('\\', "/");
1468
1469 join_path(&self.path, &path).map(|p| Self::new_normalized(self.fs, RcStr::from(p)))
1470 }
1471
1472 pub fn try_join_inside(&self, path: &str) -> Option<FileSystemPath> {
1476 if let Some(p) = join_path(&self.path, path)
1477 && p.starts_with(&*self.path)
1478 {
1479 return Some(Self::new_normalized(self.fs, RcStr::from(p)));
1480 }
1481 None
1482 }
1483
1484 pub fn read_glob(&self, glob: Vc<Glob>) -> Vc<ReadGlobResult> {
1487 read_glob(self.clone(), glob)
1488 }
1489
1490 pub fn track_glob(&self, glob: Vc<Glob>, include_dot_files: bool) -> Vc<Completion> {
1493 track_glob(self.clone(), glob, include_dot_files)
1494 }
1495
1496 pub fn root(&self) -> Vc<Self> {
1497 self.fs().root()
1498 }
1499}
1500
1501impl FileSystemPath {
1502 pub fn fs(&self) -> Vc<Box<dyn FileSystem>> {
1503 *self.fs
1504 }
1505
1506 pub fn extension(&self) -> &str {
1507 self.extension_ref().unwrap_or_default()
1508 }
1509
1510 pub fn is_inside(&self, other: &FileSystemPath) -> bool {
1511 self.is_inside_ref(other)
1512 }
1513
1514 pub fn is_inside_or_equal(&self, other: &FileSystemPath) -> bool {
1515 self.is_inside_or_equal_ref(other)
1516 }
1517
1518 pub fn with_extension(&self, extension: &str) -> FileSystemPath {
1521 let (path_without_extension, _) = self.split_extension();
1522 Self::new_normalized(
1523 self.fs,
1524 match extension.is_empty() {
1527 true => path_without_extension.into(),
1528 false => format!("{path_without_extension}.{extension}").into(),
1529 },
1530 )
1531 }
1532
1533 pub fn file_stem(&self) -> Option<&str> {
1542 let (_, file_stem, _) = self.split_file_stem_extension();
1543 if file_stem.is_empty() {
1544 return None;
1545 }
1546 Some(file_stem)
1547 }
1548}
1549
1550impl Display for FileSystemPath {
1551 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1552 write!(f, "{}", self.path)
1553 }
1554}
1555
1556#[turbo_tasks::function]
1557pub async fn rebase(
1558 fs_path: FileSystemPath,
1559 old_base: FileSystemPath,
1560 new_base: FileSystemPath,
1561) -> Result<Vc<FileSystemPath>> {
1562 let new_path;
1563 if old_base.path.is_empty() {
1564 if new_base.path.is_empty() {
1565 new_path = fs_path.path.clone();
1566 } else {
1567 new_path = [new_base.path.as_str(), "/", &fs_path.path].concat().into();
1568 }
1569 } else {
1570 let base_path = [&old_base.path, "/"].concat();
1571 if !fs_path.path.starts_with(&base_path) {
1572 bail!(
1573 "rebasing {fs_path} from {old_base} onto {new_base} doesn't work because it's not \
1574 part of the source path",
1575 );
1576 }
1577 if new_base.path.is_empty() {
1578 new_path = [&fs_path.path[base_path.len()..]].concat().into();
1579 } else {
1580 new_path = [new_base.path.as_str(), &fs_path.path[old_base.path.len()..]]
1581 .concat()
1582 .into();
1583 }
1584 }
1585 Ok(new_base.fs.root().await?.join(&new_path)?.cell())
1586}
1587
1588impl FileSystemPath {
1590 pub fn read(&self) -> Vc<FileContent> {
1591 self.fs().read(self.clone())
1592 }
1593
1594 pub fn read_link(&self) -> Vc<LinkContent> {
1595 self.fs().read_link(self.clone())
1596 }
1597
1598 pub fn read_json(&self) -> Vc<FileJsonContent> {
1599 self.fs().read(self.clone()).parse_json()
1600 }
1601
1602 pub fn read_json5(&self) -> Vc<FileJsonContent> {
1603 self.fs().read(self.clone()).parse_json5()
1604 }
1605
1606 pub fn raw_read_dir(&self) -> Vc<RawDirectoryContent> {
1611 self.fs().raw_read_dir(self.clone())
1612 }
1613
1614 pub fn write(&self, content: Vc<FileContent>) -> Vc<()> {
1615 self.fs().write(self.clone(), content)
1616 }
1617
1618 pub fn write_link(&self, target: Vc<LinkContent>) -> Vc<()> {
1619 self.fs().write_link(self.clone(), target)
1620 }
1621
1622 pub fn metadata(&self) -> Vc<FileMeta> {
1623 self.fs().metadata(self.clone())
1624 }
1625
1626 pub async fn realpath(&self) -> Result<FileSystemPath> {
1629 let result = &(*self.realpath_with_links().await?);
1630 match &result.path_result {
1631 Ok(path) => Ok(path.clone()),
1632 Err(error) => Err(anyhow::anyhow!(error.as_error_message(self, result))),
1633 }
1634 }
1635
1636 pub fn rebase(
1637 fs_path: FileSystemPath,
1638 old_base: FileSystemPath,
1639 new_base: FileSystemPath,
1640 ) -> Vc<FileSystemPath> {
1641 rebase(fs_path, old_base, new_base)
1642 }
1643}
1644
1645impl FileSystemPath {
1646 pub fn read_dir(&self) -> Vc<DirectoryContent> {
1651 read_dir(self.clone())
1652 }
1653
1654 pub fn parent(&self) -> FileSystemPath {
1655 let path = &self.path;
1656 if path.is_empty() {
1657 return self.clone();
1658 }
1659 FileSystemPath::new_normalized(self.fs, RcStr::from(get_parent_path(path)))
1660 }
1661
1662 pub fn get_type(&self) -> Vc<FileSystemEntryType> {
1671 get_type(self.clone())
1672 }
1673
1674 pub fn realpath_with_links(&self) -> Vc<RealPathResult> {
1675 realpath_with_links(self.clone())
1676 }
1677}
1678
1679#[turbo_tasks::value_impl]
1680impl ValueToString for FileSystemPath {
1681 #[turbo_tasks::function]
1682 async fn to_string(&self) -> Result<Vc<RcStr>> {
1683 Ok(Vc::cell(
1684 format!("[{}]/{}", self.fs.to_string().await?, self.path).into(),
1685 ))
1686 }
1687}
1688
1689#[derive(Clone, Debug)]
1690#[turbo_tasks::value(shared)]
1691pub struct RealPathResult {
1692 pub path_result: Result<FileSystemPath, RealPathResultError>,
1693 pub symlinks: Vec<FileSystemPath>,
1694}
1695
1696#[derive(Debug, Clone, Hash, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
1699pub enum RealPathResultError {
1700 TooManySymlinks,
1701 CycleDetected,
1702 Invalid,
1703 NotFound,
1704}
1705
1706impl RealPathResultError {
1707 pub fn as_error_message(&self, orig: &FileSystemPath, result: &RealPathResult) -> String {
1709 match self {
1710 RealPathResultError::TooManySymlinks => format!(
1711 "Symlink {orig} leads to too many other symlinks ({len} links)",
1712 len = result.symlinks.len()
1713 ),
1714 RealPathResultError::CycleDetected => {
1715 format!("Symlink {orig} is in a symlink loop: {:?}", result.symlinks)
1716 }
1717 RealPathResultError::Invalid => {
1718 format!("Symlink {orig} is invalid, it points out of the filesystem root")
1719 }
1720 RealPathResultError::NotFound => {
1721 format!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1722 }
1723 }
1724 }
1725}
1726
1727#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
1728#[turbo_tasks::value(shared)]
1729pub enum Permissions {
1730 Readable,
1731 #[default]
1732 Writable,
1733 Executable,
1734}
1735
1736#[cfg(target_family = "unix")]
1739impl From<Permissions> for std::fs::Permissions {
1740 fn from(perm: Permissions) -> Self {
1741 use std::os::unix::fs::PermissionsExt;
1742 match perm {
1743 Permissions::Readable => std::fs::Permissions::from_mode(0o444),
1744 Permissions::Writable => std::fs::Permissions::from_mode(0o664),
1745 Permissions::Executable => std::fs::Permissions::from_mode(0o755),
1746 }
1747 }
1748}
1749
1750#[cfg(target_family = "unix")]
1751impl From<std::fs::Permissions> for Permissions {
1752 fn from(perm: std::fs::Permissions) -> Self {
1753 use std::os::unix::fs::PermissionsExt;
1754 if perm.readonly() {
1755 Permissions::Readable
1756 } else {
1757 if perm.mode() & 0o111 != 0 {
1759 Permissions::Executable
1760 } else {
1761 Permissions::Writable
1762 }
1763 }
1764 }
1765}
1766
1767#[cfg(not(target_family = "unix"))]
1768impl From<std::fs::Permissions> for Permissions {
1769 fn from(_: std::fs::Permissions) -> Self {
1770 Permissions::default()
1771 }
1772}
1773
1774#[turbo_tasks::value(shared)]
1775#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
1776pub enum FileContent {
1777 Content(File),
1778 NotFound,
1779}
1780
1781impl From<File> for FileContent {
1782 fn from(file: File) -> Self {
1783 FileContent::Content(file)
1784 }
1785}
1786
1787#[derive(Clone, Debug, Eq, PartialEq)]
1788enum FileComparison {
1789 Create,
1790 Equal,
1791 NotEqual,
1792}
1793
1794impl FileContent {
1795 async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
1798 let old_file = extract_disk_access(
1799 retry_blocking(path.to_path_buf(), |path| std::fs::File::open(path)).await,
1800 path,
1801 )?;
1802 let Some(old_file) = old_file else {
1803 return Ok(match self {
1804 FileContent::NotFound => FileComparison::Equal,
1805 _ => FileComparison::Create,
1806 });
1807 };
1808 let FileContent::Content(new_file) = self else {
1810 return Ok(FileComparison::NotEqual);
1811 };
1812
1813 let old_meta = extract_disk_access(
1814 retry_blocking(path.to_path_buf(), {
1815 let file_for_metadata = old_file.try_clone()?;
1816 move |_| file_for_metadata.metadata()
1817 })
1818 .await,
1819 path,
1820 )?;
1821 let Some(old_meta) = old_meta else {
1822 return Ok(FileComparison::Create);
1826 };
1827 if new_file.meta != old_meta.into() {
1829 return Ok(FileComparison::NotEqual);
1830 }
1831
1832 let mut new_contents = new_file.read();
1835 let mut old_contents = BufReader::new(old_file);
1836 Ok(loop {
1837 let new_chunk = new_contents.fill_buf()?;
1838 let Ok(old_chunk) = old_contents.fill_buf() else {
1839 break FileComparison::NotEqual;
1840 };
1841
1842 let len = min(new_chunk.len(), old_chunk.len());
1843 if len == 0 {
1844 if new_chunk.len() == old_chunk.len() {
1845 break FileComparison::Equal;
1846 } else {
1847 break FileComparison::NotEqual;
1848 }
1849 }
1850
1851 if new_chunk[0..len] != old_chunk[0..len] {
1852 break FileComparison::NotEqual;
1853 }
1854
1855 new_contents.consume(len);
1856 old_contents.consume(len);
1857 })
1858 }
1859}
1860
1861bitflags! {
1862 #[derive(
1863 Default,
1864 TraceRawVcs,
1865 NonLocalValue,
1866 DeterministicHash,
1867 Encode,
1868 Decode,
1869 )]
1870 pub struct LinkType: u8 {
1871 const DIRECTORY = 0b00000001;
1872 const ABSOLUTE = 0b00000010;
1873 }
1874}
1875
1876#[turbo_tasks::value(shared)]
1877#[derive(Debug)]
1878pub enum LinkContent {
1879 Link { target: RcStr, link_type: LinkType },
1886 Invalid,
1888 NotFound,
1890}
1891
1892#[turbo_tasks::value(shared)]
1893#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
1894pub struct File {
1895 #[turbo_tasks(debug_ignore)]
1896 content: Rope,
1897 meta: FileMeta,
1898}
1899
1900impl File {
1901 fn from_path(p: &Path) -> io::Result<Self> {
1903 let mut file = std::fs::File::open(p)?;
1904 let metadata = file.metadata()?;
1905
1906 let mut output = Vec::with_capacity(metadata.len() as usize);
1907 file.read_to_end(&mut output)?;
1908
1909 Ok(File {
1910 meta: metadata.into(),
1911 content: Rope::from(output),
1912 })
1913 }
1914
1915 fn from_bytes(content: Vec<u8>) -> Self {
1917 File {
1918 meta: FileMeta::default(),
1919 content: Rope::from(content),
1920 }
1921 }
1922
1923 fn from_rope(content: Rope) -> Self {
1925 File {
1926 meta: FileMeta::default(),
1927 content,
1928 }
1929 }
1930
1931 pub fn content_type(&self) -> Option<&Mime> {
1933 self.meta.content_type.as_ref()
1934 }
1935
1936 pub fn with_content_type(mut self, content_type: Mime) -> Self {
1938 self.meta.content_type = Some(content_type);
1939 self
1940 }
1941
1942 pub fn read(&self) -> RopeReader<'_> {
1944 self.content.read()
1945 }
1946}
1947
1948impl Debug for File {
1949 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1950 f.debug_struct("File")
1951 .field("meta", &self.meta)
1952 .field("content (hash)", &hash_xxh3_hash64(&self.content))
1953 .finish()
1954 }
1955}
1956
1957impl From<RcStr> for File {
1958 fn from(s: RcStr) -> Self {
1959 s.into_owned().into()
1960 }
1961}
1962
1963impl From<String> for File {
1964 fn from(s: String) -> Self {
1965 File::from_bytes(s.into_bytes())
1966 }
1967}
1968
1969impl From<ReadRef<RcStr>> for File {
1970 fn from(s: ReadRef<RcStr>) -> Self {
1971 File::from_bytes(s.as_bytes().to_vec())
1972 }
1973}
1974
1975impl From<&str> for File {
1976 fn from(s: &str) -> Self {
1977 File::from_bytes(s.as_bytes().to_vec())
1978 }
1979}
1980
1981impl From<Vec<u8>> for File {
1982 fn from(bytes: Vec<u8>) -> Self {
1983 File::from_bytes(bytes)
1984 }
1985}
1986
1987impl From<&[u8]> for File {
1988 fn from(bytes: &[u8]) -> Self {
1989 File::from_bytes(bytes.to_vec())
1990 }
1991}
1992
1993impl From<ReadRef<Rope>> for File {
1994 fn from(rope: ReadRef<Rope>) -> Self {
1995 File::from_rope(ReadRef::into_owned(rope))
1996 }
1997}
1998
1999impl From<Rope> for File {
2000 fn from(rope: Rope) -> Self {
2001 File::from_rope(rope)
2002 }
2003}
2004
2005impl File {
2006 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
2007 Self {
2008 meta,
2009 content: Rope::from(content),
2010 }
2011 }
2012
2013 pub fn meta(&self) -> &FileMeta {
2015 &self.meta
2016 }
2017
2018 pub fn content(&self) -> &Rope {
2020 &self.content
2021 }
2022}
2023
2024#[turbo_tasks::value(shared)]
2025#[derive(Debug, Clone, Default)]
2026pub struct FileMeta {
2027 permissions: Permissions,
2030 #[bincode(with = "turbo_bincode::mime_option")]
2031 #[turbo_tasks(trace_ignore)]
2032 content_type: Option<Mime>,
2033}
2034
2035impl Ord for FileMeta {
2036 fn cmp(&self, other: &Self) -> Ordering {
2037 self.permissions
2038 .cmp(&other.permissions)
2039 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
2040 }
2041}
2042
2043impl PartialOrd for FileMeta {
2044 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2045 Some(self.cmp(other))
2046 }
2047}
2048
2049impl From<std::fs::Metadata> for FileMeta {
2050 fn from(meta: std::fs::Metadata) -> Self {
2051 let permissions = meta.permissions().into();
2052
2053 Self {
2054 permissions,
2055 content_type: None,
2056 }
2057 }
2058}
2059
2060impl DeterministicHash for FileMeta {
2061 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
2062 self.permissions.deterministic_hash(state);
2063 if let Some(content_type) = &self.content_type {
2064 content_type.to_string().deterministic_hash(state);
2065 }
2066 }
2067}
2068
2069impl FileContent {
2070 pub fn new(file: File) -> Self {
2071 FileContent::Content(file)
2072 }
2073
2074 pub fn is_content(&self) -> bool {
2075 matches!(self, FileContent::Content(_))
2076 }
2077
2078 pub fn as_content(&self) -> Option<&File> {
2079 match self {
2080 FileContent::Content(file) => Some(file),
2081 FileContent::NotFound => None,
2082 }
2083 }
2084
2085 pub fn parse_json_ref(&self) -> FileJsonContent {
2086 match self {
2087 FileContent::Content(file) => {
2088 let content = file.content.clone().into_bytes();
2089 let de = &mut serde_json::Deserializer::from_slice(&content);
2090 match serde_path_to_error::deserialize(de) {
2091 Ok(data) => FileJsonContent::Content(data),
2092 Err(e) => FileJsonContent::Unparsable(Box::new(
2093 UnparsableJson::from_serde_path_to_error(e),
2094 )),
2095 }
2096 }
2097 FileContent::NotFound => FileJsonContent::NotFound,
2098 }
2099 }
2100
2101 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
2102 match self {
2103 FileContent::Content(file) => match file.content.to_str() {
2104 Ok(string) => match parse_to_serde_value(
2105 &string,
2106 &ParseOptions {
2107 allow_comments: true,
2108 allow_trailing_commas: true,
2109 allow_loose_object_property_names: false,
2110 },
2111 ) {
2112 Ok(data) => match data {
2113 Some(value) => FileJsonContent::Content(value),
2114 None => FileJsonContent::unparsable(rcstr!(
2115 "text content doesn't contain any json data"
2116 )),
2117 },
2118 Err(e) => FileJsonContent::Unparsable(Box::new(
2119 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2120 )),
2121 },
2122 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2123 },
2124 FileContent::NotFound => FileJsonContent::NotFound,
2125 }
2126 }
2127
2128 pub fn parse_json5_ref(&self) -> FileJsonContent {
2129 match self {
2130 FileContent::Content(file) => match file.content.to_str() {
2131 Ok(string) => match parse_to_serde_value(
2132 &string,
2133 &ParseOptions {
2134 allow_comments: true,
2135 allow_trailing_commas: true,
2136 allow_loose_object_property_names: true,
2137 },
2138 ) {
2139 Ok(data) => match data {
2140 Some(value) => FileJsonContent::Content(value),
2141 None => FileJsonContent::unparsable(rcstr!(
2142 "text content doesn't contain any json data"
2143 )),
2144 },
2145 Err(e) => FileJsonContent::Unparsable(Box::new(
2146 UnparsableJson::from_jsonc_error(e, string.as_ref()),
2147 )),
2148 },
2149 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
2150 },
2151 FileContent::NotFound => FileJsonContent::NotFound,
2152 }
2153 }
2154
2155 pub fn lines_ref(&self) -> FileLinesContent {
2156 match self {
2157 FileContent::Content(file) => match file.content.to_str() {
2158 Ok(string) => {
2159 let mut bytes_offset = 0;
2160 FileLinesContent::Lines(
2161 string
2162 .split('\n')
2163 .map(|l| {
2164 let line = FileLine {
2165 content: l.to_string(),
2166 bytes_offset,
2167 };
2168 bytes_offset += (l.len() + 1) as u32;
2169 line
2170 })
2171 .collect(),
2172 )
2173 }
2174 Err(_) => FileLinesContent::Unparsable,
2175 },
2176 FileContent::NotFound => FileLinesContent::NotFound,
2177 }
2178 }
2179}
2180
2181#[turbo_tasks::value_impl]
2182impl FileContent {
2183 #[turbo_tasks::function]
2184 pub fn len(&self) -> Result<Vc<Option<u64>>> {
2185 Ok(Vc::cell(match self {
2186 FileContent::Content(file) => Some(file.content.len() as u64),
2187 FileContent::NotFound => None,
2188 }))
2189 }
2190
2191 #[turbo_tasks::function]
2192 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
2193 Ok(self.parse_json_ref().cell())
2194 }
2195
2196 #[turbo_tasks::function]
2197 pub async fn parse_json_with_comments(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
2198 let this = self.await?;
2199 Ok(this.parse_json_with_comments_ref().cell())
2200 }
2201
2202 #[turbo_tasks::function]
2203 pub async fn parse_json5(self: Vc<Self>) -> Result<Vc<FileJsonContent>> {
2204 let this = self.await?;
2205 Ok(this.parse_json5_ref().cell())
2206 }
2207
2208 #[turbo_tasks::function]
2209 pub async fn lines(self: Vc<Self>) -> Result<Vc<FileLinesContent>> {
2210 let this = self.await?;
2211 Ok(this.lines_ref().cell())
2212 }
2213
2214 #[turbo_tasks::function]
2215 pub async fn hash(self: Vc<Self>) -> Result<Vc<u64>> {
2216 Ok(Vc::cell(hash_xxh3_hash64(&self.await?)))
2217 }
2218}
2219
2220#[turbo_tasks::value(shared, serialization = "none")]
2222pub enum FileJsonContent {
2223 Content(Value),
2224 Unparsable(Box<UnparsableJson>),
2225 NotFound,
2226}
2227
2228#[turbo_tasks::value_impl]
2229impl ValueToString for FileJsonContent {
2230 #[turbo_tasks::function]
2235 fn to_string(&self) -> Result<Vc<RcStr>> {
2236 match self {
2237 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
2238 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2239 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2240 }
2241 }
2242}
2243
2244#[turbo_tasks::value_impl]
2245impl FileJsonContent {
2246 #[turbo_tasks::function]
2247 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
2248 match &*self.await? {
2249 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
2250 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
2251 FileJsonContent::NotFound => Err(anyhow!("File not found")),
2252 }
2253 }
2254}
2255impl FileJsonContent {
2256 pub fn unparsable(message: RcStr) -> Self {
2257 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2258 message,
2259 path: None,
2260 start_location: None,
2261 end_location: None,
2262 }))
2263 }
2264
2265 pub fn unparsable_with_message(message: RcStr) -> Self {
2266 FileJsonContent::Unparsable(Box::new(UnparsableJson {
2267 message,
2268 path: None,
2269 start_location: None,
2270 end_location: None,
2271 }))
2272 }
2273}
2274
2275#[derive(Debug, PartialEq, Eq)]
2276pub struct FileLine {
2277 pub content: String,
2278 pub bytes_offset: u32,
2279}
2280
2281impl FileLine {
2282 pub fn len(&self) -> usize {
2283 self.content.len()
2284 }
2285
2286 #[must_use]
2287 pub fn is_empty(&self) -> bool {
2288 self.len() == 0
2289 }
2290}
2291
2292#[turbo_tasks::value(shared, serialization = "none")]
2293pub enum FileLinesContent {
2294 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
2295 Unparsable,
2296 NotFound,
2297}
2298
2299#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2300pub enum RawDirectoryEntry {
2301 File,
2302 Directory,
2303 Symlink,
2304 Other,
2306}
2307
2308#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
2309pub enum DirectoryEntry {
2310 File(FileSystemPath),
2311 Directory(FileSystemPath),
2312 Symlink(FileSystemPath),
2313 Other(FileSystemPath),
2314 Error(RcStr),
2315}
2316
2317impl DirectoryEntry {
2318 pub async fn resolve_symlink(self) -> Result<Self> {
2322 if let DirectoryEntry::Symlink(symlink) = &self {
2323 let result = &*symlink.realpath_with_links().await?;
2324 let real_path = match &result.path_result {
2325 Ok(path) => path,
2326 Err(error) => {
2327 return Ok(DirectoryEntry::Error(
2328 error.as_error_message(symlink, result).into(),
2329 ));
2330 }
2331 };
2332 Ok(match *real_path.get_type().await? {
2333 FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2334 FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2335 FileSystemEntryType::NotFound => DirectoryEntry::Error(
2337 format!("Symlink {symlink} points at {real_path} which does not exist").into(),
2338 ),
2339 FileSystemEntryType::Symlink => bail!(
2341 "Symlink {symlink} points at a symlink but realpath_with_links returned a path"
2342 ),
2343 _ => self,
2344 })
2345 } else {
2346 Ok(self)
2347 }
2348 }
2349
2350 pub fn path(self) -> Option<FileSystemPath> {
2351 match self {
2352 DirectoryEntry::File(path)
2353 | DirectoryEntry::Directory(path)
2354 | DirectoryEntry::Symlink(path)
2355 | DirectoryEntry::Other(path) => Some(path),
2356 DirectoryEntry::Error(_) => None,
2357 }
2358 }
2359}
2360
2361#[turbo_tasks::value]
2362#[derive(Hash, Clone, Copy, Debug)]
2363pub enum FileSystemEntryType {
2364 NotFound,
2365 File,
2366 Directory,
2367 Symlink,
2368 Other,
2370 Error,
2371}
2372
2373impl From<FileType> for FileSystemEntryType {
2374 fn from(file_type: FileType) -> Self {
2375 match file_type {
2376 t if t.is_dir() => FileSystemEntryType::Directory,
2377 t if t.is_file() => FileSystemEntryType::File,
2378 t if t.is_symlink() => FileSystemEntryType::Symlink,
2379 _ => FileSystemEntryType::Other,
2380 }
2381 }
2382}
2383
2384impl From<DirectoryEntry> for FileSystemEntryType {
2385 fn from(entry: DirectoryEntry) -> Self {
2386 FileSystemEntryType::from(&entry)
2387 }
2388}
2389
2390impl From<&DirectoryEntry> for FileSystemEntryType {
2391 fn from(entry: &DirectoryEntry) -> Self {
2392 match entry {
2393 DirectoryEntry::File(_) => FileSystemEntryType::File,
2394 DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
2395 DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
2396 DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2397 DirectoryEntry::Error(_) => FileSystemEntryType::Error,
2398 }
2399 }
2400}
2401
2402impl From<RawDirectoryEntry> for FileSystemEntryType {
2403 fn from(entry: RawDirectoryEntry) -> Self {
2404 FileSystemEntryType::from(&entry)
2405 }
2406}
2407
2408impl From<&RawDirectoryEntry> for FileSystemEntryType {
2409 fn from(entry: &RawDirectoryEntry) -> Self {
2410 match entry {
2411 RawDirectoryEntry::File => FileSystemEntryType::File,
2412 RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
2413 RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
2414 RawDirectoryEntry::Other => FileSystemEntryType::Other,
2415 }
2416 }
2417}
2418
2419#[turbo_tasks::value]
2420#[derive(Debug)]
2421pub enum RawDirectoryContent {
2422 Entries(AutoMap<RcStr, RawDirectoryEntry>),
2425 NotFound,
2426}
2427
2428impl RawDirectoryContent {
2429 pub fn new(entries: AutoMap<RcStr, RawDirectoryEntry>) -> Vc<Self> {
2430 Self::cell(RawDirectoryContent::Entries(entries))
2431 }
2432
2433 pub fn not_found() -> Vc<Self> {
2434 Self::cell(RawDirectoryContent::NotFound)
2435 }
2436}
2437
2438#[turbo_tasks::value]
2439#[derive(Debug)]
2440pub enum DirectoryContent {
2441 Entries(AutoMap<RcStr, DirectoryEntry>),
2442 NotFound,
2443}
2444
2445impl DirectoryContent {
2446 pub fn new(entries: AutoMap<RcStr, DirectoryEntry>) -> Vc<Self> {
2447 Self::cell(DirectoryContent::Entries(entries))
2448 }
2449
2450 pub fn not_found() -> Vc<Self> {
2451 Self::cell(DirectoryContent::NotFound)
2452 }
2453}
2454
2455#[turbo_tasks::value(shared)]
2456pub struct NullFileSystem;
2457
2458#[turbo_tasks::value_impl]
2459impl FileSystem for NullFileSystem {
2460 #[turbo_tasks::function]
2461 fn read(&self, _fs_path: FileSystemPath) -> Vc<FileContent> {
2462 FileContent::NotFound.cell()
2463 }
2464
2465 #[turbo_tasks::function]
2466 fn read_link(&self, _fs_path: FileSystemPath) -> Vc<LinkContent> {
2467 LinkContent::NotFound.cell()
2468 }
2469
2470 #[turbo_tasks::function]
2471 fn raw_read_dir(&self, _fs_path: FileSystemPath) -> Vc<RawDirectoryContent> {
2472 RawDirectoryContent::not_found()
2473 }
2474
2475 #[turbo_tasks::function]
2476 fn write(&self, _fs_path: FileSystemPath, _content: Vc<FileContent>) -> Vc<()> {
2477 Vc::default()
2478 }
2479
2480 #[turbo_tasks::function]
2481 fn write_link(&self, _fs_path: FileSystemPath, _target: Vc<LinkContent>) -> Vc<()> {
2482 Vc::default()
2483 }
2484
2485 #[turbo_tasks::function]
2486 fn metadata(&self, _fs_path: FileSystemPath) -> Vc<FileMeta> {
2487 FileMeta::default().cell()
2488 }
2489}
2490
2491#[turbo_tasks::value_impl]
2492impl ValueToString for NullFileSystem {
2493 #[turbo_tasks::function]
2494 fn to_string(&self) -> Vc<RcStr> {
2495 Vc::cell(rcstr!("null"))
2496 }
2497}
2498
2499pub async fn to_sys_path(mut path: FileSystemPath) -> Result<Option<PathBuf>> {
2500 loop {
2501 if let Some(fs) = ResolvedVc::try_downcast_type::<AttachedFileSystem>(path.fs) {
2502 path = fs.get_inner_fs_path(path).owned().await?;
2503 continue;
2504 }
2505
2506 if let Some(fs) = ResolvedVc::try_downcast_type::<DiskFileSystem>(path.fs) {
2507 let sys_path = fs.await?.to_sys_path(&path);
2508 return Ok(Some(sys_path));
2509 }
2510
2511 return Ok(None);
2512 }
2513}
2514
2515#[turbo_tasks::function]
2516async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
2517 let fs = path.fs().to_resolved().await?;
2518 match &*fs.raw_read_dir(path.clone()).await? {
2519 RawDirectoryContent::NotFound => Ok(DirectoryContent::not_found()),
2520 RawDirectoryContent::Entries(entries) => {
2521 let mut normalized_entries = AutoMap::new();
2522 let dir_path = &path.path;
2523 for (name, entry) in entries {
2524 let path = if dir_path.is_empty() {
2528 name.clone()
2529 } else {
2530 RcStr::from(format!("{dir_path}/{name}"))
2531 };
2532
2533 let entry_path = FileSystemPath::new_normalized(fs, path);
2534 let entry = match entry {
2535 RawDirectoryEntry::File => DirectoryEntry::File(entry_path),
2536 RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
2537 RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
2538 RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2539 };
2540 normalized_entries.insert(name.clone(), entry);
2541 }
2542 Ok(DirectoryContent::new(normalized_entries))
2543 }
2544 }
2545}
2546
2547#[turbo_tasks::function]
2548async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
2549 if path.is_root() {
2550 return Ok(FileSystemEntryType::Directory.cell());
2551 }
2552 let parent = path.parent();
2553 let dir_content = parent.raw_read_dir().await?;
2554 match &*dir_content {
2555 RawDirectoryContent::NotFound => Ok(FileSystemEntryType::NotFound.cell()),
2556 RawDirectoryContent::Entries(entries) => {
2557 let (_, file_name) = path.split_file_name();
2558 if let Some(entry) = entries.get(file_name) {
2559 Ok(FileSystemEntryType::from(entry).cell())
2560 } else {
2561 Ok(FileSystemEntryType::NotFound.cell())
2562 }
2563 }
2564 }
2565}
2566
2567#[turbo_tasks::function]
2568async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2569 let mut current_path = path;
2570 let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
2571 let mut visited: AutoSet<RcStr> = AutoSet::new();
2572 let mut error = RealPathResultError::TooManySymlinks;
2573 for _i in 0..40 {
2576 if current_path.is_root() {
2577 return Ok(RealPathResult {
2579 path_result: Ok(current_path),
2580 symlinks: symlinks.into_iter().collect(),
2581 }
2582 .cell());
2583 }
2584
2585 if !visited.insert(current_path.path.clone()) {
2586 error = RealPathResultError::CycleDetected;
2587 break; }
2589
2590 let parent = current_path.parent();
2592 let parent_result = parent.realpath_with_links().owned().await?;
2593 let basename = current_path
2594 .path
2595 .rsplit_once('/')
2596 .map_or(current_path.path.as_str(), |(_, name)| name);
2597 symlinks.extend(parent_result.symlinks);
2598 let parent_path = match parent_result.path_result {
2599 Ok(path) => {
2600 if path != parent {
2601 current_path = path.join(basename)?;
2602 }
2603 path
2604 }
2605 Err(parent_error) => {
2606 error = parent_error;
2607 break;
2608 }
2609 };
2610
2611 if !matches!(
2614 *current_path.get_type().await?,
2615 FileSystemEntryType::Symlink
2616 ) {
2617 return Ok(RealPathResult {
2618 path_result: Ok(current_path),
2619 symlinks: symlinks.into_iter().collect(), }
2621 .cell());
2622 }
2623
2624 match &*current_path.read_link().await? {
2625 LinkContent::Link { target, link_type } => {
2626 symlinks.insert(current_path.clone());
2627 current_path = if link_type.contains(LinkType::ABSOLUTE) {
2628 current_path.root().owned().await?
2629 } else {
2630 parent_path
2631 }
2632 .join(target)?;
2633 }
2634 LinkContent::NotFound => {
2635 error = RealPathResultError::NotFound;
2636 break;
2637 }
2638 LinkContent::Invalid => {
2639 error = RealPathResultError::Invalid;
2640 break;
2641 }
2642 }
2643 }
2644
2645 Ok(RealPathResult {
2653 path_result: Err(error),
2654 symlinks: symlinks.into_iter().collect(),
2655 }
2656 .cell())
2657}
2658
2659#[cfg(test)]
2660mod tests {
2661 use turbo_rcstr::rcstr;
2662 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2663
2664 use super::*;
2665
2666 #[test]
2667 fn test_get_relative_path_to() {
2668 assert_eq!(get_relative_path_to("a/b/c", "a/b/c").as_str(), ".");
2669 assert_eq!(get_relative_path_to("a/c/d", "a/b/c").as_str(), "../../b/c");
2670 assert_eq!(get_relative_path_to("", "a/b/c").as_str(), "./a/b/c");
2671 assert_eq!(get_relative_path_to("a/b/c", "").as_str(), "../../..");
2672 assert_eq!(
2673 get_relative_path_to("a/b/c", "c/b/a").as_str(),
2674 "../../../c/b/a"
2675 );
2676 assert_eq!(
2677 get_relative_path_to("file:///a/b/c", "file:///c/b/a").as_str(),
2678 "../../../c/b/a"
2679 );
2680 }
2681
2682 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2683 async fn with_extension() {
2684 turbo_tasks_testing::VcStorage::with(async {
2685 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2686 .to_resolved()
2687 .await?;
2688
2689 let path_txt = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2690
2691 let path_json = path_txt.with_extension("json");
2692 assert_eq!(&*path_json.path, "foo/bar.json");
2693
2694 let path_no_ext = path_txt.with_extension("");
2695 assert_eq!(&*path_no_ext.path, "foo/bar");
2696
2697 let path_new_ext = path_no_ext.with_extension("json");
2698 assert_eq!(&*path_new_ext.path, "foo/bar.json");
2699
2700 let path_no_slash_txt = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2701
2702 let path_no_slash_json = path_no_slash_txt.with_extension("json");
2703 assert_eq!(path_no_slash_json.path.as_str(), "bar.json");
2704
2705 let path_no_slash_no_ext = path_no_slash_txt.with_extension("");
2706 assert_eq!(path_no_slash_no_ext.path.as_str(), "bar");
2707
2708 let path_no_slash_new_ext = path_no_slash_no_ext.with_extension("json");
2709 assert_eq!(path_no_slash_new_ext.path.as_str(), "bar.json");
2710
2711 anyhow::Ok(())
2712 })
2713 .await
2714 .unwrap()
2715 }
2716
2717 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2718 async fn file_stem() {
2719 turbo_tasks_testing::VcStorage::with(async {
2720 let fs = Vc::upcast::<Box<dyn FileSystem>>(VirtualFileSystem::new())
2721 .to_resolved()
2722 .await?;
2723
2724 let path = FileSystemPath::new_normalized(fs, rcstr!(""));
2725 assert_eq!(path.file_stem(), None);
2726
2727 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar.txt"));
2728 assert_eq!(path.file_stem(), Some("bar"));
2729
2730 let path = FileSystemPath::new_normalized(fs, rcstr!("bar.txt"));
2731 assert_eq!(path.file_stem(), Some("bar"));
2732
2733 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/bar"));
2734 assert_eq!(path.file_stem(), Some("bar"));
2735
2736 let path = FileSystemPath::new_normalized(fs, rcstr!("foo/.bar"));
2737 assert_eq!(path.file_stem(), Some(".bar"));
2738
2739 anyhow::Ok(())
2740 })
2741 .await
2742 .unwrap()
2743 }
2744
2745 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2746 async fn test_try_from_sys_path() {
2747 let sys_root = if cfg!(windows) {
2748 Path::new(r"C:\fake\root")
2749 } else {
2750 Path::new(r"/fake/root")
2751 };
2752
2753 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2754 BackendOptions::default(),
2755 noop_backing_storage(),
2756 ));
2757 tt.run_once(async {
2758 let fs_vc =
2759 DiskFileSystem::new(rcstr!("temp"), RcStr::from(sys_root.to_str().unwrap()))
2760 .to_resolved()
2761 .await?;
2762 let fs = fs_vc.await?;
2763 let fs_root_path = fs_vc.root().await?;
2764
2765 assert_eq!(
2766 fs.try_from_sys_path(
2767 fs_vc,
2768 &Path::new("relative").join("directory"),
2769 None,
2770 )
2771 .unwrap()
2772 .path,
2773 "relative/directory"
2774 );
2775
2776 assert_eq!(
2777 fs.try_from_sys_path(
2778 fs_vc,
2779 &sys_root
2780 .join("absolute")
2781 .join("directory")
2782 .join("..")
2783 .join("normalized_path"),
2784 Some(&fs_root_path.join("ignored").unwrap()),
2785 )
2786 .unwrap()
2787 .path,
2788 "absolute/normalized_path"
2789 );
2790
2791 assert_eq!(
2792 fs.try_from_sys_path(
2793 fs_vc,
2794 Path::new("child"),
2795 Some(&fs_root_path.join("parent").unwrap()),
2796 )
2797 .unwrap()
2798 .path,
2799 "parent/child"
2800 );
2801
2802 assert_eq!(
2803 fs.try_from_sys_path(
2804 fs_vc,
2805 &Path::new("..").join("parallel_dir"),
2806 Some(&fs_root_path.join("parent").unwrap()),
2807 )
2808 .unwrap()
2809 .path,
2810 "parallel_dir"
2811 );
2812
2813 assert_eq!(
2814 fs.try_from_sys_path(
2815 fs_vc,
2816 &Path::new("relative")
2817 .join("..")
2818 .join("..")
2819 .join("leaves_root"),
2820 None,
2821 ),
2822 None
2823 );
2824
2825 assert_eq!(
2826 fs.try_from_sys_path(
2827 fs_vc,
2828 &sys_root
2829 .join("absolute")
2830 .join("..")
2831 .join("..")
2832 .join("leaves_root"),
2833 None,
2834 ),
2835 None
2836 );
2837
2838 anyhow::Ok(())
2839 })
2840 .await
2841 .unwrap();
2842 }
2843
2844 #[cfg(test)]
2845 mod symlink_tests {
2846 use std::{
2847 fs::{File, create_dir_all, read_to_string},
2848 io::Write,
2849 };
2850
2851 use turbo_rcstr::{RcStr, rcstr};
2852 use turbo_tasks::{ResolvedVc, apply_effects};
2853 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2854
2855 use crate::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
2856
2857 #[turbo_tasks::function(operation)]
2858 async fn test_write_link_effect(
2859 fs: ResolvedVc<DiskFileSystem>,
2860 path: FileSystemPath,
2861 target: RcStr,
2862 ) -> anyhow::Result<()> {
2863 let write_file = |f| {
2864 fs.write_link(
2865 f,
2866 LinkContent::Link {
2867 target: format!("{target}/data.txt").into(),
2868 link_type: LinkType::empty(),
2869 }
2870 .cell(),
2871 )
2872 };
2873 write_file(path.join("symlink-file")?).await?;
2875 write_file(path.join("symlink-file")?).await?;
2876
2877 let write_dir = |f| {
2878 fs.write_link(
2879 f,
2880 LinkContent::Link {
2881 target: target.clone(),
2882 link_type: LinkType::DIRECTORY,
2883 }
2884 .cell(),
2885 )
2886 };
2887 write_dir(path.join("symlink-dir")?).await?;
2889 write_dir(path.join("symlink-dir")?).await?;
2890
2891 Ok(())
2892 }
2893
2894 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2895 async fn test_write_link() {
2896 let scratch = tempfile::tempdir().unwrap();
2897 let path = scratch.path().to_owned();
2898
2899 create_dir_all(path.join("subdir-a")).unwrap();
2900 File::create_new(path.join("subdir-a/data.txt"))
2901 .unwrap()
2902 .write_all(b"foo")
2903 .unwrap();
2904 create_dir_all(path.join("subdir-b")).unwrap();
2905 File::create_new(path.join("subdir-b/data.txt"))
2906 .unwrap()
2907 .write_all(b"bar")
2908 .unwrap();
2909 let root = path.to_str().unwrap().into();
2910
2911 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2912 BackendOptions::default(),
2913 noop_backing_storage(),
2914 ));
2915
2916 tt.run_once(async move {
2917 let fs = DiskFileSystem::new(rcstr!("test"), root)
2918 .to_resolved()
2919 .await?;
2920 let root_path = fs.root().owned().await?;
2921
2922 let write_result =
2923 test_write_link_effect(fs, root_path.clone(), rcstr!("subdir-a"));
2924 write_result.read_strongly_consistent().await?;
2925 apply_effects(write_result).await?;
2926
2927 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "foo");
2928 assert_eq!(
2929 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
2930 "foo"
2931 );
2932
2933 let write_result = test_write_link_effect(fs, root_path, rcstr!("subdir-b"));
2935 write_result.read_strongly_consistent().await?;
2936 apply_effects(write_result).await?;
2937
2938 assert_eq!(read_to_string(path.join("symlink-file")).unwrap(), "bar");
2939 assert_eq!(
2940 read_to_string(path.join("symlink-dir/data.txt")).unwrap(),
2941 "bar"
2942 );
2943
2944 anyhow::Ok(())
2945 })
2946 .await
2947 .unwrap();
2948 }
2949 }
2950
2951 #[cfg(test)]
2953 mod denied_path_tests {
2954 use std::{
2955 fs::{File, create_dir_all},
2956 io::Write,
2957 };
2958
2959 use turbo_rcstr::{RcStr, rcstr};
2960 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2961
2962 use crate::{
2963 DirectoryContent, DiskFileSystem, File as TurboFile, FileContent, FileSystem,
2964 FileSystemPath,
2965 glob::{Glob, GlobOptions},
2966 };
2967
2968 fn setup_test_fs() -> (tempfile::TempDir, RcStr, RcStr) {
2971 let scratch = tempfile::tempdir().unwrap();
2972 let path = scratch.path();
2973
2974 File::create_new(path.join("allowed_file.txt"))
2981 .unwrap()
2982 .write_all(b"allowed content")
2983 .unwrap();
2984
2985 create_dir_all(path.join("allowed_dir")).unwrap();
2986 File::create_new(path.join("allowed_dir/file.txt"))
2987 .unwrap()
2988 .write_all(b"allowed dir content")
2989 .unwrap();
2990
2991 File::create_new(path.join("other_file.txt"))
2992 .unwrap()
2993 .write_all(b"other content")
2994 .unwrap();
2995
2996 create_dir_all(path.join("denied_dir/nested")).unwrap();
2997 File::create_new(path.join("denied_dir/secret.txt"))
2998 .unwrap()
2999 .write_all(b"secret content")
3000 .unwrap();
3001 File::create_new(path.join("denied_dir/nested/deep.txt"))
3002 .unwrap()
3003 .write_all(b"deep secret")
3004 .unwrap();
3005
3006 let root: RcStr = path.to_str().unwrap().into();
3007 let denied_path: RcStr = rcstr!("denied_dir");
3009
3010 (scratch, root, denied_path)
3011 }
3012
3013 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3014 async fn test_denied_path_read() {
3015 let (_scratch, root, denied_path) = setup_test_fs();
3016 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3017 BackendOptions::default(),
3018 noop_backing_storage(),
3019 ));
3020
3021 tt.run_once(async {
3022 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3023 let root_path = fs.root().await?;
3024
3025 let allowed_file = root_path.join("allowed_file.txt")?;
3027 let content = allowed_file.read().await?;
3028 assert!(
3029 matches!(&*content, FileContent::Content(_)),
3030 "allowed file should be readable"
3031 );
3032
3033 let denied_file = root_path.join("denied_dir/secret.txt")?;
3035 let content = denied_file.read().await?;
3036 assert!(
3037 matches!(&*content, FileContent::NotFound),
3038 "denied file should return NotFound, got {:?}",
3039 content
3040 );
3041
3042 let nested_denied = root_path.join("denied_dir/nested/deep.txt")?;
3044 let content = nested_denied.read().await?;
3045 assert!(
3046 matches!(&*content, FileContent::NotFound),
3047 "nested denied file should return NotFound"
3048 );
3049
3050 let denied_dir = root_path.join("denied_dir")?;
3052 let content = denied_dir.read().await?;
3053 assert!(
3054 matches!(&*content, FileContent::NotFound),
3055 "denied directory should return NotFound"
3056 );
3057
3058 anyhow::Ok(())
3059 })
3060 .await
3061 .unwrap();
3062 }
3063
3064 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3065 async fn test_denied_path_read_dir() {
3066 let (_scratch, root, denied_path) = setup_test_fs();
3067 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3068 BackendOptions::default(),
3069 noop_backing_storage(),
3070 ));
3071
3072 tt.run_once(async {
3073 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3074 let root_path = fs.root().await?;
3075
3076 let dir_content = root_path.read_dir().await?;
3078 match &*dir_content {
3079 DirectoryContent::Entries(entries) => {
3080 assert!(
3081 entries.contains_key(&rcstr!("allowed_dir")),
3082 "allowed_dir should be visible"
3083 );
3084 assert!(
3085 entries.contains_key(&rcstr!("other_file.txt")),
3086 "other_file.txt should be visible"
3087 );
3088 assert!(
3089 entries.contains_key(&rcstr!("allowed_file.txt")),
3090 "allowed_file.txt should be visible"
3091 );
3092 assert!(
3093 !entries.contains_key(&rcstr!("denied_dir")),
3094 "denied_dir should NOT be visible in read_dir"
3095 );
3096 }
3097 DirectoryContent::NotFound => panic!("root directory should exist"),
3098 }
3099
3100 let denied_dir = root_path.join("denied_dir")?;
3102 let dir_content = denied_dir.read_dir().await?;
3103 assert!(
3104 matches!(&*dir_content, DirectoryContent::NotFound),
3105 "denied_dir read_dir should return NotFound"
3106 );
3107
3108 anyhow::Ok(())
3109 })
3110 .await
3111 .unwrap();
3112 }
3113
3114 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3115 async fn test_denied_path_read_glob() {
3116 let (_scratch, root, denied_path) = setup_test_fs();
3117 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3118 BackendOptions::default(),
3119 noop_backing_storage(),
3120 ));
3121
3122 tt.run_once(async {
3123 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3124 let root_path = fs.root().await?;
3125
3126 let glob_result = root_path
3128 .read_glob(Glob::new(rcstr!("**/*.txt"), GlobOptions::default()))
3129 .await?;
3130
3131 assert!(
3133 glob_result.results.contains_key("allowed_file.txt"),
3134 "allowed_file.txt should be found"
3135 );
3136 assert!(
3137 glob_result.results.contains_key("other_file.txt"),
3138 "other_file.txt should be found"
3139 );
3140 assert!(
3141 !glob_result.results.contains_key("denied_dir"),
3142 "denied_dir should NOT appear in glob results"
3143 );
3144
3145 assert!(
3147 !glob_result.inner.contains_key("denied_dir"),
3148 "denied_dir should NOT appear in glob inner results"
3149 );
3150
3151 assert!(
3153 glob_result.inner.contains_key("allowed_dir"),
3154 "allowed_dir directory should be present"
3155 );
3156 let sub_inner = glob_result.inner.get("allowed_dir").unwrap().await?;
3157 assert!(
3158 sub_inner.results.contains_key("file.txt"),
3159 "allowed_dir/file.txt should be found"
3160 );
3161
3162 anyhow::Ok(())
3163 })
3164 .await
3165 .unwrap();
3166 }
3167
3168 #[turbo_tasks::function(operation)]
3169 async fn write_file(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
3170 path.write(
3171 FileContent::Content(TurboFile::from_bytes(contents.to_string().into_bytes()))
3172 .cell(),
3173 )
3174 .await?;
3175 Ok(())
3176 }
3177
3178 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3179 async fn test_denied_path_write() {
3180 use turbo_tasks::apply_effects;
3181
3182 let (_scratch, root, denied_path) = setup_test_fs();
3183 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3184 BackendOptions::default(),
3185 noop_backing_storage(),
3186 ));
3187
3188 tt.run_once(async {
3189 let fs = DiskFileSystem::new_with_denied_path(rcstr!("test"), root, denied_path);
3190 let root_path = fs.root().await?;
3191
3192 let allowed_file = root_path.join("allowed_dir/new_file.txt")?;
3194 let write_result = write_file(allowed_file.clone(), rcstr!("test content"));
3195 write_result.read_strongly_consistent().await?;
3196 apply_effects(write_result).await?;
3197
3198 let read_content = allowed_file.read().await?;
3200 assert!(
3201 matches!(&*read_content, FileContent::Content(_)),
3202 "allowed file write should succeed"
3203 );
3204
3205 let denied_file = root_path.join("denied_dir/forbidden.txt")?;
3207 let write_result = write_file(denied_file, rcstr!("forbidden"));
3208 let result = write_result.read_strongly_consistent().await;
3209 assert!(
3210 result.is_err(),
3211 "writing to denied path should return an error"
3212 );
3213
3214 let nested_denied = root_path.join("denied_dir/nested/file.txt")?;
3216 let write_result = write_file(nested_denied, rcstr!("nested"));
3217 let result = write_result.read_strongly_consistent().await;
3218 assert!(
3219 result.is_err(),
3220 "writing to nested denied path should return an error"
3221 );
3222
3223 anyhow::Ok(())
3224 })
3225 .await
3226 .unwrap();
3227 }
3228 }
3229}