Skip to main content

pitboard_core/host/
mod.rs

1//! The machine Pitboard runs on, behind one seam.
2//!
3//! Everything that differs between operating systems is answered here, so the rest of the
4//! crate asks a question and never which system it is on. The seam has two faces.
5//!
6//! [`Host`] is the part a test replaces: the stores secrets and logins live in, the process
7//! list, the scheduler and whether this process runs as the person, reached through the
8//! [`Context`] every call carries. A test puts [`memory::MemoryHost`] there and can make any
9//! of them fail, or say this process runs as root.
10//!
11//! [`fs`], [`proc`] and [`user`] are plain functions for what the machine does the same way
12//! whoever asks, which the tests run for real: creating a file only its owner can reach, and
13//! every other change to the disk, each taking the [`Permit`] the one gate every change
14//! passes makes; asking whether a process is still alive; naming the person signed in.
15//! [`login_path`] is one too: the `PATH` the person's login shell builds, which an app the
16//! system started does not have.
17//!
18//! The system is chosen once, in this file, and nowhere else. A fact that differs by system
19//! is a `match` on [`OS`], and [`Os`] lists every system Pitboard runs on, so a system added
20//! there does not compile until each such fact has been said for it. This replaced branches
21//! that read "macOS, or else Linux", which compiled anywhere and did the Linux thing.
22
23use crate::context::{Context, Environment};
24use crate::error::Result;
25use crate::service::Permit;
26use crate::store::RawStore;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29
30#[cfg_attr(
31    not(windows),
32    allow(
33        dead_code,
34        reason = "only Windows reads an access list; every system's tests read these rules"
35    )
36)]
37pub(crate) mod acl;
38pub(crate) mod administered;
39#[cfg(target_os = "linux")]
40mod linux;
41#[cfg(target_os = "macos")]
42mod macos;
43#[cfg(any(test, feature = "test-support"))]
44pub(crate) mod memory;
45#[cfg(any(windows, test))]
46pub(crate) mod owner;
47pub(crate) mod program;
48#[cfg_attr(
49    not(windows),
50    allow(
51        dead_code,
52        reason = "only Windows replaces a file this way; every system's tests read these rules"
53    )
54)]
55pub(crate) mod replace;
56#[cfg_attr(
57    not(windows),
58    allow(
59        dead_code,
60        reason = "only Windows schedules a task; every system's tests read these rules"
61    )
62)]
63pub(crate) mod task;
64pub(crate) mod token;
65#[cfg(unix)]
66mod unix;
67#[cfg(windows)]
68mod windows;
69
70#[cfg(target_os = "linux")]
71use linux as os;
72#[cfg(target_os = "macos")]
73use macos as os;
74#[cfg(windows)]
75use windows as os;
76
77pub(crate) use administered::{Administered, Hive};
78pub(crate) use os::{fs, proc, user};
79#[cfg(all(windows, any(test, feature = "test-support")))]
80pub(crate) use windows::credman as windows_credman;
81#[cfg(all(windows, test))]
82pub(crate) use windows::seal::Dpapi;
83
84/// The operating systems Pitboard runs on. Windows only once it is released
85/// ([`crate::release`]): until then a Windows build of a release refuses everything, and its
86/// face, `host/windows`, refuses whatever is not built yet.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Os {
89    MacOs,
90    Linux,
91    Windows,
92}
93
94/// The system this build runs on.
95pub const OS: Os = os::OS;
96
97/// The only program trusted to read Claude Code's keychain item. Named here so the backend
98/// that runs it and the doctor check that looks for it cannot drift apart.
99pub(crate) const SECURITY: &str = "/usr/bin/security";
100
101impl Os {
102    /// The program Pitboard reaches the system's own store of secrets through, where it goes
103    /// through one rather than calling the system directly.
104    pub(crate) fn secrets_tool(self) -> Option<&'static str> {
105        match self {
106            Os::MacOs => Some(SECURITY),
107            // Credential Manager is called directly, through Windows' own API.
108            Os::Linux | Os::Windows => None,
109        }
110    }
111
112    // What a person types to make `paths` private to themselves: on Windows two `icacls` a
113    // path, naming them by `sid`, and none without it or for files and folders alike.
114    pub(crate) fn make_private_commands(
115        self,
116        kind: Kind,
117        paths: &[&str],
118        sid: Option<&str>,
119    ) -> Vec<String> {
120        match self {
121            Os::MacOs | Os::Linux => {
122                let mode = match kind {
123                    Kind::Directory => "700",
124                    Kind::File => "600",
125                    Kind::Any => "go-rwx",
126                };
127                vec![format!("chmod {mode} {}", paths.join(" "))]
128            }
129            Os::Windows => match (kind, sid) {
130                (Kind::Directory | Kind::File, Some(sid)) => paths
131                    .iter()
132                    .flat_map(|path| acl::icacls(kind == Kind::Directory, path, sid))
133                    .collect(),
134                _ => Vec::new(),
135            },
136        }
137    }
138
139    pub(crate) fn remove_file_command(self, path: &Path) -> String {
140        match self {
141            Os::MacOs | Os::Linux => {
142                format!("rm '{}'", path.display().to_string().replace('\'', r"'\''"))
143            }
144            Os::Windows => {
145                let mut quoted = String::new();
146                for c in path.display().to_string().chars() {
147                    // PowerShell takes each of these as a single quote, so each is doubled.
148                    if matches!(c, '\'' | '\u{2018}' | '\u{2019}' | '\u{201a}' | '\u{201b}') {
149                        quoted.push(c);
150                    }
151                    quoted.push(c);
152                }
153                format!("Remove-Item -LiteralPath '{quoted}'")
154            }
155        }
156    }
157
158    // Looking in one asks the person whether Pitboard may. Linux and Windows guard none.
159    pub(crate) fn guarded_folders(self) -> &'static [&'static str] {
160        match self {
161            Os::MacOs => &[
162                "Desktop",
163                "Documents",
164                "Downloads",
165                "Library/Mobile Documents",
166                "Library/CloudStorage",
167            ],
168            Os::Linux | Os::Windows => &[],
169        }
170    }
171
172    // Homebrew's on a Mac, also npm's global `bin` there (Claude Code 2.1.289); none on Linux.
173    pub(crate) fn package_bins(self, env: &Environment) -> Vec<PathBuf> {
174        match self {
175            Os::MacOs => vec!["/opt/homebrew/bin".into(), "/usr/local/bin".into()],
176            Os::Linux => Vec::new(),
177            Os::Windows => [
178                ("APPDATA", &["npm"][..]),
179                ("LOCALAPPDATA", &["Microsoft", "WinGet", "Links"]),
180                ("USERPROFILE", &["scoop", "shims"]),
181            ]
182            .into_iter()
183            .filter_map(|(variable, below)| {
184                let mut place = env.named_folder(variable)?;
185                place.extend(below);
186                Some(place)
187            })
188            .collect(),
189        }
190    }
191
192    /// Whether `a` and `b` are one path, as Pitboard compares the places a program runs
193    /// from: component by component, exactly on macOS and Linux, and in any case on Windows,
194    /// whose file systems take `Codex.exe` and `codex.exe` for one file. Their process lists
195    /// give a program's path as it was started or as the file system has it, and the places a
196    /// tool's holders name are named by their makers in one case. Every comparison of where
197    /// a program runs from goes through this, so a system whose paths ignore case says so
198    /// once.
199    pub(crate) fn same_path(self, a: &Path, b: &Path) -> bool {
200        match self {
201            Os::MacOs | Os::Linux => a == b,
202            Os::Windows => same_path_in_any_case(a, b),
203        }
204    }
205
206    pub(crate) fn variable_key(self, name: &std::ffi::OsStr) -> std::ffi::OsString {
207        match self {
208            Os::MacOs | Os::Linux => name.to_os_string(),
209            Os::Windows => name
210                .to_string_lossy()
211                .chars()
212                .map(upper_one_for_one)
213                .collect::<String>()
214                .into(),
215        }
216    }
217
218    pub(crate) fn user_variable(self) -> &'static str {
219        match self {
220            Os::MacOs | Os::Linux => "USER",
221            Os::Windows => "USERNAME",
222        }
223    }
224
225    // Bun 1.4.3's `os.homedir` on Windows is libuv's `uv_os_homedir`: `USERPROFILE`, else
226    // `GetUserProfileDirectoryW`. Git Bash sets `HOME`, which no tool takes for the home there.
227    pub fn home_variable(self) -> &'static str {
228        match self {
229            Os::MacOs | Os::Linux => "HOME",
230            Os::Windows => "USERPROFILE",
231        }
232    }
233
234    // From the home it is handed, so a scheduled run and one by hand agree.
235    pub(crate) fn default_pitboard_home(self, home: &Path) -> PathBuf {
236        match self {
237            Os::MacOs | Os::Linux => home.join(".pitboard"),
238            Os::Windows => home.join("AppData").join("Local").join("Pitboard"),
239        }
240    }
241
242    // Never beside the Windows app's `Pitboard.exe`, since NTFS takes the two names for one.
243    pub(crate) fn app_command_line(self, app: &Path) -> Option<PathBuf> {
244        match self {
245            Os::MacOs => (app.extension() == Some("app".as_ref()))
246                .then(|| app.join("Contents/Helpers/pitboard")),
247            Os::Linux => None,
248            Os::Windows => app
249                .file_name()
250                .filter(|name| name.eq_ignore_ascii_case("Pitboard.exe"))
251                .and_then(|_| app.parent())
252                .map(|install| install.join("bin").join("pitboard.exe")),
253        }
254    }
255}
256
257/// Whether `a` and `b` are one path as Windows compares them: component by component, each
258/// name in any case ([`same_name_in_any_case`]). Said once, for every comparison of paths on
259/// Windows, the core's and an app's.
260pub fn same_path_in_any_case(a: &Path, b: &Path) -> bool {
261    let (mut a, mut b) = (a.components(), b.components());
262    loop {
263        match (a.next(), b.next()) {
264            (None, None) => return true,
265            (Some(one), Some(other))
266                if same_name_in_any_case(one.as_os_str(), other.as_os_str()) => {}
267            _ => return false,
268        }
269    }
270}
271
272// One text per Windows path, read lexically on every system: two spellings of one folder key
273// alike, and a short name or a link keys apart, as a symlink does on macOS.
274pub fn path_key_in_any_case(path: &str) -> String {
275    let path = path.replace('/', r"\");
276    let path = match path.get(..8) {
277        Some(verbatim) if verbatim.eq_ignore_ascii_case(r"\\?\UNC\") => {
278            format!(r"\\{}", &path[8..])
279        }
280        _ => path.strip_prefix(r"\\?\").unwrap_or(&path).to_owned(),
281    };
282    let upper = |name: &str| name.chars().map(upper_one_for_one).collect::<String>();
283    let drive = path
284        .as_bytes()
285        .get(..2)
286        .filter(|head| head[0].is_ascii_alphabetic() && head[1] == b':')
287        .map(|head| char::from(head[0].to_ascii_uppercase()));
288    let (root, rest) = if let Some(unc) = path.strip_prefix(r"\\") {
289        let mut parts = unc.splitn(3, '\\');
290        let (server, share) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));
291        (
292            format!(r"\\{}\{}\", upper(server), upper(share)),
293            parts.next().unwrap_or(""),
294        )
295    } else if let Some(drive) = drive {
296        match path[2..].strip_prefix('\\') {
297            Some(rest) => (format!(r"{drive}:\"), rest),
298            None => (format!("{drive}:"), &path[2..]),
299        }
300    } else if let Some(rest) = path.strip_prefix('\\') {
301        (r"\".to_owned(), rest)
302    } else {
303        (String::new(), path.as_str())
304    };
305    let rooted = root.ends_with('\\');
306    let mut names: Vec<&str> = Vec::new();
307    for name in rest.split('\\') {
308        match name {
309            "" | "." => {}
310            ".." if names.last().is_some_and(|last| *last != "..") => {
311                names.pop();
312            }
313            ".." if rooted => {}
314            name => names.push(name),
315        }
316    }
317    let names: Vec<String> = names.into_iter().map(upper).collect();
318    root + &names.join(r"\")
319}
320
321// As libuv's realpath and Codex's `AbsolutePathBuf` spell what `GetFinalPathNameByHandleW`
322// gives, read lexically on every system.
323pub(crate) fn without_verbatim_prefix(path: PathBuf) -> PathBuf {
324    let Some(text) = path.to_str() else {
325        return path;
326    };
327    if let Some(unc) = text.strip_prefix(r"\\?\UNC\") {
328        return PathBuf::from(format!(r"\\{unc}"));
329    }
330    match text.strip_prefix(r"\\?\") {
331        Some(rest) if drive_absolute(rest) => PathBuf::from(rest),
332        _ => path,
333    }
334}
335
336fn drive_absolute(path: &str) -> bool {
337    matches!(path.as_bytes(), [drive, b':', b'\\' | b'/', ..] if drive.is_ascii_alphabetic())
338}
339
340// Claude Desktop's on Windows, which no test reads (AGENTS.md). Its `Claude-3p` there is named
341// by analogy with macOS's, unmeasured: a guard may refuse more than is there.
342#[cfg(any(test, feature = "test-support"))]
343pub(crate) fn claude_desktop_on_windows(roaming: &[PathBuf], local: &[PathBuf]) -> Vec<PathBuf> {
344    let roaming = roaming
345        .iter()
346        .flat_map(|dir| ["Claude", "Claude-3p"].map(|name| dir.join(name)));
347    roaming
348        .chain(local.iter().map(|dir| dir.join("AnthropicClaude")))
349        .collect()
350}
351
352// Where the Windows app's account windows keep their WebView2 sign-ins, each build its own.
353#[cfg(any(test, feature = "test-support"))]
354pub(crate) fn pitboard_app_on_windows(local: &[PathBuf]) -> Vec<PathBuf> {
355    local
356        .iter()
357        .flat_map(|dir| ["Pitboard App", "Pitboard App Debug"].map(|name| dir.join(name)))
358        .collect()
359}
360
361// Where this account keeps a real login, or would, and where `started_in` says, for a build for
362// tests to refuse. This account's own are asked of the system, never of the environment.
363#[cfg(any(test, feature = "test-support"))]
364pub(crate) fn real_login_places(
365    started_in: Option<&Context>,
366) -> std::result::Result<Vec<PathBuf>, String> {
367    let home = user::accounts_own_home().ok_or("this system names no home for this account")?;
368    let pitboard = user::accounts_own_pitboard_home()
369        .ok_or("this system names no Pitboard folder for this account")?;
370    let own = Context::new(home).with_pitboard_home(pitboard);
371    // A test's child is started with scratch app data folders inside its own config folder.
372    let (roaming, local) = user::app_data_folders(started_in.is_some());
373    Ok(std::iter::once(&own)
374        .chain(started_in)
375        .flat_map(|ctx| {
376            let mut places = vec![
377                crate::provider::claude::paths::config_dir(ctx),
378                crate::provider::claude::paths::config_file(ctx),
379                crate::provider::codex::paths::home(ctx),
380                ctx.pitboard_home.clone(),
381                ctx.default_pitboard_home(),
382            ];
383            if ctx.os != Os::Windows {
384                places.extend([
385                    ctx.home.join("Library/Application Support/Claude"),
386                    ctx.home.join("Library/Application Support/Claude-3p"),
387                    PathBuf::from("/Applications/Claude.app"),
388                ]);
389            }
390            places
391        })
392        .chain(claude_desktop_on_windows(&roaming, &local))
393        .chain(pitboard_app_on_windows(&local))
394        .filter(|place| place.is_absolute())
395        .collect())
396}
397
398#[cfg(all(windows, any(test, feature = "test-support")))]
399pub(crate) use windows::lasting_environment;
400
401// What the account's lasting environment names, which a test's own may have inherited: nothing
402// off Windows.
403#[cfg(all(not(windows), any(test, feature = "test-support")))]
404pub(crate) fn lasting_environment(_name: &str) -> Vec<String> {
405    Vec::new()
406}
407
408// Is, holds or lies in, for a build for tests to refuse, the same on every system: through
409// every link, one to nothing yet included, and in any case, as macOS's own volumes compare.
410#[cfg(any(test, feature = "test-support"))]
411pub(crate) fn lies_in(path: &Path, place: &Path) -> bool {
412    let path = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
413    let (real, real_place) = (resolved(&path, LINKS), resolved(place, LINKS));
414    near(&path, place)
415        || near(&real, &real_place)
416        || meets(&real, &real_place)
417        || meets(&real_place, &real)
418}
419
420// So a loop of links ends, as the system's own lookup does.
421#[cfg(any(test, feature = "test-support"))]
422const LINKS: u8 = 32;
423
424#[cfg(any(test, feature = "test-support"))]
425fn near(a: &Path, b: &Path) -> bool {
426    begins(a, b) || begins(b, a)
427}
428
429// `canonicalize` spells `C:\x` as `\\?\C:\x`.
430#[cfg(any(test, feature = "test-support"))]
431fn begins(path: &Path, with: &Path) -> bool {
432    let with = without_verbatim_prefix(with.to_path_buf());
433    without_verbatim_prefix(path.to_path_buf())
434        .ancestors()
435        .any(|above| same_path_in_any_case(above, &with))
436}
437
438// A folder above `a` that is `b`'s deepest existing one, whatever either is named:
439// `canonicalize` keeps /System/Volumes/Data/Users, which is /Users.
440#[cfg(any(test, feature = "test-support"))]
441fn meets(a: &Path, b: &Path) -> bool {
442    let Some((deepest, below_b)) = b
443        .ancestors()
444        .find_map(|above| Some((fs::file_id(above)?, b.strip_prefix(above).ok()?)))
445    else {
446        return false;
447    };
448    a.ancestors().any(|above| {
449        fs::file_id(above) == Some(deepest)
450            && a.strip_prefix(above)
451                .is_ok_and(|below_a| near(below_a, below_b))
452    })
453}
454
455// The deepest part that exists, resolved, or a link there followed though it leads nowhere
456// yet; the rest after it as written, less each `..`, as a folder made there would take it.
457#[cfg(any(test, feature = "test-support"))]
458fn resolved(path: &Path, links: u8) -> PathBuf {
459    for found in path.ancestors() {
460        let Ok(rest) = path.strip_prefix(found) else {
461            continue;
462        };
463        if let Ok(real) = std::fs::canonicalize(found) {
464            return beyond(real, rest);
465        }
466        let link = std::fs::symlink_metadata(found).is_ok_and(|it| it.file_type().is_symlink());
467        if let (true, Some(left), Ok(to)) = (link, links.checked_sub(1), std::fs::read_link(found))
468        {
469            let to = match found.parent() {
470                Some(parent) => parent.join(to),
471                None => to,
472            };
473            return resolved(&beyond(to, rest), left);
474        }
475    }
476    path.to_path_buf()
477}
478
479#[cfg(any(test, feature = "test-support"))]
480fn beyond(mut at: PathBuf, rest: &Path) -> PathBuf {
481    for part in rest.components() {
482        match part {
483            std::path::Component::ParentDir => {
484                at.pop();
485            }
486            std::path::Component::CurDir => {}
487            part => at.push(part),
488        }
489    }
490    at
491}
492
493fn same_name_in_any_case(a: &std::ffi::OsStr, b: &std::ffi::OsStr) -> bool {
494    let (a, b) = (a.to_string_lossy(), b.to_string_lossy());
495    a.chars()
496        .map(upper_one_for_one)
497        .eq(b.chars().map(upper_one_for_one))
498}
499
500// As Windows upper-cases a name: one UTF-16 unit for one, so ß and anything past the BMP stay.
501pub(crate) fn upper_one_for_one(c: char) -> char {
502    if u32::from(c) > 0xFFFF {
503        return c;
504    }
505    let mut upper = c.to_uppercase();
506    match (upper.next(), upper.next()) {
507        (Some(one), None) => one,
508        _ => c,
509    }
510}
511
512/// What the person's login shell said its `PATH` is.
513#[derive(Debug, Clone, PartialEq, Eq)]
514#[cfg_attr(
515    windows,
516    allow(
517        dead_code,
518        reason = "only a login shell is late, and Windows has none: its `Path` is read from \
519                  the registry"
520    )
521)]
522pub(crate) enum LoginPath {
523    /// It said this.
524    Said(String),
525    /// It could not be asked, or did not say. Asking again would get the same.
526    Unknown,
527    /// It had not answered in time, and was stopped. Startup files are slowest while the
528    /// machine is busy, which is when an app that opens at login first asks, so this is not
529    /// the last word: a later ask may find what this one could not.
530    Late,
531}
532
533// A login shell can take seconds, so never on an app's main thread.
534pub(crate) fn login_path(env: &Environment) -> LoginPath {
535    os::login_path(env)
536}
537
538/// Who besides a file's owner can reach it, as the system says.
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub(crate) struct Access {
541    /// Whether anybody but the owner can read or change it.
542    pub(crate) shared: bool,
543    // For a person to read: `mode 600`, or on Windows who may reach it, never by SID.
544    pub(crate) described: String,
545}
546
547// What a folder Pitboard keeps logins in is, as `fs::private_dir` finds it.
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum Private {
550    Yes,
551    Shared,
552    NotOwn,
553    Link,
554    NotADirectory,
555    Missing,
556}
557
558// What a person makes private with the commands `Os::make_private_commands` gives.
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
560pub(crate) enum Kind {
561    Directory,
562    File,
563    /// Directories and files alike.
564    Any,
565}
566
567/// Whether this process runs as the person themselves, as the system says. Pitboard changes
568/// nothing unless it is [`Elevation::Normal`]: what a run with more rights than the person's
569/// own writes is not theirs, in their own home and their own keychain, and their next
570/// ordinary run may not be able to read it, replace it or take it away.
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub enum Elevation {
573    /// As the person themselves.
574    Normal,
575    /// With rights that are not the person's own. `why` says how, in words that follow
576    /// "Pitboard runs", such as `as root`, `with sudo` or `as administrator`.
577    Elevated { why: &'static str },
578    /// The system could not say, which Pitboard takes as a reason to change nothing.
579    Unknown,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub enum Floor {
584    Met,
585    Below { build: u32 },
586    Unknown,
587}
588
589// What Windows' settings say that changes what Pitboard or a tool does there.
590#[derive(Debug, Clone, Default, PartialEq, Eq)]
591pub(crate) struct WindowsSettings {
592    // The distributions WSL holds for this account, by `DistributionName` under HKCU's `Lxss`.
593    pub(crate) wsl: Vec<String>,
594    // `ProxyServer` where `ProxyEnable` is not 0, as hyper-util 0.1.20 reads them for Codex.
595    pub(crate) proxy: Option<String>,
596    // `EnableLUA`, which runs administrators in Admin Approval Mode where it is not 0.
597    pub(crate) admin_approval: Option<bool>,
598}
599
600// Windows 11 24H2's build, which Windows Server 2025 shares.
601pub(crate) const WINDOWS_FLOOR: u32 = 26100;
602
603#[cfg(any(windows, test))]
604pub(crate) fn windows_floor(version: Option<(u32, u32, u32)>) -> Floor {
605    match version {
606        None => Floor::Unknown,
607        Some((major, minor, build)) if (major, minor, build) >= (10, 0, WINDOWS_FLOOR) => {
608            Floor::Met
609        }
610        Some((_, _, build)) => Floor::Below { build },
611    }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq)]
615pub(crate) struct Process {
616    pub(crate) pid: u32,
617    // macOS gives what it was started as, which a shell gives as the bare name.
618    pub(crate) path: PathBuf,
619    // The family of the Windows package it runs from.
620    pub(crate) package: Option<String>,
621}
622
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct Listed {
625    pub pid: u32,
626    pub parent: u32,
627    pub path: PathBuf,
628    pub package: Option<String>,
629    // False where Windows would not say whose it is or where it runs from: `path` is its name.
630    pub placed: bool,
631}
632
633// None where one of them cannot be placed, since it may be any kind of holder.
634pub(crate) fn named(os: Os, all: Vec<Listed>, program: &str) -> Option<Vec<Process>> {
635    let files: Vec<String> = match os {
636        Os::MacOs | Os::Linux => vec![program.to_owned()],
637        Os::Windows => {
638            let mut files = vec![format!("{program}.exe")];
639            // winget's Codex is the nested portable codex-<triple>.exe behind Links\codex.exe;
640            // Toolhelp names the image the link resolves to (codex_windows_install_places).
641            if program == "codex" {
642                files.push("codex-x86_64-pc-windows-msvc.exe".to_owned());
643                files.push("codex-aarch64-pc-windows-msvc.exe".to_owned());
644            }
645            files
646        }
647    };
648    let runs = |name: &std::ffi::OsStr| match os {
649        Os::MacOs | Os::Linux => files.iter().any(|file| name == file.as_str()),
650        Os::Windows => files
651            .iter()
652            .any(|file| same_name_in_any_case(name, std::ffi::OsStr::new(file))),
653    };
654    let found: Vec<Listed> = all
655        .into_iter()
656        .filter(|p| p.path.file_name().is_some_and(runs))
657        .collect();
658    if found.iter().any(|p| !p.placed) {
659        return None;
660    }
661    Some(
662        found
663            .into_iter()
664            .map(|p| Process {
665                pid: p.pid,
666                path: p.path,
667                package: p.package,
668            })
669            .collect(),
670    )
671}
672
673pub struct Jar {
674    pub version: i64,
675    pub compatible: i64,
676    pub cookies: Vec<Cookie>,
677}
678
679pub struct Cookie {
680    pub host: String,
681    pub name: String,
682    pub path: String,
683    pub stored: Vec<u8>,
684    pub expires_utc: i64,
685}
686
687// Lengths only: a cookie's value is a login.
688impl std::fmt::Debug for Cookie {
689    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
690        f.debug_struct("Cookie")
691            .field("host", &self.host)
692            .field("name", &self.name)
693            .field("path", &self.path)
694            .field("stored", &format_args!("{} bytes", self.stored.len()))
695            .field("expires_utc", &self.expires_utc)
696            .finish()
697    }
698}
699
700#[derive(Debug)]
701pub enum JarError {
702    Absent,
703    // a write is under way, or the jar is held
704    Unsettled(String),
705    // read, and not a shape Pitboard knows
706    Unknown(String),
707    Unreadable(String),
708    Unsupported,
709}
710
711#[cfg(any(test, feature = "test-support"))]
712pub(crate) use os::write_jar;
713
714/// What a program run with a ceiling ([`proc::run_within`]) came to: its exit status, `None`
715/// where a signal or the ceiling ended it, and what it printed on standard output.
716#[derive(Debug, Clone, Default, PartialEq, Eq)]
717pub struct Ran {
718    pub exit: Option<i32>,
719    pub stdout: String,
720    pub timed_out: bool,
721}
722
723/// The machine Pitboard is standing on, as one value rather than a set of `cfg` branches
724/// spread through the crate. A host answers where another program's secrets may be, where
725/// Pitboard's own parked logins go, what this user is running and how daily renewal is
726/// scheduled. It takes the context on every call because a context is built by a builder
727/// and can still change after it exists.
728///
729/// It was called `Platform` until a second provider was on the way. The name said "which
730/// operating system", the body reached into Claude Code's own slot hashing, and once
731/// "provider" became a word this codebase uses, a reader meeting `Platform` could not tell
732/// which of the two axes it meant. What stays behind this name is the machine, and only the
733/// machine: which item or file a tool keeps its login in is the tool's to say.
734pub(crate) trait Host: Send + Sync + std::fmt::Debug {
735    /// Secrets another program keeps in the system's own store of them, under `account`:
736    /// the login keychain on macOS, Credential Manager's generic items on Windows. `None`
737    /// where the system has no such store, and a tool keeps its login in a file instead.
738    ///
739    /// Which items and which account is the other program's business, so both are handed
740    /// in. Deriving them here is how Claude Code's slot hashing came to live inside what
741    /// claimed to be an operating-system abstraction.
742    fn foreign_secrets(&self, ctx: &Context, account: &str) -> Option<Box<dyn RawStore>>;
743
744    /// The single file at `path`, as a store.
745    fn file(&self, path: PathBuf) -> Box<dyn RawStore>;
746
747    /// Where Pitboard's own parked logins go: the system's store of secrets where there is
748    /// one, a private directory of files where there is not. This one really is a fact
749    /// about the machine.
750    fn vault(&self, ctx: &Context) -> Box<dyn RawStore>;
751
752    /// Whether every `PITBOARD_HOME` on this machine parks its logins in the one vault. A
753    /// keychain belongs to the whole login session, so a park in it that one home cannot
754    /// account for may be another home's; a vault of files lives inside its home, and
755    /// nothing in it can be anybody else's.
756    fn vault_is_shared(&self) -> bool;
757
758    // Every process this user runs, in any session; Err says why the list could not be read.
759    fn listing(&self) -> std::result::Result<Vec<Listed>, String>;
760
761    // By the program's own name, so a script that merely mentions it is not counted. None is
762    // not the same as none running. Another user's processes use that user's login.
763    fn processes(&self, program: &str) -> Option<Vec<Process>> {
764        named(OS, self.listing().ok()?, program)
765    }
766
767    // a Chromium jar's rows for `hosts`, and those named one of `names` on any host, as stored
768    fn cookies(
769        &self,
770        jar: &Path,
771        hosts: &[&str],
772        names: &[&str],
773    ) -> std::result::Result<Jar, JarError>;
774
775    // None on a real host in a build for tests; a test names a version through `app_at`.
776    fn app_version(&self, bundle: &Path) -> Option<String>;
777    fn app_id(&self, bundle: &Path) -> Option<String>;
778
779    /// The system's own scheduler, which runs daily renewal. `None` where there is none
780    /// Pitboard knows how to ask.
781    fn scheduler(&self) -> Option<&dyn Scheduler>;
782
783    /// Whether this process runs as the person themselves, which is what the one gate every
784    /// change passes asks ([`crate::service::Permit`]). Takes the context because part of
785    /// the answer can be in the environment this process was started with, such as the
786    /// `SUDO_UID` sudo sets.
787    fn elevation(&self, ctx: &Context) -> Elevation;
788
789    fn floor(&self) -> Floor;
790
791    /// The file at `path`, outside every home, that only an administrator writes, such as
792    /// Codex's `/etc/codex/requirements.toml`. Which file is the tool's to say.
793    ///
794    /// Unset in a build for tests, on the real hosts ([`administered::READ_BY_REAL_HOSTS`]).
795    fn administered_file(&self, path: &Path) -> Administered {
796        if administered::READ_BY_REAL_HOSTS {
797            administered::read_text(path)
798        } else {
799            Administered::Unset
800        }
801    }
802
803    /// The text an administrator's configuration profile forces `key` of the program whose
804    /// preferences are `domain` to: a managed preference, on macOS. Unset where nothing
805    /// forces it, whoever set it otherwise, and on a system with no managed preferences.
806    ///
807    /// Unset in a build for tests, on the real hosts ([`administered::READ_BY_REAL_HOSTS`]).
808    fn managed_preference(&self, domain: &str, key: &str) -> Administered;
809
810    // None in a build for tests, on the real hosts.
811    fn administered_folder(&self, dir: &Path) -> Vec<PathBuf> {
812        if administered::READ_BY_REAL_HOSTS {
813            administered::listed(dir)
814        } else {
815            Vec::new()
816        }
817    }
818
819    // A Windows policy value; unset elsewhere, and in a build for tests, on the real hosts.
820    fn policy(&self, _hive: Hive, _key: &str, _value: &str) -> Administered {
821        Administered::Unset
822    }
823
824    // Windows' `FOLDERID_ProgramData`, where it can say; none elsewhere.
825    fn program_data(&self) -> Option<PathBuf> {
826        None
827    }
828
829    // The names of Credential Manager's items that begin `prefix`, never what they hold; none
830    // elsewhere, and none in a build for tests on the real hosts.
831    fn credential_names(&self, _prefix: &str) -> std::result::Result<Vec<String>, String> {
832        Ok(Vec::new())
833    }
834
835    // As `HKCU\Environment` holds it, which a program started from now on is given; unset
836    // elsewhere, and in a build for tests on the real hosts.
837    fn user_environment(&self, _name: &str) -> Administered {
838        Administered::Unset
839    }
840
841    // As `HKLM`'s `Session Manager\Environment` holds it, which a program started from now on is
842    // given where `HKCU\Environment` does not name it; unset elsewhere, and in a build for tests
843    // on the real hosts.
844    fn machine_environment(&self, _name: &str) -> Administered {
845        Administered::Unset
846    }
847
848    // Whether Credential Manager holds a generic item named `target`, never what it holds; Err
849    // says why it cannot tell. None is held where there is no Credential Manager.
850    fn credential_held(
851        &self,
852        _ctx: &Context,
853        _target: &str,
854    ) -> std::result::Result<bool, crate::store::Error> {
855        Ok(false)
856    }
857
858    // None elsewhere, and none in a build for tests on the real hosts.
859    fn windows_settings(&self) -> WindowsSettings {
860        WindowsSettings::default()
861    }
862
863    // Whether `/proc/version` names Microsoft, as Codex 0.163.0 tells WSL by; false elsewhere,
864    // and in a build for tests on the real hosts.
865    fn kernel_is_wsl(&self) -> bool {
866        false
867    }
868
869    // Whether `path`, or the nearest folder above it that opens, is on 9p, as WSL 2 mounts
870    // Windows' drives; false off Linux.
871    fn on_a_windows_drive(&self, _path: &Path) -> bool {
872        false
873    }
874}
875
876/// The system's own scheduler, which starts `pitboard renew` once a day: launchd on macOS,
877/// a systemd user timer on Linux, Task Scheduler on Windows. Never a homemade daemon.
878///
879/// What Pitboard decides about the schedule, which program it runs, how often, and whether
880/// it is this home's to change, is [`crate::schedule`]'s. This is only how the system is
881/// asked, and what it says back.
882pub(crate) trait Scheduler: Send + Sync + std::fmt::Debug {
883    /// Where the schedule is kept, where a person would look for it.
884    fn location(&self, ctx: &Context) -> crate::schedule::Location;
885
886    /// Whether a schedule is there.
887    fn installed(&self, ctx: &Context) -> bool;
888
889    /// The Pitboard the schedule runs, read back from what [`Scheduler::put`] wrote. `None`
890    /// where nothing is installed, and where what is there does not name one the way `put`
891    /// writes it.
892    fn program(&self, ctx: &Context) -> Option<PathBuf>;
893
894    /// The variables the schedule's job is given, by name, as [`Scheduler::put`] wrote them,
895    /// read back. Empty where nothing is installed, and where what is there gives none the way
896    /// `put` writes them. The system can give a job more of its own, which this does not say.
897    fn environment(&self, ctx: &Context) -> Vec<(String, String)>;
898
899    /// Schedule `program renew`, given `environment`, each variable by name, besides what the
900    /// system gives every job, and ask the system to start it. What is written can hold a
901    /// proxy's password, so only the person can read it, whatever was there before.
902    ///
903    /// Where the system will not start it, what was there goes back as it was, and is
904    /// started again. The status, doctor and the app all read what is there, so a schedule
905    /// left written that nothing runs would say renewal is on while it is not, which is the
906    /// failure nobody would notice until the parked logins had run out.
907    fn put(
908        &self,
909        ctx: &Context,
910        permit: Permit,
911        program: &Path,
912        environment: &[(String, String)],
913    ) -> Result<()>;
914
915    /// Stop the schedule and take it away. `false` when nothing was there.
916    fn remove(&self, ctx: &Context, permit: Permit) -> Result<bool>;
917
918    /// Whether this process is a run the schedule itself started. `said` is the job a test
919    /// says this process runs as; `None` leaves it to what the system says.
920    fn started_this_process(&self, said: Option<&str>) -> bool;
921
922    // What a run it started reads for itself of what `put` was given: none where the system
923    // hands the job its variables.
924    fn read_by_its_runs(&self, _ctx: &Context) -> Vec<(String, String)> {
925        Vec::new()
926    }
927
928    // What the system says of the job beyond what Pitboard wrote: Task Scheduler's alone.
929    fn task(&self, _ctx: &Context) -> Option<task::Task> {
930        None
931    }
932}
933
934// Time awake since a fixed moment (boot on Windows, the first call elsewhere); a sleep adds none.
935pub fn awake() -> std::time::Duration {
936    os::awake()
937}
938
939#[cfg(any(windows, test))]
940pub(crate) fn from_interrupt_time(units: u64) -> std::time::Duration {
941    std::time::Duration::from_secs(units / 10_000_000)
942        + std::time::Duration::from_nanos(units % 10_000_000 * 100)
943}
944
945/// The host this build is standing on, which is what every real context uses.
946pub(crate) fn current() -> Arc<dyn Host> {
947    os::host()
948}
949
950/// The path this program was started by, where that lasts longer than the file it runs.
951pub(crate) fn current_program() -> std::io::Result<PathBuf> {
952    os::current_program()
953}
954
955#[cfg(test)]
956mod tests {
957    use super::*;
958
959    // This crate's clippy.toml replaces the root's, so it repeats each of its rules.
960    #[test]
961    fn the_crates_lints_keep_every_rule_of_the_workspaces_and_refuse_stds_creates() {
962        let read = |path: &str| {
963            std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(path)).expect(path)
964        };
965        let rules = |text: &str| -> Vec<String> {
966            text.lines()
967                .filter(|line| line.trim_start().starts_with("{ path"))
968                .map(str::to_owned)
969                .collect()
970        };
971        let own = rules(&read("clippy.toml"));
972        for rule in rules(&read("../../clippy.toml")) {
973            assert!(own.contains(&rule), "{rule}");
974        }
975        for create in [
976            "std::fs::write",
977            "std::fs::OpenOptions::create",
978            "std::fs::create_dir_all",
979        ] {
980            let named = format!("{{ path = \"{create}\",");
981            assert!(own.iter().any(|rule| rule.contains(&named)), "{create}");
982        }
983    }
984
985    /// A machine without a store of secrets must say so rather than hand back something
986    /// that behaves like one. A tool's own module builds its chain out of this answer, so a
987    /// host that always offered one would build a chain that cannot work.
988    #[test]
989    fn a_store_of_secrets_is_offered_only_where_there_is_one() {
990        let ctx = Context::for_unit_test();
991        let offered = ctx.host().foreign_secrets(&ctx, "someone");
992        assert_eq!(
993            offered.map(|k| k.kind()),
994            match OS {
995                Os::MacOs => Some(crate::store::Backend::Keychain),
996                Os::Linux => None,
997                Os::Windows => Some(crate::store::Backend::CredentialManager),
998            }
999        );
1000        assert_eq!(
1001            ctx.host().file(PathBuf::from("/nowhere/at/all")).kind(),
1002            crate::store::Backend::File
1003        );
1004    }
1005
1006    fn listed(pid: u32, path: &str) -> Listed {
1007        Listed {
1008            pid,
1009            parent: 1,
1010            path: PathBuf::from(path),
1011            package: None,
1012            placed: true,
1013        }
1014    }
1015
1016    // 0.162.1's Windows package holds codex.exe beside programs whose names start like it.
1017    #[test]
1018    fn a_program_is_named_by_its_file_as_each_system_names_it() {
1019        let all = || {
1020            vec![
1021                listed(
1022                    1,
1023                    "C:/Users/a/.codex/packages/standalone/current/bin/Codex.EXE",
1024                ),
1025                listed(2, "C:/n/vendor/codex-resources/codex-command-runner.exe"),
1026                listed(3, "C:/n/vendor/bin/codex-code-mode-host.exe"),
1027                listed(4, "C:/n/vendor/codex-path/rg.exe"),
1028                listed(5, "/usr/local/bin/codex"),
1029                listed(6, "C:/n/codex"),
1030                // winget's nested portable, which Toolhelp names behind Links\codex.exe.
1031                listed(
1032                    7,
1033                    "C:/Users/a/AppData/Local/Microsoft/WinGet/Packages/OpenAI.Codex_x/codex-x86_64-pc-windows-msvc.exe",
1034                ),
1035                listed(
1036                    8,
1037                    "C:/Users/a/AppData/Local/Microsoft/WinGet/Packages/OpenAI.Codex_x/codex-aarch64-pc-windows-msvc.exe",
1038                ),
1039            ]
1040        };
1041        let pids = |os: Os| {
1042            named(os, all(), "codex").map(|found| found.iter().map(|p| p.pid).collect::<Vec<_>>())
1043        };
1044        assert_eq!(pids(Os::Windows), Some(vec![1, 7, 8]));
1045        assert_eq!(pids(Os::MacOs), Some(vec![5, 6]));
1046        assert_eq!(pids(Os::Linux), Some(vec![5, 6]));
1047    }
1048
1049    // Such as the same person's elevated codex: it may be any kind of holder.
1050    #[test]
1051    fn a_process_that_cannot_be_placed_makes_the_answer_unknown() {
1052        let unplaced = |name: &str| Listed {
1053            placed: false,
1054            ..listed(7, name)
1055        };
1056        let placed = listed(1, "C:/x/codex.exe");
1057        assert_eq!(
1058            named(
1059                Os::Windows,
1060                vec![placed.clone(), unplaced("codex.exe")],
1061                "codex"
1062            ),
1063            None
1064        );
1065        assert_eq!(
1066            named(Os::Windows, vec![placed, unplaced("svchost.exe")], "codex")
1067                .map(|found| found.len()),
1068            Some(1),
1069            "unless it is not the program"
1070        );
1071    }
1072
1073    /// A path is the same one only in the same case on macOS and Linux, as Pitboard has always
1074    /// compared where a program runs from there. A doubled or trailing separator still
1075    /// leaves it the same path.
1076    #[test]
1077    fn paths_are_compared_exactly_on_macos_and_linux() {
1078        for os in [Os::MacOs, Os::Linux] {
1079            let same = |a: &str, b: &str| os.same_path(Path::new(a), Path::new(b));
1080            assert!(same("ChatGPT.app", "ChatGPT.app"), "{os:?}");
1081            assert!(
1082                same("/Applications/ChatGPT.app/", "/Applications//ChatGPT.app"),
1083                "{os:?}"
1084            );
1085            assert!(!same("chatgpt.app", "ChatGPT.app"), "{os:?}");
1086            assert!(!same("ChatGPT.app", "ChatGPT"), "{os:?}");
1087        }
1088    }
1089
1090    /// On Windows a path is the same one in any case, and only as Windows changes a name's
1091    /// case: a name written decomposed is another name, and so is one whose upper case is
1092    /// longer than itself.
1093    #[test]
1094    fn paths_are_compared_in_any_case_on_windows() {
1095        let same = |a: &str, b: &str| Os::Windows.same_path(Path::new(a), Path::new(b));
1096        assert!(same("Codex.exe", "codex.EXE"));
1097        assert!(same("/Programs/OpenAI/Codex/", "/programs//openai/CODEX"));
1098        assert!(same(r"C:\Users\Dana\Codex.exe", r"c:\users\dana\codex.exe"));
1099        assert!(same("Éclair", "éCLAIR"));
1100        assert!(!same("e\u{301}clair", "\u{e9}clair"), "not normalised");
1101        assert!(!same("Straße", "STRASSE"), "ß is upper case as it is");
1102        assert!(!same("Codex.exe", "Codex"));
1103        assert!(!same("/a/b", "/a/b/c"));
1104    }
1105
1106    #[test]
1107    fn a_verbatim_path_is_spelled_as_libuv_and_codex_spell_it() {
1108        let plain = |path: &str| without_verbatim_prefix(PathBuf::from(path));
1109        assert_eq!(
1110            plain(r"\\?\C:\Users\X\.codex"),
1111            PathBuf::from(r"C:\Users\X\.codex")
1112        );
1113        assert_eq!(
1114            plain(r"\\?\UNC\server\share\x"),
1115            PathBuf::from(r"\\server\share\x")
1116        );
1117        for kept in [
1118            r"C:\Users\X",
1119            r"\\?\Volume{0}\x",
1120            r"\\server\share",
1121            "/Users/x",
1122        ] {
1123            assert_eq!(plain(kept), PathBuf::from(kept), "{kept}");
1124        }
1125    }
1126
1127    // Read by the text alone, on every system, as Foundation standardises a file URL on macOS.
1128    #[test]
1129    fn a_windows_path_has_one_key_however_it_is_spelled() {
1130        let key = path_key_in_any_case;
1131        for spelling in [
1132            r"C:\Users\X\AppData\Local\Pitboard",
1133            r"c:\users\x\appdata\local\pitboard\",
1134            r"\\?\C:\Users\X\AppData\Local\Pitboard",
1135            "C:/Users/X/AppData/Local/Pitboard",
1136            r"C:\Users\X\.\AppData\Local\..\Local\Pitboard",
1137            r"C:\Users\\X\AppData\Local\Pitboard",
1138        ] {
1139            assert_eq!(
1140                key(spelling),
1141                r"C:\USERS\X\APPDATA\LOCAL\PITBOARD",
1142                "{spelling}"
1143            );
1144        }
1145        assert_eq!(key(r"\\server\share\Pitboard"), r"\\SERVER\SHARE\PITBOARD");
1146        assert_eq!(
1147            key(r"\\?\UNC\server\share\Pitboard"),
1148            r"\\SERVER\SHARE\PITBOARD"
1149        );
1150        assert_eq!(key(r"C:\..\Pitboard"), r"C:\PITBOARD");
1151        assert_eq!(key(r"C:\"), r"C:\");
1152        assert_eq!(key(r"C:\Users\Straße"), r"C:\USERS\STRAßE");
1153        assert_ne!(key("C:\\Users\\e\u{301}"), key("C:\\Users\\\u{e9}"));
1154        assert_ne!(
1155            key(r"C:\Users\XAVIER~1\AppData\Local\Pitboard"),
1156            key(r"C:\Users\Xavier Dana\AppData\Local\Pitboard"),
1157            "no short name is expanded"
1158        );
1159    }
1160
1161    // One answer on every system: macOS's own volumes ignore case, and Windows drops `..` as
1162    // written, which a folder made where none is yet would do too.
1163    #[test]
1164    fn a_path_lies_in_a_place_in_any_case_and_past_a_folder_not_made_yet() {
1165        let root = std::env::temp_dir().join(format!("pitboard-lies-in-{}", std::process::id()));
1166        let place = root.join("Library/Application Support");
1167        let lies = |path: &str| lies_in(&root.join(path), &place);
1168        assert!(lies("Library/Application Support/x"));
1169        assert!(lies("library/APPLICATION SUPPORT/x"), "in any case");
1170        assert!(lies("LIBRARY"), "it holds it");
1171        assert!(lies("never-made/../Library/Application Support/x"));
1172        assert!(!lies("Library/Application Supportx"));
1173        assert!(!lies("never-made/Library/Application Support"));
1174        assert!(!lies("Library/../elsewhere/Application Support"));
1175        assert!(!root.exists());
1176    }
1177
1178    #[test]
1179    fn claude_desktops_windows_folders_are_refused_in_any_spelling() {
1180        let (roaming, local) = (
1181            PathBuf::from("/p/AppData/Roaming"),
1182            PathBuf::from("/p/AppData/Local"),
1183        );
1184        let places =
1185            claude_desktop_on_windows(std::slice::from_ref(&roaming), std::slice::from_ref(&local));
1186        let refused = |path: PathBuf| places.iter().any(|place| lies_in(&path, place));
1187        for path in [
1188            roaming.join("Claude"),
1189            roaming.join("claude").join("Cookies"),
1190            roaming.join("CLAUDE-3P"),
1191            local.join("AnthropicClaude").join("app-1.0"),
1192            roaming.clone(),
1193        ] {
1194            assert!(refused(path.clone()), "{}", path.display());
1195        }
1196        for path in [
1197            roaming.join("Claudette"),
1198            local.join("Claude"),
1199            local.join("Pitboard"),
1200        ] {
1201            assert!(!refused(path.clone()), "{}", path.display());
1202        }
1203    }
1204
1205    // Compared by component, so `Pitboard` and `Pitboard App` are never inside one another.
1206    #[test]
1207    fn the_windows_apps_folders_are_refused_in_any_spelling() {
1208        let local = PathBuf::from("/p/AppData/Local");
1209        let places = pitboard_app_on_windows(std::slice::from_ref(&local));
1210        let refused = |path: PathBuf| places.iter().any(|place| lies_in(&path, place));
1211        for path in [
1212            local.join("Pitboard App").join("WebView2"),
1213            local.join("pitboard app debug").join("x"),
1214            local.join("PITBOARD APP"),
1215            local.clone(),
1216        ] {
1217            assert!(refused(path.clone()), "{}", path.display());
1218        }
1219        for path in [
1220            local.join("Pitboard Apps"),
1221            local.join("Pitboard"),
1222            local.join("Pitboard").join("App"),
1223            local.join("Pitboard App Debugger"),
1224        ] {
1225            assert!(!refused(path.clone()), "{}", path.display());
1226        }
1227    }
1228
1229    #[test]
1230    #[cfg(windows)]
1231    fn a_verbatim_path_lies_where_its_plain_spelling_does() {
1232        let lies = |path: &str, place: &str| lies_in(Path::new(path), Path::new(place));
1233        assert!(lies(
1234            r"\\?\C:\Users\Dana\Library\x",
1235            r"c:\users\dana\library"
1236        ));
1237        assert!(lies(
1238            r"C:\Users\Dana\Library\x",
1239            r"\\?\c:\users\dana\library"
1240        ));
1241        assert!(!lies(
1242            r"\\?\D:\Users\Dana\Library\x",
1243            r"C:\Users\Dana\Library"
1244        ));
1245    }
1246
1247    // launchd, a systemd user timer and Task Scheduler.
1248    #[test]
1249    fn a_scheduler_is_offered_where_pitboard_writes_for_one() {
1250        assert!(current().scheduler().is_some());
1251    }
1252
1253    /// This system's host reads sudo from the environment the context was read from, which
1254    /// is where sudo says it, and root from the process itself. Set but empty, `SUDO_UID`
1255    /// says nothing, as every variable Pitboard reads that way.
1256    #[test]
1257    fn the_host_reads_sudo_from_the_context_and_root_from_the_process() {
1258        let under = |value: &str| {
1259            let env: Environment = [("HOME", "/Users/x"), ("SUDO_UID", value)]
1260                .into_iter()
1261                .collect();
1262            let ctx = Context::for_command_line(&env);
1263            ctx.host().elevation(&ctx)
1264        };
1265        assert_eq!(
1266            under("501"),
1267            match OS {
1268                Os::MacOs | Os::Linux => Elevation::Elevated { why: "with sudo" },
1269                Os::Windows => user::elevation(false),
1270            }
1271        );
1272        let ctx = Context::for_unit_test();
1273        assert_eq!(
1274            ctx.host().elevation(&ctx),
1275            user::elevation(false),
1276            "withheld from a unit test, so only root is read"
1277        );
1278        assert_eq!(under(""), user::elevation(false));
1279    }
1280
1281    // Instant reads the same clock on macOS and Linux, and a clock that counts sleep on Windows.
1282    #[test]
1283    fn time_awake_moves_with_this_process_while_it_is_awake() {
1284        let (awake_at, instant_at) = (awake(), std::time::Instant::now());
1285        std::thread::sleep(std::time::Duration::from_millis(50));
1286        let (passed, measured) = (awake() - awake_at, instant_at.elapsed());
1287        // Wide enough for a busy runner, narrow enough to catch a unit read ten times off.
1288        assert!(
1289            passed > measured / 2 && passed < measured * 2,
1290            "{passed:?} against {measured:?}"
1291        );
1292    }
1293
1294    #[test]
1295    fn interrupt_time_is_in_hundreds_of_nanoseconds() {
1296        use std::time::Duration;
1297        assert_eq!(from_interrupt_time(10_000_000), Duration::from_secs(1));
1298        assert_eq!(from_interrupt_time(1), Duration::from_nanos(100));
1299        assert_eq!(
1300            from_interrupt_time(36_000_000_015),
1301            Duration::new(3600, 1_500)
1302        );
1303    }
1304
1305    // 22631 is Windows 11 23H2, and 19045 Windows 10 22H2.
1306    #[test]
1307    fn the_floor_is_windows_11_24h2_with_server_2025_counted() {
1308        for met in [(10, 0, 26100), (10, 0, 26200), (10, 0, 27000), (11, 0, 100)] {
1309            assert_eq!(windows_floor(Some(met)), Floor::Met, "{met:?}");
1310        }
1311        for (version, build) in [
1312            ((10, 0, 26099), 26099),
1313            ((10, 0, 22631), 22631),
1314            ((10, 0, 19045), 19045),
1315            ((6, 3, 9600), 9600),
1316        ] {
1317            assert_eq!(
1318                windows_floor(Some(version)),
1319                Floor::Below { build },
1320                "{version:?}"
1321            );
1322        }
1323        assert_eq!(windows_floor(None), Floor::Unknown);
1324        assert_eq!(WINDOWS_FLOOR, 26100);
1325    }
1326
1327    #[test]
1328    fn the_floor_is_met_on_the_systems_the_tests_run_on() {
1329        assert_eq!(current().floor(), Floor::Met);
1330    }
1331}