Skip to main content

turbo_tasks_fs/
read_glob.rs

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