Skip to main content

zsh/
module.rs

1//! Module system for zshrs
2//!
3//! Port from zsh/Src/module.c (3,646 lines)
4//!
5//! In C, module.c provides dynamic loading of .so modules at runtime
6//! via dlopen/dlsym. In Rust, all modules are statically compiled into
7//! the binary — there's no dynamic loading. This module provides the
8//! registration, lookup, and management API that the rest of the shell
9//! uses to interact with module features (builtins, conditions, parameters,
10//! hooks, and math functions).
11
12use std::collections::HashMap;
13
14/// Module feature types
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FeatureType {
17    Builtin,
18    Condition,
19    MathFunc,
20    Parameter,
21    Hook,
22}
23
24/// A registered module feature
25#[derive(Debug, Clone)]
26pub struct ModuleFeature {
27    pub name: String,
28    pub feature_type: FeatureType,
29    pub enabled: bool,
30}
31
32/// Module state
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ModuleState {
35    Loaded,
36    Autoloaded,
37    Unloaded,
38    Failed,
39}
40
41/// A loaded module
42#[derive(Debug, Clone)]
43pub struct Module {
44    pub name: String,
45    pub state: ModuleState,
46    pub features: Vec<ModuleFeature>,
47    pub deps: Vec<String>,
48    pub autoloads: Vec<String>,
49}
50
51impl Module {
52    pub fn new(name: &str) -> Self {
53        Module {
54            name: name.to_string(),
55            state: ModuleState::Loaded,
56            features: Vec::new(),
57            deps: Vec::new(),
58            autoloads: Vec::new(),
59        }
60    }
61
62    pub fn is_loaded(&self) -> bool {
63        self.state == ModuleState::Loaded
64    }
65}
66
67/// Module table (from module.c module hash table)
68#[derive(Debug, Default)]
69pub struct ModuleTable {
70    modules: HashMap<String, Module>,
71    /// Builtin name → module name mapping for autoload
72    autoload_builtins: HashMap<String, String>,
73    /// Condition name → module name mapping for autoload
74    autoload_conditions: HashMap<String, String>,
75    /// Parameter name → module name mapping for autoload
76    autoload_params: HashMap<String, String>,
77    /// Math function name → module name mapping for autoload
78    autoload_mathfuncs: HashMap<String, String>,
79    /// Hook functions
80    hooks: HashMap<String, Vec<String>>,
81    /// Wrappers (functions wrapping builtins)
82    wrappers: Vec<Wrapper>,
83}
84
85/// Wrapper entry (from module.c addwrapper/deletewrapper)
86#[derive(Debug, Clone)]
87pub struct Wrapper {
88    pub name: String,
89    pub flags: u32,
90    pub module: String,
91}
92
93impl ModuleTable {
94    pub fn new() -> Self {
95        let mut table = Self::default();
96        table.register_builtin_modules();
97        table
98    }
99
100    /// Register all statically-compiled modules (replaces dlopen)
101    fn register_builtin_modules(&mut self) {
102        let builtin_modules = [
103            (
104                "zsh/complete",
105                &[
106                    "compctl",
107                    "compcall",
108                    "comparguments",
109                    "compdescribe",
110                    "compfiles",
111                    "compgroups",
112                    "compquote",
113                    "comptags",
114                    "comptry",
115                    "compvalues",
116                ][..],
117            ),
118            ("zsh/complist", &["complist"][..]),
119            ("zsh/computil", &["compadd", "compset"][..]),
120            ("zsh/datetime", &["strftime"][..]),
121            (
122                "zsh/files",
123                &[
124                    "mkdir", "rmdir", "ln", "mv", "cp", "rm", "chmod", "chown", "sync",
125                ][..],
126            ),
127            ("zsh/langinfo", &[][..]),
128            ("zsh/mapfile", &[][..]),
129            ("zsh/mathfunc", &[][..]),
130            ("zsh/nearcolor", &[][..]),
131            ("zsh/net/socket", &["zsocket"][..]),
132            ("zsh/net/tcp", &["ztcp"][..]),
133            ("zsh/parameter", &[][..]),
134            (
135                "zsh/pcre",
136                &["pcre_compile", "pcre_match", "pcre_study"][..],
137            ),
138            ("zsh/regex", &[][..]),
139            ("zsh/sched", &["sched"][..]),
140            ("zsh/stat", &["zstat"][..]),
141            (
142                "zsh/system",
143                &[
144                    "sysread", "syswrite", "sysopen", "sysseek", "syserror", "zsystem",
145                ][..],
146            ),
147            ("zsh/termcap", &["echotc"][..]),
148            ("zsh/terminfo", &["echoti"][..]),
149            ("zsh/watch", &["log"][..]),
150            ("zsh/zftp", &["zftp"][..]),
151            ("zsh/zleparameter", &[][..]),
152            ("zsh/zprof", &["zprof"][..]),
153            ("zsh/zpty", &["zpty"][..]),
154            ("zsh/zselect", &["zselect"][..]),
155            (
156                "zsh/zutil",
157                &["zstyle", "zformat", "zparseopts", "zregexparse"][..],
158            ),
159            (
160                "zsh/attr",
161                &["zgetattr", "zsetattr", "zdelattr", "zlistattr"][..],
162            ),
163            ("zsh/cap", &["cap", "getcap", "setcap"][..]),
164            ("zsh/clone", &["clone"][..]),
165            ("zsh/curses", &["zcurses"][..]),
166            ("zsh/db/gdbm", &["ztie", "zuntie", "zgdbmpath"][..]),
167            ("zsh/param/private", &["private"][..]),
168        ];
169
170        for (name, builtins) in &builtin_modules {
171            let mut module = Module::new(name);
172            for builtin in *builtins {
173                module.features.push(ModuleFeature {
174                    name: builtin.to_string(),
175                    feature_type: FeatureType::Builtin,
176                    enabled: true,
177                });
178            }
179            self.modules.insert(name.to_string(), module);
180        }
181    }
182
183    /// Load a module (from module.c load_module)
184    pub fn load_module(&mut self, name: &str) -> bool {
185        if self.modules.contains_key(name) {
186            if let Some(m) = self.modules.get_mut(name) {
187                m.state = ModuleState::Loaded;
188            }
189            return true;
190        }
191        // In zshrs, all modules are static — if it's not registered, it doesn't exist
192        false
193    }
194
195    /// Unload a module (from module.c unload_module)
196    pub fn unload_module(&mut self, name: &str) -> bool {
197        if let Some(module) = self.modules.get_mut(name) {
198            module.state = ModuleState::Unloaded;
199            return true;
200        }
201        false
202    }
203
204    /// Check if module is loaded
205    pub fn is_loaded(&self, name: &str) -> bool {
206        self.modules
207            .get(name)
208            .map(|m| m.is_loaded())
209            .unwrap_or(false)
210    }
211
212    /// List all loaded modules
213    pub fn list_loaded(&self) -> Vec<&str> {
214        self.modules
215            .iter()
216            .filter(|(_, m)| m.is_loaded())
217            .map(|(name, _)| name.as_str())
218            .collect()
219    }
220
221    /// List all modules (including unloaded)
222    pub fn list_all(&self) -> Vec<(&str, &ModuleState)> {
223        self.modules
224            .iter()
225            .map(|(name, m)| (name.as_str(), &m.state))
226            .collect()
227    }
228
229    // ------- Builtin management (from module.c addbuiltin/deletebuiltin) -------
230
231    /// Register a builtin (from module.c addbuiltin)
232    pub fn addbuiltin(&mut self, name: &str, module: &str) {
233        if let Some(m) = self.modules.get_mut(module) {
234            m.features.push(ModuleFeature {
235                name: name.to_string(),
236                feature_type: FeatureType::Builtin,
237                enabled: true,
238            });
239        }
240    }
241
242    /// Unregister a builtin (from module.c deletebuiltin)
243    pub fn deletebuiltin(&mut self, name: &str, module: &str) {
244        if let Some(m) = self.modules.get_mut(module) {
245            m.features
246                .retain(|f| f.name != name || f.feature_type != FeatureType::Builtin);
247        }
248    }
249
250    /// Register autoloading builtin (from module.c add_autobin)
251    pub fn add_autobin(&mut self, name: &str, module: &str) {
252        self.autoload_builtins
253            .insert(name.to_string(), module.to_string());
254    }
255
256    /// Remove autoloading builtin (from module.c del_autobin)
257    pub fn del_autobin(&mut self, name: &str) {
258        self.autoload_builtins.remove(name);
259    }
260
261    /// Set builtins en masse (from module.c setbuiltins/addbuiltins)
262    pub fn setbuiltins(&mut self, module: &str, names: &[&str]) {
263        for name in names {
264            self.addbuiltin(name, module);
265        }
266    }
267
268    // ------- Condition management (from module.c addconddef/deleteconddef) -------
269
270    /// Register a condition (from module.c addconddef)
271    pub fn addconddef(&mut self, name: &str, module: &str) {
272        if let Some(m) = self.modules.get_mut(module) {
273            m.features.push(ModuleFeature {
274                name: name.to_string(),
275                feature_type: FeatureType::Condition,
276                enabled: true,
277            });
278        }
279    }
280
281    /// Unregister a condition (from module.c deleteconddef)
282    pub fn deleteconddef(&mut self, name: &str, module: &str) {
283        if let Some(m) = self.modules.get_mut(module) {
284            m.features
285                .retain(|f| f.name != name || f.feature_type != FeatureType::Condition);
286        }
287    }
288
289    /// Get condition definition (from module.c getconddef)
290    pub fn getconddef(&self, name: &str) -> Option<&str> {
291        for (mod_name, module) in &self.modules {
292            for feature in &module.features {
293                if feature.name == name && feature.feature_type == FeatureType::Condition {
294                    return Some(mod_name);
295                }
296            }
297        }
298        None
299    }
300
301    /// Register autoloading condition (from module.c add_autocond)
302    pub fn add_autocond(&mut self, name: &str, module: &str) {
303        self.autoload_conditions
304            .insert(name.to_string(), module.to_string());
305    }
306
307    /// Remove autoloading condition (from module.c del_autocond)
308    pub fn del_autocond(&mut self, name: &str) {
309        self.autoload_conditions.remove(name);
310    }
311
312    // ------- Hook management (from module.c addhookdef/deletehookdef) -------
313
314    /// Register a hook (from module.c addhookdef)
315    pub fn addhookdef(&mut self, name: &str) {
316        self.hooks.entry(name.to_string()).or_default();
317    }
318
319    /// Register multiple hooks (from module.c addhookdefs)
320    pub fn addhookdefs(&mut self, names: &[&str]) {
321        for name in names {
322            self.addhookdef(name);
323        }
324    }
325
326    /// Unregister a hook (from module.c deletehookdef)
327    pub fn deletehookdef(&mut self, name: &str) {
328        self.hooks.remove(name);
329    }
330
331    /// Unregister multiple hooks (from module.c deletehookdefs)
332    pub fn deletehookdefs(&mut self, names: &[&str]) {
333        for name in names {
334            self.deletehookdef(name);
335        }
336    }
337
338    /// Add function to hook (from module.c addhookdeffunc/addhookfunc)
339    pub fn addhookfunc(&mut self, hook: &str, func: &str) {
340        self.hooks
341            .entry(hook.to_string())
342            .or_default()
343            .push(func.to_string());
344    }
345
346    /// Remove function from hook (from module.c deletehookdeffunc/deletehookfunc)
347    pub fn deletehookfunc(&mut self, hook: &str, func: &str) {
348        if let Some(funcs) = self.hooks.get_mut(hook) {
349            funcs.retain(|f| f != func);
350        }
351    }
352
353    /// Get hook definition (from module.c gethookdef)
354    pub fn gethookdef(&self, name: &str) -> Option<&Vec<String>> {
355        self.hooks.get(name)
356    }
357
358    /// Run hook functions (from module.c runhookdef)
359    pub fn runhookdef(&self, name: &str) -> Vec<String> {
360        self.hooks.get(name).cloned().unwrap_or_default()
361    }
362
363    // ------- Parameter management (from module.c addparamdef/deleteparamdef) -------
364
365    /// Register a parameter from module (from module.c addparamdef/checkaddparam)
366    pub fn addparamdef(&mut self, name: &str, module: &str) {
367        if let Some(m) = self.modules.get_mut(module) {
368            m.features.push(ModuleFeature {
369                name: name.to_string(),
370                feature_type: FeatureType::Parameter,
371                enabled: true,
372            });
373        }
374    }
375
376    /// Unregister a parameter (from module.c deleteparamdef)
377    pub fn deleteparamdef(&mut self, name: &str, module: &str) {
378        if let Some(m) = self.modules.get_mut(module) {
379            m.features
380                .retain(|f| f.name != name || f.feature_type != FeatureType::Parameter);
381        }
382    }
383
384    /// Set parameters en masse (from module.c setparamdefs)
385    pub fn setparamdefs(&mut self, module: &str, names: &[&str]) {
386        for name in names {
387            self.addparamdef(name, module);
388        }
389    }
390
391    /// Register autoloading parameter (from module.c add_autoparam)
392    pub fn add_autoparam(&mut self, name: &str, module: &str) {
393        self.autoload_params
394            .insert(name.to_string(), module.to_string());
395    }
396
397    /// Remove autoloading parameter (from module.c del_autoparam)
398    pub fn del_autoparam(&mut self, name: &str) {
399        self.autoload_params.remove(name);
400    }
401
402    // ------- Wrapper management (from module.c addwrapper/deletewrapper) -------
403
404    /// Add wrapper (from module.c addwrapper)
405    pub fn addwrapper(&mut self, name: &str, flags: u32, module: &str) {
406        self.wrappers.push(Wrapper {
407            name: name.to_string(),
408            flags,
409            module: module.to_string(),
410        });
411    }
412
413    /// Remove wrapper (from module.c deletewrapper)
414    pub fn deletewrapper(&mut self, module: &str, name: &str) {
415        self.wrappers
416            .retain(|w| w.module != module || w.name != name);
417    }
418
419    // ------- Feature enable/disable (from module.c features_/enables_) -------
420
421    /// Enable a feature (from module.c enables_)
422    pub fn enable_feature(&mut self, module: &str, name: &str) -> bool {
423        if let Some(m) = self.modules.get_mut(module) {
424            for feature in &mut m.features {
425                if feature.name == name {
426                    feature.enabled = true;
427                    return true;
428                }
429            }
430        }
431        false
432    }
433
434    /// Disable a feature
435    pub fn disable_feature(&mut self, module: &str, name: &str) -> bool {
436        if let Some(m) = self.modules.get_mut(module) {
437            for feature in &mut m.features {
438                if feature.name == name {
439                    feature.enabled = false;
440                    return true;
441                }
442            }
443        }
444        false
445    }
446
447    /// List features of a module (from module.c features_)
448    pub fn list_features(&self, module: &str) -> Vec<&ModuleFeature> {
449        self.modules
450            .get(module)
451            .map(|m| m.features.iter().collect())
452            .unwrap_or_default()
453    }
454
455    /// Check if a module is linked (statically compiled) (from module.c module_linked)
456    pub fn module_linked(&self, name: &str) -> bool {
457        self.modules.contains_key(name)
458    }
459
460    /// Resolve autoload — find which module provides a builtin
461    pub fn resolve_autoload_builtin(&self, name: &str) -> Option<&str> {
462        self.autoload_builtins.get(name).map(|s| s.as_str())
463    }
464
465    /// Resolve autoload — find which module provides a parameter
466    pub fn resolve_autoload_param(&self, name: &str) -> Option<&str> {
467        self.autoload_params.get(name).map(|s| s.as_str())
468    }
469
470    /// Ensure a module's feature is available
471    pub fn ensurefeature(&mut self, module: &str, feature: &str) -> bool {
472        if !self.is_loaded(module) {
473            self.load_module(module);
474        }
475        self.is_loaded(module)
476    }
477}
478
479/// Module lifecycle callbacks (from module.c setup_/boot_/cleanup_/finish_)
480pub trait ModuleLifecycle {
481    fn setup(&mut self) -> i32 {
482        0
483    }
484    fn boot(&mut self) -> i32 {
485        0
486    }
487    fn cleanup(&mut self) -> i32 {
488        0
489    }
490    fn finish(&mut self) -> i32 {
491        0
492    }
493}
494
495/// Free module node (from module.c freemodulenode)
496pub fn freemodulenode(_module: Module) {
497    // Rust Drop handles this
498}
499
500/// Print module node (from module.c printmodulenode)
501pub fn printmodulenode(name: &str, module: &Module) -> String {
502    let state = match module.state {
503        ModuleState::Loaded => "loaded",
504        ModuleState::Autoloaded => "autoloaded",
505        ModuleState::Unloaded => "unloaded",
506        ModuleState::Failed => "failed",
507    };
508    format!("{} ({})", name, state)
509}
510
511/// Create new module table (from module.c newmoduletable)
512pub fn newmoduletable() -> ModuleTable {
513    ModuleTable::new()
514}
515
516/// Register module (from module.c register_module)
517pub fn register_module(table: &mut ModuleTable, name: &str) -> bool {
518    if table.modules.contains_key(name) {
519        return false;
520    }
521    table.modules.insert(name.to_string(), Module::new(name));
522    true
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_module_table_new() {
531        let table = ModuleTable::new();
532        assert!(table.is_loaded("zsh/complete"));
533        assert!(table.is_loaded("zsh/datetime"));
534        assert!(table.is_loaded("zsh/system"));
535        assert!(!table.is_loaded("nonexistent"));
536    }
537
538    #[test]
539    fn test_load_unload() {
540        let mut table = ModuleTable::new();
541        assert!(table.is_loaded("zsh/complete"));
542
543        table.unload_module("zsh/complete");
544        assert!(!table.is_loaded("zsh/complete"));
545
546        table.load_module("zsh/complete");
547        assert!(table.is_loaded("zsh/complete"));
548    }
549
550    #[test]
551    fn test_list_loaded() {
552        let table = ModuleTable::new();
553        let loaded = table.list_loaded();
554        assert!(loaded.len() > 20);
555        assert!(loaded.contains(&"zsh/complete"));
556    }
557
558    #[test]
559    fn test_hooks() {
560        let mut table = ModuleTable::new();
561        table.addhookdef("chpwd");
562        table.addhookfunc("chpwd", "my_chpwd_handler");
563
564        let funcs = table.runhookdef("chpwd");
565        assert_eq!(funcs, vec!["my_chpwd_handler"]);
566
567        table.deletehookfunc("chpwd", "my_chpwd_handler");
568        let funcs = table.runhookdef("chpwd");
569        assert!(funcs.is_empty());
570    }
571
572    #[test]
573    fn test_autoload() {
574        let mut table = ModuleTable::new();
575        table.add_autobin("my_cmd", "zsh/mymodule");
576        assert_eq!(
577            table.resolve_autoload_builtin("my_cmd"),
578            Some("zsh/mymodule")
579        );
580        assert_eq!(table.resolve_autoload_builtin("nonexistent"), None);
581    }
582
583    #[test]
584    fn test_features() {
585        let table = ModuleTable::new();
586        let features = table.list_features("zsh/complete");
587        assert!(!features.is_empty());
588        assert!(features.iter().any(|f| f.name == "compctl"));
589    }
590
591    #[test]
592    fn test_module_linked() {
593        let table = ModuleTable::new();
594        assert!(table.module_linked("zsh/complete"));
595        assert!(table.module_linked("zsh/stat"));
596        assert!(!table.module_linked("zsh/nonexistent"));
597    }
598
599    #[test]
600    fn test_wrappers() {
601        let mut table = ModuleTable::new();
602        table.addwrapper("cd", 0, "zsh/mymod");
603        assert_eq!(table.wrappers.len(), 1);
604
605        table.deletewrapper("zsh/mymod", "cd");
606        assert!(table.wrappers.is_empty());
607    }
608
609    #[test]
610    fn test_printmodulenode() {
611        let module = Module::new("zsh/test");
612        let output = printmodulenode("zsh/test", &module);
613        assert!(output.contains("zsh/test"));
614        assert!(output.contains("loaded"));
615    }
616}