Skip to main content

turbo_tasks_fs/
read_glob.rs

1use anyhow::{Result, bail};
2use futures::try_join;
3use rustc_hash::FxHashMap;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{Completion, ResolvedVc, TryJoinIterExt, Vc};
6
7use crate::{
8    DirectoryContent, DirectoryEntry, FileSystem, FileSystemPath, LinkContent, LinkType, glob::Glob,
9};
10
11#[turbo_tasks::value]
12#[derive(Default, Debug)]
13pub struct ReadGlobResult {
14    pub results: FxHashMap<RcStr, DirectoryEntry>,
15    pub inner: FxHashMap<RcStr, ResolvedVc<ReadGlobResult>>,
16}
17
18/// Reads matches of a glob pattern. Symlinks are not resolved (and returned as-is)
19///
20/// DETERMINISM: Result is in random order. Either sort result or do not depend
21/// on the order.
22#[turbo_tasks::function(fs)]
23pub async fn read_glob(directory: FileSystemPath, glob: Vc<Glob>) -> Result<Vc<ReadGlobResult>> {
24    read_glob_internal("", directory, glob).await
25}
26
27#[turbo_tasks::function(fs)]
28async fn read_glob_inner(
29    prefix: RcStr,
30    directory: FileSystemPath,
31    glob: Vc<Glob>,
32) -> Result<Vc<ReadGlobResult>> {
33    read_glob_internal(&prefix, directory, glob).await
34}
35
36// The `prefix` represents the relative directory path where symlinks are not resolve.
37async fn read_glob_internal(
38    prefix: &str,
39    directory: FileSystemPath,
40    glob: Vc<Glob>,
41) -> Result<Vc<ReadGlobResult>> {
42    let dir = directory.read_dir().await?;
43    let mut result = ReadGlobResult::default();
44    let glob_value = glob.await?;
45    let handle_file = |result: &mut ReadGlobResult,
46                       entry_path: &RcStr,
47                       segment: &RcStr,
48                       entry: &DirectoryEntry| {
49        if glob_value.matches(entry_path) {
50            result.results.insert(segment.clone(), entry.clone());
51        }
52    };
53    let handle_dir = async |result: &mut ReadGlobResult,
54                            entry_path: RcStr,
55                            segment: &RcStr,
56                            path: &FileSystemPath| {
57        if glob_value.can_match_in_directory(&entry_path) {
58            result.inner.insert(
59                segment.clone(),
60                read_glob_inner(entry_path, path.clone(), glob)
61                    .to_resolved()
62                    .await?,
63            );
64        }
65        anyhow::Ok(())
66    };
67
68    match &*dir {
69        DirectoryContent::Entries(entries) => {
70            for (segment, entry) in entries.iter() {
71                let entry_path: RcStr = if prefix.is_empty() {
72                    segment.clone()
73                } else {
74                    format!("{prefix}/{segment}").into()
75                };
76
77                match entry {
78                    DirectoryEntry::File(_) => {
79                        handle_file(&mut result, &entry_path, segment, entry);
80                    }
81                    DirectoryEntry::Directory(path) => {
82                        // Add the directory to `results` if it is a whole match of the glob
83                        handle_file(&mut result, &entry_path, segment, entry);
84                        // Recursively handle the directory
85                        handle_dir(&mut result, entry_path, segment, path).await?;
86                    }
87                    DirectoryEntry::Symlink(path) => {
88                        if let LinkContent::Link { link_type, .. } = &*path.read_link().await? {
89                            if link_type.contains(LinkType::DIRECTORY) {
90                                // Ensure that there are no infinite link loops, but don't resolve
91                                resolve_symlink_safely(entry.clone()).await?;
92
93                                // Add the directory to `results` if it is a whole match of the glob
94                                handle_file(&mut result, &entry_path, segment, entry);
95                                // Recursively handle the directory
96                                handle_dir(&mut result, entry_path, segment, path).await?;
97                            } else {
98                                handle_file(&mut result, &entry_path, segment, entry);
99                            }
100                        }
101                    }
102                    DirectoryEntry::Other(_) | DirectoryEntry::Error(_) => continue,
103                }
104            }
105        }
106        DirectoryContent::NotFound => {}
107    }
108    Ok(ReadGlobResult::cell(result))
109}
110
111/// Resolve a symlink checking for recursion.
112async fn resolve_symlink_safely(entry: DirectoryEntry) -> Result<DirectoryEntry> {
113    let resolved_entry = entry.clone().resolve_symlink().await?;
114    if resolved_entry != entry && matches!(&resolved_entry, DirectoryEntry::Directory(_)) {
115        // We followed a symlink to a directory
116        // To prevent an infinite loop, which in the case of turbo-tasks would simply
117        // exhaust RAM or go into an infinite loop with the GC we need to check for a
118        // recursive symlink, we need to check for recursion.
119
120        // Recursion can only occur if the symlink is a directory and points to an
121        // ancestor of the current path, which can be detected via a simple prefix
122        // match.
123        let source_path = entry.path().unwrap();
124        if source_path.is_inside_or_equal(&resolved_entry.clone().path().unwrap()) {
125            bail!(
126                "'{}' is a symlink causes that causes an infinite loop!",
127                source_path.path,
128            )
129        }
130    }
131    Ok(resolved_entry)
132}
133
134/// Traverses all directories that match the given `glob`.
135///
136/// This ensures that the calling task will be invalidated
137/// whenever the directories or contents of the directories change,
138///  but unlike read_glob doesn't accumulate data.
139#[turbo_tasks::function(fs)]
140pub async fn track_glob(
141    directory: FileSystemPath,
142    glob: Vc<Glob>,
143    include_dot_files: bool,
144) -> Result<Vc<Completion>> {
145    track_glob_internal("", directory, glob, include_dot_files).await
146}
147
148#[turbo_tasks::function(fs)]
149async fn track_glob_inner(
150    prefix: RcStr,
151    directory: FileSystemPath,
152    glob: Vc<Glob>,
153    include_dot_files: bool,
154) -> Result<Vc<Completion>> {
155    track_glob_internal(&prefix, directory, glob, include_dot_files).await
156}
157
158async fn track_glob_internal(
159    prefix: &str,
160    directory: FileSystemPath,
161    glob: Vc<Glob>,
162    include_dot_files: bool,
163) -> Result<Vc<Completion>> {
164    let dir = directory.read_dir().await?;
165    let glob_value = glob.await?;
166    let fs = directory.fs().to_resolved().await?;
167    let mut reads = Vec::new();
168    let mut completions = Vec::new();
169    let mut types = Vec::new();
170    match &*dir {
171        DirectoryContent::Entries(entries) => {
172            for (segment, entry) in entries.iter() {
173                if !include_dot_files && segment.starts_with('.') {
174                    continue;
175                }
176                // This is redundant with logic inside of `read_dir` but here we track it separately
177                // so we don't follow symlinks.
178                let entry_path = if prefix.is_empty() {
179                    segment.clone()
180                } else {
181                    format!("{prefix}/{segment}").into()
182                };
183
184                match resolve_symlink_safely(entry.clone()).await? {
185                    DirectoryEntry::Directory(path) => {
186                        if glob_value.can_match_in_directory(&entry_path) {
187                            completions.push(track_glob_inner(
188                                entry_path,
189                                path.clone(),
190                                glob,
191                                include_dot_files,
192                            ));
193                        }
194                    }
195                    DirectoryEntry::File(path) => {
196                        if glob_value.matches(&entry_path) {
197                            reads.push(fs.read(path.clone()))
198                        }
199                    }
200                    DirectoryEntry::Symlink(symlink_path) => bail!(
201                        "resolve_symlink_safely() should have resolved all symlinks or returned \
202                         an error, but found unresolved symlink at path: '{}'. Found path: '{}'. \
203                         Please report this as a bug.",
204                        entry_path,
205                        symlink_path
206                    ),
207                    DirectoryEntry::Other(path) => {
208                        if glob_value.matches(&entry_path) {
209                            types.push(path.get_type())
210                        }
211                    }
212                    // The most likely case of this is actually a sylink resolution error, it is
213                    // fine to ignore since the mere act of attempting to resolve it has triggered
214                    // the ncecessary dependencies.  If this file is actually a dependency we should
215                    // get an error in the actual webpack loader when it reads it.
216                    DirectoryEntry::Error(_) => {}
217                }
218            }
219        }
220        DirectoryContent::NotFound => {}
221    }
222    try_join!(
223        reads.iter().try_join(),
224        types.iter().try_join(),
225        completions.iter().try_join()
226    )?;
227    Ok(Completion::new())
228}
229
230#[cfg(test)]
231pub mod tests {
232
233    use std::{
234        collections::HashMap,
235        fs::{File, create_dir},
236        io::prelude::*,
237    };
238
239    use turbo_rcstr::{RcStr, rcstr};
240    use turbo_tasks::{Completion, ReadRef, Vc, apply_effects};
241    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
242
243    use crate::{
244        DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath,
245        glob::{Glob, GlobOptions},
246    };
247
248    fn symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
249        target: Q,
250        path: P,
251    ) -> std::io::Result<()> {
252        assert!(target.as_ref().is_absolute());
253        let _ = std::fs::remove_dir(&path);
254        let _ = std::fs::remove_file(&path);
255
256        #[cfg(unix)]
257        {
258            std::os::unix::fs::symlink(target, path)
259        }
260        #[cfg(windows)]
261        {
262            let metadata = std::fs::metadata(&target).ok();
263            if metadata.is_none_or(|m| m.is_file()) {
264                std::os::windows::fs::symlink_file(target, path)
265            } else {
266                std::os::windows::fs::junction_point(target, path)
267            }
268        }
269    }
270
271    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
272    async fn read_glob_basic() {
273        let scratch = tempfile::tempdir().unwrap();
274        {
275            // Create a simple directory with 2 files, a subdirectory and a dotfile
276            let path = scratch.path();
277            File::create_new(path.join("foo"))
278                .unwrap()
279                .write_all(b"foo")
280                .unwrap();
281            create_dir(path.join("sub")).unwrap();
282            File::create_new(path.join("sub/bar"))
283                .unwrap()
284                .write_all(b"bar")
285                .unwrap();
286        }
287        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
288            BackendOptions::default(),
289            noop_backing_storage(),
290        ));
291        let path: RcStr = scratch.path().to_str().unwrap().into();
292        tt.run_once(async {
293            let fs = DiskFileSystem::new(rcstr!("temp"), path);
294            let root = fs.root().await?;
295            let read_dir = root
296                .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
297                .await
298                .unwrap();
299            assert_eq!(read_dir.results.len(), 2);
300            assert_eq!(
301                read_dir.results.get("foo"),
302                Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
303            );
304            assert_eq!(
305                read_dir.results.get("sub"),
306                Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
307            );
308            assert_eq!(read_dir.inner.len(), 1);
309            let inner = &*read_dir.inner.get("sub").unwrap().await?;
310            assert_eq!(inner.results.len(), 1);
311            assert_eq!(
312                inner.results.get("bar"),
313                Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
314            );
315            assert_eq!(inner.inner.len(), 0);
316
317            // Now with a more specific pattern
318            let read_dir = root
319                .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
320                .await
321                .unwrap();
322            assert_eq!(read_dir.results.len(), 0);
323            assert_eq!(read_dir.inner.len(), 1);
324            let inner = &*read_dir.inner.get("sub").unwrap().await?;
325            assert_eq!(inner.results.len(), 1);
326            assert_eq!(
327                inner.results.get("bar"),
328                Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
329            );
330
331            assert_eq!(inner.inner.len(), 0);
332
333            anyhow::Ok(())
334        })
335        .await
336        .unwrap();
337    }
338
339    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
340    async fn read_glob_symlinks() {
341        let scratch = tempfile::tempdir().unwrap();
342        {
343            // root.js
344            // sub/foo.js
345            // sub/link-foo.js -> ./foo.js
346            // sub/link-root.js -> ../root.js
347            let path = scratch.path();
348            create_dir(path.join("sub")).unwrap();
349            let foo = path.join("sub/foo.js");
350            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
351            symlink(&foo, path.join("sub/link-foo.js")).unwrap();
352
353            let root = path.join("root.js");
354            File::create_new(&root).unwrap().write_all(b"root").unwrap();
355            symlink(&root, path.join("sub/link-root.js")).unwrap();
356
357            let dir = path.join("dir");
358            create_dir(&dir).unwrap();
359            File::create_new(dir.join("index.js"))
360                .unwrap()
361                .write_all(b"dir index")
362                .unwrap();
363            symlink(&dir, path.join("sub/dir")).unwrap();
364        }
365        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
366            BackendOptions::default(),
367            noop_backing_storage(),
368        ));
369        let path: RcStr = scratch.path().to_str().unwrap().into();
370        tt.run_once(async {
371            let fs = DiskFileSystem::new(rcstr!("temp"), path);
372            let root = fs.root().await?;
373            // Symlinked files
374            let read_dir = root
375                .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
376                .await
377                .unwrap();
378            assert_eq!(read_dir.results.len(), 0);
379            let inner = &*read_dir.inner.get("sub").unwrap().await?;
380            assert_eq!(
381                inner.results,
382                HashMap::from_iter([
383                    (
384                        "link-foo.js".into(),
385                        DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
386                    ),
387                    (
388                        "link-root.js".into(),
389                        DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
390                    ),
391                    (
392                        "foo.js".into(),
393                        DirectoryEntry::File(root.join("sub/foo.js")?),
394                    ),
395                ])
396            );
397            assert_eq!(inner.inner.len(), 0);
398
399            // A symlinked folder
400            let read_dir = root
401                .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
402                .await
403                .unwrap();
404            assert_eq!(read_dir.results.len(), 0);
405            let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
406            assert_eq!(inner_sub.results.len(), 0);
407            let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
408            assert_eq!(
409                inner_sub_dir.results,
410                HashMap::from_iter([(
411                    "index.js".into(),
412                    DirectoryEntry::File(root.join("sub/dir/index.js")?),
413                )])
414            );
415            assert_eq!(inner_sub_dir.inner.len(), 0);
416
417            anyhow::Ok(())
418        })
419        .await
420        .unwrap();
421    }
422
423    #[turbo_tasks::function(operation)]
424    pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
425        path.write(FileContent::NotFound.cell()).await?;
426        Ok(())
427    }
428
429    #[turbo_tasks::function(operation)]
430    pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
431        path.write(
432            FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
433        )
434        .await?;
435        Ok(())
436    }
437
438    #[turbo_tasks::function(operation)]
439    pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
440        path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
441    }
442
443    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
444    async fn track_glob_invalidations() {
445        let scratch = tempfile::tempdir().unwrap();
446
447        // Create a simple directory with 2 files, a subdirectory and a dotfile
448        let path = scratch.path();
449        let dir = path.join("dir");
450        create_dir(&dir).unwrap();
451        File::create_new(dir.join("foo"))
452            .unwrap()
453            .write_all(b"foo")
454            .unwrap();
455        create_dir(dir.join("sub")).unwrap();
456        File::create_new(dir.join("sub/bar"))
457            .unwrap()
458            .write_all(b"bar")
459            .unwrap();
460        // Add a dotfile
461        create_dir(dir.join("sub/.vim")).unwrap();
462        let gitignore = dir.join("sub/.vim/.gitignore");
463        File::create_new(&gitignore)
464            .unwrap()
465            .write_all(b"ignore")
466            .unwrap();
467        // put a link in the dir that points at a file in the root.
468        let link_target = path.join("link_target.js");
469        File::create_new(&link_target)
470            .unwrap()
471            .write_all(b"link_target")
472            .unwrap();
473        symlink(&link_target, dir.join("link.js")).unwrap();
474
475        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
476            BackendOptions::default(),
477            noop_backing_storage(),
478        ));
479        let path: RcStr = scratch.path().to_str().unwrap().into();
480        tt.run_once(async {
481            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
482            let dir = fs.root().await?.join("dir")?;
483            let read_dir = track_star_star_glob(dir.clone())
484                .read_strongly_consistent()
485                .await?;
486
487            // Delete a file that we shouldn't be tracking
488            let delete_result = delete(fs.root().await?.join("dir/sub/.vim/.gitignore")?);
489            delete_result.read_strongly_consistent().await?;
490            apply_effects(delete_result).await?;
491
492            let read_dir2 = track_star_star_glob(dir.clone())
493                .read_strongly_consistent()
494                .await?;
495            assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
496
497            // Delete a file that we should be tracking
498            let delete_result = delete(fs.root().await?.join("dir/foo")?);
499            delete_result.read_strongly_consistent().await?;
500            apply_effects(delete_result).await?;
501
502            let read_dir2 = track_star_star_glob(dir.clone())
503                .read_strongly_consistent()
504                .await?;
505
506            assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
507
508            // Modify a symlink target file
509            let write_result = write(
510                fs.root().await?.join("link_target.js")?,
511                rcstr!("new_contents"),
512            );
513            write_result.read_strongly_consistent().await?;
514            apply_effects(write_result).await?;
515            let read_dir3 = track_star_star_glob(dir.clone())
516                .read_strongly_consistent()
517                .await?;
518
519            assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
520
521            anyhow::Ok(())
522        })
523        .await
524        .unwrap();
525    }
526
527    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
528    async fn track_glob_symlinks_loop() {
529        let scratch = tempfile::tempdir().unwrap();
530        {
531            // Create a simple directory with 1 file and a symlink pointing at at a file in a
532            // subdirectory
533            let path = scratch.path();
534            let sub = &path.join("sub");
535            create_dir(sub).unwrap();
536            let foo = sub.join("foo.js");
537            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
538            // put a link in sub that points back at its parent director
539            symlink(sub, sub.join("link")).unwrap();
540        }
541        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
542            BackendOptions::default(),
543            noop_backing_storage(),
544        ));
545        let path: RcStr = scratch.path().to_str().unwrap().into();
546        tt.run_once(async {
547            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
548            let err = fs
549                .root()
550                .await?
551                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
552                .await
553                .expect_err("Should have detected an infinite loop");
554
555            assert_eq!(
556                "'sub/link' is a symlink causes that causes an infinite loop!",
557                format!("{}", err.root_cause())
558            );
559
560            // Same when calling track glob
561            let err = fs
562                .root()
563                .await?
564                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
565                .await
566                .expect_err("Should have detected an infinite loop");
567
568            assert_eq!(
569                "'sub/link' is a symlink causes that causes an infinite loop!",
570                format!("{}", err.root_cause())
571            );
572
573            anyhow::Ok(())
574        })
575        .await
576        .unwrap();
577    }
578
579    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
580    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
581    async fn dead_symlinks() {
582        let scratch = tempfile::tempdir().unwrap();
583        {
584            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
585            let path = scratch.path();
586            let sub = &path.join("sub");
587            create_dir(sub).unwrap();
588            let foo = sub.join("foo.js");
589            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
590            // put a link in sub that points to a sibling file that doesn't exist
591            symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
592        }
593        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
594            BackendOptions::default(),
595            noop_backing_storage(),
596        ));
597        let path: RcStr = scratch.path().to_str().unwrap().into();
598        tt.run_once(async {
599            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
600            fs.root()
601                .await?
602                .track_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()), false)
603                .await
604        })
605        .await
606        .unwrap();
607        let path: RcStr = scratch.path().to_str().unwrap().into();
608        tt.run_once(async {
609            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
610            let root = fs.root().owned().await?;
611            let read_dir = root
612                .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
613                .await?;
614            assert_eq!(read_dir.results.len(), 0);
615            assert_eq!(read_dir.inner.len(), 1);
616            let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
617            assert_eq!(inner_sub.inner.len(), 0);
618            assert_eq!(
619                inner_sub.results,
620                HashMap::from_iter([
621                    (
622                        "foo.js".into(),
623                        DirectoryEntry::File(root.join("sub/foo.js")?),
624                    ),
625                    // read_glob doesn't resolve symlinks and thus doesn't detect that it is dead
626                    (
627                        "dead_link.js".into(),
628                        DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
629                    )
630                ])
631            );
632
633            anyhow::Ok(())
634        })
635        .await
636        .unwrap();
637    }
638
639    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
640    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
641    async fn symlink_escapes_fs_root() {
642        let scratch = tempfile::tempdir().unwrap();
643        {
644            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
645            let path = scratch.path();
646            let sub = &path.join("sub");
647            create_dir(sub).unwrap();
648            let foo = scratch.path().join("foo.js");
649            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
650            // put a link in sub that points to a parent file
651            symlink(foo, sub.join("escape.js")).unwrap();
652        }
653        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
654            BackendOptions::default(),
655            noop_backing_storage(),
656        ));
657        let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
658        tt.run_once(async {
659            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), root));
660            fs.root()
661                .await?
662                .track_glob(Glob::new(rcstr!("*.js"), GlobOptions::default()), false)
663                .await
664        })
665        .await
666        .unwrap();
667    }
668
669    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
670    async fn read_glob_symlinks_loop() {
671        let scratch = tempfile::tempdir().unwrap();
672        {
673            // Create a simple directory with 1 file and a symlink pointing at at a file in a
674            // subdirectory
675            let path = scratch.path();
676            let sub = &path.join("sub");
677            create_dir(sub).unwrap();
678            let foo = sub.join("foo.js");
679            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
680            // put a link in sub that points back at its parent director
681            symlink(sub, sub.join("link")).unwrap();
682        }
683        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
684            BackendOptions::default(),
685            noop_backing_storage(),
686        ));
687        let path: RcStr = scratch.path().to_str().unwrap().into();
688        tt.run_once(async {
689            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
690            let err = fs
691                .root()
692                .await?
693                .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
694                .await
695                .expect_err("Should have detected an infinite loop");
696
697            assert_eq!(
698                "'sub/link' is a symlink causes that causes an infinite loop!",
699                format!("{}", err.root_cause())
700            );
701
702            // Same when calling track glob
703            let err = fs
704                .root()
705                .await?
706                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
707                .await
708                .expect_err("Should have detected an infinite loop");
709
710            assert_eq!(
711                "'sub/link' is a symlink causes that causes an infinite loop!",
712                format!("{}", err.root_cause())
713            );
714
715            anyhow::Ok(())
716        })
717        .await
718        .unwrap();
719    }
720}