1use anyhow::{anyhow, Result};
2use git::GitHostingProviderRegistry;
3
4#[cfg(target_os = "linux")]
5use ashpd::desktop::trash;
6#[cfg(target_os = "linux")]
7use std::{fs::File, os::fd::AsFd};
8
9#[cfg(unix)]
10use std::os::unix::fs::MetadataExt;
11
12use async_tar::Archive;
13use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
14use git::repository::{GitRepository, RealGitRepository};
15use rope::Rope;
16use smol::io::AsyncWriteExt;
17use std::{
18 io::{self, Write},
19 path::{Component, Path, PathBuf},
20 pin::Pin,
21 sync::Arc,
22 time::{Duration, SystemTime},
23};
24use tempfile::{NamedTempFile, TempDir};
25use text::LineEnding;
26use util::{paths, ResultExt};
27
28#[cfg(any(test, feature = "test-support"))]
29use collections::{btree_map, BTreeMap};
30#[cfg(any(test, feature = "test-support"))]
31use git::repository::{FakeGitRepositoryState, GitFileStatus};
32#[cfg(any(test, feature = "test-support"))]
33use parking_lot::Mutex;
34#[cfg(any(test, feature = "test-support"))]
35use smol::io::AsyncReadExt;
36#[cfg(any(test, feature = "test-support"))]
37use std::ffi::OsStr;
38
39#[async_trait::async_trait]
40pub trait Fs: Send + Sync {
41 async fn create_dir(&self, path: &Path) -> Result<()>;
42 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>;
43 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
44 async fn create_file_with(
45 &self,
46 path: &Path,
47 content: Pin<&mut (dyn AsyncRead + Send)>,
48 ) -> Result<()>;
49 async fn extract_tar_file(
50 &self,
51 path: &Path,
52 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
53 ) -> Result<()>;
54 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
55 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
56 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
57 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
58 self.remove_dir(path, options).await
59 }
60 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
61 async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
62 self.remove_file(path, options).await
63 }
64 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
65 async fn load(&self, path: &Path) -> Result<String>;
66 async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
67 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
68 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
69 async fn is_file(&self, path: &Path) -> bool;
70 async fn is_dir(&self, path: &Path) -> bool;
71 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
72 async fn read_link(&self, path: &Path) -> Result<PathBuf>;
73 async fn read_dir(
74 &self,
75 path: &Path,
76 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
77
78 async fn watch(
79 &self,
80 path: &Path,
81 latency: Duration,
82 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
83
84 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
85 fn is_fake(&self) -> bool;
86 async fn is_case_sensitive(&self) -> Result<bool>;
87 #[cfg(any(test, feature = "test-support"))]
88 fn as_fake(&self) -> &FakeFs;
89}
90
91#[derive(Copy, Clone, Default)]
92pub struct CreateOptions {
93 pub overwrite: bool,
94 pub ignore_if_exists: bool,
95}
96
97#[derive(Copy, Clone, Default)]
98pub struct CopyOptions {
99 pub overwrite: bool,
100 pub ignore_if_exists: bool,
101}
102
103#[derive(Copy, Clone, Default)]
104pub struct RenameOptions {
105 pub overwrite: bool,
106 pub ignore_if_exists: bool,
107}
108
109#[derive(Copy, Clone, Default)]
110pub struct RemoveOptions {
111 pub recursive: bool,
112 pub ignore_if_not_exists: bool,
113}
114
115#[derive(Copy, Clone, Debug)]
116pub struct Metadata {
117 pub inode: u64,
118 pub mtime: SystemTime,
119 pub is_symlink: bool,
120 pub is_dir: bool,
121}
122
123#[derive(Default)]
124pub struct RealFs {
125 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
126 git_binary_path: Option<PathBuf>,
127}
128
129impl RealFs {
130 pub fn new(
131 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
132 git_binary_path: Option<PathBuf>,
133 ) -> Self {
134 Self {
135 git_hosting_provider_registry,
136 git_binary_path,
137 }
138 }
139}
140
141#[async_trait::async_trait]
142impl Fs for RealFs {
143 async fn create_dir(&self, path: &Path) -> Result<()> {
144 Ok(smol::fs::create_dir_all(path).await?)
145 }
146
147 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
148 #[cfg(unix)]
149 smol::fs::unix::symlink(target, path).await?;
150
151 #[cfg(windows)]
152 if smol::fs::metadata(&target).await?.is_dir() {
153 smol::fs::windows::symlink_dir(target, path).await?
154 } else {
155 smol::fs::windows::symlink_file(target, path).await?
156 }
157
158 Ok(())
159 }
160
161 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
162 let mut open_options = smol::fs::OpenOptions::new();
163 open_options.write(true).create(true);
164 if options.overwrite {
165 open_options.truncate(true);
166 } else if !options.ignore_if_exists {
167 open_options.create_new(true);
168 }
169 open_options.open(path).await?;
170 Ok(())
171 }
172
173 async fn create_file_with(
174 &self,
175 path: &Path,
176 content: Pin<&mut (dyn AsyncRead + Send)>,
177 ) -> Result<()> {
178 let mut file = smol::fs::File::create(&path).await?;
179 futures::io::copy(content, &mut file).await?;
180 Ok(())
181 }
182
183 async fn extract_tar_file(
184 &self,
185 path: &Path,
186 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
187 ) -> Result<()> {
188 content.unpack(path).await?;
189 Ok(())
190 }
191
192 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
193 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
194 if options.ignore_if_exists {
195 return Ok(());
196 } else {
197 return Err(anyhow!("{target:?} already exists"));
198 }
199 }
200
201 smol::fs::copy(source, target).await?;
202 Ok(())
203 }
204
205 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
206 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
207 if options.ignore_if_exists {
208 return Ok(());
209 } else {
210 return Err(anyhow!("{target:?} already exists"));
211 }
212 }
213
214 smol::fs::rename(source, target).await?;
215 Ok(())
216 }
217
218 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
219 let result = if options.recursive {
220 smol::fs::remove_dir_all(path).await
221 } else {
222 smol::fs::remove_dir(path).await
223 };
224 match result {
225 Ok(()) => Ok(()),
226 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
227 Ok(())
228 }
229 Err(err) => Err(err)?,
230 }
231 }
232
233 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
234 #[cfg(windows)]
235 if let Ok(Some(metadata)) = self.metadata(path).await {
236 if metadata.is_symlink && metadata.is_dir {
237 self.remove_dir(
238 path,
239 RemoveOptions {
240 recursive: false,
241 ignore_if_not_exists: true,
242 },
243 )
244 .await?;
245 return Ok(());
246 }
247 }
248
249 match smol::fs::remove_file(path).await {
250 Ok(()) => Ok(()),
251 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
252 Ok(())
253 }
254 Err(err) => Err(err)?,
255 }
256 }
257
258 #[cfg(target_os = "macos")]
259 async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
260 use cocoa::{
261 base::{id, nil},
262 foundation::{NSAutoreleasePool, NSString},
263 };
264 use objc::{class, msg_send, sel, sel_impl};
265
266 unsafe {
267 unsafe fn ns_string(string: &str) -> id {
268 NSString::alloc(nil).init_str(string).autorelease()
269 }
270
271 let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
272 let array: id = msg_send![class!(NSArray), arrayWithObject: url];
273 let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
274
275 let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
276 }
277 Ok(())
278 }
279
280 #[cfg(target_os = "linux")]
281 async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
282 let file = File::open(path)?;
283 match trash::trash_file(&file.as_fd()).await {
284 Ok(_) => Ok(()),
285 Err(err) => Err(anyhow::Error::new(err)),
286 }
287 }
288
289 #[cfg(target_os = "macos")]
290 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
291 self.trash_file(path, options).await
292 }
293
294 #[cfg(target_os = "linux")]
295 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
296 self.trash_file(path, options).await
297 }
298
299 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
300 Ok(Box::new(std::fs::File::open(path)?))
301 }
302
303 async fn load(&self, path: &Path) -> Result<String> {
304 let path = path.to_path_buf();
305 let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
306 Ok(text)
307 }
308
309 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
310 smol::unblock(move || {
311 let mut tmp_file = if cfg!(target_os = "linux") {
312 // Use the directory of the destination as temp dir to avoid
313 // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
314 // See https://github.com/zed-industries/zed/pull/8437 for more details.
315 NamedTempFile::new_in(path.parent().unwrap_or(&paths::TEMP_DIR))
316 } else {
317 NamedTempFile::new()
318 }?;
319 tmp_file.write_all(data.as_bytes())?;
320 tmp_file.persist(path)?;
321 Ok::<(), anyhow::Error>(())
322 })
323 .await?;
324
325 Ok(())
326 }
327
328 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
329 let buffer_size = text.summary().len.min(10 * 1024);
330 if let Some(path) = path.parent() {
331 self.create_dir(path).await?;
332 }
333 let file = smol::fs::File::create(path).await?;
334 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
335 for chunk in chunks(text, line_ending) {
336 writer.write_all(chunk.as_bytes()).await?;
337 }
338 writer.flush().await?;
339 Ok(())
340 }
341
342 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
343 Ok(smol::fs::canonicalize(path).await?)
344 }
345
346 async fn is_file(&self, path: &Path) -> bool {
347 smol::fs::metadata(path)
348 .await
349 .map_or(false, |metadata| metadata.is_file())
350 }
351
352 async fn is_dir(&self, path: &Path) -> bool {
353 smol::fs::metadata(path)
354 .await
355 .map_or(false, |metadata| metadata.is_dir())
356 }
357
358 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
359 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
360 Ok(metadata) => metadata,
361 Err(err) => {
362 return match (err.kind(), err.raw_os_error()) {
363 (io::ErrorKind::NotFound, _) => Ok(None),
364 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
365 _ => Err(anyhow::Error::new(err)),
366 }
367 }
368 };
369
370 let is_symlink = symlink_metadata.file_type().is_symlink();
371 let metadata = if is_symlink {
372 smol::fs::metadata(path).await?
373 } else {
374 symlink_metadata
375 };
376
377 #[cfg(unix)]
378 let inode = metadata.ino();
379
380 #[cfg(windows)]
381 let inode = file_id(path).await?;
382
383 Ok(Some(Metadata {
384 inode,
385 mtime: metadata.modified().unwrap(),
386 is_symlink,
387 is_dir: metadata.file_type().is_dir(),
388 }))
389 }
390
391 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
392 let path = smol::fs::read_link(path).await?;
393 Ok(path)
394 }
395
396 async fn read_dir(
397 &self,
398 path: &Path,
399 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
400 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
401 Ok(entry) => Ok(entry.path()),
402 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
403 });
404 Ok(Box::pin(result))
405 }
406
407 #[cfg(target_os = "macos")]
408 async fn watch(
409 &self,
410 path: &Path,
411 latency: Duration,
412 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
413 use fsevent::EventStream;
414
415 let (tx, rx) = smol::channel::unbounded();
416 let (stream, handle) = EventStream::new(&[path], latency);
417 std::thread::spawn(move || {
418 stream.run(move |events| {
419 smol::block_on(tx.send(events.into_iter().map(|event| event.path).collect()))
420 .is_ok()
421 });
422 });
423
424 Box::pin(rx.chain(futures::stream::once(async move {
425 drop(handle);
426 vec![]
427 })))
428 }
429
430 #[cfg(not(target_os = "macos"))]
431 async fn watch(
432 &self,
433 path: &Path,
434 _latency: Duration,
435 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
436 use notify::{event::EventKind, event::ModifyKind, Watcher};
437 // todo(linux): This spawns two threads, while the macOS impl
438 // only spawns one. Can we use a OnceLock or some such to make
439 // this better
440
441 let (tx, rx) = smol::channel::unbounded();
442
443 let mut file_watcher = notify::recommended_watcher({
444 let tx = tx.clone();
445 move |event: Result<notify::Event, _>| {
446 if let Some(event) = event.log_err() {
447 tx.try_send(event.paths).ok();
448 }
449 }
450 })
451 .expect("Could not start file watcher");
452
453 file_watcher
454 .watch(path, notify::RecursiveMode::Recursive)
455 .ok(); // It's ok if this fails, the parent watcher will add it.
456
457 let mut parent_watcher = notify::recommended_watcher({
458 let watched_path = path.to_path_buf();
459 let tx = tx.clone();
460 move |event: Result<notify::Event, _>| {
461 if let Some(event) = event.ok() {
462 if event.paths.into_iter().any(|path| *path == watched_path) {
463 match event.kind {
464 EventKind::Modify(ev) => {
465 if matches!(ev, ModifyKind::Name(_)) {
466 file_watcher
467 .watch(
468 watched_path.as_path(),
469 notify::RecursiveMode::Recursive,
470 )
471 .log_err();
472 let _ = tx.try_send(vec![watched_path.clone()]).ok();
473 }
474 }
475 EventKind::Create(_) => {
476 file_watcher
477 .watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
478 .log_err();
479 let _ = tx.try_send(vec![watched_path.clone()]).ok();
480 }
481 EventKind::Remove(_) => {
482 file_watcher.unwatch(&watched_path).log_err();
483 let _ = tx.try_send(vec![watched_path.clone()]).ok();
484 }
485 _ => {}
486 }
487 }
488 }
489 }
490 })
491 .expect("Could not start file watcher");
492
493 parent_watcher
494 .watch(
495 path.parent()
496 .expect("Watching root is probably not what you want"),
497 notify::RecursiveMode::NonRecursive,
498 )
499 .expect("Could not start watcher on parent directory");
500
501 Box::pin(rx.chain(futures::stream::once(async move {
502 drop(parent_watcher);
503 vec![]
504 })))
505 }
506
507 fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
508 let repo = git2::Repository::open(dotgit_path).log_err()?;
509 Some(Arc::new(RealGitRepository::new(
510 repo,
511 self.git_binary_path.clone(),
512 self.git_hosting_provider_registry.clone(),
513 )))
514 }
515
516 fn is_fake(&self) -> bool {
517 false
518 }
519
520 /// Checks whether the file system is case sensitive by attempting to create two files
521 /// that have the same name except for the casing.
522 ///
523 /// It creates both files in a temporary directory it removes at the end.
524 async fn is_case_sensitive(&self) -> Result<bool> {
525 let temp_dir = TempDir::new()?;
526 let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
527 let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
528
529 let create_opts = CreateOptions {
530 overwrite: false,
531 ignore_if_exists: false,
532 };
533
534 // Create file1
535 self.create_file(&test_file_1, create_opts).await?;
536
537 // Now check whether it's possible to create file2
538 let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
539 Ok(_) => Ok(true),
540 Err(e) => {
541 if let Some(io_error) = e.downcast_ref::<io::Error>() {
542 if io_error.kind() == io::ErrorKind::AlreadyExists {
543 Ok(false)
544 } else {
545 Err(e)
546 }
547 } else {
548 Err(e)
549 }
550 }
551 };
552
553 temp_dir.close()?;
554 case_sensitive
555 }
556
557 #[cfg(any(test, feature = "test-support"))]
558 fn as_fake(&self) -> &FakeFs {
559 panic!("called `RealFs::as_fake`")
560 }
561}
562
563#[cfg(any(test, feature = "test-support"))]
564pub struct FakeFs {
565 // Use an unfair lock to ensure tests are deterministic.
566 state: Mutex<FakeFsState>,
567 executor: gpui::BackgroundExecutor,
568}
569
570#[cfg(any(test, feature = "test-support"))]
571struct FakeFsState {
572 root: Arc<Mutex<FakeFsEntry>>,
573 next_inode: u64,
574 next_mtime: SystemTime,
575 event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
576 events_paused: bool,
577 buffered_events: Vec<PathBuf>,
578 metadata_call_count: usize,
579 read_dir_call_count: usize,
580}
581
582#[cfg(any(test, feature = "test-support"))]
583#[derive(Debug)]
584enum FakeFsEntry {
585 File {
586 inode: u64,
587 mtime: SystemTime,
588 content: Vec<u8>,
589 },
590 Dir {
591 inode: u64,
592 mtime: SystemTime,
593 entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
594 git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
595 },
596 Symlink {
597 target: PathBuf,
598 },
599}
600
601#[cfg(any(test, feature = "test-support"))]
602impl FakeFsState {
603 fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
604 Ok(self
605 .try_read_path(target, true)
606 .ok_or_else(|| {
607 anyhow!(io::Error::new(
608 io::ErrorKind::NotFound,
609 format!("not found: {}", target.display())
610 ))
611 })?
612 .0)
613 }
614
615 fn try_read_path(
616 &self,
617 target: &Path,
618 follow_symlink: bool,
619 ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
620 let mut path = target.to_path_buf();
621 let mut canonical_path = PathBuf::new();
622 let mut entry_stack = Vec::new();
623 'outer: loop {
624 let mut path_components = path.components().peekable();
625 while let Some(component) = path_components.next() {
626 match component {
627 Component::Prefix(_) => panic!("prefix paths aren't supported"),
628 Component::RootDir => {
629 entry_stack.clear();
630 entry_stack.push(self.root.clone());
631 canonical_path.clear();
632 canonical_path.push("/");
633 }
634 Component::CurDir => {}
635 Component::ParentDir => {
636 entry_stack.pop()?;
637 canonical_path.pop();
638 }
639 Component::Normal(name) => {
640 let current_entry = entry_stack.last().cloned()?;
641 let current_entry = current_entry.lock();
642 if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
643 let entry = entries.get(name.to_str().unwrap()).cloned()?;
644 if path_components.peek().is_some() || follow_symlink {
645 let entry = entry.lock();
646 if let FakeFsEntry::Symlink { target, .. } = &*entry {
647 let mut target = target.clone();
648 target.extend(path_components);
649 path = target;
650 continue 'outer;
651 }
652 }
653 entry_stack.push(entry.clone());
654 canonical_path.push(name);
655 } else {
656 return None;
657 }
658 }
659 }
660 }
661 break;
662 }
663 Some((entry_stack.pop()?, canonical_path))
664 }
665
666 fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
667 where
668 Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
669 {
670 let path = normalize_path(path);
671 let filename = path
672 .file_name()
673 .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
674 let parent_path = path.parent().unwrap();
675
676 let parent = self.read_path(parent_path)?;
677 let mut parent = parent.lock();
678 let new_entry = parent
679 .dir_entries(parent_path)?
680 .entry(filename.to_str().unwrap().into());
681 callback(new_entry)
682 }
683
684 fn emit_event<I, T>(&mut self, paths: I)
685 where
686 I: IntoIterator<Item = T>,
687 T: Into<PathBuf>,
688 {
689 self.buffered_events
690 .extend(paths.into_iter().map(Into::into));
691
692 if !self.events_paused {
693 self.flush_events(self.buffered_events.len());
694 }
695 }
696
697 fn flush_events(&mut self, mut count: usize) {
698 count = count.min(self.buffered_events.len());
699 let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
700 self.event_txs.retain(|tx| {
701 let _ = tx.try_send(events.clone());
702 !tx.is_closed()
703 });
704 }
705}
706
707#[cfg(any(test, feature = "test-support"))]
708lazy_static::lazy_static! {
709 pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
710}
711
712#[cfg(any(test, feature = "test-support"))]
713impl FakeFs {
714 pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
715 Arc::new(Self {
716 executor,
717 state: Mutex::new(FakeFsState {
718 root: Arc::new(Mutex::new(FakeFsEntry::Dir {
719 inode: 0,
720 mtime: SystemTime::UNIX_EPOCH,
721 entries: Default::default(),
722 git_repo_state: None,
723 })),
724 next_mtime: SystemTime::UNIX_EPOCH,
725 next_inode: 1,
726 event_txs: Default::default(),
727 buffered_events: Vec::new(),
728 events_paused: false,
729 read_dir_call_count: 0,
730 metadata_call_count: 0,
731 }),
732 })
733 }
734
735 pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
736 self.write_file_internal(path, content).unwrap()
737 }
738
739 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
740 let mut state = self.state.lock();
741 let path = path.as_ref();
742 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
743 state
744 .write_path(path.as_ref(), move |e| match e {
745 btree_map::Entry::Vacant(e) => {
746 e.insert(file);
747 Ok(())
748 }
749 btree_map::Entry::Occupied(mut e) => {
750 *e.get_mut() = file;
751 Ok(())
752 }
753 })
754 .unwrap();
755 state.emit_event([path]);
756 }
757
758 fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
759 let mut state = self.state.lock();
760 let path = path.as_ref();
761 let inode = state.next_inode;
762 let mtime = state.next_mtime;
763 state.next_inode += 1;
764 state.next_mtime += Duration::from_nanos(1);
765 let file = Arc::new(Mutex::new(FakeFsEntry::File {
766 inode,
767 mtime,
768 content,
769 }));
770 state.write_path(path, move |entry| {
771 match entry {
772 btree_map::Entry::Vacant(e) => {
773 e.insert(file);
774 }
775 btree_map::Entry::Occupied(mut e) => {
776 *e.get_mut() = file;
777 }
778 }
779 Ok(())
780 })?;
781 state.emit_event([path]);
782 Ok(())
783 }
784
785 pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
786 let path = path.as_ref();
787 let path = normalize_path(path);
788 let state = self.state.lock();
789 let entry = state.read_path(&path)?;
790 let entry = entry.lock();
791 entry.file_content(&path).cloned()
792 }
793
794 async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
795 let path = path.as_ref();
796 let path = normalize_path(path);
797 self.simulate_random_delay().await;
798 let state = self.state.lock();
799 let entry = state.read_path(&path)?;
800 let entry = entry.lock();
801 entry.file_content(&path).cloned()
802 }
803
804 pub fn pause_events(&self) {
805 self.state.lock().events_paused = true;
806 }
807
808 pub fn buffered_event_count(&self) -> usize {
809 self.state.lock().buffered_events.len()
810 }
811
812 pub fn flush_events(&self, count: usize) {
813 self.state.lock().flush_events(count);
814 }
815
816 #[must_use]
817 pub fn insert_tree<'a>(
818 &'a self,
819 path: impl 'a + AsRef<Path> + Send,
820 tree: serde_json::Value,
821 ) -> futures::future::BoxFuture<'a, ()> {
822 use futures::FutureExt as _;
823 use serde_json::Value::*;
824
825 async move {
826 let path = path.as_ref();
827
828 match tree {
829 Object(map) => {
830 self.create_dir(path).await.unwrap();
831 for (name, contents) in map {
832 let mut path = PathBuf::from(path);
833 path.push(name);
834 self.insert_tree(&path, contents).await;
835 }
836 }
837 Null => {
838 self.create_dir(path).await.unwrap();
839 }
840 String(contents) => {
841 self.insert_file(&path, contents.into_bytes()).await;
842 }
843 _ => {
844 panic!("JSON object must contain only objects, strings, or null");
845 }
846 }
847 }
848 .boxed()
849 }
850
851 pub fn insert_tree_from_real_fs<'a>(
852 &'a self,
853 path: impl 'a + AsRef<Path> + Send,
854 src_path: impl 'a + AsRef<Path> + Send,
855 ) -> futures::future::BoxFuture<'a, ()> {
856 use futures::FutureExt as _;
857
858 async move {
859 let path = path.as_ref();
860 if std::fs::metadata(&src_path).unwrap().is_file() {
861 let contents = std::fs::read(src_path).unwrap();
862 self.insert_file(path, contents).await;
863 } else {
864 self.create_dir(path).await.unwrap();
865 for entry in std::fs::read_dir(&src_path).unwrap() {
866 let entry = entry.unwrap();
867 self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path())
868 .await;
869 }
870 }
871 }
872 .boxed()
873 }
874
875 pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
876 where
877 F: FnOnce(&mut FakeGitRepositoryState),
878 {
879 let mut state = self.state.lock();
880 let entry = state.read_path(dot_git).unwrap();
881 let mut entry = entry.lock();
882
883 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
884 let repo_state = git_repo_state.get_or_insert_with(Default::default);
885 let mut repo_state = repo_state.lock();
886
887 f(&mut repo_state);
888
889 if emit_git_event {
890 state.emit_event([dot_git]);
891 }
892 } else {
893 panic!("not a directory");
894 }
895 }
896
897 pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
898 self.with_git_state(dot_git, true, |state| {
899 state.branch_name = branch.map(Into::into)
900 })
901 }
902
903 pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
904 self.with_git_state(dot_git, true, |state| {
905 state.index_contents.clear();
906 state.index_contents.extend(
907 head_state
908 .iter()
909 .map(|(path, content)| (path.to_path_buf(), content.clone())),
910 );
911 });
912 }
913
914 pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
915 self.with_git_state(dot_git, true, |state| {
916 state.blames.clear();
917 state.blames.extend(
918 blames
919 .into_iter()
920 .map(|(path, blame)| (path.to_path_buf(), blame)),
921 );
922 });
923 }
924
925 pub fn set_status_for_repo_via_working_copy_change(
926 &self,
927 dot_git: &Path,
928 statuses: &[(&Path, GitFileStatus)],
929 ) {
930 self.with_git_state(dot_git, false, |state| {
931 state.worktree_statuses.clear();
932 state.worktree_statuses.extend(
933 statuses
934 .iter()
935 .map(|(path, content)| ((**path).into(), *content)),
936 );
937 });
938 self.state.lock().emit_event(
939 statuses
940 .iter()
941 .map(|(path, _)| dot_git.parent().unwrap().join(path)),
942 );
943 }
944
945 pub fn set_status_for_repo_via_git_operation(
946 &self,
947 dot_git: &Path,
948 statuses: &[(&Path, GitFileStatus)],
949 ) {
950 self.with_git_state(dot_git, true, |state| {
951 state.worktree_statuses.clear();
952 state.worktree_statuses.extend(
953 statuses
954 .iter()
955 .map(|(path, content)| ((**path).into(), *content)),
956 );
957 });
958 }
959
960 pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
961 let mut result = Vec::new();
962 let mut queue = collections::VecDeque::new();
963 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
964 while let Some((path, entry)) = queue.pop_front() {
965 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
966 for (name, entry) in entries {
967 queue.push_back((path.join(name), entry.clone()));
968 }
969 }
970 if include_dot_git
971 || !path
972 .components()
973 .any(|component| component.as_os_str() == *FS_DOT_GIT)
974 {
975 result.push(path);
976 }
977 }
978 result
979 }
980
981 pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
982 let mut result = Vec::new();
983 let mut queue = collections::VecDeque::new();
984 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
985 while let Some((path, entry)) = queue.pop_front() {
986 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
987 for (name, entry) in entries {
988 queue.push_back((path.join(name), entry.clone()));
989 }
990 if include_dot_git
991 || !path
992 .components()
993 .any(|component| component.as_os_str() == *FS_DOT_GIT)
994 {
995 result.push(path);
996 }
997 }
998 }
999 result
1000 }
1001
1002 pub fn files(&self) -> Vec<PathBuf> {
1003 let mut result = Vec::new();
1004 let mut queue = collections::VecDeque::new();
1005 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
1006 while let Some((path, entry)) = queue.pop_front() {
1007 let e = entry.lock();
1008 match &*e {
1009 FakeFsEntry::File { .. } => result.push(path),
1010 FakeFsEntry::Dir { entries, .. } => {
1011 for (name, entry) in entries {
1012 queue.push_back((path.join(name), entry.clone()));
1013 }
1014 }
1015 FakeFsEntry::Symlink { .. } => {}
1016 }
1017 }
1018 result
1019 }
1020
1021 /// How many `read_dir` calls have been issued.
1022 pub fn read_dir_call_count(&self) -> usize {
1023 self.state.lock().read_dir_call_count
1024 }
1025
1026 /// How many `metadata` calls have been issued.
1027 pub fn metadata_call_count(&self) -> usize {
1028 self.state.lock().metadata_call_count
1029 }
1030
1031 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
1032 self.executor.simulate_random_delay()
1033 }
1034}
1035
1036#[cfg(any(test, feature = "test-support"))]
1037impl FakeFsEntry {
1038 fn is_file(&self) -> bool {
1039 matches!(self, Self::File { .. })
1040 }
1041
1042 fn is_symlink(&self) -> bool {
1043 matches!(self, Self::Symlink { .. })
1044 }
1045
1046 fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
1047 if let Self::File { content, .. } = self {
1048 Ok(content)
1049 } else {
1050 Err(anyhow!("not a file: {}", path.display()))
1051 }
1052 }
1053
1054 fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
1055 if let Self::File { content, mtime, .. } = self {
1056 *mtime = SystemTime::now();
1057 *content = new_content;
1058 Ok(())
1059 } else {
1060 Err(anyhow!("not a file: {}", path.display()))
1061 }
1062 }
1063
1064 fn dir_entries(
1065 &mut self,
1066 path: &Path,
1067 ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
1068 if let Self::Dir { entries, .. } = self {
1069 Ok(entries)
1070 } else {
1071 Err(anyhow!("not a directory: {}", path.display()))
1072 }
1073 }
1074}
1075
1076#[cfg(any(test, feature = "test-support"))]
1077#[async_trait::async_trait]
1078impl Fs for FakeFs {
1079 async fn create_dir(&self, path: &Path) -> Result<()> {
1080 self.simulate_random_delay().await;
1081
1082 let mut created_dirs = Vec::new();
1083 let mut cur_path = PathBuf::new();
1084 for component in path.components() {
1085 let mut state = self.state.lock();
1086 cur_path.push(component);
1087 if cur_path == Path::new("/") {
1088 continue;
1089 }
1090
1091 let inode = state.next_inode;
1092 let mtime = state.next_mtime;
1093 state.next_mtime += Duration::from_nanos(1);
1094 state.next_inode += 1;
1095 state.write_path(&cur_path, |entry| {
1096 entry.or_insert_with(|| {
1097 created_dirs.push(cur_path.clone());
1098 Arc::new(Mutex::new(FakeFsEntry::Dir {
1099 inode,
1100 mtime,
1101 entries: Default::default(),
1102 git_repo_state: None,
1103 }))
1104 });
1105 Ok(())
1106 })?
1107 }
1108
1109 self.state.lock().emit_event(&created_dirs);
1110 Ok(())
1111 }
1112
1113 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
1114 self.simulate_random_delay().await;
1115 let mut state = self.state.lock();
1116 let inode = state.next_inode;
1117 let mtime = state.next_mtime;
1118 state.next_mtime += Duration::from_nanos(1);
1119 state.next_inode += 1;
1120 let file = Arc::new(Mutex::new(FakeFsEntry::File {
1121 inode,
1122 mtime,
1123 content: Vec::new(),
1124 }));
1125 state.write_path(path, |entry| {
1126 match entry {
1127 btree_map::Entry::Occupied(mut e) => {
1128 if options.overwrite {
1129 *e.get_mut() = file;
1130 } else if !options.ignore_if_exists {
1131 return Err(anyhow!("path already exists: {}", path.display()));
1132 }
1133 }
1134 btree_map::Entry::Vacant(e) => {
1135 e.insert(file);
1136 }
1137 }
1138 Ok(())
1139 })?;
1140 state.emit_event([path]);
1141 Ok(())
1142 }
1143
1144 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
1145 let mut state = self.state.lock();
1146 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
1147 state
1148 .write_path(path.as_ref(), move |e| match e {
1149 btree_map::Entry::Vacant(e) => {
1150 e.insert(file);
1151 Ok(())
1152 }
1153 btree_map::Entry::Occupied(mut e) => {
1154 *e.get_mut() = file;
1155 Ok(())
1156 }
1157 })
1158 .unwrap();
1159 state.emit_event(&[path]);
1160 Ok(())
1161 }
1162
1163 async fn create_file_with(
1164 &self,
1165 path: &Path,
1166 mut content: Pin<&mut (dyn AsyncRead + Send)>,
1167 ) -> Result<()> {
1168 let mut bytes = Vec::new();
1169 content.read_to_end(&mut bytes).await?;
1170 self.write_file_internal(path, bytes)?;
1171 Ok(())
1172 }
1173
1174 async fn extract_tar_file(
1175 &self,
1176 path: &Path,
1177 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
1178 ) -> Result<()> {
1179 let mut entries = content.entries()?;
1180 while let Some(entry) = entries.next().await {
1181 let mut entry = entry?;
1182 if entry.header().entry_type().is_file() {
1183 let path = path.join(entry.path()?.as_ref());
1184 let mut bytes = Vec::new();
1185 entry.read_to_end(&mut bytes).await?;
1186 self.create_dir(path.parent().unwrap()).await?;
1187 self.write_file_internal(&path, bytes)?;
1188 }
1189 }
1190 Ok(())
1191 }
1192
1193 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
1194 self.simulate_random_delay().await;
1195
1196 let old_path = normalize_path(old_path);
1197 let new_path = normalize_path(new_path);
1198
1199 let mut state = self.state.lock();
1200 let moved_entry = state.write_path(&old_path, |e| {
1201 if let btree_map::Entry::Occupied(e) = e {
1202 Ok(e.get().clone())
1203 } else {
1204 Err(anyhow!("path does not exist: {}", &old_path.display()))
1205 }
1206 })?;
1207
1208 state.write_path(&new_path, |e| {
1209 match e {
1210 btree_map::Entry::Occupied(mut e) => {
1211 if options.overwrite {
1212 *e.get_mut() = moved_entry;
1213 } else if !options.ignore_if_exists {
1214 return Err(anyhow!("path already exists: {}", new_path.display()));
1215 }
1216 }
1217 btree_map::Entry::Vacant(e) => {
1218 e.insert(moved_entry);
1219 }
1220 }
1221 Ok(())
1222 })?;
1223
1224 state
1225 .write_path(&old_path, |e| {
1226 if let btree_map::Entry::Occupied(e) = e {
1227 Ok(e.remove())
1228 } else {
1229 unreachable!()
1230 }
1231 })
1232 .unwrap();
1233
1234 state.emit_event(&[old_path, new_path]);
1235 Ok(())
1236 }
1237
1238 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
1239 self.simulate_random_delay().await;
1240
1241 let source = normalize_path(source);
1242 let target = normalize_path(target);
1243 let mut state = self.state.lock();
1244 let mtime = state.next_mtime;
1245 let inode = util::post_inc(&mut state.next_inode);
1246 state.next_mtime += Duration::from_nanos(1);
1247 let source_entry = state.read_path(&source)?;
1248 let content = source_entry.lock().file_content(&source)?.clone();
1249 let entry = state.write_path(&target, |e| match e {
1250 btree_map::Entry::Occupied(e) => {
1251 if options.overwrite {
1252 Ok(Some(e.get().clone()))
1253 } else if !options.ignore_if_exists {
1254 return Err(anyhow!("{target:?} already exists"));
1255 } else {
1256 Ok(None)
1257 }
1258 }
1259 btree_map::Entry::Vacant(e) => Ok(Some(
1260 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
1261 inode,
1262 mtime,
1263 content: Vec::new(),
1264 })))
1265 .clone(),
1266 )),
1267 })?;
1268 if let Some(entry) = entry {
1269 entry.lock().set_file_content(&target, content)?;
1270 }
1271 state.emit_event(&[target]);
1272 Ok(())
1273 }
1274
1275 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1276 self.simulate_random_delay().await;
1277
1278 let path = normalize_path(path);
1279 let parent_path = path
1280 .parent()
1281 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1282 let base_name = path.file_name().unwrap();
1283
1284 let mut state = self.state.lock();
1285 let parent_entry = state.read_path(parent_path)?;
1286 let mut parent_entry = parent_entry.lock();
1287 let entry = parent_entry
1288 .dir_entries(parent_path)?
1289 .entry(base_name.to_str().unwrap().into());
1290
1291 match entry {
1292 btree_map::Entry::Vacant(_) => {
1293 if !options.ignore_if_not_exists {
1294 return Err(anyhow!("{path:?} does not exist"));
1295 }
1296 }
1297 btree_map::Entry::Occupied(e) => {
1298 {
1299 let mut entry = e.get().lock();
1300 let children = entry.dir_entries(&path)?;
1301 if !options.recursive && !children.is_empty() {
1302 return Err(anyhow!("{path:?} is not empty"));
1303 }
1304 }
1305 e.remove();
1306 }
1307 }
1308 state.emit_event(&[path]);
1309 Ok(())
1310 }
1311
1312 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1313 self.simulate_random_delay().await;
1314
1315 let path = normalize_path(path);
1316 let parent_path = path
1317 .parent()
1318 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1319 let base_name = path.file_name().unwrap();
1320 let mut state = self.state.lock();
1321 let parent_entry = state.read_path(parent_path)?;
1322 let mut parent_entry = parent_entry.lock();
1323 let entry = parent_entry
1324 .dir_entries(parent_path)?
1325 .entry(base_name.to_str().unwrap().into());
1326 match entry {
1327 btree_map::Entry::Vacant(_) => {
1328 if !options.ignore_if_not_exists {
1329 return Err(anyhow!("{path:?} does not exist"));
1330 }
1331 }
1332 btree_map::Entry::Occupied(e) => {
1333 e.get().lock().file_content(&path)?;
1334 e.remove();
1335 }
1336 }
1337 state.emit_event(&[path]);
1338 Ok(())
1339 }
1340
1341 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
1342 let bytes = self.load_internal(path).await?;
1343 Ok(Box::new(io::Cursor::new(bytes)))
1344 }
1345
1346 async fn load(&self, path: &Path) -> Result<String> {
1347 let content = self.load_internal(path).await?;
1348 Ok(String::from_utf8(content.clone())?)
1349 }
1350
1351 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
1352 self.simulate_random_delay().await;
1353 let path = normalize_path(path.as_path());
1354 self.write_file_internal(path, data.into_bytes())?;
1355 Ok(())
1356 }
1357
1358 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
1359 self.simulate_random_delay().await;
1360 let path = normalize_path(path);
1361 let content = chunks(text, line_ending).collect::<String>();
1362 if let Some(path) = path.parent() {
1363 self.create_dir(path).await?;
1364 }
1365 self.write_file_internal(path, content.into_bytes())?;
1366 Ok(())
1367 }
1368
1369 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
1370 let path = normalize_path(path);
1371 self.simulate_random_delay().await;
1372 let state = self.state.lock();
1373 if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
1374 Ok(canonical_path)
1375 } else {
1376 Err(anyhow!("path does not exist: {}", path.display()))
1377 }
1378 }
1379
1380 async fn is_file(&self, path: &Path) -> bool {
1381 let path = normalize_path(path);
1382 self.simulate_random_delay().await;
1383 let state = self.state.lock();
1384 if let Some((entry, _)) = state.try_read_path(&path, true) {
1385 entry.lock().is_file()
1386 } else {
1387 false
1388 }
1389 }
1390
1391 async fn is_dir(&self, path: &Path) -> bool {
1392 self.metadata(path)
1393 .await
1394 .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
1395 }
1396
1397 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
1398 self.simulate_random_delay().await;
1399 let path = normalize_path(path);
1400 let mut state = self.state.lock();
1401 state.metadata_call_count += 1;
1402 if let Some((mut entry, _)) = state.try_read_path(&path, false) {
1403 let is_symlink = entry.lock().is_symlink();
1404 if is_symlink {
1405 if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
1406 entry = e;
1407 } else {
1408 return Ok(None);
1409 }
1410 }
1411
1412 let entry = entry.lock();
1413 Ok(Some(match &*entry {
1414 FakeFsEntry::File { inode, mtime, .. } => Metadata {
1415 inode: *inode,
1416 mtime: *mtime,
1417 is_dir: false,
1418 is_symlink,
1419 },
1420 FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
1421 inode: *inode,
1422 mtime: *mtime,
1423 is_dir: true,
1424 is_symlink,
1425 },
1426 FakeFsEntry::Symlink { .. } => unreachable!(),
1427 }))
1428 } else {
1429 Ok(None)
1430 }
1431 }
1432
1433 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
1434 self.simulate_random_delay().await;
1435 let path = normalize_path(path);
1436 let state = self.state.lock();
1437 if let Some((entry, _)) = state.try_read_path(&path, false) {
1438 let entry = entry.lock();
1439 if let FakeFsEntry::Symlink { target } = &*entry {
1440 Ok(target.clone())
1441 } else {
1442 Err(anyhow!("not a symlink: {}", path.display()))
1443 }
1444 } else {
1445 Err(anyhow!("path does not exist: {}", path.display()))
1446 }
1447 }
1448
1449 async fn read_dir(
1450 &self,
1451 path: &Path,
1452 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
1453 self.simulate_random_delay().await;
1454 let path = normalize_path(path);
1455 let mut state = self.state.lock();
1456 state.read_dir_call_count += 1;
1457 let entry = state.read_path(&path)?;
1458 let mut entry = entry.lock();
1459 let children = entry.dir_entries(&path)?;
1460 let paths = children
1461 .keys()
1462 .map(|file_name| Ok(path.join(file_name)))
1463 .collect::<Vec<_>>();
1464 Ok(Box::pin(futures::stream::iter(paths)))
1465 }
1466
1467 async fn watch(
1468 &self,
1469 path: &Path,
1470 _: Duration,
1471 ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
1472 self.simulate_random_delay().await;
1473 let (tx, rx) = smol::channel::unbounded();
1474 self.state.lock().event_txs.push(tx);
1475 let path = path.to_path_buf();
1476 let executor = self.executor.clone();
1477 Box::pin(futures::StreamExt::filter(rx, move |events| {
1478 let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
1479 let executor = executor.clone();
1480 async move {
1481 executor.simulate_random_delay().await;
1482 result
1483 }
1484 }))
1485 }
1486
1487 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
1488 let state = self.state.lock();
1489 let entry = state.read_path(abs_dot_git).unwrap();
1490 let mut entry = entry.lock();
1491 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
1492 let state = git_repo_state
1493 .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
1494 .clone();
1495 Some(git::repository::FakeGitRepository::open(state))
1496 } else {
1497 None
1498 }
1499 }
1500
1501 fn is_fake(&self) -> bool {
1502 true
1503 }
1504
1505 async fn is_case_sensitive(&self) -> Result<bool> {
1506 Ok(true)
1507 }
1508
1509 #[cfg(any(test, feature = "test-support"))]
1510 fn as_fake(&self) -> &FakeFs {
1511 self
1512 }
1513}
1514
1515fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
1516 rope.chunks().flat_map(move |chunk| {
1517 let mut newline = false;
1518 chunk.split('\n').flat_map(move |line| {
1519 let ending = if newline {
1520 Some(line_ending.as_str())
1521 } else {
1522 None
1523 };
1524 newline = true;
1525 ending.into_iter().chain([line])
1526 })
1527 })
1528}
1529
1530pub fn normalize_path(path: &Path) -> PathBuf {
1531 let mut components = path.components().peekable();
1532 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1533 components.next();
1534 PathBuf::from(c.as_os_str())
1535 } else {
1536 PathBuf::new()
1537 };
1538
1539 for component in components {
1540 match component {
1541 Component::Prefix(..) => unreachable!(),
1542 Component::RootDir => {
1543 ret.push(component.as_os_str());
1544 }
1545 Component::CurDir => {}
1546 Component::ParentDir => {
1547 ret.pop();
1548 }
1549 Component::Normal(c) => {
1550 ret.push(c);
1551 }
1552 }
1553 }
1554 ret
1555}
1556
1557pub fn copy_recursive<'a>(
1558 fs: &'a dyn Fs,
1559 source: &'a Path,
1560 target: &'a Path,
1561 options: CopyOptions,
1562) -> BoxFuture<'a, Result<()>> {
1563 use futures::future::FutureExt;
1564
1565 async move {
1566 let metadata = fs
1567 .metadata(source)
1568 .await?
1569 .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
1570 if metadata.is_dir {
1571 if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
1572 if options.ignore_if_exists {
1573 return Ok(());
1574 } else {
1575 return Err(anyhow!("{target:?} already exists"));
1576 }
1577 }
1578
1579 let _ = fs
1580 .remove_dir(
1581 target,
1582 RemoveOptions {
1583 recursive: true,
1584 ignore_if_not_exists: true,
1585 },
1586 )
1587 .await;
1588 fs.create_dir(target).await?;
1589 let mut children = fs.read_dir(source).await?;
1590 while let Some(child_path) = children.next().await {
1591 if let Ok(child_path) = child_path {
1592 if let Some(file_name) = child_path.file_name() {
1593 let child_target_path = target.join(file_name);
1594 copy_recursive(fs, &child_path, &child_target_path, options).await?;
1595 }
1596 }
1597 }
1598
1599 Ok(())
1600 } else {
1601 fs.copy_file(source, target, options).await
1602 }
1603 }
1604 .boxed()
1605}
1606
1607// todo(windows)
1608// can we get file id not open the file twice?
1609// https://github.com/rust-lang/rust/issues/63010
1610#[cfg(target_os = "windows")]
1611async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
1612 use std::os::windows::io::AsRawHandle;
1613
1614 use smol::fs::windows::OpenOptionsExt;
1615 use windows::Win32::{
1616 Foundation::HANDLE,
1617 Storage::FileSystem::{
1618 GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
1619 },
1620 };
1621
1622 let file = smol::fs::OpenOptions::new()
1623 .read(true)
1624 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
1625 .open(path)
1626 .await?;
1627
1628 let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
1629 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
1630 // This function supports Windows XP+
1631 smol::unblock(move || {
1632 unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
1633
1634 Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
1635 })
1636 .await
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641 use super::*;
1642 use gpui::BackgroundExecutor;
1643 use serde_json::json;
1644
1645 #[gpui::test]
1646 async fn test_fake_fs(executor: BackgroundExecutor) {
1647 let fs = FakeFs::new(executor.clone());
1648 fs.insert_tree(
1649 "/root",
1650 json!({
1651 "dir1": {
1652 "a": "A",
1653 "b": "B"
1654 },
1655 "dir2": {
1656 "c": "C",
1657 "dir3": {
1658 "d": "D"
1659 }
1660 }
1661 }),
1662 )
1663 .await;
1664
1665 assert_eq!(
1666 fs.files(),
1667 vec![
1668 PathBuf::from("/root/dir1/a"),
1669 PathBuf::from("/root/dir1/b"),
1670 PathBuf::from("/root/dir2/c"),
1671 PathBuf::from("/root/dir2/dir3/d"),
1672 ]
1673 );
1674
1675 fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
1676 .await
1677 .unwrap();
1678
1679 assert_eq!(
1680 fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
1681 .await
1682 .unwrap(),
1683 PathBuf::from("/root/dir2/dir3"),
1684 );
1685 assert_eq!(
1686 fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
1687 .await
1688 .unwrap(),
1689 PathBuf::from("/root/dir2/dir3/d"),
1690 );
1691 assert_eq!(
1692 fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
1693 "D",
1694 );
1695 }
1696}