1use std::{
2 ops::Bound,
3 path::{Path, PathBuf},
4};
5
6use turbo_frozenmap::FrozenMap;
7use turbo_rcstr::RcStr;
8use turbo_tasks::{OperationVc, ResolvedVc, Vc};
9use turbo_unix_path::sys_to_unix;
10
11use crate::{DiskFileSystem, FileSystemPath, canonicalized_path_cache::CanonicalizedPathWalkCache};
12
13#[turbo_tasks::value(shared, eq = "manual")]
18pub struct DiskFileSystemMap {
19 roots: FrozenMap<PathBuf, ResolvedVc<DiskFileSystem>>,
20 #[turbo_tasks(debug_ignore, unsafe_ignore)]
21 #[bincode(skip)]
22 pub(crate) canonicalized_paths: CanonicalizedPathWalkCache,
23}
24
25impl PartialEq for DiskFileSystemMap {
26 fn eq(&self, other: &Self) -> bool {
27 self.roots == other.roots
28 }
29}
30
31impl Eq for DiskFileSystemMap {}
32
33impl FromIterator<(PathBuf, ResolvedVc<DiskFileSystem>)> for DiskFileSystemMap {
34 fn from_iter<T: IntoIterator<Item = (PathBuf, ResolvedVc<DiskFileSystem>)>>(iter: T) -> Self {
35 let filesystems = FrozenMap::from_iter(iter);
36 for pair in filesystems.as_slice().windows(2) {
37 let (previous_root, _) = &pair[0];
38 let (root, _) = &pair[1];
39 assert!(
40 !root.starts_with(previous_root),
41 "filesystem root {} overlaps another filesystem root",
42 root.display()
43 );
44 }
45 DiskFileSystemMap {
46 roots: filesystems,
47 canonicalized_paths: Default::default(),
48 }
49 }
50}
51
52impl DiskFileSystemMap {
53 pub(crate) fn contains(&self, root: &Path, current: ResolvedVc<DiskFileSystem>) -> bool {
54 self.roots.get(root).is_some_and(|fs| *fs == current)
55 }
56
57 pub fn len(&self) -> usize {
58 self.roots.len()
59 }
60
61 pub fn is_empty(&self) -> bool {
62 self.roots.is_empty()
63 }
64
65 pub(crate) fn lookup_sys_path_suffix<'a>(
67 &self,
68 path: &'a Path,
69 ) -> Option<LookupSysPathSuffix<'a>> {
70 let (root, fs) = self
71 .roots
72 .range::<Path, _>((Bound::Unbounded, Bound::Included(path)))
73 .next_back()?;
74 Some(LookupSysPathSuffix {
75 fs: *fs,
76 remaining: path.strip_prefix(root).ok()?,
77 })
78 }
79
80 pub fn lookup_fs_path(&self, path: &Path) -> Option<FileSystemPath> {
85 let LookupSysPathSuffix {
86 fs,
87 remaining: relative,
88 } = self.lookup_sys_path_suffix(path)?;
89 let relative = relative.to_str()?;
90 Some(FileSystemPath::new_normalized_unchecked(
91 ResolvedVc::upcast(fs),
92 RcStr::from(sys_to_unix(relative)),
93 ))
94 }
95
96 pub fn empty() -> OperationVc<DiskFileSystemMap> {
99 #[turbo_tasks::function(operation)]
100 pub fn operation() -> Vc<DiskFileSystemMap> {
101 DiskFileSystemMap {
102 roots: FrozenMap::new(),
103 canonicalized_paths: Default::default(),
104 }
105 .cell()
106 }
107 operation()
108 }
109}
110
111pub struct LookupSysPathSuffix<'a> {
112 pub fs: ResolvedVc<DiskFileSystem>,
113 pub remaining: &'a Path,
114}
115
116#[cfg(test)]
117mod tests {
118 use turbo_rcstr::rcstr;
119 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
120
121 use super::*;
122
123 #[tokio::test]
124 async fn component_safe_lookup() {
125 #[turbo_tasks::function(operation, root)]
126 async fn assert_component_safe_lookup() -> anyhow::Result<()> {
127 let fs = DiskFileSystem::new(rcstr!("root"), Vc::cell(rcstr!("/tmp/root")))
128 .to_resolved()
129 .await?;
130 let map: DiskFileSystemMap = [(PathBuf::from("/tmp/root"), fs)].into_iter().collect();
131 assert_eq!(map.len(), 1);
132 assert!(map.contains(Path::new("/tmp/root"), fs));
133 assert_eq!(
134 map.lookup_fs_path(Path::new("/tmp/root/file"))
135 .unwrap()
136 .path,
137 "file"
138 );
139 assert!(
140 map.lookup_fs_path(Path::new("/tmp/root-other/file"))
141 .is_none()
142 );
143 Ok(())
144 }
145
146 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
147 BackendOptions::default(),
148 noop_backing_storage(),
149 ));
150 tt.run_once(async {
151 assert_component_safe_lookup()
152 .read_strongly_consistent()
153 .await
154 })
155 .await
156 .unwrap();
157 }
158}