|
| 1 | +//! Full Disk Access gate. |
| 2 | +//! |
| 3 | +//! At first launch on macOS, Cmdr shows an in-app modal that walks the user |
| 4 | +//! through granting Full Disk Access (FDA) before the indexer scans `/`. |
| 5 | +//! The same gate applies more broadly: any launch-time code that touches |
| 6 | +//! TCC-protected paths (Downloads, Documents, Desktop, ...) or NSWorkspace |
| 7 | +//! icon/LaunchServices APIs on those paths must skip work while the FDA |
| 8 | +//! decision is pending. Otherwise macOS stacks several native permission |
| 9 | +//! popups (MediaLibrary, AppData, Desktop, Documents, Downloads, ...) on |
| 10 | +//! top of our in-app modal — exactly the onboarding-flood UX we want to |
| 11 | +//! avoid. |
| 12 | +//! |
| 13 | +//! The gate has two pieces: |
| 14 | +//! |
| 15 | +//! 1. `is_fda_pending(fda_choice, os_fda_granted)` — pure decision used at |
| 16 | +//! startup and by tests. Pending iff the user hasn't decided AND the OS |
| 17 | +//! reports FDA isn't granted. |
| 18 | +//! 2. A process-global `AtomicBool` set once at startup (and cleared when |
| 19 | +//! the user denies FDA in-session). Read by code that runs after startup |
| 20 | +//! via `is_fda_pending_runtime()`. |
| 21 | +//! |
| 22 | +//! On non-macOS platforms FDA doesn't exist; the runtime gate is always |
| 23 | +//! `false` (open) so cross-platform callers get the right behaviour without |
| 24 | +//! cfg-guards at every site. |
| 25 | +
|
| 26 | +use std::sync::OnceLock; |
| 27 | +use std::sync::atomic::{AtomicBool, Ordering}; |
| 28 | + |
| 29 | +use crate::settings::FullDiskAccessChoice; |
| 30 | + |
| 31 | +static FDA_PENDING: OnceLock<AtomicBool> = OnceLock::new(); |
| 32 | + |
| 33 | +/// Pure decision: is the FDA decision still pending at this moment? |
| 34 | +/// |
| 35 | +/// Returns `true` only when the user hasn't decided AND the OS confirms FDA |
| 36 | +/// isn't currently granted. If the OS check returns `true` we know the |
| 37 | +/// per-folder TCC services are subsumed by FDA, so it's safe to access |
| 38 | +/// protected paths even if no in-app choice has been recorded yet. |
| 39 | +pub fn is_fda_pending(fda_choice: FullDiskAccessChoice, os_fda_granted: bool) -> bool { |
| 40 | + fda_choice == FullDiskAccessChoice::NotAskedYet && !os_fda_granted |
| 41 | +} |
| 42 | + |
| 43 | +/// Set the runtime gate. Call once at startup with the result of |
| 44 | +/// `is_fda_pending(...)`, and again with `false` after the user makes a |
| 45 | +/// choice in-session (deny path — the allow path requires a restart and |
| 46 | +/// re-enters startup). |
| 47 | +pub fn set_fda_pending(pending: bool) { |
| 48 | + FDA_PENDING |
| 49 | + .get_or_init(|| AtomicBool::new(pending)) |
| 50 | + .store(pending, Ordering::Release); |
| 51 | +} |
| 52 | + |
| 53 | +/// Read the runtime gate. Returns `false` until `set_fda_pending` has been |
| 54 | +/// called — safe default for tests and any non-macOS build that never sets |
| 55 | +/// it. |
| 56 | +pub fn is_fda_pending_runtime() -> bool { |
| 57 | + FDA_PENDING.get().is_some_and(|f| f.load(Ordering::Acquire)) |
| 58 | +} |
| 59 | + |
| 60 | +#[cfg(test)] |
| 61 | +mod tests { |
| 62 | + use super::*; |
| 63 | + |
| 64 | + #[test] |
| 65 | + fn pending_only_when_not_asked_and_os_denies() { |
| 66 | + assert!(is_fda_pending(FullDiskAccessChoice::NotAskedYet, false)); |
| 67 | + assert!(!is_fda_pending(FullDiskAccessChoice::NotAskedYet, true)); |
| 68 | + assert!(!is_fda_pending(FullDiskAccessChoice::Allow, false)); |
| 69 | + assert!(!is_fda_pending(FullDiskAccessChoice::Allow, true)); |
| 70 | + assert!(!is_fda_pending(FullDiskAccessChoice::Deny, false)); |
| 71 | + assert!(!is_fda_pending(FullDiskAccessChoice::Deny, true)); |
| 72 | + } |
| 73 | +} |
0 commit comments