fs.rs

   1#[cfg(target_os = "macos")]
   2mod mac_watcher;
   3
   4pub mod encodings;
   5#[cfg(not(target_os = "macos"))]
   6pub mod fs_watcher;
   7
   8use anyhow::{Context as _, Result, anyhow};
   9#[cfg(any(target_os = "linux", target_os = "freebsd"))]
  10use ashpd::desktop::trash;
  11use futures::stream::iter;
  12use gpui::App;
  13use gpui::BackgroundExecutor;
  14use gpui::Global;
  15use gpui::ReadGlobal as _;
  16use std::borrow::Cow;
  17use util::command::new_smol_command;
  18
  19#[cfg(unix)]
  20use std::os::fd::{AsFd, AsRawFd};
  21
  22#[cfg(unix)]
  23use std::os::unix::fs::{FileTypeExt, MetadataExt};
  24
  25#[cfg(any(target_os = "macos", target_os = "freebsd"))]
  26use std::mem::MaybeUninit;
  27
  28use async_tar::Archive;
  29use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};
  30use git::repository::{GitRepository, RealGitRepository};
  31use rope::Rope;
  32use serde::{Deserialize, Serialize};
  33use smol::io::AsyncWriteExt;
  34use std::{
  35    io::{self, Write},
  36    path::{Component, Path, PathBuf},
  37    pin::Pin,
  38    sync::Arc,
  39    time::{Duration, SystemTime, UNIX_EPOCH},
  40};
  41use tempfile::TempDir;
  42use text::LineEnding;
  43
  44#[cfg(any(test, feature = "test-support"))]
  45mod fake_git_repo;
  46#[cfg(any(test, feature = "test-support"))]
  47use collections::{BTreeMap, btree_map};
  48#[cfg(any(test, feature = "test-support"))]
  49use fake_git_repo::FakeGitRepositoryState;
  50#[cfg(any(test, feature = "test-support"))]
  51use git::{
  52    repository::{RepoPath, repo_path},
  53    status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
  54};
  55#[cfg(any(test, feature = "test-support"))]
  56use parking_lot::Mutex;
  57#[cfg(any(test, feature = "test-support"))]
  58use smol::io::AsyncReadExt;
  59#[cfg(any(test, feature = "test-support"))]
  60use std::ffi::OsStr;
  61
  62#[cfg(any(test, feature = "test-support"))]
  63pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK};
  64use crate::encodings::EncodingWrapper;
  65use crate::encodings::from_utf8;
  66
  67pub trait Watcher: Send + Sync {
  68    fn add(&self, path: &Path) -> Result<()>;
  69    fn remove(&self, path: &Path) -> Result<()>;
  70}
  71
  72#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
  73pub enum PathEventKind {
  74    Removed,
  75    Created,
  76    Changed,
  77}
  78
  79#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
  80pub struct PathEvent {
  81    pub path: PathBuf,
  82    pub kind: Option<PathEventKind>,
  83}
  84
  85impl From<PathEvent> for PathBuf {
  86    fn from(event: PathEvent) -> Self {
  87        event.path
  88    }
  89}
  90
  91#[async_trait::async_trait]
  92pub trait Fs: Send + Sync {
  93    async fn create_dir(&self, path: &Path) -> Result<()>;
  94    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>;
  95    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
  96    async fn create_file_with(
  97        &self,
  98        path: &Path,
  99        content: Pin<&mut (dyn AsyncRead + Send)>,
 100    ) -> Result<()>;
 101    async fn extract_tar_file(
 102        &self,
 103        path: &Path,
 104        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
 105    ) -> Result<()>;
 106    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
 107    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
 108    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
 109    async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 110        self.remove_dir(path, options).await
 111    }
 112    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
 113    async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 114        self.remove_file(path, options).await
 115    }
 116    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>;
 117    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>>;
 118    async fn load(&self, path: &Path) -> Result<String> {
 119        Ok(String::from_utf8(self.load_bytes(path).await?)?)
 120    }
 121
 122    /// Load a file with the specified encoding, returning a UTF-8 string.
 123    async fn load_with_encoding(
 124        &self,
 125        path: PathBuf,
 126        encoding: EncodingWrapper,
 127    ) -> anyhow::Result<String> {
 128        Ok(encodings::to_utf8(self.load_bytes(path.as_path()).await?, encoding).await?)
 129    }
 130
 131    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
 132    async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
 133    async fn save(
 134        &self,
 135        path: &Path,
 136        text: &Rope,
 137        line_ending: LineEnding,
 138        encoding: EncodingWrapper,
 139    ) -> Result<()>;
 140    async fn write(&self, path: &Path, content: &[u8]) -> Result<()>;
 141    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
 142    async fn is_file(&self, path: &Path) -> bool;
 143    async fn is_dir(&self, path: &Path) -> bool;
 144    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
 145    async fn read_link(&self, path: &Path) -> Result<PathBuf>;
 146    async fn read_dir(
 147        &self,
 148        path: &Path,
 149    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
 150
 151    async fn watch(
 152        &self,
 153        path: &Path,
 154        latency: Duration,
 155    ) -> (
 156        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
 157        Arc<dyn Watcher>,
 158    );
 159
 160    fn open_repo(
 161        &self,
 162        abs_dot_git: &Path,
 163        system_git_binary_path: Option<&Path>,
 164    ) -> Option<Arc<dyn GitRepository>>;
 165    async fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String)
 166    -> Result<()>;
 167    async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>;
 168    fn is_fake(&self) -> bool;
 169    async fn is_case_sensitive(&self) -> Result<bool>;
 170
 171    #[cfg(any(test, feature = "test-support"))]
 172    fn as_fake(&self) -> Arc<FakeFs> {
 173        panic!("called as_fake on a real fs");
 174    }
 175}
 176
 177struct GlobalFs(Arc<dyn Fs>);
 178
 179impl Global for GlobalFs {}
 180
 181impl dyn Fs {
 182    /// Returns the global [`Fs`].
 183    pub fn global(cx: &App) -> Arc<Self> {
 184        GlobalFs::global(cx).0.clone()
 185    }
 186
 187    /// Sets the global [`Fs`].
 188    pub fn set_global(fs: Arc<Self>, cx: &mut App) {
 189        cx.set_global(GlobalFs(fs));
 190    }
 191}
 192
 193#[derive(Copy, Clone, Default)]
 194pub struct CreateOptions {
 195    pub overwrite: bool,
 196    pub ignore_if_exists: bool,
 197}
 198
 199#[derive(Copy, Clone, Default)]
 200pub struct CopyOptions {
 201    pub overwrite: bool,
 202    pub ignore_if_exists: bool,
 203}
 204
 205#[derive(Copy, Clone, Default)]
 206pub struct RenameOptions {
 207    pub overwrite: bool,
 208    pub ignore_if_exists: bool,
 209}
 210
 211#[derive(Copy, Clone, Default)]
 212pub struct RemoveOptions {
 213    pub recursive: bool,
 214    pub ignore_if_not_exists: bool,
 215}
 216
 217#[derive(Copy, Clone, Debug)]
 218pub struct Metadata {
 219    pub inode: u64,
 220    pub mtime: MTime,
 221    pub is_symlink: bool,
 222    pub is_dir: bool,
 223    pub len: u64,
 224    pub is_fifo: bool,
 225}
 226
 227/// Filesystem modification time. The purpose of this newtype is to discourage use of operations
 228/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using
 229/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it
 230/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).
 231///
 232/// Do not derive Ord, PartialOrd, or arithmetic operation traits.
 233#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
 234#[serde(transparent)]
 235pub struct MTime(SystemTime);
 236
 237impl MTime {
 238    /// Conversion intended for persistence and testing.
 239    pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self {
 240        MTime(UNIX_EPOCH + Duration::new(secs, nanos))
 241    }
 242
 243    /// Conversion intended for persistence.
 244    pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> {
 245        self.0
 246            .duration_since(UNIX_EPOCH)
 247            .ok()
 248            .map(|duration| (duration.as_secs(), duration.subsec_nanos()))
 249    }
 250
 251    /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including
 252    /// "_for_user" is to discourage misuse - this method should not be used when making decisions
 253    /// about file dirtiness.
 254    pub fn timestamp_for_user(self) -> SystemTime {
 255        self.0
 256    }
 257
 258    /// Temporary method to split out the behavior changes from introduction of this newtype.
 259    pub fn bad_is_greater_than(self, other: MTime) -> bool {
 260        self.0 > other.0
 261    }
 262}
 263
 264impl From<proto::Timestamp> for MTime {
 265    fn from(timestamp: proto::Timestamp) -> Self {
 266        MTime(timestamp.into())
 267    }
 268}
 269
 270impl From<MTime> for proto::Timestamp {
 271    fn from(mtime: MTime) -> Self {
 272        mtime.0.into()
 273    }
 274}
 275
 276pub struct RealFs {
 277    bundled_git_binary_path: Option<PathBuf>,
 278    executor: BackgroundExecutor,
 279}
 280
 281pub trait FileHandle: Send + Sync + std::fmt::Debug {
 282    fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>;
 283}
 284
 285impl FileHandle for std::fs::File {
 286    #[cfg(target_os = "macos")]
 287    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
 288        use std::{
 289            ffi::{CStr, OsStr},
 290            os::unix::ffi::OsStrExt,
 291        };
 292
 293        let fd = self.as_fd();
 294        let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit();
 295
 296        let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
 297        if result == -1 {
 298            anyhow::bail!("fcntl returned -1".to_string());
 299        }
 300
 301        // SAFETY: `fcntl` will initialize the path buffer.
 302        let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) };
 303        let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
 304        Ok(path)
 305    }
 306
 307    #[cfg(target_os = "linux")]
 308    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
 309        let fd = self.as_fd();
 310        let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd());
 311        let new_path = std::fs::read_link(fd_path)?;
 312        if new_path
 313            .file_name()
 314            .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)"))
 315        {
 316            anyhow::bail!("file was deleted")
 317        };
 318
 319        Ok(new_path)
 320    }
 321
 322    #[cfg(target_os = "freebsd")]
 323    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
 324        use std::{
 325            ffi::{CStr, OsStr},
 326            os::unix::ffi::OsStrExt,
 327        };
 328
 329        let fd = self.as_fd();
 330        let mut kif = MaybeUninit::<libc::kinfo_file>::uninit();
 331        kif.kf_structsize = libc::KINFO_FILE_SIZE;
 332
 333        let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) };
 334        if result == -1 {
 335            anyhow::bail!("fcntl returned -1".to_string());
 336        }
 337
 338        // SAFETY: `fcntl` will initialize the kif.
 339        let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) };
 340        let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
 341        Ok(path)
 342    }
 343
 344    #[cfg(target_os = "windows")]
 345    fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
 346        use std::ffi::OsString;
 347        use std::os::windows::ffi::OsStringExt;
 348        use std::os::windows::io::AsRawHandle;
 349
 350        use windows::Win32::Foundation::HANDLE;
 351        use windows::Win32::Storage::FileSystem::{
 352            FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW,
 353        };
 354
 355        let handle = HANDLE(self.as_raw_handle() as _);
 356
 357        // Query required buffer size (in wide chars)
 358        let required_len =
 359            unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) };
 360        if required_len == 0 {
 361            anyhow::bail!("GetFinalPathNameByHandleW returned 0 length");
 362        }
 363
 364        // Allocate buffer and retrieve the path
 365        let mut buf: Vec<u16> = vec![0u16; required_len as usize + 1];
 366        let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) };
 367        if written == 0 {
 368            anyhow::bail!("GetFinalPathNameByHandleW failed to write path");
 369        }
 370
 371        let os_str: OsString = OsString::from_wide(&buf[..written as usize]);
 372        Ok(PathBuf::from(os_str))
 373    }
 374}
 375
 376pub struct RealWatcher {}
 377
 378impl RealFs {
 379    pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
 380        Self {
 381            bundled_git_binary_path: git_binary_path,
 382            executor,
 383        }
 384    }
 385}
 386
 387#[async_trait::async_trait]
 388impl Fs for RealFs {
 389    async fn create_dir(&self, path: &Path) -> Result<()> {
 390        Ok(smol::fs::create_dir_all(path).await?)
 391    }
 392
 393    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
 394        #[cfg(unix)]
 395        smol::fs::unix::symlink(target, path).await?;
 396
 397        #[cfg(windows)]
 398        if smol::fs::metadata(&target).await?.is_dir() {
 399            let status = smol::process::Command::new("cmd")
 400                .args(["/C", "mklink", "/J"])
 401                .args([path, target.as_path()])
 402                .status()
 403                .await?;
 404
 405            if !status.success() {
 406                return Err(anyhow::anyhow!(
 407                    "Failed to create junction from {:?} to {:?}",
 408                    path,
 409                    target
 410                ));
 411            }
 412        } else {
 413            smol::fs::windows::symlink_file(target, path).await?
 414        }
 415
 416        Ok(())
 417    }
 418
 419    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
 420        let mut open_options = smol::fs::OpenOptions::new();
 421        open_options.write(true).create(true);
 422        if options.overwrite {
 423            open_options.truncate(true);
 424        } else if !options.ignore_if_exists {
 425            open_options.create_new(true);
 426        }
 427        open_options.open(path).await?;
 428        Ok(())
 429    }
 430
 431    async fn create_file_with(
 432        &self,
 433        path: &Path,
 434        content: Pin<&mut (dyn AsyncRead + Send)>,
 435    ) -> Result<()> {
 436        let mut file = smol::fs::File::create(&path).await?;
 437        futures::io::copy(content, &mut file).await?;
 438        Ok(())
 439    }
 440
 441    async fn extract_tar_file(
 442        &self,
 443        path: &Path,
 444        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
 445    ) -> Result<()> {
 446        content.unpack(path).await?;
 447        Ok(())
 448    }
 449
 450    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
 451        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
 452            if options.ignore_if_exists {
 453                return Ok(());
 454            } else {
 455                anyhow::bail!("{target:?} already exists");
 456            }
 457        }
 458
 459        smol::fs::copy(source, target).await?;
 460        Ok(())
 461    }
 462
 463    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
 464        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
 465            if options.ignore_if_exists {
 466                return Ok(());
 467            } else {
 468                anyhow::bail!("{target:?} already exists");
 469            }
 470        }
 471
 472        smol::fs::rename(source, target).await?;
 473        Ok(())
 474    }
 475
 476    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 477        let result = if options.recursive {
 478            smol::fs::remove_dir_all(path).await
 479        } else {
 480            smol::fs::remove_dir(path).await
 481        };
 482        match result {
 483            Ok(()) => Ok(()),
 484            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
 485                Ok(())
 486            }
 487            Err(err) => Err(err)?,
 488        }
 489    }
 490
 491    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 492        #[cfg(windows)]
 493        if let Ok(Some(metadata)) = self.metadata(path).await
 494            && metadata.is_symlink
 495            && metadata.is_dir
 496        {
 497            self.remove_dir(
 498                path,
 499                RemoveOptions {
 500                    recursive: false,
 501                    ignore_if_not_exists: true,
 502                },
 503            )
 504            .await?;
 505            return Ok(());
 506        }
 507
 508        match smol::fs::remove_file(path).await {
 509            Ok(()) => Ok(()),
 510            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
 511                Ok(())
 512            }
 513            Err(err) => Err(err)?,
 514        }
 515    }
 516
 517    #[cfg(target_os = "macos")]
 518    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
 519        use cocoa::{
 520            base::{id, nil},
 521            foundation::{NSAutoreleasePool, NSString},
 522        };
 523        use objc::{class, msg_send, sel, sel_impl};
 524
 525        unsafe {
 526            unsafe fn ns_string(string: &str) -> id {
 527                unsafe { NSString::alloc(nil).init_str(string).autorelease() }
 528            }
 529
 530            let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
 531            let array: id = msg_send![class!(NSArray), arrayWithObject: url];
 532            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
 533
 534            let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
 535        }
 536        Ok(())
 537    }
 538
 539    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 540    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
 541        if let Ok(Some(metadata)) = self.metadata(path).await
 542            && metadata.is_symlink
 543        {
 544            // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
 545            return self.remove_file(path, RemoveOptions::default()).await;
 546        }
 547        let file = smol::fs::File::open(path).await?;
 548        match trash::trash_file(&file.as_fd()).await {
 549            Ok(_) => Ok(()),
 550            Err(err) => {
 551                log::error!("Failed to trash file: {}", err);
 552                // Trashing files can fail if you don't have a trashing dbus service configured.
 553                // In that case, delete the file directly instead.
 554                return self.remove_file(path, RemoveOptions::default()).await;
 555            }
 556        }
 557    }
 558
 559    #[cfg(target_os = "windows")]
 560    async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
 561        use util::paths::SanitizedPath;
 562        use windows::{
 563            Storage::{StorageDeleteOption, StorageFile},
 564            core::HSTRING,
 565        };
 566        // todo(windows)
 567        // When new version of `windows-rs` release, make this operation `async`
 568        let path = path.canonicalize()?;
 569        let path = SanitizedPath::new(&path);
 570        let path_string = path.to_string();
 571        let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
 572        file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
 573        Ok(())
 574    }
 575
 576    #[cfg(target_os = "macos")]
 577    async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 578        self.trash_file(path, options).await
 579    }
 580
 581    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 582    async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
 583        self.trash_file(path, options).await
 584    }
 585
 586    #[cfg(target_os = "windows")]
 587    async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
 588        use util::paths::SanitizedPath;
 589        use windows::{
 590            Storage::{StorageDeleteOption, StorageFolder},
 591            core::HSTRING,
 592        };
 593
 594        // todo(windows)
 595        // When new version of `windows-rs` release, make this operation `async`
 596        let path = path.canonicalize()?;
 597        let path = SanitizedPath::new(&path);
 598        let path_string = path.to_string();
 599        let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
 600        folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
 601        Ok(())
 602    }
 603
 604    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>> {
 605        Ok(Box::new(std::fs::File::open(path)?))
 606    }
 607
 608    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
 609        let mut options = std::fs::OpenOptions::new();
 610        options.read(true);
 611        #[cfg(windows)]
 612        {
 613            use std::os::windows::fs::OpenOptionsExt;
 614            options.custom_flags(windows::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS.0);
 615        }
 616        Ok(Arc::new(options.open(path)?))
 617    }
 618
 619    async fn load(&self, path: &Path) -> Result<String> {
 620        let path = path.to_path_buf();
 621        let encoding = EncodingWrapper::new(encoding::all::UTF_8);
 622        let text =
 623            smol::unblock(async || Ok(encodings::to_utf8(std::fs::read(path)?, encoding).await?))
 624                .await
 625                .await;
 626        text
 627    }
 628
 629    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
 630        let path = path.to_path_buf();
 631        let bytes = self
 632            .executor
 633            .spawn(async move { std::fs::read(path) })
 634            .await?;
 635        Ok(bytes)
 636    }
 637
 638    #[cfg(not(target_os = "windows"))]
 639    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
 640        smol::unblock(move || {
 641            // Use the directory of the destination as temp dir to avoid
 642            // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
 643            // See https://github.com/zed-industries/zed/pull/8437 for more details.
 644            let mut tmp_file =
 645                tempfile::NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?;
 646            tmp_file.write_all(data.as_bytes())?;
 647            tmp_file.persist(path)?;
 648            anyhow::Ok(())
 649        })
 650        .await?;
 651
 652        Ok(())
 653    }
 654
 655    #[cfg(target_os = "windows")]
 656    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
 657        smol::unblock(move || {
 658            // If temp dir is set to a different drive than the destination,
 659            // we receive error:
 660            //
 661            // failed to persist temporary file:
 662            // The system cannot move the file to a different disk drive. (os error 17)
 663            //
 664            // This is because `ReplaceFileW` does not support cross volume moves.
 665            // See the remark section: "The backup file, replaced file, and replacement file must all reside on the same volume."
 666            // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew#remarks
 667            //
 668            // So we use the directory of the destination as a temp dir to avoid it.
 669            // https://github.com/zed-industries/zed/issues/16571
 670            let temp_dir = TempDir::new_in(path.parent().unwrap_or(paths::temp_dir()))?;
 671            let temp_file = {
 672                let temp_file_path = temp_dir.path().join("temp_file");
 673                let mut file = std::fs::File::create_new(&temp_file_path)?;
 674                file.write_all(data.as_bytes())?;
 675                temp_file_path
 676            };
 677            atomic_replace(path.as_path(), temp_file.as_path())?;
 678            anyhow::Ok(())
 679        })
 680        .await?;
 681        Ok(())
 682    }
 683
 684    async fn save(
 685        &self,
 686        path: &Path,
 687        text: &Rope,
 688        line_ending: LineEnding,
 689        encoding: EncodingWrapper,
 690    ) -> Result<()> {
 691        let buffer_size = text.summary().len.min(10 * 1024);
 692        if let Some(path) = path.parent() {
 693            self.create_dir(path).await?;
 694        }
 695        let file = smol::fs::File::create(path).await?;
 696        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
 697        for chunk in chunks(text, line_ending) {
 698            writer
 699                .write_all(&from_utf8(chunk.to_string(), encoding.clone()).await?)
 700                .await?;
 701        }
 702        writer.flush().await?;
 703        Ok(())
 704    }
 705
 706    async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
 707        if let Some(path) = path.parent() {
 708            self.create_dir(path).await?;
 709        }
 710        let path = path.to_owned();
 711        let contents = content.to_owned();
 712        self.executor
 713            .spawn(async move {
 714                std::fs::write(path, contents)?;
 715                Ok(())
 716            })
 717            .await
 718    }
 719
 720    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
 721        let path = path.to_owned();
 722        self.executor
 723            .spawn(async move {
 724                std::fs::canonicalize(&path).with_context(|| format!("canonicalizing {path:?}"))
 725            })
 726            .await
 727    }
 728
 729    async fn is_file(&self, path: &Path) -> bool {
 730        let path = path.to_owned();
 731        self.executor
 732            .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) })
 733            .await
 734    }
 735
 736    async fn is_dir(&self, path: &Path) -> bool {
 737        let path = path.to_owned();
 738        self.executor
 739            .spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) })
 740            .await
 741    }
 742
 743    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
 744        let path_buf = path.to_owned();
 745        let symlink_metadata = match self
 746            .executor
 747            .spawn(async move { std::fs::symlink_metadata(&path_buf) })
 748            .await
 749        {
 750            Ok(metadata) => metadata,
 751            Err(err) => {
 752                return match (err.kind(), err.raw_os_error()) {
 753                    (io::ErrorKind::NotFound, _) => Ok(None),
 754                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
 755                    _ => Err(anyhow::Error::new(err)),
 756                };
 757            }
 758        };
 759
 760        let is_symlink = symlink_metadata.file_type().is_symlink();
 761        let metadata = if is_symlink {
 762            let path_buf = path.to_path_buf();
 763            let path_exists = self
 764                .executor
 765                .spawn(async move {
 766                    path_buf
 767                        .try_exists()
 768                        .with_context(|| format!("checking existence for path {path_buf:?}"))
 769                })
 770                .await?;
 771            if path_exists {
 772                let path_buf = path.to_path_buf();
 773                self.executor
 774                    .spawn(async move { std::fs::metadata(path_buf) })
 775                    .await
 776                    .with_context(|| "accessing symlink for path {path}")?
 777            } else {
 778                symlink_metadata
 779            }
 780        } else {
 781            symlink_metadata
 782        };
 783
 784        #[cfg(unix)]
 785        let inode = metadata.ino();
 786
 787        #[cfg(windows)]
 788        let inode = file_id(path).await?;
 789
 790        #[cfg(windows)]
 791        let is_fifo = false;
 792
 793        #[cfg(unix)]
 794        let is_fifo = metadata.file_type().is_fifo();
 795
 796        Ok(Some(Metadata {
 797            inode,
 798            mtime: MTime(metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH)),
 799            len: metadata.len(),
 800            is_symlink,
 801            is_dir: metadata.file_type().is_dir(),
 802            is_fifo,
 803        }))
 804    }
 805
 806    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
 807        let path = path.to_owned();
 808        let path = self
 809            .executor
 810            .spawn(async move { std::fs::read_link(&path) })
 811            .await?;
 812        Ok(path)
 813    }
 814
 815    async fn read_dir(
 816        &self,
 817        path: &Path,
 818    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
 819        let path = path.to_owned();
 820        let result = iter(
 821            self.executor
 822                .spawn(async move { std::fs::read_dir(path) })
 823                .await?,
 824        )
 825        .map(|entry| match entry {
 826            Ok(entry) => Ok(entry.path()),
 827            Err(error) => Err(anyhow!("failed to read dir entry {error:?}")),
 828        });
 829        Ok(Box::pin(result))
 830    }
 831
 832    #[cfg(target_os = "macos")]
 833    async fn watch(
 834        &self,
 835        path: &Path,
 836        latency: Duration,
 837    ) -> (
 838        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
 839        Arc<dyn Watcher>,
 840    ) {
 841        use fsevent::StreamFlags;
 842
 843        let (events_tx, events_rx) = smol::channel::unbounded();
 844        let handles = Arc::new(parking_lot::Mutex::new(collections::BTreeMap::default()));
 845        let watcher = Arc::new(mac_watcher::MacWatcher::new(
 846            events_tx,
 847            Arc::downgrade(&handles),
 848            latency,
 849        ));
 850        watcher.add(path).expect("handles can't be dropped");
 851
 852        (
 853            Box::pin(
 854                events_rx
 855                    .map(|events| {
 856                        events
 857                            .into_iter()
 858                            .map(|event| {
 859                                log::trace!("fs path event: {event:?}");
 860                                let kind = if event.flags.contains(StreamFlags::ITEM_REMOVED) {
 861                                    Some(PathEventKind::Removed)
 862                                } else if event.flags.contains(StreamFlags::ITEM_CREATED) {
 863                                    Some(PathEventKind::Created)
 864                                } else if event.flags.contains(StreamFlags::ITEM_MODIFIED)
 865                                    | event.flags.contains(StreamFlags::ITEM_RENAMED)
 866                                {
 867                                    Some(PathEventKind::Changed)
 868                                } else {
 869                                    None
 870                                };
 871                                PathEvent {
 872                                    path: event.path,
 873                                    kind,
 874                                }
 875                            })
 876                            .collect()
 877                    })
 878                    .chain(futures::stream::once(async move {
 879                        drop(handles);
 880                        vec![]
 881                    })),
 882            ),
 883            watcher,
 884        )
 885    }
 886
 887    #[cfg(not(target_os = "macos"))]
 888    async fn watch(
 889        &self,
 890        path: &Path,
 891        latency: Duration,
 892    ) -> (
 893        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
 894        Arc<dyn Watcher>,
 895    ) {
 896        use parking_lot::Mutex;
 897        use util::{ResultExt as _, paths::SanitizedPath};
 898
 899        let (tx, rx) = smol::channel::unbounded();
 900        let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
 901        let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
 902
 903        // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
 904        if let Err(e) = watcher.add(path)
 905            && let Some(parent) = path.parent()
 906            && let Err(parent_e) = watcher.add(parent)
 907        {
 908            log::warn!(
 909                "Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}",
 910                path.display(),
 911                parent.display()
 912            );
 913        }
 914
 915        // Check if path is a symlink and follow the target parent
 916        if let Some(mut target) = self.read_link(path).await.ok() {
 917            log::trace!("watch symlink {path:?} -> {target:?}");
 918            // Check if symlink target is relative path, if so make it absolute
 919            if target.is_relative()
 920                && let Some(parent) = path.parent()
 921            {
 922                target = parent.join(target);
 923                if let Ok(canonical) = self.canonicalize(&target).await {
 924                    target = SanitizedPath::new(&canonical).as_path().to_path_buf();
 925                }
 926            }
 927            watcher.add(&target).ok();
 928            if let Some(parent) = target.parent() {
 929                watcher.add(parent).log_err();
 930            }
 931        }
 932
 933        (
 934            Box::pin(rx.filter_map({
 935                let watcher = watcher.clone();
 936                move |_| {
 937                    let _ = watcher.clone();
 938                    let pending_paths = pending_paths.clone();
 939                    async move {
 940                        smol::Timer::after(latency).await;
 941                        let paths = std::mem::take(&mut *pending_paths.lock());
 942                        (!paths.is_empty()).then_some(paths)
 943                    }
 944                }
 945            })),
 946            watcher,
 947        )
 948    }
 949
 950    fn open_repo(
 951        &self,
 952        dotgit_path: &Path,
 953        system_git_binary_path: Option<&Path>,
 954    ) -> Option<Arc<dyn GitRepository>> {
 955        Some(Arc::new(RealGitRepository::new(
 956            dotgit_path,
 957            self.bundled_git_binary_path.clone(),
 958            system_git_binary_path.map(|path| path.to_path_buf()),
 959            self.executor.clone(),
 960        )?))
 961    }
 962
 963    async fn git_init(
 964        &self,
 965        abs_work_directory_path: &Path,
 966        fallback_branch_name: String,
 967    ) -> Result<()> {
 968        let config = new_smol_command("git")
 969            .current_dir(abs_work_directory_path)
 970            .args(&["config", "--global", "--get", "init.defaultBranch"])
 971            .output()
 972            .await?;
 973
 974        let branch_name;
 975
 976        if config.status.success() && !config.stdout.is_empty() {
 977            branch_name = String::from_utf8_lossy(&config.stdout);
 978        } else {
 979            branch_name = Cow::Borrowed(fallback_branch_name.as_str());
 980        }
 981
 982        new_smol_command("git")
 983            .current_dir(abs_work_directory_path)
 984            .args(&["init", "-b"])
 985            .arg(branch_name.trim())
 986            .output()
 987            .await?;
 988
 989        Ok(())
 990    }
 991
 992    async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> {
 993        let output = new_smol_command("git")
 994            .current_dir(abs_work_directory)
 995            .args(&["clone", repo_url])
 996            .output()
 997            .await?;
 998
 999        if !output.status.success() {
1000            anyhow::bail!(
1001                "git clone failed: {}",
1002                String::from_utf8_lossy(&output.stderr)
1003            );
1004        }
1005
1006        Ok(())
1007    }
1008
1009    fn is_fake(&self) -> bool {
1010        false
1011    }
1012
1013    /// Checks whether the file system is case sensitive by attempting to create two files
1014    /// that have the same name except for the casing.
1015    ///
1016    /// It creates both files in a temporary directory it removes at the end.
1017    async fn is_case_sensitive(&self) -> Result<bool> {
1018        let temp_dir = TempDir::new()?;
1019        let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
1020        let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
1021
1022        let create_opts = CreateOptions {
1023            overwrite: false,
1024            ignore_if_exists: false,
1025        };
1026
1027        // Create file1
1028        self.create_file(&test_file_1, create_opts).await?;
1029
1030        // Now check whether it's possible to create file2
1031        let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
1032            Ok(_) => Ok(true),
1033            Err(e) => {
1034                if let Some(io_error) = e.downcast_ref::<io::Error>() {
1035                    if io_error.kind() == io::ErrorKind::AlreadyExists {
1036                        Ok(false)
1037                    } else {
1038                        Err(e)
1039                    }
1040                } else {
1041                    Err(e)
1042                }
1043            }
1044        };
1045
1046        temp_dir.close()?;
1047        case_sensitive
1048    }
1049}
1050
1051#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
1052impl Watcher for RealWatcher {
1053    fn add(&self, _: &Path) -> Result<()> {
1054        Ok(())
1055    }
1056
1057    fn remove(&self, _: &Path) -> Result<()> {
1058        Ok(())
1059    }
1060}
1061
1062#[cfg(any(test, feature = "test-support"))]
1063pub struct FakeFs {
1064    this: std::sync::Weak<Self>,
1065    // Use an unfair lock to ensure tests are deterministic.
1066    state: Arc<Mutex<FakeFsState>>,
1067    executor: gpui::BackgroundExecutor,
1068}
1069
1070#[cfg(any(test, feature = "test-support"))]
1071struct FakeFsState {
1072    root: FakeFsEntry,
1073    next_inode: u64,
1074    next_mtime: SystemTime,
1075    git_event_tx: smol::channel::Sender<PathBuf>,
1076    event_txs: Vec<(PathBuf, smol::channel::Sender<Vec<PathEvent>>)>,
1077    events_paused: bool,
1078    buffered_events: Vec<PathEvent>,
1079    metadata_call_count: usize,
1080    read_dir_call_count: usize,
1081    path_write_counts: std::collections::HashMap<PathBuf, usize>,
1082    moves: std::collections::HashMap<u64, PathBuf>,
1083}
1084
1085#[cfg(any(test, feature = "test-support"))]
1086#[derive(Clone, Debug)]
1087enum FakeFsEntry {
1088    File {
1089        inode: u64,
1090        mtime: MTime,
1091        len: u64,
1092        content: Vec<u8>,
1093        // The path to the repository state directory, if this is a gitfile.
1094        git_dir_path: Option<PathBuf>,
1095    },
1096    Dir {
1097        inode: u64,
1098        mtime: MTime,
1099        len: u64,
1100        entries: BTreeMap<String, FakeFsEntry>,
1101        git_repo_state: Option<Arc<Mutex<FakeGitRepositoryState>>>,
1102    },
1103    Symlink {
1104        target: PathBuf,
1105    },
1106}
1107
1108#[cfg(any(test, feature = "test-support"))]
1109impl PartialEq for FakeFsEntry {
1110    fn eq(&self, other: &Self) -> bool {
1111        match (self, other) {
1112            (
1113                Self::File {
1114                    inode: l_inode,
1115                    mtime: l_mtime,
1116                    len: l_len,
1117                    content: l_content,
1118                    git_dir_path: l_git_dir_path,
1119                },
1120                Self::File {
1121                    inode: r_inode,
1122                    mtime: r_mtime,
1123                    len: r_len,
1124                    content: r_content,
1125                    git_dir_path: r_git_dir_path,
1126                },
1127            ) => {
1128                l_inode == r_inode
1129                    && l_mtime == r_mtime
1130                    && l_len == r_len
1131                    && l_content == r_content
1132                    && l_git_dir_path == r_git_dir_path
1133            }
1134            (
1135                Self::Dir {
1136                    inode: l_inode,
1137                    mtime: l_mtime,
1138                    len: l_len,
1139                    entries: l_entries,
1140                    git_repo_state: l_git_repo_state,
1141                },
1142                Self::Dir {
1143                    inode: r_inode,
1144                    mtime: r_mtime,
1145                    len: r_len,
1146                    entries: r_entries,
1147                    git_repo_state: r_git_repo_state,
1148                },
1149            ) => {
1150                let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) {
1151                    (Some(l), Some(r)) => Arc::ptr_eq(l, r),
1152                    (None, None) => true,
1153                    _ => false,
1154                };
1155                l_inode == r_inode
1156                    && l_mtime == r_mtime
1157                    && l_len == r_len
1158                    && l_entries == r_entries
1159                    && same_repo_state
1160            }
1161            (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => {
1162                l_target == r_target
1163            }
1164            _ => false,
1165        }
1166    }
1167}
1168
1169#[cfg(any(test, feature = "test-support"))]
1170impl FakeFsState {
1171    fn get_and_increment_mtime(&mut self) -> MTime {
1172        let mtime = self.next_mtime;
1173        self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL;
1174        MTime(mtime)
1175    }
1176
1177    fn get_and_increment_inode(&mut self) -> u64 {
1178        let inode = self.next_inode;
1179        self.next_inode += 1;
1180        inode
1181    }
1182
1183    fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option<PathBuf> {
1184        let mut canonical_path = PathBuf::new();
1185        let mut path = target.to_path_buf();
1186        let mut entry_stack = Vec::new();
1187        'outer: loop {
1188            let mut path_components = path.components().peekable();
1189            let mut prefix = None;
1190            while let Some(component) = path_components.next() {
1191                match component {
1192                    Component::Prefix(prefix_component) => prefix = Some(prefix_component),
1193                    Component::RootDir => {
1194                        entry_stack.clear();
1195                        entry_stack.push(&self.root);
1196                        canonical_path.clear();
1197                        match prefix {
1198                            Some(prefix_component) => {
1199                                canonical_path = PathBuf::from(prefix_component.as_os_str());
1200                                // Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
1201                                canonical_path.push(std::path::MAIN_SEPARATOR_STR);
1202                            }
1203                            None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
1204                        }
1205                    }
1206                    Component::CurDir => {}
1207                    Component::ParentDir => {
1208                        entry_stack.pop()?;
1209                        canonical_path.pop();
1210                    }
1211                    Component::Normal(name) => {
1212                        let current_entry = *entry_stack.last()?;
1213                        if let FakeFsEntry::Dir { entries, .. } = current_entry {
1214                            let entry = entries.get(name.to_str().unwrap())?;
1215                            if (path_components.peek().is_some() || follow_symlink)
1216                                && let FakeFsEntry::Symlink { target, .. } = entry
1217                            {
1218                                let mut target = target.clone();
1219                                target.extend(path_components);
1220                                path = target;
1221                                continue 'outer;
1222                            }
1223                            entry_stack.push(entry);
1224                            canonical_path = canonical_path.join(name);
1225                        } else {
1226                            return None;
1227                        }
1228                    }
1229                }
1230            }
1231            break;
1232        }
1233
1234        if entry_stack.is_empty() {
1235            None
1236        } else {
1237            Some(canonical_path)
1238        }
1239    }
1240
1241    fn try_entry(
1242        &mut self,
1243        target: &Path,
1244        follow_symlink: bool,
1245    ) -> Option<(&mut FakeFsEntry, PathBuf)> {
1246        let canonical_path = self.canonicalize(target, follow_symlink)?;
1247
1248        let mut components = canonical_path
1249            .components()
1250            .skip_while(|component| matches!(component, Component::Prefix(_)));
1251        let Some(Component::RootDir) = components.next() else {
1252            panic!(
1253                "the path {:?} was not canonicalized properly {:?}",
1254                target, canonical_path
1255            )
1256        };
1257
1258        let mut entry = &mut self.root;
1259        for component in components {
1260            match component {
1261                Component::Normal(name) => {
1262                    if let FakeFsEntry::Dir { entries, .. } = entry {
1263                        entry = entries.get_mut(name.to_str().unwrap())?;
1264                    } else {
1265                        return None;
1266                    }
1267                }
1268                _ => {
1269                    panic!(
1270                        "the path {:?} was not canonicalized properly {:?}",
1271                        target, canonical_path
1272                    )
1273                }
1274            }
1275        }
1276
1277        Some((entry, canonical_path))
1278    }
1279
1280    fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> {
1281        Ok(self
1282            .try_entry(target, true)
1283            .ok_or_else(|| {
1284                anyhow!(io::Error::new(
1285                    io::ErrorKind::NotFound,
1286                    format!("not found: {target:?}")
1287                ))
1288            })?
1289            .0)
1290    }
1291
1292    fn write_path<Fn, T>(&mut self, path: &Path, callback: Fn) -> Result<T>
1293    where
1294        Fn: FnOnce(btree_map::Entry<String, FakeFsEntry>) -> Result<T>,
1295    {
1296        let path = normalize_path(path);
1297        let filename = path.file_name().context("cannot overwrite the root")?;
1298        let parent_path = path.parent().unwrap();
1299
1300        let parent = self.entry(parent_path)?;
1301        let new_entry = parent
1302            .dir_entries(parent_path)?
1303            .entry(filename.to_str().unwrap().into());
1304        callback(new_entry)
1305    }
1306
1307    fn emit_event<I, T>(&mut self, paths: I)
1308    where
1309        I: IntoIterator<Item = (T, Option<PathEventKind>)>,
1310        T: Into<PathBuf>,
1311    {
1312        self.buffered_events
1313            .extend(paths.into_iter().map(|(path, kind)| PathEvent {
1314                path: path.into(),
1315                kind,
1316            }));
1317
1318        if !self.events_paused {
1319            self.flush_events(self.buffered_events.len());
1320        }
1321    }
1322
1323    fn flush_events(&mut self, mut count: usize) {
1324        count = count.min(self.buffered_events.len());
1325        let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
1326        self.event_txs.retain(|(_, tx)| {
1327            let _ = tx.try_send(events.clone());
1328            !tx.is_closed()
1329        });
1330    }
1331}
1332
1333#[cfg(any(test, feature = "test-support"))]
1334pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
1335    std::sync::LazyLock::new(|| OsStr::new(".git"));
1336
1337#[cfg(any(test, feature = "test-support"))]
1338impl FakeFs {
1339    /// We need to use something large enough for Windows and Unix to consider this a new file.
1340    /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
1341    const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100);
1342
1343    pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
1344        let (tx, rx) = smol::channel::bounded::<PathBuf>(10);
1345
1346        let this = Arc::new_cyclic(|this| Self {
1347            this: this.clone(),
1348            executor: executor.clone(),
1349            state: Arc::new(Mutex::new(FakeFsState {
1350                root: FakeFsEntry::Dir {
1351                    inode: 0,
1352                    mtime: MTime(UNIX_EPOCH),
1353                    len: 0,
1354                    entries: Default::default(),
1355                    git_repo_state: None,
1356                },
1357                git_event_tx: tx,
1358                next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
1359                next_inode: 1,
1360                event_txs: Default::default(),
1361                buffered_events: Vec::new(),
1362                events_paused: false,
1363                read_dir_call_count: 0,
1364                metadata_call_count: 0,
1365                path_write_counts: Default::default(),
1366                moves: Default::default(),
1367            })),
1368        });
1369
1370        executor.spawn({
1371            let this = this.clone();
1372            async move {
1373                while let Ok(git_event) = rx.recv().await {
1374                    if let Some(mut state) = this.state.try_lock() {
1375                        state.emit_event([(git_event, Some(PathEventKind::Changed))]);
1376                    } else {
1377                        panic!("Failed to lock file system state, this execution would have caused a test hang");
1378                    }
1379                }
1380            }
1381        }).detach();
1382
1383        this
1384    }
1385
1386    pub fn set_next_mtime(&self, next_mtime: SystemTime) {
1387        let mut state = self.state.lock();
1388        state.next_mtime = next_mtime;
1389    }
1390
1391    pub fn get_and_increment_mtime(&self) -> MTime {
1392        let mut state = self.state.lock();
1393        state.get_and_increment_mtime()
1394    }
1395
1396    pub async fn touch_path(&self, path: impl AsRef<Path>) {
1397        let mut state = self.state.lock();
1398        let path = path.as_ref();
1399        let new_mtime = state.get_and_increment_mtime();
1400        let new_inode = state.get_and_increment_inode();
1401        state
1402            .write_path(path, move |entry| {
1403                match entry {
1404                    btree_map::Entry::Vacant(e) => {
1405                        e.insert(FakeFsEntry::File {
1406                            inode: new_inode,
1407                            mtime: new_mtime,
1408                            content: Vec::new(),
1409                            len: 0,
1410                            git_dir_path: None,
1411                        });
1412                    }
1413                    btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() {
1414                        FakeFsEntry::File { mtime, .. } => *mtime = new_mtime,
1415                        FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime,
1416                        FakeFsEntry::Symlink { .. } => {}
1417                    },
1418                }
1419                Ok(())
1420            })
1421            .unwrap();
1422        state.emit_event([(path.to_path_buf(), Some(PathEventKind::Changed))]);
1423    }
1424
1425    pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
1426        self.write_file_internal(path, content, true).unwrap()
1427    }
1428
1429    pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
1430        let mut state = self.state.lock();
1431        let path = path.as_ref();
1432        let file = FakeFsEntry::Symlink { target };
1433        state
1434            .write_path(path.as_ref(), move |e| match e {
1435                btree_map::Entry::Vacant(e) => {
1436                    e.insert(file);
1437                    Ok(())
1438                }
1439                btree_map::Entry::Occupied(mut e) => {
1440                    *e.get_mut() = file;
1441                    Ok(())
1442                }
1443            })
1444            .unwrap();
1445        state.emit_event([(path, Some(PathEventKind::Created))]);
1446    }
1447
1448    fn write_file_internal(
1449        &self,
1450        path: impl AsRef<Path>,
1451        new_content: Vec<u8>,
1452        recreate_inode: bool,
1453    ) -> Result<()> {
1454        let mut state = self.state.lock();
1455        let path_buf = path.as_ref().to_path_buf();
1456        *state.path_write_counts.entry(path_buf).or_insert(0) += 1;
1457        let new_inode = state.get_and_increment_inode();
1458        let new_mtime = state.get_and_increment_mtime();
1459        let new_len = new_content.len() as u64;
1460        let mut kind = None;
1461        state.write_path(path.as_ref(), |entry| {
1462            match entry {
1463                btree_map::Entry::Vacant(e) => {
1464                    kind = Some(PathEventKind::Created);
1465                    e.insert(FakeFsEntry::File {
1466                        inode: new_inode,
1467                        mtime: new_mtime,
1468                        len: new_len,
1469                        content: new_content,
1470                        git_dir_path: None,
1471                    });
1472                }
1473                btree_map::Entry::Occupied(mut e) => {
1474                    kind = Some(PathEventKind::Changed);
1475                    if let FakeFsEntry::File {
1476                        inode,
1477                        mtime,
1478                        len,
1479                        content,
1480                        ..
1481                    } = e.get_mut()
1482                    {
1483                        *mtime = new_mtime;
1484                        *content = new_content;
1485                        *len = new_len;
1486                        if recreate_inode {
1487                            *inode = new_inode;
1488                        }
1489                    } else {
1490                        anyhow::bail!("not a file")
1491                    }
1492                }
1493            }
1494            Ok(())
1495        })?;
1496        state.emit_event([(path.as_ref(), kind)]);
1497        Ok(())
1498    }
1499
1500    pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1501        let path = path.as_ref();
1502        let path = normalize_path(path);
1503        let mut state = self.state.lock();
1504        let entry = state.entry(&path)?;
1505        entry.file_content(&path).cloned()
1506    }
1507
1508    async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
1509        let path = path.as_ref();
1510        let path = normalize_path(path);
1511        self.simulate_random_delay().await;
1512        let mut state = self.state.lock();
1513        let entry = state.entry(&path)?;
1514        entry.file_content(&path).cloned()
1515    }
1516
1517    pub fn pause_events(&self) {
1518        self.state.lock().events_paused = true;
1519    }
1520
1521    pub fn unpause_events_and_flush(&self) {
1522        self.state.lock().events_paused = false;
1523        self.flush_events(usize::MAX);
1524    }
1525
1526    pub fn buffered_event_count(&self) -> usize {
1527        self.state.lock().buffered_events.len()
1528    }
1529
1530    pub fn flush_events(&self, count: usize) {
1531        self.state.lock().flush_events(count);
1532    }
1533
1534    pub(crate) fn entry(&self, target: &Path) -> Result<FakeFsEntry> {
1535        self.state.lock().entry(target).cloned()
1536    }
1537
1538    pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> {
1539        let mut state = self.state.lock();
1540        state.write_path(target, |entry| {
1541            match entry {
1542                btree_map::Entry::Vacant(vacant_entry) => {
1543                    vacant_entry.insert(new_entry);
1544                }
1545                btree_map::Entry::Occupied(mut occupied_entry) => {
1546                    occupied_entry.insert(new_entry);
1547                }
1548            }
1549            Ok(())
1550        })
1551    }
1552
1553    #[must_use]
1554    pub fn insert_tree<'a>(
1555        &'a self,
1556        path: impl 'a + AsRef<Path> + Send,
1557        tree: serde_json::Value,
1558    ) -> futures::future::BoxFuture<'a, ()> {
1559        use futures::FutureExt as _;
1560        use serde_json::Value::*;
1561
1562        async move {
1563            let path = path.as_ref();
1564
1565            match tree {
1566                Object(map) => {
1567                    self.create_dir(path).await.unwrap();
1568                    for (name, contents) in map {
1569                        let mut path = PathBuf::from(path);
1570                        path.push(name);
1571                        self.insert_tree(&path, contents).await;
1572                    }
1573                }
1574                Null => {
1575                    self.create_dir(path).await.unwrap();
1576                }
1577                String(contents) => {
1578                    self.insert_file(&path, contents.into_bytes()).await;
1579                }
1580                _ => {
1581                    panic!("JSON object must contain only objects, strings, or null");
1582                }
1583            }
1584        }
1585        .boxed()
1586    }
1587
1588    pub fn insert_tree_from_real_fs<'a>(
1589        &'a self,
1590        path: impl 'a + AsRef<Path> + Send,
1591        src_path: impl 'a + AsRef<Path> + Send,
1592    ) -> futures::future::BoxFuture<'a, ()> {
1593        use futures::FutureExt as _;
1594
1595        async move {
1596            let path = path.as_ref();
1597            if std::fs::metadata(&src_path).unwrap().is_file() {
1598                let contents = std::fs::read(src_path).unwrap();
1599                self.insert_file(path, contents).await;
1600            } else {
1601                self.create_dir(path).await.unwrap();
1602                for entry in std::fs::read_dir(&src_path).unwrap() {
1603                    let entry = entry.unwrap();
1604                    self.insert_tree_from_real_fs(path.join(entry.file_name()), entry.path())
1605                        .await;
1606                }
1607            }
1608        }
1609        .boxed()
1610    }
1611
1612    pub fn with_git_state_and_paths<T, F>(
1613        &self,
1614        dot_git: &Path,
1615        emit_git_event: bool,
1616        f: F,
1617    ) -> Result<T>
1618    where
1619        F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T,
1620    {
1621        let mut state = self.state.lock();
1622        let git_event_tx = state.git_event_tx.clone();
1623        let entry = state.entry(dot_git).context("open .git")?;
1624
1625        if let FakeFsEntry::Dir { git_repo_state, .. } = entry {
1626            let repo_state = git_repo_state.get_or_insert_with(|| {
1627                log::debug!("insert git state for {dot_git:?}");
1628                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
1629            });
1630            let mut repo_state = repo_state.lock();
1631
1632            let result = f(&mut repo_state, dot_git, dot_git);
1633
1634            drop(repo_state);
1635            if emit_git_event {
1636                state.emit_event([(dot_git, Some(PathEventKind::Changed))]);
1637            }
1638
1639            Ok(result)
1640        } else if let FakeFsEntry::File {
1641            content,
1642            git_dir_path,
1643            ..
1644        } = &mut *entry
1645        {
1646            let path = match git_dir_path {
1647                Some(path) => path,
1648                None => {
1649                    let path = std::str::from_utf8(content)
1650                        .ok()
1651                        .and_then(|content| content.strip_prefix("gitdir:"))
1652                        .context("not a valid gitfile")?
1653                        .trim();
1654                    git_dir_path.insert(normalize_path(&dot_git.parent().unwrap().join(path)))
1655                }
1656            }
1657            .clone();
1658            let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else {
1659                anyhow::bail!("pointed-to git dir {path:?} not found")
1660            };
1661            let FakeFsEntry::Dir {
1662                git_repo_state,
1663                entries,
1664                ..
1665            } = git_dir_entry
1666            else {
1667                anyhow::bail!("gitfile points to a non-directory")
1668            };
1669            let common_dir = if let Some(child) = entries.get("commondir") {
1670                Path::new(
1671                    std::str::from_utf8(child.file_content("commondir".as_ref())?)
1672                        .context("commondir content")?,
1673                )
1674                .to_owned()
1675            } else {
1676                canonical_path.clone()
1677            };
1678            let repo_state = git_repo_state.get_or_insert_with(|| {
1679                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
1680            });
1681            let mut repo_state = repo_state.lock();
1682
1683            let result = f(&mut repo_state, &canonical_path, &common_dir);
1684
1685            if emit_git_event {
1686                drop(repo_state);
1687                state.emit_event([(canonical_path, Some(PathEventKind::Changed))]);
1688            }
1689
1690            Ok(result)
1691        } else {
1692            anyhow::bail!("not a valid git repository");
1693        }
1694    }
1695
1696    pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
1697    where
1698        F: FnOnce(&mut FakeGitRepositoryState) -> T,
1699    {
1700        self.with_git_state_and_paths(dot_git, emit_git_event, |state, _, _| f(state))
1701    }
1702
1703    pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
1704        self.with_git_state(dot_git, true, |state| {
1705            let branch = branch.map(Into::into);
1706            state.branches.extend(branch.clone());
1707            state.current_branch_name = branch
1708        })
1709        .unwrap();
1710    }
1711
1712    pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
1713        self.with_git_state(dot_git, true, |state| {
1714            if let Some(first) = branches.first()
1715                && state.current_branch_name.is_none()
1716            {
1717                state.current_branch_name = Some(first.to_string())
1718            }
1719            state
1720                .branches
1721                .extend(branches.iter().map(ToString::to_string));
1722        })
1723        .unwrap();
1724    }
1725
1726    pub fn set_unmerged_paths_for_repo(
1727        &self,
1728        dot_git: &Path,
1729        unmerged_state: &[(RepoPath, UnmergedStatus)],
1730    ) {
1731        self.with_git_state(dot_git, true, |state| {
1732            state.unmerged_paths.clear();
1733            state.unmerged_paths.extend(
1734                unmerged_state
1735                    .iter()
1736                    .map(|(path, content)| (path.clone(), *content)),
1737            );
1738        })
1739        .unwrap();
1740    }
1741
1742    pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(&str, String)]) {
1743        self.with_git_state(dot_git, true, |state| {
1744            state.index_contents.clear();
1745            state.index_contents.extend(
1746                index_state
1747                    .iter()
1748                    .map(|(path, content)| (repo_path(path), content.clone())),
1749            );
1750        })
1751        .unwrap();
1752    }
1753
1754    pub fn set_head_for_repo(
1755        &self,
1756        dot_git: &Path,
1757        head_state: &[(&str, String)],
1758        sha: impl Into<String>,
1759    ) {
1760        self.with_git_state(dot_git, true, |state| {
1761            state.head_contents.clear();
1762            state.head_contents.extend(
1763                head_state
1764                    .iter()
1765                    .map(|(path, content)| (repo_path(path), content.clone())),
1766            );
1767            state.refs.insert("HEAD".into(), sha.into());
1768        })
1769        .unwrap();
1770    }
1771
1772    pub fn set_head_and_index_for_repo(&self, dot_git: &Path, contents_by_path: &[(&str, String)]) {
1773        self.with_git_state(dot_git, true, |state| {
1774            state.head_contents.clear();
1775            state.head_contents.extend(
1776                contents_by_path
1777                    .iter()
1778                    .map(|(path, contents)| (repo_path(path), contents.clone())),
1779            );
1780            state.index_contents = state.head_contents.clone();
1781        })
1782        .unwrap();
1783    }
1784
1785    pub fn set_merge_base_content_for_repo(
1786        &self,
1787        dot_git: &Path,
1788        contents_by_path: &[(&str, String)],
1789    ) {
1790        self.with_git_state(dot_git, true, |state| {
1791            use git::Oid;
1792
1793            state.merge_base_contents.clear();
1794            let oids = (1..)
1795                .map(|n| n.to_string())
1796                .map(|n| Oid::from_bytes(n.repeat(20).as_bytes()).unwrap());
1797            for ((path, content), oid) in contents_by_path.iter().zip(oids) {
1798                state.merge_base_contents.insert(repo_path(path), oid);
1799                state.oids.insert(oid, content.clone());
1800            }
1801        })
1802        .unwrap();
1803    }
1804
1805    pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
1806        self.with_git_state(dot_git, true, |state| {
1807            state.blames.clear();
1808            state.blames.extend(blames);
1809        })
1810        .unwrap();
1811    }
1812
1813    /// Put the given git repository into a state with the given status,
1814    /// by mutating the head, index, and unmerged state.
1815    pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&str, FileStatus)]) {
1816        let workdir_path = dot_git.parent().unwrap();
1817        let workdir_contents = self.files_with_contents(workdir_path);
1818        self.with_git_state(dot_git, true, |state| {
1819            state.index_contents.clear();
1820            state.head_contents.clear();
1821            state.unmerged_paths.clear();
1822            for (path, content) in workdir_contents {
1823                use util::{paths::PathStyle, rel_path::RelPath};
1824
1825                let repo_path: RepoPath = RelPath::new(path.strip_prefix(&workdir_path).unwrap(), PathStyle::local()).unwrap().into();
1826                let status = statuses
1827                    .iter()
1828                    .find_map(|(p, status)| (*p == repo_path.as_unix_str()).then_some(status));
1829                let mut content = String::from_utf8_lossy(&content).to_string();
1830
1831                let mut index_content = None;
1832                let mut head_content = None;
1833                match status {
1834                    None => {
1835                        index_content = Some(content.clone());
1836                        head_content = Some(content);
1837                    }
1838                    Some(FileStatus::Untracked | FileStatus::Ignored) => {}
1839                    Some(FileStatus::Unmerged(unmerged_status)) => {
1840                        state
1841                            .unmerged_paths
1842                            .insert(repo_path.clone(), *unmerged_status);
1843                        content.push_str(" (unmerged)");
1844                        index_content = Some(content.clone());
1845                        head_content = Some(content);
1846                    }
1847                    Some(FileStatus::Tracked(TrackedStatus {
1848                        index_status,
1849                        worktree_status,
1850                    })) => {
1851                        match worktree_status {
1852                            StatusCode::Modified => {
1853                                let mut content = content.clone();
1854                                content.push_str(" (modified in working copy)");
1855                                index_content = Some(content);
1856                            }
1857                            StatusCode::TypeChanged | StatusCode::Unmodified => {
1858                                index_content = Some(content.clone());
1859                            }
1860                            StatusCode::Added => {}
1861                            StatusCode::Deleted | StatusCode::Renamed | StatusCode::Copied => {
1862                                panic!("cannot create these statuses for an existing file");
1863                            }
1864                        };
1865                        match index_status {
1866                            StatusCode::Modified => {
1867                                let mut content = index_content.clone().expect(
1868                                    "file cannot be both modified in index and created in working copy",
1869                                );
1870                                content.push_str(" (modified in index)");
1871                                head_content = Some(content);
1872                            }
1873                            StatusCode::TypeChanged | StatusCode::Unmodified => {
1874                                head_content = Some(index_content.clone().expect("file cannot be both unmodified in index and created in working copy"));
1875                            }
1876                            StatusCode::Added => {}
1877                            StatusCode::Deleted  => {
1878                                head_content = Some("".into());
1879                            }
1880                            StatusCode::Renamed | StatusCode::Copied => {
1881                                panic!("cannot create these statuses for an existing file");
1882                            }
1883                        };
1884                    }
1885                };
1886
1887                if let Some(content) = index_content {
1888                    state.index_contents.insert(repo_path.clone(), content);
1889                }
1890                if let Some(content) = head_content {
1891                    state.head_contents.insert(repo_path.clone(), content);
1892                }
1893            }
1894        }).unwrap();
1895    }
1896
1897    pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
1898        self.with_git_state(dot_git, true, |state| {
1899            state.simulated_index_write_error_message = message;
1900        })
1901        .unwrap();
1902    }
1903
1904    pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
1905        let mut result = Vec::new();
1906        let mut queue = collections::VecDeque::new();
1907        let state = &*self.state.lock();
1908        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1909        while let Some((path, entry)) = queue.pop_front() {
1910            if let FakeFsEntry::Dir { entries, .. } = entry {
1911                for (name, entry) in entries {
1912                    queue.push_back((path.join(name), entry));
1913                }
1914            }
1915            if include_dot_git
1916                || !path
1917                    .components()
1918                    .any(|component| component.as_os_str() == *FS_DOT_GIT)
1919            {
1920                result.push(path);
1921            }
1922        }
1923        result
1924    }
1925
1926    pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
1927        let mut result = Vec::new();
1928        let mut queue = collections::VecDeque::new();
1929        let state = &*self.state.lock();
1930        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1931        while let Some((path, entry)) = queue.pop_front() {
1932            if let FakeFsEntry::Dir { entries, .. } = entry {
1933                for (name, entry) in entries {
1934                    queue.push_back((path.join(name), entry));
1935                }
1936                if include_dot_git
1937                    || !path
1938                        .components()
1939                        .any(|component| component.as_os_str() == *FS_DOT_GIT)
1940                {
1941                    result.push(path);
1942                }
1943            }
1944        }
1945        result
1946    }
1947
1948    pub fn files(&self) -> Vec<PathBuf> {
1949        let mut result = Vec::new();
1950        let mut queue = collections::VecDeque::new();
1951        let state = &*self.state.lock();
1952        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1953        while let Some((path, entry)) = queue.pop_front() {
1954            match entry {
1955                FakeFsEntry::File { .. } => result.push(path),
1956                FakeFsEntry::Dir { entries, .. } => {
1957                    for (name, entry) in entries {
1958                        queue.push_back((path.join(name), entry));
1959                    }
1960                }
1961                FakeFsEntry::Symlink { .. } => {}
1962            }
1963        }
1964        result
1965    }
1966
1967    pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec<u8>)> {
1968        let mut result = Vec::new();
1969        let mut queue = collections::VecDeque::new();
1970        let state = &*self.state.lock();
1971        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
1972        while let Some((path, entry)) = queue.pop_front() {
1973            match entry {
1974                FakeFsEntry::File { content, .. } => {
1975                    if path.starts_with(prefix) {
1976                        result.push((path, content.clone()));
1977                    }
1978                }
1979                FakeFsEntry::Dir { entries, .. } => {
1980                    for (name, entry) in entries {
1981                        queue.push_back((path.join(name), entry));
1982                    }
1983                }
1984                FakeFsEntry::Symlink { .. } => {}
1985            }
1986        }
1987        result
1988    }
1989
1990    /// How many `read_dir` calls have been issued.
1991    pub fn read_dir_call_count(&self) -> usize {
1992        self.state.lock().read_dir_call_count
1993    }
1994
1995    pub fn watched_paths(&self) -> Vec<PathBuf> {
1996        let state = self.state.lock();
1997        state
1998            .event_txs
1999            .iter()
2000            .filter_map(|(path, tx)| Some(path.clone()).filter(|_| !tx.is_closed()))
2001            .collect()
2002    }
2003
2004    /// How many `metadata` calls have been issued.
2005    pub fn metadata_call_count(&self) -> usize {
2006        self.state.lock().metadata_call_count
2007    }
2008
2009    /// How many write operations have been issued for a specific path.
2010    pub fn write_count_for_path(&self, path: impl AsRef<Path>) -> usize {
2011        let path = path.as_ref().to_path_buf();
2012        self.state
2013            .lock()
2014            .path_write_counts
2015            .get(&path)
2016            .copied()
2017            .unwrap_or(0)
2018    }
2019
2020    pub fn emit_fs_event(&self, path: impl Into<PathBuf>, event: Option<PathEventKind>) {
2021        self.state.lock().emit_event(std::iter::once((path, event)));
2022    }
2023
2024    fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
2025        self.executor.simulate_random_delay()
2026    }
2027}
2028
2029#[cfg(any(test, feature = "test-support"))]
2030impl FakeFsEntry {
2031    fn is_file(&self) -> bool {
2032        matches!(self, Self::File { .. })
2033    }
2034
2035    fn is_symlink(&self) -> bool {
2036        matches!(self, Self::Symlink { .. })
2037    }
2038
2039    fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
2040        if let Self::File { content, .. } = self {
2041            Ok(content)
2042        } else {
2043            anyhow::bail!("not a file: {path:?}");
2044        }
2045    }
2046
2047    fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap<String, FakeFsEntry>> {
2048        if let Self::Dir { entries, .. } = self {
2049            Ok(entries)
2050        } else {
2051            anyhow::bail!("not a directory: {path:?}");
2052        }
2053    }
2054}
2055
2056#[cfg(any(test, feature = "test-support"))]
2057struct FakeWatcher {
2058    tx: smol::channel::Sender<Vec<PathEvent>>,
2059    original_path: PathBuf,
2060    fs_state: Arc<Mutex<FakeFsState>>,
2061    prefixes: Mutex<Vec<PathBuf>>,
2062}
2063
2064#[cfg(any(test, feature = "test-support"))]
2065impl Watcher for FakeWatcher {
2066    fn add(&self, path: &Path) -> Result<()> {
2067        if path.starts_with(&self.original_path) {
2068            return Ok(());
2069        }
2070        self.fs_state
2071            .try_lock()
2072            .unwrap()
2073            .event_txs
2074            .push((path.to_owned(), self.tx.clone()));
2075        self.prefixes.lock().push(path.to_owned());
2076        Ok(())
2077    }
2078
2079    fn remove(&self, _: &Path) -> Result<()> {
2080        Ok(())
2081    }
2082}
2083
2084#[cfg(any(test, feature = "test-support"))]
2085#[derive(Debug)]
2086struct FakeHandle {
2087    inode: u64,
2088}
2089
2090#[cfg(any(test, feature = "test-support"))]
2091impl FileHandle for FakeHandle {
2092    fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
2093        let fs = fs.as_fake();
2094        let mut state = fs.state.lock();
2095        let Some(target) = state.moves.get(&self.inode).cloned() else {
2096            anyhow::bail!("fake fd not moved")
2097        };
2098
2099        if state.try_entry(&target, false).is_some() {
2100            return Ok(target);
2101        }
2102        anyhow::bail!("fake fd target not found")
2103    }
2104}
2105
2106#[cfg(any(test, feature = "test-support"))]
2107#[async_trait::async_trait]
2108impl Fs for FakeFs {
2109    async fn create_dir(&self, path: &Path) -> Result<()> {
2110        self.simulate_random_delay().await;
2111
2112        let mut created_dirs = Vec::new();
2113        let mut cur_path = PathBuf::new();
2114        for component in path.components() {
2115            let should_skip = matches!(component, Component::Prefix(..) | Component::RootDir);
2116            cur_path.push(component);
2117            if should_skip {
2118                continue;
2119            }
2120            let mut state = self.state.lock();
2121
2122            let inode = state.get_and_increment_inode();
2123            let mtime = state.get_and_increment_mtime();
2124            state.write_path(&cur_path, |entry| {
2125                entry.or_insert_with(|| {
2126                    created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
2127                    FakeFsEntry::Dir {
2128                        inode,
2129                        mtime,
2130                        len: 0,
2131                        entries: Default::default(),
2132                        git_repo_state: None,
2133                    }
2134                });
2135                Ok(())
2136            })?
2137        }
2138
2139        self.state.lock().emit_event(created_dirs);
2140        Ok(())
2141    }
2142
2143    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
2144        self.simulate_random_delay().await;
2145        let mut state = self.state.lock();
2146        let inode = state.get_and_increment_inode();
2147        let mtime = state.get_and_increment_mtime();
2148        let file = FakeFsEntry::File {
2149            inode,
2150            mtime,
2151            len: 0,
2152            content: Vec::new(),
2153            git_dir_path: None,
2154        };
2155        let mut kind = Some(PathEventKind::Created);
2156        state.write_path(path, |entry| {
2157            match entry {
2158                btree_map::Entry::Occupied(mut e) => {
2159                    if options.overwrite {
2160                        kind = Some(PathEventKind::Changed);
2161                        *e.get_mut() = file;
2162                    } else if !options.ignore_if_exists {
2163                        anyhow::bail!("path already exists: {path:?}");
2164                    }
2165                }
2166                btree_map::Entry::Vacant(e) => {
2167                    e.insert(file);
2168                }
2169            }
2170            Ok(())
2171        })?;
2172        state.emit_event([(path, kind)]);
2173        Ok(())
2174    }
2175
2176    async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
2177        let mut state = self.state.lock();
2178        let file = FakeFsEntry::Symlink { target };
2179        state
2180            .write_path(path.as_ref(), move |e| match e {
2181                btree_map::Entry::Vacant(e) => {
2182                    e.insert(file);
2183                    Ok(())
2184                }
2185                btree_map::Entry::Occupied(mut e) => {
2186                    *e.get_mut() = file;
2187                    Ok(())
2188                }
2189            })
2190            .unwrap();
2191        state.emit_event([(path, Some(PathEventKind::Created))]);
2192
2193        Ok(())
2194    }
2195
2196    async fn create_file_with(
2197        &self,
2198        path: &Path,
2199        mut content: Pin<&mut (dyn AsyncRead + Send)>,
2200    ) -> Result<()> {
2201        let mut bytes = Vec::new();
2202        content.read_to_end(&mut bytes).await?;
2203        self.write_file_internal(path, bytes, true)?;
2204        Ok(())
2205    }
2206
2207    async fn extract_tar_file(
2208        &self,
2209        path: &Path,
2210        content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
2211    ) -> Result<()> {
2212        let mut entries = content.entries()?;
2213        while let Some(entry) = entries.next().await {
2214            let mut entry = entry?;
2215            if entry.header().entry_type().is_file() {
2216                let path = path.join(entry.path()?.as_ref());
2217                let mut bytes = Vec::new();
2218                entry.read_to_end(&mut bytes).await?;
2219                self.create_dir(path.parent().unwrap()).await?;
2220                self.write_file_internal(&path, bytes, true)?;
2221            }
2222        }
2223        Ok(())
2224    }
2225
2226    async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
2227        self.simulate_random_delay().await;
2228
2229        let old_path = normalize_path(old_path);
2230        let new_path = normalize_path(new_path);
2231
2232        let mut state = self.state.lock();
2233        let moved_entry = state.write_path(&old_path, |e| {
2234            if let btree_map::Entry::Occupied(e) = e {
2235                Ok(e.get().clone())
2236            } else {
2237                anyhow::bail!("path does not exist: {old_path:?}")
2238            }
2239        })?;
2240
2241        let inode = match moved_entry {
2242            FakeFsEntry::File { inode, .. } => inode,
2243            FakeFsEntry::Dir { inode, .. } => inode,
2244            _ => 0,
2245        };
2246
2247        state.moves.insert(inode, new_path.clone());
2248
2249        state.write_path(&new_path, |e| {
2250            match e {
2251                btree_map::Entry::Occupied(mut e) => {
2252                    if options.overwrite {
2253                        *e.get_mut() = moved_entry;
2254                    } else if !options.ignore_if_exists {
2255                        anyhow::bail!("path already exists: {new_path:?}");
2256                    }
2257                }
2258                btree_map::Entry::Vacant(e) => {
2259                    e.insert(moved_entry);
2260                }
2261            }
2262            Ok(())
2263        })?;
2264
2265        state
2266            .write_path(&old_path, |e| {
2267                if let btree_map::Entry::Occupied(e) = e {
2268                    Ok(e.remove())
2269                } else {
2270                    unreachable!()
2271                }
2272            })
2273            .unwrap();
2274
2275        state.emit_event([
2276            (old_path, Some(PathEventKind::Removed)),
2277            (new_path, Some(PathEventKind::Created)),
2278        ]);
2279        Ok(())
2280    }
2281
2282    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
2283        self.simulate_random_delay().await;
2284
2285        let source = normalize_path(source);
2286        let target = normalize_path(target);
2287        let mut state = self.state.lock();
2288        let mtime = state.get_and_increment_mtime();
2289        let inode = state.get_and_increment_inode();
2290        let source_entry = state.entry(&source)?;
2291        let content = source_entry.file_content(&source)?.clone();
2292        let mut kind = Some(PathEventKind::Created);
2293        state.write_path(&target, |e| match e {
2294            btree_map::Entry::Occupied(e) => {
2295                if options.overwrite {
2296                    kind = Some(PathEventKind::Changed);
2297                    Ok(Some(e.get().clone()))
2298                } else if !options.ignore_if_exists {
2299                    anyhow::bail!("{target:?} already exists");
2300                } else {
2301                    Ok(None)
2302                }
2303            }
2304            btree_map::Entry::Vacant(e) => Ok(Some(
2305                e.insert(FakeFsEntry::File {
2306                    inode,
2307                    mtime,
2308                    len: content.len() as u64,
2309                    content,
2310                    git_dir_path: None,
2311                })
2312                .clone(),
2313            )),
2314        })?;
2315        state.emit_event([(target, kind)]);
2316        Ok(())
2317    }
2318
2319    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
2320        self.simulate_random_delay().await;
2321
2322        let path = normalize_path(path);
2323        let parent_path = path.parent().context("cannot remove the root")?;
2324        let base_name = path.file_name().context("cannot remove the root")?;
2325
2326        let mut state = self.state.lock();
2327        let parent_entry = state.entry(parent_path)?;
2328        let entry = parent_entry
2329            .dir_entries(parent_path)?
2330            .entry(base_name.to_str().unwrap().into());
2331
2332        match entry {
2333            btree_map::Entry::Vacant(_) => {
2334                if !options.ignore_if_not_exists {
2335                    anyhow::bail!("{path:?} does not exist");
2336                }
2337            }
2338            btree_map::Entry::Occupied(mut entry) => {
2339                {
2340                    let children = entry.get_mut().dir_entries(&path)?;
2341                    if !options.recursive && !children.is_empty() {
2342                        anyhow::bail!("{path:?} is not empty");
2343                    }
2344                }
2345                entry.remove();
2346            }
2347        }
2348        state.emit_event([(path, Some(PathEventKind::Removed))]);
2349        Ok(())
2350    }
2351
2352    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
2353        self.simulate_random_delay().await;
2354
2355        let path = normalize_path(path);
2356        let parent_path = path.parent().context("cannot remove the root")?;
2357        let base_name = path.file_name().unwrap();
2358        let mut state = self.state.lock();
2359        let parent_entry = state.entry(parent_path)?;
2360        let entry = parent_entry
2361            .dir_entries(parent_path)?
2362            .entry(base_name.to_str().unwrap().into());
2363        match entry {
2364            btree_map::Entry::Vacant(_) => {
2365                if !options.ignore_if_not_exists {
2366                    anyhow::bail!("{path:?} does not exist");
2367                }
2368            }
2369            btree_map::Entry::Occupied(mut entry) => {
2370                entry.get_mut().file_content(&path)?;
2371                entry.remove();
2372            }
2373        }
2374        state.emit_event([(path, Some(PathEventKind::Removed))]);
2375        Ok(())
2376    }
2377
2378    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read + Send + Sync>> {
2379        let bytes = self.load_internal(path).await?;
2380        Ok(Box::new(io::Cursor::new(bytes)))
2381    }
2382
2383    async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
2384        self.simulate_random_delay().await;
2385        let mut state = self.state.lock();
2386        let inode = match state.entry(path)? {
2387            FakeFsEntry::File { inode, .. } => *inode,
2388            FakeFsEntry::Dir { inode, .. } => *inode,
2389            _ => unreachable!(),
2390        };
2391        Ok(Arc::new(FakeHandle { inode }))
2392    }
2393
2394    async fn load(&self, path: &Path) -> Result<String> {
2395        let content = self.load_internal(path).await?;
2396        Ok(String::from_utf8(content)?)
2397    }
2398
2399    async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
2400        self.load_internal(path).await
2401    }
2402
2403    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
2404        self.simulate_random_delay().await;
2405        let path = normalize_path(path.as_path());
2406        if let Some(path) = path.parent() {
2407            self.create_dir(path).await?;
2408        }
2409        self.write_file_internal(path, data.into_bytes(), true)?;
2410        Ok(())
2411    }
2412
2413    async fn save(
2414        &self,
2415        path: &Path,
2416        text: &Rope,
2417        line_ending: LineEnding,
2418        encoding: EncodingWrapper,
2419    ) -> Result<()> {
2420        use crate::encodings::from_utf8;
2421
2422        self.simulate_random_delay().await;
2423        let path = normalize_path(path);
2424        let content = chunks(text, line_ending).collect::<String>();
2425        if let Some(path) = path.parent() {
2426            self.create_dir(path).await?;
2427        }
2428        self.write_file_internal(path, from_utf8(content, encoding).await?, false)?;
2429        Ok(())
2430    }
2431
2432    async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
2433        self.simulate_random_delay().await;
2434        let path = normalize_path(path);
2435        if let Some(path) = path.parent() {
2436            self.create_dir(path).await?;
2437        }
2438        self.write_file_internal(path, content.to_vec(), false)?;
2439        Ok(())
2440    }
2441
2442    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
2443        let path = normalize_path(path);
2444        self.simulate_random_delay().await;
2445        let state = self.state.lock();
2446        let canonical_path = state
2447            .canonicalize(&path, true)
2448            .with_context(|| format!("path does not exist: {path:?}"))?;
2449        Ok(canonical_path)
2450    }
2451
2452    async fn is_file(&self, path: &Path) -> bool {
2453        let path = normalize_path(path);
2454        self.simulate_random_delay().await;
2455        let mut state = self.state.lock();
2456        if let Some((entry, _)) = state.try_entry(&path, true) {
2457            entry.is_file()
2458        } else {
2459            false
2460        }
2461    }
2462
2463    async fn is_dir(&self, path: &Path) -> bool {
2464        self.metadata(path)
2465            .await
2466            .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
2467    }
2468
2469    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
2470        self.simulate_random_delay().await;
2471        let path = normalize_path(path);
2472        let mut state = self.state.lock();
2473        state.metadata_call_count += 1;
2474        if let Some((mut entry, _)) = state.try_entry(&path, false) {
2475            let is_symlink = entry.is_symlink();
2476            if is_symlink {
2477                if let Some(e) = state.try_entry(&path, true).map(|e| e.0) {
2478                    entry = e;
2479                } else {
2480                    return Ok(None);
2481                }
2482            }
2483
2484            Ok(Some(match &*entry {
2485                FakeFsEntry::File {
2486                    inode, mtime, len, ..
2487                } => Metadata {
2488                    inode: *inode,
2489                    mtime: *mtime,
2490                    len: *len,
2491                    is_dir: false,
2492                    is_symlink,
2493                    is_fifo: false,
2494                },
2495                FakeFsEntry::Dir {
2496                    inode, mtime, len, ..
2497                } => Metadata {
2498                    inode: *inode,
2499                    mtime: *mtime,
2500                    len: *len,
2501                    is_dir: true,
2502                    is_symlink,
2503                    is_fifo: false,
2504                },
2505                FakeFsEntry::Symlink { .. } => unreachable!(),
2506            }))
2507        } else {
2508            Ok(None)
2509        }
2510    }
2511
2512    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
2513        self.simulate_random_delay().await;
2514        let path = normalize_path(path);
2515        let mut state = self.state.lock();
2516        let (entry, _) = state
2517            .try_entry(&path, false)
2518            .with_context(|| format!("path does not exist: {path:?}"))?;
2519        if let FakeFsEntry::Symlink { target } = entry {
2520            Ok(target.clone())
2521        } else {
2522            anyhow::bail!("not a symlink: {path:?}")
2523        }
2524    }
2525
2526    async fn read_dir(
2527        &self,
2528        path: &Path,
2529    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
2530        self.simulate_random_delay().await;
2531        let path = normalize_path(path);
2532        let mut state = self.state.lock();
2533        state.read_dir_call_count += 1;
2534        let entry = state.entry(&path)?;
2535        let children = entry.dir_entries(&path)?;
2536        let paths = children
2537            .keys()
2538            .map(|file_name| Ok(path.join(file_name)))
2539            .collect::<Vec<_>>();
2540        Ok(Box::pin(futures::stream::iter(paths)))
2541    }
2542
2543    async fn watch(
2544        &self,
2545        path: &Path,
2546        _: Duration,
2547    ) -> (
2548        Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
2549        Arc<dyn Watcher>,
2550    ) {
2551        self.simulate_random_delay().await;
2552        let (tx, rx) = smol::channel::unbounded();
2553        let path = path.to_path_buf();
2554        self.state.lock().event_txs.push((path.clone(), tx.clone()));
2555        let executor = self.executor.clone();
2556        let watcher = Arc::new(FakeWatcher {
2557            tx,
2558            original_path: path.to_owned(),
2559            fs_state: self.state.clone(),
2560            prefixes: Mutex::new(vec![path]),
2561        });
2562        (
2563            Box::pin(futures::StreamExt::filter(rx, {
2564                let watcher = watcher.clone();
2565                move |events| {
2566                    let result = events.iter().any(|evt_path| {
2567                        watcher
2568                            .prefixes
2569                            .lock()
2570                            .iter()
2571                            .any(|prefix| evt_path.path.starts_with(prefix))
2572                    });
2573                    let executor = executor.clone();
2574                    async move {
2575                        executor.simulate_random_delay().await;
2576                        result
2577                    }
2578                }
2579            })),
2580            watcher,
2581        )
2582    }
2583
2584    fn open_repo(
2585        &self,
2586        abs_dot_git: &Path,
2587        _system_git_binary: Option<&Path>,
2588    ) -> Option<Arc<dyn GitRepository>> {
2589        use util::ResultExt as _;
2590
2591        self.with_git_state_and_paths(
2592            abs_dot_git,
2593            false,
2594            |_, repository_dir_path, common_dir_path| {
2595                Arc::new(fake_git_repo::FakeGitRepository {
2596                    fs: self.this.upgrade().unwrap(),
2597                    executor: self.executor.clone(),
2598                    dot_git_path: abs_dot_git.to_path_buf(),
2599                    repository_dir_path: repository_dir_path.to_owned(),
2600                    common_dir_path: common_dir_path.to_owned(),
2601                    checkpoints: Arc::default(),
2602                }) as _
2603            },
2604        )
2605        .log_err()
2606    }
2607
2608    async fn git_init(
2609        &self,
2610        abs_work_directory_path: &Path,
2611        _fallback_branch_name: String,
2612    ) -> Result<()> {
2613        self.create_dir(&abs_work_directory_path.join(".git")).await
2614    }
2615
2616    async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> {
2617        anyhow::bail!("Git clone is not supported in fake Fs")
2618    }
2619
2620    fn is_fake(&self) -> bool {
2621        true
2622    }
2623
2624    async fn is_case_sensitive(&self) -> Result<bool> {
2625        Ok(true)
2626    }
2627
2628    #[cfg(any(test, feature = "test-support"))]
2629    fn as_fake(&self) -> Arc<FakeFs> {
2630        self.this.upgrade().unwrap()
2631    }
2632}
2633
2634fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
2635    rope.chunks().flat_map(move |chunk| {
2636        let mut newline = false;
2637        let end_with_newline = chunk.ends_with('\n').then_some(line_ending.as_str());
2638        chunk
2639            .lines()
2640            .flat_map(move |line| {
2641                let ending = if newline {
2642                    Some(line_ending.as_str())
2643                } else {
2644                    None
2645                };
2646                newline = true;
2647                ending.into_iter().chain([line])
2648            })
2649            .chain(end_with_newline)
2650    })
2651}
2652
2653pub fn normalize_path(path: &Path) -> PathBuf {
2654    let mut components = path.components().peekable();
2655    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
2656        components.next();
2657        PathBuf::from(c.as_os_str())
2658    } else {
2659        PathBuf::new()
2660    };
2661
2662    for component in components {
2663        match component {
2664            Component::Prefix(..) => unreachable!(),
2665            Component::RootDir => {
2666                ret.push(component.as_os_str());
2667            }
2668            Component::CurDir => {}
2669            Component::ParentDir => {
2670                ret.pop();
2671            }
2672            Component::Normal(c) => {
2673                ret.push(c);
2674            }
2675        }
2676    }
2677    ret
2678}
2679
2680pub async fn copy_recursive<'a>(
2681    fs: &'a dyn Fs,
2682    source: &'a Path,
2683    target: &'a Path,
2684    options: CopyOptions,
2685) -> Result<()> {
2686    for (item, is_dir) in read_dir_items(fs, source).await? {
2687        let Ok(item_relative_path) = item.strip_prefix(source) else {
2688            continue;
2689        };
2690        let target_item = if item_relative_path == Path::new("") {
2691            target.to_path_buf()
2692        } else {
2693            target.join(item_relative_path)
2694        };
2695        if is_dir {
2696            if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
2697                if options.ignore_if_exists {
2698                    continue;
2699                } else {
2700                    anyhow::bail!("{target_item:?} already exists");
2701                }
2702            }
2703            let _ = fs
2704                .remove_dir(
2705                    &target_item,
2706                    RemoveOptions {
2707                        recursive: true,
2708                        ignore_if_not_exists: true,
2709                    },
2710                )
2711                .await;
2712            fs.create_dir(&target_item).await?;
2713        } else {
2714            fs.copy_file(&item, &target_item, options).await?;
2715        }
2716    }
2717    Ok(())
2718}
2719
2720/// Recursively reads all of the paths in the given directory.
2721///
2722/// Returns a vector of tuples of (path, is_dir).
2723pub async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(PathBuf, bool)>> {
2724    let mut items = Vec::new();
2725    read_recursive(fs, source, &mut items).await?;
2726    Ok(items)
2727}
2728
2729fn read_recursive<'a>(
2730    fs: &'a dyn Fs,
2731    source: &'a Path,
2732    output: &'a mut Vec<(PathBuf, bool)>,
2733) -> BoxFuture<'a, Result<()>> {
2734    use futures::future::FutureExt;
2735
2736    async move {
2737        let metadata = fs
2738            .metadata(source)
2739            .await?
2740            .with_context(|| format!("path does not exist: {source:?}"))?;
2741
2742        if metadata.is_dir {
2743            output.push((source.to_path_buf(), true));
2744            let mut children = fs.read_dir(source).await?;
2745            while let Some(child_path) = children.next().await {
2746                if let Ok(child_path) = child_path {
2747                    read_recursive(fs, &child_path, output).await?;
2748                }
2749            }
2750        } else {
2751            output.push((source.to_path_buf(), false));
2752        }
2753        Ok(())
2754    }
2755    .boxed()
2756}
2757
2758// todo(windows)
2759// can we get file id not open the file twice?
2760// https://github.com/rust-lang/rust/issues/63010
2761#[cfg(target_os = "windows")]
2762async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
2763    use std::os::windows::io::AsRawHandle;
2764
2765    use smol::fs::windows::OpenOptionsExt;
2766    use windows::Win32::{
2767        Foundation::HANDLE,
2768        Storage::FileSystem::{
2769            BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS, GetFileInformationByHandle,
2770        },
2771    };
2772
2773    let file = smol::fs::OpenOptions::new()
2774        .read(true)
2775        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
2776        .open(path)
2777        .await?;
2778
2779    let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
2780    // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
2781    // This function supports Windows XP+
2782    smol::unblock(move || {
2783        unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
2784
2785        Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
2786    })
2787    .await
2788}
2789
2790#[cfg(target_os = "windows")]
2791fn atomic_replace<P: AsRef<Path>>(
2792    replaced_file: P,
2793    replacement_file: P,
2794) -> windows::core::Result<()> {
2795    use windows::{
2796        Win32::Storage::FileSystem::{REPLACE_FILE_FLAGS, ReplaceFileW},
2797        core::HSTRING,
2798    };
2799
2800    // If the file does not exist, create it.
2801    let _ = std::fs::File::create_new(replaced_file.as_ref());
2802
2803    unsafe {
2804        ReplaceFileW(
2805            &HSTRING::from(replaced_file.as_ref().to_string_lossy().into_owned()),
2806            &HSTRING::from(replacement_file.as_ref().to_string_lossy().into_owned()),
2807            None,
2808            REPLACE_FILE_FLAGS::default(),
2809            None,
2810            None,
2811        )
2812    }
2813}
2814
2815#[cfg(test)]
2816mod tests {
2817    use super::*;
2818    use gpui::BackgroundExecutor;
2819    use serde_json::json;
2820    use util::path;
2821
2822    #[gpui::test]
2823    async fn test_fake_fs(executor: BackgroundExecutor) {
2824        let fs = FakeFs::new(executor.clone());
2825        fs.insert_tree(
2826            path!("/root"),
2827            json!({
2828                "dir1": {
2829                    "a": "A",
2830                    "b": "B"
2831                },
2832                "dir2": {
2833                    "c": "C",
2834                    "dir3": {
2835                        "d": "D"
2836                    }
2837                }
2838            }),
2839        )
2840        .await;
2841
2842        assert_eq!(
2843            fs.files(),
2844            vec![
2845                PathBuf::from(path!("/root/dir1/a")),
2846                PathBuf::from(path!("/root/dir1/b")),
2847                PathBuf::from(path!("/root/dir2/c")),
2848                PathBuf::from(path!("/root/dir2/dir3/d")),
2849            ]
2850        );
2851
2852        fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
2853            .await
2854            .unwrap();
2855
2856        assert_eq!(
2857            fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
2858                .await
2859                .unwrap(),
2860            PathBuf::from(path!("/root/dir2/dir3")),
2861        );
2862        assert_eq!(
2863            fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
2864                .await
2865                .unwrap(),
2866            PathBuf::from(path!("/root/dir2/dir3/d")),
2867        );
2868        assert_eq!(
2869            fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
2870                .await
2871                .unwrap(),
2872            "D",
2873        );
2874    }
2875
2876    #[gpui::test]
2877    async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
2878        let fs = FakeFs::new(executor.clone());
2879        fs.insert_tree(
2880            path!("/outer"),
2881            json!({
2882                "a": "A",
2883                "b": "B",
2884                "inner": {}
2885            }),
2886        )
2887        .await;
2888
2889        assert_eq!(
2890            fs.files(),
2891            vec![
2892                PathBuf::from(path!("/outer/a")),
2893                PathBuf::from(path!("/outer/b")),
2894            ]
2895        );
2896
2897        let source = Path::new(path!("/outer/a"));
2898        let target = Path::new(path!("/outer/a copy"));
2899        copy_recursive(fs.as_ref(), source, target, Default::default())
2900            .await
2901            .unwrap();
2902
2903        assert_eq!(
2904            fs.files(),
2905            vec![
2906                PathBuf::from(path!("/outer/a")),
2907                PathBuf::from(path!("/outer/a copy")),
2908                PathBuf::from(path!("/outer/b")),
2909            ]
2910        );
2911
2912        let source = Path::new(path!("/outer/a"));
2913        let target = Path::new(path!("/outer/inner/a copy"));
2914        copy_recursive(fs.as_ref(), source, target, Default::default())
2915            .await
2916            .unwrap();
2917
2918        assert_eq!(
2919            fs.files(),
2920            vec![
2921                PathBuf::from(path!("/outer/a")),
2922                PathBuf::from(path!("/outer/a copy")),
2923                PathBuf::from(path!("/outer/b")),
2924                PathBuf::from(path!("/outer/inner/a copy")),
2925            ]
2926        );
2927    }
2928
2929    #[gpui::test]
2930    async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
2931        let fs = FakeFs::new(executor.clone());
2932        fs.insert_tree(
2933            path!("/outer"),
2934            json!({
2935                "a": "A",
2936                "empty": {},
2937                "non-empty": {
2938                    "b": "B",
2939                }
2940            }),
2941        )
2942        .await;
2943
2944        assert_eq!(
2945            fs.files(),
2946            vec![
2947                PathBuf::from(path!("/outer/a")),
2948                PathBuf::from(path!("/outer/non-empty/b")),
2949            ]
2950        );
2951        assert_eq!(
2952            fs.directories(false),
2953            vec![
2954                PathBuf::from(path!("/")),
2955                PathBuf::from(path!("/outer")),
2956                PathBuf::from(path!("/outer/empty")),
2957                PathBuf::from(path!("/outer/non-empty")),
2958            ]
2959        );
2960
2961        let source = Path::new(path!("/outer/empty"));
2962        let target = Path::new(path!("/outer/empty copy"));
2963        copy_recursive(fs.as_ref(), source, target, Default::default())
2964            .await
2965            .unwrap();
2966
2967        assert_eq!(
2968            fs.files(),
2969            vec![
2970                PathBuf::from(path!("/outer/a")),
2971                PathBuf::from(path!("/outer/non-empty/b")),
2972            ]
2973        );
2974        assert_eq!(
2975            fs.directories(false),
2976            vec![
2977                PathBuf::from(path!("/")),
2978                PathBuf::from(path!("/outer")),
2979                PathBuf::from(path!("/outer/empty")),
2980                PathBuf::from(path!("/outer/empty copy")),
2981                PathBuf::from(path!("/outer/non-empty")),
2982            ]
2983        );
2984
2985        let source = Path::new(path!("/outer/non-empty"));
2986        let target = Path::new(path!("/outer/non-empty copy"));
2987        copy_recursive(fs.as_ref(), source, target, Default::default())
2988            .await
2989            .unwrap();
2990
2991        assert_eq!(
2992            fs.files(),
2993            vec![
2994                PathBuf::from(path!("/outer/a")),
2995                PathBuf::from(path!("/outer/non-empty/b")),
2996                PathBuf::from(path!("/outer/non-empty copy/b")),
2997            ]
2998        );
2999        assert_eq!(
3000            fs.directories(false),
3001            vec![
3002                PathBuf::from(path!("/")),
3003                PathBuf::from(path!("/outer")),
3004                PathBuf::from(path!("/outer/empty")),
3005                PathBuf::from(path!("/outer/empty copy")),
3006                PathBuf::from(path!("/outer/non-empty")),
3007                PathBuf::from(path!("/outer/non-empty copy")),
3008            ]
3009        );
3010    }
3011
3012    #[gpui::test]
3013    async fn test_copy_recursive(executor: BackgroundExecutor) {
3014        let fs = FakeFs::new(executor.clone());
3015        fs.insert_tree(
3016            path!("/outer"),
3017            json!({
3018                "inner1": {
3019                    "a": "A",
3020                    "b": "B",
3021                    "inner3": {
3022                        "d": "D",
3023                    },
3024                    "inner4": {}
3025                },
3026                "inner2": {
3027                    "c": "C",
3028                }
3029            }),
3030        )
3031        .await;
3032
3033        assert_eq!(
3034            fs.files(),
3035            vec![
3036                PathBuf::from(path!("/outer/inner1/a")),
3037                PathBuf::from(path!("/outer/inner1/b")),
3038                PathBuf::from(path!("/outer/inner2/c")),
3039                PathBuf::from(path!("/outer/inner1/inner3/d")),
3040            ]
3041        );
3042        assert_eq!(
3043            fs.directories(false),
3044            vec![
3045                PathBuf::from(path!("/")),
3046                PathBuf::from(path!("/outer")),
3047                PathBuf::from(path!("/outer/inner1")),
3048                PathBuf::from(path!("/outer/inner2")),
3049                PathBuf::from(path!("/outer/inner1/inner3")),
3050                PathBuf::from(path!("/outer/inner1/inner4")),
3051            ]
3052        );
3053
3054        let source = Path::new(path!("/outer"));
3055        let target = Path::new(path!("/outer/inner1/outer"));
3056        copy_recursive(fs.as_ref(), source, target, Default::default())
3057            .await
3058            .unwrap();
3059
3060        assert_eq!(
3061            fs.files(),
3062            vec![
3063                PathBuf::from(path!("/outer/inner1/a")),
3064                PathBuf::from(path!("/outer/inner1/b")),
3065                PathBuf::from(path!("/outer/inner2/c")),
3066                PathBuf::from(path!("/outer/inner1/inner3/d")),
3067                PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3068                PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3069                PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3070                PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
3071            ]
3072        );
3073        assert_eq!(
3074            fs.directories(false),
3075            vec![
3076                PathBuf::from(path!("/")),
3077                PathBuf::from(path!("/outer")),
3078                PathBuf::from(path!("/outer/inner1")),
3079                PathBuf::from(path!("/outer/inner2")),
3080                PathBuf::from(path!("/outer/inner1/inner3")),
3081                PathBuf::from(path!("/outer/inner1/inner4")),
3082                PathBuf::from(path!("/outer/inner1/outer")),
3083                PathBuf::from(path!("/outer/inner1/outer/inner1")),
3084                PathBuf::from(path!("/outer/inner1/outer/inner2")),
3085                PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
3086                PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
3087            ]
3088        );
3089    }
3090
3091    #[gpui::test]
3092    async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
3093        let fs = FakeFs::new(executor.clone());
3094        fs.insert_tree(
3095            path!("/outer"),
3096            json!({
3097                "inner1": {
3098                    "a": "A",
3099                    "b": "B",
3100                    "outer": {
3101                        "inner1": {
3102                            "a": "B"
3103                        }
3104                    }
3105                },
3106                "inner2": {
3107                    "c": "C",
3108                }
3109            }),
3110        )
3111        .await;
3112
3113        assert_eq!(
3114            fs.files(),
3115            vec![
3116                PathBuf::from(path!("/outer/inner1/a")),
3117                PathBuf::from(path!("/outer/inner1/b")),
3118                PathBuf::from(path!("/outer/inner2/c")),
3119                PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3120            ]
3121        );
3122        assert_eq!(
3123            fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3124                .await
3125                .unwrap(),
3126            "B",
3127        );
3128
3129        let source = Path::new(path!("/outer"));
3130        let target = Path::new(path!("/outer/inner1/outer"));
3131        copy_recursive(
3132            fs.as_ref(),
3133            source,
3134            target,
3135            CopyOptions {
3136                overwrite: true,
3137                ..Default::default()
3138            },
3139        )
3140        .await
3141        .unwrap();
3142
3143        assert_eq!(
3144            fs.files(),
3145            vec![
3146                PathBuf::from(path!("/outer/inner1/a")),
3147                PathBuf::from(path!("/outer/inner1/b")),
3148                PathBuf::from(path!("/outer/inner2/c")),
3149                PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3150                PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3151                PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3152                PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
3153            ]
3154        );
3155        assert_eq!(
3156            fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3157                .await
3158                .unwrap(),
3159            "A"
3160        );
3161    }
3162
3163    #[gpui::test]
3164    async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
3165        let fs = FakeFs::new(executor.clone());
3166        fs.insert_tree(
3167            path!("/outer"),
3168            json!({
3169                "inner1": {
3170                    "a": "A",
3171                    "b": "B",
3172                    "outer": {
3173                        "inner1": {
3174                            "a": "B"
3175                        }
3176                    }
3177                },
3178                "inner2": {
3179                    "c": "C",
3180                }
3181            }),
3182        )
3183        .await;
3184
3185        assert_eq!(
3186            fs.files(),
3187            vec![
3188                PathBuf::from(path!("/outer/inner1/a")),
3189                PathBuf::from(path!("/outer/inner1/b")),
3190                PathBuf::from(path!("/outer/inner2/c")),
3191                PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3192            ]
3193        );
3194        assert_eq!(
3195            fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3196                .await
3197                .unwrap(),
3198            "B",
3199        );
3200
3201        let source = Path::new(path!("/outer"));
3202        let target = Path::new(path!("/outer/inner1/outer"));
3203        copy_recursive(
3204            fs.as_ref(),
3205            source,
3206            target,
3207            CopyOptions {
3208                ignore_if_exists: true,
3209                ..Default::default()
3210            },
3211        )
3212        .await
3213        .unwrap();
3214
3215        assert_eq!(
3216            fs.files(),
3217            vec![
3218                PathBuf::from(path!("/outer/inner1/a")),
3219                PathBuf::from(path!("/outer/inner1/b")),
3220                PathBuf::from(path!("/outer/inner2/c")),
3221                PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
3222                PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
3223                PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
3224                PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
3225            ]
3226        );
3227        assert_eq!(
3228            fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
3229                .await
3230                .unwrap(),
3231            "B"
3232        );
3233    }
3234
3235    #[gpui::test]
3236    async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
3237        // With the file handle still open, the file should be replaced
3238        // https://github.com/zed-industries/zed/issues/30054
3239        let fs = RealFs {
3240            bundled_git_binary_path: None,
3241            executor,
3242        };
3243        let temp_dir = TempDir::new().unwrap();
3244        let file_to_be_replaced = temp_dir.path().join("file.txt");
3245        let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
3246        file.write_all(b"Hello").unwrap();
3247        // drop(file);  // We still hold the file handle here
3248        let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3249        assert_eq!(content, "Hello");
3250        smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
3251        let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3252        assert_eq!(content, "World");
3253    }
3254
3255    #[gpui::test]
3256    async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
3257        let fs = RealFs {
3258            bundled_git_binary_path: None,
3259            executor,
3260        };
3261        let temp_dir = TempDir::new().unwrap();
3262        let file_to_be_replaced = temp_dir.path().join("file.txt");
3263        smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
3264        let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
3265        assert_eq!(content, "Hello");
3266    }
3267}