use crate::Runtime;
use crate::expansion::expand_text;
use crate::expansion::expand_word;
use crate::xtrace::XTrace;
use enumset::EnumSet;
use enumset::enum_set;
use std::borrow::Cow;
use std::ffi::CString;
use std::ffi::NulError;
use std::fmt::Write as _;
use std::num::ParseIntError;
use std::ops::Deref;
use std::ops::DerefMut;
use thiserror::Error;
use yash_env::Env;
use yash_env::io::Fd;
use yash_env::io::MIN_INTERNAL_FD;
use yash_env::option::Option::Clobber;
use yash_env::option::State::Off;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::system::Stat as _;
use yash_env::system::{Close, Dup, Errno, Fcntl, FdFlag, Fstat, Mode, OfdAccess, Open, OpenFlag};
use yash_quote::quoted;
use yash_syntax::source::Location;
use yash_syntax::source::pretty::{Report, ReportType, Snippet};
use yash_syntax::syntax::HereDoc;
use yash_syntax::syntax::Redir;
use yash_syntax::syntax::RedirBody;
use yash_syntax::syntax::RedirOp;
use yash_syntax::syntax::Unquote as _;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct SavedFd {
original: Fd,
save: Option<Fd>,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ErrorCause {
#[error(transparent)]
Expansion(#[from] crate::expansion::ErrorCause),
#[error(transparent)]
NulByte(#[from] NulError),
#[error("{1}")]
FdNotOverwritten(Fd, Errno),
#[error("file descriptor {0} is reserved by the shell")]
ReservedFd(Fd),
#[error("cannot open file '{}': {}", .0.to_string_lossy(), .1)]
OpenFile(CString, Errno),
#[error("{0} is not a valid file descriptor: {1}")]
MalformedFd(String, ParseIntError),
#[error("{0} is not a readable file descriptor")]
UnreadableFd(Fd),
#[error("{0} is not a writable file descriptor")]
UnwritableFd(Fd),
#[error("cannot prepare temporary file for here-document: {0}")]
TemporaryFileUnavailable(Errno),
#[error("pipe redirection is not yet implemented")]
UnsupportedPipeRedirection,
#[error("here-string redirection is not yet implemented")]
UnsupportedHereString,
}
impl ErrorCause {
#[must_use]
pub fn message(&self) -> &str {
use ErrorCause::*;
match self {
Expansion(e) => e.message(),
NulByte(_) => "nul byte found in the pathname",
FdNotOverwritten(_, _) | ReservedFd(_) => "cannot redirect the file descriptor",
OpenFile(_, _) => "cannot open the file",
MalformedFd(_, _) => "not a valid file descriptor",
UnreadableFd(_) | UnwritableFd(_) => "cannot copy file descriptor",
TemporaryFileUnavailable(_) => "cannot prepare here-document",
UnsupportedPipeRedirection | UnsupportedHereString => "unsupported redirection",
}
}
#[must_use]
pub fn label(&self) -> Cow<'_, str> {
use ErrorCause::*;
match self {
Expansion(e) => e.label(),
NulByte(_) => "pathname should not contain a nul byte".into(),
FdNotOverwritten(_, errno) => errno.to_string().into(),
ReservedFd(fd) => format!("file descriptor {fd} reserved by shell").into(),
OpenFile(path, errno) => format!("{}: {}", path.to_string_lossy(), errno).into(),
MalformedFd(value, error) => format!("{value}: {error}").into(),
UnreadableFd(fd) => format!("{fd}: not a readable file descriptor").into(),
UnwritableFd(fd) => format!("{fd}: not a writable file descriptor").into(),
TemporaryFileUnavailable(errno) => errno.to_string().into(),
UnsupportedPipeRedirection => "pipe redirection is not yet implemented".into(),
UnsupportedHereString => "here-string redirection is not yet implemented".into(),
}
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("{cause}")]
pub struct Error {
pub cause: ErrorCause,
pub location: Location,
}
impl From<crate::expansion::Error> for Error {
fn from(e: crate::expansion::Error) -> Self {
Error {
cause: e.cause.into(),
location: e.location,
}
}
}
impl Error {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = self.cause.message().into();
report.snippets = Snippet::with_primary_span(&self.location, self.cause.label());
report
}
}
impl<'a> From<&'a Error> for Report<'a> {
#[inline(always)]
fn from(error: &'a Error) -> Self {
error.to_report()
}
}
#[derive(Debug)]
enum FdSpec {
Owned(Fd),
Borrowed(Fd),
Closed,
}
impl FdSpec {
fn as_fd(&self) -> Option<Fd> {
match self {
&FdSpec::Owned(fd) | &FdSpec::Borrowed(fd) => Some(fd),
&FdSpec::Closed => None,
}
}
fn close<S: Close>(self, system: &S) {
match self {
FdSpec::Owned(fd) => {
let _ = system.close(fd);
}
FdSpec::Borrowed(_) | FdSpec::Closed => (),
}
}
}
const MODE: Mode = Mode::ALL_READ.union(Mode::ALL_WRITE);
fn is_cloexec<S: Fcntl>(env: &Env<S>, fd: Fd) -> bool {
matches!(env.system.fcntl_getfd(fd), Ok(flags) if flags.contains(FdFlag::CloseOnExec))
}
fn into_c_string_value_and_origin(field: Field) -> Result<(CString, Location), Error> {
match CString::new(field.value) {
Ok(value) => Ok((value, field.origin)),
Err(e) => Err(Error {
cause: ErrorCause::NulByte(e),
location: field.origin,
}),
}
}
async fn open_file<S: Open>(
env: &mut Env<S>,
access: OfdAccess,
flags: EnumSet<OpenFlag>,
path: Field,
) -> Result<(FdSpec, Location), Error> {
let system = &mut env.system;
let (path, origin) = into_c_string_value_and_origin(path)?;
match system.open(&path, access, flags, MODE).await {
Ok(fd) => Ok((FdSpec::Owned(fd), origin)),
Err(errno) => Err(Error {
cause: ErrorCause::OpenFile(path, errno),
location: origin,
}),
}
}
async fn open_file_noclobber<S>(env: &mut Env<S>, path: Field) -> Result<(FdSpec, Location), Error>
where
S: Open + Fstat + Close,
{
let system = &mut env.system;
let (path, origin) = into_c_string_value_and_origin(path)?;
const FLAGS_EXCL: EnumSet<OpenFlag> = enum_set!(OpenFlag::Create | OpenFlag::Exclusive);
match system
.open(&path, OfdAccess::WriteOnly, FLAGS_EXCL, MODE)
.await
{
Ok(fd) => return Ok((FdSpec::Owned(fd), origin)),
Err(Errno::EEXIST) => (),
Err(errno) => {
return Err(Error {
cause: ErrorCause::OpenFile(path, errno),
location: origin,
});
}
}
match system
.open(&path, OfdAccess::WriteOnly, EnumSet::empty(), MODE)
.await
{
Ok(fd) => {
let is_regular = system.fstat(fd).is_ok_and(|stat| stat.is_regular_file());
if is_regular {
let _: Result<_, _> = system.close(fd);
Err(Error {
cause: ErrorCause::OpenFile(path, Errno::EEXIST),
location: origin,
})
} else {
Ok((FdSpec::Owned(fd), origin))
}
}
Err(Errno::ENOENT) => {
Err(Error {
cause: ErrorCause::OpenFile(path, Errno::EEXIST),
location: origin,
})
}
Err(errno) => Err(Error {
cause: ErrorCause::OpenFile(path, errno),
location: origin,
}),
}
}
fn copy_fd<S: Fcntl>(
env: &mut Env<S>,
target: Field,
expected_access: OfdAccess,
) -> Result<(FdSpec, Location), Error> {
if target.value == "-" {
return Ok((FdSpec::Closed, target.origin));
}
let fd = match target.value.parse() {
Ok(number) => Fd(number),
Err(error) => {
return Err(Error {
cause: ErrorCause::MalformedFd(target.value, error),
location: target.origin,
});
}
};
fn is_fd_valid<S: Fcntl>(system: &S, fd: Fd, expected_access: OfdAccess) -> bool {
system
.ofd_access(fd)
.is_ok_and(|access| access == expected_access || access == OfdAccess::ReadWrite)
}
fn fd_mode_error(
fd: Fd,
expected_access: OfdAccess,
target: Field,
) -> Result<(FdSpec, Location), Error> {
let cause = match expected_access {
OfdAccess::ReadOnly => ErrorCause::UnreadableFd(fd),
OfdAccess::WriteOnly => ErrorCause::UnwritableFd(fd),
_ => unreachable!("unexpected expected access {expected_access:?}"),
};
let location = target.origin;
Err(Error { cause, location })
}
if !is_fd_valid(&env.system, fd, expected_access) {
return fd_mode_error(fd, expected_access, target);
}
if is_cloexec(env, fd) {
return Err(Error {
cause: ErrorCause::ReservedFd(fd),
location: target.origin,
});
}
Ok((FdSpec::Borrowed(fd), target.origin))
}
async fn open_normal<S>(
env: &mut Env<S>,
operator: RedirOp,
operand: Field,
) -> Result<(FdSpec, Location), Error>
where
S: Close + Fstat + Fcntl + Open,
{
use RedirOp::*;
match operator {
FileIn => open_file(env, OfdAccess::ReadOnly, EnumSet::empty(), operand).await,
FileOut if env.options.get(Clobber) == Off => open_file_noclobber(env, operand).await,
FileOut | FileClobber => {
open_file(
env,
OfdAccess::WriteOnly,
OpenFlag::Create | OpenFlag::Truncate,
operand,
)
.await
}
FileAppend => {
open_file(
env,
OfdAccess::WriteOnly,
OpenFlag::Create | OpenFlag::Append,
operand,
)
.await
}
FileInOut => open_file(env, OfdAccess::ReadWrite, OpenFlag::Create.into(), operand).await,
FdIn => copy_fd(env, operand, OfdAccess::ReadOnly),
FdOut => copy_fd(env, operand, OfdAccess::WriteOnly),
Pipe => Err(Error {
cause: ErrorCause::UnsupportedPipeRedirection,
location: operand.origin,
}),
String => Err(Error {
cause: ErrorCause::UnsupportedHereString,
location: operand.origin,
}),
}
}
fn trace_normal(xtrace: Option<&mut XTrace>, target_fd: Fd, operator: RedirOp, operand: &Field) {
if let Some(xtrace) = xtrace {
write!(
xtrace.redirs(),
"{}{}{} ",
target_fd,
operator,
quoted(&operand.value)
)
.unwrap();
}
}
fn trace_here_doc(xtrace: Option<&mut XTrace>, target_fd: Fd, here_doc: &HereDoc, content: &str) {
if let Some(xtrace) = xtrace {
write!(xtrace.redirs(), "{target_fd}{here_doc} ").unwrap();
let (delimiter, _is_quoted) = here_doc.delimiter.unquote();
writeln!(xtrace.here_doc_contents(), "{content}{delimiter}").unwrap();
}
}
mod here_doc;
async fn perform<S>(
env: &mut Env<S>,
redir: &Redir,
xtrace: Option<&mut XTrace>,
) -> Result<(SavedFd, Option<ExitStatus>), Error>
where
S: Runtime + 'static,
{
let target_fd = redir.fd_or_default();
if is_cloexec(env, target_fd) {
return Err(Error {
cause: ErrorCause::ReservedFd(target_fd),
location: redir.body.operand().location.clone(),
});
}
let save = match env
.system
.dup(target_fd, MIN_INTERNAL_FD, FdFlag::CloseOnExec.into())
{
Ok(save_fd) => Some(save_fd),
Err(Errno::EBADF) => None,
Err(errno) => {
return Err(Error {
cause: ErrorCause::FdNotOverwritten(target_fd, errno),
location: redir.body.operand().location.clone(),
});
}
};
let (fd_spec, location, exit_status) = match &redir.body {
RedirBody::Normal { operator, operand } => {
let (expansion, exit_status) = expand_word(env, operand).await?;
trace_normal(xtrace, target_fd, *operator, &expansion);
let (fd, location) = open_normal(env, *operator, expansion).await?;
(fd, location, exit_status)
}
RedirBody::HereDoc(here_doc) => {
let content_ref = here_doc.content.get();
let content = content_ref.map(Cow::Borrowed).unwrap_or_default();
let (content, exit_status) = expand_text(env, &content).await?;
trace_here_doc(xtrace, target_fd, here_doc, &content);
let location = here_doc.delimiter.location.clone();
match here_doc::open_fd(env, content).await {
Ok(fd) => (FdSpec::Owned(fd), location, exit_status),
Err(cause) => return Err(Error { cause, location }),
}
}
};
if let Some(fd) = fd_spec.as_fd() {
if fd != target_fd {
let dup_result = env.system.dup2(fd, target_fd);
fd_spec.close(&env.system);
match dup_result {
Ok(new_fd) => assert_eq!(new_fd, target_fd),
Err(errno) => {
return Err(Error {
cause: ErrorCause::FdNotOverwritten(target_fd, errno),
location,
});
}
}
}
} else {
let _: Result<(), Errno> = env.system.close(target_fd);
}
let original = target_fd;
Ok((SavedFd { original, save }, exit_status))
}
#[derive(Debug)]
pub struct RedirGuard<'e, S: Close + Dup> {
env: &'e mut yash_env::Env<S>,
saved_fds: Vec<SavedFd>,
}
impl<S: Close + Dup> Deref for RedirGuard<'_, S> {
type Target = yash_env::Env<S>;
fn deref(&self) -> &yash_env::Env<S> {
self.env
}
}
impl<S: Close + Dup> DerefMut for RedirGuard<'_, S> {
fn deref_mut(&mut self) -> &mut yash_env::Env<S> {
self.env
}
}
impl<S: Close + Dup> std::ops::Drop for RedirGuard<'_, S> {
fn drop(&mut self) {
self.undo_redirs()
}
}
impl<'e, S: Close + Dup> RedirGuard<'e, S> {
pub fn new(env: &'e mut yash_env::Env<S>) -> Self {
let saved_fds = Vec::new();
RedirGuard { env, saved_fds }
}
pub async fn perform_redir(
&mut self,
redir: &Redir,
xtrace: Option<&mut XTrace>,
) -> Result<Option<ExitStatus>, Error>
where
S: Runtime + 'static,
{
let (saved_fd, exit_status) = perform(self, redir, xtrace).await?;
self.saved_fds.push(saved_fd);
Ok(exit_status)
}
pub async fn perform_redirs<'a, I>(
&mut self,
redirs: I,
mut xtrace: Option<&mut XTrace>,
) -> Result<Option<ExitStatus>, Error>
where
S: Runtime + 'static,
I: IntoIterator<Item = &'a Redir>,
{
let mut exit_status = None;
for redir in redirs {
let new_exit_status = self.perform_redir(redir, xtrace.as_deref_mut()).await?;
exit_status = new_exit_status.or(exit_status);
}
Ok(exit_status)
}
pub fn undo_redirs(&mut self) {
for SavedFd { original, save } in self.saved_fds.drain(..).rev() {
if let Some(save) = save {
assert_ne!(save, original);
let _: Result<_, _> = self.env.system.dup2(save, original);
let _: Result<_, _> = self.env.system.close(save);
} else {
let _: Result<_, _> = self.env.system.close(original);
}
}
}
pub fn preserve_redirs(&mut self) {
for SavedFd { original: _, save } in self.saved_fds.drain(..) {
if let Some(save) = save {
let _: Result<_, _> = self.env.system.close(save);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use crate::tests::return_builtin;
use assert_matches::assert_matches;
use futures_util::FutureExt as _;
use std::cell::RefCell;
use std::rc::Rc;
use yash_env::Env;
use yash_env::VirtualSystem;
use yash_env::system::Concurrent;
use yash_env::system::Read as _;
use yash_env::system::Write as _;
use yash_env::system::resource::LimitPair;
use yash_env::system::resource::Resource;
use yash_env::system::resource::SetRlimit as _;
use yash_env::system::r#virtual::FileBody;
use yash_env::system::r#virtual::Inode;
use yash_env::system::r#virtual::SystemState;
use yash_env::test_helper::in_virtual_system;
use yash_env::waker::WakerSet;
use yash_syntax::syntax::Text;
fn env_with_nofile_limit() -> (Env<Rc<Concurrent<VirtualSystem>>>, Rc<RefCell<SystemState>>) {
let system = VirtualSystem::new();
system
.setrlimit(
Resource::NOFILE,
LimitPair {
soft: 1024,
hard: 1024,
},
)
.unwrap();
let state = Rc::clone(&system.state);
let env = Env::with_system(Rc::new(Concurrent::new(system)));
(env, state)
}
#[test]
fn basic_file_in_redirection() {
let (mut env, state) = env_with_nofile_limit();
let file = Rc::new(RefCell::new(Inode::new([42, 123, 254])));
let mut state = state.borrow_mut();
state.file_system.save("foo", file).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = "3< foo".parse().unwrap();
let result = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(result, None);
let mut buffer = [0; 4];
let read_count = env
.system
.read(Fd(3), &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 3);
assert_eq!(buffer, [42, 123, 254, 0]);
}
#[test]
fn moving_fd() {
let (mut env, state) = env_with_nofile_limit();
let file = Rc::new(RefCell::new(Inode::new([42, 123, 254])));
let mut state = state.borrow_mut();
state.file_system.save("foo", file).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = "< foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 4];
let read_count = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 3);
assert_eq!(buffer, [42, 123, 254, 0]);
let e = env
.system
.read(Fd(3), &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn saving_and_undoing_fd() {
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("file", Rc::default()).unwrap();
state
.file_system
.get("/dev/stdin")
.unwrap()
.borrow_mut()
.body = FileBody::new([17]);
drop(state);
let mut redir_env = RedirGuard::new(&mut env);
let redir = "< file".parse().unwrap();
redir_env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
redir_env.undo_redirs();
drop(redir_env);
let mut buffer = [0; 2];
let read_count = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 1);
assert_eq!(buffer[0], 17);
}
#[test]
fn preserving_fd() {
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("file", Rc::default()).unwrap();
state
.file_system
.get("/dev/stdin")
.unwrap()
.borrow_mut()
.body = FileBody::new([17]);
drop(state);
let mut redir_env = RedirGuard::new(&mut env);
let redir = "< file".parse().unwrap();
redir_env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
redir_env.preserve_redirs();
drop(redir_env);
let mut buffer = [0; 2];
let read_count = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 0);
let e = env
.system
.read(MIN_INTERNAL_FD, &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn undoing_without_initial_fd() {
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("input", Rc::default()).unwrap();
drop(state);
let mut redir_env = RedirGuard::new(&mut env);
let redir = "4< input".parse().unwrap();
redir_env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
redir_env.undo_redirs();
drop(redir_env);
let mut buffer = [0; 1];
let e = env
.system
.read(Fd(4), &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn unreadable_file() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "< no_such_file".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::OpenFile(c"no_such_file".to_owned(), Errno::ENOENT)
);
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn multiple_redirections() {
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
let file = Rc::new(RefCell::new(Inode::new([100])));
state.file_system.save("foo", file).unwrap();
let file = Rc::new(RefCell::new(Inode::new([200])));
state.file_system.save("bar", file).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
env.perform_redir(&"< foo".parse().unwrap(), None)
.now_or_never()
.unwrap()
.unwrap();
env.perform_redir(&"3< bar".parse().unwrap(), None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 1];
let read_count = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 1);
assert_eq!(buffer, [100]);
let read_count = env
.system
.read(Fd(3), &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 1);
assert_eq!(buffer, [200]);
}
#[test]
fn later_redirection_wins() {
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
let file = Rc::new(RefCell::new(Inode::new([100])));
state.file_system.save("foo", file).unwrap();
let file = Rc::new(RefCell::new(Inode::new([200])));
state.file_system.save("bar", file).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
env.perform_redir(&"< foo".parse().unwrap(), None)
.now_or_never()
.unwrap()
.unwrap();
env.perform_redir(&"< bar".parse().unwrap(), None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 1];
let read_count = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 1);
assert_eq!(buffer, [200]);
}
#[test]
fn target_with_cloexec() {
let mut env = env_with_nofile_limit().0;
let fd = env
.system
.open(
c"foo",
OfdAccess::WriteOnly,
OpenFlag::Create.into(),
Mode::ALL_9,
)
.now_or_never()
.unwrap()
.unwrap();
env.system
.fcntl_setfd(fd, FdFlag::CloseOnExec.into())
.unwrap();
let mut env = RedirGuard::new(&mut env);
let redir = format!("{fd}> bar").parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::ReservedFd(fd));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn exit_status_of_command_substitution_in_normal() {
in_virtual_system(|mut env, state| async move {
env.builtins.insert("echo", echo_builtin());
env.builtins.insert("return", return_builtin());
let mut env = RedirGuard::new(&mut env);
let redir = "3> $(echo foo; return -n 79)".parse().unwrap();
let result = env.perform_redir(&redir, None).await.unwrap();
assert_eq!(result, Some(ExitStatus(79)));
let file = state.borrow().file_system.get("foo");
assert!(file.is_ok(), "{file:?}");
})
}
#[test]
fn exit_status_of_command_substitution_in_here_doc() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("echo", echo_builtin());
env.builtins.insert("return", return_builtin());
let mut env = RedirGuard::new(&mut env);
let redir = Redir {
fd: Some(Fd(4)),
body: RedirBody::HereDoc(Rc::new(HereDoc {
delimiter: "-END".parse().unwrap(),
remove_tabs: false,
content: "$(echo foo)$(echo bar; return -n 42)\n"
.parse::<Text>()
.unwrap()
.into(),
})),
};
let result = env.perform_redir(&redir, None).await.unwrap();
assert_eq!(result, Some(ExitStatus(42)));
let mut buffer = [0; 10];
let count = env
.system
.read(Fd(4), &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(count, 7);
assert_eq!(&buffer[..7], b"foobar\n");
})
}
#[test]
fn xtrace_normal() {
let mut xtrace = XTrace::new();
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
env.perform_redir(&"> foo${unset-&}".parse().unwrap(), Some(&mut xtrace))
.now_or_never()
.unwrap()
.unwrap();
env.perform_redir(&"3>> bar".parse().unwrap(), Some(&mut xtrace))
.now_or_never()
.unwrap()
.unwrap();
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "1>'foo&' 3>>bar\n");
}
#[test]
fn xtrace_here_doc() {
let mut xtrace = XTrace::new();
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = Redir {
fd: Some(Fd(4)),
body: RedirBody::HereDoc(Rc::new(HereDoc {
delimiter: r"-\END".parse().unwrap(),
remove_tabs: false,
content: "foo\n".parse::<Text>().unwrap().into(),
})),
};
env.perform_redir(&redir, Some(&mut xtrace))
.now_or_never()
.unwrap()
.unwrap();
let redir = Redir {
fd: Some(Fd(5)),
body: RedirBody::HereDoc(Rc::new(HereDoc {
delimiter: r"EOF".parse().unwrap(),
remove_tabs: false,
content: "bar${unset-}\n".parse::<Text>().unwrap().into(),
})),
};
env.perform_redir(&redir, Some(&mut xtrace))
.now_or_never()
.unwrap()
.unwrap();
let result = xtrace.finish(&mut env).now_or_never().unwrap();
assert_eq!(result, "4<< -\\END 5<<EOF\nfoo\n-END\nbar\nEOF\n");
}
#[test]
fn file_in_closes_opened_file_on_error() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999</dev/stdin".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let mut buffer = [0; 1];
let e = env
.system
.read(Fd(3), &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn file_out_creates_empty_file() {
let (mut env, state) = env_with_nofile_limit();
let mut env = RedirGuard::new(&mut env);
let redir = "3> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(Fd(3), &[42, 123, 57])
.now_or_never()
.unwrap()
.unwrap();
let file = state.borrow().file_system.get("foo").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [42, 123, 57]);
});
}
#[test]
fn file_out_truncates_existing_file() {
let file = Rc::new(RefCell::new(Inode::new([42, 123, 254])));
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("foo", Rc::clone(&file)).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = "3> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], []);
});
}
#[test]
fn file_out_noclobber_with_regular_file() {
let file = Rc::new(RefCell::new(Inode::new([42, 123, 254])));
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("foo", Rc::clone(&file)).unwrap();
drop(state);
env.options.set(Clobber, Off);
let mut env = RedirGuard::new(&mut env);
let redir = "3> foo".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::OpenFile(c"foo".to_owned(), Errno::EEXIST)
);
assert_eq!(e.location, redir.body.operand().location);
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [42, 123, 254]);
});
}
#[test]
fn file_out_noclobber_with_non_regular_file() {
let inode = Inode {
body: FileBody::Fifo {
content: Default::default(),
readers: 1,
writers: 0,
pending_open_wakers: WakerSet::new(),
pending_read_wakers: WakerSet::new(),
pending_write_wakers: WakerSet::new(),
},
permissions: Default::default(),
};
let file = Rc::new(RefCell::new(inode));
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("foo", Rc::clone(&file)).unwrap();
drop(state);
env.options.set(Clobber, Off);
let mut env = RedirGuard::new(&mut env);
let redir = "3> foo".parse().unwrap();
let result = env.perform_redir(&redir, None).now_or_never().unwrap();
assert_eq!(result, Ok(None));
}
#[test]
fn file_out_closes_opened_file_on_error() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999>foo".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let e = env
.system
.write(Fd(3), &[0x20])
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn file_clobber_creates_empty_file() {
let (mut env, state) = env_with_nofile_limit();
let mut env = RedirGuard::new(&mut env);
let redir = "3>| foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(Fd(3), &[42, 123, 57])
.now_or_never()
.unwrap()
.unwrap();
let file = state.borrow().file_system.get("foo").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [42, 123, 57]);
});
}
#[test]
fn file_clobber_by_default_truncates_existing_file() {
let file = Rc::new(RefCell::new(Inode::new([42, 123, 254])));
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("foo", Rc::clone(&file)).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = "3>| foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], []);
});
}
#[test]
fn file_clobber_closes_opened_file_on_error() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999>|foo".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let e = env
.system
.write(Fd(3), &[0x20])
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn file_append_creates_empty_file() {
let (mut env, state) = env_with_nofile_limit();
let mut env = RedirGuard::new(&mut env);
let redir = "3>> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(Fd(3), &[42, 123, 57])
.now_or_never()
.unwrap()
.unwrap();
let file = state.borrow().file_system.get("foo").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [42, 123, 57]);
});
}
#[test]
fn file_append_appends_to_existing_file() {
let file = Rc::new(RefCell::new(Inode::new(*b"one\n")));
let (mut env, state) = env_with_nofile_limit();
let mut state = state.borrow_mut();
state.file_system.save("foo", Rc::clone(&file)).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = ">> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(Fd::STDOUT, "two\n".as_bytes())
.now_or_never()
.unwrap()
.unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(std::str::from_utf8(content), Ok("one\ntwo\n"));
});
}
#[test]
fn file_append_closes_opened_file_on_error() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999>>foo".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let e = env
.system
.write(Fd(3), &[0x20])
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn file_in_out_creates_empty_file() {
let (mut env, state) = env_with_nofile_limit();
let mut env = RedirGuard::new(&mut env);
let redir = "3<> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(Fd(3), &[230, 175, 26])
.now_or_never()
.unwrap()
.unwrap();
let file = state.borrow().file_system.get("foo").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [230, 175, 26]);
});
}
#[test]
fn file_in_out_leaves_existing_file_content() {
let (mut env, state) = env_with_nofile_limit();
let file = Rc::new(RefCell::new(Inode::new([132, 79, 210])));
let mut state = state.borrow_mut();
state.file_system.save("foo", file).unwrap();
drop(state);
let mut env = RedirGuard::new(&mut env);
let redir = "3<> foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 4];
let read_count = env
.system
.read(Fd(3), &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 3);
assert_eq!(buffer, [132, 79, 210, 0]);
}
#[test]
fn file_in_out_closes_opened_file_on_error() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999<>foo".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let e = env
.system
.write(Fd(3), &[0x20])
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn fd_in_copies_fd() {
for fd in [Fd(0), Fd(3)] {
let (mut env, state) = env_with_nofile_limit();
state
.borrow_mut()
.file_system
.get("/dev/stdin")
.unwrap()
.borrow_mut()
.body = FileBody::new([1, 2, 42]);
let mut env = RedirGuard::new(&mut env);
let redir = "3<& 0".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 4];
let read_count = env
.system
.read(fd, &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 3);
assert_eq!(buffer, [1, 2, 42, 0]);
}
}
#[test]
fn fd_in_closes_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "<& -".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 1];
let e = env
.system
.read(Fd::STDIN, &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn fd_in_rejects_unreadable_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3>foo".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let redir = "<&3".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnreadableFd(Fd(3)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn fd_in_rejects_unopened_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3<&3".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnreadableFd(Fd(3)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn fd_in_rejects_fd_with_cloexec() {
let mut env = env_with_nofile_limit().0;
env.system
.fcntl_setfd(Fd(0), FdFlag::CloseOnExec.into())
.unwrap();
let mut env = RedirGuard::new(&mut env);
let redir = "3<& 0".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::ReservedFd(Fd(0)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn keep_target_fd_open_on_error_in_fd_in() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999<&0".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let mut buffer = [0; 1];
let read_count = env
.system
.read(Fd(0), &mut buffer)
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(read_count, 0);
}
#[test]
fn fd_out_copies_fd() {
for fd in [Fd(1), Fd(4)] {
let (mut env, state) = env_with_nofile_limit();
let mut env = RedirGuard::new(&mut env);
let redir = "4>& 1".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
env.system
.write(fd, &[7, 6, 91])
.now_or_never()
.unwrap()
.unwrap();
let file = state.borrow().file_system.get("/dev/stdout").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(content[..], [7, 6, 91]);
});
}
}
#[test]
fn fd_out_closes_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = ">& -".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = [0; 1];
let e = env
.system
.read(Fd::STDOUT, &mut buffer)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e, Errno::EBADF);
}
#[test]
fn fd_out_rejects_unwritable_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3</dev/stdin".parse().unwrap();
env.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap();
let redir = ">&3".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnwritableFd(Fd(3)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn fd_out_rejects_unopened_fd() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3>&3".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnwritableFd(Fd(3)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn fd_out_rejects_fd_with_cloexec() {
let mut env = env_with_nofile_limit().0;
env.system
.fcntl_setfd(Fd(1), FdFlag::CloseOnExec.into())
.unwrap();
let mut env = RedirGuard::new(&mut env);
let redir = "4>& 1".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::ReservedFd(Fd(1)));
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn keep_target_fd_open_on_error_in_fd_out() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "999999999>&1".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::FdNotOverwritten(Fd(999999999), Errno::EBADF)
);
assert_eq!(e.location, redir.body.operand().location);
let write_count = env
.system
.write(Fd(1), &[0x20])
.now_or_never()
.unwrap()
.unwrap();
assert_eq!(write_count, 1);
}
#[test]
fn pipe_redirection_not_yet_implemented() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3>>|4".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnsupportedPipeRedirection);
assert_eq!(e.location, redir.body.operand().location);
}
#[test]
fn here_string_not_yet_implemented() {
let mut env = env_with_nofile_limit().0;
let mut env = RedirGuard::new(&mut env);
let redir = "3<<< here".parse().unwrap();
let e = env
.perform_redir(&redir, None)
.now_or_never()
.unwrap()
.unwrap_err();
assert_eq!(e.cause, ErrorCause::UnsupportedHereString);
assert_eq!(e.location, redir.body.operand().location);
}
}