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 token_path_for_socket(socket: &Path) -> PathBuf {
41 socket.parent().map_or_else(
42 || PathBuf::from("bridge.token"),
43 |dir| dir.join("bridge.token"),
44 )
45}
46
47pub fn log_path_for_socket(socket: &Path) -> PathBuf {
53 socket
54 .parent()
55 .map_or_else(|| PathBuf::from("daemon.log"), |dir| dir.join("daemon.log"))
56}
57
58pub fn ensure_dir_0700(dir: &Path) -> Result<()> {
66 let mut builder = std::fs::DirBuilder::new();
67 builder.recursive(true);
68 #[cfg(unix)]
69 {
70 use std::os::unix::fs::DirBuilderExt;
71 builder.mode(0o700);
72 }
73 builder
74 .create(dir)
75 .with_context(|| format!("failed to create directory {}", dir.display()))?;
76 #[cfg(unix)]
77 {
78 use std::os::unix::fs::PermissionsExt;
79 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
80 .with_context(|| format!("failed to set 0700 on {}", dir.display()))?;
81 }
82 Ok(())
83}
84
85pub fn write_file_0600(path: &Path, contents: &[u8]) -> Result<()> {
93 use std::io::Write;
94
95 let mut options = std::fs::OpenOptions::new();
96 options.write(true).create(true).truncate(true);
97 #[cfg(unix)]
98 {
99 use std::os::unix::fs::OpenOptionsExt;
100 options.mode(0o600);
101 }
102 let mut file = options
103 .open(path)
104 .with_context(|| format!("failed to create file {}", path.display()))?;
105 ensure_handle_0600(&file)
106 .with_context(|| format!("failed to set 0600 on {}", path.display()))?;
107 file.write_all(contents)
108 .with_context(|| format!("failed to write file {}", path.display()))?;
109 Ok(())
110}
111
112pub fn ensure_handle_0600(file: &std::fs::File) -> Result<()> {
118 #[cfg(unix)]
119 {
120 use std::os::unix::fs::PermissionsExt;
121 let mode = file
122 .metadata()
123 .context("failed to read file metadata")?
124 .permissions()
125 .mode();
126 if mode & 0o077 != 0 {
127 file.set_permissions(std::fs::Permissions::from_mode(0o600))
128 .context("failed to set 0600 on open file")?;
129 }
130 }
131 #[cfg(not(unix))]
132 {
133 let _ = file;
134 }
135 Ok(())
136}
137
138pub fn set_file_0600(path: &Path) -> Result<()> {
140 #[cfg(unix)]
141 {
142 use std::os::unix::fs::PermissionsExt;
143 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
144 .with_context(|| format!("failed to set 0600 on {}", path.display()))?;
145 }
146 #[cfg(not(unix))]
147 {
148 let _ = path;
149 }
150 Ok(())
151}
152
153pub fn check_socket_path_len(path: &Path) -> Result<()> {
156 let len = path.as_os_str().len();
157 if len >= MAX_SOCKET_PATH_LEN {
158 bail!(
159 "socket path is {len} bytes, exceeding the {MAX_SOCKET_PATH_LEN}-byte limit: {} — \
160 pass a shorter --socket path",
161 path.display()
162 );
163 }
164 Ok(())
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used, clippy::expect_used)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn default_paths_sit_under_the_runtime_dir() {
174 let rt = runtime_dir().unwrap();
175 assert!(socket_path().unwrap().starts_with(&rt));
176 assert!(token_path().unwrap().starts_with(&rt));
177 }
178
179 #[test]
180 fn token_sits_beside_its_socket() {
181 assert_eq!(
182 token_path_for_socket(Path::new("/tmp/x/daemon.sock")),
183 Path::new("/tmp/x/bridge.token")
184 );
185 assert_eq!(
187 token_path_for_socket(Path::new("daemon.sock")),
188 Path::new("bridge.token")
189 );
190 }
191
192 #[test]
193 fn log_sits_beside_its_socket() {
194 assert_eq!(
195 log_path_for_socket(Path::new("/tmp/x/daemon.sock")),
196 Path::new("/tmp/x/daemon.log")
197 );
198 assert_eq!(
200 log_path_for_socket(Path::new("daemon.sock")),
201 Path::new("daemon.log")
202 );
203 }
204
205 #[test]
206 fn socket_path_length_guard() {
207 check_socket_path_len(Path::new("/tmp/short.sock")).unwrap();
208 let too_long = PathBuf::from(format!("/{}", "a".repeat(MAX_SOCKET_PATH_LEN)));
209 assert!(check_socket_path_len(&too_long).is_err());
210 }
211
212 #[test]
213 fn write_file_0600_creates_owner_only() {
214 let dir = tempfile::tempdir().unwrap();
215 let file = dir.path().join("fresh.token");
216 write_file_0600(&file, b"secret").unwrap();
217 assert_eq!(std::fs::read(&file).unwrap(), b"secret");
218 #[cfg(unix)]
219 {
220 use std::os::unix::fs::PermissionsExt;
221 assert_eq!(
222 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
223 0o600
224 );
225 }
226 }
227
228 #[cfg(unix)]
229 #[test]
230 fn write_file_0600_retightens_preexisting_loose_file() {
231 use std::os::unix::fs::PermissionsExt;
232 let dir = tempfile::tempdir().unwrap();
233 let file = dir.path().join("stale.token");
234 std::fs::write(&file, "old-secret-with-longer-content").unwrap();
235 std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
236 write_file_0600(&file, b"new").unwrap();
237 assert_eq!(std::fs::read(&file).unwrap(), b"new");
238 assert_eq!(
239 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
240 0o600
241 );
242 }
243
244 #[test]
245 fn ensure_dir_and_file_perms() {
246 let dir = tempfile::tempdir().unwrap();
247 let sub = dir.path().join("run");
248 ensure_dir_0700(&sub).unwrap();
249 assert!(sub.is_dir());
250 let file = sub.join("k");
251 std::fs::write(&file, "x").unwrap();
252 set_file_0600(&file).unwrap();
253 #[cfg(unix)]
254 {
255 use std::os::unix::fs::PermissionsExt;
256 assert_eq!(
257 std::fs::metadata(&sub).unwrap().permissions().mode() & 0o777,
258 0o700
259 );
260 assert_eq!(
261 std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
262 0o600
263 );
264 }
265 }
266}