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,
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 fn symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
249 target: Q,
250 path: P,
251 ) -> std::io::Result<()> {
252 assert!(target.as_ref().is_absolute());
253 let _ = std::fs::remove_dir(&path);
254 let _ = std::fs::remove_file(&path);
255
256 #[cfg(unix)]
257 {
258 std::os::unix::fs::symlink(target, path)
259 }
260 #[cfg(windows)]
261 {
262 let metadata = std::fs::metadata(&target).ok();
263 if metadata.is_none_or(|m| m.is_file()) {
264 std::os::windows::fs::symlink_file(target, path)
265 } else {
266 std::os::windows::fs::junction_point(target, path)
267 }
268 }
269 }
270
271 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
272 async fn read_glob_basic() {
273 let scratch = tempfile::tempdir().unwrap();
274 {
275 let path = scratch.path();
277 File::create_new(path.join("foo"))
278 .unwrap()
279 .write_all(b"foo")
280 .unwrap();
281 create_dir(path.join("sub")).unwrap();
282 File::create_new(path.join("sub/bar"))
283 .unwrap()
284 .write_all(b"bar")
285 .unwrap();
286 }
287 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
288 BackendOptions::default(),
289 noop_backing_storage(),
290 ));
291 let path: RcStr = scratch.path().to_str().unwrap().into();
292 tt.run_once(async {
293 let fs = DiskFileSystem::new(rcstr!("temp"), path);
294 let root = fs.root().await?;
295 let read_dir = root
296 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
297 .await
298 .unwrap();
299 assert_eq!(read_dir.results.len(), 2);
300 assert_eq!(
301 read_dir.results.get("foo"),
302 Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
303 );
304 assert_eq!(
305 read_dir.results.get("sub"),
306 Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
307 );
308 assert_eq!(read_dir.inner.len(), 1);
309 let inner = &*read_dir.inner.get("sub").unwrap().await?;
310 assert_eq!(inner.results.len(), 1);
311 assert_eq!(
312 inner.results.get("bar"),
313 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
314 );
315 assert_eq!(inner.inner.len(), 0);
316
317 let read_dir = root
319 .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
320 .await
321 .unwrap();
322 assert_eq!(read_dir.results.len(), 0);
323 assert_eq!(read_dir.inner.len(), 1);
324 let inner = &*read_dir.inner.get("sub").unwrap().await?;
325 assert_eq!(inner.results.len(), 1);
326 assert_eq!(
327 inner.results.get("bar"),
328 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
329 );
330
331 assert_eq!(inner.inner.len(), 0);
332
333 anyhow::Ok(())
334 })
335 .await
336 .unwrap();
337 }
338
339 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
340 async fn read_glob_symlinks() {
341 let scratch = tempfile::tempdir().unwrap();
342 {
343 let path = scratch.path();
348 create_dir(path.join("sub")).unwrap();
349 let foo = path.join("sub/foo.js");
350 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
351 symlink(&foo, path.join("sub/link-foo.js")).unwrap();
352
353 let root = path.join("root.js");
354 File::create_new(&root).unwrap().write_all(b"root").unwrap();
355 symlink(&root, path.join("sub/link-root.js")).unwrap();
356
357 let dir = path.join("dir");
358 create_dir(&dir).unwrap();
359 File::create_new(dir.join("index.js"))
360 .unwrap()
361 .write_all(b"dir index")
362 .unwrap();
363 symlink(&dir, path.join("sub/dir")).unwrap();
364 }
365 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
366 BackendOptions::default(),
367 noop_backing_storage(),
368 ));
369 let path: RcStr = scratch.path().to_str().unwrap().into();
370 tt.run_once(async {
371 let fs = DiskFileSystem::new(rcstr!("temp"), path);
372 let root = fs.root().await?;
373 let read_dir = root
375 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
376 .await
377 .unwrap();
378 assert_eq!(read_dir.results.len(), 0);
379 let inner = &*read_dir.inner.get("sub").unwrap().await?;
380 assert_eq!(
381 inner.results,
382 HashMap::from_iter([
383 (
384 "link-foo.js".into(),
385 DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
386 ),
387 (
388 "link-root.js".into(),
389 DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
390 ),
391 (
392 "foo.js".into(),
393 DirectoryEntry::File(root.join("sub/foo.js")?),
394 ),
395 ])
396 );
397 assert_eq!(inner.inner.len(), 0);
398
399 let read_dir = root
401 .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
402 .await
403 .unwrap();
404 assert_eq!(read_dir.results.len(), 0);
405 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
406 assert_eq!(inner_sub.results.len(), 0);
407 let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
408 assert_eq!(
409 inner_sub_dir.results,
410 HashMap::from_iter([(
411 "index.js".into(),
412 DirectoryEntry::File(root.join("sub/dir/index.js")?),
413 )])
414 );
415 assert_eq!(inner_sub_dir.inner.len(), 0);
416
417 anyhow::Ok(())
418 })
419 .await
420 .unwrap();
421 }
422
423 #[turbo_tasks::function(operation)]
424 pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
425 path.write(FileContent::NotFound.cell()).await?;
426 Ok(())
427 }
428
429 #[turbo_tasks::function(operation)]
430 pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
431 path.write(
432 FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
433 )
434 .await?;
435 Ok(())
436 }
437
438 #[turbo_tasks::function(operation)]
439 pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
440 path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
441 }
442
443 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
444 async fn track_glob_invalidations() {
445 let scratch = tempfile::tempdir().unwrap();
446
447 let path = scratch.path();
449 let dir = path.join("dir");
450 create_dir(&dir).unwrap();
451 File::create_new(dir.join("foo"))
452 .unwrap()
453 .write_all(b"foo")
454 .unwrap();
455 create_dir(dir.join("sub")).unwrap();
456 File::create_new(dir.join("sub/bar"))
457 .unwrap()
458 .write_all(b"bar")
459 .unwrap();
460 create_dir(dir.join("sub/.vim")).unwrap();
462 let gitignore = dir.join("sub/.vim/.gitignore");
463 File::create_new(&gitignore)
464 .unwrap()
465 .write_all(b"ignore")
466 .unwrap();
467 let link_target = path.join("link_target.js");
469 File::create_new(&link_target)
470 .unwrap()
471 .write_all(b"link_target")
472 .unwrap();
473 symlink(&link_target, dir.join("link.js")).unwrap();
474
475 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
476 BackendOptions::default(),
477 noop_backing_storage(),
478 ));
479 let path: RcStr = scratch.path().to_str().unwrap().into();
480 tt.run_once(async {
481 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
482 let dir = fs.root().await?.join("dir")?;
483 let read_dir = track_star_star_glob(dir.clone())
484 .read_strongly_consistent()
485 .await?;
486
487 let delete_result = delete(fs.root().await?.join("dir/sub/.vim/.gitignore")?);
489 delete_result.read_strongly_consistent().await?;
490 apply_effects(delete_result).await?;
491
492 let read_dir2 = track_star_star_glob(dir.clone())
493 .read_strongly_consistent()
494 .await?;
495 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
496
497 let delete_result = delete(fs.root().await?.join("dir/foo")?);
499 delete_result.read_strongly_consistent().await?;
500 apply_effects(delete_result).await?;
501
502 let read_dir2 = track_star_star_glob(dir.clone())
503 .read_strongly_consistent()
504 .await?;
505
506 assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
507
508 let write_result = write(
510 fs.root().await?.join("link_target.js")?,
511 rcstr!("new_contents"),
512 );
513 write_result.read_strongly_consistent().await?;
514 apply_effects(write_result).await?;
515 let read_dir3 = track_star_star_glob(dir.clone())
516 .read_strongly_consistent()
517 .await?;
518
519 assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
520
521 anyhow::Ok(())
522 })
523 .await
524 .unwrap();
525 }
526
527 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
528 async fn track_glob_symlinks_loop() {
529 let scratch = tempfile::tempdir().unwrap();
530 {
531 let path = scratch.path();
534 let sub = &path.join("sub");
535 create_dir(sub).unwrap();
536 let foo = sub.join("foo.js");
537 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
538 symlink(sub, sub.join("link")).unwrap();
540 }
541 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
542 BackendOptions::default(),
543 noop_backing_storage(),
544 ));
545 let path: RcStr = scratch.path().to_str().unwrap().into();
546 tt.run_once(async {
547 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
548 let err = fs
549 .root()
550 .await?
551 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
552 .await
553 .expect_err("Should have detected an infinite loop");
554
555 assert_eq!(
556 "'sub/link' is a symlink causes that causes an infinite loop!",
557 format!("{}", err.root_cause())
558 );
559
560 let err = fs
562 .root()
563 .await?
564 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
565 .await
566 .expect_err("Should have detected an infinite loop");
567
568 assert_eq!(
569 "'sub/link' is a symlink causes that causes an infinite loop!",
570 format!("{}", err.root_cause())
571 );
572
573 anyhow::Ok(())
574 })
575 .await
576 .unwrap();
577 }
578
579 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
581 async fn dead_symlinks() {
582 let scratch = tempfile::tempdir().unwrap();
583 {
584 let path = scratch.path();
586 let sub = &path.join("sub");
587 create_dir(sub).unwrap();
588 let foo = sub.join("foo.js");
589 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
590 symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
592 }
593 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
594 BackendOptions::default(),
595 noop_backing_storage(),
596 ));
597 let path: RcStr = scratch.path().to_str().unwrap().into();
598 tt.run_once(async {
599 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
600 fs.root()
601 .await?
602 .track_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()), false)
603 .await
604 })
605 .await
606 .unwrap();
607 let path: RcStr = scratch.path().to_str().unwrap().into();
608 tt.run_once(async {
609 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
610 let root = fs.root().owned().await?;
611 let read_dir = root
612 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
613 .await?;
614 assert_eq!(read_dir.results.len(), 0);
615 assert_eq!(read_dir.inner.len(), 1);
616 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
617 assert_eq!(inner_sub.inner.len(), 0);
618 assert_eq!(
619 inner_sub.results,
620 HashMap::from_iter([
621 (
622 "foo.js".into(),
623 DirectoryEntry::File(root.join("sub/foo.js")?),
624 ),
625 (
627 "dead_link.js".into(),
628 DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
629 )
630 ])
631 );
632
633 anyhow::Ok(())
634 })
635 .await
636 .unwrap();
637 }
638
639 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
641 async fn symlink_escapes_fs_root() {
642 let scratch = tempfile::tempdir().unwrap();
643 {
644 let path = scratch.path();
646 let sub = &path.join("sub");
647 create_dir(sub).unwrap();
648 let foo = scratch.path().join("foo.js");
649 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
650 symlink(foo, sub.join("escape.js")).unwrap();
652 }
653 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
654 BackendOptions::default(),
655 noop_backing_storage(),
656 ));
657 let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
658 tt.run_once(async {
659 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), root));
660 fs.root()
661 .await?
662 .track_glob(Glob::new(rcstr!("*.js"), GlobOptions::default()), false)
663 .await
664 })
665 .await
666 .unwrap();
667 }
668
669 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
670 async fn read_glob_symlinks_loop() {
671 let scratch = tempfile::tempdir().unwrap();
672 {
673 let path = scratch.path();
676 let sub = &path.join("sub");
677 create_dir(sub).unwrap();
678 let foo = sub.join("foo.js");
679 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
680 symlink(sub, sub.join("link")).unwrap();
682 }
683 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
684 BackendOptions::default(),
685 noop_backing_storage(),
686 ));
687 let path: RcStr = scratch.path().to_str().unwrap().into();
688 tt.run_once(async {
689 let fs = Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), path));
690 let err = fs
691 .root()
692 .await?
693 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
694 .await
695 .expect_err("Should have detected an infinite loop");
696
697 assert_eq!(
698 "'sub/link' is a symlink causes that causes an infinite loop!",
699 format!("{}", err.root_cause())
700 );
701
702 let err = fs
704 .root()
705 .await?
706 .track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
707 .await
708 .expect_err("Should have detected an infinite loop");
709
710 assert_eq!(
711 "'sub/link' is a symlink causes that causes an infinite loop!",
712 format!("{}", err.root_cause())
713 );
714
715 anyhow::Ok(())
716 })
717 .await
718 .unwrap();
719 }
720}