1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Os {
89 MacOs,
90 Linux,
91 Windows,
92}
93
94pub const OS: Os = os::OS;
96
97pub(crate) const SECURITY: &str = "/usr/bin/security";
100
101impl Os {
102 pub(crate) fn secrets_tool(self) -> Option<&'static str> {
105 match self {
106 Os::MacOs => Some(SECURITY),
107 Os::Linux | Os::Windows => None,
109 }
110 }
111
112 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 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 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 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 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 pub fn home_variable(self) -> &'static str {
228 match self {
229 Os::MacOs | Os::Linux => "HOME",
230 Os::Windows => "USERPROFILE",
231 }
232 }
233
234 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 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
257pub 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
272pub 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
321pub(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#[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#[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#[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 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#[cfg(all(not(windows), any(test, feature = "test-support")))]
404pub(crate) fn lasting_environment(_name: &str) -> Vec<String> {
405 Vec::new()
406}
407
408#[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#[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#[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#[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#[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
500pub(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#[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 Said(String),
525 Unknown,
527 Late,
531}
532
533pub(crate) fn login_path(env: &Environment) -> LoginPath {
535 os::login_path(env)
536}
537
538#[derive(Debug, Clone, PartialEq, Eq)]
540pub(crate) struct Access {
541 pub(crate) shared: bool,
543 pub(crate) described: String,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum Private {
550 Yes,
551 Shared,
552 NotOwn,
553 Link,
554 NotADirectory,
555 Missing,
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
560pub(crate) enum Kind {
561 Directory,
562 File,
563 Any,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub enum Elevation {
573 Normal,
575 Elevated { why: &'static str },
578 Unknown,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub enum Floor {
584 Met,
585 Below { build: u32 },
586 Unknown,
587}
588
589#[derive(Debug, Clone, Default, PartialEq, Eq)]
591pub(crate) struct WindowsSettings {
592 pub(crate) wsl: Vec<String>,
594 pub(crate) proxy: Option<String>,
596 pub(crate) admin_approval: Option<bool>,
598}
599
600pub(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 pub(crate) path: PathBuf,
619 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 pub placed: bool,
631}
632
633pub(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 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
687impl 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 Unsettled(String),
705 Unknown(String),
707 Unreadable(String),
708 Unsupported,
709}
710
711#[cfg(any(test, feature = "test-support"))]
712pub(crate) use os::write_jar;
713
714#[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
723pub(crate) trait Host: Send + Sync + std::fmt::Debug {
735 fn foreign_secrets(&self, ctx: &Context, account: &str) -> Option<Box<dyn RawStore>>;
743
744 fn file(&self, path: PathBuf) -> Box<dyn RawStore>;
746
747 fn vault(&self, ctx: &Context) -> Box<dyn RawStore>;
751
752 fn vault_is_shared(&self) -> bool;
757
758 fn listing(&self) -> std::result::Result<Vec<Listed>, String>;
760
761 fn processes(&self, program: &str) -> Option<Vec<Process>> {
764 named(OS, self.listing().ok()?, program)
765 }
766
767 fn cookies(
769 &self,
770 jar: &Path,
771 hosts: &[&str],
772 names: &[&str],
773 ) -> std::result::Result<Jar, JarError>;
774
775 fn app_version(&self, bundle: &Path) -> Option<String>;
777 fn app_id(&self, bundle: &Path) -> Option<String>;
778
779 fn scheduler(&self) -> Option<&dyn Scheduler>;
782
783 fn elevation(&self, ctx: &Context) -> Elevation;
788
789 fn floor(&self) -> Floor;
790
791 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 fn managed_preference(&self, domain: &str, key: &str) -> Administered;
809
810 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 fn policy(&self, _hive: Hive, _key: &str, _value: &str) -> Administered {
821 Administered::Unset
822 }
823
824 fn program_data(&self) -> Option<PathBuf> {
826 None
827 }
828
829 fn credential_names(&self, _prefix: &str) -> std::result::Result<Vec<String>, String> {
832 Ok(Vec::new())
833 }
834
835 fn user_environment(&self, _name: &str) -> Administered {
838 Administered::Unset
839 }
840
841 fn machine_environment(&self, _name: &str) -> Administered {
845 Administered::Unset
846 }
847
848 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 fn windows_settings(&self) -> WindowsSettings {
860 WindowsSettings::default()
861 }
862
863 fn kernel_is_wsl(&self) -> bool {
866 false
867 }
868
869 fn on_a_windows_drive(&self, _path: &Path) -> bool {
872 false
873 }
874}
875
876pub(crate) trait Scheduler: Send + Sync + std::fmt::Debug {
883 fn location(&self, ctx: &Context) -> crate::schedule::Location;
885
886 fn installed(&self, ctx: &Context) -> bool;
888
889 fn program(&self, ctx: &Context) -> Option<PathBuf>;
893
894 fn environment(&self, ctx: &Context) -> Vec<(String, String)>;
898
899 fn put(
908 &self,
909 ctx: &Context,
910 permit: Permit,
911 program: &Path,
912 environment: &[(String, String)],
913 ) -> Result<()>;
914
915 fn remove(&self, ctx: &Context, permit: Permit) -> Result<bool>;
917
918 fn started_this_process(&self, said: Option<&str>) -> bool;
921
922 fn read_by_its_runs(&self, _ctx: &Context) -> Vec<(String, String)> {
925 Vec::new()
926 }
927
928 fn task(&self, _ctx: &Context) -> Option<task::Task> {
930 None
931 }
932}
933
934pub 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
945pub(crate) fn current() -> Arc<dyn Host> {
947 os::host()
948}
949
950pub(crate) fn current_program() -> std::io::Result<PathBuf> {
952 os::current_program()
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
1249 fn a_scheduler_is_offered_where_pitboard_writes_for_one() {
1250 assert!(current().scheduler().is_some());
1251 }
1252
1253 #[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 #[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 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 #[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}