1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct VncEndpoint {
12 pub host: String,
13 pub port: u16,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub novnc_url: Option<String>,
16}
17
18#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19#[serde(tag = "transport", rename_all = "camelCase")]
20pub enum AgentEndpointDescriptor {
21 #[serde(rename = "unix")]
22 #[serde(rename_all = "camelCase")]
23 Unix {
24 app_id: String,
25 pid: u32,
26 path: PathBuf,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 token: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 vnc: Option<VncEndpoint>,
31 },
32 #[serde(rename = "tcp")]
33 #[serde(rename_all = "camelCase")]
34 Tcp {
35 app_id: String,
36 pid: u32,
37 host: String,
38 port: u16,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 token: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 vnc: Option<VncEndpoint>,
43 },
44}
45
46impl AgentEndpointDescriptor {
47 pub fn unix(app_id: impl Into<String>, pid: u32, path: PathBuf) -> Self {
48 Self::Unix {
49 app_id: app_id.into(),
50 pid,
51 path,
52 token: None,
53 vnc: None,
54 }
55 }
56
57 pub fn tcp(app_id: impl Into<String>, pid: u32, host: impl Into<String>, port: u16) -> Self {
58 Self::Tcp {
59 app_id: app_id.into(),
60 pid,
61 host: host.into(),
62 port,
63 token: None,
64 vnc: None,
65 }
66 }
67
68 pub fn app_id(&self) -> &str {
69 match self {
70 Self::Unix { app_id, .. } | Self::Tcp { app_id, .. } => app_id,
71 }
72 }
73
74 pub fn token(&self) -> Option<&str> {
76 match self {
77 Self::Unix { token, .. } | Self::Tcp { token, .. } => token.as_deref(),
78 }
79 }
80
81 pub fn with_token(mut self, value: Option<String>) -> Self {
83 match &mut self {
84 Self::Unix { token, .. } | Self::Tcp { token, .. } => *token = value,
85 }
86 self
87 }
88
89 pub fn vnc(&self) -> Option<&VncEndpoint> {
91 match self {
92 Self::Unix { vnc, .. } | Self::Tcp { vnc, .. } => vnc.as_ref(),
93 }
94 }
95
96 pub fn with_vnc(mut self, endpoint: Option<VncEndpoint>) -> Self {
98 match &mut self {
99 Self::Unix { vnc, .. } | Self::Tcp { vnc, .. } => *vnc = endpoint,
100 }
101 self
102 }
103}
104
105#[derive(Debug, thiserror::Error)]
106pub enum EndpointRegistryError {
107 #[error("endpoint registry not found for app: {0}")]
108 NotFound(String),
109 #[error("endpoint registry I/O error at {path:?}: {source}")]
110 Io {
111 path: PathBuf,
112 #[source]
113 source: std::io::Error,
114 },
115 #[error("invalid endpoint registry at {path:?}: {source}")]
116 Json {
117 path: PathBuf,
118 #[source]
119 source: serde_json::Error,
120 },
121}
122
123pub fn endpoint_runtime_dir(app_id: &str, runtime_base: Option<PathBuf>) -> PathBuf {
124 runtime_base
125 .unwrap_or_else(default_runtime_base)
126 .join("tauri-agent")
127 .join(safe_app_id(app_id))
128}
129
130pub fn endpoint_registry_path(app_id: &str, runtime_base: Option<PathBuf>) -> PathBuf {
131 endpoint_runtime_dir(app_id, runtime_base).join("endpoint.json")
132}
133
134pub fn write_endpoint_registry(
135 descriptor: &AgentEndpointDescriptor,
136 runtime_base: Option<PathBuf>,
137) -> Result<(), EndpointRegistryError> {
138 let runtime_dir = endpoint_runtime_dir(descriptor.app_id(), runtime_base.clone());
139 std::fs::create_dir_all(&runtime_dir).map_err(|source| EndpointRegistryError::Io {
140 path: runtime_dir.clone(),
141 source,
142 })?;
143
144 let path = endpoint_registry_path(descriptor.app_id(), runtime_base);
145 let contents =
146 serde_json::to_string_pretty(descriptor).map_err(|source| EndpointRegistryError::Json {
147 path: path.clone(),
148 source,
149 })?;
150
151 let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
154 let write_result = std::fs::write(&tmp, format!("{contents}\n"))
155 .map_err(|source| EndpointRegistryError::Io {
156 path: tmp.clone(),
157 source,
158 })
159 .and_then(|()| restrict_registry_permissions(&tmp))
160 .and_then(|()| {
161 std::fs::rename(&tmp, &path).map_err(|source| EndpointRegistryError::Io {
162 path: path.clone(),
163 source,
164 })
165 });
166 if write_result.is_err() {
167 let _ = std::fs::remove_file(&tmp);
168 }
169 write_result
170}
171
172#[cfg(unix)]
176fn restrict_registry_permissions(path: &std::path::Path) -> Result<(), EndpointRegistryError> {
177 use std::os::unix::fs::PermissionsExt;
178 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|source| {
179 EndpointRegistryError::Io {
180 path: path.to_path_buf(),
181 source,
182 }
183 })
184}
185
186#[cfg(not(unix))]
187fn restrict_registry_permissions(_path: &std::path::Path) -> Result<(), EndpointRegistryError> {
188 Ok(())
189}
190
191pub fn read_endpoint_registry(
192 app_id: &str,
193 runtime_base: Option<PathBuf>,
194) -> Result<AgentEndpointDescriptor, EndpointRegistryError> {
195 let path = endpoint_registry_path(app_id, runtime_base);
196 let contents = std::fs::read_to_string(&path).map_err(|source| {
197 if source.kind() == std::io::ErrorKind::NotFound {
198 EndpointRegistryError::NotFound(app_id.to_string())
199 } else {
200 EndpointRegistryError::Io {
201 path: path.clone(),
202 source,
203 }
204 }
205 })?;
206
207 serde_json::from_str(&contents).map_err(|source| EndpointRegistryError::Json { path, source })
208}
209
210pub fn remove_endpoint_registry(
211 app_id: &str,
212 runtime_base: Option<PathBuf>,
213) -> Result<(), EndpointRegistryError> {
214 let path = endpoint_registry_path(app_id, runtime_base);
215 match std::fs::remove_file(&path) {
216 Ok(()) => Ok(()),
217 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
218 Err(source) => Err(EndpointRegistryError::Io { path, source }),
219 }
220}
221
222fn default_runtime_base() -> PathBuf {
223 std::env::var_os("XDG_RUNTIME_DIR")
224 .or_else(|| std::env::var_os("TMPDIR"))
225 .or_else(|| std::env::var_os("TEMP"))
226 .or_else(|| std::env::var_os("TMP"))
227 .map(PathBuf::from)
228 .unwrap_or_else(std::env::temp_dir)
229}
230
231fn safe_app_id(app_id: &str) -> String {
232 let sanitized: String = app_id
233 .chars()
234 .map(|ch| {
235 if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
236 ch
237 } else {
238 '_'
239 }
240 })
241 .collect();
242
243 if sanitized.is_empty() {
246 return "_".to_string();
247 }
248 if sanitized.chars().all(|ch| ch == '.') {
249 return sanitized.replace('.', "_");
250 }
251 sanitized
252}