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        };
 166        Self {
 167            user_name,
 168            host_identifier: host_name,
 169        }
 170    }
 171}
 172
 173/// A unit of trust consideration inside a particular host:
 174/// either a familiar worktree, or a path that may influence other worktrees' trust.
 175/// See module-level documentation on the trust model.
 176#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 177pub enum PathTrust {
 178    /// A worktree that is familiar to this workspace.
 179    /// Either a single file or a directory worktree.
 180    Worktree(WorktreeId),
 181    /// A path that may be another worktree yet not loaded into any workspace (hence, without any `WorktreeId`),
 182    /// or a parent path coming out of the security modal.
 183    AbsPath(PathBuf),
 184}
 185
 186impl PathTrust {
 187    fn to_proto(&self) -> proto::PathTrust {
 188        match self {
 189            Self::Worktree(worktree_id) => proto::PathTrust {
 190                content: Some(proto::path_trust::Content::WorktreeId(
 191                    worktree_id.to_proto(),
 192                )),
 193            },
 194            Self::AbsPath(path_buf) => proto::PathTrust {
 195                content: Some(proto::path_trust::Content::AbsPath(
 196                    path_buf.to_string_lossy().to_string(),
 197                )),
 198            },
 199        }
 200    }
 201
 202    pub fn from_proto(proto: proto::PathTrust) -> Option<Self> {
 203        Some(match proto.content? {
 204            proto::path_trust::Content::WorktreeId(id) => {
 205                Self::Worktree(WorktreeId::from_proto(id))
 206            }
 207            proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
 208        })
 209    }
 210}
 211
 212/// A change of trust on a certain host.
 213#[derive(Debug)]
 214pub enum TrustedWorktreesEvent {
 215    Trusted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
 216    Restricted(WeakEntity<WorktreeStore>, HashSet<PathTrust>),
 217}
 218
 219impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 220
 221type TrustedPaths = HashMap<WeakEntity<WorktreeStore>, HashSet<PathTrust>>;
 222pub type DbTrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>;
 223
 224impl TrustedWorktreesStore {
 225    fn new(
 226        db_trusted_paths: DbTrustedPaths,
 227        downstream_client: Option<(AnyProtoClient, ProjectId)>,
 228        upstream_client: Option<(AnyProtoClient, ProjectId)>,
 229    ) -> Self {
 230        if let Some((upstream_client, upstream_project_id)) = &upstream_client {
 231            let trusted_paths = db_trusted_paths
 232                .iter()
 233                .flat_map(|(_, paths)| {
 234                    paths
 235                        .iter()
 236                        .cloned()
 237                        .map(PathTrust::AbsPath)
 238                        .map(|trusted_path| trusted_path.to_proto())
 239                })
 240                .collect::<Vec<_>>();
 241            if !trusted_paths.is_empty() {
 242                upstream_client
 243                    .send(proto::TrustWorktrees {
 244                        project_id: upstream_project_id.0,
 245                        trusted_paths,
 246                    })
 247                    .ok();
 248            }
 249        }
 250
 251        Self {
 252            db_trusted_paths,
 253            downstream_clients: downstream_client.into_iter().collect(),
 254            upstream_clients: upstream_client.into_iter().collect(),
 255            trusted_paths: HashMap::default(),
 256            worktree_stores: HashMap::default(),
 257            restricted: HashMap::default(),
 258            worktree_trust_serialization: Task::ready(()),
 259        }
 260    }
 261
 262    /// Whether a particular worktree store has associated worktrees that are restricted, or an associated host is restricted.
 263    pub fn has_restricted_worktrees(
 264        &self,
 265        worktree_store: &Entity<WorktreeStore>,
 266        cx: &App,
 267    ) -> bool {
 268        self.restricted
 269            .get(&worktree_store.downgrade())
 270            .is_some_and(|restricted_worktrees| {
 271                restricted_worktrees.iter().any(|restricted_worktree| {
 272                    worktree_store
 273                        .read(cx)
 274                        .worktree_for_id(*restricted_worktree, cx)
 275                        .is_some()
 276                })
 277            })
 278    }
 279
 280    /// Adds certain entities on this host to the trusted list.
 281    /// This will emit [`TrustedWorktreesEvent::Trusted`] event for all passed entries
 282    /// and the ones that got auto trusted based on trust hierarchy (see module-level docs).
 283    pub fn trust(
 284        &mut self,
 285        worktree_store: &Entity<WorktreeStore>,
 286        mut trusted_paths: HashSet<PathTrust>,
 287        cx: &mut Context<Self>,
 288    ) {
 289        let weak_worktree_store = worktree_store.downgrade();
 290        let mut new_trusted_single_file_worktrees = HashSet::default();
 291        let mut new_trusted_other_worktrees = HashSet::default();
 292        let mut new_trusted_abs_paths = HashSet::default();
 293        for trusted_path in trusted_paths.iter().chain(
 294            self.trusted_paths
 295                .remove(&weak_worktree_store)
 296                .iter()
 297                .flat_map(|current_trusted| current_trusted.iter()),
 298        ) {
 299            match trusted_path {
 300                PathTrust::Worktree(worktree_id) => {
 301                    if let Some(restricted_worktrees) =
 302                        self.restricted.get_mut(&weak_worktree_store)
 303                    {
 304                        restricted_worktrees.remove(worktree_id);
 305                    };
 306
 307                    if let Some(worktree) =
 308                        worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
 309                    {
 310                        if worktree.read(cx).is_single_file() {
 311                            new_trusted_single_file_worktrees.insert(*worktree_id);
 312                        } else {
 313                            new_trusted_other_worktrees
 314                                .insert((worktree.read(cx).abs_path(), *worktree_id));
 315                        }
 316                    }
 317                }
 318                PathTrust::AbsPath(abs_path) => {
 319                    debug_assert!(
 320                        abs_path.is_absolute(),
 321                        "Cannot trust non-absolute path {abs_path:?}"
 322                    );
 323                    if let Some((worktree_id, is_file)) =
 324                        find_worktree_in_store(worktree_store.read(cx), abs_path, cx)
 325                    {
 326                        if is_file {
 327                            new_trusted_single_file_worktrees.insert(worktree_id);
 328                        } else {
 329                            new_trusted_other_worktrees
 330                                .insert((Arc::from(abs_path.as_path()), worktree_id));
 331                        }
 332                    }
 333                    new_trusted_abs_paths.insert(abs_path.clone());
 334                }
 335            }
 336        }
 337
 338        new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
 339            new_trusted_abs_paths
 340                .iter()
 341                .all(|new_trusted_path| !worktree_abs_path.starts_with(new_trusted_path))
 342        });
 343        if !new_trusted_other_worktrees.is_empty() {
 344            new_trusted_single_file_worktrees.clear();
 345        }
 346
 347        if let Some(restricted_worktrees) = self.restricted.remove(&weak_worktree_store) {
 348            let new_restricted_worktrees = restricted_worktrees
 349                .into_iter()
 350                .filter(|restricted_worktree| {
 351                    let Some(worktree) = worktree_store
 352                        .read(cx)
 353                        .worktree_for_id(*restricted_worktree, cx)
 354                    else {
 355                        return false;
 356                    };
 357                    let is_file = worktree.read(cx).is_single_file();
 358
 359                    // When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.
 360                    if is_file && !new_trusted_abs_paths.is_empty() {
 361                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 362                        return false;
 363                    }
 364
 365                    let restricted_worktree_path = worktree.read(cx).abs_path();
 366                    let retain = (!is_file || new_trusted_other_worktrees.is_empty())
 367                        && new_trusted_abs_paths.iter().all(|new_trusted_path| {
 368                            !restricted_worktree_path.starts_with(new_trusted_path)
 369                        });
 370                    if !retain {
 371                        trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
 372                    }
 373                    retain
 374                })
 375                .collect();
 376            self.restricted
 377                .insert(weak_worktree_store.clone(), new_restricted_worktrees);
 378        }
 379
 380        {
 381            let trusted_paths = self
 382                .trusted_paths
 383                .entry(weak_worktree_store.clone())
 384                .or_default();
 385            trusted_paths.extend(new_trusted_abs_paths.into_iter().map(PathTrust::AbsPath));
 386            trusted_paths.extend(
 387                new_trusted_other_worktrees
 388                    .into_iter()
 389                    .map(|(_, worktree_id)| PathTrust::Worktree(worktree_id)),
 390            );
 391            trusted_paths.extend(
 392                new_trusted_single_file_worktrees
 393                    .into_iter()
 394                    .map(PathTrust::Worktree),
 395            );
 396        }
 397
 398        cx.emit(TrustedWorktreesEvent::Trusted(
 399            weak_worktree_store,
 400            trusted_paths.clone(),
 401        ));
 402
 403        for (upstream_client, upstream_project_id) in &self.upstream_clients {
 404            let trusted_paths = trusted_paths
 405                .iter()
 406                .map(|trusted_path| trusted_path.to_proto())
 407                .collect::<Vec<_>>();
 408            if !trusted_paths.is_empty() {
 409                upstream_client
 410                    .send(proto::TrustWorktrees {
 411                        project_id: upstream_project_id.0,
 412                        trusted_paths,
 413                    })
 414                    .ok();
 415            }
 416        }
 417    }
 418
 419    /// Restricts certain entities on this host.
 420    /// This will emit [`TrustedWorktreesEvent::Restricted`] event for all passed entries.
 421    pub fn restrict(
 422        &mut self,
 423        worktree_store: WeakEntity<WorktreeStore>,
 424        restricted_paths: HashSet<PathTrust>,
 425        cx: &mut Context<Self>,
 426    ) {
 427        let mut restricted = HashSet::default();
 428        for restricted_path in restricted_paths {
 429            match restricted_path {
 430                PathTrust::Worktree(worktree_id) => {
 431                    self.restricted
 432                        .entry(worktree_store.clone())
 433                        .or_default()
 434                        .insert(worktree_id);
 435                    restricted.insert(PathTrust::Worktree(worktree_id));
 436                }
 437                PathTrust::AbsPath(..) => debug_panic!("Unexpected: cannot restrict an abs path"),
 438            }
 439        }
 440
 441        cx.emit(TrustedWorktreesEvent::Restricted(
 442            worktree_store,
 443            restricted,
 444        ));
 445    }
 446
 447    /// Erases all trust information.
 448    /// Requires Zed's restart to take proper effect.
 449    pub fn clear_trusted_paths(&mut self) {
 450        self.trusted_paths.clear();
 451        self.db_trusted_paths.clear();
 452    }
 453
 454    /// Checks whether a certain worktree is trusted (or on a larger trust level).
 455    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if for the first time and not trusted, or no corresponding worktree store was found.
 456    ///
 457    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
 458    pub fn can_trust(
 459        &mut self,
 460        worktree_store: &Entity<WorktreeStore>,
 461        worktree_id: WorktreeId,
 462        cx: &mut Context<Self>,
 463    ) -> bool {
 464        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 465            return true;
 466        }
 467
 468        let weak_worktree_store = worktree_store.downgrade();
 469        let Some(worktree) = worktree_store.read(cx).worktree_for_id(worktree_id, cx) else {
 470            return false;
 471        };
 472        let worktree_path = worktree.read(cx).abs_path();
 473        // Zed opened an "internal" directory: e.g. a tmp dir for `keymap_editor.rs` needs.
 474        if !worktree.read(cx).is_visible() {
 475            log::debug!("Skipping worktree trust checks for not visible {worktree_path:?}");
 476            return true;
 477        }
 478
 479        let is_file = worktree.read(cx).is_single_file();
 480        if self
 481            .restricted
 482            .get(&weak_worktree_store)
 483            .is_some_and(|restricted_worktrees| restricted_worktrees.contains(&worktree_id))
 484        {
 485            return false;
 486        }
 487
 488        if self
 489            .trusted_paths
 490            .get(&weak_worktree_store)
 491            .is_some_and(|trusted_paths| trusted_paths.contains(&PathTrust::Worktree(worktree_id)))
 492        {
 493            return true;
 494        }
 495
 496        // * Single files are auto-approved when something else (not a single file) was approved on this host already.
 497        // * If parent path is trusted already, this worktree is stusted also.
 498        //
 499        // See module documentation for details on trust level.
 500        if let Some(trusted_paths) = self.trusted_paths.get(&weak_worktree_store) {
 501            let auto_trusted = worktree_store.read_with(cx, |worktree_store, cx| {
 502                trusted_paths.iter().any(|trusted_path| match trusted_path {
 503                    PathTrust::Worktree(worktree_id) => worktree_store
 504                        .worktree_for_id(*worktree_id, cx)
 505                        .is_some_and(|worktree| {
 506                            let worktree = worktree.read(cx);
 507                            worktree_path.starts_with(&worktree.abs_path())
 508                                || (is_file && !worktree.is_single_file())
 509                        }),
 510                    PathTrust::AbsPath(trusted_path) => {
 511                        is_file || worktree_path.starts_with(trusted_path)
 512                    }
 513                })
 514            });
 515            if auto_trusted {
 516                return true;
 517            }
 518        }
 519
 520        self.restricted
 521            .entry(weak_worktree_store.clone())
 522            .or_default()
 523            .insert(worktree_id);
 524        log::info!("Worktree {worktree_path:?} is not trusted");
 525        cx.emit(TrustedWorktreesEvent::Restricted(
 526            weak_worktree_store,
 527            HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
 528        ));
 529        for (downstream_client, downstream_project_id) in &self.downstream_clients {
 530            downstream_client
 531                .send(proto::RestrictWorktrees {
 532                    project_id: downstream_project_id.0,
 533                    worktree_ids: vec![worktree_id.to_proto()],
 534                })
 535                .ok();
 536        }
 537        for (upstream_client, upstream_project_id) in &self.upstream_clients {
 538            upstream_client
 539                .send(proto::RestrictWorktrees {
 540                    project_id: upstream_project_id.0,
 541                    worktree_ids: vec![worktree_id.to_proto()],
 542                })
 543                .ok();
 544        }
 545        false
 546    }
 547
 548    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
 549    pub fn restricted_worktrees(
 550        &self,
 551        worktree_store: &Entity<WorktreeStore>,
 552        cx: &App,
 553    ) -> HashSet<(WorktreeId, Arc<Path>)> {
 554        let mut single_file_paths = HashSet::default();
 555
 556        let other_paths = self
 557            .restricted
 558            .get(&worktree_store.downgrade())
 559            .into_iter()
 560            .flatten()
 561            .filter_map(|&restricted_worktree_id| {
 562                let worktree = worktree_store
 563                    .read(cx)
 564                    .worktree_for_id(restricted_worktree_id, cx)?;
 565                let worktree = worktree.read(cx);
 566                let abs_path = worktree.abs_path();
 567                if worktree.is_single_file() {
 568                    single_file_paths.insert((restricted_worktree_id, abs_path));
 569                    None
 570                } else {
 571                    Some((restricted_worktree_id, abs_path))
 572                }
 573            })
 574            .collect::<HashSet<_>>();
 575
 576        if !other_paths.is_empty() {
 577            return other_paths;
 578        } else {
 579            single_file_paths
 580        }
 581    }
 582
 583    /// Switches the "trust nothing" mode to "automatically trust everything".
 584    /// This does not influence already persisted data, but stops adding new worktrees there.
 585    pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
 586        for (worktree_store, worktrees) in std::mem::take(&mut self.restricted).into_iter().fold(
 587            HashMap::default(),
 588            |mut acc, (remote_host, worktrees)| {
 589                acc.entry(remote_host)
 590                    .or_insert_with(HashSet::default)
 591                    .extend(worktrees.into_iter().map(PathTrust::Worktree));
 592                acc
 593            },
 594        ) {
 595            if let Some(worktree_store) = worktree_store.upgrade() {
 596                self.trust(&worktree_store, worktrees, cx);
 597            }
 598        }
 599    }
 600
 601    pub fn schedule_serialization<S>(&mut self, cx: &mut Context<Self>, serialize: S)
 602    where
 603        S: FnOnce(HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>, &App) -> Task<()>
 604            + 'static,
 605    {
 606        self.worktree_trust_serialization = serialize(self.trusted_paths_for_serialization(cx), cx);
 607    }
 608
 609    fn trusted_paths_for_serialization(
 610        &mut self,
 611        cx: &mut Context<Self>,
 612    ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
 613        self.trusted_paths
 614            .iter()
 615            .filter_map(|(worktree_store, paths)| {
 616                let host = self.worktree_stores.get(&worktree_store)?.clone();
 617                let abs_paths = paths
 618                    .iter()
 619                    .flat_map(|path| match path {
 620                        PathTrust::Worktree(worktree_id) => worktree_store
 621                            .upgrade()
 622                            .and_then(|worktree_store| {
 623                                worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
 624                            })
 625                            .map(|worktree| worktree.read(cx).abs_path().to_path_buf()),
 626                        PathTrust::AbsPath(abs_path) => Some(abs_path.clone()),
 627                    })
 628                    .collect::<HashSet<_>>();
 629                Some((host, abs_paths))
 630            })
 631            .chain(self.db_trusted_paths.clone())
 632            .fold(HashMap::default(), |mut acc, (host, paths)| {
 633                acc.entry(host)
 634                    .or_insert_with(HashSet::default)
 635                    .extend(paths);
 636                acc
 637            })
 638    }
 639
 640    fn add_worktree_store(
 641        &mut self,
 642        worktree_store: Entity<WorktreeStore>,
 643        remote_host: Option<RemoteHostLocation>,
 644        cx: &mut Context<Self>,
 645    ) {
 646        let weak_worktree_store = worktree_store.downgrade();
 647        self.worktree_stores
 648            .insert(weak_worktree_store.clone(), remote_host.clone());
 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(), None, None, 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}