1#[cfg(unix)]
3use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
4use std::{
5 fs::{File, Metadata},
6 io,
7 path::Path,
8 process::Command,
9};
10mod network;
11#[cfg(windows)]
12mod windows;
13pub use network::local_ipv4_addresses;
14
15#[cfg(unix)]
16pub const NOFOLLOW: i32 = libc::O_NOFOLLOW;
17#[cfg(windows)]
18pub const NOFOLLOW: i32 = 0x00200000; pub struct OpenOptions {
22 inner: std::fs::OpenOptions,
23 private: bool,
24 read: bool,
25 write: bool,
26 append: bool,
27 create: bool,
28 create_new: bool,
29 truncate: bool,
30}
31impl Default for OpenOptions {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36impl OpenOptions {
37 pub fn new() -> Self {
38 Self {
39 inner: std::fs::OpenOptions::new(),
40 private: false,
41 read: false,
42 write: false,
43 append: false,
44 create: false,
45 create_new: false,
46 truncate: false,
47 }
48 }
49 pub fn read(&mut self, value: bool) -> &mut Self {
50 self.read = value;
51 self.inner.read(value);
52 self
53 }
54 pub fn write(&mut self, value: bool) -> &mut Self {
55 self.write = value;
56 self.inner.write(value);
57 self
58 }
59 pub fn append(&mut self, value: bool) -> &mut Self {
60 self.append = value;
61 self.inner.append(value);
62 self
63 }
64 pub fn create(&mut self, value: bool) -> &mut Self {
65 self.create = value;
66 self.inner.create(value);
67 self
68 }
69 pub fn create_new(&mut self, value: bool) -> &mut Self {
70 self.create_new = value;
71 self.inner.create_new(value);
72 self
73 }
74 pub fn truncate(&mut self, value: bool) -> &mut Self {
75 self.truncate = value;
76 self.inner.truncate(value);
77 self
78 }
79 pub fn mode(&mut self, mode: u32) -> &mut Self {
80 self.private = true;
81 #[cfg(unix)]
82 self.inner.mode(mode);
83 #[cfg(windows)]
84 let _ = mode;
85 self
86 }
87 pub fn custom_flags(&mut self, flags: i32) -> &mut Self {
88 #[cfg(unix)]
89 self.inner.custom_flags(flags | NOFOLLOW);
90 #[cfg(windows)]
91 {
92 use std::os::windows::fs::OpenOptionsExt;
93 self.inner.custom_flags(flags as u32 | NOFOLLOW as u32);
94 }
95 self
96 }
97 pub fn open(&mut self, path: impl AsRef<Path>) -> io::Result<File> {
98 self.custom_flags(NOFOLLOW);
99 #[cfg(unix)]
100 {
101 self.inner.open(path)
102 }
103 #[cfg(windows)]
104 {
105 windows::open(self, path.as_ref())
106 }
107 }
108}
109
110pub fn private_file(file: &File) -> io::Result<bool> {
111 let m = file.metadata()?;
112 #[cfg(unix)]
113 {
114 Ok(m.uid() == unsafe { libc::geteuid() } && m.mode() & 0o077 == 0)
115 }
116 #[cfg(windows)]
117 {
118 windows::private_file(file, &m)
119 }
120}
121pub fn private_path(path: &Path) -> io::Result<bool> {
122 #[cfg(unix)]
123 {
124 let m = std::fs::symlink_metadata(path)?;
125 Ok(!m.is_symlink() && m.uid() == unsafe { libc::geteuid() } && m.mode() & 0o077 == 0)
126 }
127 #[cfg(windows)]
128 {
129 windows::private_path(path)
130 }
131}
132pub fn owned_path(path: &Path) -> io::Result<bool> {
133 #[cfg(unix)]
134 {
135 let m = std::fs::symlink_metadata(path)?;
136 Ok(!m.is_symlink() && m.uid() == unsafe { libc::geteuid() })
137 }
138 #[cfg(windows)]
139 {
140 windows::owned_path(path)
141 }
142}
143pub fn protect(file: &File) -> io::Result<()> {
144 #[cfg(unix)]
145 {
146 use std::os::unix::fs::PermissionsExt;
147 file.set_permissions(std::fs::Permissions::from_mode(0o600))
148 }
149 #[cfg(windows)]
150 {
151 windows::protect(file)
152 }
153}
154pub fn protect_path(path: &Path, directory: bool) -> io::Result<()> {
155 if !owned_path(path)? {
156 return Err(io::Error::new(
157 io::ErrorKind::PermissionDenied,
158 "path must be owned by this user",
159 ));
160 }
161 #[cfg(unix)]
162 {
163 use std::os::unix::fs::PermissionsExt;
164 std::fs::set_permissions(
165 path,
166 std::fs::Permissions::from_mode(if directory { 0o700 } else { 0o600 }),
167 )
168 }
169 #[cfg(windows)]
170 {
171 let _ = directory;
172 windows::protect_path(path)
173 }
174}
175pub fn private_dir(path: &Path) -> io::Result<()> {
176 #[cfg(unix)]
177 {
178 use std::os::unix::fs::DirBuilderExt;
179 std::fs::DirBuilder::new()
180 .recursive(true)
181 .mode(0o700)
182 .create(path)?;
183 }
184 #[cfg(windows)]
185 windows::create_dir(path)?;
186 if !std::fs::symlink_metadata(path)?.is_dir() || !private_path(path)? {
187 return Err(io::Error::new(
188 io::ErrorKind::PermissionDenied,
189 "directory must be private, owned and not a link",
190 ));
191 }
192 Ok(())
193}
194
195pub struct PrivateTempDir {
198 path: std::path::PathBuf,
199}
200impl PrivateTempDir {
201 pub fn path(&self) -> &Path {
202 &self.path
203 }
204 pub fn keep(mut self) -> std::path::PathBuf {
205 std::mem::take(&mut self.path)
206 }
207}
208impl Drop for PrivateTempDir {
209 fn drop(&mut self) {
210 if !self.path.as_os_str().is_empty() {
211 let _ = std::fs::remove_dir_all(&self.path);
212 }
213 }
214}
215pub fn private_tempdir(prefix: &str) -> io::Result<PrivateTempDir> {
216 private_tempdir_in(&std::env::temp_dir(), prefix)
217}
218pub fn private_tempdir_in(parent: &Path, prefix: &str) -> io::Result<PrivateTempDir> {
219 let temporary = tempfile::Builder::new()
220 .prefix(prefix)
221 .rand_bytes(16)
222 .make_in(parent, |path| {
223 #[cfg(unix)]
224 {
225 use std::os::unix::fs::DirBuilderExt;
226 std::fs::DirBuilder::new().mode(0o700).create(path)?;
227 }
228 #[cfg(windows)]
229 windows::create_dir_new(path)?;
230 let directory = PrivateTempDir { path: path.into() };
231 if !private_path(path)? {
232 return Err(io::Error::new(
233 io::ErrorKind::PermissionDenied,
234 "scratch directory must be private and owned",
235 ));
236 }
237 Ok(directory)
238 })?;
239 let (directory, mut path) = temporary.into_parts();
240 path.disable_cleanup(true);
242 Ok(directory)
243}
244pub fn private_tempfile_in(parent: &Path, prefix: &str) -> io::Result<tempfile::NamedTempFile> {
245 tempfile::Builder::new()
246 .prefix(prefix)
247 .rand_bytes(16)
248 .make_in(parent, |path| {
249 OpenOptions::new()
250 .read(true)
251 .write(true)
252 .create_new(true)
253 .mode(0o600)
254 .open(path)
255 })
256}
257pub fn sync_dir(path: &Path) -> io::Result<()> {
259 #[cfg(unix)]
260 {
261 File::open(path)?.sync_all()
262 }
263 #[cfg(windows)]
264 {
265 let _ = path;
266 Ok(())
267 }
268}
269pub fn path_bytes(path: &Path) -> Vec<u8> {
270 #[cfg(unix)]
271 {
272 use std::os::unix::ffi::OsStrExt;
273 path.as_os_str().as_bytes().to_vec()
274 }
275 #[cfg(windows)]
276 {
277 use std::os::windows::ffi::OsStrExt;
278 path.as_os_str()
279 .encode_wide()
280 .flat_map(u16::to_le_bytes)
281 .collect()
282 }
283}
284pub fn user_key() -> String {
285 #[cfg(unix)]
286 {
287 unsafe { libc::geteuid() }.to_string()
288 }
289 #[cfg(windows)]
290 {
291 windows::user_sid_string().expect("current Windows user SID")
292 }
293}
294#[cfg(unix)]
295pub fn same_file(a: &Metadata, b: &Metadata) -> bool {
296 #[cfg(unix)]
297 {
298 a.dev() == b.dev() && a.ino() == b.ino()
299 }
300}
301pub fn detach(command: &mut Command) {
302 #[cfg(unix)]
303 {
304 use std::os::unix::process::CommandExt;
305 unsafe {
306 command.pre_exec(|| {
307 if libc::setsid() < 0 {
308 Err(io::Error::last_os_error())
309 } else {
310 Ok(())
311 }
312 });
313 }
314 }
315 #[cfg(windows)]
316 {
317 use std::os::windows::process::CommandExt;
318 command.creation_flags(0x00000200 | 0x00000008);
319 }
320}
321pub fn process_identity(pid: u32) -> Option<(String, std::path::PathBuf)> {
323 #[cfg(target_os = "linux")]
324 {
325 let text = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
326 let tail: Vec<_> = text.rsplit_once(')')?.1.split_whitespace().collect();
327 if tail.first() == Some(&"Z") {
328 return None;
329 }
330 Some((
331 tail.get(19)?.to_string(),
332 std::fs::read_link(format!("/proc/{pid}/exe")).ok()?,
333 ))
334 }
335 #[cfg(target_os = "macos")]
336 {
337 let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
338 let size = std::mem::size_of_val(&info) as i32;
339 if unsafe {
340 libc::proc_pidinfo(
341 pid as i32,
342 libc::PROC_PIDTBSDINFO,
343 0,
344 &mut info as *mut _ as *mut _,
345 size,
346 )
347 } != size
348 || info.pbi_status == 5
349 {
350 return None;
351 }
352 let mut path = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
353 let count =
354 unsafe { libc::proc_pidpath(pid as i32, path.as_mut_ptr().cast(), path.len() as u32) };
355 if count <= 0 {
356 return None;
357 }
358 let end = path.iter().position(|b| *b == 0)?;
359 use std::os::unix::ffi::OsStrExt;
360 Some((
361 format!("{}:{}", info.pbi_start_tvsec, info.pbi_start_tvusec),
362 Path::new(std::ffi::OsStr::from_bytes(&path[..end])).to_owned(),
363 ))
364 }
365 #[cfg(windows)]
366 {
367 windows::process_identity(pid)
368 }
369}
370
371pub fn publish_directory(source: &Path, destination: &Path) -> io::Result<()> {
373 #[cfg(unix)]
374 {
375 let a = std::ffi::CString::new(path_bytes(source))?;
376 let b = std::ffi::CString::new(path_bytes(destination))?;
377 #[cfg(target_os = "linux")]
378 let result = unsafe {
379 libc::renameat2(
380 libc::AT_FDCWD,
381 a.as_ptr(),
382 libc::AT_FDCWD,
383 b.as_ptr(),
384 libc::RENAME_NOREPLACE,
385 )
386 };
387 #[cfg(target_os = "macos")]
388 let result = unsafe { libc::renamex_np(a.as_ptr(), b.as_ptr(), libc::RENAME_EXCL) };
389 if result != 0 {
390 return Err(io::Error::last_os_error());
391 }
392 }
393 #[cfg(windows)]
394 windows::publish_directory(source, destination)?;
395 sync_dir(
396 destination
397 .parent()
398 .ok_or_else(|| io::Error::other("destination needs parent"))?,
399 )
400}
401pub fn is_link(metadata: &Metadata) -> bool {
403 #[cfg(unix)]
404 {
405 metadata.is_symlink()
406 }
407 #[cfg(windows)]
408 {
409 use std::os::windows::fs::MetadataExt;
410 metadata.file_attributes() & 0x400 != 0
411 }
412}
413pub fn same_path_file(path: &Path, file: &File) -> io::Result<bool> {
414 #[cfg(unix)]
415 {
416 Ok(same_file(
417 &std::fs::symlink_metadata(path)?,
418 &file.metadata()?,
419 ))
420 }
421 #[cfg(windows)]
422 {
423 windows::same_path_file(path, file)
424 }
425}
426
427pub fn sync_file(path: &Path) -> io::Result<()> {
428 #[cfg(unix)]
429 {
430 File::open(path)?.sync_all()
431 }
432 #[cfg(windows)]
433 {
434 OpenOptions::new().write(true).open(path)?.sync_all()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use std::io::Write;
442 #[test]
443 fn scratch_ownership_cleanup_and_no_overwrite_persistence() {
444 let parent = tempfile::tempdir().unwrap();
445 let directory_path;
446 {
447 let directory = private_tempdir_in(parent.path(), "private scratch ").unwrap();
448 directory_path = directory.path().to_owned();
449 assert!(private_path(directory.path()).unwrap());
450 let mut file = private_tempfile_in(directory.path(), ".private-").unwrap();
451 assert!(private_path(file.path()).unwrap());
452 file.write_all(b"synthetic data").unwrap();
453 let destination = directory.path().join("saved");
454 file.persist_noclobber(&destination).unwrap();
455 assert!(private_path(&destination).unwrap());
456 let another = private_tempfile_in(directory.path(), ".private-").unwrap();
457 assert!(another.persist_noclobber(&destination).is_err());
458 assert_eq!(std::fs::read(destination).unwrap(), b"synthetic data");
459 }
460 assert!(!directory_path.exists());
461 let kept = private_tempdir_in(parent.path(), "kept-").unwrap().keep();
462 assert!(private_path(&kept).unwrap());
463 std::fs::remove_dir(kept).unwrap();
464 }
465 #[test]
466 fn private_creation_and_no_overwrite_publication() {
467 let temp = tempfile::tempdir().unwrap();
468 let root = temp.path().join("private root");
469 private_dir(&root).unwrap();
470 assert!(private_path(&root).unwrap());
471 let key = root.join("key");
472 let mut file = OpenOptions::new()
473 .write(true)
474 .create_new(true)
475 .mode(0o600)
476 .open(&key)
477 .unwrap();
478 file.write_all(b"synthetic key").unwrap();
479 assert!(private_file(&file).unwrap());
480 assert!(same_path_file(&key, &file).unwrap());
481 let stage = root.join("stage");
482 private_dir(&stage).unwrap();
483 let destination = root.join("destination");
484 publish_directory(&stage, &destination).unwrap();
485 private_dir(&stage).unwrap();
486 assert!(publish_directory(&stage, &destination).is_err());
487 assert!(stage.is_dir() && destination.is_dir());
488 }
489 #[test]
490 fn process_identity_includes_creation_time_and_executable() {
491 let (created, executable) = process_identity(std::process::id()).unwrap();
492 assert!(!created.is_empty());
493 assert_eq!(
494 executable,
495 std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap()
496 );
497 assert!(process_identity(u32::MAX).is_none());
498 }
499 #[test]
500 #[cfg(unix)]
501 fn public_permissions_and_final_symlinks_are_rejected() {
502 use std::os::unix::fs::{symlink, PermissionsExt};
503 let temp = tempfile::tempdir().unwrap();
504 let path = temp.path().join("key");
505 std::fs::write(&path, b"synthetic").unwrap();
506 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
507 assert!(!private_path(&path).unwrap());
508 symlink(&path, temp.path().join("link")).unwrap();
509 assert!(OpenOptions::new()
510 .read(true)
511 .open(temp.path().join("link"))
512 .is_err());
513 }
514}