//! What differs between operating systems: file modes and links, file //! times, starting the saver on its own, the machine's name, the user's //! folder and opening a browser. Windows works like git does there: files //! keep the kind (executable, link) Onset knows them as, since the disk //! can't always say, and a link it can't make is a file holding its target. use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use onset_core::tree::EntryKind; /// The user's home folder. pub fn user_home() -> Option { std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).filter(|h| !h.is_empty()).map(PathBuf::from) } /// The user's name, for an author when nothing else says. pub fn user_name() -> Option { std::env::var("USER").or_else(|_| std::env::var("USERNAME")).ok().filter(|u| !u.is_empty()) } /// A folder's full path, links resolved. On Windows, without the `\\?\` /// prefix where the path doesn't need it, so it reads as people write it. pub fn canonical(path: &Path) -> std::io::Result { let full = path.canonicalize()?; if cfg!(windows) && let Some(s) = full.to_str() { if let Some(rest) = s.strip_prefix(r"\\?\UNC\") { return Ok(PathBuf::from(format!(r"\\{rest}"))); } if let Some(rest) = s.strip_prefix(r"\\?\") && rest.len() < 260 { return Ok(PathBuf::from(rest)); } } Ok(full) } /// Whether this system can have a file or folder by this name. Windows /// can't: reserved device names (`aux.c`, `NUL`), `<>:"|?*`, control /// characters, or a trailing dot or space. (`:` would also reach a file's /// hidden streams there.) pub fn fits(name: &str) -> bool { if !cfg!(windows) { return true; } if name.ends_with(['.', ' ']) || name.chars().any(|c| (c as u32) < 32 || "<>:\"|?*".contains(c)) { return false; } let stem = name.split('.').next().unwrap_or("").trim_end_matches(' ').to_ascii_uppercase(); let numbered = |p: &str| stem.len() == 4 && stem.starts_with(p) && matches!(stem.as_bytes()[3], b'0'..=b'9'); !(matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$") || numbered("COM") || numbered("LPT")) } /// A path inside a workspace as trees name it: "a/b", whatever the OS's separator. pub fn tree_path(rel: &Path) -> Option { let s = rel.to_str()?; Some(if cfg!(windows) { s.replace('\\', "/") } else { s.to_string() }) } /// A file's modification time in nanoseconds, and its file number where /// the OS gives one cheaply (0 otherwise). #[cfg(unix)] pub fn times(meta: &fs::Metadata) -> (i64, u64) { use std::os::unix::fs::MetadataExt; (meta.mtime() * 1_000_000_000 + meta.mtime_nsec(), meta.ino()) } #[cfg(windows)] pub fn times(meta: &fs::Metadata) -> (i64, u64) { use std::os::windows::fs::MetadataExt; // 100-nanosecond ticks since 1601; as nanoseconds since 1970, like Unix. const TICKS_TO_1970: i64 = 116_444_736_000_000_000; (((meta.last_write_time() as i64) - TICKS_TO_1970).saturating_mul(100), 0) } /// What a file on disk is. `was` is what Onset knew it as. #[cfg(unix)] pub fn kind_of(meta: &fs::Metadata, _was: Option) -> EntryKind { use std::os::unix::fs::PermissionsExt; if meta.file_type().is_symlink() { EntryKind::Link } else if meta.permissions().mode() & 0o111 != 0 { EntryKind::Exec } else { EntryKind::File } } #[cfg(windows)] pub fn kind_of(meta: &fs::Metadata, was: Option) -> EntryKind { if meta.file_type().is_symlink() { return EntryKind::Link; } match was { Some(k @ (EntryKind::Exec | EntryKind::Link)) => k, _ => EntryKind::File, } } /// Whether file `path` of the workspace at `root` is a lazy workspace's /// placeholder just as Onset made it, with `id`'s content (Windows, where /// the placeholder says so itself). It's unchanged then whatever its times /// say, and reading it would only fetch it from the host. Elsewhere there /// are none. pub fn untouched_placeholder(root: &Path, path: &str, id: &onset_core::Id) -> bool { #[cfg(windows)] { crate::lazy_windows::untouched(root, path, id) } #[cfg(not(windows))] { let _ = (root, path, id); false } } /// A link's target, as trees keep it ("a/b"). pub fn read_link(path: &Path) -> std::io::Result> { match fs::read_link(path) { Ok(target) => Ok(target.to_string_lossy().replace(if cfg!(windows) { '\\' } else { '/' }, "/").into_bytes()), // Written as a file holding its target (Windows without links). Err(_) if cfg!(windows) => fs::read(path), Err(e) => Err(e), } } /// Makes a link at `at` to `target` ("a/b"). pub fn write_link(target: &str, at: &Path) -> std::io::Result<()> { #[cfg(unix)] { std::os::unix::fs::symlink(target, at) } #[cfg(windows)] { let native = target.replace('/', "\\"); let to_dir = at.parent().is_some_and(|p| p.join(&native).is_dir()); let made = if to_dir { std::os::windows::fs::symlink_dir(&native, at) } else { std::os::windows::fs::symlink_file(&native, at) }; // Links need Developer Mode or admin rights there; one that can't // be read back is no use either. if made.is_ok() && fs::symlink_metadata(at).is_ok() { return Ok(()); } let _ = fs::remove_file(at); fs::write(at, target) } } /// Marks a file executable or not (Windows has no such mark). pub fn set_exec(path: &Path, exec: bool) -> std::io::Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(path, fs::Permissions::from_mode(if exec { 0o755 } else { 0o644 })) } #[cfg(windows)] { let _ = (path, exec); Ok(()) } } /// Makes a file readable only by its owner. On Windows, files in the /// user's own folders already are. pub fn owner_only(path: &Path, exec: bool) -> std::io::Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(path, fs::Permissions::from_mode(if exec { 0o700 } else { 0o600 })) } #[cfg(windows)] { let _ = (path, exec); Ok(()) } } /// This machine's name. pub fn hostname() -> String { #[cfg(unix)] { let mut buf = [0u8; 256]; // SAFETY: gethostname writes at most buf.len() bytes into buf. if unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) } == 0 { let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len()); return String::from_utf8_lossy(&buf[..end]).into_owned(); } "this machine".into() } #[cfg(windows)] { std::env::var("COMPUTERNAME").unwrap_or_else(|_| "this machine".into()) } } /// Lets `cmd` run on its own, so closing the terminal doesn't stop it. pub fn detach(cmd: &mut Command) { #[cfg(unix)] { use std::os::unix::process::CommandExt; // SAFETY: setsid is async-signal-safe. unsafe { cmd.pre_exec(|| { libc::setsid(); Ok(()) }); } } #[cfg(windows)] { use std::os::windows::process::CommandExt; const DETACHED_PROCESS: u32 = 0x0000_0008; const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; const CREATE_NO_WINDOW: u32 = 0x0800_0000; cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); keep_std_handles(); } } /// Keeps this process's own input and output out of what it starts in the /// background. Windows hands a new process every handle that may be /// inherited, and a pipe this command writes to (an agent's shell, `$(...)`) /// would stay open in the saver or a server for as long as it runs, so /// whatever reads it would wait. Each child still gets the handles it's /// given: Rust passes it copies. #[cfg(windows)] fn keep_std_handles() { use windows_sys::Win32::Foundation::{HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation}; use windows_sys::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}; for which in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] { // SAFETY: a handle of this process's own, or none. unsafe { let handle = GetStdHandle(which); if !handle.is_null() && handle != INVALID_HANDLE_VALUE { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0); } } } } /// Opens `url` in the default browser. pub fn open_url(url: &str) { let mut cmd = if cfg!(target_os = "macos") { Command::new("open") } else if cfg!(windows) { let mut c = Command::new("rundll32"); c.arg("url.dll,FileProtocolHandler"); c } else { Command::new("xdg-open") }; let _ = cmd.arg(url).stdin(std::process::Stdio::null()).stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn(); }