reflexo_vfs/
lib.rs

1//! upstream of following files <https://github.com/rust-lang/rust-analyzer/tree/master/crates/vfs>
2//!   ::path_interner.rs -> path_interner.rs
3
4/// Provides ProxyAccessModel that makes access to JavaScript objects for
5/// browser compilation.
6#[cfg(feature = "browser")]
7pub mod browser;
8
9/// Provides SystemAccessModel that makes access to the local file system for
10/// system compilation.
11#[cfg(feature = "system")]
12pub mod system;
13
14/// Provides dummy access model.
15///
16/// Note: we can still perform compilation with dummy access model, since
17/// [`Vfs`] will make a overlay access model over the provided dummy access
18/// model.
19pub mod dummy;
20/// Provides notify access model which retrieves file system events and changes
21/// from some notify backend.
22pub mod notify;
23/// Provides overlay access model which allows to shadow the underlying access
24/// model with memory contents.
25pub mod overlay;
26/// Provides trace access model which traces the underlying access model.
27pub mod trace;
28mod utils;
29
30mod path_interner;
31
32pub use typst::foundations::Bytes;
33pub use typst::syntax::FileId as TypstFileId;
34
35pub use reflexo::time::Time;
36pub use reflexo::ImmutPath;
37
38pub(crate) use path_interner::PathInterner;
39
40use core::fmt;
41use std::{
42    collections::HashMap,
43    hash::Hash,
44    path::Path,
45    sync::{
46        atomic::{AtomicU64, Ordering},
47        Arc,
48    },
49};
50
51use parking_lot::{Mutex, RwLock};
52use reflexo::path::PathClean;
53use typst::diag::{FileError, FileResult};
54
55use self::{
56    notify::{FilesystemEvent, NotifyAccessModel},
57    overlay::OverlayAccessModel,
58};
59
60/// Handle to a file in [`Vfs`]
61///
62/// Most functions in typst-ts use this when they need to refer to a file.
63#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
64pub struct FileId(pub u32);
65
66/// safe because `FileId` is a new type of `u32`
67impl nohash_hasher::IsEnabled for FileId {}
68
69/// A trait for accessing underlying file system.
70///
71/// This trait is simplified by [`Vfs`] and requires a minimal method set for
72/// typst compilation.
73pub trait AccessModel {
74    /// Clear the cache of the access model.
75    ///
76    /// This is called when the vfs is reset. See [`Vfs`]'s reset method for
77    /// more information.
78    fn clear(&mut self) {}
79    /// Return a mtime corresponding to the path.
80    ///
81    /// Note: vfs won't touch the file entry if mtime is same between vfs reset
82    /// lifecycles for performance design.
83    fn mtime(&self, src: &Path) -> FileResult<Time>;
84
85    /// Return whether a path is corresponding to a file.
86    fn is_file(&self, src: &Path) -> FileResult<bool>;
87
88    /// Return the real path before creating a vfs file entry.
89    ///
90    /// Note: vfs will fetch the file entry once if multiple paths shares a same
91    /// real path.
92    fn real_path(&self, src: &Path) -> FileResult<ImmutPath>;
93
94    /// Return the content of a file entry.
95    fn content(&self, src: &Path) -> FileResult<Bytes>;
96}
97
98#[derive(Clone)]
99pub struct SharedAccessModel<M> {
100    pub inner: Arc<RwLock<M>>,
101}
102
103impl<M> SharedAccessModel<M> {
104    pub fn new(inner: M) -> Self {
105        Self {
106            inner: Arc::new(RwLock::new(inner)),
107        }
108    }
109}
110
111impl<M> AccessModel for SharedAccessModel<M>
112where
113    M: AccessModel,
114{
115    fn clear(&mut self) {
116        self.inner.write().clear();
117    }
118
119    fn mtime(&self, src: &Path) -> FileResult<Time> {
120        self.inner.read().mtime(src)
121    }
122
123    fn is_file(&self, src: &Path) -> FileResult<bool> {
124        self.inner.read().is_file(src)
125    }
126
127    fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
128        self.inner.read().real_path(src)
129    }
130
131    fn content(&self, src: &Path) -> FileResult<Bytes> {
132        self.inner.read().content(src)
133    }
134}
135
136/// we add notify access model here since notify access model doesn't introduce
137/// overheads by our observation
138type VfsAccessModel<M> = OverlayAccessModel<NotifyAccessModel<SharedAccessModel<M>>>;
139
140pub trait FsProvider {
141    /// Arbitrary one of file path corresponding to the given `id`.
142    fn file_path(&self, id: FileId) -> ImmutPath;
143
144    fn mtime(&self, id: FileId) -> FileResult<Time>;
145
146    fn read(&self, id: FileId) -> FileResult<Bytes>;
147
148    fn is_file(&self, id: FileId) -> FileResult<bool>;
149}
150
151#[derive(Default)]
152struct PathMapper {
153    /// The number of lifecycles since the creation of the `Vfs`.
154    ///
155    /// Note: The lifetime counter is incremented on resetting vfs.
156    clock: AtomicU64,
157    /// Map from path to slot index.
158    ///
159    /// Note: we use a owned [`FileId`] here, which is resultant from
160    /// [`PathInterner`]
161    id_cache: RwLock<HashMap<ImmutPath, FileId>>,
162    /// The path interner for canonical paths.
163    intern: Mutex<PathInterner<ImmutPath, u64>>,
164}
165
166impl PathMapper {
167    /// Reset the path references.
168    ///
169    /// It performs a rolling reset, with discard some cache file entry when it
170    /// is unused in recent 30 lifecycles.
171    ///
172    /// Note: The lifetime counter is incremented every time this function is
173    /// called.
174    pub fn reset(&self) {
175        self.clock.fetch_add(1, Ordering::SeqCst);
176
177        // todo: clean path interner.
178        // let new_lifetime_cnt = self.lifetime_cnt;
179        // self.path2slot.get_mut().clear();
180        // self.path_interner
181        //     .get_mut()
182        //     .retain(|_, lifetime| new_lifetime_cnt - *lifetime <= 30);
183    }
184
185    /// Id of the given path if it exists in the `Vfs` and is not deleted.
186    pub fn file_id(&self, path: &Path) -> FileId {
187        let quick_id = self.id_cache.read().get(path).copied();
188        if let Some(id) = quick_id {
189            return id;
190        }
191
192        let path: ImmutPath = path.clean().as_path().into();
193
194        let mut path_interner = self.intern.lock();
195        let lifetime_cnt = self.clock.load(Ordering::SeqCst);
196        let id = path_interner.intern(path.clone(), lifetime_cnt).0;
197
198        let mut path2slot = self.id_cache.write();
199        path2slot.insert(path.clone(), id);
200
201        id
202    }
203
204    /// File path corresponding to the given `file_id`.
205    pub fn file_path(&self, file_id: FileId) -> ImmutPath {
206        let path_interner = self.intern.lock();
207        path_interner.lookup(file_id).clone()
208    }
209}
210
211/// Create a new `Vfs` harnessing over the given `access_model` specific for
212/// `reflexo_world::CompilerWorld`. With vfs, we can minimize the
213/// implementation overhead for [`AccessModel`] trait.
214pub struct Vfs<M: AccessModel + Sized> {
215    paths: Arc<PathMapper>,
216
217    // access_model: TraceAccessModel<VfsAccessModel<M>>,
218    /// The wrapped access model.
219    access_model: VfsAccessModel<M>,
220}
221
222impl<M: AccessModel + Sized> fmt::Debug for Vfs<M> {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.debug_struct("Vfs").finish()
225    }
226}
227
228impl<M: AccessModel + Clone + Sized> Vfs<M> {
229    pub fn snapshot(&self) -> Self {
230        Self {
231            paths: self.paths.clone(),
232            access_model: self.access_model.clone(),
233        }
234    }
235}
236
237impl<M: AccessModel + Sized> Vfs<M> {
238    /// Create a new `Vfs` with a given `access_model`.
239    ///
240    /// Retrieving an [`AccessModel`], it will further wrap the access model
241    /// with [`OverlayAccessModel`] and [`NotifyAccessModel`]. This means that
242    /// you don't need to implement:
243    /// + overlay: allowing to shadow the underlying access model with memory
244    ///   contents, which is useful for a limited execution environment and
245    ///   instrumenting or overriding source files or packages.
246    /// + notify: regards problems of synchronizing with the file system when
247    ///   the vfs is watching the file system.
248    ///
249    /// See [`AccessModel`] for more information.
250    pub fn new(access_model: M) -> Self {
251        let access_model = SharedAccessModel::new(access_model);
252        let access_model = NotifyAccessModel::new(access_model);
253        let access_model = OverlayAccessModel::new(access_model);
254
255        // If you want to trace the access model, uncomment the following line
256        // let access_model = TraceAccessModel::new(access_model);
257
258        Self {
259            paths: Default::default(),
260            access_model,
261        }
262    }
263
264    /// Reset the source file and path references.
265    ///
266    /// It performs a rolling reset, with discard some cache file entry when it
267    /// is unused in recent 30 lifecycles.
268    ///
269    /// Note: The lifetime counter is incremented every time this function is
270    /// called.
271    pub fn reset(&mut self) {
272        self.paths.reset();
273        self.access_model.clear();
274    }
275
276    /// Reset the shadowing files in [`OverlayAccessModel`].
277    ///
278    /// Note: This function is independent from [`Vfs::reset`].
279    pub fn reset_shadow(&mut self) {
280        self.access_model.clear_shadow();
281    }
282
283    /// Get paths to all the shadowing files in [`OverlayAccessModel`].
284    pub fn shadow_paths(&self) -> Vec<Arc<Path>> {
285        self.access_model.file_paths()
286    }
287
288    /// Add a shadowing file to the [`OverlayAccessModel`].
289    pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> {
290        self.access_model.add_file(path.into(), content);
291
292        Ok(())
293    }
294
295    /// Remove a shadowing file from the [`OverlayAccessModel`].
296    pub fn remove_shadow(&mut self, path: &Path) {
297        self.access_model.remove_file(path);
298    }
299
300    /// Let the vfs notify the access model with a filesystem event.
301    ///
302    /// See [`NotifyAccessModel`] for more information.
303    pub fn notify_fs_event(&mut self, event: FilesystemEvent) {
304        self.access_model.inner.notify(event);
305    }
306
307    /// Returns the overall memory usage for the stored files.
308    pub fn memory_usage(&self) -> usize {
309        0
310    }
311
312    /// Id of the given path if it exists in the `Vfs` and is not deleted.
313    pub fn file_id(&self, path: &Path) -> FileId {
314        self.paths.file_id(path)
315    }
316
317    /// Read a file.
318    pub fn read(&self, path: &Path) -> FileResult<Bytes> {
319        if self.access_model.is_file(path)? {
320            self.access_model.content(path)
321        } else {
322            Err(FileError::IsDirectory)
323        }
324    }
325}
326
327impl<M: AccessModel> FsProvider for Vfs<M> {
328    fn file_path(&self, id: FileId) -> ImmutPath {
329        self.paths.file_path(id)
330    }
331
332    fn mtime(&self, src: FileId) -> FileResult<Time> {
333        self.access_model.mtime(&self.file_path(src))
334    }
335
336    fn read(&self, src: FileId) -> FileResult<Bytes> {
337        self.access_model.content(&self.file_path(src))
338    }
339
340    fn is_file(&self, src: FileId) -> FileResult<bool> {
341        self.access_model.is_file(&self.file_path(src))
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    fn is_send<T: Send>() {}
348    fn is_sync<T: Sync>() {}
349
350    #[test]
351    fn test_vfs_send_sync() {
352        is_send::<super::Vfs<super::dummy::DummyAccessModel>>();
353        is_sync::<super::Vfs<super::dummy::DummyAccessModel>>();
354    }
355}