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