use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::builtin::BuiltinHost;
use crate::shell;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShellOptions(u32);
impl ShellOptions {
pub const ALLEXPORT: Self = Self(shell::OPT_ALLEXPORT);
pub const NOTIFY: Self = Self(shell::OPT_NOTIFY);
pub const NOCLOBBER: Self = Self(shell::OPT_NOCLOBBER);
pub const ERREXIT: Self = Self(shell::OPT_ERREXIT);
pub const NOGLOB: Self = Self(shell::OPT_NOGLOB);
pub const MONITOR: Self = Self(shell::OPT_MONITOR);
pub const NOEXEC: Self = Self(shell::OPT_NOEXEC);
pub const IGNOREEOF: Self = Self(shell::OPT_IGNOREEOF);
pub const NOLOG: Self = Self(shell::OPT_NOLOG);
pub const VI: Self = Self(shell::OPT_VI);
pub const NOUNSET: Self = Self(shell::OPT_NOUNSET);
pub const VERBOSE: Self = Self(shell::OPT_VERBOSE);
pub const XTRACE: Self = Self(shell::OPT_XTRACE);
pub const fn empty() -> Self {
Self(0)
}
pub const fn bits(self) -> u32 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
pub(crate) const fn from_bits(bits: u32) -> Self {
Self(bits)
}
}
impl Default for ShellOptions {
fn default() -> Self {
Self::empty()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ShellOptionSpec {
pub option: ShellOptions,
pub short_name: Option<char>,
pub long_name: Option<String>,
}
impl ShellOptionSpec {
pub fn new(option: ShellOptions) -> Self {
Self {
option,
short_name: None,
long_name: None,
}
}
pub fn with_short_name(mut self, short_name: char) -> Self {
self.short_name = Some(short_name);
self
}
pub fn with_long_name(mut self, long_name: impl Into<String>) -> Self {
self.long_name = Some(long_name.into());
self
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellOptionSchema {
options: Vec<ShellOptionSpec>,
}
impl ShellOptionSchema {
pub fn empty() -> Self {
Self {
options: Vec::new(),
}
}
pub fn options(&self) -> &[ShellOptionSpec] {
&self.options
}
pub fn with_option(mut self, option: ShellOptionSpec) -> Self {
self.set_option(option);
self
}
pub fn set_option(&mut self, option: ShellOptionSpec) -> Option<ShellOptionSpec> {
let replaced = self.remove_option(option.option);
if let Some(short_name) = option.short_name {
self.options
.retain(|existing| existing.short_name != Some(short_name));
}
if let Some(long_name) = option.long_name.as_deref() {
self.options
.retain(|existing| existing.long_name.as_deref() != Some(long_name));
}
self.options.push(option);
replaced
}
pub fn remove_option(&mut self, option: ShellOptions) -> Option<ShellOptionSpec> {
let index = self
.options
.iter()
.position(|existing| existing.option == option)?;
Some(self.options.remove(index))
}
pub(crate) fn find_short_option(&self, short_name: char) -> Option<&ShellOptionSpec> {
self.options
.iter()
.find(|option| option.short_name == Some(short_name))
}
pub(crate) fn find_long_option(&self, long_name: &str) -> Option<&ShellOptionSpec> {
self.options
.iter()
.find(|option| option.long_name.as_deref() == Some(long_name))
}
}
impl Default for ShellOptionSchema {
fn default() -> Self {
Self::empty()
.with_option(
ShellOptionSpec::new(ShellOptions::ALLEXPORT)
.with_short_name('a')
.with_long_name("allexport"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::NOTIFY)
.with_short_name('b')
.with_long_name("notify"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::NOCLOBBER)
.with_short_name('C')
.with_long_name("noclobber"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::ERREXIT)
.with_short_name('e')
.with_long_name("errexit"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::NOGLOB)
.with_short_name('f')
.with_long_name("noglob"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::MONITOR)
.with_short_name('m')
.with_long_name("monitor"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::NOEXEC)
.with_short_name('n')
.with_long_name("noexec"),
)
.with_option(ShellOptionSpec::new(ShellOptions::IGNOREEOF).with_long_name("ignoreeof"))
.with_option(ShellOptionSpec::new(ShellOptions::NOLOG).with_long_name("nolog"))
.with_option(ShellOptionSpec::new(ShellOptions::VI).with_long_name("vi"))
.with_option(
ShellOptionSpec::new(ShellOptions::NOUNSET)
.with_short_name('u')
.with_long_name("nounset"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::VERBOSE)
.with_short_name('v')
.with_long_name("verbose"),
)
.with_option(
ShellOptionSpec::new(ShellOptions::XTRACE)
.with_short_name('x')
.with_long_name("xtrace"),
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VariableAttributes(u32);
impl VariableAttributes {
pub const EXPORT: Self = Self(shell::VAR_EXPORT);
pub const READONLY: Self = Self(shell::VAR_READONLY);
pub const fn empty() -> Self {
Self(0)
}
pub const fn bits(self) -> u32 {
self.0
}
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
}
impl Default for VariableAttributes {
fn default() -> Self {
Self::empty()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShellLanguage {
alias_expansion: bool,
function_definitions: bool,
}
impl ShellLanguage {
pub fn new() -> Self {
Self::default()
}
pub fn alias_expansion_enabled(&self) -> bool {
self.alias_expansion
}
pub fn function_definitions_enabled(&self) -> bool {
self.function_definitions
}
pub fn with_alias_expansion_enabled(mut self, enabled: bool) -> Self {
self.alias_expansion = enabled;
self
}
pub fn with_function_definitions_enabled(mut self, enabled: bool) -> Self {
self.function_definitions = enabled;
self
}
}
impl Default for ShellLanguage {
fn default() -> Self {
Self {
alias_expansion: true,
function_definitions: true,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StartupSources {
system_profile: Option<PathBuf>,
user_profile: Option<PathBuf>,
env_file_var: Option<String>,
}
impl StartupSources {
pub fn new() -> Self {
Self::default()
}
pub fn system_profile(&self) -> Option<&Path> {
self.system_profile.as_deref()
}
pub fn user_profile(&self) -> Option<&Path> {
self.user_profile.as_deref()
}
pub fn env_file_var(&self) -> Option<&str> {
self.env_file_var.as_deref()
}
pub fn with_system_profile<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.system_profile = Some(path.into());
self
}
pub fn without_system_profile(mut self) -> Self {
self.system_profile = None;
self
}
pub fn with_user_profile<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.user_profile = Some(path.into());
self
}
pub fn without_user_profile(mut self) -> Self {
self.user_profile = None;
self
}
pub fn with_env_file_var(mut self, env_file_var: impl Into<String>) -> Self {
self.env_file_var = Some(env_file_var.into());
self
}
pub fn without_env_file_var(mut self) -> Self {
self.env_file_var = None;
self
}
}
impl Default for StartupSources {
fn default() -> Self {
Self {
system_profile: Some(PathBuf::from("/etc/profile")),
user_profile: Some(PathBuf::from(".profile")),
env_file_var: Some("ENV".to_string()),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum StartupPolicy {
#[default]
None,
PosixLoginFiles,
InteractiveEnvHook,
PosixLoginFilesAndInteractiveEnvHook,
}
impl StartupPolicy {
pub(crate) const fn should_source_profile(self) -> bool {
matches!(
self,
Self::PosixLoginFiles | Self::PosixLoginFilesAndInteractiveEnvHook
)
}
pub(crate) const fn should_source_env(self) -> bool {
matches!(
self,
Self::InteractiveEnvHook | Self::PosixLoginFilesAndInteractiveEnvHook
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShellSecurityPolicy {
allow_implicit_history_file: bool,
allow_ambient_fds: bool,
allow_background_jobs: bool,
allow_source_builtin: bool,
allow_process_control_builtins: bool,
allow_path_search: bool,
allow_redirect_target_symlinks: bool,
}
impl ShellSecurityPolicy {
pub const fn permissive() -> Self {
Self {
allow_implicit_history_file: true,
allow_ambient_fds: true,
allow_background_jobs: true,
allow_source_builtin: true,
allow_process_control_builtins: true,
allow_path_search: true,
allow_redirect_target_symlinks: true,
}
}
pub const fn multi_tenant() -> Self {
Self {
allow_implicit_history_file: false,
allow_ambient_fds: false,
allow_background_jobs: false,
allow_source_builtin: false,
allow_process_control_builtins: false,
allow_path_search: false,
allow_redirect_target_symlinks: false,
}
}
pub const fn allow_implicit_history_file(self) -> bool {
self.allow_implicit_history_file
}
pub const fn allow_ambient_fds(self) -> bool {
self.allow_ambient_fds
}
pub const fn allow_background_jobs(self) -> bool {
self.allow_background_jobs
}
pub const fn allow_source_builtin(self) -> bool {
self.allow_source_builtin
}
pub const fn allow_process_control_builtins(self) -> bool {
self.allow_process_control_builtins
}
pub const fn allow_path_search(self) -> bool {
self.allow_path_search
}
pub const fn allow_redirect_target_symlinks(self) -> bool {
self.allow_redirect_target_symlinks
}
pub const fn with_implicit_history_file(mut self, allow: bool) -> Self {
self.allow_implicit_history_file = allow;
self
}
pub const fn with_ambient_fds(mut self, allow: bool) -> Self {
self.allow_ambient_fds = allow;
self
}
pub const fn with_background_jobs(mut self, allow: bool) -> Self {
self.allow_background_jobs = allow;
self
}
pub const fn with_source_builtin(mut self, allow: bool) -> Self {
self.allow_source_builtin = allow;
self
}
pub const fn with_process_control_builtins(mut self, allow: bool) -> Self {
self.allow_process_control_builtins = allow;
self
}
pub const fn with_path_search(mut self, allow: bool) -> Self {
self.allow_path_search = allow;
self
}
pub const fn with_redirect_target_symlinks(mut self, allow: bool) -> Self {
self.allow_redirect_target_symlinks = allow;
self
}
}
impl Default for ShellSecurityPolicy {
fn default() -> Self {
Self::permissive()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellIdentity {
name: String,
history_env_var: String,
machine_payload_fd_env_var: String,
default_history_file: String,
}
impl ShellIdentity {
pub fn named(name: impl Into<String>) -> Self {
let name = name.into();
let env_prefix = name
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() {
ch.to_ascii_uppercase()
} else {
'_'
}
})
.collect::<String>();
Self {
history_env_var: format!("{env_prefix}_HISTORY_FILE"),
machine_payload_fd_env_var: format!("{env_prefix}_MACHINE_PAYLOAD_FD"),
default_history_file: format!(".{name}_history"),
name,
}
}
pub fn with_history_env_var(mut self, history_env_var: impl Into<String>) -> Self {
self.history_env_var = history_env_var.into();
self
}
pub fn with_machine_payload_fd_env_var(
mut self,
machine_payload_fd_env_var: impl Into<String>,
) -> Self {
self.machine_payload_fd_env_var = machine_payload_fd_env_var.into();
self
}
pub fn with_default_history_file(mut self, default_history_file: impl Into<String>) -> Self {
self.default_history_file = default_history_file.into();
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn history_env_var(&self) -> &str {
&self.history_env_var
}
pub fn machine_payload_fd_env_var(&self) -> &str {
&self.machine_payload_fd_env_var
}
pub fn default_history_file(&self) -> &str {
&self.default_history_file
}
}
impl Default for ShellIdentity {
fn default() -> Self {
Self::named("mxsh")
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum BackgroundLauncher {
#[default]
CurrentExecutable,
Program {
program: String,
args: Vec<String>,
},
}
impl BackgroundLauncher {
pub(crate) fn machine_command(&self, shell_name: &str) -> Option<(String, Vec<String>)> {
match self {
Self::CurrentExecutable => {
let program = shell::resolve_machine_program_path(shell_name)?;
Ok::<_, ()>((program.clone(), vec![program, "--machine".to_string()])).ok()
}
Self::Program { program, args } => {
let mut argv = Vec::with_capacity(args.len() + 2);
argv.push(program.clone());
argv.extend(args.iter().cloned());
argv.push("--machine".to_string());
Some((program.clone(), argv))
}
}
}
}
type CommandOverrideMatcher = dyn Fn(&[String]) -> bool + Send + Sync + 'static;
type CommandOverrideCallback =
dyn for<'a> Fn(&mut BuiltinHost<'a>, &[String]) -> i32 + Send + Sync + 'static;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PortableCommandOverride {
PrintfUsage,
}
#[derive(Clone)]
pub struct CommandOverride {
matcher: Arc<CommandOverrideMatcher>,
handler: Arc<CommandOverrideCallback>,
portable: Option<PortableCommandOverride>,
}
impl CommandOverride {
pub fn new<F>(handler: F) -> Self
where
F: for<'a> Fn(&mut BuiltinHost<'a>, &[String]) -> i32 + Send + Sync + 'static,
{
Self {
matcher: Arc::new(|_argv| true),
handler: Arc::new(handler),
portable: None,
}
}
pub(crate) fn portable<F>(portable: PortableCommandOverride, handler: F) -> Self
where
F: for<'a> Fn(&mut BuiltinHost<'a>, &[String]) -> i32 + Send + Sync + 'static,
{
Self {
matcher: Arc::new(|_argv| true),
handler: Arc::new(handler),
portable: Some(portable),
}
}
pub fn with_matcher<M>(mut self, matcher: M) -> Self
where
M: Fn(&[String]) -> bool + Send + Sync + 'static,
{
self.matcher = Arc::new(matcher);
self
}
pub(crate) fn matches(&self, argv: &[String]) -> bool {
(self.matcher)(argv)
}
pub(crate) fn run(&self, context: &mut BuiltinHost<'_>, argv: &[String]) -> i32 {
(self.handler)(context, argv)
}
pub(crate) fn portable_kind(&self) -> Option<PortableCommandOverride> {
self.portable
}
}
impl fmt::Debug for CommandOverride {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CommandOverride").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct CommandPolicy {
unspecified_utilities: HashSet<String>,
overrides: HashMap<String, CommandOverride>,
command_not_found_handler: Option<Arc<CommandOverrideCallback>>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PortableCommandPolicy {
unspecified_utilities: Vec<String>,
overrides: Vec<(String, PortableCommandOverride)>,
}
impl CommandPolicy {
pub fn empty() -> Self {
Self {
unspecified_utilities: HashSet::new(),
overrides: HashMap::new(),
command_not_found_handler: None,
}
}
pub fn clear_unspecified_utilities(mut self) -> Self {
self.unspecified_utilities.clear();
self
}
pub fn add_unspecified_utility(mut self, name: impl Into<String>) -> Self {
self.unspecified_utilities.insert(name.into());
self
}
pub fn remove_unspecified_utility(mut self, name: &str) -> Self {
self.unspecified_utilities.remove(name);
self
}
pub fn register_override(
mut self,
name: impl Into<String>,
command_override: CommandOverride,
) -> Self {
self.overrides.insert(name.into(), command_override);
self
}
pub fn remove_override(mut self, name: &str) -> Self {
self.overrides.remove(name);
self
}
pub(crate) fn is_unspecified_utility(&self, name: &str) -> bool {
self.unspecified_utilities.contains(name)
}
pub(crate) fn override_for(&self, name: &str) -> Option<&CommandOverride> {
self.overrides.get(name)
}
pub fn register_command_not_found_handler<F>(mut self, handler: F) -> Self
where
F: for<'a> Fn(&mut BuiltinHost<'a>, &[String]) -> i32 + Send + Sync + 'static,
{
self.command_not_found_handler = Some(Arc::new(handler));
self
}
pub fn remove_command_not_found_handler(mut self) -> Self {
self.command_not_found_handler = None;
self
}
pub(crate) fn command_not_found_handler(&self) -> Option<&Arc<CommandOverrideCallback>> {
self.command_not_found_handler.as_ref()
}
pub(crate) fn portable_background_checkpoint<I>(
&self,
non_portable_builtin_names: I,
) -> PortableCommandPolicy
where
I: IntoIterator<Item = String>,
{
let mut unspecified_utilities = self.unspecified_utilities.clone();
unspecified_utilities.extend(non_portable_builtin_names);
let mut overrides = Vec::with_capacity(self.overrides.len());
for (name, command_override) in &self.overrides {
let Some(portable) = command_override.portable_kind() else {
unspecified_utilities.insert(name.clone());
continue;
};
overrides.push((name.clone(), portable));
}
let mut unspecified_utilities = unspecified_utilities.into_iter().collect::<Vec<_>>();
unspecified_utilities.sort();
overrides.sort_by(|left, right| left.0.cmp(&right.0));
PortableCommandPolicy {
unspecified_utilities,
overrides,
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) fn from_portable_background_checkpoint(checkpoint: PortableCommandPolicy) -> Self {
let mut policy = Self::empty();
for name in checkpoint.unspecified_utilities {
policy.unspecified_utilities.insert(name);
}
for (name, portable) in checkpoint.overrides {
policy
.overrides
.insert(name, portable_command_override(portable));
}
policy
}
}
impl Default for CommandPolicy {
fn default() -> Self {
let mut policy = Self::empty();
for name in shell::default_unspecified_utility_names() {
policy.unspecified_utilities.insert((*name).to_string());
}
policy.overrides.insert(
"printf".to_string(),
portable_command_override(PortableCommandOverride::PrintfUsage),
);
policy
}
}
fn portable_command_override(portable: PortableCommandOverride) -> CommandOverride {
match portable {
PortableCommandOverride::PrintfUsage => {
CommandOverride::portable(portable, |context, _argv| {
let _ = context.write_stderr("printf: usage: printf [-v var] format [arguments]");
2
})
.with_matcher(|argv| argv.len() == 1)
}
}
}
impl fmt::Debug for CommandPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut unspecified = self.unspecified_utilities.iter().collect::<Vec<_>>();
unspecified.sort();
let mut overrides = self.overrides.keys().collect::<Vec<_>>();
overrides.sort();
f.debug_struct("CommandPolicy")
.field("unspecified_utilities", &unspecified)
.field("override_names", &overrides)
.finish()
}
}