yash_cli/startup/
init_file.rsuse super::args::InitFile;
use std::cell::RefCell;
use std::ffi::CString;
use std::num::NonZeroU64;
use std::rc::Rc;
use thiserror::Error;
use yash_env::input::{Echo, FdReader};
use yash_env::io::Fd;
use yash_env::option::Option::Interactive;
use yash_env::option::State::Off;
use yash_env::stack::Frame;
use yash_env::system::{Errno, Mode, OfdAccess, OpenFlag, SystemEx};
use yash_env::variable::ENV;
use yash_env::Env;
use yash_env::System;
use yash_semantics::expansion::expand_text;
use yash_semantics::read_eval_loop;
use yash_semantics::Handle;
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 Handle for DefaultFilePathError {
async fn handle(&self, env: &mut Env) -> 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(env: &mut Env) -> Result<String, DefaultFilePathError> {
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(
env: &mut Env,
file: InitFile,
) -> Result<String, DefaultFilePathError> {
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(env: &mut Env, path: &str) {
if path.is_empty() {
return;
}
fn open_fd<S: System>(system: &mut S, path: String) -> Result<Fd, Errno> {
let c_path = CString::new(path).map_err(|_| Errno::EILSEQ)?;
let fd = system.open(
&c_path,
OfdAccess::ReadOnly,
OpenFlag::CloseOnExec.into(),
Mode::empty(),
)?;
system.move_fd_internal(fd)
}
let fd = match open_fd(&mut env.system, path.to_owned()) {
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(FdReader::new(fd, system), &ref_env));
let start_line_number = NonZeroU64::MIN;
let source = Rc::new(Source::InitFile {
path: path.to_owned(),
});
let mut lexer = Lexer::new(input, start_line_number, source);
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(env: &mut Env, file: InitFile) {
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::option::State::On;
use yash_env::system::{Gid, Uid};
use yash_env::variable::Scope::Global;
use yash_env::VirtualSystem;
#[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 mut system = Box::new(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 mut system = Box::new(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(), "");
}
}