trusted_worktrees.rs

   1//! A module, responsible for managing the trust logic in Zed.
   2//!
   3//! It deals with multiple hosts, distinguished by [`RemoteHostLocation`].
   4//! Each [`crate::Project`] and `HeadlessProject` should call [`init_global`], if wants to establish the trust mechanism.
   5//! This will set up a [`gpui::Global`] with [`TrustedWorktrees`] entity that will persist, restore and allow querying for worktree trust.
   6//! It's also possible to subscribe on [`TrustedWorktreesEvent`] events of this entity to track trust changes dynamically.
   7//!
   8//! The implementation can synchronize trust information with the remote hosts: currently, WSL and SSH.
   9//! Docker and Collab remotes do not employ trust mechanism, as manage that themselves.
  10//!
  11//! Unless `trust_all_worktrees` auto trust is enabled, does not trust anything that was not persisted before.
  12//! When dealing with "restricted" and other related concepts in the API, it means all explicitly restricted, after any of the [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_global`] calls.
  13//!
  14//! Zed does not consider invisible, `worktree.is_visible() == false` worktrees in Zed, as those are programmatically created inside Zed for internal needs, e.g. a tmp dir for `keymap_editor.rs` needs.
  15//!
  16//!
  17//! Path rust hierarchy.
  18//!
  19//! Zed has multiple layers of trust, based on the requests and [`PathTrust`] enum variants.
  20//! From the least to the most trusted level:
  21//!
  22//! * "single file worktree"
  23//!
  24//! After opening an empty Zed it's possible to open just a file, same as after opening a directory in Zed it's possible to open a file outside of this directory.
  25//! Usual scenario for both cases is opening Zed's settings.json file via `zed: open settings file` command: that starts a language server for a new file open, which originates from a newly created, single file worktree.
  26//!
  27//! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.
  28//! Each single file worktree requires a separate trust permission, unless a more global level is trusted.
  29//!
  30//! * "directory worktree"
  31//!
  32//! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
  33//! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted.
  34//!
  35//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.
  36//!
  37//! * "path override"
  38//!
  39//! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
  40//! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
  41
  42use client::ProjectId;
  43use collections::{HashMap, HashSet};
  44use gpui::{
  45    App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
  46};
  47use remote::RemoteConnectionOptions;
  48use rpc::{AnyProtoClient, proto};
  49use settings::{Settings as _, WorktreeId};
  50use std::{
  51    path::{Path, PathBuf},
  52    sync::Arc,
  53};
  54use util::debug_panic;
  55
  56use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
  57
  58pub fn init(db_trusted_paths: DbTrustedPaths, cx: &mut App) {
  59    if TrustedWorktrees::try_get_global(cx).is_none() {
  60        let trusted_worktrees = cx.new(|_| TrustedWorktreesStore::new(db_trusted_paths));
  61        cx.set_global(TrustedWorktrees(trusted_worktrees))
  62    }
  63}
  64
  65/// An initialization call to set up trust global for a particular project (remote or local).
  66pub fn track_worktree_trust(
  67    worktree_store: Entity<WorktreeStore>,
  68    remote_host: Option<RemoteHostLocation>,
  69    downstream_client: Option<(AnyProtoClient, ProjectId)>,
  70    upstream_client: Option<(AnyProtoClient, ProjectId)>,
  71    cx: &mut App,
  72) {
  73    match TrustedWorktrees::try_get_global(cx) {
  74        Some(trusted_worktrees) => {
  75            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
  76                trusted_worktrees.add_worktree_store(
  77                    worktree_store.clone(),
  78                    remote_host,
  79                    downstream_client,
  80                    upstream_client.clone(),
  81                    cx,
  82                );
  83
  84                if let Some((upstream_client, upstream_project_id)) = upstream_client {
  85                    let trusted_paths = trusted_worktrees
  86                        .trusted_paths
  87                        .get(&worktree_store.downgrade())
  88                        .into_iter()
  89                        .flatten()
  90                        .map(|trusted_path| trusted_path.to_proto())
  91                        .collect::<Vec<_>>();
  92                    if !trusted_paths.is_empty() {
  93                        upstream_client
  94                            .send(proto::TrustWorktrees {
  95                                project_id: upstream_project_id.0,
  96                                trusted_paths,
  97                            })
  98                            .ok();
  99                    }
 100                }
 101            });
 102        }
 103        None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
 104    }
 105}
 106
 107/// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
 108pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
 109
 110impl Global for TrustedWorktrees {}
 111
 112impl TrustedWorktrees {
 113    pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
 114        cx.try_global::<Self>().map(|this| this.0.clone())
 115    }
 116}
 117
 118/// A collection of worktrees that are considered trusted and not trusted.
 119/// This can be used when checking for this criteria before enabling certain features.
 120///
 121/// Emits an event each time the worktree was checked and found not trusted,
 122/// or a certain worktree had been trusted.
 123#[derive(Debug)]
 124pub struct TrustedWorktreesStore {
 125    worktree_stores: HashMap<WeakEntity<WorktreeStore>, StoreData>,
 126    db_trusted_paths: DbTrustedPaths,
 127    trusted_paths: TrustedPaths,
 128    restricted: HashMap<WeakEntity<WorktreeStore>, HashSet<WorktreeId>>,
 129    worktree_trust_serialization: Task<()>,
 130}
 131
 132#[derive(Debug, Default)]
 133struct StoreData {
 134    upstream_client: Option<(AnyProtoClient, ProjectId)>,
 135    downstream_client: Option<(AnyProtoClient, ProjectId)>,
 136    host: Option<RemoteHostLocation>,
 137}
 138
 139/// An identifier of a host to split the trust questions by.
 140/// Each trusted data change and event is done for a particular host.
 141/// A host may contain more than one worktree or even project open concurrently.
 142#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 143pub struct RemoteHostLocation {
 144    pub user_name: Option<SharedString>,
 145    pub host_identifier: SharedString,
 146}
 147
 148impl From<RemoteConnectionOptions> for RemoteHostLocation {
 149    fn from(options: RemoteConnectionOptions) -> Self {
 150        let (user_name, host_name) = match options {
 151            RemoteConnectionOptions::Ssh(ssh) => (
 152                ssh.username.map(SharedString::new),
 153                SharedString::new(ssh.host.to_string()),
 154            ),
 155            RemoteConnectionOptions::Wsl(wsl) => (
 156                wsl.user.map(SharedString::new),
 157                SharedString::new(wsl.distro_name),
 158            ),
 159            RemoteConnectionOptions::Docker(docker_connection_options) => (
 160                Some(SharedString::new(docker_connection_options.name)),
 161                SharedString::new(docker_connection_options.container_id),
 162            ),
 163            #[cfg(any(test, feature = "test-support"))]
 164            RemoteConnectionOptions::Mock(mock) => {
 165                (None, SharedString::new(format!("mock-{}", mock.id)))
 166            }
 167        };
 168        Self {
 169            user_name,
 170            host_identifier: host_name,
 171        }
 172    }
 173}
 174
 175/// A unit of trust consideration inside a particular host:
 176/// either a familiar worktree, or a path that may influence other worktrees' trust.
 177/// See module-level documentation on the trust model.
 178#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 179pub enum PathTrust {
 180    /// A worktree that is familiar to this workspace.
 181    /// Either a single file or a directory worktree.
 182    Worktree(WorktreeId),
 183    /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
 184    /// or a parent path coming out of the security modal.
 185    AbsPath(PathBuf),
 186}
 187
 188impl PathTrust {
 189    fn to_proto(&self) -> proto::PathTrust {
 190        match self {
 191            Self::Worktree(worktree_id) => proto::PathTrust {
 192                content: Some(proto::path_trust::Content::WorktreeId(
 193                    worktree_id.to_proto(),
 194                )),
 195            },
 196            Self::AbsPath(path_buf) => proto::PathTrust {
 197                content: Some(proto::path_trust::Content::AbsPath(
 198                    path_buf.to_string_lossy().to_string(),
 199                )),
 200            },
 201        }
 202    }
 203
 204    pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
 205        Some(match proto.content? {
 206            proto::path_trust::Content::WorktreeId(id) => {
 207                Self::Worktree(WorktreeId::from_proto(id))
 208            }
 209            proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
 210        })
 211    }
 212}
 213
 214/// A change of trust on a certain host.
 215#[derive(Debug)]
 216pub enum TrustedWorktreesEvent {
 217    Trusted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
 218    Restricted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
 219}
 220
 221impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 222
 223type TrustedPaths = HashMap<WeakEntity<WorktreeStore>, HashSet<PathTrust>>;
 224pub type DbTrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>;
 225
 226impl TrustedWorktreesStore {
 227    fn new(db_trusted_paths: DbTrustedPaths) -> Self {
 228        Self {
 229            db_trusted_paths,
 230            trusted_paths: HashMap::default(),
 231            worktree_stores: HashMap::default(),
 232            restricted: HashMap::default(),
 233            worktree_trust_serialization: Task::ready(()),
 234        }
 235    }
 236
 237    /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
 238    pub fn has_restricted_worktrees(
 239        &self,
 240        worktree_store: &Entity<WorktreeStore>,
 241        cx: &App,
 242    ) -> bool {
 243        self.restricted
 244            .get(&worktree_store.downgrade())
 245            .is_some_and(|restricted_worktrees| {
 246                restricted_worktrees.iter().any(|restricted_worktree| {
 247                    worktree_store
 248                        .read(cx)
 249                        .worktree_for_id(*restricted_worktree, cx)
 250                        .is_some()
 251                })
 252            })
 253    }
 254
 255    /// Adds certain entities on this host to the trusted list.
 256    /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
 257    /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
 258    pub fn trust(
 259        &mut self,
 260        worktree_store: &Entity<WorktreeStore>,
 261        mut trusted_paths: HashSet<PathTrust>,
 262        cx: &mut Context<Self>,
 263    ) {
 264        let weak_worktree_store = worktree_store.downgrade();
 265        let mut new_trusted_single_file_worktrees = HashSet::default();
 266        let mut new_trusted_other_worktrees = HashSet::default();
 267        let mut new_trusted_abs_paths = HashSet::default();
 268        for trusted_path in trusted_paths.iter().chain(
 269            self.trusted_paths
 270                .remove(&weak_worktree_store)
 271                .iter()
 272                .flat_map(|current_trusted| current_trusted.iter()),
 273        ) {
 274            match trusted_path {
 275                PathTrust::Worktree(worktree_id) => {
 276                    if let Some(restricted_worktrees) =
 277                        self.restricted.get_mut(&weak_worktree_store)
 278                    {
 279                        restricted_worktrees.remove(worktree_id);
 280                        if restricted_worktrees.is_empty() {
 281                            self.restricted.remove(&weak_worktree_store);
 282                        }
 283                    };
 284
 285                    if let Some(worktree) =
 286                        worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
 287                    {
 288                        if worktree.read(cx).is_single_file() {
 289                            new_trusted_single_file_worktrees.insert(*worktree_id);
 290                        } else {
 291                            new_trusted_other_worktrees
 292                                .insert((worktree.read(cx).abs_path(), *worktree_id));
 293                        }
 294                    }
 295                }
 296                PathTrust::AbsPath(abs_path) => {
 297                    debug_assert!(
 298                        util::paths::is_absolute(
 299                            &abs_path.to_string_lossy(),
 300                            worktree_store.read(cx).path_style()
 301                        ),
 302                        "Cannot trust non-absolute path {abs_path:?} on path style {style:?}",
 303                        style = worktree_store.read(cx).path_style()
 304                    );
 305                    if let Some((worktree_id, is_file)) =
 306                        find_worktree_in_store(worktree_store.read(cx), abs_path, cx)
 307                    {
 308                        if is_file {
 309                            new_trusted_single_file_worktrees.insert(worktree_id);
 310                        } else {
 311                            new_trusted_other_worktrees
 312                                .insert((Arc::from(abs_path.as_path()), worktree_id));
 313                        }
 314                    }
 315                    new_trusted_abs_paths.insert(abs_path.clone());
 316                }
 317            }
 318        }
 319
 320        new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
 321            new_trusted_abs_paths
 322                .iter()
 323                .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
 324        });
 325        if !new_trusted_other_worktrees.is_empty() {
 326            new_trusted_single_file_worktrees.clear();
 327        }
 328
 329        if let Some(restricted_worktrees) = self.restricted.remove(&weak_worktree_store) {
 330            let new_restricted_worktrees = restricted_worktrees
 331                .into_iter()
 332                .filter(|restricted_worktree| {
 333                    let Some(worktree) = worktree_store
 334                        .read(cx)
 335                        .worktree_for_id(*restricted_worktree, cx)
 336                    else {
 337                        return false;
 338                    };
 339                    let is_file = worktree.read(cx).is_single_file();
 340
 341                    // When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.
 342                    if is_file && !new_trusted_abs_paths.is_empty() {
 343                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 344                        return false;
 345                    }
 346
 347                    let restricted_worktree_path = worktree.read(cx).abs_path();
 348                    let retain = (!is_file || new_trusted_other_worktrees.is_empty())
 349                        && new_trusted_abs_paths.iter().all(|new_trusted_path| {
 350                            !restricted_worktree_path.starts_with(new_trusted_path)
 351                        });
 352                    if !retain {
 353                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 354                    }
 355                    retain
 356                })
 357                .collect();
 358            self.restricted
 359                .insert(weak_worktree_store.clone(), new_restricted_worktrees);
 360        }
 361
 362        {
 363            let trusted_paths = self
 364                .trusted_paths
 365                .entry(weak_worktree_store.clone())
 366                .or_default();
 367            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
 368            trusted_paths.extend(
 369                new_trusted_other_worktrees
 370                    .into_iter()
 371                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
 372            );
 373            trusted_paths.extend(
 374                new_trusted_single_file_worktrees
 375                    .into_iter()
 376                    .map(PathTrust::Worktree),
 377            );
 378        }
 379
 380        if let Some(store_data) = self.worktree_stores.get(&weak_worktree_store) {
 381            if let Some((upstream_client, upstream_project_id)) = &store_data.upstream_client {
 382                let trusted_paths = trusted_paths
 383                    .iter()
 384                    .map(|trusted_path| trusted_path.to_proto())
 385                    .collect::<Vec<_>>();
 386                if !trusted_paths.is_empty() {
 387                    upstream_client
 388                        .send(proto::TrustWorktrees {
 389                            project_id: upstream_project_id.0,
 390                            trusted_paths,
 391                        })
 392                        .ok();
 393                }
 394            }
 395        }
 396        cx.emit(TrustedWorktreesEvent::Trusted(
 397            weak_worktree_store,
 398            trusted_paths,
 399        ));
 400    }
 401
 402    /// Restricts certain entities on this host.
 403    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
 404    pub fn restrict(
 405        &mut self,
 406        worktree_store: WeakEntity<WorktreeStore>,
 407        restricted_paths: HashSet<PathTrust>,
 408        cx: &mut Context<Self>,
 409    ) {
 410        let mut restricted = HashSet::default();
 411        for restricted_path in restricted_paths {
 412            match restricted_path {
 413                PathTrust::Worktree(worktree_id) => {
 414                    self.restricted
 415                        .entry(worktree_store.clone())
 416                        .or_default()
 417                        .insert(worktree_id);
 418                    restricted.insert(PathTrust::Worktree(worktree_id));
 419                }
 420                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
 421            }
 422        }
 423
 424        cx.emit(TrustedWorktreesEvent::Restricted(
 425            worktree_store,
 426            restricted,
 427        ));
 428    }
 429
 430    /// Erases all trust information.
 431    /// Requires Zed's restart to take proper effect.
 432    pub fn clear_trusted_paths(&mut self) {
 433        self.trusted_paths.clear();
 434        self.db_trusted_paths.clear();
 435    }
 436
 437    /// Checks whether a certain worktree is trusted (or on a larger trust level).
 438    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
 439    ///
 440    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 441    pub fn can_trust(
 442        &mut self,
 443        worktree_store: &Entity<WorktreeStore>,
 444        worktree_id: WorktreeId,
 445        cx: &mut Context<Self>,
 446    ) -> bool {
 447        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 448            return true;
 449        }
 450
 451        let weak_worktree_store = worktree_store.downgrade();
 452        let Some(worktree) = worktree_store.read(cx).worktree_for_id(worktree_id, cx) else {
 453            return false;
 454        };
 455        let worktree_path = worktree.read(cx).abs_path();
 456        // Zed opened an "internal" directory: e.g. a tmp dir for `keymap_editor.rs` needs.
 457        if !worktree.read(cx).is_visible() {
 458            log::debug!("Skipping worktree trust checks for not visible {worktree_path:?}");
 459            return true;
 460        }
 461
 462        let is_file = worktree.read(cx).is_single_file();
 463        if self
 464            .restricted
 465            .get(&weak_worktree_store)
 466            .is_some_and(|restricted_worktrees| restricted_worktrees.contains(&worktree_id))
 467        {
 468            return false;
 469        }
 470
 471        if self
 472            .trusted_paths
 473            .get(&weak_worktree_store)
 474            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
 475        {
 476            return true;
 477        }
 478
 479        // * Single files are auto-approved when something else (not a single file) was approved on this host already.
 480        // * If parent path is trusted already, this worktree is stusted also.
 481        //
 482        // See module documentation for details on trust level.
 483        if let Some(trusted_paths) = self.trusted_paths.get(&weak_worktree_store) {
 484            let auto_trusted = worktree_store.read_with(cx, |worktree_store, cx| {
 485                trusted_paths.iter().any(|trusted_path| match trusted_path {
 486                    PathTrust::Worktree(worktree_id) => worktree_store
 487                        .worktree_for_id(*worktree_id, cx)
 488                        .is_some_and(|worktree| {
 489                            let worktree = worktree.read(cx);
 490                            worktree_path.starts_with(&worktree.abs_path())
 491                                || (is_file && !worktree.is_single_file())
 492                        }),
 493                    PathTrust::AbsPath(trusted_path) => {
 494                        is_file || worktree_path.starts_with(trusted_path)
 495                    }
 496                })
 497            });
 498            if auto_trusted {
 499                return true;
 500            }
 501        }
 502
 503        self.restricted
 504            .entry(weak_worktree_store.clone())
 505            .or_default()
 506            .insert(worktree_id);
 507        log::info!("Worktree {worktree_path:?} is not trusted");
 508        if let Some(store_data) = self.worktree_stores.get(&weak_worktree_store) {
 509            if let Some((downstream_client, downstream_project_id)) = &store_data.downstream_client
 510            {
 511                downstream_client
 512                    .send(proto::RestrictWorktrees {
 513                        project_id: downstream_project_id.0,
 514                        worktree_ids: vec![worktree_id.to_proto()],
 515                    })
 516                    .ok();
 517            }
 518            if let Some((upstream_client, upstream_project_id)) = &store_data.upstream_client {
 519                upstream_client
 520                    .send(proto::RestrictWorktrees {
 521                        project_id: upstream_project_id.0,
 522                        worktree_ids: vec![worktree_id.to_proto()],
 523                    })
 524                    .ok();
 525            }
 526        }
 527        cx.emit(TrustedWorktreesEvent::Restricted(
 528            weak_worktree_store,
 529            HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 530        ));
 531        false
 532    }
 533
 534    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
 535    pub fn restricted_worktrees(
 536        &self,
 537        worktree_store: &Entity<WorktreeStore>,
 538        cx: &App,
 539    ) -> HashSet<(WorktreeId, Arc<Path>)> {
 540        let mut single_file_paths = HashSet::default();
 541
 542        let other_paths = self
 543            .restricted
 544            .get(&worktree_store.downgrade())
 545            .into_iter()
 546            .flatten()
 547            .filter_map(|&restricted_worktree_id| {
 548                let worktree = worktree_store
 549                    .read(cx)
 550                    .worktree_for_id(restricted_worktree_id, cx)?;
 551                let worktree = worktree.read(cx);
 552                let abs_path = worktree.abs_path();
 553                if worktree.is_single_file() {
 554                    single_file_paths.insert((restricted_worktree_id, abs_path));
 555                    None
 556                } else {
 557                    Some((restricted_worktree_id, abs_path))
 558                }
 559            })
 560            .collect::<HashSet<_>>();
 561
 562        if !other_paths.is_empty() {
 563            return other_paths;
 564        } else {
 565            single_file_paths
 566        }
 567    }
 568
 569    /// Switches the "trust nothing" mode to "automatically trust everything".
 570    /// This does not influence already persisted data, but stops adding new worktrees there.
 571    pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
 572        for (worktree_store, worktrees) in std::mem::take(&mut self.restricted).into_iter().fold(
 573            HashMap::default(),
 574            |mut acc, (remote_host, worktrees)| {
 575                acc.entry(remote_host)
 576                    .or_insert_with(HashSet::default)
 577                    .extend(worktrees.into_iter().map(PathTrust::Worktree));
 578                acc
 579            },
 580        ) {
 581            if let Some(worktree_store) = worktree_store.upgrade() {
 582                self.trust(&worktree_store, worktrees, cx);
 583            }
 584        }
 585    }
 586
 587    pub fn schedule_serialization<S>(&mut self, cx: &mut Context<Self>, serialize: S)
 588    where
 589        S: FnOnce(HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>, &App) -> Task<()>
 590            + 'static,
 591    {
 592        self.worktree_trust_serialization = serialize(self.trusted_paths_for_serialization(cx), cx);
 593    }
 594
 595    fn trusted_paths_for_serialization(
 596        &mut self,
 597        cx: &mut Context<Self>,
 598    ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
 599        let new_trusted_paths = self
 600            .trusted_paths
 601            .iter()
 602            .filter_map(|(worktree_store, paths)| {
 603                let host = self.worktree_stores.get(&worktree_store)?.host.clone();
 604                let abs_paths = paths
 605                    .iter()
 606                    .flat_map(|path| match path {
 607                        PathTrust::Worktree(worktree_id) => worktree_store
 608                            .upgrade()
 609                            .and_then(|worktree_store| {
 610                                worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
 611                            })
 612                            .map(|worktree| worktree.read(cx).abs_path().to_path_buf()),
 613                        PathTrust::AbsPath(abs_path) => Some(abs_path.clone()),
 614                    })
 615                    .collect::<HashSet<_>>();
 616                Some((host, abs_paths))
 617            })
 618            .chain(self.db_trusted_paths.drain())
 619            .fold(HashMap::default(), |mut acc, (host, paths)| {
 620                acc.entry(host)
 621                    .or_insert_with(HashSet::default)
 622                    .extend(paths);
 623                acc
 624            });
 625
 626        self.db_trusted_paths = new_trusted_paths.clone();
 627        new_trusted_paths
 628    }
 629
 630    fn add_worktree_store(
 631        &mut self,
 632        worktree_store: Entity<WorktreeStore>,
 633        remote_host: Option<RemoteHostLocation>,
 634        downstream_client: Option<(AnyProtoClient, ProjectId)>,
 635        upstream_client: Option<(AnyProtoClient, ProjectId)>,
 636        cx: &mut Context<Self>,
 637    ) {
 638        self.worktree_stores
 639            .retain(|worktree_store, _| worktree_store.is_upgradable());
 640        let weak_worktree_store = worktree_store.downgrade();
 641        self.worktree_stores.insert(
 642            weak_worktree_store.clone(),
 643            StoreData {
 644                host: remote_host.clone(),
 645                downstream_client,
 646                upstream_client,
 647            },
 648        );
 649
 650        let mut new_trusted_paths = HashSet::default();
 651        if let Some(db_trusted_paths) = self.db_trusted_paths.get(&remote_host) {
 652            new_trusted_paths.extend(db_trusted_paths.clone().into_iter().map(PathTrust::AbsPath));
 653        }
 654        if let Some(trusted_paths) = self.trusted_paths.remove(&weak_worktree_store) {
 655            new_trusted_paths.extend(trusted_paths);
 656        }
 657        if !new_trusted_paths.is_empty() {
 658            self.trusted_paths.insert(
 659                weak_worktree_store,
 660                new_trusted_paths
 661                    .into_iter()
 662                    .map(|path_trust| match path_trust {
 663                        PathTrust::AbsPath(abs_path) => {
 664                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
 665                                .map(|(worktree_id, _)| PathTrust::Worktree(worktree_id))
 666                                .unwrap_or_else(|| PathTrust::AbsPath(abs_path))
 667                        }
 668                        other => other,
 669                    })
 670                    .collect(),
 671            );
 672        }
 673    }
 674}
 675
 676fn find_worktree_in_store(
 677    worktree_store: &WorktreeStore,
 678    abs_path: &Path,
 679    cx: &App,
 680) -> Option<(WorktreeId, bool)> {
 681    let (worktree, path_in_worktree) = worktree_store.find_worktree(&abs_path, cx)?;
 682    if path_in_worktree.is_empty() {
 683        Some((worktree.read(cx).id(), worktree.read(cx).is_single_file()))
 684    } else {
 685        None
 686    }
 687}
 688
 689#[cfg(test)]
 690mod tests {
 691    use std::{cell::RefCell, path::PathBuf, rc::Rc};
 692
 693    use collections::HashSet;
 694    use gpui::TestAppContext;
 695    use serde_json::json;
 696    use settings::SettingsStore;
 697    use util::path;
 698
 699    use crate::{FakeFs, Project};
 700
 701    use super::*;
 702
 703    fn init_test(cx: &mut TestAppContext) {
 704        cx.update(|cx| {
 705            if cx.try_global::<SettingsStore>().is_none() {
 706                let settings_store = SettingsStore::test(cx);
 707                cx.set_global(settings_store);
 708            }
 709            if cx.try_global::<TrustedWorktrees>().is_some() {
 710                cx.remove_global::<TrustedWorktrees>();
 711            }
 712        });
 713    }
 714
 715    fn init_trust_global(
 716        worktree_store: Entity<WorktreeStore>,
 717        cx: &mut TestAppContext,
 718    ) -> Entity<TrustedWorktreesStore> {
 719        cx.update(|cx| {
 720            init(HashMap::default(), cx);
 721            track_worktree_trust(worktree_store, None, None, None, cx);
 722            TrustedWorktrees::try_get_global(cx).expect("global should be set")
 723        })
 724    }
 725
 726    #[gpui::test]
 727    async fn test_single_worktree_trust(cx: &mut TestAppContext) {
 728        init_test(cx);
 729
 730        let fs = FakeFs::new(cx.executor());
 731        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
 732            .await;
 733
 734        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 735        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 736        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 737            store.worktrees().next().unwrap().read(cx).id()
 738        });
 739
 740        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 741
 742        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 743        cx.update({
 744            let events = events.clone();
 745            |cx| {
 746                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 747                    events.borrow_mut().push(match event {
 748                        TrustedWorktreesEvent::Trusted(host, paths) => {
 749                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 750                        }
 751                        TrustedWorktreesEvent::Restricted(host, paths) => {
 752                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 753                        }
 754                    });
 755                })
 756            }
 757        })
 758        .detach();
 759
 760        let can_trust = trusted_worktrees.update(cx, |store, cx| {
 761            store.can_trust(&worktree_store, worktree_id, cx)
 762        });
 763        assert!(!can_trust, "worktree should be restricted by default");
 764
 765        {
 766            let events = events.borrow();
 767            assert_eq!(events.len(), 1);
 768            match &events[0] {
 769                TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
 770                    assert_eq!(event_worktree_store, &worktree_store.downgrade());
 771                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 772                }
 773                _ => panic!("expected Restricted event"),
 774            }
 775        }
 776
 777        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
 778            store.has_restricted_worktrees(&worktree_store, cx)
 779        });
 780        assert!(has_restricted, "should have restricted worktrees");
 781
 782        let restricted = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
 783            trusted_worktrees.restricted_worktrees(&worktree_store, cx)
 784        });
 785        assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
 786
 787        events.borrow_mut().clear();
 788
 789        let can_trust_again = trusted_worktrees.update(cx, |store, cx| {
 790            store.can_trust(&worktree_store, worktree_id, cx)
 791        });
 792        assert!(!can_trust_again, "worktree should still be restricted");
 793        assert!(
 794            events.borrow().is_empty(),
 795            "no duplicate Restricted event on repeated can_trust"
 796        );
 797
 798        trusted_worktrees.update(cx, |store, cx| {
 799            store.trust(
 800                &worktree_store,
 801                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 802                cx,
 803            );
 804        });
 805
 806        {
 807            let events = events.borrow();
 808            assert_eq!(events.len(), 1);
 809            match &events[0] {
 810                TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
 811                    assert_eq!(event_worktree_store, &worktree_store.downgrade());
 812                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 813                }
 814                _ => panic!("expected Trusted event"),
 815            }
 816        }
 817
 818        let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
 819            store.can_trust(&worktree_store, worktree_id, cx)
 820        });
 821        assert!(can_trust_after, "worktree should be trusted after trust()");
 822
 823        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
 824            store.has_restricted_worktrees(&worktree_store, cx)
 825        });
 826        assert!(
 827            !has_restricted_after,
 828            "should have no restricted worktrees after trust"
 829        );
 830
 831        let restricted_after = trusted_worktrees.read_with(cx, |trusted_worktrees, cx| {
 832            trusted_worktrees.restricted_worktrees(&worktree_store, cx)
 833        });
 834        assert!(
 835            restricted_after.is_empty(),
 836            "restricted set should be empty"
 837        );
 838    }
 839
 840    #[gpui::test]
 841    async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
 842        init_test(cx);
 843
 844        let fs = FakeFs::new(cx.executor());
 845        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
 846            .await;
 847
 848        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
 849        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 850        let worktree_id = worktree_store.read_with(cx, |store, cx| {
 851            let worktree = store.worktrees().next().unwrap();
 852            let worktree = worktree.read(cx);
 853            assert!(worktree.is_single_file(), "expected single-file worktree");
 854            worktree.id()
 855        });
 856
 857        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 858
 859        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
 860        cx.update({
 861            let events = events.clone();
 862            |cx| {
 863                cx.subscribe(&trusted_worktrees, move |_, event, _| {
 864                    events.borrow_mut().push(match event {
 865                        TrustedWorktreesEvent::Trusted(host, paths) => {
 866                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
 867                        }
 868                        TrustedWorktreesEvent::Restricted(host, paths) => {
 869                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
 870                        }
 871                    });
 872                })
 873            }
 874        })
 875        .detach();
 876
 877        let can_trust = trusted_worktrees.update(cx, |store, cx| {
 878            store.can_trust(&worktree_store, worktree_id, cx)
 879        });
 880        assert!(
 881            !can_trust,
 882            "single-file worktree should be restricted by default"
 883        );
 884
 885        {
 886            let events = events.borrow();
 887            assert_eq!(events.len(), 1);
 888            match &events[0] {
 889                TrustedWorktreesEvent::Restricted(event_worktree_store, paths) => {
 890                    assert_eq!(event_worktree_store, &worktree_store.downgrade());
 891                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 892                }
 893                _ => panic!("expected Restricted event"),
 894            }
 895        }
 896
 897        events.borrow_mut().clear();
 898
 899        trusted_worktrees.update(cx, |store, cx| {
 900            store.trust(
 901                &worktree_store,
 902                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 903                cx,
 904            );
 905        });
 906
 907        {
 908            let events = events.borrow();
 909            assert_eq!(events.len(), 1);
 910            match &events[0] {
 911                TrustedWorktreesEvent::Trusted(event_worktree_store, paths) => {
 912                    assert_eq!(event_worktree_store, &worktree_store.downgrade());
 913                    assert!(paths.contains(&PathTrust::Worktree(worktree_id)));
 914                }
 915                _ => panic!("expected Trusted event"),
 916            }
 917        }
 918
 919        let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
 920            store.can_trust(&worktree_store, worktree_id, cx)
 921        });
 922        assert!(
 923            can_trust_after,
 924            "single-file worktree should be trusted after trust()"
 925        );
 926    }
 927
 928    #[gpui::test]
 929    async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
 930        init_test(cx);
 931
 932        let fs = FakeFs::new(cx.executor());
 933        fs.insert_tree(
 934            path!("/root"),
 935            json!({
 936                "a.rs": "fn a() {}",
 937                "b.rs": "fn b() {}",
 938                "c.rs": "fn c() {}"
 939            }),
 940        )
 941        .await;
 942
 943        let project = Project::test(
 944            fs,
 945            [
 946                path!("/root/a.rs").as_ref(),
 947                path!("/root/b.rs").as_ref(),
 948                path!("/root/c.rs").as_ref(),
 949            ],
 950            cx,
 951        )
 952        .await;
 953        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
 954        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
 955            store
 956                .worktrees()
 957                .map(|worktree| {
 958                    let worktree = worktree.read(cx);
 959                    assert!(worktree.is_single_file());
 960                    worktree.id()
 961                })
 962                .collect()
 963        });
 964        assert_eq!(worktree_ids.len(), 3);
 965
 966        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
 967
 968        for &worktree_id in &worktree_ids {
 969            let can_trust = trusted_worktrees.update(cx, |store, cx| {
 970                store.can_trust(&worktree_store, worktree_id, cx)
 971            });
 972            assert!(
 973                !can_trust,
 974                "worktree {worktree_id:?} should be restricted initially"
 975            );
 976        }
 977
 978        trusted_worktrees.update(cx, |store, cx| {
 979            store.trust(
 980                &worktree_store,
 981                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
 982                cx,
 983            );
 984        });
 985
 986        let can_trust_0 = trusted_worktrees.update(cx, |store, cx| {
 987            store.can_trust(&worktree_store, worktree_ids[0], cx)
 988        });
 989        let can_trust_1 = trusted_worktrees.update(cx, |store, cx| {
 990            store.can_trust(&worktree_store, worktree_ids[1], cx)
 991        });
 992        let can_trust_2 = trusted_worktrees.update(cx, |store, cx| {
 993            store.can_trust(&worktree_store, worktree_ids[2], cx)
 994        });
 995
 996        assert!(!can_trust_0, "worktree 0 should still be restricted");
 997        assert!(can_trust_1, "worktree 1 should be trusted");
 998        assert!(!can_trust_2, "worktree 2 should still be restricted");
 999    }
1000
1001    #[gpui::test]
1002    async fn test_two_directory_worktrees_separate_trust(cx: &mut TestAppContext) {
1003        init_test(cx);
1004
1005        let fs = FakeFs::new(cx.executor());
1006        fs.insert_tree(
1007            path!("/projects"),
1008            json!({
1009                "project_a": { "main.rs": "fn main() {}" },
1010                "project_b": { "lib.rs": "pub fn lib() {}" }
1011            }),
1012        )
1013        .await;
1014
1015        let project = Project::test(
1016            fs,
1017            [
1018                path!("/projects/project_a").as_ref(),
1019                path!("/projects/project_b").as_ref(),
1020            ],
1021            cx,
1022        )
1023        .await;
1024        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1025        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1026            store
1027                .worktrees()
1028                .map(|worktree| {
1029                    let worktree = worktree.read(cx);
1030                    assert!(!worktree.is_single_file());
1031                    worktree.id()
1032                })
1033                .collect()
1034        });
1035        assert_eq!(worktree_ids.len(), 2);
1036
1037        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1038
1039        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1040            store.can_trust(&worktree_store, worktree_ids[0], cx)
1041        });
1042        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1043            store.can_trust(&worktree_store, worktree_ids[1], cx)
1044        });
1045        assert!(!can_trust_a, "project_a should be restricted initially");
1046        assert!(!can_trust_b, "project_b should be restricted initially");
1047
1048        trusted_worktrees.update(cx, |store, cx| {
1049            store.trust(
1050                &worktree_store,
1051                HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1052                cx,
1053            );
1054        });
1055
1056        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1057            store.can_trust(&worktree_store, worktree_ids[0], cx)
1058        });
1059        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1060            store.can_trust(&worktree_store, worktree_ids[1], cx)
1061        });
1062        assert!(can_trust_a, "project_a should be trusted after trust()");
1063        assert!(!can_trust_b, "project_b should still be restricted");
1064
1065        trusted_worktrees.update(cx, |store, cx| {
1066            store.trust(
1067                &worktree_store,
1068                HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1069                cx,
1070            );
1071        });
1072
1073        let can_trust_a = trusted_worktrees.update(cx, |store, cx| {
1074            store.can_trust(&worktree_store, worktree_ids[0], cx)
1075        });
1076        let can_trust_b = trusted_worktrees.update(cx, |store, cx| {
1077            store.can_trust(&worktree_store, worktree_ids[1], cx)
1078        });
1079        assert!(can_trust_a, "project_a should remain trusted");
1080        assert!(can_trust_b, "project_b should now be trusted");
1081    }
1082
1083    #[gpui::test]
1084    async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
1085        init_test(cx);
1086
1087        let fs = FakeFs::new(cx.executor());
1088        fs.insert_tree(
1089            path!("/"),
1090            json!({
1091                "project": { "main.rs": "fn main() {}" },
1092                "standalone.rs": "fn standalone() {}"
1093            }),
1094        )
1095        .await;
1096
1097        let project = Project::test(
1098            fs,
1099            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1100            cx,
1101        )
1102        .await;
1103        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1104        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1105            let worktrees: Vec<_> = store.worktrees().collect();
1106            assert_eq!(worktrees.len(), 2);
1107            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1108                (&worktrees[1], &worktrees[0])
1109            } else {
1110                (&worktrees[0], &worktrees[1])
1111            };
1112            assert!(!dir_worktree.read(cx).is_single_file());
1113            assert!(file_worktree.read(cx).is_single_file());
1114            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1115        });
1116
1117        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1118
1119        let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1120            store.can_trust(&worktree_store, file_worktree_id, cx)
1121        });
1122        assert!(
1123            !can_trust_file,
1124            "single-file worktree should be restricted initially"
1125        );
1126
1127        let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1128            store.can_trust(&worktree_store, dir_worktree_id, cx)
1129        });
1130        assert!(
1131            !can_trust_directory,
1132            "directory worktree should be restricted initially"
1133        );
1134
1135        trusted_worktrees.update(cx, |store, cx| {
1136            store.trust(
1137                &worktree_store,
1138                HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
1139                cx,
1140            );
1141        });
1142
1143        let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1144            store.can_trust(&worktree_store, dir_worktree_id, cx)
1145        });
1146        let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1147            store.can_trust(&worktree_store, file_worktree_id, cx)
1148        });
1149        assert!(can_trust_dir, "directory worktree should be trusted");
1150        assert!(
1151            can_trust_file_after,
1152            "single-file worktree should be trusted after directory worktree trust"
1153        );
1154    }
1155
1156    #[gpui::test]
1157    async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
1158        init_test(cx);
1159
1160        let fs = FakeFs::new(cx.executor());
1161        fs.insert_tree(
1162            path!("/"),
1163            json!({
1164                "project": { "main.rs": "fn main() {}" },
1165                "standalone.rs": "fn standalone() {}"
1166            }),
1167        )
1168        .await;
1169
1170        let project = Project::test(
1171            fs,
1172            [path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
1173            cx,
1174        )
1175        .await;
1176        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1177        let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
1178            let worktrees: Vec<_> = store.worktrees().collect();
1179            assert_eq!(worktrees.len(), 2);
1180            let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
1181                (&worktrees[1], &worktrees[0])
1182            } else {
1183                (&worktrees[0], &worktrees[1])
1184            };
1185            assert!(!dir_worktree.read(cx).is_single_file());
1186            assert!(file_worktree.read(cx).is_single_file());
1187            (dir_worktree.read(cx).id(), file_worktree.read(cx).id())
1188        });
1189
1190        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1191
1192        let can_trust_file = trusted_worktrees.update(cx, |store, cx| {
1193            store.can_trust(&worktree_store, file_worktree_id, cx)
1194        });
1195        assert!(
1196            !can_trust_file,
1197            "single-file worktree should be restricted initially"
1198        );
1199
1200        let can_trust_directory = trusted_worktrees.update(cx, |store, cx| {
1201            store.can_trust(&worktree_store, dir_worktree_id, cx)
1202        });
1203        assert!(
1204            !can_trust_directory,
1205            "directory worktree should be restricted initially"
1206        );
1207
1208        trusted_worktrees.update(cx, |store, cx| {
1209            store.trust(
1210                &worktree_store,
1211                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
1212                cx,
1213            );
1214        });
1215
1216        let can_trust_dir = trusted_worktrees.update(cx, |store, cx| {
1217            store.can_trust(&worktree_store, dir_worktree_id, cx)
1218        });
1219        let can_trust_file_after = trusted_worktrees.update(cx, |store, cx| {
1220            store.can_trust(&worktree_store, file_worktree_id, cx)
1221        });
1222        assert!(
1223            can_trust_dir,
1224            "directory worktree should be trusted after its parent is trusted"
1225        );
1226        assert!(
1227            can_trust_file_after,
1228            "single-file worktree should be trusted after directory worktree trust via its parent directory trust"
1229        );
1230    }
1231
1232    #[gpui::test]
1233    async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
1234        init_test(cx);
1235
1236        let fs = FakeFs::new(cx.executor());
1237        fs.insert_tree(
1238            path!("/root"),
1239            json!({
1240                "project_a": { "main.rs": "fn main() {}" },
1241                "project_b": { "lib.rs": "pub fn lib() {}" }
1242            }),
1243        )
1244        .await;
1245
1246        let project = Project::test(
1247            fs,
1248            [
1249                path!("/root/project_a").as_ref(),
1250                path!("/root/project_b").as_ref(),
1251            ],
1252            cx,
1253        )
1254        .await;
1255        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1256        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1257            store
1258                .worktrees()
1259                .map(|worktree| worktree.read(cx).id())
1260                .collect()
1261        });
1262        assert_eq!(worktree_ids.len(), 2);
1263
1264        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1265
1266        for &worktree_id in &worktree_ids {
1267            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1268                store.can_trust(&worktree_store, worktree_id, cx)
1269            });
1270            assert!(!can_trust, "worktree should be restricted initially");
1271        }
1272
1273        trusted_worktrees.update(cx, |store, cx| {
1274            store.trust(
1275                &worktree_store,
1276                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
1277                cx,
1278            );
1279        });
1280
1281        for &worktree_id in &worktree_ids {
1282            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1283                store.can_trust(&worktree_store, worktree_id, cx)
1284            });
1285            assert!(
1286                can_trust,
1287                "worktree should be trusted after parent path trust"
1288            );
1289        }
1290    }
1291
1292    #[gpui::test]
1293    async fn test_auto_trust_all(cx: &mut TestAppContext) {
1294        init_test(cx);
1295
1296        let fs = FakeFs::new(cx.executor());
1297        fs.insert_tree(
1298            path!("/"),
1299            json!({
1300                "project_a": { "main.rs": "fn main() {}" },
1301                "project_b": { "lib.rs": "pub fn lib() {}" },
1302                "single.rs": "fn single() {}"
1303            }),
1304        )
1305        .await;
1306
1307        let project = Project::test(
1308            fs,
1309            [
1310                path!("/project_a").as_ref(),
1311                path!("/project_b").as_ref(),
1312                path!("/single.rs").as_ref(),
1313            ],
1314            cx,
1315        )
1316        .await;
1317        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1318        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1319            store
1320                .worktrees()
1321                .map(|worktree| worktree.read(cx).id())
1322                .collect()
1323        });
1324        assert_eq!(worktree_ids.len(), 3);
1325
1326        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1327
1328        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1329        cx.update({
1330            let events = events.clone();
1331            |cx| {
1332                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1333                    events.borrow_mut().push(match event {
1334                        TrustedWorktreesEvent::Trusted(host, paths) => {
1335                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1336                        }
1337                        TrustedWorktreesEvent::Restricted(host, paths) => {
1338                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1339                        }
1340                    });
1341                })
1342            }
1343        })
1344        .detach();
1345
1346        for &worktree_id in &worktree_ids {
1347            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1348                store.can_trust(&worktree_store, worktree_id, cx)
1349            });
1350            assert!(!can_trust, "worktree should be restricted initially");
1351        }
1352
1353        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1354            store.has_restricted_worktrees(&worktree_store, cx)
1355        });
1356        assert!(has_restricted, "should have restricted worktrees");
1357
1358        events.borrow_mut().clear();
1359
1360        trusted_worktrees.update(cx, |store, cx| {
1361            store.auto_trust_all(cx);
1362        });
1363
1364        for &worktree_id in &worktree_ids {
1365            let can_trust = trusted_worktrees.update(cx, |store, cx| {
1366                store.can_trust(&worktree_store, worktree_id, cx)
1367            });
1368            assert!(
1369                can_trust,
1370                "worktree {worktree_id:?} should be trusted after auto_trust_all"
1371            );
1372        }
1373
1374        let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
1375            store.has_restricted_worktrees(&worktree_store, cx)
1376        });
1377        assert!(
1378            !has_restricted_after,
1379            "should have no restricted worktrees after auto_trust_all"
1380        );
1381
1382        let trusted_event_count = events
1383            .borrow()
1384            .iter()
1385            .filter(|e| matches!(e, TrustedWorktreesEvent::Trusted(..)))
1386            .count();
1387        assert!(
1388            trusted_event_count > 0,
1389            "should have emitted Trusted events"
1390        );
1391    }
1392
1393    #[gpui::test]
1394    async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
1395        init_test(cx);
1396
1397        let fs = FakeFs::new(cx.executor());
1398        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
1399            .await;
1400
1401        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
1402        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1403        let worktree_id = worktree_store.read_with(cx, |store, cx| {
1404            store.worktrees().next().unwrap().read(cx).id()
1405        });
1406
1407        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1408
1409        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1410        cx.update({
1411            let events = events.clone();
1412            |cx| {
1413                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1414                    events.borrow_mut().push(match event {
1415                        TrustedWorktreesEvent::Trusted(host, paths) => {
1416                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1417                        }
1418                        TrustedWorktreesEvent::Restricted(host, paths) => {
1419                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1420                        }
1421                    });
1422                })
1423            }
1424        })
1425        .detach();
1426
1427        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1428            store.can_trust(&worktree_store, worktree_id, cx)
1429        });
1430        assert!(!can_trust, "should be restricted initially");
1431        assert_eq!(events.borrow().len(), 1);
1432        events.borrow_mut().clear();
1433
1434        trusted_worktrees.update(cx, |store, cx| {
1435            store.trust(
1436                &worktree_store,
1437                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1438                cx,
1439            );
1440        });
1441        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1442            store.can_trust(&worktree_store, worktree_id, cx)
1443        });
1444        assert!(can_trust, "should be trusted after trust()");
1445        assert_eq!(events.borrow().len(), 1);
1446        assert!(matches!(
1447            &events.borrow()[0],
1448            TrustedWorktreesEvent::Trusted(..)
1449        ));
1450        events.borrow_mut().clear();
1451
1452        trusted_worktrees.update(cx, |store, cx| {
1453            store.restrict(
1454                worktree_store.downgrade(),
1455                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1456                cx,
1457            );
1458        });
1459        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1460            store.can_trust(&worktree_store, worktree_id, cx)
1461        });
1462        assert!(!can_trust, "should be restricted after restrict()");
1463        assert_eq!(events.borrow().len(), 1);
1464        assert!(matches!(
1465            &events.borrow()[0],
1466            TrustedWorktreesEvent::Restricted(..)
1467        ));
1468
1469        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1470            store.has_restricted_worktrees(&worktree_store, cx)
1471        });
1472        assert!(has_restricted);
1473        events.borrow_mut().clear();
1474
1475        trusted_worktrees.update(cx, |store, cx| {
1476            store.trust(
1477                &worktree_store,
1478                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
1479                cx,
1480            );
1481        });
1482        let can_trust = trusted_worktrees.update(cx, |store, cx| {
1483            store.can_trust(&worktree_store, worktree_id, cx)
1484        });
1485        assert!(can_trust, "should be trusted again after second trust()");
1486        assert_eq!(events.borrow().len(), 1);
1487        assert!(matches!(
1488            &events.borrow()[0],
1489            TrustedWorktreesEvent::Trusted(..)
1490        ));
1491
1492        let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
1493            store.has_restricted_worktrees(&worktree_store, cx)
1494        });
1495        assert!(!has_restricted);
1496    }
1497
1498    #[gpui::test]
1499    async fn test_multi_host_trust_isolation(cx: &mut TestAppContext) {
1500        init_test(cx);
1501
1502        let fs = FakeFs::new(cx.executor());
1503        fs.insert_tree(
1504            path!("/"),
1505            json!({
1506                "local_project": { "main.rs": "fn main() {}" },
1507                "remote_project": { "lib.rs": "pub fn lib() {}" }
1508            }),
1509        )
1510        .await;
1511
1512        let project = Project::test(
1513            fs,
1514            [
1515                path!("/local_project").as_ref(),
1516                path!("/remote_project").as_ref(),
1517            ],
1518            cx,
1519        )
1520        .await;
1521        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1522        let worktree_ids: Vec<_> = worktree_store.read_with(cx, |store, cx| {
1523            store
1524                .worktrees()
1525                .map(|worktree| worktree.read(cx).id())
1526                .collect()
1527        });
1528        assert_eq!(worktree_ids.len(), 2);
1529        let local_worktree = worktree_ids[0];
1530        let _remote_worktree = worktree_ids[1];
1531
1532        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1533
1534        let can_trust_local = trusted_worktrees.update(cx, |store, cx| {
1535            store.can_trust(&worktree_store, local_worktree, cx)
1536        });
1537        assert!(!can_trust_local, "local worktree restricted on host_a");
1538
1539        trusted_worktrees.update(cx, |store, cx| {
1540            store.trust(
1541                &worktree_store,
1542                HashSet::from_iter([PathTrust::Worktree(local_worktree)]),
1543                cx,
1544            );
1545        });
1546
1547        let can_trust_local_after = trusted_worktrees.update(cx, |store, cx| {
1548            store.can_trust(&worktree_store, local_worktree, cx)
1549        });
1550        assert!(
1551            can_trust_local_after,
1552            "local worktree should be trusted on local host"
1553        );
1554    }
1555
1556    #[gpui::test]
1557    async fn test_invisible_worktree_stores_do_not_affect_trust(cx: &mut TestAppContext) {
1558        init_test(cx);
1559
1560        let fs = FakeFs::new(cx.executor());
1561        fs.insert_tree(
1562            path!("/"),
1563            json!({
1564                "visible": { "main.rs": "fn main() {}" },
1565                "other": { "a.rs": "fn other() {}" },
1566                "invisible": { "b.rs": "fn invisible() {}" }
1567            }),
1568        )
1569        .await;
1570
1571        let project = Project::test(fs, [path!("/visible").as_ref()], cx).await;
1572        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1573        let visible_worktree_id = worktree_store.read_with(cx, |store, cx| {
1574            store
1575                .worktrees()
1576                .find(|worktree| worktree.read(cx).root_dir().unwrap().ends_with("visible"))
1577                .expect("visible worktree")
1578                .read(cx)
1579                .id()
1580        });
1581        let trusted_worktrees = init_trust_global(worktree_store.clone(), cx);
1582
1583        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
1584        cx.update({
1585            let events = events.clone();
1586            |cx| {
1587                cx.subscribe(&trusted_worktrees, move |_, event, _| {
1588                    events.borrow_mut().push(match event {
1589                        TrustedWorktreesEvent::Trusted(host, paths) => {
1590                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
1591                        }
1592                        TrustedWorktreesEvent::Restricted(host, paths) => {
1593                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
1594                        }
1595                    });
1596                })
1597            }
1598        })
1599        .detach();
1600
1601        assert!(
1602            !trusted_worktrees.update(cx, |store, cx| {
1603                store.can_trust(&worktree_store, visible_worktree_id, cx)
1604            }),
1605            "visible worktree should be restricted initially"
1606        );
1607        assert_eq!(
1608            HashSet::from_iter([(visible_worktree_id)]),
1609            trusted_worktrees.read_with(cx, |store, _| {
1610                store
1611                    .restricted
1612                    .get(&worktree_store.downgrade())
1613                    .unwrap()
1614                    .clone()
1615            }),
1616            "only visible worktree should be restricted",
1617        );
1618
1619        let (new_visible_worktree, new_invisible_worktree) =
1620            worktree_store.update(cx, |worktree_store, cx| {
1621                let new_visible_worktree = worktree_store.create_worktree("/other", true, cx);
1622                let new_invisible_worktree =
1623                    worktree_store.create_worktree("/invisible", false, cx);
1624                (new_visible_worktree, new_invisible_worktree)
1625            });
1626        let (new_visible_worktree, new_invisible_worktree) = (
1627            new_visible_worktree.await.unwrap(),
1628            new_invisible_worktree.await.unwrap(),
1629        );
1630
1631        let new_visible_worktree_id =
1632            new_visible_worktree.read_with(cx, |new_visible_worktree, _| new_visible_worktree.id());
1633        assert!(
1634            !trusted_worktrees.update(cx, |store, cx| {
1635                store.can_trust(&worktree_store, new_visible_worktree_id, cx)
1636            }),
1637            "new visible worktree should be restricted initially",
1638        );
1639        assert!(
1640            trusted_worktrees.update(cx, |store, cx| {
1641                store.can_trust(&worktree_store, new_invisible_worktree.read(cx).id(), cx)
1642            }),
1643            "invisible worktree should be skipped",
1644        );
1645        assert_eq!(
1646            HashSet::from_iter([visible_worktree_id, new_visible_worktree_id]),
1647            trusted_worktrees.read_with(cx, |store, _| {
1648                store
1649                    .restricted
1650                    .get(&worktree_store.downgrade())
1651                    .unwrap()
1652                    .clone()
1653            }),
1654            "only visible worktrees should be restricted"
1655        );
1656    }
1657}