use std::cmp::min;
use std::ffi::{c_char, c_int, CStr, CString};
use std::sync::OnceLock;
use std::{env, mem, process, ptr};
use nix::unistd::{getpid, Pid};
use once_cell::sync::Lazy;
use crate::shm::create_shm;
use crate::{bash, error, ExecStatus};
static SHELL: OnceLock<CString> = OnceLock::new();
pub fn init(restricted: bool) {
let name = CString::new("scallop").unwrap();
let shm = create_shm("scallop", 4096).unwrap_or_else(|e| panic!("failed creating shm: {e}"));
unsafe {
bash::lib_error_handlers(Some(bash_error), Some(error::bash_warning_log));
bash::lib_init(name.as_ptr() as *mut _, shm, restricted as i32);
}
SHELL.set(name).expect("failed setting shell name");
}
#[no_mangle]
extern "C" fn bash_error(msg: *mut c_char) {
error::bash_error(msg, 1)
}
fn pid() -> Pid {
Pid::from_raw(unsafe { bash::SHELL_PID })
}
pub(crate) fn fork_init() {
let shm = create_shm("scallop", 4096).unwrap_or_else(|e| panic!("failed creating shm: {e}"));
unsafe {
bash::SHM_BUF = shm;
bash::SHELL_PID = getpid().as_raw();
}
}
pub fn reset(ignore_vars: &[&str]) {
let cached: Vec<_> = ignore_vars
.iter()
.filter_map(|&s| env::var(s).ok().map(|val| (s, val)))
.collect();
error::reset();
unsafe { bash::lib_reset() };
for (var, value) in cached {
env::set_var(var, value);
}
}
pub fn interactive() {
let mut argv_ptrs: Vec<_> = env::args()
.map(|s| CString::new(s).unwrap().into_raw())
.collect();
let argc: c_int = argv_ptrs.len().try_into().unwrap();
argv_ptrs.push(ptr::null_mut());
argv_ptrs.shrink_to_fit();
let argv = argv_ptrs.as_mut_ptr();
mem::forget(argv_ptrs);
let mut env_ptrs: Vec<_> = env::vars()
.map(|(key, val)| CString::new(format!("{key}={val}")).unwrap().into_raw())
.collect();
env_ptrs.push(ptr::null_mut());
env_ptrs.shrink_to_fit();
let env = env_ptrs.as_mut_ptr();
mem::forget(env_ptrs);
let ret: i32;
unsafe {
bash::lib_error_handlers(Some(error::stderr_output), Some(error::stderr_output));
ret = bash::bash_main(argc, argv, env);
}
process::exit(ret)
}
pub(crate) fn set_shm_error(msg: &str, bail: bool) {
let data = CString::new(msg).unwrap().into_bytes_with_nul();
let len = min(data.len(), 4096);
let status = if bail { bash::EX_LONGJMP as u8 } else { 1 };
unsafe {
let shm = bash::SHM_BUF as *mut u8;
ptr::copy_nonoverlapping(data.as_ptr(), shm, len);
ptr::write_bytes(shm.offset(4094), 0, 1);
ptr::write_bytes(shm.offset(4095), status, 1);
}
}
pub(crate) fn raise_shm_error() {
unsafe {
let shm = bash::SHM_BUF as *mut u8;
if *shm != 0 {
let msg = bash::SHM_BUF as *mut c_char;
let status = *shm.offset(4095);
error::bash_error(msg, status);
ptr::write_bytes(shm, 0, 1);
}
}
}
pub fn toggle_restricted(status: bool) {
unsafe { bash::scallop_toggle_restricted(status as i32) }
}
pub fn in_subshell() -> bool {
subshell_level() > 0
}
pub fn subshell_level() -> i32 {
unsafe { bash::SUBSHELL_LEVEL }
}
pub fn in_main() -> bool {
pid() == getpid()
}
pub fn is_restricted() -> bool {
unsafe { bash::RESTRICTED != 0 }
}
pub fn is_restricted_shell() -> bool {
unsafe { bash::RESTRICTED_SHELL != 0 }
}
pub fn restricted<F>(func: F) -> crate::Result<ExecStatus>
where
F: FnOnce() -> crate::Result<ExecStatus>,
{
let orig_path = env::var("PATH").ok();
let orig_restricted = is_restricted();
if !orig_restricted {
toggle_restricted(true);
}
let result = func();
if !orig_restricted {
toggle_restricted(false);
if let Some(s) = orig_path {
env::set_var("PATH", s);
}
}
result
}
pub static BASH_VERSION: Lazy<String> = Lazy::new(|| unsafe {
let version = CStr::from_ptr(bash::DIST_VERSION).to_str().unwrap();
format!("{version}.{}", bash::PATCH_LEVEL)
});
pub fn executing_line_number() -> i32 {
unsafe { bash::executing_line_number() }
}
#[cfg(test)]
mod tests {
use crate::{functions, source, variables};
use super::*;
#[test]
fn test_restricted() {
assert!(!is_restricted_shell());
assert!(!is_restricted());
toggle_restricted(true);
assert!(is_restricted());
toggle_restricted(false);
assert!(!is_restricted());
restricted(|| {
assert!(is_restricted());
Ok(ExecStatus::Success)
})
.unwrap();
assert!(!is_restricted());
}
#[test]
fn test_bash_version() {
assert!(!BASH_VERSION.is_empty());
}
#[test]
fn test_reset_var() {
variables::bind("VAR", "1", None, None).unwrap();
assert_eq!(variables::optional("VAR").unwrap(), "1");
reset(&[]);
assert_eq!(variables::optional("VAR"), None);
}
#[test]
fn test_reset_func() {
assert!(functions::find("func").is_none());
source::string("func() { :; }").unwrap();
assert!(functions::find("func").is_some());
reset(&[]);
assert!(functions::find("func").is_none());
}
}