1use anyhow::{Result, bail};
2use futures::try_join;
3use rustc_hash::FxHashMap;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{Completion, ResolvedVc, TryJoinIterExt, Vc};
6
7use crate::{
8 DirectoryContent, DirectoryEntry, FileSystem, FileSystemPath, LinkContent, LinkType, glob::Glob,
9};
10
11#[turbo_tasks::value]
12#[derive(Default, Debug)]
13pub struct ReadGlobResult {
14 pub results: FxHashMap<RcStr, DirectoryEntry>,
15 pub inner: FxHashMap<RcStr, ResolvedVc<ReadGlobResult>>,
16}
17
18#[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
36async 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 handle_file(&mut result, &entry_path, segment, entry);
84 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 resolve_symlink_safely(entry.clone()).await?;
92
93 handle_file(&mut result, &entry_path, segment, entry);
95 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
111async 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 let source_path = entry.path().unwrap();
124 if source_path.is_inside_or_equal(&resolved_entry.clone().path().unwrap()) {
125 bail!(
126 "'{}' is a symlink causes that causes an infinite loop!",
127 source_path.path.to_string()
128 )
129 }
130 }
131 Ok(resolved_entry)
132}
133
134#[turbo_tasks::function(fs)]
140pub async fn track_glob(
141 directory: FileSystemPath,
142 glob: Vc<Glob>,
143 include_dot_files: bool,
144) -> Result<Vc<Completion>> {
145 track_glob_internal("", directory, glob, include_dot_files).await
146}
147
148#[turbo_tasks::function(fs)]
149async fn track_glob_inner(
150 prefix: RcStr,
151 directory: FileSystemPath,
152 glob: Vc<Glob>,
153 include_dot_files: bool,
154) -> Result<Vc<Completion>> {
155 track_glob_internal(&prefix, directory, glob, include_dot_files).await
156}
157
158async fn track_glob_internal(
159 prefix: &str,
160 directory: FileSystemPath,
161 glob: Vc<Glob>,
162 include_dot_files: bool,
163) -> Result<Vc<Completion>> {
164 let dir = directory.read_dir().await?;
165 let glob_value = glob.await?;
166 let fs = directory.fs().to_resolved().await?;
167 let mut reads = Vec::new();
168 let mut completions = Vec::new();
169 let mut types = Vec::new();
170 match &*dir {
171 DirectoryContent::Entries(entries) => {
172 for (segment, entry) in entries.iter() {
173 if !include_dot_files && segment.starts_with('.') {
174 continue;
175 }
176 let entry_path = if prefix.is_empty() {
179 segment.clone()
180 } else {
181 format!("{prefix}/{segment}").into()
182 };
183
184 match resolve_symlink_safely(entry.clone()).await? {
185 DirectoryEntry::Directory(path) => {
186 if glob_value.can_match_in_directory(&entry_path) {
187 completions.push(track_glob_inner(
188 entry_path,
189 path.clone(),
190 glob,
191 include_dot_files,
192 ));
193 }
194 }
195 DirectoryEntry::File(path) => {
196 if glob_value.matches(&entry_path) {
197 reads.push(fs.read(path.clone()))
198 }
199 }
200 DirectoryEntry::Symlink(symlink_path) => bail!(
201 "resolve_symlink_safely() should have resolved all symlinks or returned \
202 an error, but found unresolved symlink at path: '{}'. Found path: '{}'. \
203 Please report this as a bug.",
204 entry_path,
205 symlink_path
206 ),
207 DirectoryEntry::Other(path) => {
208 if glob_value.matches(&entry_path) {
209 types.push(path.get_type())
210 }
211 }
212 DirectoryEntry::Error(_) => {}
217 }
218 }
219 }
220 DirectoryContent::NotFound => {}
221 }
222 try_join!(
223 reads.iter().try_join(),
224 types.iter().try_join(),
225 completions.iter().try_join()
226 )?;
227 Ok(Completion::new())
228}
229
230#[cfg(test)]
231pub mod tests {
232
233 use std::{
234 collections::HashMap,
235 fs::{File, create_dir},
236 io::prelude::*,
237 };
238
239 use turbo_rcstr::{RcStr, rcstr};
240 use turbo_tasks::{Completion, ReadRef, Vc, apply_effects};
241 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
242
243 use crate::{
244 DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath,
245 glob::{Glob, GlobOptions},
246 };
247
248 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
249 async fn read_glob_basic() {
250 let scratch = tempfile::tempdir().unwrap();
251 {
252 let path = scratch.path();
254 File::create_new(path.join("foo"))
255 .unwrap()
256 .write_all(b"foo")
257 .unwrap();
258 create_dir(path.join("sub")).unwrap();
259 File::create_new(path.join("sub/bar"))
260 .unwrap()
261 .write_all(b"bar")
262 .unwrap();
263 }
264 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
265 BackendOptions::default(),
266 noop_backing_storage(),
267 ));
268 let path: RcStr = scratch.path().to_str().unwrap().into();
269 tt.run_once(async {
270 let fs = DiskFileSystem::new(rcstr!("temp"), path);
271 let root = fs.root().await?;
272 let read_dir = root
273 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
274 .await
275 .unwrap();
276 assert_eq!(read_dir.results.len(), 2);
277 assert_eq!(
278 read_dir.results.get("foo"),
279 Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
280 );
281 assert_eq!(
282 read_dir.results.get("sub"),
283 Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
284 );
285 assert_eq!(read_dir.inner.len(), 1);
286 let inner = &*read_dir.inner.get("sub").unwrap().await?;
287 assert_eq!(inner.results.len(), 1);
288 assert_eq!(
289 inner.results.get("bar"),
290 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
291 );
292 assert_eq!(inner.inner.len(), 0);
293
294 let read_dir = root
296 .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
297 .await
298 .unwrap();
299 assert_eq!(read_dir.results.len(), 0);
300 assert_eq!(read_dir.inner.len(), 1);
301 let inner = &*read_dir.inner.get("sub").unwrap().await?;
302 assert_eq!(inner.results.len(), 1);
303 assert_eq!(
304 inner.results.get("bar"),
305 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
306 );
307
308 assert_eq!(inner.inner.len(), 0);
309
310 anyhow::Ok(())
311 })
312 .await
313 .unwrap();
314 }
315
316 #[cfg(unix)]
317 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
318 async fn read_glob_symlinks() {
319 use std::os::unix::fs::symlink;
320
321 let scratch = tempfile::tempdir().unwrap();
322 {
323 let path = scratch.path();
328 create_dir(path.join("sub")).unwrap();
329 let foo = path.join("sub/foo.js");
330 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
331 symlink(&foo, path.join("sub/link-foo.js")).unwrap();
332
333 let root = path.join("root.js");
334 File::create_new(&root).unwrap().write_all(b"root").unwrap();
335 symlink(&root, path.join("sub/link-root.js")).unwrap();
336
337 let dir = path.join("dir");
338 create_dir(&dir).unwrap();
339 File::create_new(dir.join("index.js"))
340 .unwrap()
341 .write_all(b"dir index")
342 .unwrap();
343 symlink(&dir, path.join("sub/dir")).unwrap();
344 }
345 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
346 BackendOptions::default(),
347 noop_backing_storage(),
348 ));
349 let path: RcStr = scratch.path().to_str().unwrap().into();
350 tt.run_once(async {
351 let fs = DiskFileSystem::new(rcstr!("temp"), path);
352 let root = fs.root().await?;
353 let read_dir = root
355 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
356 .await
357 .unwrap();
358 assert_eq!(read_dir.results.len(), 0);
359 let inner = &*read_dir.inner.get("sub").unwrap().await?;
360 assert_eq!(
361 inner.results,
362 HashMap::from_iter([
363 (
364 "link-foo.js".into(),
365 DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
366 ),
367 (
368 "link-root.js".into(),
369 DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
370 ),
371 (
372 "foo.js".into(),
373 DirectoryEntry::File(root.join("sub/foo.js")?),
374 ),
375 ])
376 );
377 assert_eq!(inner.inner.len(), 0);
378
379 let read_dir = root
381 .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
382 .await
383 .unwrap();
384 assert_eq!(read_dir.results.len(), 0);
385 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
386 assert_eq!(inner_sub.results.len(), 0);
387 let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
388 assert_eq!(
389 inner_sub_dir.results,
390 HashMap::from_iter([(
391 "index.js".into(),
392 DirectoryEntry::File(root.join("sub/dir/index.js")?),
393 )])
394 );
395 assert_eq!(inner_sub_dir.inner.len(), 0);
396
397 anyhow::Ok(())
398 })
399 .await
400 .unwrap();
401 }
402
403 #[turbo_tasks::function(operation)]
404 pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
405 path.write(FileContent::NotFound.cell()).await?;
406 Ok(())
407 }
408
409 #[turbo_tasks::function(operation)]
410 pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
411 path.write(
412 FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
413 )
414 .await?;
415 Ok(())
416 }
417
418 #[turbo_tasks::function(operation)]
419 pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
420 path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
421 }
422
423 #[cfg(unix)]
424 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
425 async fn track_glob_invalidations() {
426 use std::os::unix::fs::symlink;
427 let scratch = tempfile::tempdir().unwrap();
428
429 let path = scratch.path();
431 let dir = path.join("dir");
432 create_dir(&dir).unwrap();
433 File::create_new(dir.join("foo"))
434 .unwrap()
435 .write_all(b"foo")
436 .unwrap();
437 create_dir(dir.join("sub")).unwrap();
438 File::create_new(dir.join("sub/bar"))
439 .unwrap()
440 .write_all(b"bar")
441 .unwrap();
442 create_dir(dir.join("sub/.vim")).unwrap();
444 let gitignore = dir.join("sub/.vim/.gitignore");
445 File::create_new(&gitignore)
446 .unwrap()
447 .write_all(b"ignore")
448 .unwrap();
449 let link_target = path.join("link_target.js");
451 File::create_new(&link_target)
452 .unwrap()
453 .write_all(b"link_target")
454 .unwrap();
455 symlink(&link_target, dir.join("link.js")).unwrap();
456
457 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
458 BackendOptions::default(),
459 noop_backing_storage(),
460 ));
461 let path: RcStr = scratch.path().to_str().unwrap().into();
462 tt.run_once(async {
463 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
464 let dir = fs.root().await?.join("dir")?;
465 let read_dir = track_star_star_glob(dir.clone())
466 .read_strongly_consistent()
467 .await?;
468
469 let delete_result = delete(fs.root().await?.join("dir/sub/.vim/.gitignore")?);
471 delete_result.read_strongly_consistent().await?;
472 apply_effects(delete_result).await?;
473
474 let read_dir2 = track_star_star_glob(dir.clone())
475 .read_strongly_consistent()
476 .await?;
477 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
478
479 let delete_result = delete(fs.root().await?.join("dir/foo")?);
481 delete_result.read_strongly_consistent().await?;
482 apply_effects(delete_result).await?;
483
484 let read_dir2 = track_star_star_glob(dir.clone())
485 .read_strongly_consistent()
486 .await?;
487
488 assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
489
490 let write_result = write(
492 fs.root().await?.join("link_target.js")?,
493 rcstr!("new_contents"),
494 );
495 write_result.read_strongly_consistent().await?;
496 apply_effects(write_result).await?;
497 let read_dir3 = track_star_star_glob(dir.clone())
498 .read_strongly_consistent()
499 .await?;
500
501 assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
502
503 anyhow::Ok(())
504 })
505 .await
506 .unwrap();
507 }
508
509 #[cfg(unix)]
510 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
511 async fn track_glob_symlinks_loop() {
512 let scratch = tempfile::tempdir().unwrap();
513 {
514 use std::os::unix::fs::symlink;
515
516 let path = scratch.path();
519 let sub = &path.join("sub");
520 create_dir(sub).unwrap();
521 let foo = sub.join("foo.js");
522 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
523 symlink(sub, sub.join("link")).unwrap();
525 }
526 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
527 BackendOptions::default(),
528 noop_backing_storage(),
529 ));
530 let path: RcStr = scratch.path().to_str().unwrap().into();
531 tt.run_once(async {
532 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
533 let err = fs
534 .root()
535 .await?
536 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
537 .await
538 .expect_err("Should have detected an infinite loop");
539
540 assert_eq!(
541 "'sub/link' is a symlink causes that causes an infinite loop!",
542 format!("{}", err.root_cause())
543 );
544
545 let err = fs
547 .root()
548 .await?
549 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
550 .await
551 .expect_err("Should have detected an infinite loop");
552
553 assert_eq!(
554 "'sub/link' is a symlink causes that causes an infinite loop!",
555 format!("{}", err.root_cause())
556 );
557
558 anyhow::Ok(())
559 })
560 .await
561 .unwrap();
562 }
563
564 #[cfg(unix)]
566 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
567 async fn dead_symlinks() {
568 let scratch = tempfile::tempdir().unwrap();
569 {
570 use std::os::unix::fs::symlink;
571
572 let path = scratch.path();
574 let sub = &path.join("sub");
575 create_dir(sub).unwrap();
576 let foo = sub.join("foo.js");
577 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
578 symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
580 }
581 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
582 BackendOptions::default(),
583 noop_backing_storage(),
584 ));
585 let path: RcStr = scratch.path().to_str().unwrap().into();
586 tt.run_once(async {
587 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
588 fs.root()
589 .await?
590 .track_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()), false)
591 .await
592 })
593 .await
594 .unwrap();
595 let path: RcStr = scratch.path().to_str().unwrap().into();
596 tt.run_once(async {
597 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
598 let root = fs.root().owned().await?;
599 let read_dir = root
600 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
601 .await?;
602 assert_eq!(read_dir.results.len(), 0);
603 assert_eq!(read_dir.inner.len(), 1);
604 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
605 assert_eq!(inner_sub.inner.len(), 0);
606 assert_eq!(
607 inner_sub.results,
608 HashMap::from_iter([
609 (
610 "foo.js".into(),
611 DirectoryEntry::File(root.join("sub/foo.js")?),
612 ),
613 (
615 "dead_link.js".into(),
616 DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
617 )
618 ])
619 );
620
621 anyhow::Ok(())
622 })
623 .await
624 .unwrap();
625 }
626
627 #[cfg(unix)]
629 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
630 async fn symlink_escapes_fs_root() {
631 let scratch = tempfile::tempdir().unwrap();
632 {
633 use std::os::unix::fs::symlink;
634
635 let path = scratch.path();
637 let sub = &path.join("sub");
638 create_dir(sub).unwrap();
639 let foo = scratch.path().join("foo.js");
640 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
641 symlink(foo, sub.join("escape.js")).unwrap();
643 }
644 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
645 BackendOptions::default(),
646 noop_backing_storage(),
647 ));
648 let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
649 tt.run_once(async {
650 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), root));
651 fs.root()
652 .await?
653 .track_glob(Glob::new(rcstr!("*.js"), GlobOptions::default()), false)
654 .await
655 })
656 .await
657 .unwrap();
658 }
659
660 #[cfg(unix)]
661 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
662 async fn read_glob_symlinks_loop() {
663 let scratch = tempfile::tempdir().unwrap();
664 {
665 use std::os::unix::fs::symlink;
666
667 let path = scratch.path();
670 let sub = &path.join("sub");
671 create_dir(sub).unwrap();
672 let foo = sub.join("foo.js");
673 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
674 symlink(sub, sub.join("link")).unwrap();
676 }
677 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
678 BackendOptions::default(),
679 noop_backing_storage(),
680 ));
681 let path: RcStr = scratch.path().to_str().unwrap().into();
682 tt.run_once(async {
683 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
684 let err = fs
685 .root()
686 .await?
687 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
688 .await
689 .expect_err("Should have detected an infinite loop");
690
691 assert_eq!(
692 "'sub/link' is a symlink causes that causes an infinite loop!",
693 format!("{}", err.root_cause())
694 );
695
696 let err = fs
698 .root()
699 .await?
700 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
701 .await
702 .expect_err("Should have detected an infinite loop");
703
704 assert_eq!(
705 "'sub/link' is a symlink causes that causes an infinite loop!",
706 format!("{}", err.root_cause())
707 );
708
709 anyhow::Ok(())
710 })
711 .await
712 .unwrap();
713 }
714}