1use anyhow::{Result, bail};
2use auto_hash_map::AutoMap;
3use futures::try_join;
4use rustc_hash::FxHashMap;
5use turbo_rcstr::RcStr;
6use turbo_tasks::{Completion, ResolvedVc, TryJoinIterExt, Vc, turbobail};
7
8use crate::{
9 DirectoryContent, DirectoryEntry, FileSystem, FileSystemEntryType, FileSystemPath, LinkContent,
10 glob::Glob,
11};
12
13#[turbo_tasks::value]
14#[derive(Default, Debug)]
15pub struct ReadGlobResult {
16 pub results: FxHashMap<RcStr, DirectoryEntry>,
17 pub inner: FxHashMap<RcStr, ResolvedVc<ReadGlobResult>>,
18}
19
20async fn resolve_glob_root(directory: FileSystemPath) -> Result<FileSystemPath> {
21 Ok(directory
22 .realpath()
23 .await?
24 .unwrap_or_else(|_| directory.clone()))
25}
26
27#[turbo_tasks::function(fs)]
36pub async fn read_glob(directory: FileSystemPath, glob: Vc<Glob>) -> Result<Vc<ReadGlobResult>> {
37 let root = directory.clone();
38 let directory = resolve_glob_root(directory).await?;
39 read_glob_internal("", &root, directory, glob).await
40}
41
42#[turbo_tasks::function(fs)]
43async fn read_glob_inner(
44 prefix: RcStr,
45 root: FileSystemPath,
46 directory: FileSystemPath,
47 glob: Vc<Glob>,
48) -> Result<Vc<ReadGlobResult>> {
49 read_glob_internal(&prefix, &root, directory, glob).await
50}
51
52async fn read_glob_internal(
54 prefix: &str,
55 root: &FileSystemPath,
56 directory: FileSystemPath,
57 glob: Vc<Glob>,
58) -> Result<Vc<ReadGlobResult>> {
59 let dir = directory.read_dir().await?;
60 let mut result = ReadGlobResult::default();
61 let glob_value = glob.await?;
62 let handle_file = |result: &mut ReadGlobResult,
63 entry_path: &RcStr,
64 segment: &RcStr,
65 entry: &DirectoryEntry| {
66 if glob_value.matches(entry_path) {
67 result.results.insert(segment.clone(), entry.clone());
68 }
69 };
70 let handle_dir = async |result: &mut ReadGlobResult,
71 entry_path: RcStr,
72 segment: &RcStr,
73 path: &FileSystemPath| {
74 if glob_value.can_match_in_directory(&entry_path) {
75 result.inner.insert(
76 segment.clone(),
77 read_glob_inner(entry_path, root.clone(), path.clone(), glob)
78 .to_resolved()
79 .await?,
80 );
81 }
82 anyhow::Ok(())
83 };
84
85 match &*dir {
86 DirectoryContent::Entries(entries) => {
87 for (segment, entry) in entries.iter() {
88 let entry_path: RcStr = if prefix.is_empty() {
89 segment.clone()
90 } else {
91 format!("{prefix}/{segment}").into()
92 };
93
94 let output_path = root.join(&entry_path)?;
95 let output_entry = match entry {
96 DirectoryEntry::File(_) => DirectoryEntry::File(output_path),
97 DirectoryEntry::Directory(_) => DirectoryEntry::Directory(output_path),
98 DirectoryEntry::Symlink(_) => DirectoryEntry::Symlink(output_path),
99 DirectoryEntry::Other(_) => DirectoryEntry::Other(output_path),
100 DirectoryEntry::Error(error) => DirectoryEntry::Error(error.clone()),
101 };
102
103 match entry {
104 DirectoryEntry::File(_) => {
105 handle_file(&mut result, &entry_path, segment, &output_entry);
106 }
107 DirectoryEntry::Directory(path) => {
108 handle_file(&mut result, &entry_path, segment, &output_entry);
110 handle_dir(&mut result, entry_path, segment, path).await?;
112 }
113 DirectoryEntry::Symlink(path) => {
114 let link_content = path.read_link().await?;
116 if let LinkContent::Link { target } = &*link_content {
117 let Ok(realpath) = target.file_system_path().realpath().await? else {
118 handle_file(&mut result, &entry_path, segment, &output_entry);
120 continue;
121 };
122 if matches!(*realpath.get_type().await?, FileSystemEntryType::Directory)
123 {
124 check_symlink_directory_recursion(path, &realpath)?;
126
127 handle_file(&mut result, &entry_path, segment, &output_entry);
129 handle_dir(&mut result, entry_path, segment, &realpath).await?;
132 } else {
133 handle_file(&mut result, &entry_path, segment, &output_entry);
134 }
135 }
136 }
137 DirectoryEntry::Other(_) | DirectoryEntry::Error(_) => continue,
138 }
139 }
140 }
141 DirectoryContent::NotFound => {}
142 }
143 Ok(ReadGlobResult::cell(result))
144}
145
146async fn resolve_symlink_safely(entry: DirectoryEntry) -> Result<DirectoryEntry> {
148 let resolved_entry = entry.clone().resolve_symlink().await?;
149 if resolved_entry != entry && matches!(&resolved_entry, DirectoryEntry::Directory(_)) {
150 check_symlink_directory_recursion(
159 &entry.path().unwrap(),
160 &resolved_entry.clone().path().unwrap(),
161 )?;
162 }
163 Ok(resolved_entry)
164}
165
166fn check_symlink_directory_recursion(
167 source_path: &FileSystemPath,
168 realpath: &FileSystemPath,
169) -> Result<()> {
170 if source_path.is_inside_or_equal(realpath) {
179 bail!("'{source_path}' is a symlink causes that causes an infinite loop!",)
180 }
181 Ok(())
182}
183
184#[turbo_tasks::function(fs)]
191pub async fn track_glob(
192 directory: FileSystemPath,
193 glob: Vc<Glob>,
194 include_dot_files: bool,
195) -> Result<Vc<Completion>> {
196 let directory = resolve_glob_root(directory).await?;
197 track_glob_internal("", directory, glob, include_dot_files).await
198}
199
200#[turbo_tasks::function(fs)]
201async fn track_glob_inner(
202 prefix: RcStr,
203 directory: FileSystemPath,
204 glob: Vc<Glob>,
205 include_dot_files: bool,
206) -> Result<Vc<Completion>> {
207 track_glob_internal(&prefix, directory, glob, include_dot_files).await
208}
209
210async fn track_glob_internal(
211 prefix: &str,
212 directory: FileSystemPath,
213 glob: Vc<Glob>,
214 include_dot_files: bool,
215) -> Result<Vc<Completion>> {
216 let dir = read_dir_for_track_glob(directory.clone(), include_dot_files).await?;
217 let glob_value = glob.await?;
218 let fs = directory.fs().to_resolved().await?;
219 let mut reads = Vec::new();
220 let mut completions = Vec::new();
221 let mut types = Vec::new();
222 match &*dir {
223 DirectoryContent::Entries(entries) => {
224 for (segment, entry) in entries.iter() {
225 let entry_path = if prefix.is_empty() {
228 segment.clone()
229 } else {
230 format!("{prefix}/{segment}").into()
231 };
232
233 match resolve_symlink_safely(entry.clone()).await? {
234 DirectoryEntry::Directory(path) => {
235 if glob_value.can_match_in_directory(&entry_path) {
236 completions.push(track_glob_inner(
237 entry_path,
238 path.clone(),
239 glob,
240 include_dot_files,
241 ));
242 }
243 }
244 DirectoryEntry::File(path) => {
245 if glob_value.matches(&entry_path) {
246 reads.push(fs.read(path.clone()))
247 }
248 }
249 DirectoryEntry::Symlink(symlink_path) => turbobail!(
250 "resolve_symlink_safely() should have resolved all symlinks or returned \
251 an error, but found unresolved symlink at path: '{entry_path}'. Found \
252 path: '{symlink_path}'. Please report this as a bug.",
253 ),
254 DirectoryEntry::Other(path) => {
255 if glob_value.matches(&entry_path) {
256 types.push(path.get_type())
257 }
258 }
259 DirectoryEntry::Error(_) => {}
264 }
265 }
266 }
267 DirectoryContent::NotFound => {}
268 }
269 try_join!(
270 reads.iter().try_join(),
271 types.iter().try_join(),
272 completions.iter().try_join()
273 )?;
274 Ok(Completion::new())
275}
276
277#[turbo_tasks::function(fs)]
279async fn read_dir_for_track_glob(
280 directory: FileSystemPath,
281 include_dot_files: bool,
282) -> Result<Vc<DirectoryContent>> {
283 let read_dir = directory.read_dir();
284 if include_dot_files {
285 return Ok(read_dir);
286 }
287
288 Ok(match &*read_dir.await? {
289 DirectoryContent::Entries(entries) => DirectoryContent::new(
290 entries
291 .iter()
292 .filter(|(segment, _)| !segment.starts_with('.'))
293 .map(|(segment, entry)| (segment.clone(), entry.clone()))
294 .collect::<AutoMap<_, _>>(),
295 ),
296 DirectoryContent::NotFound => read_dir,
297 })
298}
299
300#[cfg(test)]
301pub mod tests {
302
303 use std::{
304 collections::HashMap,
305 fs::{File, create_dir},
306 io::prelude::*,
307 };
308
309 use turbo_rcstr::{RcStr, rcstr};
310 use turbo_tasks::{
311 Completion, Effects, OperationVc, ReadRef, Vc, read_strongly_consistent_and_apply_effects,
312 take_effects,
313 };
314 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
315
316 use crate::{
317 DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath, ReadGlobResult,
318 glob::{Glob, GlobOptions},
319 };
320
321 fn symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
322 target: Q,
323 path: P,
324 ) -> std::io::Result<()> {
325 assert!(target.as_ref().is_absolute());
326 let _ = std::fs::remove_dir(&path);
327 let _ = std::fs::remove_file(&path);
328
329 #[cfg(unix)]
330 {
331 std::os::unix::fs::symlink(target, path)
332 }
333 #[cfg(windows)]
334 {
335 let metadata = std::fs::metadata(&target).ok();
336 if metadata.is_none_or(|m| m.is_file()) {
337 std::os::windows::fs::symlink_file(target, path)
338 } else {
339 std::os::windows::fs::junction_point(target, path)
340 }
341 }
342 }
343
344 #[turbo_tasks::function(operation, root)]
345 async fn assert_read_glob_basic_operation(path: RcStr) -> anyhow::Result<()> {
346 let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
347 let root = fs.root().await?;
348 let read_dir = root
349 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
350 .await
351 .unwrap();
352 assert_eq!(read_dir.results.len(), 2);
353 assert_eq!(
354 read_dir.results.get("foo"),
355 Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
356 );
357 assert_eq!(
358 read_dir.results.get("sub"),
359 Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
360 );
361 assert_eq!(read_dir.inner.len(), 1);
362 let inner = &*read_dir.inner.get("sub").unwrap().await?;
363 assert_eq!(inner.results.len(), 1);
364 assert_eq!(
365 inner.results.get("bar"),
366 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
367 );
368 assert_eq!(inner.inner.len(), 0);
369
370 let read_dir = root
371 .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
372 .await
373 .unwrap();
374 assert_eq!(read_dir.results.len(), 0);
375 assert_eq!(read_dir.inner.len(), 1);
376 let inner = &*read_dir.inner.get("sub").unwrap().await?;
377 assert_eq!(inner.results.len(), 1);
378 assert_eq!(
379 inner.results.get("bar"),
380 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
381 );
382 assert_eq!(inner.inner.len(), 0);
383
384 Ok(())
385 }
386
387 #[turbo_tasks::function(operation, root)]
388 async fn assert_read_glob_symlinks_operation(path: RcStr) -> anyhow::Result<()> {
389 let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
390 let root = fs.root().await?;
391 let read_dir = root
393 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
394 .await
395 .unwrap();
396 assert_eq!(read_dir.results.len(), 0);
397 let inner = &*read_dir.inner.get("sub").unwrap().await?;
398 assert_eq!(
399 inner.results,
400 HashMap::from_iter([
401 (
402 "link-foo.js".into(),
403 DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
404 ),
405 (
406 "link-root.js".into(),
407 DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
408 ),
409 (
410 "foo.js".into(),
411 DirectoryEntry::File(root.join("sub/foo.js")?),
412 ),
413 ])
414 );
415 assert_eq!(inner.inner.len(), 0);
416
417 let read_dir = root
419 .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
420 .await
421 .unwrap();
422 assert_eq!(read_dir.results.len(), 0);
423 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
424 assert_eq!(inner_sub.results.len(), 0);
425 let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
426 assert_eq!(
427 inner_sub_dir.results,
428 HashMap::from_iter([
429 (
430 "index.js".into(),
431 DirectoryEntry::File(root.join("sub/dir/index.js")?),
432 ),
433 (
434 "dead.js".into(),
435 DirectoryEntry::Symlink(root.join("sub/dir/dead.js")?),
436 ),
437 ])
438 );
439 assert_eq!(inner_sub_dir.inner.len(), 0);
440
441 let read_dir = root
443 .read_glob(Glob::new(rcstr!("sub/dir-chain/*"), GlobOptions::default()))
444 .await
445 .unwrap();
446 assert_eq!(read_dir.results.len(), 0);
447 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
448 assert_eq!(inner_sub.results.len(), 0);
449 let inner_sub_dir = &*inner_sub.inner.get("dir-chain").unwrap().await?;
450 assert_eq!(
451 inner_sub_dir.results,
452 HashMap::from_iter([
453 (
454 "index.js".into(),
455 DirectoryEntry::File(root.join("sub/dir-chain/index.js")?),
456 ),
457 (
458 "dead.js".into(),
459 DirectoryEntry::Symlink(root.join("sub/dir-chain/dead.js")?),
460 ),
461 ])
462 );
463 assert_eq!(inner_sub_dir.inner.len(), 0);
464
465 Ok(())
466 }
467
468 #[turbo_tasks::function(operation, root)]
469 async fn assert_dead_symlink_read_glob_operation(path: RcStr) -> anyhow::Result<()> {
470 let fs =
471 Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
472 let root = fs.root().owned().await?;
473 let read_dir = root
474 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
475 .await?;
476 assert_eq!(read_dir.results.len(), 0);
477 assert_eq!(read_dir.inner.len(), 1);
478 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
479 assert_eq!(inner_sub.inner.len(), 0);
480 assert_eq!(
481 inner_sub.results,
482 HashMap::from_iter([
483 (
484 "foo.js".into(),
485 DirectoryEntry::File(root.join("sub/foo.js")?),
486 ),
487 (
488 "dead_link.js".into(),
489 DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
490 )
491 ])
492 );
493
494 Ok(())
495 }
496
497 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
498 async fn read_glob_basic() {
499 let scratch = tempfile::tempdir().unwrap();
500 {
501 let path = scratch.path();
503 File::create_new(path.join("foo"))
504 .unwrap()
505 .write_all(b"foo")
506 .unwrap();
507 create_dir(path.join("sub")).unwrap();
508 File::create_new(path.join("sub/bar"))
509 .unwrap()
510 .write_all(b"bar")
511 .unwrap();
512 }
513 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
514 BackendOptions::default(),
515 noop_backing_storage(),
516 ));
517 let path: RcStr = scratch.path().to_str().unwrap().into();
518 tt.run_once(async {
519 assert_read_glob_basic_operation(path)
520 .read_strongly_consistent()
521 .await?;
522
523 anyhow::Ok(())
524 })
525 .await
526 .unwrap();
527 }
528
529 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
530 async fn read_glob_symlinks() {
531 let scratch = tempfile::tempdir().unwrap();
532 {
533 let path = scratch.path();
538 create_dir(path.join("sub")).unwrap();
539 let foo = path.join("sub/foo.js");
540 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
541 symlink(&foo, path.join("sub/link-foo.js")).unwrap();
542
543 let root = path.join("root.js");
544 File::create_new(&root).unwrap().write_all(b"root").unwrap();
545 symlink(&root, path.join("sub/link-root.js")).unwrap();
546
547 let dir = path.join("dir");
548 create_dir(&dir).unwrap();
549 File::create_new(dir.join("index.js"))
550 .unwrap()
551 .write_all(b"dir index")
552 .unwrap();
553 symlink(dir.join("missing.js"), dir.join("dead.js")).unwrap();
554 symlink(&dir, path.join("sub/dir")).unwrap();
555 let dir_link = path.join("dir-link");
556 symlink(&dir, &dir_link).unwrap();
557 symlink(dir_link, path.join("sub/dir-chain")).unwrap();
558 }
559 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
560 BackendOptions::default(),
561 noop_backing_storage(),
562 ));
563 let path: RcStr = scratch.path().to_str().unwrap().into();
564 tt.run_once(async {
565 assert_read_glob_symlinks_operation(path)
566 .read_strongly_consistent()
567 .await?;
568
569 anyhow::Ok(())
570 })
571 .await
572 .unwrap();
573 }
574
575 #[turbo_tasks::function(operation, root)]
576 pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
577 path.write(FileContent::NotFound.cell()).await?;
578 Ok(())
579 }
580
581 #[turbo_tasks::function(operation, root)]
582 pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
583 path.write(
584 FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
585 )
586 .await?;
587 Ok(())
588 }
589
590 #[turbo_tasks::function(operation, root)]
591 pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
592 path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
593 }
594
595 #[turbo_tasks::function(operation, root)]
596 fn disk_file_system_root_operation(path: RcStr) -> Vc<FileSystemPath> {
597 let fs =
598 Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
599 fs.root()
600 }
601
602 #[turbo_tasks::function(operation, root)]
603 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
604 let _ = op.resolve().strongly_consistent().await?;
605 Ok(take_effects(op).await?.cell())
606 }
607
608 #[turbo_tasks::function(operation, root)]
609 async fn track_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
610 let root = disk_file_system_root_operation(path)
611 .read_strongly_consistent()
612 .await?;
613 root.track_glob(Glob::new(glob, GlobOptions::default()), false)
614 .await?;
615 Ok(())
616 }
617
618 #[turbo_tasks::function(operation, root)]
619 async fn read_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
620 let root = disk_file_system_root_operation(path)
621 .read_strongly_consistent()
622 .await?;
623 root.read_glob(Glob::new(glob, GlobOptions::default()))
624 .await?;
625 Ok(())
626 }
627
628 #[turbo_tasks::function(operation, root)]
629 async fn read_glob_from_operation(
630 path: RcStr,
631 directory: RcStr,
632 glob: RcStr,
633 ) -> anyhow::Result<Vc<ReadGlobResult>> {
634 let root = disk_file_system_root_operation(path)
635 .read_strongly_consistent()
636 .await?;
637 Ok(root
638 .join(&directory)?
639 .read_glob(Glob::new(glob, GlobOptions::default())))
640 }
641
642 #[turbo_tasks::function(operation, root)]
643 async fn track_glob_from_operation(
644 path: RcStr,
645 directory: RcStr,
646 glob: RcStr,
647 ) -> anyhow::Result<Vc<Completion>> {
648 let root = disk_file_system_root_operation(path)
649 .read_strongly_consistent()
650 .await?;
651 Ok(root
652 .join(&directory)?
653 .track_glob(Glob::new(glob, GlobOptions::default()), false))
654 }
655
656 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
657 async fn glob_roots_resolve_symlink_parents() {
658 let scratch = tempfile::tempdir().unwrap();
659 let path = scratch.path();
660 let target = path.join("target/inner/path");
661 std::fs::create_dir_all(&target).unwrap();
662 File::create_new(target.join("file.txt"))
663 .unwrap()
664 .write_all(b"initial")
665 .unwrap();
666 std::fs::create_dir_all(path.join("path/to")).unwrap();
667 symlink(path.join("target"), path.join("path/to/symlink")).unwrap();
668
669 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
670 BackendOptions::default(),
671 noop_backing_storage(),
672 ));
673 let disk_root: RcStr = path.to_str().unwrap().into();
674 tt.run_once(async move {
675 let root = disk_file_system_root_operation(disk_root.clone())
676 .read_strongly_consistent()
677 .await?;
678 let logical_base = root.join("path/to/symlink/inner/path")?;
679
680 let initial = read_glob_from_operation(
681 disk_root.clone(),
682 rcstr!("path/to/symlink/inner/path"),
683 rcstr!("*"),
684 )
685 .read_strongly_consistent()
686 .await?;
687 assert_eq!(
688 initial.results.get("file.txt"),
689 Some(&DirectoryEntry::File(logical_base.join("file.txt")?))
690 );
691
692 let wildcard = read_glob_from_operation(
693 disk_root.clone(),
694 rcstr!(""),
695 rcstr!("path/to/*/inner/path/*"),
696 )
697 .read_strongly_consistent()
698 .await?;
699 let path_result = wildcard.inner.get("path").unwrap().await?;
700 let to_result = path_result.inner.get("to").unwrap().await?;
701 let symlink_result = to_result.inner.get("symlink").unwrap().await?;
702 let inner_result = symlink_result.inner.get("inner").unwrap().await?;
703 let final_result = inner_result.inner.get("path").unwrap().await?;
704 assert_eq!(
705 final_result.results.get("file.txt"),
706 Some(&DirectoryEntry::File(logical_base.join("file.txt")?))
707 );
708
709 let initial_tracking = track_glob_from_operation(
710 disk_root.clone(),
711 rcstr!("path/to/symlink/inner/path"),
712 rcstr!("*"),
713 )
714 .read_strongly_consistent()
715 .await?;
716 let wildcard_tracking = track_glob_from_operation(
717 disk_root.clone(),
718 rcstr!(""),
719 rcstr!("path/to/*/inner/path/*"),
720 )
721 .read_strongly_consistent()
722 .await?;
723
724 read_strongly_consistent_and_apply_effects(
725 extract_effects_operation(write(
726 root.join("target/inner/path/file.txt")?,
727 rcstr!("updated"),
728 )),
729 |e| e,
730 )
731 .await?;
732
733 let initial_tracking_after = track_glob_from_operation(
734 disk_root.clone(),
735 rcstr!("path/to/symlink/inner/path"),
736 rcstr!("*"),
737 )
738 .read_strongly_consistent()
739 .await?;
740 let wildcard_tracking_after =
741 track_glob_from_operation(disk_root, rcstr!(""), rcstr!("path/to/*/inner/path/*"))
742 .read_strongly_consistent()
743 .await?;
744
745 assert!(!ReadRef::ptr_eq(&initial_tracking, &initial_tracking_after));
746 assert!(!ReadRef::ptr_eq(
747 &wildcard_tracking,
748 &wildcard_tracking_after
749 ));
750 anyhow::Ok(())
751 })
752 .await
753 .unwrap();
754 }
755
756 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
757 async fn track_glob_invalidations() {
758 let scratch = tempfile::tempdir().unwrap();
759
760 let path = scratch.path();
762 let dir = path.join("dir");
763 create_dir(&dir).unwrap();
764 File::create_new(dir.join("foo"))
765 .unwrap()
766 .write_all(b"foo")
767 .unwrap();
768 create_dir(dir.join("sub")).unwrap();
769 File::create_new(dir.join("sub/bar"))
770 .unwrap()
771 .write_all(b"bar")
772 .unwrap();
773 create_dir(dir.join("sub/.vim")).unwrap();
775 let gitignore = dir.join("sub/.vim/.gitignore");
776 File::create_new(&gitignore)
777 .unwrap()
778 .write_all(b"ignore")
779 .unwrap();
780 let link_target = path.join("link_target.js");
782 File::create_new(&link_target)
783 .unwrap()
784 .write_all(b"link_target")
785 .unwrap();
786 symlink(&link_target, dir.join("link.js")).unwrap();
787
788 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
789 BackendOptions::default(),
790 noop_backing_storage(),
791 ));
792 let path: RcStr = scratch.path().to_str().unwrap().into();
793 tt.run_once(async {
794 let root = disk_file_system_root_operation(path)
795 .read_strongly_consistent()
796 .await?;
797 let dir = root.join("dir")?;
798 let read_dir = track_star_star_glob(dir.clone())
799 .read_strongly_consistent()
800 .await?;
801
802 read_strongly_consistent_and_apply_effects(
804 extract_effects_operation(delete(root.join("dir/sub/.vim/.gitignore")?)),
805 |e| e,
806 )
807 .await?;
808
809 let read_dir2 = track_star_star_glob(dir.clone())
810 .read_strongly_consistent()
811 .await?;
812 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
813
814 read_strongly_consistent_and_apply_effects(
816 extract_effects_operation(write(root.join("dir/sub/.ignored")?, rcstr!("ignored"))),
817 |e| e,
818 )
819 .await?;
820 let read_dir2 = track_star_star_glob(dir.clone())
821 .read_strongly_consistent()
822 .await?;
823 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
824
825 read_strongly_consistent_and_apply_effects(
826 extract_effects_operation(delete(root.join("dir/sub/.ignored")?)),
827 |e| e,
828 )
829 .await?;
830 let read_dir2 = track_star_star_glob(dir.clone())
831 .read_strongly_consistent()
832 .await?;
833 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
834
835 read_strongly_consistent_and_apply_effects(
837 extract_effects_operation(delete(root.join("dir/foo")?)),
838 |e| e,
839 )
840 .await?;
841
842 let read_dir2 = track_star_star_glob(dir.clone())
843 .read_strongly_consistent()
844 .await?;
845
846 assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
847
848 read_strongly_consistent_and_apply_effects(
850 extract_effects_operation(write(
851 root.join("link_target.js")?,
852 rcstr!("new_contents"),
853 )),
854 |e| e,
855 )
856 .await?;
857 let read_dir3 = track_star_star_glob(dir.clone())
858 .read_strongly_consistent()
859 .await?;
860
861 assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
862
863 anyhow::Ok(())
864 })
865 .await
866 .unwrap();
867 }
868
869 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
870 async fn track_glob_symlinks_loop() {
871 let scratch = tempfile::tempdir().unwrap();
872 {
873 let path = scratch.path();
876 let sub = &path.join("sub");
877 create_dir(sub).unwrap();
878 let foo = sub.join("foo.js");
879 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
880 symlink(sub, sub.join("link")).unwrap();
882 }
883 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
884 BackendOptions::default(),
885 noop_backing_storage(),
886 ));
887 let path: RcStr = scratch.path().to_str().unwrap().into();
888 tt.run_once(async {
889 let err = track_glob_operation(path.clone(), rcstr!("**"))
890 .read_strongly_consistent()
891 .await
892 .expect_err("Should have detected an infinite loop");
893
894 assert_eq!(
895 "'sub/link' is a symlink causes that causes an infinite loop!",
896 format!("{}", err.root_cause())
897 );
898
899 let err = track_glob_operation(path, rcstr!("**"))
901 .read_strongly_consistent()
902 .await
903 .expect_err("Should have detected an infinite loop");
904
905 assert_eq!(
906 "'sub/link' is a symlink causes that causes an infinite loop!",
907 format!("{}", err.root_cause())
908 );
909
910 anyhow::Ok(())
911 })
912 .await
913 .unwrap();
914 }
915
916 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
918 async fn dead_symlinks() {
919 let scratch = tempfile::tempdir().unwrap();
920 {
921 let path = scratch.path();
923 let sub = &path.join("sub");
924 create_dir(sub).unwrap();
925 let foo = sub.join("foo.js");
926 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
927 symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
929 }
930 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
931 BackendOptions::default(),
932 noop_backing_storage(),
933 ));
934 let path: RcStr = scratch.path().to_str().unwrap().into();
935 tt.run_once(async {
936 track_glob_operation(path, rcstr!("sub/*.js"))
937 .read_strongly_consistent()
938 .await?;
939 anyhow::Ok(())
940 })
941 .await
942 .unwrap();
943 let path: RcStr = scratch.path().to_str().unwrap().into();
944 tt.run_once(async {
945 assert_dead_symlink_read_glob_operation(path)
946 .read_strongly_consistent()
947 .await?;
948 anyhow::Ok(())
949 })
950 .await
951 .unwrap();
952 }
953
954 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
956 async fn symlink_escapes_fs_root() {
957 let scratch = tempfile::tempdir().unwrap();
958 {
959 let path = scratch.path();
961 let sub = &path.join("sub");
962 create_dir(sub).unwrap();
963 let foo = scratch.path().join("foo.js");
964 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
965 symlink(foo, sub.join("escape.js")).unwrap();
967 }
968 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
969 BackendOptions::default(),
970 noop_backing_storage(),
971 ));
972 let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
973 tt.run_once(async {
974 track_glob_operation(root, rcstr!("*.js"))
975 .read_strongly_consistent()
976 .await?;
977 anyhow::Ok(())
978 })
979 .await
980 .unwrap();
981 }
982
983 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
984 async fn read_glob_symlinks_loop() {
985 let scratch = tempfile::tempdir().unwrap();
986 {
987 let path = scratch.path();
990 let sub = &path.join("sub");
991 create_dir(sub).unwrap();
992 let foo = sub.join("foo.js");
993 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
994 symlink(sub, sub.join("link")).unwrap();
996 }
997 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
998 BackendOptions::default(),
999 noop_backing_storage(),
1000 ));
1001 let path: RcStr = scratch.path().to_str().unwrap().into();
1002 tt.run_once(async {
1003 let err = read_glob_operation(path.clone(), rcstr!("**"))
1004 .read_strongly_consistent()
1005 .await
1006 .expect_err("Should have detected an infinite loop");
1007
1008 assert_eq!(
1009 "'sub/link' is a symlink causes that causes an infinite loop!",
1010 format!("{}", err.root_cause())
1011 );
1012
1013 let err = track_glob_operation(path, rcstr!("**"))
1015 .read_strongly_consistent()
1016 .await
1017 .expect_err("Should have detected an infinite loop");
1018
1019 assert_eq!(
1020 "'sub/link' is a symlink causes that causes an infinite loop!",
1021 format!("{}", err.root_cause())
1022 );
1023
1024 anyhow::Ok(())
1025 })
1026 .await
1027 .unwrap();
1028 }
1029}