From 68023f2a9b2dd730bae8f5e0f9f26ca66fb67b29 Mon Sep 17 00:00:00 2001 From: Dilawar Singh Date: Thu, 3 Jul 2025 11:51:05 +0530 Subject: [PATCH 1/4] Use `sudo` to escalate privilege and try again to copy/edit ini file --- crates/cli/Cargo.toml | 1 + crates/cli/src/lib.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 062bf60a40..9240a39269 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,6 +19,7 @@ dialoguer = "0.11" libloading = "0.8" cargo_metadata = "0.20" semver = "1.0" +sudo = "0.6.0" [lints.rust] missing_docs = "warn" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index ca71d62e68..fd097857f9 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -221,9 +221,16 @@ impl Install { ext_dir.push(ext_name); } - std::fs::copy(&ext_path, &ext_dir).with_context(|| { - "Failed to copy extension from target directory to extension directory" - })?; + // We copying of file fails, escalate the privilege and try again. + if let Err(_) = std::fs::copy(&ext_path, &ext_dir) { + // failed to copy. escalate the privileges and try again. + #[cfg(unix)] + let _ = sudo::escalate_if_needed().ok(); + + std::fs::copy(&ext_path, &ext_dir).with_context(|| { + "Failed to copy extension from target directory to extension directory" + })?; + } if let Some(php_ini) = php_ini { let mut file = OpenOptions::new() From a4c6d58747151bff01e3002258c3387bceda9517 Mon Sep 17 00:00:00 2001 From: Dilawar Singh Date: Thu, 3 Jul 2025 12:18:44 +0530 Subject: [PATCH 2/4] Adds option --bypass-root-check to install and remove command And escalate sudo by default in both commands. --- crates/cli/Cargo.toml | 2 +- crates/cli/src/lib.rs | 99 +++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9240a39269..6918346809 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,7 +19,7 @@ dialoguer = "0.11" libloading = "0.8" cargo_metadata = "0.20" semver = "1.0" -sudo = "0.6.0" +elevate = "0.6.1" [lints.rust] missing_docs = "warn" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index fd097857f9..82e3c72162 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -121,6 +121,9 @@ struct Install { /// Whether to bypass the install prompt. #[clap(long)] yes: bool, + /// Whether to bypass the root check + #[clap(long)] + bypass_root_check: bool, } #[derive(Parser)] @@ -140,6 +143,9 @@ struct Remove { /// Whether to bypass the remove prompt. #[clap(long)] yes: bool, + /// Whether to bypass the root check + #[clap(long)] + bypass_root_check: bool, } #[cfg(not(windows))] @@ -193,6 +199,13 @@ impl Install { self.no_default_features, )?; + if !self.bypass_root_check { + anyhow::ensure!( + elevate::check() == elevate::RunningAs::User, + "Running as root is not recommended. Use --bypass-root-check to override." + ); + } + let (mut ext_dir, mut php_ini) = if let Some(install_dir) = self.install_dir { (install_dir, None) } else { @@ -221,50 +234,74 @@ impl Install { ext_dir.push(ext_name); } - // We copying of file fails, escalate the privilege and try again. - if let Err(_) = std::fs::copy(&ext_path, &ext_dir) { - // failed to copy. escalate the privileges and try again. - #[cfg(unix)] - let _ = sudo::escalate_if_needed().ok(); + copy_extension(&ext_path, &ext_dir).with_context(|| { + "Failed to copy extension from target directory to extension directory" + })?; - std::fs::copy(&ext_path, &ext_dir).with_context(|| { - "Failed to copy extension from target directory to extension directory" - })?; + if let Some(php_ini) = php_ini { + copy_ini_file(&php_ini, ext_name, self.disable) + .with_context(|| "Failed to update `php.ini`")?; } - if let Some(php_ini) = php_ini { - let mut file = OpenOptions::new() + Ok(()) + } +} + +// Copy ini file, if fails, try with sudo again. +fn copy_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow::Result<()> { + let mut file = match OpenOptions::new().read(true).write(true).open(php_ini) { + Ok(x) => x, + Err(_e) => { + #[cfg(unix)] + { + elevate::escalate_if_needed().expect("sudo failed"); + } + OpenOptions::new() .read(true) .write(true) .open(php_ini) - .with_context(|| "Failed to open `php.ini`")?; + .with_context(|| "Failed to open `php.ini`")? + } + }; - let mut ext_line = format!("extension={ext_name}"); + let mut ext_line = format!("extension={ext_name}"); - let mut new_lines = vec![]; - for line in BufReader::new(&file).lines() { - let line = line.with_context(|| "Failed to read line from `php.ini`")?; - if line.contains(&ext_line) { - bail!("Extension already enabled."); - } + let mut new_lines = vec![]; + for line in BufReader::new(&file).lines() { + let line = line.with_context(|| "Failed to read line from `php.ini`")?; + if line.contains(&ext_line) { + bail!("Extension already enabled."); + } - new_lines.push(line); - } + new_lines.push(line); + } - // Comment out extension if user specifies disable flag - if self.disable { - ext_line.insert(0, ';'); - } + // Comment out extension if user specifies disable flag + if disable { + ext_line.insert(0, ';'); + } - new_lines.push(ext_line); - file.rewind()?; - file.set_len(0)?; - file.write(new_lines.join("\n").as_bytes()) - .with_context(|| "Failed to update `php.ini`")?; - } + new_lines.push(ext_line); + file.rewind()?; + file.set_len(0)?; + let _ = file.write(new_lines.join("\n").as_bytes())?; + Ok(()) +} - Ok(()) +// Copy extension, if fails, try with sudo again. +// +// We can check if we have write permission for ext_dir but due to ACL, group +// list and and other nuances, it may not be reliable. See +// https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly +fn copy_extension(ext_path: &Utf8PathBuf, ext_dir: &PathBuf) -> anyhow::Result<()> { + if let Err(_e) = std::fs::copy(ext_path, ext_dir) { + #[cfg(unix)] + { + elevate::escalate_if_needed().expect("sudo failed"); + } + std::fs::copy(ext_path, ext_dir)?; } + Ok(()) } /// Returns the path to the extension directory utilised by the PHP interpreter, From 854820552abcd5e24c3260505152128738bb1dd5 Mon Sep 17 00:00:00 2001 From: Dilawar Singh Date: Thu, 3 Jul 2025 22:42:16 +0530 Subject: [PATCH 3/4] Use `sudo mv` command when writing to file fails --- crates/cli/src/lib.rs | 125 ++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 82e3c72162..8f6b20a202 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -9,8 +9,7 @@ use clap::Parser; use dialoguer::{Confirm, Select}; use std::{ - fs::OpenOptions, - io::{BufRead, BufReader, Seek, Write}, + io::BufReader, path::PathBuf, process::{Command, Stdio}, }; @@ -239,7 +238,7 @@ impl Install { })?; if let Some(php_ini) = php_ini { - copy_ini_file(&php_ini, ext_name, self.disable) + update_ini_file(&php_ini, ext_name, self.disable) .with_context(|| "Failed to update `php.ini`")?; } @@ -247,33 +246,19 @@ impl Install { } } -// Copy ini file, if fails, try with sudo again. -fn copy_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow::Result<()> { - let mut file = match OpenOptions::new().read(true).write(true).open(php_ini) { - Ok(x) => x, - Err(_e) => { - #[cfg(unix)] - { - elevate::escalate_if_needed().expect("sudo failed"); - } - OpenOptions::new() - .read(true) - .write(true) - .open(php_ini) - .with_context(|| "Failed to open `php.ini`")? - } - }; - +// Update extension information in the ini file, if fails, try with sudo again. +// +// Write to a temp file then move it to given path. Use `sudo` on unix to move +// file if needed. +fn update_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow::Result<()> { + let current_ini_content = std::fs::read_to_string(php_ini)?; let mut ext_line = format!("extension={ext_name}"); - let mut new_lines = vec![]; - for line in BufReader::new(&file).lines() { - let line = line.with_context(|| "Failed to read line from `php.ini`")?; + let mut new_lines = current_ini_content.lines().collect::>(); + for line in &new_lines { if line.contains(&ext_line) { bail!("Extension already enabled."); } - - new_lines.push(line); } // Comment out extension if user specifies disable flag @@ -281,25 +266,26 @@ fn copy_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow::Re ext_line.insert(0, ';'); } - new_lines.push(ext_line); - file.rewind()?; - file.set_len(0)?; - let _ = file.write(new_lines.join("\n").as_bytes())?; + new_lines.push(&ext_line); + write_to_file(new_lines.join("\n"), php_ini)?; Ok(()) } -// Copy extension, if fails, try with sudo again. +// Copy extension, if fails, try with sudo cp. // -// We can check if we have write permission for ext_dir but due to ACL, group -// list and and other nuances, it may not be reliable. See +// Checking if we have write permission for ext_dir may fail due to ACL, group +// list and and other nuances. See // https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly fn copy_extension(ext_path: &Utf8PathBuf, ext_dir: &PathBuf) -> anyhow::Result<()> { if let Err(_e) = std::fs::copy(ext_path, ext_dir) { #[cfg(unix)] { - elevate::escalate_if_needed().expect("sudo failed"); + let _ = std::process::Command::new("sudo") + .arg("cp") + .arg(ext_path) + .arg(ext_dir) + .status()?; } - std::fs::copy(ext_path, ext_dir)?; } Ok(()) } @@ -395,28 +381,28 @@ impl Remove { bail!("Installation cancelled."); } - std::fs::remove_file(ext_path).with_context(|| "Failed to remove extension")?; - - if let Some(php_ini) = php_ini.filter(|path| path.is_file()) { - let mut file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(php_ini) - .with_context(|| "Failed to open `php.ini`")?; - - let mut new_lines = vec![]; - for line in BufReader::new(&file).lines() { - let line = line.with_context(|| "Failed to read line from `php.ini`")?; - if !line.contains(&ext_file) { - new_lines.push(line); - } + if let Err(_e) = std::fs::remove_file(&ext_path) { + #[cfg(unix)] + { + let _ = std::process::Command::new("sudo") + .arg("rm") + .arg("-f") + .arg(&ext_path) + .status()?; } + } + anyhow::ensure!(!ext_path.is_file(), "Failed to remove {ext_path:?}"); - file.rewind()?; - file.set_len(0)?; - file.write(new_lines.join("\n").as_bytes()) + // modify the ini file + if let Some(php_ini) = php_ini.filter(|path| path.is_file()) { + let ini_file_content = std::fs::read_to_string(&php_ini)?; + + let new_ini_content = ini_file_content + .lines() + .filter(|x| x.contains(&ext_file)) + .collect::>() + .join("\n"); + write_to_file(new_ini_content, &php_ini) .with_context(|| "Failed to update `php.ini`")?; } @@ -612,3 +598,34 @@ fn build_ext( bail!("Failed to retrieve extension path from artifact") } + +// Write given string to a given filepath. +// +// - Write to a temp file first. +// - Copy the temp file to the given filepath. +// - If it fails, move tempfile using `sudo mv`. +// +// TODO: Try with sudo when error is permission related. +fn write_to_file(content: String, filepath: &PathBuf) -> anyhow::Result<()> { + // write to a temp file + let tempf = std::env::temp_dir().join("__tmp_cargo_php"); + std::fs::write(&tempf, content)?; + + // Now move. `rename` will overwrite existing file. + if std::fs::rename(&tempf, filepath).is_err() { + #[cfg(unix)] + { + // if not successful, try with sudo on unix. + let _ = std::process::Command::new("sudo") + .arg("mv") + .arg(&tempf) + .arg(filepath) + .status()?; + } + + #[cfg(not(unix))] + anyhow::bail!("failed to write to {filepath:?}"); + } + + Ok(()) +} From 65d8f24cb55df7cb0eccee7ce6fcdfcc4f60b797 Mon Sep 17 00:00:00 2001 From: Dilawar Singh Date: Fri, 4 Jul 2025 10:24:59 +0530 Subject: [PATCH 4/4] Use libc to check if current user is `sudo` since libc is already in cargo.lock Update crates/cli/src/lib.rs Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs comment -> doc Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs comment -> doc Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs remove commented out line Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> Update crates/cli/src/lib.rs Co-authored-by: Xenira <1288524+Xenira@users.noreply.github.com> --- crates/cli/Cargo.toml | 4 ++- crates/cli/src/lib.rs | 64 ++++++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 6918346809..7838b4397d 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,7 +19,9 @@ dialoguer = "0.11" libloading = "0.8" cargo_metadata = "0.20" semver = "1.0" -elevate = "0.6.1" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" [lints.rust] missing_docs = "warn" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 8f6b20a202..b9104df6b8 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -142,6 +142,7 @@ struct Remove { /// Whether to bypass the remove prompt. #[clap(long)] yes: bool, + #[cfg(unix)] /// Whether to bypass the root check #[clap(long)] bypass_root_check: bool, @@ -198,12 +199,11 @@ impl Install { self.no_default_features, )?; - if !self.bypass_root_check { - anyhow::ensure!( - elevate::check() == elevate::RunningAs::User, - "Running as root is not recommended. Use --bypass-root-check to override." - ); - } + #[cfg(unix)] + anyhow::ensure!( + self.bypass_root_check || !is_root(), + "Running as root is not recommended. Use --bypass-root-check to override." + ); let (mut ext_dir, mut php_ini) = if let Some(install_dir) = self.install_dir { (install_dir, None) @@ -246,10 +246,10 @@ impl Install { } } -// Update extension information in the ini file, if fails, try with sudo again. -// -// Write to a temp file then move it to given path. Use `sudo` on unix to move -// file if needed. +/// Update extension line in the ini file. +/// +/// Write to a temp file then copy it to a given path. If this fails, then try +/// `sudo mv` on unix. fn update_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow::Result<()> { let current_ini_content = std::fs::read_to_string(php_ini)?; let mut ext_line = format!("extension={ext_name}"); @@ -267,25 +267,29 @@ fn update_ini_file(php_ini: &PathBuf, ext_name: &str, disable: bool) -> anyhow:: } new_lines.push(&ext_line); + write_to_file(new_lines.join("\n"), php_ini)?; Ok(()) } -// Copy extension, if fails, try with sudo cp. -// -// Checking if we have write permission for ext_dir may fail due to ACL, group -// list and and other nuances. See -// https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly +/// Copy extension, if fails, try with sudo cp. +/// +/// Checking if we have write permission for ext_dir may fail due to ACL, group +/// list and and other nuances. See +/// https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly. fn copy_extension(ext_path: &Utf8PathBuf, ext_dir: &PathBuf) -> anyhow::Result<()> { if let Err(_e) = std::fs::copy(ext_path, ext_dir) { #[cfg(unix)] { - let _ = std::process::Command::new("sudo") + let s = std::process::Command::new("sudo") .arg("cp") .arg(ext_path) .arg(ext_dir) .status()?; + anyhow::ensure!(s.success(), "Failed to copy extension"); } + #[cfg(not(unix))] + anyhow::bail!("Failed to copy extension: {_e}"); } Ok(()) } @@ -599,28 +603,27 @@ fn build_ext( bail!("Failed to retrieve extension path from artifact") } -// Write given string to a given filepath. -// -// - Write to a temp file first. -// - Copy the temp file to the given filepath. -// - If it fails, move tempfile using `sudo mv`. -// -// TODO: Try with sudo when error is permission related. +/// Write content to a given filepath. +/// +/// We may not have write permission but we may have sudo privilege on unix. So +/// we write to a temp file and then try moving it to given filepath, and retry +/// with sudo on unix. fn write_to_file(content: String, filepath: &PathBuf) -> anyhow::Result<()> { // write to a temp file let tempf = std::env::temp_dir().join("__tmp_cargo_php"); std::fs::write(&tempf, content)?; - // Now move. `rename` will overwrite existing file. + // Now try moving, `rename` will overwrite existing file. if std::fs::rename(&tempf, filepath).is_err() { #[cfg(unix)] { // if not successful, try with sudo on unix. - let _ = std::process::Command::new("sudo") + let s = std::process::Command::new("sudo") .arg("mv") .arg(&tempf) .arg(filepath) .status()?; + anyhow::ensure!(s.success(), "Falied to write to {filepath:?}"); } #[cfg(not(unix))] @@ -629,3 +632,14 @@ fn write_to_file(content: String, filepath: &PathBuf) -> anyhow::Result<()> { Ok(()) } + +#[cfg(unix)] +fn is_root() -> bool { + let uid = unsafe { libc::getuid() }; + let euid = unsafe { libc::geteuid() }; + + match (uid, euid) { + (_, 0) => true, // suid set + (_, _) => false, + } +}