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.to_string()
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    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
249    async fn read_glob_basic() {
250        let scratch = tempfile::tempdir().unwrap();
251        {
252            // Create a simple directory with 2 files, a subdirectory and a dotfile
253            let path = scratch.path();
254            File::create_new(path.join("foo"))
255                .unwrap()
256                .write_all(b"foo")
257                .unwrap();
258            create_dir(path.join("sub")).unwrap();
259            File::create_new(path.join("sub/bar"))
260                .unwrap()
261                .write_all(b"bar")
262                .unwrap();
263        }
264        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
265            BackendOptions::default(),
266            noop_backing_storage(),
267        ));
268        let path: RcStr = scratch.path().to_str().unwrap().into();
269        tt.run_once(async {
270            let fs = DiskFileSystem::new(rcstr!("temp"), path);
271            let root = fs.root().await?;
272            let read_dir = root
273                .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
274                .await
275                .unwrap();
276            assert_eq!(read_dir.results.len(), 2);
277            assert_eq!(
278                read_dir.results.get("foo"),
279                Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
280            );
281            assert_eq!(
282                read_dir.results.get("sub"),
283                Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
284            );
285            assert_eq!(read_dir.inner.len(), 1);
286            let inner = &*read_dir.inner.get("sub").unwrap().await?;
287            assert_eq!(inner.results.len(), 1);
288            assert_eq!(
289                inner.results.get("bar"),
290                Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
291            );
292            assert_eq!(inner.inner.len(), 0);
293
294            // Now with a more specific pattern
295            let read_dir = root
296                .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
297                .await
298                .unwrap();
299            assert_eq!(read_dir.results.len(), 0);
300            assert_eq!(read_dir.inner.len(), 1);
301            let inner = &*read_dir.inner.get("sub").unwrap().await?;
302            assert_eq!(inner.results.len(), 1);
303            assert_eq!(
304                inner.results.get("bar"),
305                Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
306            );
307
308            assert_eq!(inner.inner.len(), 0);
309
310            anyhow::Ok(())
311        })
312        .await
313        .unwrap();
314    }
315
316    #[cfg(unix)]
317    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
318    async fn read_glob_symlinks() {
319        use std::os::unix::fs::symlink;
320
321        let scratch = tempfile::tempdir().unwrap();
322        {
323            // root.js
324            // sub/foo.js
325            // sub/link-foo.js -> ./foo.js
326            // sub/link-root.js -> ../root.js
327            let path = scratch.path();
328            create_dir(path.join("sub")).unwrap();
329            let foo = path.join("sub/foo.js");
330            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
331            symlink(&foo, path.join("sub/link-foo.js")).unwrap();
332
333            let root = path.join("root.js");
334            File::create_new(&root).unwrap().write_all(b"root").unwrap();
335            symlink(&root, path.join("sub/link-root.js")).unwrap();
336
337            let dir = path.join("dir");
338            create_dir(&dir).unwrap();
339            File::create_new(dir.join("index.js"))
340                .unwrap()
341                .write_all(b"dir index")
342                .unwrap();
343            symlink(&dir, path.join("sub/dir")).unwrap();
344        }
345        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
346            BackendOptions::default(),
347            noop_backing_storage(),
348        ));
349        let path: RcStr = scratch.path().to_str().unwrap().into();
350        tt.run_once(async {
351            let fs = DiskFileSystem::new(rcstr!("temp"), path);
352            let root = fs.root().await?;
353            // Symlinked files
354            let read_dir = root
355                .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
356                .await
357                .unwrap();
358            assert_eq!(read_dir.results.len(), 0);
359            let inner = &*read_dir.inner.get("sub").unwrap().await?;
360            assert_eq!(
361                inner.results,
362                HashMap::from_iter([
363                    (
364                        "link-foo.js".into(),
365                        DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
366                    ),
367                    (
368                        "link-root.js".into(),
369                        DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
370                    ),
371                    (
372                        "foo.js".into(),
373                        DirectoryEntry::File(root.join("sub/foo.js")?),
374                    ),
375                ])
376            );
377            assert_eq!(inner.inner.len(), 0);
378
379            // A symlinked folder
380            let read_dir = root
381                .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
382                .await
383                .unwrap();
384            assert_eq!(read_dir.results.len(), 0);
385            let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
386            assert_eq!(inner_sub.results.len(), 0);
387            let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
388            assert_eq!(
389                inner_sub_dir.results,
390                HashMap::from_iter([(
391                    "index.js".into(),
392                    DirectoryEntry::File(root.join("sub/dir/index.js")?),
393                )])
394            );
395            assert_eq!(inner_sub_dir.inner.len(), 0);
396
397            anyhow::Ok(())
398        })
399        .await
400        .unwrap();
401    }
402
403    #[turbo_tasks::function(operation)]
404    pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
405        path.write(FileContent::NotFound.cell()).await?;
406        Ok(())
407    }
408
409    #[turbo_tasks::function(operation)]
410    pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
411        path.write(
412            FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
413        )
414        .await?;
415        Ok(())
416    }
417
418    #[turbo_tasks::function(operation)]
419    pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
420        path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
421    }
422
423    #[cfg(unix)]
424    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
425    async fn track_glob_invalidations() {
426        use std::os::unix::fs::symlink;
427        let scratch = tempfile::tempdir().unwrap();
428
429        // Create a simple directory with 2 files, a subdirectory and a dotfile
430        let path = scratch.path();
431        let dir = path.join("dir");
432        create_dir(&dir).unwrap();
433        File::create_new(dir.join("foo"))
434            .unwrap()
435            .write_all(b"foo")
436            .unwrap();
437        create_dir(dir.join("sub")).unwrap();
438        File::create_new(dir.join("sub/bar"))
439            .unwrap()
440            .write_all(b"bar")
441            .unwrap();
442        // Add a dotfile
443        create_dir(dir.join("sub/.vim")).unwrap();
444        let gitignore = dir.join("sub/.vim/.gitignore");
445        File::create_new(&gitignore)
446            .unwrap()
447            .write_all(b"ignore")
448            .unwrap();
449        // put a link in the dir that points at a file in the root.
450        let link_target = path.join("link_target.js");
451        File::create_new(&link_target)
452            .unwrap()
453            .write_all(b"link_target")
454            .unwrap();
455        symlink(&link_target, dir.join("link.js")).unwrap();
456
457        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
458            BackendOptions::default(),
459            noop_backing_storage(),
460        ));
461        let path: RcStr = scratch.path().to_str().unwrap().into();
462        tt.run_once(async {
463            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
464            let dir = fs.root().await?.join("dir")?;
465            let read_dir = track_star_star_glob(dir.clone())
466                .read_strongly_consistent()
467                .await?;
468
469            // Delete a file that we shouldn't be tracking
470            let delete_result = delete(fs.root().await?.join("dir/sub/.vim/.gitignore")?);
471            delete_result.read_strongly_consistent().await?;
472            apply_effects(delete_result).await?;
473
474            let read_dir2 = track_star_star_glob(dir.clone())
475                .read_strongly_consistent()
476                .await?;
477            assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
478
479            // Delete a file that we should be tracking
480            let delete_result = delete(fs.root().await?.join("dir/foo")?);
481            delete_result.read_strongly_consistent().await?;
482            apply_effects(delete_result).await?;
483
484            let read_dir2 = track_star_star_glob(dir.clone())
485                .read_strongly_consistent()
486                .await?;
487
488            assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
489
490            // Modify a symlink target file
491            let write_result = write(
492                fs.root().await?.join("link_target.js")?,
493                rcstr!("new_contents"),
494            );
495            write_result.read_strongly_consistent().await?;
496            apply_effects(write_result).await?;
497            let read_dir3 = track_star_star_glob(dir.clone())
498                .read_strongly_consistent()
499                .await?;
500
501            assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
502
503            anyhow::Ok(())
504        })
505        .await
506        .unwrap();
507    }
508
509    #[cfg(unix)]
510    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
511    async fn track_glob_symlinks_loop() {
512        let scratch = tempfile::tempdir().unwrap();
513        {
514            use std::os::unix::fs::symlink;
515
516            // Create a simple directory with 1 file and a symlink pointing at at a file in a
517            // subdirectory
518            let path = scratch.path();
519            let sub = &path.join("sub");
520            create_dir(sub).unwrap();
521            let foo = sub.join("foo.js");
522            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
523            // put a link in sub that points back at its parent director
524            symlink(sub, sub.join("link")).unwrap();
525        }
526        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
527            BackendOptions::default(),
528            noop_backing_storage(),
529        ));
530        let path: RcStr = scratch.path().to_str().unwrap().into();
531        tt.run_once(async {
532            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
533            let err = fs
534                .root()
535                .await?
536                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
537                .await
538                .expect_err("Should have detected an infinite loop");
539
540            assert_eq!(
541                "'sub/link' is a symlink causes that causes an infinite loop!",
542                format!("{}", err.root_cause())
543            );
544
545            // Same when calling track glob
546            let err = fs
547                .root()
548                .await?
549                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
550                .await
551                .expect_err("Should have detected an infinite loop");
552
553            assert_eq!(
554                "'sub/link' is a symlink causes that causes an infinite loop!",
555                format!("{}", err.root_cause())
556            );
557
558            anyhow::Ok(())
559        })
560        .await
561        .unwrap();
562    }
563
564    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
565    #[cfg(unix)]
566    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
567    async fn dead_symlinks() {
568        let scratch = tempfile::tempdir().unwrap();
569        {
570            use std::os::unix::fs::symlink;
571
572            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
573            let path = scratch.path();
574            let sub = &path.join("sub");
575            create_dir(sub).unwrap();
576            let foo = sub.join("foo.js");
577            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
578            // put a link in sub that points to a sibling file that doesn't exist
579            symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
580        }
581        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
582            BackendOptions::default(),
583            noop_backing_storage(),
584        ));
585        let path: RcStr = scratch.path().to_str().unwrap().into();
586        tt.run_once(async {
587            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
588            fs.root()
589                .await?
590                .track_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()), false)
591                .await
592        })
593        .await
594        .unwrap();
595        let path: RcStr = scratch.path().to_str().unwrap().into();
596        tt.run_once(async {
597            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
598            let root = fs.root().owned().await?;
599            let read_dir = root
600                .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
601                .await?;
602            assert_eq!(read_dir.results.len(), 0);
603            assert_eq!(read_dir.inner.len(), 1);
604            let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
605            assert_eq!(inner_sub.inner.len(), 0);
606            assert_eq!(
607                inner_sub.results,
608                HashMap::from_iter([
609                    (
610                        "foo.js".into(),
611                        DirectoryEntry::File(root.join("sub/foo.js")?),
612                    ),
613                    // read_glob doesn't resolve symlinks and thus doesn't detect that it is dead
614                    (
615                        "dead_link.js".into(),
616                        DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
617                    )
618                ])
619            );
620
621            anyhow::Ok(())
622        })
623        .await
624        .unwrap();
625    }
626
627    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
628    #[cfg(unix)]
629    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
630    async fn symlink_escapes_fs_root() {
631        let scratch = tempfile::tempdir().unwrap();
632        {
633            use std::os::unix::fs::symlink;
634
635            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
636            let path = scratch.path();
637            let sub = &path.join("sub");
638            create_dir(sub).unwrap();
639            let foo = scratch.path().join("foo.js");
640            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
641            // put a link in sub that points to a parent file
642            symlink(foo, sub.join("escape.js")).unwrap();
643        }
644        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
645            BackendOptions::default(),
646            noop_backing_storage(),
647        ));
648        let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
649        tt.run_once(async {
650            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), root));
651            fs.root()
652                .await?
653                .track_glob(Glob::new(rcstr!("*.js"), GlobOptions::default()), false)
654                .await
655        })
656        .await
657        .unwrap();
658    }
659
660    #[cfg(unix)]
661    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
662    async fn read_glob_symlinks_loop() {
663        let scratch = tempfile::tempdir().unwrap();
664        {
665            use std::os::unix::fs::symlink;
666
667            // Create a simple directory with 1 file and a symlink pointing at at a file in a
668            // subdirectory
669            let path = scratch.path();
670            let sub = &path.join("sub");
671            create_dir(sub).unwrap();
672            let foo = sub.join("foo.js");
673            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
674            // put a link in sub that points back at its parent director
675            symlink(sub, sub.join("link")).unwrap();
676        }
677        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
678            BackendOptions::default(),
679            noop_backing_storage(),
680        ));
681        let path: RcStr = scratch.path().to_str().unwrap().into();
682        tt.run_once(async {
683            let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
684            let err = fs
685                .root()
686                .await?
687                .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
688                .await
689                .expect_err("Should have detected an infinite loop");
690
691            assert_eq!(
692                "'sub/link' is a symlink causes that causes an infinite loop!",
693                format!("{}", err.root_cause())
694            );
695
696            // Same when calling track glob
697            let err = fs
698                .root()
699                .await?
700                .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
701                .await
702                .expect_err("Should have detected an infinite loop");
703
704            assert_eq!(
705                "'sub/link' is a symlink causes that causes an infinite loop!",
706                format!("{}", err.root_cause())
707            );
708
709            anyhow::Ok(())
710        })
711        .await
712        .unwrap();
713    }
714}