use super::args::InitFile;
use std::cell::RefCell;
use std::ffi::CString;
use std::rc::Rc;
use thiserror::Error;
use yash_env::Env;
use yash_env::input::{Echo, FdReader2};
use yash_env::io::Fd;
use yash_env::io::move_fd_internal;
use yash_env::option::Option::Interactive;
use yash_env::option::State::Off;
use yash_env::parser::Config;
use yash_env::stack::Frame;
use yash_env::system::{
Close, Dup, Errno, Fcntl, GetUid, Isatty, Mode, OfdAccess, Open, OpenFlag, Write,
};
use yash_env::variable::ENV;
use yash_semantics::expansion::expand_text;
use yash_semantics::read_eval_loop;
use yash_semantics::{Handle, Runtime};
use yash_syntax::parser::lex::Lexer;
use yash_syntax::source::Source;
#[derive(Clone, Debug, Error, PartialEq)]
#[error(transparent)]
pub enum DefaultFilePathError {
ParseError(#[from] yash_syntax::parser::Error),
ExpansionError(#[from] yash_semantics::expansion::Error),
}
impl<S> Handle<S> for DefaultFilePathError
where
S: Fcntl + Isatty + Write,
{
async fn handle(&self, env: &mut Env<S>) -> yash_semantics::Result {
match self {
DefaultFilePathError::ParseError(e) => e.handle(env).await,
DefaultFilePathError::ExpansionError(e) => e.handle(env).await,
}
}
}
pub async fn default_rcfile_path<S>(env: &mut Env<S>) -> Result<String, DefaultFilePathError>
where
S: Runtime + 'static,
{
let raw_value = env.variables.get_scalar(ENV).unwrap_or_default();
let text = {
let name = ENV.to_owned();
let source = Source::VariableValue { name };
let mut lexer = Lexer::from_memory(raw_value, source);
lexer.text(|_| false, |_| false).await?
};
Ok(expand_text(env, &text).await?.0)
}
pub async fn resolve_rcfile_path<S>(
env: &mut Env<S>,
file: InitFile,
) -> Result<String, DefaultFilePathError>
where
S: GetUid + Runtime + 'static,
{
if file == InitFile::None
|| env.options.get(Interactive) == Off
|| env.system.getuid() != env.system.geteuid()
|| env.system.getgid() != env.system.getegid()
{
return Ok(String::default());
}
match file {
InitFile::None => unreachable!(),
InitFile::Default => default_rcfile_path(env).await,
InitFile::File { path } => Ok(path),
}
}
pub async fn run_init_file<S>(env: &mut Env<S>, path: &str)
where
S: Runtime + 'static,
{
if path.is_empty() {
return;
}
async fn open_fd<S>(system: &S, path: String) -> Result<Fd, Errno>
where
S: Close + Dup + Open,
{
let c_path = CString::new(path).map_err(|_| Errno::EILSEQ)?;
let fd = system
.open(
&c_path,
OfdAccess::ReadOnly,
OpenFlag::CloseOnExec.into(),
Mode::empty(),
)
.await?;
move_fd_internal(system, fd)
}
let fd = match open_fd(&env.system, path.to_owned()).await {
Ok(fd) => fd,
Err(errno) => {
env.system
.print_error(&format!(
"{}: cannot open initialization file {path:?}: {errno}\n",
&env.arg0
))
.await;
return;
}
};
let env = &mut *env.push_frame(Frame::InitFile);
let system = env.system.clone();
let ref_env = RefCell::new(&mut *env);
let input = Box::new(Echo::new(FdReader2::new(fd, system), &ref_env));
let mut config = Config::with_input(input);
config.source = Some(Rc::new(Source::InitFile {
path: path.to_owned(),
}));
let mut lexer = config.into();
_ = read_eval_loop(&ref_env, &mut { lexer }).await;
if let Err(errno) = env.system.close(fd) {
env.system
.print_error(&format!(
"{}: cannot close initialization file {path:?}: {errno}\n",
&env.arg0
))
.await;
}
}
pub async fn run_rcfile<S>(env: &mut Env<S>, file: InitFile)
where
S: GetUid + Runtime + 'static,
{
match resolve_rcfile_path(env, file).await {
Ok(path) => run_init_file(env, &path).await,
Err(e) => drop(e.handle(env).await),
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use futures_util::FutureExt as _;
use yash_env::VirtualSystem;
use yash_env::option::State::On;
use yash_env::system::{Gid, Uid};
use yash_env::variable::Scope::Global;
#[test]
fn default_rcfile_path_with_unset_env() {
let mut env = Env::new_virtual();
let result = default_rcfile_path(&mut env).now_or_never().unwrap();
assert_eq!(result.unwrap(), "");
}
#[test]
fn default_rcfile_path_with_empty_env() {
let mut env = Env::new_virtual();
env.variables
.get_or_new(ENV, Global)
.assign("", None)
.unwrap();
let result = default_rcfile_path(&mut env).now_or_never().unwrap();
assert_eq!(result.unwrap(), "");
}
#[test]
fn default_rcfile_path_with_env_without_expansion() {
let mut env = Env::new_virtual();
env.variables
.get_or_new(ENV, Global)
.assign("foo", None)
.unwrap();
let result = default_rcfile_path(&mut env).now_or_never().unwrap();
assert_eq!(result.unwrap(), "foo");
}
#[test]
fn default_rcfile_path_with_env_with_unparsable_expansion() {
let mut env = Env::new_virtual();
env.variables
.get_or_new(ENV, Global)
.assign("foo${bar", None)
.unwrap();
let result = default_rcfile_path(&mut env).now_or_never().unwrap();
assert_matches!(result, Err(DefaultFilePathError::ParseError(_)));
}
#[test]
fn default_rcfile_path_with_env_with_failing_expansion() {
let mut env = Env::new_virtual();
env.variables
.get_or_new(ENV, Global)
.assign("${unset?}", None)
.unwrap();
let result = default_rcfile_path(&mut env).now_or_never().unwrap();
assert_matches!(result, Err(DefaultFilePathError::ExpansionError(_)));
}
#[test]
fn resolve_rcfile_path_none() {
let mut env = Env::new_virtual();
env.options.set(Interactive, On);
let result = resolve_rcfile_path(&mut env, InitFile::None)
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
}
#[test]
fn resolve_rcfile_path_default() {
let mut env = Env::new_virtual();
env.options.set(Interactive, On);
env.variables
.get_or_new(ENV, Global)
.assign("foo/bar", None)
.unwrap();
let result = resolve_rcfile_path(&mut env, InitFile::Default)
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "foo/bar");
}
#[test]
fn resolve_rcfile_path_exact() {
let mut env = Env::new_virtual();
env.options.set(Interactive, On);
let path = "/path/to/rcfile".to_string();
let file = InitFile::File { path };
let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
assert_eq!(result.unwrap(), "/path/to/rcfile");
}
#[test]
fn resolve_rcfile_path_non_interactive() {
let mut env = Env::new_virtual();
env.options.set(Interactive, Off);
let path = "/path/to/rcfile".to_string();
let file = InitFile::File { path };
let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
assert_eq!(result.unwrap(), "");
}
#[test]
fn resolve_rcfile_path_non_real_user() {
let system = VirtualSystem::new();
system.current_process_mut().set_uid(Uid(0));
system.current_process_mut().set_euid(Uid(10));
let mut env = Env::with_system(system);
env.options.set(Interactive, On);
let path = "/path/to/rcfile".to_string();
let file = InitFile::File { path };
let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
assert_eq!(result.unwrap(), "");
}
#[test]
fn resolve_rcfile_path_non_real_group() {
let system = VirtualSystem::new();
system.current_process_mut().set_gid(Gid(0));
system.current_process_mut().set_egid(Gid(10));
let mut env = Env::with_system(system);
env.options.set(Interactive, On);
let path = "/path/to/rcfile".to_string();
let file = InitFile::File { path };
let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
assert_eq!(result.unwrap(), "");
}
}