1use std::path::{Path, PathBuf};
13
14use anyhow::{bail, Context, Result};
15
16pub const MAX_SOCKET_PATH_LEN: usize = 104;
19
20pub fn runtime_dir() -> Result<PathBuf> {
22 let base = dirs::data_dir().context("could not determine the user data directory")?;
23 Ok(base.join("omni-dev"))
24}
25
26pub fn socket_path() -> Result<PathBuf> {
28 Ok(runtime_dir()?.join("daemon.sock"))
29}
30
31pub fn token_path() -> Result<PathBuf> {
34 Ok(runtime_dir()?.join("bridge.token"))
35}
36
37pub fn worktrees_polling_path() -> Result<PathBuf> {
43 Ok(runtime_dir()?.join("worktrees-polling.json"))
44}
45
46pub fn worktrees_pr_cache_path() -> Result<PathBuf> {
54 Ok(runtime_dir()?.join("worktrees-pr-cache.json"))
55}
56
57pub fn token_path_for_socket(socket: &Path) -> PathBuf {
61 socket.parent().map_or_else(
62 || PathBuf::from("bridge.token"),
63 |dir| dir.join("bridge.token"),
64 )
65}
66
67pub fn log_path_for_socket(socket: &Path) -> PathBuf {
73 socket
74 .parent()
75 .map_or_else(|| PathBuf::from("daemon.log"), |dir| dir.join("daemon.log"))
76}
77
78pub fn ensure_dir_0700(dir: &Path) -> Result<()> {
86 let mut builder = std::fs::DirBuilder::new();
87 builder.recursive(true);
88 #[cfg(unix)]
89 {
90 use std::os::unix::fs::DirBuilderExt;
91 builder.mode(0o700);
92 }
93 builder
94 .create(dir)
95 .with_context(|| format!("failed to create directory {}", dir.display()))?;
96 #[cfg(unix)]
97 {
98 use std::os::unix::fs::PermissionsExt;
99 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
100 .with_context(|| format!("failed to set 0700 on {}", dir.display()))?;
101 }
102 Ok(())
103}
104
105pub fn write_file_0600(path: &Path, contents: &[u8]) -> Result<()> {
113 use std::io::Write;
114
115 let mut options = std::fs::OpenOptions::new();
116 options.write(true).create(true).truncate(true);
117 #[cfg(unix)]
118 {
119 use std::os::unix::fs::OpenOptionsExt;
120 options.mode(0o600);
121 }
122 let mut file = options
123 .open(path)
124 .with_context(|| format!("failed to create file {}", path.display()))?;
125 ensure_handle_0600(&file)
126 .with_context(|| format!("failed to set 0600 on {}", path.display()))?;
127 file.write_all(contents)
128 .with_context(|| format!("failed to write file {}", path.display()))?;
129 Ok(())
130}
131
132pub fn ensure_handle_0600(file: &std::fs::File) -> Result<()> {
138 #[cfg(unix)]
139 {
140 use std::os::unix::fs::PermissionsExt;
141 let mode = file
142 .metadata()
143 .context("failed to read file metadata")?
144 .permissions()
145 .mode();
146 if mode & 0o077 != 0 {
147 file.set_permissions(std::fs::Permissions::from_mode(0o600))
148 .context("failed to set 0600 on open file")?;
149 }
150 }
151 #[cfg(not(unix))]
152 {
153 let _ = file;
154 }
155 Ok(())
156}
157
158pub fn set_file_0600(path: &Path) -> Result<()> {
160 #[cfg(unix)]
161 {
162 use std::os::unix::fs::PermissionsExt;
163 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
164 .with_context(|| format!("failed to set 0600 on {}", path.display()))?;
165 }
166 #[cfg(not(unix))]
167 {
168 let _ = path;
169 }
170 Ok(())
171}
172
173pub fn check_socket_path_len(path: &Path) -> Result<()> {
176 let len = path.as_os_str().len();
177 if len >= MAX_SOCKET_PATH_LEN {
178 bail!(
179 "socket path is {len} bytes, exceeding the {MAX_SOCKET_PATH_LEN}-byte limit: {} — \
180 pass a shorter --socket path",
181 path.display()
182 );
183 }
184 Ok(())
185}
186
187#[cfg(test)]
188#[allow(clippy::unwrap_used, clippy::expect_used)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn default_paths_sit_under_the_runtime_dir() {
194 let rt = runtime_dir().unwrap();
195 assert!(socket_path().unwrap().starts_with(&rt));
196 assert!(token_path().unwrap().starts_with(&rt));
197 let polling = worktrees_polling_path().unwrap();
198 assert!(polling.starts_with(&rt));
199 assert_eq!(polling.file_name().unwrap(), "worktrees-polling.json");
200 }
201
202 #[test]
203 fn token_sits_beside_its_socket() {
204 assert_eq!(
205 token_path_for_socket(Path::new("/tmp/x/daemon.sock")),
206 Path::new("/tmp/x/bridge.token")
207 );
208 assert_eq!(
210 token_path_for_socket(Path::new("daemon.sock")),
211 Path::new("bridge.token")
212 );
213 }
214
215 #[test]
216 fn log_sits_beside_its_socket() {
217 assert_eq!(
218 log_path_for_socket(Path::new("/tmp/x/daemon.sock")),
219 Path::new("/tmp/x/daemon.log")
220 );
221 assert_eq!(
223 log_path_for_socket(Path::new("daemon.sock")),
224 Path::new("daemon.log")
225 );
226 }
227
228 #[test]
229 fn socket_path_length_guard() {
230 check_socket_path_len(Path::new("/tmp/short.sock")).unwrap();
231 let too_long = PathBuf::from(format!("/{}", "a".repeat(MAX_SOCKET_PATH_LEN)));
232 assert!(check_socket_path_len(&too_long).is_err());
233 }
234
235 #[test]
236 fn write_file_0600_creates_owner_only() {
237 let dir = tempfile::tempdir().unwrap();
238 let file = dir.path().join("fresh.token");
239 write_file_0600(&file, b"secret").unwrap();
240 assert_eq!(std::fs::read(&file).unwrap(), b"secret");
241 #[cfg(unix)]
242 {
243 use std::os::unix::fs::PermissionsExt;
244 assert_eq!(
245 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
246 0o600
247 );
248 }
249 }
250
251 #[cfg(unix)]
252 #[test]
253 fn write_file_0600_retightens_preexisting_loose_file() {
254 use std::os::unix::fs::PermissionsExt;
255 let dir = tempfile::tempdir().unwrap();
256 let file = dir.path().join("stale.token");
257 std::fs::write(&file, "old-secret-with-longer-content").unwrap();
258 std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
259 write_file_0600(&file, b"new").unwrap();
260 assert_eq!(std::fs::read(&file).unwrap(), b"new");
261 assert_eq!(
262 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
263 0o600
264 );
265 }
266
267 #[test]
268 fn ensure_dir_and_file_perms() {
269 let dir = tempfile::tempdir().unwrap();
270 let sub = dir.path().join("run");
271 ensure_dir_0700(&sub).unwrap();
272 assert!(sub.is_dir());
273 let file = sub.join("k");
274 std::fs::write(&file, "x").unwrap();
275 set_file_0600(&file).unwrap();
276 #[cfg(unix)]
277 {
278 use std::os::unix::fs::PermissionsExt;
279 assert_eq!(
280 std::fs::metadata(&sub).unwrap().permissions().mode() & 0o777,
281 0o700
282 );
283 assert_eq!(
284 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
285 0o600
286 );
287 }
288 }
289}