Mountain/IPC/DevLog/AppDataPrefix.rs
1
2//! Resolve the Tauri app-data prefix for THIS profile so logs
3//! and aliasing pick the right `~/Library/Application Support/
4//! land.editor.*.mountain` directory. The detection walks the
5//! Application Support tree, prefers a strict suffix match
6//! against the binary signature, falls back to the first
7//! `*.mountain` candidate so a mismatch still produces a
8//! usable path.
9
10use std::sync::{Mutex, OnceLock};
11
12// Two-phase resolution:
13// • `RESOLVED` is set permanently once we find a real prefix.
14// • `FAILED_ONCE` records whether the first attempt returned None so
15// subsequent writes can retry after Tauri has created the directory,
16// rather than caching None forever and routing all logs to /tmp.
17static RESOLVED:OnceLock<String> = OnceLock::new();
18
19static RETRY:Mutex<bool> = Mutex::new(true);
20
21pub fn Fn() -> Option<&'static str> {
22 if let Some(S) = RESOLVED.get() {
23 return Some(S.as_str());
24 }
25
26 // Fast-path: guard without taking the mutex if we already have a result.
27 let Ok(mut Guard) = RETRY.try_lock() else {
28 return None;
29 };
30
31 if !*Guard {
32 return None;
33 }
34
35 if let Some(Prefix) = DetectAppDataPrefix() {
36 // RESOLVED may already be set by a concurrent caller - that's fine.
37 let _ = RESOLVED.set(Prefix);
38
39 *Guard = false;
40 return RESOLVED.get().map(String::as_str);
41 }
42
43 // Not found yet - leave RETRY=true so the next write retries.
44 None
45}
46
47fn BinarySignature() -> String {
48 let PackageName = env!("CARGO_PKG_NAME");
49
50 let Segments:Vec<&str> = PackageName.split('_').collect();
51
52 let Take = Segments.len().min(4);
53
54 let Start = Segments.len().saturating_sub(Take);
55
56 Segments[Start..]
57 .iter()
58 .flat_map(|Segment| SplitPascalCaseIntoWords(Segment))
59 .collect::<Vec<String>>()
60 .join(".")
61 .to_ascii_lowercase()
62}
63
64fn SplitPascalCaseIntoWords(Segment:&str) -> Vec<String> {
65 let mut Words:Vec<String> = Vec::new();
66
67 let mut Current = String::new();
68
69 let mut PrevWasUpper = false;
70
71 let mut PrevWasDigit = false;
72
73 for Ch in Segment.chars() {
74 let IsUpper = Ch.is_ascii_uppercase();
75
76 let IsDigit = Ch.is_ascii_digit();
77
78 let NeedBreak =
79 !Current.is_empty() && ((IsUpper && !PrevWasUpper) || (IsDigit != PrevWasDigit && !Current.is_empty()));
80
81 if NeedBreak {
82 Words.push(std::mem::take(&mut Current));
83 }
84
85 Current.push(Ch);
86
87 PrevWasUpper = IsUpper;
88
89 PrevWasDigit = IsDigit;
90 }
91
92 if !Current.is_empty() {
93 Words.push(Current);
94 }
95
96 Words.into_iter().filter(|Word| !Word.is_empty()).collect()
97}
98
99fn DetectAppDataPrefix() -> Option<String> {
100 let Home = std::env::var("HOME").ok()?;
101
102 let Base = format!("{}/Library/Application Support", Home);
103
104 let Signature = BinarySignature();
105
106 let mut FirstMatchingMountain:Option<String> = None;
107
108 if let Ok(Entries) = std::fs::read_dir(&Base) {
109 for Entry in Entries.flatten() {
110 let Name = Entry.file_name();
111
112 let Name = Name.to_string_lossy().into_owned();
113
114 if !Name.starts_with("land.editor.") || !Name.contains("mountain") {
115 continue;
116 }
117
118 if Name.ends_with(&Signature) {
119 return Some(format!("{}/{}", Base, Name));
120 }
121
122 if FirstMatchingMountain.is_none() {
123 FirstMatchingMountain = Some(format!("{}/{}", Base, Name));
124 }
125 }
126 }
127
128 FirstMatchingMountain
129}