Skip to main content

cfgd_core/util/
fs_perms.rs

1/// Create a symbolic link. On Unix, uses `std::os::unix::fs::symlink`.
2/// On Windows, uses `symlink_file` or `symlink_dir` based on the source type.
3/// If symlink creation fails on Windows due to insufficient privileges,
4/// returns an error with guidance to enable Developer Mode or run as admin.
5pub fn create_symlink(source: &std::path::Path, target: &std::path::Path) -> std::io::Result<()> {
6    #[cfg(unix)]
7    {
8        create_symlink_impl(source, target)
9    }
10    #[cfg(windows)]
11    {
12        use super::paths::PathDisplayExt;
13        create_symlink_impl(source, target).map_err(|e| {
14            if e.raw_os_error() == Some(1314) {
15                // ERROR_PRIVILEGE_NOT_HELD
16                return std::io::Error::new(
17                    e.kind(),
18                    format!(
19                        "symlink creation requires Developer Mode or admin privileges: {} -> {}\n\
20                         Enable Developer Mode: Settings > Update & Security > For developers",
21                        source.posix(),
22                        target.posix()
23                    ),
24                );
25            }
26            e
27        })
28    }
29}
30
31#[cfg(unix)]
32fn create_symlink_impl(source: &std::path::Path, target: &std::path::Path) -> std::io::Result<()> {
33    std::os::unix::fs::symlink(source, target)
34}
35
36#[cfg(windows)]
37fn create_symlink_impl(source: &std::path::Path, target: &std::path::Path) -> std::io::Result<()> {
38    if source.is_dir() {
39        std::os::windows::fs::symlink_dir(source, target)
40    } else {
41        std::os::windows::fs::symlink_file(source, target)
42    }
43}
44
45/// Get Unix permission mode bits from file metadata. Returns None on Windows.
46#[cfg(unix)]
47pub fn file_permissions_mode(metadata: &std::fs::Metadata) -> Option<u32> {
48    use std::os::unix::fs::PermissionsExt;
49    Some(metadata.permissions().mode() & 0o777)
50}
51
52#[cfg(windows)]
53pub fn file_permissions_mode(_metadata: &std::fs::Metadata) -> Option<u32> {
54    None
55}
56
57/// Set Unix permission mode bits on a file. No-op on Windows (NTFS uses inherited ACLs).
58#[cfg(unix)]
59pub fn set_file_permissions(path: &std::path::Path, mode: u32) -> std::io::Result<()> {
60    use std::os::unix::fs::PermissionsExt;
61    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
62}
63
64#[cfg(windows)]
65pub fn set_file_permissions(_path: &std::path::Path, _mode: u32) -> std::io::Result<()> {
66    tracing::debug!("set_file_permissions is a no-op on Windows (NTFS uses inherited ACLs)");
67    Ok(())
68}
69
70/// Check if a file is executable.
71/// Unix: checks the executable bit in mode.
72/// Windows: checks file extension against known executable types.
73#[cfg(unix)]
74pub fn is_executable(_path: &std::path::Path, metadata: &std::fs::Metadata) -> bool {
75    use std::os::unix::fs::PermissionsExt;
76    metadata.permissions().mode() & 0o111 != 0
77}
78
79#[cfg(windows)]
80pub fn is_executable(path: &std::path::Path, _metadata: &std::fs::Metadata) -> bool {
81    const EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "cmd", "bat", "ps1", "com"];
82    path.extension()
83        .and_then(|e| e.to_str())
84        .map(|e| EXECUTABLE_EXTENSIONS.contains(&e.to_lowercase().as_str()))
85        .unwrap_or(false)
86}
87
88/// Check if two paths refer to the same file (same inode on Unix, same file index on Windows).
89#[cfg(unix)]
90pub fn is_same_inode(a: &std::path::Path, b: &std::path::Path) -> bool {
91    use std::os::unix::fs::MetadataExt;
92    match (std::fs::metadata(a), std::fs::metadata(b)) {
93        (Ok(ma), Ok(mb)) => ma.ino() == mb.ino() && ma.dev() == mb.dev(),
94        _ => false,
95    }
96}
97
98#[cfg(windows)]
99pub fn is_same_inode(a: &std::path::Path, b: &std::path::Path) -> bool {
100    use std::os::windows::io::AsRawHandle;
101    use windows_sys::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION;
102    use windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandle;
103
104    fn file_info(path: &std::path::Path) -> Option<BY_HANDLE_FILE_INFORMATION> {
105        let file = std::fs::File::open(path).ok()?;
106        // SAFETY: `BY_HANDLE_FILE_INFORMATION` is a plain-old-data struct of
107        // integer fields; the all-zero bit pattern is a valid initial value
108        // that `GetFileInformationByHandle` will overwrite before we read it.
109        let mut info = unsafe { std::mem::zeroed() };
110        // SAFETY: `file.as_raw_handle()` returns a valid, open Win32 file
111        // handle owned by `file`, which outlives the call. `&mut info`
112        // points to sufficient, aligned, writable memory for the out
113        // parameter. No aliasing: `info` is stack-local.
114        let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) };
115        if ret != 0 { Some(info) } else { None }
116    }
117
118    match (file_info(a), file_info(b)) {
119        (Some(ia), Some(ib)) => {
120            ia.dwVolumeSerialNumber == ib.dwVolumeSerialNumber
121                && ia.nFileIndexHigh == ib.nFileIndexHigh
122                && ia.nFileIndexLow == ib.nFileIndexLow
123        }
124        _ => false,
125    }
126}