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