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