From 8b0a301bf9ed6fe1de8f908572c818df67955a4e Mon Sep 17 00:00:00 2001 From: aditya singh rathore <142787780+Adez017@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:22:30 +0530 Subject: [PATCH 1/5] Create pull_request_template.md --- .github/pull_request_template.md | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..907d905 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,40 @@ +## Which issue does this PR close? + + + +- Closes #. + +## Rationale for this change + + + +## What changes are included in this PR? + + + +## Are these changes tested? + + + +## Are there any user-facing changes? + + + + From 5d579cbbc4f79f4146795a7b79360e88ee93462f Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Tue, 29 Jul 2025 22:19:31 +0530 Subject: [PATCH 2/5] Added Suport for Fast Sudo --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 28 +++++++++++++++++++++++++--- src-tauri/src/sudo.rs | 0 src/components/PasswordDialog.tsx | 18 +++++++++++++----- src/hooks/useFastSudo.ts | 0 6 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src-tauri/src/sudo.rs create mode 100644 src/hooks/useFastSudo.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3040c2b..c4fbbc3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -96,6 +96,7 @@ version = "0.1.0" dependencies = [ "dirs 5.0.1", "dotenvy", + "libc", "log", "reqwest", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c530d91..3d0753d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,3 +30,4 @@ tauri-plugin-fs = "2.0.0-rc" tauri-plugin-log = "2.0.0-rc" tauri-plugin-shell = "2" dirs = "5.0" +libc = "0.2" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8959dce..fc3a9bd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,14 +2,32 @@ pub mod commands; pub mod utils; +pub mod sudo; + +use sudo::{SudoCache, fast_sudo, clear_sudo_cache, direct_privilege_escalation, check_sudo_privileges}; +use tauri::Manager; fn main() { dotenvy::dotenv().ok(); tauri::Builder::default() + .setup(|app| { + app.manage(SudoCache::new()); + + let cache = app.state::(); + let cache_clone = cache.inner().clone(); + + std::thread::spawn(move || { + loop { + std::thread::sleep(std::time::Duration::from_secs(300)); + cache_clone.clear_expired(15); + } + }); + + Ok(()) + }) .invoke_handler(tauri::generate_handler![ commands::shell::run_shell, - commands::shell::run_sudo_command, commands::shell::get_current_dir, commands::shell::list_directory_contents, commands::shell::change_directory, @@ -17,8 +35,12 @@ fn main() { commands::api_key::save_api_key, commands::api_key::get_api_key, commands::api_key::validate_api_key, - commands::api_key::delete_api_key + commands::api_key::delete_api_key, + fast_sudo, + clear_sudo_cache, + direct_privilege_escalation, + check_sudo_privileges ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} +} \ No newline at end of file diff --git a/src-tauri/src/sudo.rs b/src-tauri/src/sudo.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/components/PasswordDialog.tsx b/src/components/PasswordDialog.tsx index 479876f..c0f5303 100644 --- a/src/components/PasswordDialog.tsx +++ b/src/components/PasswordDialog.tsx @@ -33,6 +33,12 @@ const PasswordDialog: React.FC = ({ isOpen, onClose, onSubm onSubmit(password); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape' && !isSubmitting) { + onClose(); + } + }; + if (!isOpen) return null; return ( @@ -44,7 +50,7 @@ const PasswordDialog: React.FC = ({ isOpen, onClose, onSubm -

Administrator Privileges Required

+

⚡ Fast Sudo Authentication

@@ -54,7 +60,7 @@ const PasswordDialog: React.FC = ({ isOpen, onClose, onSubm {commandText} -

+
= ({ isOpen, onClose, onSubm disabled={isSubmitting} />

- Your password will not be stored and is only used to execute this command. + Password will be cached for 15 minutes for faster subsequent commands.

@@ -94,7 +100,9 @@ const PasswordDialog: React.FC = ({ isOpen, onClose, onSubm Authenticating... ) : ( - 'Submit' + <> + ⚡ Authenticate + )} @@ -104,4 +112,4 @@ const PasswordDialog: React.FC = ({ isOpen, onClose, onSubm ); }; -export default PasswordDialog; +export default PasswordDialog; \ No newline at end of file diff --git a/src/hooks/useFastSudo.ts b/src/hooks/useFastSudo.ts new file mode 100644 index 0000000..e69de29 From 048ca0b1e9b31d021021b5c952c1c9073cbb76b0 Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Fri, 1 Aug 2025 08:54:43 +0530 Subject: [PATCH 3/5] Updated content --- src-tauri/src/commands/shell.rs | 567 +++++++++++++++++++++++++------- src-tauri/src/utils/mod.rs | 419 ++++++++++++++++++++++- 2 files changed, 859 insertions(+), 127 deletions(-) diff --git a/src-tauri/src/commands/shell.rs b/src-tauri/src/commands/shell.rs index f1bdb0a..d358903 100644 --- a/src-tauri/src/commands/shell.rs +++ b/src-tauri/src/commands/shell.rs @@ -1,4 +1,273 @@ +// use std::process::Command; + +// #[tauri::command] +// pub async fn run_shell(command: String) -> Result { +// let cmd = command.trim(); + +// if cmd == "exit" { +// return Ok("__EXIT_SHELL__".to_string()); +// } + +// // Special handling for ls/dir commands to add file type indicators +// if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { +// let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// if cmd == "ls" { +// "ls -la".to_string() +// } else if cmd.starts_with("ls ") { +// if !command.contains(" -l") && !command.contains(" -a") { +// format!("{} -la", command) +// } else if !command.contains(" -l") { +// format!("{} -l", command) +// } else if !command.contains(" -a") { +// format!("{} -a", command) +// } else { +// command.clone() +// } +// } else { +// command.clone() +// } +// } else { +// command.clone() +// }; + +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &enhanced_command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&enhanced_command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let stderr = String::from_utf8_lossy(&out.stderr); + +// if !stderr.is_empty() && stdout.is_empty() { +// return Ok(stderr.to_string()); +// } + +// // Process the output to add file type indicators +// return Ok(crate::utils::format_directory_listing(&stdout)); +// } +// Err(e) => return Err(format!("Failed to run command: {}", e)), +// } +// } + +// // Regular command execution (non-ls commands) +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let stderr = String::from_utf8_lossy(&out.stderr); + +// if !stderr.is_empty() && stdout.is_empty() { +// Ok(stderr.to_string()) +// } else if !stdout.is_empty() { +// Ok(stdout.to_string()) +// } else { +// Ok(String::from( +// "Command executed successfully with no output.", +// )) +// } +// } +// Err(e) => Err(format!("Failed to run command: {}", e)), +// } +// } + +// #[tauri::command] +// pub async fn run_sudo_command(command: String, password: String) -> Result { +// if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { +// return Err("Sudo is only supported on Linux and macOS".to_string()); +// } + +// let cmd = if command.starts_with("sudo ") { +// command[5..].to_string() +// } else { +// command +// }; + +// let temp_dir = std::env::temp_dir(); +// let output_file = temp_dir.join(format!("term_sudo_{}", std::process::id())); +// let output_path = output_file.to_string_lossy(); + +// let script = format!( +// r#"#!/bin/bash +// echo "{}" | sudo -S {} > "{}" 2>&1 +// exit_code=$? +// if [ $exit_code -ne 0 ]; then +// echo "Command failed with exit code $exit_code" >> "{}" +// fi +// "#, +// password.replace("\"", "\\\""), +// cmd.replace("\"", "\\\""), +// output_path, +// output_path +// ); + +// let script_file = temp_dir.join(format!("term_sudo_script_{}", std::process::id())); +// std::fs::write(&script_file, script).map_err(|e| format!("Failed to create script: {}", e))?; + +// #[cfg(not(target_os = "windows"))] +// { +// use std::os::unix::fs::PermissionsExt; +// let mut perms = std::fs::metadata(&script_file) +// .map_err(|e| format!("Failed to get file metadata: {}", e))? +// .permissions(); +// perms.set_mode(0o755); +// std::fs::set_permissions(&script_file, perms) +// .map_err(|e| format!("Failed to set permissions: {}", e))?; +// } + +// let _status = tokio::process::Command::new(&script_file) +// .status() +// .await +// .map_err(|e| format!("Failed to execute script: {}", e))?; + +// let _ = std::fs::remove_file(&script_file); + +// let output = std::fs::read_to_string(&output_file) +// .map_err(|e| format!("Failed to read output: {}", e))?; + +// let _ = std::fs::remove_file(&output_file); + +// if output.contains("incorrect password") +// || output.contains("Sorry, try again") +// || output.contains("Authentication failure") +// || output.contains("authentication failure") +// || output.contains("sudo: no password was provided") +// || output.contains("sudo: 1 incorrect password attempt") +// { +// return Err("Incorrect password provided".to_string()); +// } + +// Ok(output) +// } + +// #[tauri::command] +// pub fn get_current_dir() -> Result { +// std::env::current_dir() +// .map(|path| path.to_string_lossy().into_owned()) +// .map_err(|e| format!("Failed to get current directory: {}", e)) +// } + +// #[tauri::command] +// pub async fn list_directory_contents(path: Option) -> Result, String> { +// let dir_path = match path { +// Some(p) if !p.is_empty() => p, +// _ => ".".to_string(), +// }; + +// let ls_command = if cfg!(target_os = "windows") { +// format!("dir /b \"{}\"", dir_path) +// } else { +// format!("ls -la \"{}\"", dir_path) +// }; + +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &ls_command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&ls_command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let lines: Vec<&str> = stdout.lines().collect(); +// let mut file_list = Vec::new(); + +// // Skip the first line if it starts with "total" (ls summary) +// let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { +// 1 +// } else { +// 0 +// }; + +// for line in lines.iter().skip(start_idx) { +// let line_trim = line.trim(); +// if line_trim.is_empty() { +// continue; +// } + +// // Unix-style ls output with permissions +// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// if line_trim.len() < 10 { +// continue; +// } + +// let parts: Vec<&str> = line_trim.split_whitespace().collect(); +// if parts.len() < 9 { +// continue; +// } + +// // Join all parts from index 8 to handle filenames with spaces +// let filename = parts[8..].join(" "); + +// // Skip . and .. entries +// if filename == "." || filename == ".." { +// continue; +// } + +// // Check file type and add appropriate suffix +// let file_type = line_trim.chars().next().unwrap_or('?'); +// if file_type == 'd' { +// file_list.push(format!("{}/", filename)); +// } else if line_trim.contains("x") && file_type == '-' { +// file_list.push(format!("{}*", filename)); +// } else { +// file_list.push(filename); +// } +// } else { +// // Windows directory listing (simpler format) +// if line_trim != "." && line_trim != ".." { +// let path = std::path::Path::new(line_trim); +// if path.is_dir() { +// file_list.push(format!("{}/", line_trim)); +// } else if line_trim.ends_with(".exe") +// || line_trim.ends_with(".bat") +// || line_trim.ends_with(".cmd") +// { +// file_list.push(format!("{}*", line_trim)); +// } else { +// file_list.push(line_trim.to_string()); +// } +// } +// } +// } + +// Ok(file_list) +// } +// Err(e) => Err(format!("Failed to list directory: {}", e)), +// } +// } + +// #[tauri::command] +// pub fn change_directory(path: String) -> Result { +// let expanded_path = if path.starts_with("~") { +// if let Ok(home) = std::env::var("HOME") { +// path.replacen("~", &home, 1) +// } else { +// path +// } +// } else { +// path +// }; + +// match std::env::set_current_dir(expanded_path) { +// Ok(_) => { +// let new_dir = std::env::current_dir() +// .map_err(|e| format!("Failed to get current directory: {}", e))?; +// Ok(new_dir.to_string_lossy().into_owned()) +// }, +// Err(e) => Err(format!("Failed to change directory: {}", e)) +// } +// } use std::process::Command; +use std::path::{Path, PathBuf}; +use std::fs; #[tauri::command] pub async fn run_shell(command: String) -> Result { @@ -8,48 +277,9 @@ pub async fn run_shell(command: String) -> Result { return Ok("__EXIT_SHELL__".to_string()); } - // Special handling for ls/dir commands to add file type indicators + // Special handling for ls/dir commands with robust parsing if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { - let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if cmd == "ls" { - "ls -la".to_string() - } else if cmd.starts_with("ls ") { - if !command.contains(" -l") && !command.contains(" -a") { - format!("{} -la", command) - } else if !command.contains(" -l") { - format!("{} -l", command) - } else if !command.contains(" -a") { - format!("{} -a", command) - } else { - command.clone() - } - } else { - command.clone() - } - } else { - command.clone() - }; - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &enhanced_command]).output() - } else { - Command::new("sh").arg("-c").arg(&enhanced_command).output() - }; - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - - if !stderr.is_empty() && stdout.is_empty() { - return Ok(stderr.to_string()); - } - - // Process the output to add file type indicators - return Ok(crate::utils::format_directory_listing(&stdout)); - } - Err(e) => return Err(format!("Failed to run command: {}", e)), - } + return handle_ls_command(command).await; } // Regular command execution (non-ls commands) @@ -78,6 +308,82 @@ pub async fn run_shell(command: String) -> Result { } } +async fn handle_ls_command(original_command: String) -> Result { + let cmd = original_command.trim(); + + // Extract the path from the ls command + let target_path = extract_path_from_ls_command(cmd); + let current_dir = std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?; + let absolute_path = resolve_path(&target_path, ¤t_dir); + + // Use a more reliable ls format for Unix systems + let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + // Use --time-style=long-iso for consistent parsing and -la for details + if cmd == "ls" { + format!("ls -la --time-style=long-iso \"{}\"", absolute_path.display()) + } else if cmd.starts_with("ls ") { + // Check if user already specified format options + if !cmd.contains(" -l") && !cmd.contains(" -a") && !cmd.contains("--time-style") { + format!("{} -la --time-style=long-iso", cmd) + } else if !cmd.contains("--time-style") { + format!("{} --time-style=long-iso", cmd) + } else { + cmd.to_string() + } + } else { + cmd.to_string() + } + } else { + // Windows - use dir command with specific formatting + format!("dir /a \"{}\"", absolute_path.display()) + }; + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", &enhanced_command]).output() + } else { + Command::new("sh").arg("-c").arg(&enhanced_command).output() + }; + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + + if !stderr.is_empty() && stdout.is_empty() { + return Ok(stderr.to_string()); + } + + // Process the output with robust parsing + Ok(crate::utils::format_directory_listing_robust(&stdout, &absolute_path)) + } + Err(e) => Err(format!("Failed to run command: {}", e)), + } +} + +fn extract_path_from_ls_command(cmd: &str) -> String { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + + // Look for the last argument that doesn't start with '-' + for part in parts.iter().rev() { + if !part.starts_with('-') && *part != "ls" && *part != "dir" { + return part.to_string(); + } + } + + // Default to current directory + ".".to_string() +} + +fn resolve_path(path: &str, current_dir: &Path) -> PathBuf { + let path_buf = PathBuf::from(path); + + if path_buf.is_absolute() { + path_buf + } else { + current_dir.join(path_buf) + } +} + #[tauri::command] pub async fn run_sudo_command(command: String, password: String) -> Result { if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { @@ -157,111 +463,120 @@ pub fn get_current_dir() -> Result { #[tauri::command] pub async fn list_directory_contents(path: Option) -> Result, String> { let dir_path = match path { - Some(p) if !p.is_empty() => p, - _ => ".".to_string(), - }; - - let ls_command = if cfg!(target_os = "windows") { - format!("dir /b \"{}\"", dir_path) - } else { - format!("ls -la \"{}\"", dir_path) - }; - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &ls_command]).output() - } else { - Command::new("sh").arg("-c").arg(&ls_command).output() + Some(p) if !p.is_empty() => { + let expanded = crate::utils::expand_home_path(&p) + .map_err(|e| format!("Failed to expand path: {}", e))?; + PathBuf::from(expanded) + }, + _ => std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?, }; - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let lines: Vec<&str> = stdout.lines().collect(); + // Use direct file system reading instead of shell commands for reliability + match fs::read_dir(&dir_path) { + Ok(entries) => { let mut file_list = Vec::new(); - - // Skip the first line if it starts with "total" (ls summary) - let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { - 1 - } else { - 0 - }; - - for line in lines.iter().skip(start_idx) { - let line_trim = line.trim(); - if line_trim.is_empty() { - continue; - } - - // Unix-style ls output with permissions - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if line_trim.len() < 10 { - continue; - } - - let parts: Vec<&str> = line_trim.split_whitespace().collect(); - if parts.len() < 9 { - continue; + + for entry in entries { + match entry { + Ok(dir_entry) => { + let file_name = dir_entry.file_name().to_string_lossy().into_owned(); + + // Skip hidden files starting with . (make this configurable if needed) + if file_name.starts_with('.') && file_name != "." && file_name != ".." { + continue; + } + + let file_type = get_file_type_robust(&dir_entry.path()); + + match file_type { + FileType::Directory => file_list.push(format!("{}/", file_name)), + FileType::Executable => file_list.push(format!("{}*", file_name)), + FileType::Symlink => file_list.push(format!("{}@", file_name)), + FileType::Regular => file_list.push(file_name), + } } - - // Join all parts from index 8 to handle filenames with spaces - let filename = parts[8..].join(" "); - - // Skip . and .. entries - if filename == "." || filename == ".." { + Err(e) => { + eprintln!("Error reading directory entry: {}", e); continue; } - - // Check file type and add appropriate suffix - let file_type = line_trim.chars().next().unwrap_or('?'); - if file_type == 'd' { - file_list.push(format!("{}/", filename)); - } else if line_trim.contains("x") && file_type == '-' { - file_list.push(format!("{}*", filename)); - } else { - file_list.push(filename); - } - } else { - // Windows directory listing (simpler format) - if line_trim != "." && line_trim != ".." { - let path = std::path::Path::new(line_trim); - if path.is_dir() { - file_list.push(format!("{}/", line_trim)); - } else if line_trim.ends_with(".exe") - || line_trim.ends_with(".bat") - || line_trim.ends_with(".cmd") - { - file_list.push(format!("{}*", line_trim)); - } else { - file_list.push(line_trim.to_string()); - } - } } } - + + // Sort the results for consistent output + file_list.sort(); Ok(file_list) } - Err(e) => Err(format!("Failed to list directory: {}", e)), + Err(e) => Err(format!("Failed to read directory '{}': {}", dir_path.display(), e)), } } #[tauri::command] pub fn change_directory(path: String) -> Result { - let expanded_path = if path.starts_with("~") { - if let Ok(home) = std::env::var("HOME") { - path.replacen("~", &home, 1) - } else { - path - } - } else { - path - }; + let expanded_path = crate::utils::expand_home_path(&path) + .map_err(|e| format!("Failed to expand path: {}", e))?; - match std::env::set_current_dir(expanded_path) { + match std::env::set_current_dir(&expanded_path) { Ok(_) => { let new_dir = std::env::current_dir() .map_err(|e| format!("Failed to get current directory: {}", e))?; Ok(new_dir.to_string_lossy().into_owned()) }, - Err(e) => Err(format!("Failed to change directory: {}", e)) + Err(e) => Err(format!("Failed to change directory to '{}': {}", expanded_path, e)) } } + +#[derive(Debug)] +enum FileType { + Directory, + Symlink, + Executable, + Regular, +} + +fn get_file_type_robust(path: &Path) -> FileType { + // Use std::fs to get accurate file information + match fs::symlink_metadata(path) { + Ok(metadata) => { + if metadata.file_type().is_symlink() { + FileType::Symlink + } else if metadata.is_dir() { + FileType::Directory + } else if is_executable(&metadata, path) { + FileType::Executable + } else { + FileType::Regular + } + } + Err(_) => { + // Fallback to basic checks if metadata fails + if path.is_dir() { + FileType::Directory + } else { + FileType::Regular + } + } + } +} + +#[cfg(unix)] +fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode(); + mode & 0o111 != 0 // Check if any execute bit is set +} + +#[cfg(windows)] +fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { + // On Windows, check file extension + if let Some(extension) = path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") + } else { + false + } +} + +#[cfg(not(any(unix, windows)))] +fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { + false +} \ No newline at end of file diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index d543357..99b52b3 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,3 +1,61 @@ +// pub fn format_directory_listing(output: &str) -> String { +// let lines: Vec<&str> = output.lines().collect(); +// let mut formatted_output = String::new(); + +// for line in lines { +// if line.trim().is_empty() { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// continue; +// } + +// if line.starts_with("total ") || line.contains("Directory of") { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// continue; +// } + +// // Special handling for Unix-style ls output +// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// let first_char = line.chars().next().unwrap_or(' '); + +// if first_char == 'd' { +// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); +// formatted_output.push('\n'); +// continue; +// } else if first_char == 'l' { +// formatted_output.push_str(&format!("{{LINK}}{}{{/LINK}}", line)); +// formatted_output.push('\n'); +// continue; +// } else if first_char == '-' || first_char.is_alphanumeric() { +// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); +// formatted_output.push('\n'); +// continue; +// } +// } + +// // Windows DIR command handling or fallback +// let tokens: Vec<&str> = line.split_whitespace().collect(); +// if !tokens.is_empty() { +// let name = tokens.last().unwrap_or(&""); + +// if line.contains("") || name.ends_with("/") || name.ends_with("\\") { +// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); +// } else { +// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); +// } +// formatted_output.push('\n'); +// } else { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// } +// } + +// formatted_output +// } +use std::path::Path; +use std::fs; + pub fn format_directory_listing(output: &str) -> String { let lines: Vec<&str> = output.lines().collect(); let mut formatted_output = String::new(); @@ -15,7 +73,15 @@ pub fn format_directory_listing(output: &str) -> String { continue; } - // Special handling for Unix-style ls output + // Handle the new robust formatting tags + if line.contains("{DIR}") || line.contains("{FILE}") || line.contains("{LINK}") || line.contains("{EXEC}") { + let formatted_line = format_tagged_line(line); + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + continue; + } + + // Legacy handling for backwards compatibility if cfg!(target_os = "linux") || cfg!(target_os = "macos") { let first_char = line.chars().next().unwrap_or(' '); @@ -53,3 +119,354 @@ pub fn format_directory_listing(output: &str) -> String { formatted_output } + +pub fn format_directory_listing_robust(output: &str, dir_path: &Path) -> String { + let lines: Vec<&str> = output.lines().collect(); + let mut formatted_output = String::new(); + + for line in lines { + let line_trim = line.trim(); + if line_trim.is_empty() { + formatted_output.push_str(line); + formatted_output.push('\n'); + continue; + } + + // Skip total line and directory headers + if line_trim.starts_with("total ") || line_trim.contains("Directory of") { + formatted_output.push_str(line); + formatted_output.push('\n'); + continue; + } + + // Robust Unix-style ls parsing with --time-style=long-iso + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + if let Some(formatted_line) = parse_unix_ls_line_robust(line_trim, dir_path) { + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + } else { + // Fallback for lines that don't match expected format + formatted_output.push_str(line); + formatted_output.push('\n'); + } + } else { + // Windows parsing + if let Some(formatted_line) = parse_windows_dir_line_robust(line_trim, dir_path) { + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + } else { + formatted_output.push_str(line); + formatted_output.push('\n'); + } + } + } + + formatted_output +} + +fn parse_unix_ls_line_robust(line: &str, dir_path: &Path) -> Option { + // Expected format with --time-style=long-iso: + // drwxr-xr-x 2 user group 4096 2023-12-01 10:30 filename + + // Skip . and .. entries + if line.ends_with(" .") || line.ends_with(" ..") { + return None; + } + + // Check if it's a proper ls line (starts with permissions) + if line.len() < 10 || !line.chars().next().map_or(false, |c| "dl-".contains(c)) { + return None; + } + + // More robust parsing: find the filename by looking for the last space after date/time + // Format: permissions links owner group size date time filename + let parts: Vec<&str> = line.split_whitespace().collect(); + + if parts.len() < 8 { + return None; + } + + // Find filename by reconstructing from the end + // Date format: YYYY-MM-DD, Time format: HH:MM + let mut filename_start_idx = None; + + // Look for the time pattern (HH:MM) and take everything after it as filename + for (i, part) in parts.iter().enumerate() { + if part.len() == 5 && part.contains(':') && part.matches(':').count() == 1 { + // Validate it's actually a time (digits on both sides of colon) + let time_parts: Vec<&str> = part.split(':').collect(); + if time_parts.len() == 2 { + if time_parts[0].parse::().is_ok() && time_parts[1].parse::().is_ok() { + filename_start_idx = Some(i + 1); + break; + } + } + } + } + + let filename = if let Some(start_idx) = filename_start_idx { + if start_idx < parts.len() { + parts[start_idx..].join(" ") + } else { + return None; + } + } else { + // Fallback: assume last part is filename + parts.last()?.to_string() + }; + + // Skip hidden files starting with . (except if we want to show them) + if filename.starts_with('.') && filename != "." && filename != ".." { + // You might want to make this configurable + } + + // Use actual file system check instead of relying on ls output parsing + let file_path = dir_path.join(&filename); + let file_type = get_file_type_robust(&file_path); + + match file_type { + FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), + FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), + FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), + FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), + } +} + +fn parse_windows_dir_line_robust(line: &str, dir_path: &Path) -> Option { + // Windows dir output format varies, but generally: + // MM/DD/YYYY HH:MM AM/PM dirname + // MM/DD/YYYY HH:MM AM/PM 1,234 filename.ext + + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 4 { + return None; + } + + // Extract filename (last part typically) + let filename = parts.last()?.to_string(); + + // Skip . and .. entries + if filename == "." || filename == ".." { + return None; + } + + // Use file system check for accurate type detection + let file_path = dir_path.join(&filename); + let file_type = get_file_type_robust(&file_path); + + match file_type { + FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), + FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), + FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), + FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), + } +} + +fn format_tagged_line(line: &str) -> String { + if line.contains("{DIR}") { + let clean_line = line.replace("{DIR}", "").replace("{/DIR}", ""); + format!("📁 {}", clean_line.trim()) + } else if line.contains("{LINK}") { + let clean_line = line.replace("{LINK}", "").replace("{/LINK}", ""); + format!("🔗 {}", clean_line.trim()) + } else if line.contains("{EXEC}") { + let clean_line = line.replace("{EXEC}", "").replace("{/EXEC}", ""); + format!("⚙️ {}", clean_line.trim()) + } else if line.contains("{FILE}") { + let clean_line = line.replace("{FILE}", "").replace("{/FILE}", ""); + + // Add file type icons based on extension + let icon = get_file_icon(&clean_line); + format!("{} {}", icon, clean_line.trim()) + } else { + line.to_string() + } +} + +fn get_file_icon(filename: &str) -> &'static str { + let lower_name = filename.to_lowercase(); + + if lower_name.ends_with(".jpg") || lower_name.ends_with(".jpeg") || + lower_name.ends_with(".png") || lower_name.ends_with(".gif") || + lower_name.ends_with(".bmp") || lower_name.ends_with(".svg") || + lower_name.ends_with(".webp") { + "🖼️" + } else if lower_name.ends_with(".pdf") || lower_name.ends_with(".doc") || + lower_name.ends_with(".docx") || lower_name.ends_with(".txt") || + lower_name.ends_with(".md") || lower_name.ends_with(".rtf") { + "📝" + } else if lower_name.ends_with(".zip") || lower_name.ends_with(".tar") || + lower_name.ends_with(".gz") || lower_name.ends_with(".rar") || + lower_name.ends_with(".7z") || lower_name.ends_with(".bz2") { + "📦" + } else if lower_name.ends_with(".mp3") || lower_name.ends_with(".wav") || + lower_name.ends_with(".flac") || lower_name.ends_with(".ogg") || + lower_name.ends_with(".m4a") { + "🎵" + } else if lower_name.ends_with(".mp4") || lower_name.ends_with(".avi") || + lower_name.ends_with(".mkv") || lower_name.ends_with(".mov") || + lower_name.ends_with(".wmv") || lower_name.ends_with(".webm") { + "🎬" + } else if lower_name.ends_with(".js") || lower_name.ends_with(".ts") || + lower_name.ends_with(".jsx") || lower_name.ends_with(".tsx") || + lower_name.ends_with(".html") || lower_name.ends_with(".css") || + lower_name.ends_with(".json") || lower_name.ends_with(".xml") { + "💻" + } else if lower_name.ends_with(".rs") || lower_name.ends_with(".py") || + lower_name.ends_with(".java") || lower_name.ends_with(".cpp") || + lower_name.ends_with(".c") || lower_name.ends_with(".h") { + "⚡" + } else { + "📄" + } +} + +#[derive(Debug)] +enum FileType { + Directory, + Symlink, + Executable, + Regular, +} + +fn get_file_type_robust(path: &Path) -> FileType { + // Use std::fs to get accurate file information + match fs::symlink_metadata(path) { + Ok(metadata) => { + if metadata.file_type().is_symlink() { + FileType::Symlink + } else if metadata.is_dir() { + FileType::Directory + } else if is_executable(&metadata, path) { + FileType::Executable + } else { + FileType::Regular + } + } + Err(_) => { + // Fallback to basic checks if metadata fails + if path.is_dir() { + FileType::Directory + } else { + FileType::Regular + } + } + } +} + +#[cfg(unix)] +fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode(); + mode & 0o111 != 0 // Check if any execute bit is set +} + +#[cfg(windows)] +fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { + // On Windows, check file extension + if let Some(extension) = path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") + } else { + false + } +} + +#[cfg(not(any(unix, windows)))] +fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { + false +} + +// Additional utility functions for robust path handling +pub fn normalize_path(path: &str) -> String { + // Handle different path separators and clean up the path + let normalized = if cfg!(target_os = "windows") { + path.replace('/', "\\") + } else { + path.replace('\\', "/") + }; + + // Remove redundant separators + let separator = if cfg!(target_os = "windows") { "\\" } else { "/" }; + let double_sep = format!("{}{}", separator, separator); + + normalized.replace(&double_sep, separator) +} + +pub fn expand_home_path(path: &str) -> Result { + if path.starts_with("~") { + if let Ok(home) = std::env::var("HOME") { + Ok(path.replacen("~", &home, 1)) + } else if cfg!(target_os = "windows") { + if let Ok(home) = std::env::var("USERPROFILE") { + Ok(path.replacen("~", &home, 1)) + } else { + Err("Cannot determine home directory".to_string()) + } + } else { + Err("Cannot determine home directory".to_string()) + } + } else { + Ok(path.to_string()) + } +} + +pub fn get_file_size_human_readable(size: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + const THRESHOLD: u64 = 1024; + + if size == 0 { + return "0 B".to_string(); + } + + let mut size_f = size as f64; + let mut unit_index = 0; + + while size_f >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 { + size_f /= THRESHOLD as f64; + unit_index += 1; + } + + if unit_index == 0 { + format!("{} {}", size, UNITS[unit_index]) + } else { + format!("{:.1} {}", size_f, UNITS[unit_index]) + } +} + +pub fn format_permissions(mode: u32) -> String { + #[cfg(unix)] + { + let mut perms = String::with_capacity(10); + + // File type + perms.push(match mode & 0o170000 { + 0o040000 => 'd', // directory + 0o120000 => 'l', // symlink + 0o100000 => '-', // regular file + _ => '?', + }); + + // Owner permissions + perms.push(if mode & 0o400 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o200 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o100 != 0 { 'x' } else { '-' }); + + // Group permissions + perms.push(if mode & 0o040 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o020 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o010 != 0 { 'x' } else { '-' }); + + // Other permissions + perms.push(if mode & 0o004 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o002 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o001 != 0 { 'x' } else { '-' }); + + perms + } + + #[cfg(not(unix))] + { + // Simplified for non-Unix systems + "rwxrwxrwx".to_string() + } \ No newline at end of file From 0b05dbfb0b1ce4dc6e31c27f792d88fe3337f00f Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Fri, 1 Aug 2025 08:58:50 +0530 Subject: [PATCH 4/5] revert changes --- src-tauri/src/commands/shell.rs | 567 +++++++------------------------- src-tauri/src/utils/mod.rs | 419 +---------------------- 2 files changed, 127 insertions(+), 859 deletions(-) diff --git a/src-tauri/src/commands/shell.rs b/src-tauri/src/commands/shell.rs index d358903..f1bdb0a 100644 --- a/src-tauri/src/commands/shell.rs +++ b/src-tauri/src/commands/shell.rs @@ -1,273 +1,4 @@ -// use std::process::Command; - -// #[tauri::command] -// pub async fn run_shell(command: String) -> Result { -// let cmd = command.trim(); - -// if cmd == "exit" { -// return Ok("__EXIT_SHELL__".to_string()); -// } - -// // Special handling for ls/dir commands to add file type indicators -// if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { -// let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { -// if cmd == "ls" { -// "ls -la".to_string() -// } else if cmd.starts_with("ls ") { -// if !command.contains(" -l") && !command.contains(" -a") { -// format!("{} -la", command) -// } else if !command.contains(" -l") { -// format!("{} -l", command) -// } else if !command.contains(" -a") { -// format!("{} -a", command) -// } else { -// command.clone() -// } -// } else { -// command.clone() -// } -// } else { -// command.clone() -// }; - -// let output = if cfg!(target_os = "windows") { -// Command::new("cmd").args(["/C", &enhanced_command]).output() -// } else { -// Command::new("sh").arg("-c").arg(&enhanced_command).output() -// }; - -// match output { -// Ok(out) => { -// let stdout = String::from_utf8_lossy(&out.stdout); -// let stderr = String::from_utf8_lossy(&out.stderr); - -// if !stderr.is_empty() && stdout.is_empty() { -// return Ok(stderr.to_string()); -// } - -// // Process the output to add file type indicators -// return Ok(crate::utils::format_directory_listing(&stdout)); -// } -// Err(e) => return Err(format!("Failed to run command: {}", e)), -// } -// } - -// // Regular command execution (non-ls commands) -// let output = if cfg!(target_os = "windows") { -// Command::new("cmd").args(["/C", &command]).output() -// } else { -// Command::new("sh").arg("-c").arg(&command).output() -// }; - -// match output { -// Ok(out) => { -// let stdout = String::from_utf8_lossy(&out.stdout); -// let stderr = String::from_utf8_lossy(&out.stderr); - -// if !stderr.is_empty() && stdout.is_empty() { -// Ok(stderr.to_string()) -// } else if !stdout.is_empty() { -// Ok(stdout.to_string()) -// } else { -// Ok(String::from( -// "Command executed successfully with no output.", -// )) -// } -// } -// Err(e) => Err(format!("Failed to run command: {}", e)), -// } -// } - -// #[tauri::command] -// pub async fn run_sudo_command(command: String, password: String) -> Result { -// if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { -// return Err("Sudo is only supported on Linux and macOS".to_string()); -// } - -// let cmd = if command.starts_with("sudo ") { -// command[5..].to_string() -// } else { -// command -// }; - -// let temp_dir = std::env::temp_dir(); -// let output_file = temp_dir.join(format!("term_sudo_{}", std::process::id())); -// let output_path = output_file.to_string_lossy(); - -// let script = format!( -// r#"#!/bin/bash -// echo "{}" | sudo -S {} > "{}" 2>&1 -// exit_code=$? -// if [ $exit_code -ne 0 ]; then -// echo "Command failed with exit code $exit_code" >> "{}" -// fi -// "#, -// password.replace("\"", "\\\""), -// cmd.replace("\"", "\\\""), -// output_path, -// output_path -// ); - -// let script_file = temp_dir.join(format!("term_sudo_script_{}", std::process::id())); -// std::fs::write(&script_file, script).map_err(|e| format!("Failed to create script: {}", e))?; - -// #[cfg(not(target_os = "windows"))] -// { -// use std::os::unix::fs::PermissionsExt; -// let mut perms = std::fs::metadata(&script_file) -// .map_err(|e| format!("Failed to get file metadata: {}", e))? -// .permissions(); -// perms.set_mode(0o755); -// std::fs::set_permissions(&script_file, perms) -// .map_err(|e| format!("Failed to set permissions: {}", e))?; -// } - -// let _status = tokio::process::Command::new(&script_file) -// .status() -// .await -// .map_err(|e| format!("Failed to execute script: {}", e))?; - -// let _ = std::fs::remove_file(&script_file); - -// let output = std::fs::read_to_string(&output_file) -// .map_err(|e| format!("Failed to read output: {}", e))?; - -// let _ = std::fs::remove_file(&output_file); - -// if output.contains("incorrect password") -// || output.contains("Sorry, try again") -// || output.contains("Authentication failure") -// || output.contains("authentication failure") -// || output.contains("sudo: no password was provided") -// || output.contains("sudo: 1 incorrect password attempt") -// { -// return Err("Incorrect password provided".to_string()); -// } - -// Ok(output) -// } - -// #[tauri::command] -// pub fn get_current_dir() -> Result { -// std::env::current_dir() -// .map(|path| path.to_string_lossy().into_owned()) -// .map_err(|e| format!("Failed to get current directory: {}", e)) -// } - -// #[tauri::command] -// pub async fn list_directory_contents(path: Option) -> Result, String> { -// let dir_path = match path { -// Some(p) if !p.is_empty() => p, -// _ => ".".to_string(), -// }; - -// let ls_command = if cfg!(target_os = "windows") { -// format!("dir /b \"{}\"", dir_path) -// } else { -// format!("ls -la \"{}\"", dir_path) -// }; - -// let output = if cfg!(target_os = "windows") { -// Command::new("cmd").args(["/C", &ls_command]).output() -// } else { -// Command::new("sh").arg("-c").arg(&ls_command).output() -// }; - -// match output { -// Ok(out) => { -// let stdout = String::from_utf8_lossy(&out.stdout); -// let lines: Vec<&str> = stdout.lines().collect(); -// let mut file_list = Vec::new(); - -// // Skip the first line if it starts with "total" (ls summary) -// let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { -// 1 -// } else { -// 0 -// }; - -// for line in lines.iter().skip(start_idx) { -// let line_trim = line.trim(); -// if line_trim.is_empty() { -// continue; -// } - -// // Unix-style ls output with permissions -// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { -// if line_trim.len() < 10 { -// continue; -// } - -// let parts: Vec<&str> = line_trim.split_whitespace().collect(); -// if parts.len() < 9 { -// continue; -// } - -// // Join all parts from index 8 to handle filenames with spaces -// let filename = parts[8..].join(" "); - -// // Skip . and .. entries -// if filename == "." || filename == ".." { -// continue; -// } - -// // Check file type and add appropriate suffix -// let file_type = line_trim.chars().next().unwrap_or('?'); -// if file_type == 'd' { -// file_list.push(format!("{}/", filename)); -// } else if line_trim.contains("x") && file_type == '-' { -// file_list.push(format!("{}*", filename)); -// } else { -// file_list.push(filename); -// } -// } else { -// // Windows directory listing (simpler format) -// if line_trim != "." && line_trim != ".." { -// let path = std::path::Path::new(line_trim); -// if path.is_dir() { -// file_list.push(format!("{}/", line_trim)); -// } else if line_trim.ends_with(".exe") -// || line_trim.ends_with(".bat") -// || line_trim.ends_with(".cmd") -// { -// file_list.push(format!("{}*", line_trim)); -// } else { -// file_list.push(line_trim.to_string()); -// } -// } -// } -// } - -// Ok(file_list) -// } -// Err(e) => Err(format!("Failed to list directory: {}", e)), -// } -// } - -// #[tauri::command] -// pub fn change_directory(path: String) -> Result { -// let expanded_path = if path.starts_with("~") { -// if let Ok(home) = std::env::var("HOME") { -// path.replacen("~", &home, 1) -// } else { -// path -// } -// } else { -// path -// }; - -// match std::env::set_current_dir(expanded_path) { -// Ok(_) => { -// let new_dir = std::env::current_dir() -// .map_err(|e| format!("Failed to get current directory: {}", e))?; -// Ok(new_dir.to_string_lossy().into_owned()) -// }, -// Err(e) => Err(format!("Failed to change directory: {}", e)) -// } -// } use std::process::Command; -use std::path::{Path, PathBuf}; -use std::fs; #[tauri::command] pub async fn run_shell(command: String) -> Result { @@ -277,9 +8,48 @@ pub async fn run_shell(command: String) -> Result { return Ok("__EXIT_SHELL__".to_string()); } - // Special handling for ls/dir commands with robust parsing + // Special handling for ls/dir commands to add file type indicators if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { - return handle_ls_command(command).await; + let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + if cmd == "ls" { + "ls -la".to_string() + } else if cmd.starts_with("ls ") { + if !command.contains(" -l") && !command.contains(" -a") { + format!("{} -la", command) + } else if !command.contains(" -l") { + format!("{} -l", command) + } else if !command.contains(" -a") { + format!("{} -a", command) + } else { + command.clone() + } + } else { + command.clone() + } + } else { + command.clone() + }; + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", &enhanced_command]).output() + } else { + Command::new("sh").arg("-c").arg(&enhanced_command).output() + }; + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + + if !stderr.is_empty() && stdout.is_empty() { + return Ok(stderr.to_string()); + } + + // Process the output to add file type indicators + return Ok(crate::utils::format_directory_listing(&stdout)); + } + Err(e) => return Err(format!("Failed to run command: {}", e)), + } } // Regular command execution (non-ls commands) @@ -308,82 +78,6 @@ pub async fn run_shell(command: String) -> Result { } } -async fn handle_ls_command(original_command: String) -> Result { - let cmd = original_command.trim(); - - // Extract the path from the ls command - let target_path = extract_path_from_ls_command(cmd); - let current_dir = std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?; - let absolute_path = resolve_path(&target_path, ¤t_dir); - - // Use a more reliable ls format for Unix systems - let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - // Use --time-style=long-iso for consistent parsing and -la for details - if cmd == "ls" { - format!("ls -la --time-style=long-iso \"{}\"", absolute_path.display()) - } else if cmd.starts_with("ls ") { - // Check if user already specified format options - if !cmd.contains(" -l") && !cmd.contains(" -a") && !cmd.contains("--time-style") { - format!("{} -la --time-style=long-iso", cmd) - } else if !cmd.contains("--time-style") { - format!("{} --time-style=long-iso", cmd) - } else { - cmd.to_string() - } - } else { - cmd.to_string() - } - } else { - // Windows - use dir command with specific formatting - format!("dir /a \"{}\"", absolute_path.display()) - }; - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &enhanced_command]).output() - } else { - Command::new("sh").arg("-c").arg(&enhanced_command).output() - }; - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - - if !stderr.is_empty() && stdout.is_empty() { - return Ok(stderr.to_string()); - } - - // Process the output with robust parsing - Ok(crate::utils::format_directory_listing_robust(&stdout, &absolute_path)) - } - Err(e) => Err(format!("Failed to run command: {}", e)), - } -} - -fn extract_path_from_ls_command(cmd: &str) -> String { - let parts: Vec<&str> = cmd.split_whitespace().collect(); - - // Look for the last argument that doesn't start with '-' - for part in parts.iter().rev() { - if !part.starts_with('-') && *part != "ls" && *part != "dir" { - return part.to_string(); - } - } - - // Default to current directory - ".".to_string() -} - -fn resolve_path(path: &str, current_dir: &Path) -> PathBuf { - let path_buf = PathBuf::from(path); - - if path_buf.is_absolute() { - path_buf - } else { - current_dir.join(path_buf) - } -} - #[tauri::command] pub async fn run_sudo_command(command: String, password: String) -> Result { if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { @@ -463,120 +157,111 @@ pub fn get_current_dir() -> Result { #[tauri::command] pub async fn list_directory_contents(path: Option) -> Result, String> { let dir_path = match path { - Some(p) if !p.is_empty() => { - let expanded = crate::utils::expand_home_path(&p) - .map_err(|e| format!("Failed to expand path: {}", e))?; - PathBuf::from(expanded) - }, - _ => std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?, + Some(p) if !p.is_empty() => p, + _ => ".".to_string(), + }; + + let ls_command = if cfg!(target_os = "windows") { + format!("dir /b \"{}\"", dir_path) + } else { + format!("ls -la \"{}\"", dir_path) }; - // Use direct file system reading instead of shell commands for reliability - match fs::read_dir(&dir_path) { - Ok(entries) => { + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", &ls_command]).output() + } else { + Command::new("sh").arg("-c").arg(&ls_command).output() + }; + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + let lines: Vec<&str> = stdout.lines().collect(); let mut file_list = Vec::new(); - - for entry in entries { - match entry { - Ok(dir_entry) => { - let file_name = dir_entry.file_name().to_string_lossy().into_owned(); - - // Skip hidden files starting with . (make this configurable if needed) - if file_name.starts_with('.') && file_name != "." && file_name != ".." { - continue; - } - - let file_type = get_file_type_robust(&dir_entry.path()); - - match file_type { - FileType::Directory => file_list.push(format!("{}/", file_name)), - FileType::Executable => file_list.push(format!("{}*", file_name)), - FileType::Symlink => file_list.push(format!("{}@", file_name)), - FileType::Regular => file_list.push(file_name), - } + + // Skip the first line if it starts with "total" (ls summary) + let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { + 1 + } else { + 0 + }; + + for line in lines.iter().skip(start_idx) { + let line_trim = line.trim(); + if line_trim.is_empty() { + continue; + } + + // Unix-style ls output with permissions + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + if line_trim.len() < 10 { + continue; + } + + let parts: Vec<&str> = line_trim.split_whitespace().collect(); + if parts.len() < 9 { + continue; } - Err(e) => { - eprintln!("Error reading directory entry: {}", e); + + // Join all parts from index 8 to handle filenames with spaces + let filename = parts[8..].join(" "); + + // Skip . and .. entries + if filename == "." || filename == ".." { continue; } + + // Check file type and add appropriate suffix + let file_type = line_trim.chars().next().unwrap_or('?'); + if file_type == 'd' { + file_list.push(format!("{}/", filename)); + } else if line_trim.contains("x") && file_type == '-' { + file_list.push(format!("{}*", filename)); + } else { + file_list.push(filename); + } + } else { + // Windows directory listing (simpler format) + if line_trim != "." && line_trim != ".." { + let path = std::path::Path::new(line_trim); + if path.is_dir() { + file_list.push(format!("{}/", line_trim)); + } else if line_trim.ends_with(".exe") + || line_trim.ends_with(".bat") + || line_trim.ends_with(".cmd") + { + file_list.push(format!("{}*", line_trim)); + } else { + file_list.push(line_trim.to_string()); + } + } } } - - // Sort the results for consistent output - file_list.sort(); + Ok(file_list) } - Err(e) => Err(format!("Failed to read directory '{}': {}", dir_path.display(), e)), + Err(e) => Err(format!("Failed to list directory: {}", e)), } } #[tauri::command] pub fn change_directory(path: String) -> Result { - let expanded_path = crate::utils::expand_home_path(&path) - .map_err(|e| format!("Failed to expand path: {}", e))?; + let expanded_path = if path.starts_with("~") { + if let Ok(home) = std::env::var("HOME") { + path.replacen("~", &home, 1) + } else { + path + } + } else { + path + }; - match std::env::set_current_dir(&expanded_path) { + match std::env::set_current_dir(expanded_path) { Ok(_) => { let new_dir = std::env::current_dir() .map_err(|e| format!("Failed to get current directory: {}", e))?; Ok(new_dir.to_string_lossy().into_owned()) }, - Err(e) => Err(format!("Failed to change directory to '{}': {}", expanded_path, e)) + Err(e) => Err(format!("Failed to change directory: {}", e)) } } - -#[derive(Debug)] -enum FileType { - Directory, - Symlink, - Executable, - Regular, -} - -fn get_file_type_robust(path: &Path) -> FileType { - // Use std::fs to get accurate file information - match fs::symlink_metadata(path) { - Ok(metadata) => { - if metadata.file_type().is_symlink() { - FileType::Symlink - } else if metadata.is_dir() { - FileType::Directory - } else if is_executable(&metadata, path) { - FileType::Executable - } else { - FileType::Regular - } - } - Err(_) => { - // Fallback to basic checks if metadata fails - if path.is_dir() { - FileType::Directory - } else { - FileType::Regular - } - } - } -} - -#[cfg(unix)] -fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { - use std::os::unix::fs::PermissionsExt; - let mode = metadata.permissions().mode(); - mode & 0o111 != 0 // Check if any execute bit is set -} - -#[cfg(windows)] -fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { - // On Windows, check file extension - if let Some(extension) = path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") - } else { - false - } -} - -#[cfg(not(any(unix, windows)))] -fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { - false -} \ No newline at end of file diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 99b52b3..d543357 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,61 +1,3 @@ -// pub fn format_directory_listing(output: &str) -> String { -// let lines: Vec<&str> = output.lines().collect(); -// let mut formatted_output = String::new(); - -// for line in lines { -// if line.trim().is_empty() { -// formatted_output.push_str(line); -// formatted_output.push('\n'); -// continue; -// } - -// if line.starts_with("total ") || line.contains("Directory of") { -// formatted_output.push_str(line); -// formatted_output.push('\n'); -// continue; -// } - -// // Special handling for Unix-style ls output -// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { -// let first_char = line.chars().next().unwrap_or(' '); - -// if first_char == 'd' { -// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); -// formatted_output.push('\n'); -// continue; -// } else if first_char == 'l' { -// formatted_output.push_str(&format!("{{LINK}}{}{{/LINK}}", line)); -// formatted_output.push('\n'); -// continue; -// } else if first_char == '-' || first_char.is_alphanumeric() { -// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); -// formatted_output.push('\n'); -// continue; -// } -// } - -// // Windows DIR command handling or fallback -// let tokens: Vec<&str> = line.split_whitespace().collect(); -// if !tokens.is_empty() { -// let name = tokens.last().unwrap_or(&""); - -// if line.contains("") || name.ends_with("/") || name.ends_with("\\") { -// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); -// } else { -// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); -// } -// formatted_output.push('\n'); -// } else { -// formatted_output.push_str(line); -// formatted_output.push('\n'); -// } -// } - -// formatted_output -// } -use std::path::Path; -use std::fs; - pub fn format_directory_listing(output: &str) -> String { let lines: Vec<&str> = output.lines().collect(); let mut formatted_output = String::new(); @@ -73,15 +15,7 @@ pub fn format_directory_listing(output: &str) -> String { continue; } - // Handle the new robust formatting tags - if line.contains("{DIR}") || line.contains("{FILE}") || line.contains("{LINK}") || line.contains("{EXEC}") { - let formatted_line = format_tagged_line(line); - formatted_output.push_str(&formatted_line); - formatted_output.push('\n'); - continue; - } - - // Legacy handling for backwards compatibility + // Special handling for Unix-style ls output if cfg!(target_os = "linux") || cfg!(target_os = "macos") { let first_char = line.chars().next().unwrap_or(' '); @@ -119,354 +53,3 @@ pub fn format_directory_listing(output: &str) -> String { formatted_output } - -pub fn format_directory_listing_robust(output: &str, dir_path: &Path) -> String { - let lines: Vec<&str> = output.lines().collect(); - let mut formatted_output = String::new(); - - for line in lines { - let line_trim = line.trim(); - if line_trim.is_empty() { - formatted_output.push_str(line); - formatted_output.push('\n'); - continue; - } - - // Skip total line and directory headers - if line_trim.starts_with("total ") || line_trim.contains("Directory of") { - formatted_output.push_str(line); - formatted_output.push('\n'); - continue; - } - - // Robust Unix-style ls parsing with --time-style=long-iso - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if let Some(formatted_line) = parse_unix_ls_line_robust(line_trim, dir_path) { - formatted_output.push_str(&formatted_line); - formatted_output.push('\n'); - } else { - // Fallback for lines that don't match expected format - formatted_output.push_str(line); - formatted_output.push('\n'); - } - } else { - // Windows parsing - if let Some(formatted_line) = parse_windows_dir_line_robust(line_trim, dir_path) { - formatted_output.push_str(&formatted_line); - formatted_output.push('\n'); - } else { - formatted_output.push_str(line); - formatted_output.push('\n'); - } - } - } - - formatted_output -} - -fn parse_unix_ls_line_robust(line: &str, dir_path: &Path) -> Option { - // Expected format with --time-style=long-iso: - // drwxr-xr-x 2 user group 4096 2023-12-01 10:30 filename - - // Skip . and .. entries - if line.ends_with(" .") || line.ends_with(" ..") { - return None; - } - - // Check if it's a proper ls line (starts with permissions) - if line.len() < 10 || !line.chars().next().map_or(false, |c| "dl-".contains(c)) { - return None; - } - - // More robust parsing: find the filename by looking for the last space after date/time - // Format: permissions links owner group size date time filename - let parts: Vec<&str> = line.split_whitespace().collect(); - - if parts.len() < 8 { - return None; - } - - // Find filename by reconstructing from the end - // Date format: YYYY-MM-DD, Time format: HH:MM - let mut filename_start_idx = None; - - // Look for the time pattern (HH:MM) and take everything after it as filename - for (i, part) in parts.iter().enumerate() { - if part.len() == 5 && part.contains(':') && part.matches(':').count() == 1 { - // Validate it's actually a time (digits on both sides of colon) - let time_parts: Vec<&str> = part.split(':').collect(); - if time_parts.len() == 2 { - if time_parts[0].parse::().is_ok() && time_parts[1].parse::().is_ok() { - filename_start_idx = Some(i + 1); - break; - } - } - } - } - - let filename = if let Some(start_idx) = filename_start_idx { - if start_idx < parts.len() { - parts[start_idx..].join(" ") - } else { - return None; - } - } else { - // Fallback: assume last part is filename - parts.last()?.to_string() - }; - - // Skip hidden files starting with . (except if we want to show them) - if filename.starts_with('.') && filename != "." && filename != ".." { - // You might want to make this configurable - } - - // Use actual file system check instead of relying on ls output parsing - let file_path = dir_path.join(&filename); - let file_type = get_file_type_robust(&file_path); - - match file_type { - FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), - FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), - FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), - FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), - } -} - -fn parse_windows_dir_line_robust(line: &str, dir_path: &Path) -> Option { - // Windows dir output format varies, but generally: - // MM/DD/YYYY HH:MM AM/PM dirname - // MM/DD/YYYY HH:MM AM/PM 1,234 filename.ext - - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() < 4 { - return None; - } - - // Extract filename (last part typically) - let filename = parts.last()?.to_string(); - - // Skip . and .. entries - if filename == "." || filename == ".." { - return None; - } - - // Use file system check for accurate type detection - let file_path = dir_path.join(&filename); - let file_type = get_file_type_robust(&file_path); - - match file_type { - FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), - FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), - FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), - FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), - } -} - -fn format_tagged_line(line: &str) -> String { - if line.contains("{DIR}") { - let clean_line = line.replace("{DIR}", "").replace("{/DIR}", ""); - format!("📁 {}", clean_line.trim()) - } else if line.contains("{LINK}") { - let clean_line = line.replace("{LINK}", "").replace("{/LINK}", ""); - format!("🔗 {}", clean_line.trim()) - } else if line.contains("{EXEC}") { - let clean_line = line.replace("{EXEC}", "").replace("{/EXEC}", ""); - format!("⚙️ {}", clean_line.trim()) - } else if line.contains("{FILE}") { - let clean_line = line.replace("{FILE}", "").replace("{/FILE}", ""); - - // Add file type icons based on extension - let icon = get_file_icon(&clean_line); - format!("{} {}", icon, clean_line.trim()) - } else { - line.to_string() - } -} - -fn get_file_icon(filename: &str) -> &'static str { - let lower_name = filename.to_lowercase(); - - if lower_name.ends_with(".jpg") || lower_name.ends_with(".jpeg") || - lower_name.ends_with(".png") || lower_name.ends_with(".gif") || - lower_name.ends_with(".bmp") || lower_name.ends_with(".svg") || - lower_name.ends_with(".webp") { - "🖼️" - } else if lower_name.ends_with(".pdf") || lower_name.ends_with(".doc") || - lower_name.ends_with(".docx") || lower_name.ends_with(".txt") || - lower_name.ends_with(".md") || lower_name.ends_with(".rtf") { - "📝" - } else if lower_name.ends_with(".zip") || lower_name.ends_with(".tar") || - lower_name.ends_with(".gz") || lower_name.ends_with(".rar") || - lower_name.ends_with(".7z") || lower_name.ends_with(".bz2") { - "📦" - } else if lower_name.ends_with(".mp3") || lower_name.ends_with(".wav") || - lower_name.ends_with(".flac") || lower_name.ends_with(".ogg") || - lower_name.ends_with(".m4a") { - "🎵" - } else if lower_name.ends_with(".mp4") || lower_name.ends_with(".avi") || - lower_name.ends_with(".mkv") || lower_name.ends_with(".mov") || - lower_name.ends_with(".wmv") || lower_name.ends_with(".webm") { - "🎬" - } else if lower_name.ends_with(".js") || lower_name.ends_with(".ts") || - lower_name.ends_with(".jsx") || lower_name.ends_with(".tsx") || - lower_name.ends_with(".html") || lower_name.ends_with(".css") || - lower_name.ends_with(".json") || lower_name.ends_with(".xml") { - "💻" - } else if lower_name.ends_with(".rs") || lower_name.ends_with(".py") || - lower_name.ends_with(".java") || lower_name.ends_with(".cpp") || - lower_name.ends_with(".c") || lower_name.ends_with(".h") { - "⚡" - } else { - "📄" - } -} - -#[derive(Debug)] -enum FileType { - Directory, - Symlink, - Executable, - Regular, -} - -fn get_file_type_robust(path: &Path) -> FileType { - // Use std::fs to get accurate file information - match fs::symlink_metadata(path) { - Ok(metadata) => { - if metadata.file_type().is_symlink() { - FileType::Symlink - } else if metadata.is_dir() { - FileType::Directory - } else if is_executable(&metadata, path) { - FileType::Executable - } else { - FileType::Regular - } - } - Err(_) => { - // Fallback to basic checks if metadata fails - if path.is_dir() { - FileType::Directory - } else { - FileType::Regular - } - } - } -} - -#[cfg(unix)] -fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { - use std::os::unix::fs::PermissionsExt; - let mode = metadata.permissions().mode(); - mode & 0o111 != 0 // Check if any execute bit is set -} - -#[cfg(windows)] -fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { - // On Windows, check file extension - if let Some(extension) = path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") - } else { - false - } -} - -#[cfg(not(any(unix, windows)))] -fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { - false -} - -// Additional utility functions for robust path handling -pub fn normalize_path(path: &str) -> String { - // Handle different path separators and clean up the path - let normalized = if cfg!(target_os = "windows") { - path.replace('/', "\\") - } else { - path.replace('\\', "/") - }; - - // Remove redundant separators - let separator = if cfg!(target_os = "windows") { "\\" } else { "/" }; - let double_sep = format!("{}{}", separator, separator); - - normalized.replace(&double_sep, separator) -} - -pub fn expand_home_path(path: &str) -> Result { - if path.starts_with("~") { - if let Ok(home) = std::env::var("HOME") { - Ok(path.replacen("~", &home, 1)) - } else if cfg!(target_os = "windows") { - if let Ok(home) = std::env::var("USERPROFILE") { - Ok(path.replacen("~", &home, 1)) - } else { - Err("Cannot determine home directory".to_string()) - } - } else { - Err("Cannot determine home directory".to_string()) - } - } else { - Ok(path.to_string()) - } -} - -pub fn get_file_size_human_readable(size: u64) -> String { - const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; - const THRESHOLD: u64 = 1024; - - if size == 0 { - return "0 B".to_string(); - } - - let mut size_f = size as f64; - let mut unit_index = 0; - - while size_f >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 { - size_f /= THRESHOLD as f64; - unit_index += 1; - } - - if unit_index == 0 { - format!("{} {}", size, UNITS[unit_index]) - } else { - format!("{:.1} {}", size_f, UNITS[unit_index]) - } -} - -pub fn format_permissions(mode: u32) -> String { - #[cfg(unix)] - { - let mut perms = String::with_capacity(10); - - // File type - perms.push(match mode & 0o170000 { - 0o040000 => 'd', // directory - 0o120000 => 'l', // symlink - 0o100000 => '-', // regular file - _ => '?', - }); - - // Owner permissions - perms.push(if mode & 0o400 != 0 { 'r' } else { '-' }); - perms.push(if mode & 0o200 != 0 { 'w' } else { '-' }); - perms.push(if mode & 0o100 != 0 { 'x' } else { '-' }); - - // Group permissions - perms.push(if mode & 0o040 != 0 { 'r' } else { '-' }); - perms.push(if mode & 0o020 != 0 { 'w' } else { '-' }); - perms.push(if mode & 0o010 != 0 { 'x' } else { '-' }); - - // Other permissions - perms.push(if mode & 0o004 != 0 { 'r' } else { '-' }); - perms.push(if mode & 0o002 != 0 { 'w' } else { '-' }); - perms.push(if mode & 0o001 != 0 { 'x' } else { '-' }); - - perms - } - - #[cfg(not(unix))] - { - // Simplified for non-Unix systems - "rwxrwxrwx".to_string() - } \ No newline at end of file From 57a52930137dbfd22ef2ef317d8ed959022008d0 Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Fri, 29 Aug 2025 23:45:07 +0530 Subject: [PATCH 5/5] Fixed missing code --- src-tauri/src/sudo.rs | 297 +++++++++++++++++++++++++++++++++++++++ src/hooks/useFastSudo.ts | 84 +++++++++++ 2 files changed, 381 insertions(+) diff --git a/src-tauri/src/sudo.rs b/src-tauri/src/sudo.rs index e69de29..f255e63 100644 --- a/src-tauri/src/sudo.rs +++ b/src-tauri/src/sudo.rs @@ -0,0 +1,297 @@ +// src-tauri/src/sudo.rs +use std::collections::HashMap; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use std::io::Write; +use tauri::State; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub struct AuthToken { + timestamp: Instant, + user_id: u32, +} + +#[derive(Default)] +pub struct SudoCache { + pub tokens: Arc>>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SudoRequest { + pub command: String, + pub args: Vec, + pub password: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SudoResponse { + pub success: bool, + pub output: String, + pub error: Option, + pub cached: bool, + pub needs_password: bool, +} + +impl SudoCache { + pub fn new() -> Self { + Self { + tokens: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn is_authenticated(&self, user_id: u32, timeout_minutes: u64) -> bool { + if let Ok(tokens) = self.tokens.lock() { + if let Some(token) = tokens.get(&user_id) { + return token.timestamp.elapsed() < Duration::from_secs(timeout_minutes * 60); + } + } + false + } + + pub fn authenticate(&self, user_id: u32) { + if let Ok(mut tokens) = self.tokens.lock() { + tokens.insert(user_id, AuthToken { + timestamp: Instant::now(), + user_id, + }); + } + } + + pub fn clear_expired(&self, timeout_minutes: u64) { + if let Ok(mut tokens) = self.tokens.lock() { + let timeout = Duration::from_secs(timeout_minutes * 60); + tokens.retain(|_, token| token.timestamp.elapsed() < timeout); + } + } + + pub fn clear_all(&self) { + if let Ok(mut tokens) = self.tokens.lock() { + tokens.clear(); + } + } +} + +fn get_current_user_id() -> Result> { + unsafe { + Ok(libc::getuid()) + } +} + +fn verify_password(password: &str) -> Result> { + let mut child = Command::new("sudo") + .args(&["-S", "-v"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(stdin) = child.stdin.as_mut() { + writeln!(stdin, "{}", password)?; + } + + let output = child.wait_with_output()?; + Ok(output.status.success()) +} + +async fn execute_sudo_command( + command: &str, + args: &[String], + use_cached: bool, +) -> Result { + let mut cmd_args = Vec::new(); + + if use_cached { + cmd_args.push("-n".to_string()); // Non-interactive mode for cached auth + } + + cmd_args.push(command.to_string()); + cmd_args.extend_from_slice(args); + + let output = Command::new("sudo") + .args(&cmd_args) + .output() + .map_err(|e| format!("Failed to execute command: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if output.status.success() { + Ok(SudoResponse { + success: true, + output: stdout, + error: None, + cached: use_cached, + needs_password: false, + }) + } else { + // Check if it failed because of missing authentication + if use_cached && stderr.contains("no password entry") { + Ok(SudoResponse { + success: false, + output: String::new(), + error: Some("Authentication required".to_string()), + cached: false, + needs_password: true, + }) + } else { + Ok(SudoResponse { + success: false, + output: stdout, + error: Some(stderr), + cached: use_cached, + needs_password: false, + }) + } + } +} + +#[tauri::command] +pub async fn fast_sudo( + request: SudoRequest, + cache: State<'_, SudoCache>, +) -> Result { + let user_id = get_current_user_id().map_err(|e| e.to_string())?; + let timeout_minutes = 15; // 15 minute timeout + + // Clear expired tokens + cache.clear_expired(timeout_minutes); + + let mut needs_auth = true; + let mut use_cached = false; + + // Check if already authenticated + if cache.is_authenticated(user_id, timeout_minutes) { + use_cached = true; + needs_auth = false; + } + + // If we have cached auth, try to use it first + if use_cached { + match execute_sudo_command(&request.command, &request.args, true).await { + Ok(response) => { + if response.success { + return Ok(response); + } else if response.needs_password { + // Cache expired, need to re-authenticate + needs_auth = true; + use_cached = false; + } else { + return Ok(response); + } + } + Err(e) => return Err(e), + } + } + + // If not cached and no password provided, request password + if needs_auth && request.password.is_none() { + return Ok(SudoResponse { + success: false, + output: String::new(), + error: Some("Password required".to_string()), + cached: false, + needs_password: true, + }); + } + + // Verify password if needed + if needs_auth { + if let Some(ref password) = request.password { + match verify_password(password) { + Ok(true) => { + cache.authenticate(user_id); + use_cached = false; // First time auth, not cached + } + Ok(false) => { + return Ok(SudoResponse { + success: false, + output: String::new(), + error: Some("Invalid password".to_string()), + cached: false, + needs_password: true, + }); + } + Err(e) => { + return Ok(SudoResponse { + success: false, + output: String::new(), + error: Some(format!("Authentication error: {}", e)), + cached: false, + needs_password: false, + }); + } + } + } + } + + // Execute the command + execute_sudo_command(&request.command, &request.args, false).await +} + +#[tauri::command] +pub async fn clear_sudo_cache(cache: State<'_, SudoCache>) -> Result<(), String> { + cache.clear_all(); + + // Also clear system sudo cache + let _ = Command::new("sudo") + .args(&["-k"]) + .output(); + + Ok(()) +} + +#[tauri::command] +pub async fn check_sudo_privileges() -> Result { + let output = Command::new("sudo") + .args(&["-n", "true"]) + .output() + .map_err(|e| format!("Failed to check privileges: {}", e))?; + + Ok(output.status.success()) +} + +#[tauri::command] +pub async fn direct_privilege_escalation( + command: String, + args: Vec, +) -> Result { + // Direct privilege escalation without sudo + + // For now, fall back to regular sudo + let output = Command::new("sudo") + .arg(&command) + .args(&args) + .output() + .map_err(|e| format!("Failed to execute command: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + Ok(SudoResponse { + success: output.status.success(), + output: stdout, + error: if stderr.is_empty() { None } else { Some(stderr) }, + cached: false, + needs_password: false, + }) +} + +// Utility function to parse sudo commands +pub fn parse_sudo_command(input: &str) -> Option<(String, Vec)> { + let parts: Vec<&str> = input.trim().split_whitespace().collect(); + + if parts.is_empty() || parts[0] != "sudo" { + return None; + } + + if parts.len() < 2 { + return None; + } + + let command = parts[1].to_string(); + let args = parts[2..].iter().map(|s| s.to_string()).collect(); + + Some((command, args)) +} \ No newline at end of file diff --git a/src/hooks/useFastSudo.ts b/src/hooks/useFastSudo.ts index e69de29..da69924 100644 --- a/src/hooks/useFastSudo.ts +++ b/src/hooks/useFastSudo.ts @@ -0,0 +1,84 @@ +// src/hooks/useFastSudo.ts +import { invoke } from '@tauri-apps/api/core'; +import { useState, useCallback } from 'react'; + +interface SudoRequest { + command: string; + args: string[]; + password?: string; +} + +interface SudoResponse { + success: boolean; + output: string; + error?: string; + cached: boolean; + needs_password: boolean; +} + +export const useFastSudo = () => { + const [isLoading, setIsLoading] = useState(false); + const [needsPassword, setNeedsPassword] = useState(false); + + const executeSudo = useCallback(async (request: SudoRequest): Promise => { + setIsLoading(true); + try { + const response = await invoke('fast_sudo', { request }); + + setNeedsPassword(response.needs_password); + + return response; + } catch (error) { + console.error('Fast sudo error:', error); + return { + success: false, + output: '', + error: `Failed to execute sudo command: ${error}`, + cached: false, + needs_password: false, + }; + } finally { + setIsLoading(false); + } + }, []); + + const clearCache = useCallback(async (): Promise => { + try { + await invoke('clear_sudo_cache'); + setNeedsPassword(false); + } catch (error) { + console.error('Failed to clear sudo cache:', error); + } + }, []); + + const checkPrivileges = useCallback(async (): Promise => { + try { + return await invoke('check_sudo_privileges'); + } catch (error) { + console.error('Failed to check sudo privileges:', error); + return false; + } + }, []); + + return { + executeSudo, + clearCache, + checkPrivileges, + isLoading, + needsPassword, + }; +}; + +export const parseSudoCommand = (input: string): { command: string; args: string[] } | null => { + const trimmed = input.trim(); + const parts = trimmed.split(/\s+/); + + if (parts.length < 2 || parts[0] !== 'sudo') { + return null; + } + + return { + command: parts[1], + args: parts.slice(2), + }; +}; \ No newline at end of file