use super::path::resolve_redirection_target;
use super::*;
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::path::Component;
pub(super) struct SavedFd {
target: RedirectTarget,
previous_fd: sys::FileDescriptor,
}
pub(super) struct RedirectGuard {
saved: Vec<SavedFd>,
}
pub(super) struct AssignmentRestore {
name: String,
old: Option<Variable>,
applied: Variable,
}
impl RedirectGuard {
pub(super) fn apply<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
redirects: &[IoRedirect],
) -> Result<Self, i32> {
let mut saved = Vec::new();
for redir in redirects {
match apply_one_redirect(state, runtime, redir) {
Some(entry) => saved.push(entry),
None => {
restore_saved_redirects(state, saved);
return Err(1);
}
}
}
Ok(Self { saved })
}
pub(super) fn restore(self, state: &mut ShellState) {
restore_saved_redirects(state, self.saved);
}
pub(super) fn commit(self, state: &ShellState) {
for saved in self.saved {
let current = current_redirect_fd(state, saved.target);
if saved.previous_fd.is_valid() && saved.previous_fd != current {
saved.previous_fd.close();
}
}
}
}
#[derive(Clone, Copy)]
enum RedirectTarget {
Stdin,
Stdout,
Stderr,
Raw(sys::FileDescriptor),
}
fn redirect_target_for_fd(fd: i32) -> RedirectTarget {
match fd {
0 => RedirectTarget::Stdin,
1 => RedirectTarget::Stdout,
2 => RedirectTarget::Stderr,
_ => RedirectTarget::Raw(sys::FileDescriptor::from(fd)),
}
}
fn target_fd_for_redirect(redir: &IoRedirect) -> RedirectTarget {
if let Some(n) = redir.io_number {
return redirect_target_for_fd(n as i32);
}
match redir.op {
IoRedirectOp::Less
| IoRedirectOp::LessGreat
| IoRedirectOp::DLess
| IoRedirectOp::DLessDash => RedirectTarget::Stdin,
_ => RedirectTarget::Stdout,
}
}
fn current_redirect_fd(state: &ShellState, target: RedirectTarget) -> sys::FileDescriptor {
match target {
RedirectTarget::Stdin => state.stdin_fd,
RedirectTarget::Stdout => state.stdout_fd,
RedirectTarget::Stderr => state.stderr_fd,
RedirectTarget::Raw(fd) => state
.fd_table
.raw_fds
.get(&fd.as_i32())
.copied()
.unwrap_or(sys::FileDescriptor::INVALID),
}
}
fn set_redirect_fd(state: &mut ShellState, target: RedirectTarget, fd: sys::FileDescriptor) {
match target {
RedirectTarget::Stdin => state.stdin_fd = fd,
RedirectTarget::Stdout => state.stdout_fd = fd,
RedirectTarget::Stderr => state.stderr_fd = fd,
RedirectTarget::Raw(target_fd) => {
let key = target_fd.as_i32();
state.fd_table.raw_fds.insert(key, fd);
}
}
}
fn close_redirect_fd_if_replaced(
state: &ShellState,
target: RedirectTarget,
restore_fd: sys::FileDescriptor,
) {
let current = current_redirect_fd(state, target);
if current.is_valid() && current != restore_fd {
current.close();
}
}
fn open_path_component_at(
dir: &OwnedFd,
component: &std::ffi::OsStr,
flags: libc::c_int,
mode: libc::mode_t,
) -> io::Result<OwnedFd> {
let component = CString::new(component.as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"))?;
let fd = unsafe {
libc::openat(
dir.as_raw_fd(),
component.as_ptr(),
flags,
mode as libc::c_uint,
)
};
if fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
}
fn open_redirect_target_without_symlinks(
path: &Path,
flags: libc::c_int,
mode: libc::mode_t,
) -> io::Result<fs::File> {
let start_path = if path.is_absolute() { "/" } else { "." };
let start_flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC;
let start_path = CString::new(start_path)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"))?;
let start_fd = unsafe { libc::open(start_path.as_ptr(), start_flags) };
let start_fd = if start_fd < 0 {
return Err(io::Error::last_os_error());
} else {
unsafe { OwnedFd::from_raw_fd(start_fd) }
};
let mut dir = start_fd;
let mut components = path.components().peekable();
while let Some(component) = components.next() {
let name = match component {
Component::RootDir | Component::CurDir => continue,
Component::ParentDir => std::ffi::OsStr::new(".."),
Component::Normal(name) => name,
Component::Prefix(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"unsupported path prefix in redirect target",
));
}
};
let last = components.peek().is_none();
if last {
let final_flags = flags | libc::O_CLOEXEC | libc::O_NOFOLLOW;
let file = open_path_component_at(&dir, name, final_flags, mode)?;
return Ok(fs::File::from(file));
}
let next_flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
dir = open_path_component_at(&dir, name, next_flags, 0)?;
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"redirect target must not resolve to the filesystem root",
))
}
fn open_redirect_target(
path: &Path,
flags: libc::c_int,
mode: libc::mode_t,
allow_symlinks: bool,
) -> io::Result<fs::File> {
if allow_symlinks {
let mut options = OpenOptions::new();
if (flags & libc::O_RDWR) != 0 {
options.read(true).write(true);
} else if (flags & libc::O_WRONLY) != 0 {
options.write(true);
} else {
options.read(true);
}
options.create((flags & libc::O_CREAT) != 0);
options.create_new((flags & libc::O_EXCL) != 0);
options.append((flags & libc::O_APPEND) != 0);
options.truncate((flags & libc::O_TRUNC) != 0);
options.open(path)
} else {
open_redirect_target_without_symlinks(path, flags, mode)
}
}
fn duplicate_redirect_source_fd(
state: &ShellState,
fd: i32,
) -> Result<sys::FileDescriptor, std::io::Error> {
let source = current_redirect_fd(state, redirect_target_for_fd(fd));
if !source.is_valid() {
return Err(std::io::Error::from_raw_os_error(libc::EBADF));
}
source.dup()
}
fn open_redirect_file(
state: &ShellState,
filename: &str,
opened: io::Result<fs::File>,
) -> Option<sys::FileDescriptor> {
match opened {
Ok(file) => Some(sys::FileDescriptor::from(file.into_raw_fd())),
Err(err) => {
shell_errln(state, &format!("{filename}: {err}"));
None
}
}
}
fn set_fd_cloexec(fd: i32) -> io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn here_document_tempfile_template() -> io::Result<Vec<u8>> {
let mut path = std::env::temp_dir();
path.push(format!("mxsh-heredoc-{}-XXXXXX", std::process::id()));
let mut template = path.as_os_str().as_bytes().to_vec();
if template.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"temporary directory path contains NUL",
));
}
template.push(0);
Ok(template)
}
fn open_here_document_content(content: &[u8]) -> io::Result<sys::FileDescriptor> {
let mut template = here_document_tempfile_template()?;
let fd = unsafe { libc::mkstemp(template.as_mut_ptr() as *mut libc::c_char) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let result = (|| {
if unsafe { libc::unlink(template.as_ptr() as *const libc::c_char) } < 0 {
return Err(io::Error::last_os_error());
}
set_fd_cloexec(fd)?;
let fd = sys::FileDescriptor::from(fd);
fd.write_all(content)?;
if unsafe { libc::lseek(fd.into_raw_fd(), 0, libc::SEEK_SET) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(fd)
})();
if result.is_err() {
sys::FileDescriptor::from(fd).close();
}
result
}
fn assign_variable(
state: &mut ShellState,
name: &str,
value: String,
attrib: u32,
context: &str,
) -> Result<(), i32> {
if state.env_set(name, value, attrib) {
Ok(())
} else {
let message = if context.is_empty() {
format!("{name}: readonly variable")
} else {
format!("{context}: {name}: readonly variable")
};
shell_errln(state, &message);
Err(1)
}
}
fn apply_one_redirect<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
redir: &IoRedirect,
) -> Option<SavedFd> {
let target = target_fd_for_redirect(redir);
let previous_fd = current_redirect_fd(state, target);
let filename = shell_expand::expand_redirection_target(state, runtime, &redir.name);
if state.take_expansion_error().is_some() {
return None;
}
let filename_path = resolve_redirection_target(state, Path::new(&filename));
let allow_symlinks = state
.definition
.security_policy
.allow_redirect_target_symlinks();
let new_fd = match redir.op {
IoRedirectOp::Less => open_redirect_file(
state,
&filename,
open_redirect_target(&filename_path, libc::O_RDONLY, 0, allow_symlinks),
)?,
IoRedirectOp::Great => {
let flags = if state.has_option(OPT_NOCLOBBER) {
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL
} else {
libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC
};
open_redirect_file(
state,
&filename,
open_redirect_target(&filename_path, flags, 0o666, allow_symlinks),
)?
}
IoRedirectOp::Clobber => open_redirect_file(
state,
&filename,
open_redirect_target(
&filename_path,
libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
0o666,
allow_symlinks,
),
)?,
IoRedirectOp::DGreat => open_redirect_file(
state,
&filename,
open_redirect_target(
&filename_path,
libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
0o666,
allow_symlinks,
),
)?,
IoRedirectOp::LessAnd | IoRedirectOp::GreatAnd => {
if filename == "-" {
set_redirect_fd(state, target, sys::FileDescriptor::INVALID);
return Some(SavedFd {
target,
previous_fd,
});
}
match filename.parse::<i32>() {
Ok(fd) => match duplicate_redirect_source_fd(state, fd) {
Ok(fd) => fd,
Err(_) => {
shell_errln(state, &format!("{filename}: bad file descriptor"));
return None;
}
},
Err(_) => {
shell_errln(state, &format!("{filename}: bad file descriptor"));
return None;
}
}
}
IoRedirectOp::LessGreat => open_redirect_file(
state,
&filename,
open_redirect_target(
&filename_path,
libc::O_RDWR | libc::O_CREAT,
0o666,
allow_symlinks,
),
)?,
IoRedirectOp::DLess | IoRedirectOp::DLessDash => {
let mut content = String::new();
for w in &redir.here_document {
if redir.here_document_expand {
content.push_str(&shell_expand::expand_here_document_word(state, runtime, w));
if state.take_expansion_error().is_some() {
return None;
}
} else {
content.push_str(&crate::ast::canonical_here_doc_literal_line(w));
}
content.push('\n');
}
match open_here_document_content(content.as_bytes()) {
Ok(fd) => fd,
Err(err) => {
shell_errln(state, &format!("here-document: {err}"));
return None;
}
}
}
};
set_redirect_fd(state, target, new_fd);
Some(SavedFd {
target,
previous_fd,
})
}
fn restore_saved_redirects(state: &mut ShellState, saved: Vec<SavedFd>) {
for s in saved.into_iter().rev() {
close_redirect_fd_if_replaced(state, s.target, s.previous_fd);
set_redirect_fd(state, s.target, s.previous_fd);
}
}
pub(super) fn set_assignment_values<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
assignments: &[Assignment],
attrib: u32,
capture_old: bool,
context: &str,
) -> Result<Vec<AssignmentRestore>, i32> {
let mut old_vars = Vec::new();
for assign in assignments {
let value =
shell_expand::expand_assignment_word(state, runtime, &assign.name, &assign.value);
if let Some(status) = state.take_expansion_error() {
if capture_old {
restore_assignment_values(state, old_vars);
}
return Err(status);
}
let old = state.variable_store.vars.get(&assign.name).cloned();
if let Err(status) = assign_variable(state, &assign.name, value, attrib, context) {
if capture_old {
restore_assignment_values(state, old_vars);
}
return Err(status);
}
if capture_old && let Some(applied) = state.variable_store.vars.get(&assign.name).cloned() {
old_vars.push(AssignmentRestore {
name: assign.name.clone(),
old,
applied,
});
}
}
Ok(old_vars)
}
pub(super) fn restore_assignment_values(state: &mut ShellState, old_vars: Vec<AssignmentRestore>) {
for restore in old_vars.into_iter().rev() {
if state.variable_store.vars.get(&restore.name) != Some(&restore.applied) {
continue;
}
if let Some(var) = restore.old {
state.variable_store.vars.insert(restore.name, var);
} else {
state.variable_store.vars.remove(&restore.name);
}
}
}