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