1use anyhow::{anyhow, Result};
2use fsevent::EventStream;
3use futures::{Stream, StreamExt};
4use smol::io::{AsyncReadExt, AsyncWriteExt};
5use std::{
6 io,
7 os::unix::fs::MetadataExt,
8 path::{Component, Path, PathBuf},
9 pin::Pin,
10 time::{Duration, SystemTime},
11};
12use text::Rope;
13
14#[async_trait::async_trait]
15pub trait Fs: Send + Sync {
16 async fn create_dir(&self, path: &Path) -> Result<()>;
17 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
18 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
19 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
20 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
21 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
22 async fn load(&self, path: &Path) -> Result<String>;
23 async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
24 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
25 async fn is_file(&self, path: &Path) -> bool;
26 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
27 async fn read_dir(
28 &self,
29 path: &Path,
30 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
31 async fn watch(
32 &self,
33 path: &Path,
34 latency: Duration,
35 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
36 fn is_fake(&self) -> bool;
37 #[cfg(any(test, feature = "test-support"))]
38 fn as_fake(&self) -> &FakeFs;
39}
40
41#[derive(Copy, Clone, Default)]
42pub struct CreateOptions {
43 pub overwrite: bool,
44 pub ignore_if_exists: bool,
45}
46
47#[derive(Copy, Clone, Default)]
48pub struct RenameOptions {
49 pub overwrite: bool,
50 pub ignore_if_exists: bool,
51}
52
53#[derive(Copy, Clone, Default)]
54pub struct RemoveOptions {
55 pub recursive: bool,
56 pub ignore_if_not_exists: bool,
57}
58
59#[derive(Clone, Debug)]
60pub struct Metadata {
61 pub inode: u64,
62 pub mtime: SystemTime,
63 pub is_symlink: bool,
64 pub is_dir: bool,
65}
66
67pub struct RealFs;
68
69#[async_trait::async_trait]
70impl Fs for RealFs {
71 async fn create_dir(&self, path: &Path) -> Result<()> {
72 Ok(smol::fs::create_dir_all(path).await?)
73 }
74
75 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
76 let mut open_options = smol::fs::OpenOptions::new();
77 open_options.write(true).create(true);
78 if options.overwrite {
79 open_options.truncate(true);
80 } else if !options.ignore_if_exists {
81 open_options.create_new(true);
82 }
83 open_options.open(path).await?;
84 Ok(())
85 }
86
87 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
88 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
89 if options.ignore_if_exists {
90 return Ok(());
91 } else {
92 return Err(anyhow!("{target:?} already exists"));
93 }
94 }
95
96 smol::fs::rename(source, target).await?;
97 Ok(())
98 }
99
100 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
101 let result = if options.recursive {
102 smol::fs::remove_dir_all(path).await
103 } else {
104 smol::fs::remove_dir(path).await
105 };
106 match result {
107 Ok(()) => Ok(()),
108 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
109 Ok(())
110 }
111 Err(err) => Err(err)?,
112 }
113 }
114
115 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
116 match smol::fs::remove_file(path).await {
117 Ok(()) => Ok(()),
118 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
119 Ok(())
120 }
121 Err(err) => Err(err)?,
122 }
123 }
124
125 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
126 Ok(Box::new(std::fs::File::open(path)?))
127 }
128
129 async fn load(&self, path: &Path) -> Result<String> {
130 let mut file = smol::fs::File::open(path).await?;
131 let mut text = String::new();
132 file.read_to_string(&mut text).await?;
133 Ok(text)
134 }
135
136 async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
137 let buffer_size = text.summary().bytes.min(10 * 1024);
138 let file = smol::fs::File::create(path).await?;
139 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
140 for chunk in text.chunks() {
141 writer.write_all(chunk.as_bytes()).await?;
142 }
143 writer.flush().await?;
144 Ok(())
145 }
146
147 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
148 Ok(smol::fs::canonicalize(path).await?)
149 }
150
151 async fn is_file(&self, path: &Path) -> bool {
152 smol::fs::metadata(path)
153 .await
154 .map_or(false, |metadata| metadata.is_file())
155 }
156
157 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
158 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
159 Ok(metadata) => metadata,
160 Err(err) => {
161 return match (err.kind(), err.raw_os_error()) {
162 (io::ErrorKind::NotFound, _) => Ok(None),
163 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
164 _ => Err(anyhow::Error::new(err)),
165 }
166 }
167 };
168
169 let is_symlink = symlink_metadata.file_type().is_symlink();
170 let metadata = if is_symlink {
171 smol::fs::metadata(path).await?
172 } else {
173 symlink_metadata
174 };
175 Ok(Some(Metadata {
176 inode: metadata.ino(),
177 mtime: metadata.modified().unwrap(),
178 is_symlink,
179 is_dir: metadata.file_type().is_dir(),
180 }))
181 }
182
183 async fn read_dir(
184 &self,
185 path: &Path,
186 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
187 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
188 Ok(entry) => Ok(entry.path()),
189 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
190 });
191 Ok(Box::pin(result))
192 }
193
194 async fn watch(
195 &self,
196 path: &Path,
197 latency: Duration,
198 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
199 let (tx, rx) = smol::channel::unbounded();
200 let (stream, handle) = EventStream::new(&[path], latency);
201 std::mem::forget(handle);
202 std::thread::spawn(move || {
203 stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
204 });
205 Box::pin(rx)
206 }
207
208 fn is_fake(&self) -> bool {
209 false
210 }
211 #[cfg(any(test, feature = "test-support"))]
212 fn as_fake(&self) -> &FakeFs {
213 panic!("called `RealFs::as_fake`")
214 }
215}
216
217#[cfg(any(test, feature = "test-support"))]
218#[derive(Clone, Debug)]
219struct FakeFsEntry {
220 metadata: Metadata,
221 content: Option<String>,
222}
223
224#[cfg(any(test, feature = "test-support"))]
225struct FakeFsState {
226 entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
227 next_inode: u64,
228 event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
229}
230
231#[cfg(any(test, feature = "test-support"))]
232impl FakeFsState {
233 fn validate_path(&self, path: &Path) -> Result<()> {
234 if path.is_absolute()
235 && path
236 .parent()
237 .and_then(|path| self.entries.get(path))
238 .map_or(false, |e| e.metadata.is_dir)
239 {
240 Ok(())
241 } else {
242 Err(anyhow!("invalid path {:?}", path))
243 }
244 }
245
246 async fn emit_event<I, T>(&mut self, paths: I)
247 where
248 I: IntoIterator<Item = T>,
249 T: Into<PathBuf>,
250 {
251 let events = paths
252 .into_iter()
253 .map(|path| fsevent::Event {
254 event_id: 0,
255 flags: fsevent::StreamFlags::empty(),
256 path: path.into(),
257 })
258 .collect::<Vec<_>>();
259
260 self.event_txs.retain(|tx| {
261 let _ = tx.try_send(events.clone());
262 !tx.is_closed()
263 });
264 }
265}
266
267#[cfg(any(test, feature = "test-support"))]
268pub struct FakeFs {
269 // Use an unfair lock to ensure tests are deterministic.
270 state: futures::lock::Mutex<FakeFsState>,
271 executor: std::sync::Weak<gpui::executor::Background>,
272}
273
274#[cfg(any(test, feature = "test-support"))]
275impl FakeFs {
276 pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
277 let mut entries = std::collections::BTreeMap::new();
278 entries.insert(
279 Path::new("/").to_path_buf(),
280 FakeFsEntry {
281 metadata: Metadata {
282 inode: 0,
283 mtime: SystemTime::now(),
284 is_dir: true,
285 is_symlink: false,
286 },
287 content: None,
288 },
289 );
290 std::sync::Arc::new(Self {
291 executor: std::sync::Arc::downgrade(&executor),
292 state: futures::lock::Mutex::new(FakeFsState {
293 entries,
294 next_inode: 1,
295 event_txs: Default::default(),
296 }),
297 })
298 }
299
300 pub async fn insert_dir(&self, path: impl AsRef<Path>) {
301 let mut state = self.state.lock().await;
302 let path = path.as_ref();
303 state.validate_path(path).unwrap();
304
305 let inode = state.next_inode;
306 state.next_inode += 1;
307 state.entries.insert(
308 path.to_path_buf(),
309 FakeFsEntry {
310 metadata: Metadata {
311 inode,
312 mtime: SystemTime::now(),
313 is_dir: true,
314 is_symlink: false,
315 },
316 content: None,
317 },
318 );
319 state.emit_event(&[path]).await;
320 }
321
322 pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
323 let mut state = self.state.lock().await;
324 let path = path.as_ref();
325 state.validate_path(path).unwrap();
326
327 let inode = state.next_inode;
328 state.next_inode += 1;
329 state.entries.insert(
330 path.to_path_buf(),
331 FakeFsEntry {
332 metadata: Metadata {
333 inode,
334 mtime: SystemTime::now(),
335 is_dir: false,
336 is_symlink: false,
337 },
338 content: Some(content),
339 },
340 );
341 state.emit_event(&[path]).await;
342 }
343
344 #[must_use]
345 pub fn insert_tree<'a>(
346 &'a self,
347 path: impl 'a + AsRef<Path> + Send,
348 tree: serde_json::Value,
349 ) -> futures::future::BoxFuture<'a, ()> {
350 use futures::FutureExt as _;
351 use serde_json::Value::*;
352
353 async move {
354 let path = path.as_ref();
355
356 match tree {
357 Object(map) => {
358 self.insert_dir(path).await;
359 for (name, contents) in map {
360 let mut path = PathBuf::from(path);
361 path.push(name);
362 self.insert_tree(&path, contents).await;
363 }
364 }
365 Null => {
366 self.insert_dir(&path).await;
367 }
368 String(contents) => {
369 self.insert_file(&path, contents).await;
370 }
371 _ => {
372 panic!("JSON object must contain only objects, strings, or null");
373 }
374 }
375 }
376 .boxed()
377 }
378
379 pub async fn files(&self) -> Vec<PathBuf> {
380 self.state
381 .lock()
382 .await
383 .entries
384 .iter()
385 .filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone()))
386 .collect()
387 }
388
389 async fn simulate_random_delay(&self) {
390 self.executor
391 .upgrade()
392 .expect("executor has been dropped")
393 .simulate_random_delay()
394 .await;
395 }
396}
397
398#[cfg(any(test, feature = "test-support"))]
399#[async_trait::async_trait]
400impl Fs for FakeFs {
401 async fn create_dir(&self, path: &Path) -> Result<()> {
402 self.simulate_random_delay().await;
403 let state = &mut *self.state.lock().await;
404 let path = normalize_path(path);
405 let mut ancestor_path = PathBuf::new();
406 let mut created_dir_paths = Vec::new();
407 for component in path.components() {
408 ancestor_path.push(component);
409 let entry = state
410 .entries
411 .entry(ancestor_path.clone())
412 .or_insert_with(|| {
413 let inode = state.next_inode;
414 state.next_inode += 1;
415 created_dir_paths.push(ancestor_path.clone());
416 FakeFsEntry {
417 metadata: Metadata {
418 inode,
419 mtime: SystemTime::now(),
420 is_dir: true,
421 is_symlink: false,
422 },
423 content: None,
424 }
425 });
426 if !entry.metadata.is_dir {
427 return Err(anyhow!(
428 "cannot create directory because {:?} is a file",
429 ancestor_path
430 ));
431 }
432 }
433 state.emit_event(&created_dir_paths).await;
434
435 Ok(())
436 }
437
438 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
439 self.simulate_random_delay().await;
440 let mut state = self.state.lock().await;
441 let path = normalize_path(path);
442 state.validate_path(&path)?;
443 if let Some(entry) = state.entries.get_mut(&path) {
444 if entry.metadata.is_dir || entry.metadata.is_symlink {
445 return Err(anyhow!(
446 "cannot create file because {:?} is a dir or a symlink",
447 path
448 ));
449 }
450
451 if options.overwrite {
452 entry.metadata.mtime = SystemTime::now();
453 entry.content = Some(Default::default());
454 } else if !options.ignore_if_exists {
455 return Err(anyhow!(
456 "cannot create file because {:?} already exists",
457 &path
458 ));
459 }
460 } else {
461 let inode = state.next_inode;
462 state.next_inode += 1;
463 let entry = FakeFsEntry {
464 metadata: Metadata {
465 inode,
466 mtime: SystemTime::now(),
467 is_dir: false,
468 is_symlink: false,
469 },
470 content: Some(Default::default()),
471 };
472 state.entries.insert(path.to_path_buf(), entry);
473 }
474 state.emit_event(&[path]).await;
475
476 Ok(())
477 }
478
479 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
480 let source = normalize_path(source);
481 let target = normalize_path(target);
482
483 let mut state = self.state.lock().await;
484 state.validate_path(&source)?;
485 state.validate_path(&target)?;
486
487 if !options.overwrite && state.entries.contains_key(&target) {
488 if options.ignore_if_exists {
489 return Ok(());
490 } else {
491 return Err(anyhow!("{target:?} already exists"));
492 }
493 }
494
495 let mut removed = Vec::new();
496 state.entries.retain(|path, entry| {
497 if let Ok(relative_path) = path.strip_prefix(&source) {
498 removed.push((relative_path.to_path_buf(), entry.clone()));
499 false
500 } else {
501 true
502 }
503 });
504
505 for (relative_path, entry) in removed {
506 let new_path = normalize_path(&target.join(relative_path));
507 state.entries.insert(new_path, entry);
508 }
509
510 state.emit_event(&[source, target]).await;
511 Ok(())
512 }
513
514 async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
515 let dir_path = normalize_path(dir_path);
516 let mut state = self.state.lock().await;
517 state.validate_path(&dir_path)?;
518 if let Some(entry) = state.entries.get(&dir_path) {
519 if !entry.metadata.is_dir {
520 return Err(anyhow!(
521 "cannot remove {dir_path:?} because it is not a dir"
522 ));
523 }
524
525 if !options.recursive {
526 let descendants = state
527 .entries
528 .keys()
529 .filter(|path| path.starts_with(path))
530 .count();
531 if descendants > 1 {
532 return Err(anyhow!("{dir_path:?} is not empty"));
533 }
534 }
535
536 state.entries.retain(|path, _| !path.starts_with(&dir_path));
537 state.emit_event(&[dir_path]).await;
538 } else if !options.ignore_if_not_exists {
539 return Err(anyhow!("{dir_path:?} does not exist"));
540 }
541
542 Ok(())
543 }
544
545 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
546 let path = normalize_path(path);
547 let mut state = self.state.lock().await;
548 state.validate_path(&path)?;
549 if let Some(entry) = state.entries.get(&path) {
550 if entry.metadata.is_dir {
551 return Err(anyhow!("cannot remove {path:?} because it is not a file"));
552 }
553
554 state.entries.remove(&path);
555 state.emit_event(&[path]).await;
556 } else if !options.ignore_if_not_exists {
557 return Err(anyhow!("{path:?} does not exist"));
558 }
559 Ok(())
560 }
561
562 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
563 let text = self.load(path).await?;
564 Ok(Box::new(io::Cursor::new(text)))
565 }
566
567 async fn load(&self, path: &Path) -> Result<String> {
568 let path = normalize_path(path);
569 self.simulate_random_delay().await;
570 let state = self.state.lock().await;
571 let text = state
572 .entries
573 .get(&path)
574 .and_then(|e| e.content.as_ref())
575 .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
576 Ok(text.clone())
577 }
578
579 async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
580 self.simulate_random_delay().await;
581 let mut state = self.state.lock().await;
582 let path = normalize_path(path);
583 state.validate_path(&path)?;
584 if let Some(entry) = state.entries.get_mut(&path) {
585 if entry.metadata.is_dir {
586 Err(anyhow!("cannot overwrite a directory with a file"))
587 } else {
588 entry.content = Some(text.chunks().collect());
589 entry.metadata.mtime = SystemTime::now();
590 state.emit_event(&[path]).await;
591 Ok(())
592 }
593 } else {
594 let inode = state.next_inode;
595 state.next_inode += 1;
596 let entry = FakeFsEntry {
597 metadata: Metadata {
598 inode,
599 mtime: SystemTime::now(),
600 is_dir: false,
601 is_symlink: false,
602 },
603 content: Some(text.chunks().collect()),
604 };
605 state.entries.insert(path.to_path_buf(), entry);
606 state.emit_event(&[path]).await;
607 Ok(())
608 }
609 }
610
611 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
612 self.simulate_random_delay().await;
613 Ok(normalize_path(path))
614 }
615
616 async fn is_file(&self, path: &Path) -> bool {
617 let path = normalize_path(path);
618 self.simulate_random_delay().await;
619 let state = self.state.lock().await;
620 state
621 .entries
622 .get(&path)
623 .map_or(false, |entry| !entry.metadata.is_dir)
624 }
625
626 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
627 self.simulate_random_delay().await;
628 let state = self.state.lock().await;
629 let path = normalize_path(path);
630 Ok(state.entries.get(&path).map(|entry| entry.metadata.clone()))
631 }
632
633 async fn read_dir(
634 &self,
635 abs_path: &Path,
636 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
637 use futures::{future, stream};
638 self.simulate_random_delay().await;
639 let state = self.state.lock().await;
640 let abs_path = normalize_path(abs_path);
641 Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
642 move |(child_path, _)| {
643 future::ready(if child_path.parent() == Some(&abs_path) {
644 Some(Ok(child_path))
645 } else {
646 None
647 })
648 },
649 )))
650 }
651
652 async fn watch(
653 &self,
654 path: &Path,
655 _: Duration,
656 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
657 let mut state = self.state.lock().await;
658 self.simulate_random_delay().await;
659 let (tx, rx) = smol::channel::unbounded();
660 state.event_txs.push(tx);
661 let path = path.to_path_buf();
662 let executor = self.executor.clone();
663 Box::pin(futures::StreamExt::filter(rx, move |events| {
664 let result = events.iter().any(|event| event.path.starts_with(&path));
665 let executor = executor.clone();
666 async move {
667 if let Some(executor) = executor.clone().upgrade() {
668 executor.simulate_random_delay().await;
669 }
670 result
671 }
672 }))
673 }
674
675 fn is_fake(&self) -> bool {
676 true
677 }
678
679 #[cfg(any(test, feature = "test-support"))]
680 fn as_fake(&self) -> &FakeFs {
681 self
682 }
683}
684
685pub fn normalize_path(path: &Path) -> PathBuf {
686 let mut components = path.components().peekable();
687 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
688 components.next();
689 PathBuf::from(c.as_os_str())
690 } else {
691 PathBuf::new()
692 };
693
694 for component in components {
695 match component {
696 Component::Prefix(..) => unreachable!(),
697 Component::RootDir => {
698 ret.push(component.as_os_str());
699 }
700 Component::CurDir => {}
701 Component::ParentDir => {
702 ret.pop();
703 }
704 Component::Normal(c) => {
705 ret.push(c);
706 }
707 }
708 }
709 ret
710}