Skip to main content

wechat_platform/
lib.rs

1//! OS boundaries shared by the native CLI, daemon and profile storage.
2#[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; // FILE_FLAG_OPEN_REPARSE_POINT
19
20/// Files never follow a final symlink/reparse point. Private creation is atomic.
21pub 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
195/// A randomly named scratch directory, owned by this user from creation.
196/// Windows' default temporary-directory owner can be Administrators when elevated.
197pub 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    // PrivateTempDir owns recursive cleanup; TempPath only removes files.
241    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}
257/// Windows has no directory fsync equivalent; files are flushed before publication.
258pub 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}
321/// PID reuse is excluded by comparing creation time and the canonical executable.
322pub 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
371/// Atomically publish a new directory, refusing any existing destination.
372pub 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}
401/// Includes Windows junctions and other reparse points, not only symlinks.
402pub 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}