Skip to main content

rightkit_daemon/
mode.rs

1//! The one runtime decision between a release run and a test run (M21), and the only sanctioned
2//! reader of a test or override selector (M23).
3//!
4//! Design rule: the strongest guarantee is that a release binary contains **no code that reads a
5//! test or override selector**. So there are two builds of this module:
6//!
7//! * **Without the `test-mode` cargo feature** (the default, and the only thing a release build may
8//!   use): [`decide`] returns [`Mode::Release`] and performs no environment or argument read at all.
9//!   `test_selector!` expands to `None` and drops its name literal. The binary carries no selector
10//!   name and no `TEST_MODE_MARKER`. [`roots`] resolves from the operating system's account
11//!   database ([`Suite::per_user_default`]), never from the environment.
12//! * **With `test-mode`** (QA and dev builds only): [`decide`] reads the activation selector. A run
13//!   is a test run only when the activation selector AND a root that passes every check below are
14//!   present; anything partial is a [`Refusal`] before any store, log, credential or service opens.
15//!   The root must be absolute, an existing directory, not itself a symlink, disjoint from the real
16//!   per-user suite root and the user's Documents/Pictures-style folders (equal, ancestor,
17//!   descendant, or reached through a symlinked ancestor), outside cloud-sync folders, private to the
18//!   current user on Unix, and carry the binding file (`.rightkit-app.json`, `{"app": "<id>"}`)
19//!   that `rightkit-qa` writes when it creates an isolated home, naming this app.
20//!
21//! `@rightkit/release` (`release-switch-scan.mjs`) refuses a release build that enables
22//! `test-mode`, and refuses a native binary that contains `TEST_MODE_MARKER` text or any selector
23//! name. Selector names therefore live only behind the feature, and no message in this module that is
24//! compiled into a release build names one.
25//!
26//! The decision is deliberately injectable ([`ModeInput`]) so tests can prove the "reads nothing"
27//! and "refuses" properties without touching the real process environment.
28
29use std::fmt;
30use std::io;
31use std::path::{Path, PathBuf};
32use std::sync::OnceLock;
33
34use crate::suite::{Suite, SuiteError, SUITE_DIR_NAME};
35
36/// Whether this build compiled the test capability (cargo feature `test-mode`).
37pub const TEST_CAPABILITY: bool = cfg!(feature = "test-mode");
38
39/// Text of the marker a test-capable binary carries. Keep identical to `TEST_MODE_MARKER` in
40/// `packages/release/release-switch-scan.mjs`.
41#[cfg(feature = "test-mode")]
42pub const TEST_MODE_MARKER_TEXT: &str = "RIGHTKIT-TEST-MODE-COMPILED";
43
44/// Present in the binary only when `test-mode` is compiled; the release scan refuses any binary
45/// that contains it. Secondary evidence: the authoritative gate is the resolved feature closure
46/// (`cargo tree -e features`) checked by `@rightkit/release`. The bytes are inline in a named
47/// section, exported, and `#[used]`, so a linker's section garbage collection keeps them.
48#[cfg(feature = "test-mode")]
49#[used]
50#[no_mangle]
51#[cfg_attr(target_os = "macos", link_section = "__DATA,__rkit_tm")]
52#[cfg_attr(windows, link_section = ".rkittm")]
53#[cfg_attr(not(any(target_os = "macos", windows)), link_section = ".rkit_tm")]
54pub static TEST_MODE_MARKER: [u8; 27] = *b"RIGHTKIT-TEST-MODE-COMPILED";
55
56/// Activation selector (class S1). Test-capable builds only.
57#[cfg(feature = "test-mode")]
58const SELECTOR_ACTIVATION: &str = "RIGHTKIT_UNDER_TEST";
59/// Test root selector (class S4). Test-capable builds only.
60#[cfg(feature = "test-mode")]
61const SELECTOR_ROOT: &str = "RIGHTKIT_QA_DATA_DIR";
62/// Binding file `rightkit-qa` writes inside every home it creates.
63#[cfg(feature = "test-mode")]
64const BINDING_FILE: &str = ".rightkit-app.json";
65
66/// The app asking for a mode. `id` is the binding key (`"viewright"`); `dir_name` is the folder
67/// under `Orthic Labs` that holds the app's own data (`"ViewRight"`).
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct AppId {
70    id: &'static str,
71    dir_name: &'static str,
72}
73
74impl AppId {
75    /// An app whose data folder carries the same name as its id.
76    pub const fn new(id: &'static str) -> AppId {
77        AppId { id, dir_name: id }
78    }
79
80    /// An app whose data folder differs from its id.
81    pub const fn with_dir(id: &'static str, dir_name: &'static str) -> AppId {
82        AppId { id, dir_name }
83    }
84
85    pub const fn id(&self) -> &'static str {
86        self.id
87    }
88
89    pub const fn dir_name(&self) -> &'static str {
90        self.dir_name
91    }
92}
93
94/// Why a test run was refused. Each variant names its rule through [`Refusal::rule`]. Messages
95/// never name an environment variable: this enum is compiled into release builds too.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum Refusal {
98    /// The activation selector is set but no root is given.
99    ActivationWithoutRoot,
100    /// A root is given but the activation selector is not set.
101    RootWithoutActivation,
102    RootNotAbsolute,
103    /// Missing, or not a directory.
104    RootNotDirectory,
105    /// The root itself is a symlink.
106    RootHasSymlink,
107    /// Equal to, an ancestor of, a descendant of, or resolving into a real user location.
108    RootOverlapsUserData,
109    /// Inside a cloud-synchronised folder (name heuristic, see [`Refusal::rule`]).
110    RootOnRemovableOrCloud,
111    /// Not private to the current user (Unix: not owned by the user, or group/other bits set).
112    RootNotPrivate,
113    /// No readable binding file: not a home a runner created.
114    RootNotCreatedByRunner,
115    /// The binding names a different app.
116    BindingAppMismatch,
117    /// The real per-user root could not be determined, so disjointness cannot be proven.
118    UserRootUnknown,
119    /// A filesystem error while verifying or resetting.
120    Io(String),
121}
122
123impl Refusal {
124    /// A stable kebab-case rule id (the vocabulary `rightkit-qa`'s guard shares).
125    pub fn rule(&self) -> &'static str {
126        match self {
127            Refusal::ActivationWithoutRoot => "activation-without-root",
128            Refusal::RootWithoutActivation => "root-without-activation",
129            Refusal::RootNotAbsolute => "root-not-absolute",
130            Refusal::RootNotDirectory => "root-not-directory",
131            Refusal::RootHasSymlink => "root-has-symlink",
132            Refusal::RootOverlapsUserData => "root-overlaps-user-data",
133            Refusal::RootOnRemovableOrCloud => "root-on-removable-or-cloud",
134            Refusal::RootNotPrivate => "root-not-private",
135            Refusal::RootNotCreatedByRunner => "root-not-created-by-runner",
136            Refusal::BindingAppMismatch => "binding-app-mismatch",
137            Refusal::UserRootUnknown => "user-root-unknown",
138            Refusal::Io(_) => "io",
139        }
140    }
141}
142
143impl fmt::Display for Refusal {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Refusal::Io(e) => write!(f, "test mode refused ({}): {e}", self.rule()),
147            _ => write!(f, "test mode refused ({})", self.rule()),
148        }
149    }
150}
151
152impl std::error::Error for Refusal {}
153
154impl From<io::Error> for Refusal {
155    fn from(e: io::Error) -> Self {
156        Refusal::Io(e.to_string())
157    }
158}
159
160/// A verified test root: canonical, symlink-free at the leaf, disjoint from real user data,
161/// private, and bound to one app by the runner. Constructible only by [`decide`] in a build that
162/// compiled `test-mode`.
163#[derive(Debug, Clone)]
164pub struct TestRoot {
165    path: PathBuf,
166    suite: Suite,
167    #[cfg_attr(not(feature = "test-mode"), allow(dead_code))]
168    app_id: &'static str,
169}
170
171impl TestRoot {
172    /// The canonical root directory.
173    pub fn path(&self) -> &Path {
174        &self.path
175    }
176
177    /// The suite root for this run: `<root>/shared`.
178    pub fn suite(&self) -> Suite {
179        self.suite.clone()
180    }
181
182    /// A unit-test stand-in for what [`decide`] builds after validation.
183    #[cfg(test)]
184    pub(crate) fn for_test(path: PathBuf, suite: Suite) -> TestRoot {
185        TestRoot {
186            path,
187            suite,
188            app_id: "test",
189        }
190    }
191
192    /// The app's own data directory inside the root: `<root>/<dir_name>`.
193    pub fn app_dir(&self, app: &AppId) -> PathBuf {
194        self.path.join(app.dir_name)
195    }
196}
197
198/// How a run was decided. `Release` is the only value a build without `test-mode` can produce.
199#[derive(Debug, Clone)]
200pub enum Mode {
201    Release,
202    Test(TestRoot),
203}
204
205impl Mode {
206    pub fn is_test(&self) -> bool {
207        matches!(self, Mode::Test(_))
208    }
209
210    /// The test root, when this is a test run.
211    pub fn test_root(&self) -> Option<&TestRoot> {
212        match self {
213            Mode::Test(root) => Some(root),
214            Mode::Release => None,
215        }
216    }
217
218    /// Read an override selector. Only [`test_selector!`] calls this, and only in a build that
219    /// compiled `test-mode`; always `None` for [`Mode::Release`].
220    #[cfg(feature = "test-mode")]
221    #[doc(hidden)]
222    pub fn selector_env(&self, name: &str) -> Option<String> {
223        match self {
224            Mode::Test(_) => std::env::var(name).ok().filter(|v| !v.trim().is_empty()),
225            Mode::Release => None,
226        }
227    }
228}
229
230/// The only sanctioned reader of an override selector: `test_selector!(mode, "NAME")` returns the
231/// variable's value for a test run and `None` otherwise. In a build without `test-mode` it expands
232/// to `None` and the name literal does not appear in the binary.
233#[cfg(feature = "test-mode")]
234#[macro_export]
235macro_rules! test_selector {
236    ($mode:expr, $name:literal) => {
237        $crate::mode::Mode::selector_env(&$mode, $name)
238    };
239}
240
241/// See the `test-mode` form: this one drops the name.
242#[cfg(not(feature = "test-mode"))]
243#[macro_export]
244macro_rules! test_selector {
245    ($mode:expr, $name:literal) => {{
246        let _ = &$mode;
247        ::core::option::Option::<::std::string::String>::None
248    }};
249}
250
251/// Where [`decide`] reads from. The default implementation of the provided methods is the real
252/// operating system. A test supplies a fake, including one that panics on any read.
253pub trait ModeInput {
254    /// An environment variable. Called only by a build that compiled `test-mode`.
255    fn env(&self, name: &str) -> Option<String>;
256
257    /// The real per-user suite root, from the account database.
258    fn per_user_default(&self) -> Result<Suite, SuiteError> {
259        Suite::per_user_default()
260    }
261
262    /// Locations a test root may never overlap, given the real suite root
263    /// (`<base>/Orthic Labs/shared`): the whole `Orthic Labs` folder and the user's content
264    /// folders under the home that `<base>` belongs to.
265    fn protected_paths(&self, real: &Suite) -> Vec<PathBuf> {
266        default_protected_paths(real)
267    }
268}
269
270/// The real process environment.
271#[derive(Debug, Clone, Copy, Default)]
272pub struct ProcessInput;
273
274impl ModeInput for ProcessInput {
275    fn env(&self, name: &str) -> Option<String> {
276        std::env::var(name).ok()
277    }
278}
279
280/// Protected paths derived from the real suite root: the whole `Orthic Labs` folder and the
281/// user's content folders under the home.
282///
283/// Fails closed with an empty list (which [`decide`] turns into [`Refusal::UserRootUnknown`])
284/// when, on Windows in a `test-mode` build, the real root lies under a `USERPROFILE` or
285/// `LOCALAPPDATA` that differs from the account's own value: such a root was shaped by the
286/// environment (a QA home), and the folders derived from it would protect the wrong place.
287pub fn default_protected_paths(real: &Suite) -> Vec<PathBuf> {
288    let mut out = Vec::new();
289    if real_root_is_env_shaped(real.root()) {
290        return out;
291    }
292    let Some(orthic) = real.root().parent() else {
293        return out;
294    };
295    out.push(orthic.to_path_buf());
296    // Only trust the shape when the parent is `Orthic Labs`: `<home>` is then three ancestors up
297    // (`<home>/Library/Application Support`, `<home>/.local/share`, `<home>\AppData\Local`).
298    if orthic.file_name().and_then(|n| n.to_str()) == Some(SUITE_DIR_NAME) {
299        if let Some(home) = orthic.ancestors().nth(3) {
300            for dir in user_content_dirs() {
301                out.push(home.join(dir));
302            }
303        }
304    }
305    out
306}
307
308/// Windows, `test-mode` build: whether `real` lies under an environment profile value that
309/// differs from the account's. An unreadable account profile counts as env-shaped. Elsewhere
310/// (and in a build without `test-mode`, which reads no environment) always `false`.
311fn real_root_is_env_shaped(real: &Path) -> bool {
312    #[cfg(all(windows, feature = "test-mode"))]
313    {
314        let Ok(profile) = crate::suite::account_profile_dir() else {
315            return true;
316        };
317        let env = |key: &str| {
318            std::env::var_os(key)
319                .filter(|v| !v.is_empty())
320                .map(PathBuf::from)
321        };
322        let local = profile.join("AppData").join("Local");
323        root_under_differing_env(
324            real,
325            &[(env("USERPROFILE"), profile), (env("LOCALAPPDATA"), local)],
326        )
327    }
328    #[cfg(not(all(windows, feature = "test-mode")))]
329    {
330        let _ = real;
331        false
332    }
333}
334
335/// True when `root` lies under (or equals) an `env` value of a pair whose `env` differs from its
336/// `account` value. Paths compare by whole components, ignoring a verbatim prefix and, on
337/// Windows, case and separator style.
338#[cfg_attr(not(all(windows, feature = "test-mode")), allow(dead_code))]
339pub(crate) fn root_under_differing_env(root: &Path, pairs: &[(Option<PathBuf>, PathBuf)]) -> bool {
340    fn norm(p: &Path) -> Vec<String> {
341        let text = p.to_string_lossy();
342        let text = text.strip_prefix(r"\\?\").unwrap_or(&text);
343        text.split(['/', '\\'])
344            .filter(|c| !c.is_empty())
345            .map(|c| {
346                if cfg!(windows) {
347                    c.to_ascii_lowercase()
348                } else {
349                    c.to_string()
350                }
351            })
352            .collect()
353    }
354    let root = norm(root);
355    pairs.iter().any(|(env, account)| {
356        env.as_deref().is_some_and(|env| {
357            let env = norm(env);
358            !env.is_empty() && env != norm(account) && root.starts_with(&env)
359        })
360    })
361}
362
363fn user_content_dirs() -> [&'static str; 7] {
364    [
365        "Documents",
366        "Pictures",
367        "Desktop",
368        "Downloads",
369        "Movies",
370        "Music",
371        "Videos",
372    ]
373}
374
375/// Resolved roots a store must come through.
376#[derive(Debug, Clone)]
377pub struct AppRoots {
378    /// The suite root (`<Orthic Labs>/shared` in release, `<test root>/shared` in a test run).
379    pub suite: Suite,
380    /// The app's own data directory.
381    pub app_dir: PathBuf,
382}
383
384/// Decide once, before any store, log, credential or service starts.
385///
386/// Without `test-mode` this returns [`Mode::Release`] and reads nothing from `input`.
387pub fn decide(app: &AppId, input: &dyn ModeInput) -> Result<Mode, Refusal> {
388    #[cfg(feature = "test-mode")]
389    {
390        capable::decide(app, input)
391    }
392    #[cfg(not(feature = "test-mode"))]
393    {
394        let _ = (app, input);
395        Ok(Mode::Release)
396    }
397}
398
399/// [`decide`] against the real process, once per process. Prints one line to stderr and exits with
400/// status 2 on a [`Refusal`]; hosts call it first in `main`.
401pub fn init_or_exit(app: &AppId) -> &'static Mode {
402    static MODE: OnceLock<Mode> = OnceLock::new();
403    if let Some(mode) = MODE.get() {
404        return mode;
405    }
406    match decide(app, &ProcessInput) {
407        Ok(mode) => MODE.get_or_init(|| mode),
408        Err(refusal) => {
409            eprintln!("{refusal}");
410            std::process::exit(2);
411        }
412    }
413}
414
415/// The roots every store must come through, against the real process.
416pub fn roots(app: &AppId, mode: &Mode) -> Result<AppRoots, SuiteError> {
417    roots_with(app, mode, &ProcessInput)
418}
419
420/// [`roots`] with an injectable source for the real per-user root.
421pub fn roots_with(app: &AppId, mode: &Mode, input: &dyn ModeInput) -> Result<AppRoots, SuiteError> {
422    match mode {
423        Mode::Release => {
424            let suite = input.per_user_default()?;
425            let orthic = suite
426                .root()
427                .parent()
428                .ok_or_else(|| SuiteError::NoRoot("the per-user suite root has no parent".into()))?
429                .to_path_buf();
430            Ok(AppRoots {
431                suite,
432                app_dir: orthic.join(app.dir_name),
433            })
434        }
435        Mode::Test(root) => Ok(AppRoots {
436            suite: root.suite(),
437            app_dir: root.app_dir(app),
438        }),
439    }
440}
441
442/// The test-capable half. Compiled only with `test-mode`.
443#[cfg(feature = "test-mode")]
444mod capable {
445    use super::*;
446    use std::fs;
447
448    fn truthy(v: &str) -> bool {
449        let t = v.trim();
450        !(t.is_empty() || t == "0" || t.eq_ignore_ascii_case("false"))
451    }
452
453    pub(super) fn decide(app: &AppId, input: &dyn ModeInput) -> Result<Mode, Refusal> {
454        let activated = input
455            .env(SELECTOR_ACTIVATION)
456            .filter(|v| truthy(v))
457            .is_some();
458        let root = input.env(SELECTOR_ROOT).filter(|v| !v.trim().is_empty());
459        match (activated, root) {
460            (false, None) => Ok(Mode::Release),
461            (false, Some(_)) => Err(Refusal::RootWithoutActivation),
462            (true, None) => Err(Refusal::ActivationWithoutRoot),
463            (true, Some(root)) => verify(app, input, &root).map(Mode::Test),
464        }
465    }
466
467    fn map_refusal(refusal: rightkit_fs::test_mode::Refusal) -> Refusal {
468        use rightkit_fs::test_mode::Refusal as FsRefusal;
469        match refusal {
470            FsRefusal::ActivationWithoutRoot => Refusal::ActivationWithoutRoot,
471            FsRefusal::RootWithoutActivation => Refusal::RootWithoutActivation,
472            FsRefusal::RootNotAbsolute => Refusal::RootNotAbsolute,
473            FsRefusal::RootNotDirectory => Refusal::RootNotDirectory,
474            FsRefusal::RootHasSymlink => Refusal::RootHasSymlink,
475            FsRefusal::RootOverlapsUserData => Refusal::RootOverlapsUserData,
476            FsRefusal::RootOnRemovableOrCloud => Refusal::RootOnRemovableOrCloud,
477            FsRefusal::RootNotPrivate => Refusal::RootNotPrivate,
478            FsRefusal::RootNotCreatedByRunner => Refusal::RootNotCreatedByRunner,
479            FsRefusal::BindingAppMismatch => Refusal::BindingAppMismatch,
480            FsRefusal::UserRootUnknown => Refusal::UserRootUnknown,
481            FsRefusal::Io(e) => Refusal::Io(e),
482            other => Refusal::Io(other.rule().to_string()),
483        }
484    }
485
486    fn check_binding(app: &AppId, canon: &Path) -> Result<(), Refusal> {
487        let bytes =
488            fs::read(canon.join(BINDING_FILE)).map_err(|_| Refusal::RootNotCreatedByRunner)?;
489        let value: serde_json::Value =
490            serde_json::from_slice(&bytes).map_err(|_| Refusal::RootNotCreatedByRunner)?;
491        match value.get("app").and_then(|v| v.as_str()) {
492            None => Err(Refusal::RootNotCreatedByRunner),
493            Some(bound) if bound == app.id => Ok(()),
494            Some(_) => Err(Refusal::BindingAppMismatch),
495        }
496    }
497
498    fn verify(app: &AppId, input: &dyn ModeInput, raw: &str) -> Result<TestRoot, Refusal> {
499        let real = input
500            .per_user_default()
501            .map_err(|_| Refusal::UserRootUnknown)?;
502        let mut protected = input.protected_paths(&real);
503        if protected.is_empty() {
504            // Nothing to prove disjointness against; the real root alone is not enough.
505            return Err(Refusal::UserRootUnknown);
506        }
507        protected.push(real.root().to_path_buf());
508        let validated =
509            rightkit_fs::test_mode::validate_root(app.id, Path::new(raw.trim()), &protected)
510                .map_err(map_refusal)?;
511        let suite_path = validated.child(Path::new("shared")).map_err(map_refusal)?;
512        let suite = Suite::at(suite_path).map_err(|e| Refusal::Io(e.to_string()))?;
513        Ok(TestRoot {
514            path: validated.path().to_path_buf(),
515            suite,
516            app_id: app.id,
517        })
518    }
519
520    impl TestRoot {
521        /// Remove everything inside the root except the runner's dot files (binding and markers),
522        /// after re-verifying the binding. Never follows a symlink out of the root, and nothing
523        /// reachable from this type is a real user location.
524        pub fn reset(&self) -> Result<(), Refusal> {
525            let app = AppId {
526                id: self.app_id,
527                dir_name: self.app_id,
528            };
529            check_binding(&app, &self.path)?;
530            for entry in fs::read_dir(&self.path)? {
531                let entry = entry?;
532                if entry.file_name().to_string_lossy().starts_with('.') {
533                    continue;
534                }
535                let ty = entry.file_type()?;
536                if ty.is_dir() && !ty.is_symlink() {
537                    fs::remove_dir_all(entry.path())?;
538                } else {
539                    fs::remove_file(entry.path())?;
540                }
541            }
542            Ok(())
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    fn pair(env: Option<&str>, account: &str) -> (Option<PathBuf>, PathBuf) {
552        (env.map(PathBuf::from), PathBuf::from(account))
553    }
554
555    #[test]
556    fn a_root_under_a_differing_env_profile_is_env_shaped() {
557        let root = Path::new("/qa/home/AppData/Local/Orthic Labs/shared");
558        // The env value differs from the account and the root lies under it: fail closed.
559        assert!(root_under_differing_env(
560            root,
561            &[pair(Some("/qa/home"), "/users/real")]
562        ));
563        assert!(root_under_differing_env(
564            root,
565            &[
566                pair(None, "/users/real"),
567                pair(Some("/qa/home/AppData/Local"), "/x")
568            ]
569        ));
570        // The env value equals the account value, is unset, or the root is not under it.
571        assert!(!root_under_differing_env(
572            root,
573            &[pair(Some("/qa/home"), "/qa/home")]
574        ));
575        assert!(!root_under_differing_env(root, &[pair(None, "/qa/home")]));
576        assert!(!root_under_differing_env(
577            root,
578            &[pair(Some("/elsewhere"), "/qa/home")]
579        ));
580        // Whole components only: `/qa/hom` is not an ancestor of `/qa/home/...`.
581        assert!(!root_under_differing_env(
582            root,
583            &[pair(Some("/qa/hom"), "/users/real")]
584        ));
585    }
586
587    #[cfg(windows)]
588    #[test]
589    fn env_comparison_ignores_case_separators_and_the_verbatim_prefix_on_windows() {
590        let root = Path::new(r"\\?\C:\Users\QA\AppData\Local\Orthic Labs\shared");
591        assert!(root_under_differing_env(
592            root,
593            &[pair(Some("c:/users/qa"), r"C:\Users\Real")]
594        ));
595        assert!(!root_under_differing_env(
596            root,
597            &[pair(Some(r"C:\USERS\QA"), r"c:\users\qa")]
598        ));
599    }
600
601    #[test]
602    fn the_default_protected_paths_keep_the_orthic_folder_and_content_dirs() {
603        let real = Suite::at(Path::new(if cfg!(windows) {
604            r"C:\Users\acct\AppData\Local\Orthic Labs\shared"
605        } else {
606            "/users/acct/AppData/Local/Orthic Labs/shared"
607        }))
608        .unwrap();
609        let paths = default_protected_paths(&real);
610        assert!(paths.iter().any(|p| p.ends_with("Orthic Labs")));
611        assert!(paths.iter().any(|p| p.ends_with("Documents")));
612    }
613}