mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
use super::*;

/// Saved file descriptor for restoration after redirections.
pub(super) struct SavedFd {
    target: RedirectTarget,
    previous_fd: sys::FileDescriptor,
}

pub(super) struct RedirectGuard {
    saved: Vec<SavedFd>,
}

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);
    }
}

#[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
            .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();
            if !fd.is_valid() {
                state.raw_fds.remove(&key);
            } else {
                state.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 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 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_word_nosplit(state, runtime, &redir.name);
    if state.take_expansion_error().is_some() {
        return None;
    }
    let filename_path = resolve_path(&state.cwd, Path::new(&filename));
    let new_fd = match redir.op {
        IoRedirectOp::Less => open_redirect_file(state, &filename, fs::File::open(&filename_path))?,
        IoRedirectOp::Great => {
            let opened = if state.has_option(OPT_NOCLOBBER) {
                OpenOptions::new()
                    .write(true)
                    .create_new(true)
                    .open(&filename_path)
            } else {
                OpenOptions::new()
                    .write(true)
                    .create(true)
                    .truncate(true)
                    .open(&filename_path)
            };
            open_redirect_file(state, &filename, opened)?
        }
        IoRedirectOp::Clobber => open_redirect_file(
            state,
            &filename,
            OpenOptions::new()
                .write(true)
                .create(true)
                .truncate(true)
                .open(&filename_path),
        )?,
        IoRedirectOp::DGreat => open_redirect_file(
            state,
            &filename,
            OpenOptions::new()
                .create(true)
                .append(true)
                .open(&filename_path),
        )?,
        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,
            OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .truncate(false)
                .open(&filename_path),
        )?,
        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_word_nosplit(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');
            }
            if let Ok(p) = sys::OsPipe::new() {
                if let Err(err) = p.write_fd.write_all(content.as_bytes()) {
                    shell_errln(state, &format!("here-document: {err}"));
                    p.write_fd.close();
                    p.read_fd.close();
                    return None;
                }
                p.write_fd.close();
                p.read_fd
            } else {
                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<(String, Option<Variable>)>, i32> {
    let mut new_values = Vec::with_capacity(assignments.len());
    for assign in assignments {
        let value = shell_expand::expand_word_nosplit(state, runtime, &assign.value);
        if let Some(status) = state.take_expansion_error() {
            return Err(status);
        }
        new_values.push((
            assign.name.clone(),
            shell_expand::expand_tilde_assignment(state, &value),
        ));
    }

    let mut old_vars = Vec::new();
    for (name, value) in new_values {
        if capture_old {
            old_vars.push((name.clone(), state.vars.get(&name).cloned()));
        }
        assign_variable(state, &name, value, attrib, context)?;
    }
    Ok(old_vars)
}

pub(super) fn restore_assignment_values(
    state: &mut ShellState,
    old_vars: Vec<(String, Option<Variable>)>,
) {
    for (name, old) in old_vars {
        if let Some(var) = old {
            state.vars.insert(name, var);
        } else {
            state.vars.remove(&name);
        }
    }
}