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, turbobail};
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!("'{source_path}' is a symlink causes that causes an infinite loop!",)
126        }
127    }
128    Ok(resolved_entry)
129}
130
131/// Traverses all directories that match the given `glob`.
132///
133/// This ensures that the calling task will be invalidated
134/// whenever the directories or contents of the directories change,
135///  but unlike read_glob doesn't accumulate data.
136#[turbo_tasks::function(fs)]
137pub async fn track_glob(
138    directory: FileSystemPath,
139    glob: Vc<Glob>,
140    include_dot_files: bool,
141) -> Result<Vc<Completion>> {
142    track_glob_internal("", directory, glob, include_dot_files).await
143}
144
145#[turbo_tasks::function(fs)]
146async fn track_glob_inner(
147    prefix: RcStr,
148    directory: FileSystemPath,
149    glob: Vc<Glob>,
150    include_dot_files: bool,
151) -> Result<Vc<Completion>> {
152    track_glob_internal(&prefix, directory, glob, include_dot_files).await
153}
154
155async fn track_glob_internal(
156    prefix: &str,
157    directory: FileSystemPath,
158    glob: Vc<Glob>,
159    include_dot_files: bool,
160) -> Result<Vc<Completion>> {
161    let dir = directory.read_dir().await?;
162    let glob_value = glob.await?;
163    let fs = directory.fs().to_resolved().await?;
164    let mut reads = Vec::new();
165    let mut completions = Vec::new();
166    let mut types = Vec::new();
167    match &*dir {
168        DirectoryContent::Entries(entries) => {
169            for (segment, entry) in entries.iter() {
170                if !include_dot_files && segment.starts_with('.') {
171                    continue;
172                }
173                // This is redundant with logic inside of `read_dir` but here we track it separately
174                // so we don't follow symlinks.
175                let entry_path = if prefix.is_empty() {
176                    segment.clone()
177                } else {
178                    format!("{prefix}/{segment}").into()
179                };
180
181                match resolve_symlink_safely(entry.clone()).await? {
182                    DirectoryEntry::Directory(path) => {
183                        if glob_value.can_match_in_directory(&entry_path) {
184                            completions.push(track_glob_inner(
185                                entry_path,
186                                path.clone(),
187                                glob,
188                                include_dot_files,
189                            ));
190                        }
191                    }
192                    DirectoryEntry::File(path) => {
193                        if glob_value.matches(&entry_path) {
194                            reads.push(fs.read(path.clone()))
195                        }
196                    }
197                    DirectoryEntry::Symlink(symlink_path) => turbobail!(
198                        "resolve_symlink_safely() should have resolved all symlinks or returned \
199                         an error, but found unresolved symlink at path: '{entry_path}'. Found \
200                         path: '{symlink_path}'. Please report this as a bug.",
201                    ),
202                    DirectoryEntry::Other(path) => {
203                        if glob_value.matches(&entry_path) {
204                            types.push(path.get_type())
205                        }
206                    }
207                    // The most likely case of this is actually a symlink resolution error, it is
208                    // fine to ignore since the mere act of attempting to resolve it has triggered
209                    // the ncecessary dependencies.  If this file is actually a dependency we should
210                    // get an error in the actual webpack loader when it reads it.
211                    DirectoryEntry::Error(_) => {}
212                }
213            }
214        }
215        DirectoryContent::NotFound => {}
216    }
217    try_join!(
218        reads.iter().try_join(),
219        types.iter().try_join(),
220        completions.iter().try_join()
221    )?;
222    Ok(Completion::new())
223}
224
225#[cfg(test)]
226pub mod tests {
227
228    use std::{
229        collections::HashMap,
230        fs::{File, create_dir},
231        io::prelude::*,
232    };
233
234    use turbo_rcstr::{RcStr, rcstr};
235    use turbo_tasks::{
236        Completion, Effects, OperationVc, ReadRef, Vc, read_strongly_consistent_and_apply_effects,
237        take_effects,
238    };
239    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
240
241    use crate::{
242        DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath,
243        glob::{Glob, GlobOptions},
244    };
245
246    fn symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
247        target: Q,
248        path: P,
249    ) -> std::io::Result<()> {
250        assert!(target.as_ref().is_absolute());
251        let _ = std::fs::remove_dir(&path);
252        let _ = std::fs::remove_file(&path);
253
254        #[cfg(unix)]
255        {
256            std::os::unix::fs::symlink(target, path)
257        }
258        #[cfg(windows)]
259        {
260            let metadata = std::fs::metadata(&target).ok();
261            if metadata.is_none_or(|m| m.is_file()) {
262                std::os::windows::fs::symlink_file(target, path)
263            } else {
264                std::os::windows::fs::junction_point(target, path)
265            }
266        }
267    }
268
269    #[turbo_tasks::function(operation, root)]
270    async fn assert_read_glob_basic_operation(path: RcStr) -> anyhow::Result<()> {
271        let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
272        let root = fs.root().await?;
273        let read_dir = root
274            .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
275            .await
276            .unwrap();
277        assert_eq!(read_dir.results.len(), 2);
278        assert_eq!(
279            read_dir.results.get("foo"),
280            Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
281        );
282        assert_eq!(
283            read_dir.results.get("sub"),
284            Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
285        );
286        assert_eq!(read_dir.inner.len(), 1);
287        let inner = &*read_dir.inner.get("sub").unwrap().await?;
288        assert_eq!(inner.results.len(), 1);
289        assert_eq!(
290            inner.results.get("bar"),
291            Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
292        );
293        assert_eq!(inner.inner.len(), 0);
294
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        assert_eq!(inner.inner.len(), 0);
308
309        Ok(())
310    }
311
312    #[turbo_tasks::function(operation, root)]
313    async fn assert_read_glob_symlinks_operation(path: RcStr) -> anyhow::Result<()> {
314        let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
315        let root = fs.root().await?;
316        // Symlinked files
317        let read_dir = root
318            .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
319            .await
320            .unwrap();
321        assert_eq!(read_dir.results.len(), 0);
322        let inner = &*read_dir.inner.get("sub").unwrap().await?;
323        assert_eq!(
324            inner.results,
325            HashMap::from_iter([
326                (
327                    "link-foo.js".into(),
328                    DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
329                ),
330                (
331                    "link-root.js".into(),
332                    DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
333                ),
334                (
335                    "foo.js".into(),
336                    DirectoryEntry::File(root.join("sub/foo.js")?),
337                ),
338            ])
339        );
340        assert_eq!(inner.inner.len(), 0);
341
342        // A symlinked folder
343        let read_dir = root
344            .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
345            .await
346            .unwrap();
347        assert_eq!(read_dir.results.len(), 0);
348        let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
349        assert_eq!(inner_sub.results.len(), 0);
350        let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
351        assert_eq!(
352            inner_sub_dir.results,
353            HashMap::from_iter([(
354                "index.js".into(),
355                DirectoryEntry::File(root.join("sub/dir/index.js")?),
356            )])
357        );
358        assert_eq!(inner_sub_dir.inner.len(), 0);
359
360        Ok(())
361    }
362
363    #[turbo_tasks::function(operation, root)]
364    async fn assert_dead_symlink_read_glob_operation(path: RcStr) -> anyhow::Result<()> {
365        let fs =
366            Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
367        let root = fs.root().owned().await?;
368        let read_dir = root
369            .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
370            .await?;
371        assert_eq!(read_dir.results.len(), 0);
372        assert_eq!(read_dir.inner.len(), 1);
373        let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
374        assert_eq!(inner_sub.inner.len(), 0);
375        assert_eq!(
376            inner_sub.results,
377            HashMap::from_iter([
378                (
379                    "foo.js".into(),
380                    DirectoryEntry::File(root.join("sub/foo.js")?),
381                ),
382                (
383                    "dead_link.js".into(),
384                    DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
385                )
386            ])
387        );
388
389        Ok(())
390    }
391
392    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
393    async fn read_glob_basic() {
394        let scratch = tempfile::tempdir().unwrap();
395        {
396            // Create a simple directory with 2 files, a subdirectory and a dotfile
397            let path = scratch.path();
398            File::create_new(path.join("foo"))
399                .unwrap()
400                .write_all(b"foo")
401                .unwrap();
402            create_dir(path.join("sub")).unwrap();
403            File::create_new(path.join("sub/bar"))
404                .unwrap()
405                .write_all(b"bar")
406                .unwrap();
407        }
408        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
409            BackendOptions::default(),
410            noop_backing_storage(),
411        ));
412        let path: RcStr = scratch.path().to_str().unwrap().into();
413        tt.run_once(async {
414            assert_read_glob_basic_operation(path)
415                .read_strongly_consistent()
416                .await?;
417
418            anyhow::Ok(())
419        })
420        .await
421        .unwrap();
422    }
423
424    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
425    async fn read_glob_symlinks() {
426        let scratch = tempfile::tempdir().unwrap();
427        {
428            // root.js
429            // sub/foo.js
430            // sub/link-foo.js -> ./foo.js
431            // sub/link-root.js -> ../root.js
432            let path = scratch.path();
433            create_dir(path.join("sub")).unwrap();
434            let foo = path.join("sub/foo.js");
435            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
436            symlink(&foo, path.join("sub/link-foo.js")).unwrap();
437
438            let root = path.join("root.js");
439            File::create_new(&root).unwrap().write_all(b"root").unwrap();
440            symlink(&root, path.join("sub/link-root.js")).unwrap();
441
442            let dir = path.join("dir");
443            create_dir(&dir).unwrap();
444            File::create_new(dir.join("index.js"))
445                .unwrap()
446                .write_all(b"dir index")
447                .unwrap();
448            symlink(&dir, path.join("sub/dir")).unwrap();
449        }
450        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
451            BackendOptions::default(),
452            noop_backing_storage(),
453        ));
454        let path: RcStr = scratch.path().to_str().unwrap().into();
455        tt.run_once(async {
456            assert_read_glob_symlinks_operation(path)
457                .read_strongly_consistent()
458                .await?;
459
460            anyhow::Ok(())
461        })
462        .await
463        .unwrap();
464    }
465
466    #[turbo_tasks::function(operation, root)]
467    pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
468        path.write(FileContent::NotFound.cell()).await?;
469        Ok(())
470    }
471
472    #[turbo_tasks::function(operation, root)]
473    pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
474        path.write(
475            FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
476        )
477        .await?;
478        Ok(())
479    }
480
481    #[turbo_tasks::function(operation, root)]
482    pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
483        path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
484    }
485
486    #[turbo_tasks::function(operation, root)]
487    fn disk_file_system_root_operation(path: RcStr) -> Vc<FileSystemPath> {
488        let fs =
489            Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
490        fs.root()
491    }
492
493    #[turbo_tasks::function(operation, root)]
494    async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
495        let _ = op.resolve().strongly_consistent().await?;
496        Ok(take_effects(op).await?.cell())
497    }
498
499    #[turbo_tasks::function(operation, root)]
500    async fn track_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
501        let root = disk_file_system_root_operation(path)
502            .read_strongly_consistent()
503            .await?;
504        root.track_glob(Glob::new(glob, GlobOptions::default()), false)
505            .await?;
506        Ok(())
507    }
508
509    #[turbo_tasks::function(operation, root)]
510    async fn read_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
511        let root = disk_file_system_root_operation(path)
512            .read_strongly_consistent()
513            .await?;
514        root.read_glob(Glob::new(glob, GlobOptions::default()))
515            .await?;
516        Ok(())
517    }
518
519    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
520    async fn track_glob_invalidations() {
521        let scratch = tempfile::tempdir().unwrap();
522
523        // Create a simple directory with 2 files, a subdirectory and a dotfile
524        let path = scratch.path();
525        let dir = path.join("dir");
526        create_dir(&dir).unwrap();
527        File::create_new(dir.join("foo"))
528            .unwrap()
529            .write_all(b"foo")
530            .unwrap();
531        create_dir(dir.join("sub")).unwrap();
532        File::create_new(dir.join("sub/bar"))
533            .unwrap()
534            .write_all(b"bar")
535            .unwrap();
536        // Add a dotfile
537        create_dir(dir.join("sub/.vim")).unwrap();
538        let gitignore = dir.join("sub/.vim/.gitignore");
539        File::create_new(&gitignore)
540            .unwrap()
541            .write_all(b"ignore")
542            .unwrap();
543        // put a link in the dir that points at a file in the root.
544        let link_target = path.join("link_target.js");
545        File::create_new(&link_target)
546            .unwrap()
547            .write_all(b"link_target")
548            .unwrap();
549        symlink(&link_target, dir.join("link.js")).unwrap();
550
551        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
552            BackendOptions::default(),
553            noop_backing_storage(),
554        ));
555        let path: RcStr = scratch.path().to_str().unwrap().into();
556        tt.run_once(async {
557            let root = disk_file_system_root_operation(path)
558                .read_strongly_consistent()
559                .await?;
560            let dir = root.join("dir")?;
561            let read_dir = track_star_star_glob(dir.clone())
562                .read_strongly_consistent()
563                .await?;
564
565            // Delete a file that we shouldn't be tracking
566            read_strongly_consistent_and_apply_effects(
567                extract_effects_operation(delete(root.join("dir/sub/.vim/.gitignore")?)),
568                |e| e,
569            )
570            .await?;
571
572            let read_dir2 = track_star_star_glob(dir.clone())
573                .read_strongly_consistent()
574                .await?;
575            assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
576
577            // Delete a file that we should be tracking
578            read_strongly_consistent_and_apply_effects(
579                extract_effects_operation(delete(root.join("dir/foo")?)),
580                |e| e,
581            )
582            .await?;
583
584            let read_dir2 = track_star_star_glob(dir.clone())
585                .read_strongly_consistent()
586                .await?;
587
588            assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
589
590            // Modify a symlink target file
591            read_strongly_consistent_and_apply_effects(
592                extract_effects_operation(write(
593                    root.join("link_target.js")?,
594                    rcstr!("new_contents"),
595                )),
596                |e| e,
597            )
598            .await?;
599            let read_dir3 = track_star_star_glob(dir.clone())
600                .read_strongly_consistent()
601                .await?;
602
603            assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
604
605            anyhow::Ok(())
606        })
607        .await
608        .unwrap();
609    }
610
611    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
612    async fn track_glob_symlinks_loop() {
613        let scratch = tempfile::tempdir().unwrap();
614        {
615            // Create a simple directory with 1 file and a symlink pointing at at a file in a
616            // subdirectory
617            let path = scratch.path();
618            let sub = &path.join("sub");
619            create_dir(sub).unwrap();
620            let foo = sub.join("foo.js");
621            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
622            // put a link in sub that points back at its parent director
623            symlink(sub, sub.join("link")).unwrap();
624        }
625        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
626            BackendOptions::default(),
627            noop_backing_storage(),
628        ));
629        let path: RcStr = scratch.path().to_str().unwrap().into();
630        tt.run_once(async {
631            let err = track_glob_operation(path.clone(), rcstr!("**"))
632                .read_strongly_consistent()
633                .await
634                .expect_err("Should have detected an infinite loop");
635
636            assert_eq!(
637                "'sub/link' is a symlink causes that causes an infinite loop!",
638                format!("{}", err.root_cause())
639            );
640
641            // Same when calling track glob
642            let err = track_glob_operation(path, rcstr!("**"))
643                .read_strongly_consistent()
644                .await
645                .expect_err("Should have detected an infinite loop");
646
647            assert_eq!(
648                "'sub/link' is a symlink causes that causes an infinite loop!",
649                format!("{}", err.root_cause())
650            );
651
652            anyhow::Ok(())
653        })
654        .await
655        .unwrap();
656    }
657
658    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
659    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
660    async fn dead_symlinks() {
661        let scratch = tempfile::tempdir().unwrap();
662        {
663            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
664            let path = scratch.path();
665            let sub = &path.join("sub");
666            create_dir(sub).unwrap();
667            let foo = sub.join("foo.js");
668            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
669            // put a link in sub that points to a sibling file that doesn't exist
670            symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
671        }
672        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
673            BackendOptions::default(),
674            noop_backing_storage(),
675        ));
676        let path: RcStr = scratch.path().to_str().unwrap().into();
677        tt.run_once(async {
678            track_glob_operation(path, rcstr!("sub/*.js"))
679                .read_strongly_consistent()
680                .await?;
681            anyhow::Ok(())
682        })
683        .await
684        .unwrap();
685        let path: RcStr = scratch.path().to_str().unwrap().into();
686        tt.run_once(async {
687            assert_dead_symlink_read_glob_operation(path)
688                .read_strongly_consistent()
689                .await?;
690            anyhow::Ok(())
691        })
692        .await
693        .unwrap();
694    }
695
696    // Reproduces an issue where a dead symlink would cause a panic when tracking/reading a glob
697    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
698    async fn symlink_escapes_fs_root() {
699        let scratch = tempfile::tempdir().unwrap();
700        {
701            // Create a simple directory with 1 file and a symlink pointing at a non-existent file
702            let path = scratch.path();
703            let sub = &path.join("sub");
704            create_dir(sub).unwrap();
705            let foo = scratch.path().join("foo.js");
706            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
707            // put a link in sub that points to a parent file
708            symlink(foo, sub.join("escape.js")).unwrap();
709        }
710        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
711            BackendOptions::default(),
712            noop_backing_storage(),
713        ));
714        let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
715        tt.run_once(async {
716            track_glob_operation(root, rcstr!("*.js"))
717                .read_strongly_consistent()
718                .await?;
719            anyhow::Ok(())
720        })
721        .await
722        .unwrap();
723    }
724
725    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
726    async fn read_glob_symlinks_loop() {
727        let scratch = tempfile::tempdir().unwrap();
728        {
729            // Create a simple directory with 1 file and a symlink pointing at at a file in a
730            // subdirectory
731            let path = scratch.path();
732            let sub = &path.join("sub");
733            create_dir(sub).unwrap();
734            let foo = sub.join("foo.js");
735            File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
736            // put a link in sub that points back at its parent director
737            symlink(sub, sub.join("link")).unwrap();
738        }
739        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
740            BackendOptions::default(),
741            noop_backing_storage(),
742        ));
743        let path: RcStr = scratch.path().to_str().unwrap().into();
744        tt.run_once(async {
745            let err = read_glob_operation(path.clone(), rcstr!("**"))
746                .read_strongly_consistent()
747                .await
748                .expect_err("Should have detected an infinite loop");
749
750            assert_eq!(
751                "'sub/link' is a symlink causes that causes an infinite loop!",
752                format!("{}", err.root_cause())
753            );
754
755            // Same when calling track glob
756            let err = track_glob_operation(path, rcstr!("**"))
757                .read_strongly_consistent()
758                .await
759                .expect_err("Should have detected an infinite loop");
760
761            assert_eq!(
762                "'sub/link' is a symlink causes that causes an infinite loop!",
763                format!("{}", err.root_cause())
764            );
765
766            anyhow::Ok(())
767        })
768        .await
769        .unwrap();
770    }
771}