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