use crate::compsys::ported::_arguments::_arguments;
use crate::compsys::ported::_files::_files;
use crate::compsys::ported::_message::_message;
use crate::compsys::ported::_multi_parts::_multi_parts;
use crate::compsys::ported::_requested::_requested;
use crate::compsys::ported::_tags::_tags;
use crate::compsys::ported::_wanted::_wanted;
use crate::compsys::ported::shared::{assoc_get, FnScope, LocalScope};
use crate::ported::params::{getaparam, getsparam, setaparam};
use crate::ported::zle::complete::bin_compset;
use crate::ported::zsh_h::{options, MAX_OPS, PM_ARRAY, PM_HASHED, PM_SCALAR};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn s(v: &str) -> String {
v.to_string()
}
fn has_key(name: &str, key: &str) -> bool {
assoc_get(name, key).is_some()
}
fn arg_specs() -> Vec<String> {
vec![
s("-n"), s("-C"),
s("-S"),
s("-s"),
s("(-R -P -i -u -d -a -b -c -I -p -f -e -F -m)-A[create module aliases]"), s("(-)-R[remove module aliases]"), s("(-A -R -F -L -m -P -l -e)-u[unload module]"), s("(-d -e -l)-a[autoload module]"), s("(-c -d -I -p -f -F -P -l -m -A -R)-b[autoload module for builtins]"), s("(-b -d -I -p -f -F -P -l -m -A -R)-c[autoload module for condition codes]"), s("(-A -R -F -I -P -a -b -c -e -f -i -l -m -p)-d[list or specify module dependencies]"), s("(-i -u -d -a -b -c -p -f -L -R)-e[test if modules are loaded]"), s("(-b -c -d -I -p -F -P -l -m -A -R)-f[autoload module for math functions]"), s("(-u -b -c -d -p -f -A -R -I)-F[handle features]"), s("(-u -b -c -d -p -f -A -R -I)-m[treat feature arguments as patterns]"), s("(-d -e)-i[suppress error if command would do nothing]"), s("(-d -e -L)-s[suppress error if module is not available]"), s("(-b -c -d -p -f -F -P -m)-I[define infix condition names]"), s("(-u -b -c -d -p -f -A -R)-l[list features]"), s("(-e -u)-L[output in the form of calls to zmodload]"), s("(-b -c -d -I -f -F -P -l -m -A -R)-p[autoload module for parameters]"), s("(-u -b -c -d -p -f -A -R)-P[array param for features]:array name:_parameters"), s("(-)*:param:->params"), ]
}
const MODULE_FILE_GLOB: &str = "*.(dll|s[ol]|bundle)(:r)";
fn loadable_module_names() -> Vec<String> {
let table = match crate::ported::module::MODULESTAB.lock() {
Ok(t) => t,
Err(_) => return Vec::new(),
};
table
.linkedmodules
.iter()
.filter(|n| n.as_str() != "zsh/main")
.cloned()
.collect()
}
pub fn _zmodload_module_files(args: &[String]) -> i32 {
let mut ret = 1;
let names = loadable_module_names();
if !names.is_empty() {
let mut mp: Vec<String> = args.to_vec();
mp.push(s("/"));
mp.push(format!("({})", names.join(" ")));
if _multi_parts(&mp) == 0 {
ret = 0;
}
}
let mut fa: Vec<String> = args.to_vec();
fa.push(s("-W"));
fa.push(s("module_path"));
fa.push(s("-g"));
fa.push(s(MODULE_FILE_GLOB));
if _files(&fa) == 0 {
ret = 0;
}
ret
}
fn feature_branch() -> i32 {
let normarg: usize = getsparam("NORMARG")
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(0);
let words = getaparam("words").unwrap_or_default();
let module = normarg
.checked_sub(1)
.and_then(|i| words.get(i).cloned())
.unwrap_or_default();
let mut _feat_scope = LocalScope::declare(&["features"], PM_ARRAY);
if assoc_get("modules", &module).as_deref() != Some("loaded") {
return _message(&[
s("-e"),
s("features"),
s("feature for unloaded module"),
]);
}
let mut ops = make_ops();
ops.ind[b'l' as usize] = 1;
ops.ind[b'F' as usize] = 1;
ops.args.push(s("features"));
ops.argscount = 1;
ops.argsalloc = 1;
ops.ind[b'P' as usize] = (1 << 2) | 1;
crate::compsys::ported::shared::set_sh_lineno(41);
let _ = crate::ported::module::bin_zmodload("zmodload", &[module], &ops, 0);
let features = getaparam("features").unwrap_or_default();
let filtered: Vec<String> = if bin_compset("compset", &[s("-P"), s("-")], &make_ops(), 0) == 0 {
features
.iter()
.filter(|f| !f.starts_with('-'))
.map(|f| strip_first_char(f))
.collect()
} else if bin_compset("compset", &[s("-P"), s("+")], &make_ops(), 0) == 0 {
features
.iter()
.filter(|f| !f.starts_with('+'))
.map(|f| strip_first_char(f))
.collect()
} else {
features
.iter()
.map(|f| {
let stripped = f.strip_prefix('-').unwrap_or(f);
match stripped.strip_prefix('+') {
Some(rest) => format!("-{}", rest),
None => stripped.to_string(),
}
})
.collect()
};
setaparam("features", filtered);
_wanted(&[
s("features"),
s("expl"),
s("feature"),
s("compadd"),
s("-a"),
s("features"),
])
}
fn strip_first_char(f: &str) -> String {
let mut it = f.chars();
it.next();
it.as_str().to_string()
}
pub fn _zmodload(args: &[String]) -> i32 {
let _fn_scope = FnScope::enter("_zmodload");
let mut _locals = LocalScope::declare(&["suf", "comp"], PM_ARRAY);
_locals.also(&["state", "line", "expl", "NORMARG"], PM_SCALAR);
_locals.also_keeping_value(&["curcontext"]);
_locals.also(&["opt_args"], PM_HASHED);
let mut ret: i32 = 1;
setaparam("suf", Vec::new());
if _arguments(&arg_specs()) == 0 {
ret = 0; }
if getsparam("state").unwrap_or_default() != "params" {
return ret;
}
if has_key("opt_args", "-A") {
crate::compsys::ported::shared::set_sh_lineno(31);
if bin_compset("compset", &[s("-S"), s("=*")], &make_ops(), 0) != 0
&& bin_compset("compset", &[s("-P"), s("*=")], &make_ops(), 0) != 0
{
setaparam("suf", vec![s("-S"), s("=")]);
}
}
let current: i64 = getsparam("CURRENT")
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(0);
let normarg: i64 = getsparam("NORMARG")
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(0);
if has_key("opt_args", "-F") && current > normarg {
return feature_branch(); }
let mut comp: Vec<String> = vec![s("files"), s("aliases")];
if has_key("opt_args", "-u") {
if has_key("opt_args", "-b") || has_key("opt_args", "-a") {
comp = vec![s("builtins")]; } else {
comp = vec![s("loadedmodules"), s("aliases")]; }
}
if has_key("opt_args", "-a") && current > 3 {
comp = vec![s("builtins")];
}
setaparam("comp", comp.clone());
let _ = _tags(&comp);
loop {
if _tags(&[]) != 0 {
break;
}
let mut r: Vec<String> = vec![
s("builtins"),
s("expl"),
s("builtin command"),
s("compadd"),
];
r.extend_from_slice(args); r.push(s("-k"));
r.push(s("builtins"));
if _requested(&r) == 0 {
ret = 0;
}
if _requested(&[
s("loadedmodules"),
s("expl"),
s("loaded module"),
s("compadd"),
s("-k"),
s("modules[(R)loaded]"),
]) == 0
{
ret = 0;
}
if _requested(&[
s("files"),
s("expl"),
s("module file"),
s("_zmodload_module_files"),
]) == 0
{
ret = 0;
}
let mut a: Vec<String> = vec![
s("aliases"),
s("expl"),
s("module alias"),
s("compadd"),
];
a.extend(getaparam("suf").unwrap_or_default()); a.push(s("-k"));
a.push(s("modules[(R)alias*]"));
if _requested(&a) == 0 {
ret = 0;
}
if ret == 0 {
return 0;
}
}
ret
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_set_is_complete_and_ordered() {
let specs = arg_specs();
assert_eq!(
&specs[..4],
&["-n".to_string(), "-C".to_string(), "-S".to_string(), "-s".to_string()],
"sh:7 flags"
);
assert_eq!(specs.len(), 4 + 18 + 1);
assert_eq!(specs.last().unwrap(), "(-)*:param:->params", "sh:26");
for letter in [
"-A", "-R", "-u", "-a", "-b", "-c", "-d", "-e", "-f", "-F", "-m", "-i", "-s", "-I",
"-l", "-L", "-p", "-P",
] {
assert!(
specs.iter().any(|sp| sp.contains(&format!("){}[", letter))),
"sh:8-25 spec for {letter} is missing"
);
}
}
#[test]
fn bare_feature_state_is_inverted() {
let invert = |f: &str| -> String {
let stripped = f.strip_prefix('-').unwrap_or(f);
match stripped.strip_prefix('+') {
Some(rest) => format!("-{}", rest),
None => stripped.to_string(),
}
};
assert_eq!(invert("+p:commands"), "-p:commands");
assert_eq!(invert("-p:aliases"), "p:aliases");
assert_eq!(invert("p:aliases"), "p:aliases");
}
#[test]
fn signed_feature_filters_keep_the_complement() {
let feats = vec![
"+p:commands".to_string(),
"-p:aliases".to_string(),
"+p:modules".to_string(),
];
let enabled: Vec<String> = feats
.iter()
.filter(|f| !f.starts_with('-'))
.map(|f| strip_first_char(f))
.collect();
assert_eq!(enabled, vec!["p:commands", "p:modules"]);
let disabled: Vec<String> = feats
.iter()
.filter(|f| !f.starts_with('+'))
.map(|f| strip_first_char(f))
.collect();
assert_eq!(disabled, vec!["p:aliases"]);
}
#[test]
fn strip_first_char_is_codepoint_safe() {
assert_eq!(strip_first_char("+abc"), "abc");
assert_eq!(strip_first_char("é!"), "!");
assert_eq!(strip_first_char(""), "");
}
#[test]
fn registry_backs_the_files_tag() {
let _g = crate::test_util::global_state_lock();
let names = loadable_module_names();
assert!(
names.len() > 20,
"registry has {} names; register_builtin_modules seeds ~36",
names.len()
);
assert!(
!names.iter().any(|n| n == "zsh/main"),
"zsh/main is the static core, not a loadable module"
);
assert!(names.iter().any(|n| n.contains('/') && n != "zsh/main"));
assert!(names.iter().all(|n| n.starts_with("zsh/")));
assert!(
names.iter().all(|n| !n.contains(char::is_whitespace)),
"a name with whitespace would break _multi_parts' literal `(a b c)` form"
);
}
#[test]
fn upstream_module_file_glob_is_unchanged() {
assert_eq!(MODULE_FILE_GLOB, "*.(dll|s[ol]|bundle)(:r)");
}
#[test]
fn sh3_locals_do_not_leak() {
let _g = crate::test_util::global_state_lock();
crate::test_util::reset_completion_state();
const NAMES: [&str; 7] = ["suf", "comp", "state", "line", "expl", "NORMARG", "opt_args"];
for n in NAMES {
crate::ported::params::unsetparam(n);
}
let sentinel = ":completion::complete:__caller__::";
let _ = crate::ported::params::setsparam("curcontext", sentinel);
crate::ported::utils::inc_locallevel();
let inner = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = _zmodload(&[]);
}));
crate::ported::params::endparamscope();
let survivors: Vec<&str> = NAMES
.iter()
.copied()
.filter(|n| {
crate::ported::params::paramtab()
.read()
.map(|t| t.get(*n).is_some())
.unwrap_or(false)
})
.collect();
let ctx = crate::ported::params::getsparam("curcontext").unwrap_or_default();
crate::ported::params::unsetparam("curcontext");
crate::test_util::reset_completion_state();
if let Err(p) = inner {
std::panic::resume_unwind(p);
}
assert!(
survivors.is_empty(),
"sh:3/sh:4 locals survived the call: {survivors:?}"
);
assert_eq!(
ctx, sentinel,
"sh:3's `curcontext=\"$curcontext\"` must restore the caller's value"
);
}
#[test]
fn both_entry_points_are_routed() {
assert!(crate::compsys::router::is_intercepted("_zmodload"));
assert!(crate::compsys::router::is_intercepted("_zmodload_module_files"));
}
#[test]
fn every_offered_name_is_module_linked() {
let _g = crate::test_util::global_state_lock();
let names = loadable_module_names();
let table = crate::ported::module::MODULESTAB.lock().unwrap();
for n in &names {
assert!(
table.module_linked(n),
"`{n}` is offered by the files tag but is not module_linked"
);
}
}
}