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