1use crate::ported::utils::{errflag, ERRFLAG_ERROR};
24#[allow(unused_imports)]
25use crate::ported::vm_helper::ShellExecutor;
26use crate::ported::zsh_h::PM_UNDEFINED;
27use rusqlite::{params, Connection};
28use std::collections::HashMap;
29use std::env;
30use std::os::unix::fs::MetadataExt;
31use std::path::{Path, PathBuf};
32use std::sync::atomic::Ordering;
33use std::sync::OnceLock;
34
35pub(crate) struct PluginSnapshot {
37 pub(crate) functions: std::collections::HashSet<String>,
38 pub(crate) aliases: std::collections::HashSet<String>,
39 pub(crate) global_aliases: std::collections::HashSet<String>,
40 pub(crate) suffix_aliases: std::collections::HashSet<String>,
41 pub(crate) variables: HashMap<String, String>,
42 pub(crate) arrays: std::collections::HashSet<String>,
43 pub(crate) assoc_arrays: std::collections::HashSet<String>,
44 pub(crate) fpath: Vec<PathBuf>,
45 pub(crate) options: HashMap<String, bool>,
46 pub(crate) hooks: HashMap<String, Vec<String>>,
47 pub(crate) autoloads: std::collections::HashSet<String>,
48}
49
50fn current_binary_mtime() -> Option<i64> {
58 static BIN_MTIME: OnceLock<Option<i64>> = OnceLock::new();
59 *BIN_MTIME.get_or_init(|| {
60 let exe = std::env::current_exe().ok()?;
61 let meta = std::fs::metadata(&exe).ok()?;
62 Some(meta.mtime())
63 })
64}
65
66#[derive(Debug, Clone, Default)]
75pub struct PluginDelta {
76 pub functions: Vec<(String, Vec<u8>)>, pub aliases: Vec<(String, String, AliasKind)>, pub global_aliases: Vec<(String, String)>,
80 pub suffix_aliases: Vec<(String, String)>,
82 pub variables: Vec<(String, String)>,
84 pub exports: Vec<(String, String)>, pub arrays: Vec<(String, Vec<String>)>,
87 pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
89 pub completions: Vec<(String, String)>, pub fpath_additions: Vec<String>,
92 pub hooks: Vec<(String, String)>, pub bindkeys: Vec<(String, String, String)>, pub zstyles: Vec<(String, String, String)>, pub options_changed: Vec<(String, bool)>, pub autoloads: Vec<(String, String)>, }
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum AliasKind {
101 Regular,
103 Global,
105 Suffix,
107}
108
109impl AliasKind {
110 fn as_i32(self) -> i32 {
111 match self {
112 AliasKind::Regular => 0,
113 AliasKind::Global => 1,
114 AliasKind::Suffix => 2,
115 }
116 }
117 fn from_i32(v: i32) -> Self {
118 match v {
119 1 => AliasKind::Global,
120 2 => AliasKind::Suffix,
121 _ => AliasKind::Regular,
122 }
123 }
124}
125
126pub struct PluginCache {
128 conn: Connection,
130}
131
132impl PluginCache {
133 pub fn open(path: &Path) -> rusqlite::Result<Self> {
135 let conn = Connection::open(path)?;
136 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
137 let cache = Self { conn };
138 cache.init_schema()?;
139 Ok(cache)
140 }
141
142 fn init_schema(&self) -> rusqlite::Result<()> {
143 self.conn.execute_batch(
144 r#"
145 CREATE TABLE IF NOT EXISTS plugins (
146 id INTEGER PRIMARY KEY,
147 path TEXT NOT NULL UNIQUE,
148 mtime_secs INTEGER NOT NULL,
149 mtime_nsecs INTEGER NOT NULL,
150 source_time_ms INTEGER NOT NULL,
151 cached_at INTEGER NOT NULL,
152 binary_mtime INTEGER NOT NULL DEFAULT 0
153 );
154
155 CREATE TABLE IF NOT EXISTS plugin_functions (
156 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
157 name TEXT NOT NULL,
158 body BLOB NOT NULL
159 );
160
161 CREATE TABLE IF NOT EXISTS plugin_aliases (
162 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
163 name TEXT NOT NULL,
164 value TEXT NOT NULL,
165 kind INTEGER NOT NULL DEFAULT 0
166 );
167
168 CREATE TABLE IF NOT EXISTS plugin_variables (
169 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
170 name TEXT NOT NULL,
171 value TEXT NOT NULL,
172 is_export INTEGER NOT NULL DEFAULT 0
173 );
174
175 CREATE TABLE IF NOT EXISTS plugin_arrays (
176 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
177 name TEXT NOT NULL,
178 value_json TEXT NOT NULL
179 );
180
181 -- Associative-array deltas (e.g. ZINIT[BIN_DIR]=...). Stored
182 -- as JSON {key: value} so insertion order isn't load-bearing
183 -- (matches HashMap semantics on the Rust side). Direct
184 -- analogue of plugin_arrays for assoc shape.
185 CREATE TABLE IF NOT EXISTS plugin_assoc_arrays (
186 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
187 name TEXT NOT NULL,
188 value_json TEXT NOT NULL
189 );
190
191 CREATE TABLE IF NOT EXISTS plugin_completions (
192 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
193 command TEXT NOT NULL,
194 function TEXT NOT NULL
195 );
196
197 CREATE TABLE IF NOT EXISTS plugin_fpath (
198 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
199 path TEXT NOT NULL
200 );
201
202 CREATE TABLE IF NOT EXISTS plugin_hooks (
203 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
204 hook TEXT NOT NULL,
205 function TEXT NOT NULL
206 );
207
208 CREATE TABLE IF NOT EXISTS plugin_bindkeys (
209 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
210 keyseq TEXT NOT NULL,
211 widget TEXT NOT NULL,
212 keymap TEXT NOT NULL DEFAULT 'main'
213 );
214
215 CREATE TABLE IF NOT EXISTS plugin_zstyles (
216 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
217 pattern TEXT NOT NULL,
218 style TEXT NOT NULL,
219 value TEXT NOT NULL
220 );
221
222 CREATE TABLE IF NOT EXISTS plugin_options (
223 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
224 name TEXT NOT NULL,
225 enabled INTEGER NOT NULL
226 );
227
228 CREATE TABLE IF NOT EXISTS plugin_autoloads (
229 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
230 function TEXT NOT NULL,
231 flags TEXT NOT NULL DEFAULT ''
232 );
233
234 -- compaudit cache: security audit results per fpath directory
235 CREATE TABLE IF NOT EXISTS compaudit_cache (
236 id INTEGER PRIMARY KEY,
237 path TEXT NOT NULL UNIQUE,
238 mtime_secs INTEGER NOT NULL,
239 mtime_nsecs INTEGER NOT NULL,
240 uid INTEGER NOT NULL,
241 mode INTEGER NOT NULL,
242 is_secure INTEGER NOT NULL,
243 checked_at INTEGER NOT NULL
244 );
245
246 CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
247 CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
248
249 -- Migration: legacy script_bytecode table (bytecode now lives in
250 -- the rkyv shard at ~/.zshrs/scripts.rkyv). Drop on open so
251 -- existing DBs reclaim the space and don't carry stale bytecode.
252 DROP INDEX IF EXISTS idx_script_bytecode_path;
253 DROP TABLE IF EXISTS script_bytecode;
254 "#,
255 )?;
256 let _ = self.conn.execute(
267 "ALTER TABLE plugins ADD COLUMN binary_mtime INTEGER NOT NULL DEFAULT 0",
268 [],
269 );
270 Ok(())
271 }
272
273 pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
284 let row: Option<(i64, i64)> = self
285 .conn
286 .query_row(
287 "SELECT id, binary_mtime FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
288 params![path, mtime_secs, mtime_nsecs],
289 |row| Ok((row.get(0)?, row.get(1)?)),
290 )
291 .ok();
292 let (id, cached_bin_mtime) = row?;
293 if let Some(bin_mtime) = current_binary_mtime() {
294 if cached_bin_mtime < bin_mtime {
295 return None;
296 }
297 }
298 Some(id)
299 }
300
301 pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
303 let mut delta = PluginDelta::default();
304
305 let mut stmt = self
307 .conn
308 .prepare("SELECT name, body FROM plugin_functions WHERE plugin_id = ?1")?;
309 let rows = stmt.query_map(params![plugin_id], |row| {
310 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
311 })?;
312 for r in rows {
313 delta.functions.push(r?);
314 }
315
316 let mut stmt = self
318 .conn
319 .prepare("SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1")?;
320 let rows = stmt.query_map(params![plugin_id], |row| {
321 Ok((
322 row.get::<_, String>(0)?,
323 row.get::<_, String>(1)?,
324 AliasKind::from_i32(row.get::<_, i32>(2)?),
325 ))
326 })?;
327 for r in rows {
328 delta.aliases.push(r?);
329 }
330
331 let mut stmt = self
333 .conn
334 .prepare("SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1")?;
335 let rows = stmt.query_map(params![plugin_id], |row| {
336 Ok((
337 row.get::<_, String>(0)?,
338 row.get::<_, String>(1)?,
339 row.get::<_, bool>(2)?,
340 ))
341 })?;
342 for r in rows {
343 let (name, value, is_export) = r?;
344 if is_export {
345 delta.exports.push((name, value));
346 } else {
347 delta.variables.push((name, value));
348 }
349 }
350
351 let mut stmt = self
353 .conn
354 .prepare("SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1")?;
355 let rows = stmt.query_map(params![plugin_id], |row| {
356 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
357 })?;
358 for r in rows {
359 let (name, json) = r?;
360 let vals: Vec<String> = json
362 .trim_matches(|c| c == '[' || c == ']')
363 .split(',')
364 .map(|s| s.trim().trim_matches('"').to_string())
365 .filter(|s| !s.is_empty())
366 .collect();
367 delta.arrays.push((name, vals));
368 }
369
370 let mut stmt = self
374 .conn
375 .prepare("SELECT name, value_json FROM plugin_assoc_arrays WHERE plugin_id = ?1")?;
376 let rows = stmt.query_map(params![plugin_id], |row| {
377 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
378 })?;
379 for r in rows {
380 let (name, json) = r?;
381 let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or_default();
382 delta.assoc_arrays.push((name, map));
383 }
384
385 let mut stmt = self
387 .conn
388 .prepare("SELECT command, function FROM plugin_completions WHERE plugin_id = ?1")?;
389 let rows = stmt.query_map(params![plugin_id], |row| {
390 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
391 })?;
392 for r in rows {
393 delta.completions.push(r?);
394 }
395
396 let mut stmt = self
398 .conn
399 .prepare("SELECT path FROM plugin_fpath WHERE plugin_id = ?1")?;
400 let rows = stmt.query_map(params![plugin_id], |row| row.get::<_, String>(0))?;
401 for r in rows {
402 delta.fpath_additions.push(r?);
403 }
404
405 let mut stmt = self
407 .conn
408 .prepare("SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1")?;
409 let rows = stmt.query_map(params![plugin_id], |row| {
410 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
411 })?;
412 for r in rows {
413 delta.hooks.push(r?);
414 }
415
416 let mut stmt = self
418 .conn
419 .prepare("SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1")?;
420 let rows = stmt.query_map(params![plugin_id], |row| {
421 Ok((
422 row.get::<_, String>(0)?,
423 row.get::<_, String>(1)?,
424 row.get::<_, String>(2)?,
425 ))
426 })?;
427 for r in rows {
428 delta.bindkeys.push(r?);
429 }
430
431 let mut stmt = self
433 .conn
434 .prepare("SELECT pattern, style, value FROM plugin_zstyles WHERE plugin_id = ?1")?;
435 let rows = stmt.query_map(params![plugin_id], |row| {
436 Ok((
437 row.get::<_, String>(0)?,
438 row.get::<_, String>(1)?,
439 row.get::<_, String>(2)?,
440 ))
441 })?;
442 for r in rows {
443 delta.zstyles.push(r?);
444 }
445
446 let mut stmt = self
448 .conn
449 .prepare("SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1")?;
450 let rows = stmt.query_map(params![plugin_id], |row| {
451 Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
452 })?;
453 for r in rows {
454 delta.options_changed.push(r?);
455 }
456
457 let mut stmt = self
459 .conn
460 .prepare("SELECT function, flags FROM plugin_autoloads WHERE plugin_id = ?1")?;
461 let rows = stmt.query_map(params![plugin_id], |row| {
462 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
463 })?;
464 for r in rows {
465 delta.autoloads.push(r?);
466 }
467
468 Ok(delta)
469 }
470
471 pub fn store(
473 &self,
474 path: &str,
475 mtime_secs: i64,
476 mtime_nsecs: i64,
477 source_time_ms: u64,
478 delta: &PluginDelta,
479 ) -> rusqlite::Result<()> {
480 let now = std::time::SystemTime::now()
481 .duration_since(std::time::UNIX_EPOCH)
482 .map(|d| d.as_secs() as i64)
483 .unwrap_or(0);
484
485 self.conn
487 .execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
488
489 let bin_mtime = current_binary_mtime().unwrap_or(0);
490 self.conn.execute(
491 "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at, binary_mtime) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
492 params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now, bin_mtime],
493 )?;
494 let plugin_id = self.conn.last_insert_rowid();
495
496 for (name, body) in &delta.functions {
498 self.conn.execute(
499 "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
500 params![plugin_id, name, body],
501 )?;
502 }
503
504 for (name, value, kind) in &delta.aliases {
506 self.conn.execute(
507 "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
508 params![plugin_id, name, value, kind.as_i32()],
509 )?;
510 }
511
512 for (name, value) in &delta.variables {
514 self.conn.execute(
515 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
516 params![plugin_id, name, value],
517 )?;
518 }
519 for (name, value) in &delta.exports {
520 self.conn.execute(
521 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
522 params![plugin_id, name, value],
523 )?;
524 }
525
526 for (name, vals) in &delta.arrays {
528 let json = format!(
529 "[{}]",
530 vals.iter()
531 .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
532 .collect::<Vec<_>>()
533 .join(",")
534 );
535 self.conn.execute(
536 "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
537 params![plugin_id, name, json],
538 )?;
539 }
540
541 for (name, map) in &delta.assoc_arrays {
547 let json = serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string());
548 self.conn.execute(
549 "INSERT INTO plugin_assoc_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
550 params![plugin_id, name, json],
551 )?;
552 }
553
554 for (cmd, func) in &delta.completions {
556 self.conn.execute(
557 "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
558 params![plugin_id, cmd, func],
559 )?;
560 }
561
562 for p in &delta.fpath_additions {
564 self.conn.execute(
565 "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
566 params![plugin_id, p],
567 )?;
568 }
569
570 for (hook, func) in &delta.hooks {
572 self.conn.execute(
573 "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
574 params![plugin_id, hook, func],
575 )?;
576 }
577
578 for (keyseq, widget, keymap) in &delta.bindkeys {
580 self.conn.execute(
581 "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
582 params![plugin_id, keyseq, widget, keymap],
583 )?;
584 }
585
586 for (pattern, style, value) in &delta.zstyles {
588 self.conn.execute(
589 "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
590 params![plugin_id, pattern, style, value],
591 )?;
592 }
593
594 for (name, enabled) in &delta.options_changed {
596 self.conn.execute(
597 "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
598 params![plugin_id, name, *enabled],
599 )?;
600 }
601
602 for (func, flags) in &delta.autoloads {
604 self.conn.execute(
605 "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
606 params![plugin_id, func, flags],
607 )?;
608 }
609
610 Ok(())
611 }
612
613 pub fn stats(&self) -> (i64, i64) {
615 let plugins: i64 = self
616 .conn
617 .query_row("SELECT COUNT(*) FROM plugins", [], |r| r.get(0))
618 .unwrap_or(0);
619 let functions: i64 = self
620 .conn
621 .query_row("SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0))
622 .unwrap_or(0);
623 (plugins, functions)
624 }
625
626 pub fn count_stale(&self) -> usize {
628 let mut stmt = match self
629 .conn
630 .prepare("SELECT path, mtime_secs, mtime_nsecs FROM plugins")
631 {
632 Ok(s) => s,
633 Err(_) => return 0,
634 };
635 let rows = match stmt.query_map([], |row| {
636 Ok((
637 row.get::<_, String>(0)?,
638 row.get::<_, i64>(1)?,
639 row.get::<_, i64>(2)?,
640 ))
641 }) {
642 Ok(r) => r,
643 Err(_) => return 0,
644 };
645 let mut count = 0;
646 for (path, cached_s, cached_ns) in rows.flatten() {
647 match file_mtime(std::path::Path::new(&path)) {
648 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
649 None => count += 1, _ => {}
651 }
652 }
653 count
654 }
655
656 pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
663 self.conn.query_row(
664 "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
665 params![dir, mtime_secs, mtime_nsecs],
666 |row| row.get::<_, bool>(0),
667 ).ok()
668 }
669
670 pub fn store_compaudit(
672 &self,
673 dir: &str,
674 mtime_secs: i64,
675 mtime_nsecs: i64,
676 uid: u32,
677 mode: u32,
678 is_secure: bool,
679 ) -> rusqlite::Result<()> {
680 let now = std::time::SystemTime::now()
681 .duration_since(std::time::UNIX_EPOCH)
682 .map(|d| d.as_secs() as i64)
683 .unwrap_or(0);
684
685 self.conn.execute(
686 "INSERT OR REPLACE INTO compaudit_cache (path, mtime_secs, mtime_nsecs, uid, mode, is_secure, checked_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
687 params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
688 )?;
689 Ok(())
690 }
691
692 pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
695 let euid = unsafe { libc::geteuid() };
696 let mut insecure = Vec::new();
697
698 for dir in fpath {
699 let dir_str = dir.to_string_lossy().to_string();
700 let meta = match std::fs::metadata(dir) {
701 Ok(m) => m,
702 Err(_) => continue, };
704 let mt_s = meta.mtime();
705 let mt_ns = meta.mtime_nsec();
706
707 if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
709 if !is_secure {
710 insecure.push(dir_str);
711 }
712 continue;
713 }
714
715 let mode = meta.mode();
717 let uid = meta.uid();
718 let is_secure = Self::check_dir_security(&meta, euid);
719
720 let parent_secure = dir
722 .parent()
723 .and_then(|p| std::fs::metadata(p).ok())
724 .map(|pm| Self::check_dir_security(&pm, euid))
725 .unwrap_or(true);
726
727 let secure = is_secure && parent_secure;
728
729 let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
731
732 if !secure {
733 insecure.push(dir_str);
734 }
735 }
736
737 if insecure.is_empty() {
738 tracing::debug!(
739 dirs = fpath.len(),
740 "compaudit: all directories secure (cached)"
741 );
742 } else {
743 tracing::warn!(
744 insecure_count = insecure.len(),
745 dirs = fpath.len(),
746 "compaudit: insecure directories found"
747 );
748 }
749
750 insecure
751 }
752
753 pub fn list_plugin_paths(&self) -> Vec<(String, i64)> {
758 let mut stmt = match self
759 .conn
760 .prepare("SELECT path, mtime_secs FROM plugins ORDER BY id")
761 {
762 Ok(s) => s,
763 Err(_) => return Vec::new(),
764 };
765 let rows = match stmt.query_map([], |row| {
766 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
767 }) {
768 Ok(r) => r,
769 Err(_) => return Vec::new(),
770 };
771 rows.flatten().collect()
772 }
773
774 fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
777 let mode = meta.mode();
778 let uid = meta.uid();
779
780 if uid == 0 || uid == euid {
782 return true;
783 }
784
785 let group_writable = mode & 0o020 != 0;
787 let world_writable = mode & 0o002 != 0;
788
789 !group_writable && !world_writable
790 }
791}
792
793pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
795 let meta = std::fs::metadata(path).ok()?;
796 Some((meta.mtime(), meta.mtime_nsec()))
797}
798
799#[derive(Debug, Clone)]
805pub struct PluginEntry {
806 pub manager: String,
807 pub name: String,
808 pub root: PathBuf,
809}
810
811fn classify_plugin_path(path: &Path) -> PluginEntry {
815 let s = path.to_string_lossy();
816
817 for marker in ["/.zinit/plugins/", "/zinit/plugins/"] {
820 if let Some(start) = s.find(marker) {
821 let after = &s[start + marker.len()..];
822 if let Some(end) = after.find('/') {
823 let dir = &after[..end];
824 let name = dir.replacen("---", "/", 1);
825 let root: PathBuf = s[..start + marker.len() + end].into();
826 return PluginEntry { manager: "zinit".into(), name, root };
827 }
828 }
829 }
830
831 for (marker, kind) in [
833 ("/.oh-my-zsh/custom/plugins/", "plugin"),
834 ("/.oh-my-zsh/plugins/", "plugin"),
835 ("/.oh-my-zsh/custom/themes/", "theme"),
836 ("/.oh-my-zsh/themes/", "theme"),
837 ] {
838 if let Some(start) = s.find(marker) {
839 let after = &s[start + marker.len()..];
840 let end = after.find('/').unwrap_or(after.len());
841 let leaf = &after[..end];
842 let name = if kind == "theme" { format!("{}.theme", leaf) } else { leaf.to_string() };
843 let root: PathBuf = s[..start + marker.len() + end].into();
844 return PluginEntry { manager: "oh-my-zsh".into(), name, root };
845 }
846 }
847
848 if let Some(start) = s.find("/.zprezto/modules/") {
850 let after = &s[start + "/.zprezto/modules/".len()..];
851 let end = after.find('/').unwrap_or(after.len());
852 let name = after[..end].to_string();
853 let root: PathBuf = s[..start + "/.zprezto/modules/".len() + end].into();
854 return PluginEntry { manager: "prezto".into(), name, root };
855 }
856
857 for marker in ["/antidote/repos/", "/.cache/antidote/"] {
860 if let Some(start) = s.find(marker) {
861 let after = &s[start + marker.len()..];
862 let mut split = after.splitn(3, '/');
864 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
865 let name = format!("{}/{}", user, repo);
866 let root: PathBuf = format!(
867 "{}{}/{}",
868 &s[..start + marker.len()],
869 user,
870 repo
871 )
872 .into();
873 return PluginEntry { manager: "antidote".into(), name, root };
874 }
875 }
876 }
877
878 if let Some(start) = s.find("/.antigen/bundles/") {
880 let after = &s[start + "/.antigen/bundles/".len()..];
881 let mut split = after.splitn(3, '/');
882 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
883 let name = format!("{}/{}", user, repo);
884 let root: PathBuf = format!(
885 "{}/{}/{}",
886 &s[..start + "/.antigen/bundles".len()],
887 user,
888 repo
889 )
890 .into();
891 return PluginEntry { manager: "antigen".into(), name, root };
892 }
893 }
894
895 if let Some(start) = s.find("/.zplug/repos/") {
897 let after = &s[start + "/.zplug/repos/".len()..];
898 let mut split = after.splitn(3, '/');
899 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
900 let name = format!("{}/{}", user, repo);
901 let root: PathBuf = format!(
902 "{}/{}/{}",
903 &s[..start + "/.zplug/repos".len()],
904 user,
905 repo
906 )
907 .into();
908 return PluginEntry { manager: "zplug".into(), name, root };
909 }
910 }
911
912 if let Some(start) = s.find("/zsh-more-completions/") {
915 let root: PathBuf = s[..start + "/zsh-more-completions".len()].into();
916 return PluginEntry {
917 manager: "zsh-more-completions".into(),
918 name: "zsh-more-completions".into(),
919 root,
920 };
921 }
922
923 for marker in ["/.zpwr/", "/zpwr/"] {
925 if let Some(start) = s.find(marker) {
926 let root: PathBuf = s[..start + marker.len() - 1].into();
927 return PluginEntry {
928 manager: "zpwr".into(),
929 name: "zpwr".into(),
930 root,
931 };
932 }
933 }
934
935 let root = path.parent().map(PathBuf::from).unwrap_or_else(|| path.into());
937 let name = root
938 .file_name()
939 .map(|n| n.to_string_lossy().into_owned())
940 .unwrap_or_else(|| "(loose)".into());
941 PluginEntry { manager: "loose".into(), name, root }
942}
943
944pub fn list_plugins(cache_path: &Path) -> Vec<PluginEntry> {
948 let cache = match PluginCache::open(cache_path) {
949 Ok(c) => c,
950 Err(_) => return Vec::new(),
951 };
952 let mut seen: std::collections::BTreeMap<(String, String, PathBuf), PluginEntry> =
953 std::collections::BTreeMap::new();
954 for (path, _mtime) in cache.list_plugin_paths() {
955 let entry = classify_plugin_path(Path::new(&path));
956 seen.entry((entry.manager.clone(), entry.name.clone(), entry.root.clone()))
957 .or_insert(entry);
958 }
959 seen.into_values().collect()
960}
961
962pub fn dump_plugins_json() -> String {
976 let entries = list_plugins(&default_cache_path());
977 let mut s = String::from("{\"schema\":1,\"plugins\":[");
978 for (i, e) in entries.iter().enumerate() {
979 if i > 0 { s.push(','); }
980 s.push_str(&format!(
981 "{{\"manager\":{},\"name\":{},\"root\":{}}}",
982 json_str(&e.manager),
983 json_str(&e.name),
984 json_str(&e.root.to_string_lossy())
985 ));
986 }
987 s.push_str("]}");
988 s
989}
990
991fn json_str(s: &str) -> String {
992 let mut out = String::with_capacity(s.len() + 2);
993 out.push('"');
994 for c in s.chars() {
995 match c {
996 '"' => out.push_str("\\\""),
997 '\\' => out.push_str("\\\\"),
998 '\n' => out.push_str("\\n"),
999 '\r' => out.push_str("\\r"),
1000 '\t' => out.push_str("\\t"),
1001 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1002 c => out.push(c),
1003 }
1004 }
1005 out.push('"');
1006 out
1007}
1008
1009pub fn default_cache_path() -> PathBuf {
1012 if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
1013 return PathBuf::from(custom).join("plugins.db");
1014 }
1015 dirs::home_dir()
1016 .unwrap_or_else(|| PathBuf::from("/tmp"))
1017 .join(".zshrs/plugins.db")
1018}
1019
1020#[cfg(test)]
1021mod migration_tests {
1022 use super::*;
1023
1024 #[test]
1025 fn opening_an_existing_db_drops_legacy_script_bytecode_table() {
1026 let _g = crate::test_util::global_state_lock();
1027 let tmp = tempfile::tempdir().unwrap();
1032 let db_path = tmp.path().join("legacy.db");
1033
1034 let pre = Connection::open(&db_path).unwrap();
1036 pre.execute_batch(
1037 r#"
1038 CREATE TABLE script_bytecode (
1039 id INTEGER PRIMARY KEY,
1040 path TEXT NOT NULL UNIQUE,
1041 mtime_secs INTEGER NOT NULL,
1042 mtime_nsecs INTEGER NOT NULL,
1043 bytecode BLOB NOT NULL,
1044 cached_at INTEGER NOT NULL
1045 );
1046 CREATE INDEX idx_script_bytecode_path ON script_bytecode(path);
1047 INSERT INTO script_bytecode (id, path, mtime_secs, mtime_nsecs, bytecode, cached_at)
1048 VALUES (1, '/fake/legacy.zsh', 0, 0, x'00deadbeef', 0);
1049 "#,
1050 )
1051 .unwrap();
1052 drop(pre);
1053
1054 let _cache = PluginCache::open(&db_path).expect("open after migration");
1056
1057 let post = Connection::open(&db_path).unwrap();
1059 let exists: i64 = post
1060 .query_row(
1061 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='script_bytecode'",
1062 [],
1063 |row| row.get(0),
1064 )
1065 .unwrap();
1066 assert_eq!(exists, 0, "legacy script_bytecode must be dropped");
1067 }
1068
1069 fn with_zshrs_home<F: FnOnce()>(value: Option<&str>, f: F) {
1074 let prev = std::env::var_os("ZSHRS_HOME");
1075 match value {
1076 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1077 None => std::env::remove_var("ZSHRS_HOME"),
1078 }
1079 f();
1080 match prev {
1081 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1082 None => std::env::remove_var("ZSHRS_HOME"),
1083 }
1084 }
1085
1086 #[test]
1087 fn default_cache_path_honors_zshrs_home() {
1088 let _g = crate::test_util::global_state_lock();
1089 with_zshrs_home(Some("/tmp/zshrs-plugin-cache-home"), || {
1090 assert_eq!(
1091 default_cache_path(),
1092 PathBuf::from("/tmp/zshrs-plugin-cache-home/plugins.db")
1093 );
1094 });
1095 }
1096
1097 #[test]
1098 fn default_cache_path_filename_is_plugins_db() {
1099 let _g = crate::test_util::global_state_lock();
1100 with_zshrs_home(Some("/tmp/zshrs-plugin-fname"), || {
1101 assert_eq!(
1102 default_cache_path().file_name().and_then(|s| s.to_str()),
1103 Some("plugins.db")
1104 );
1105 });
1106 }
1107
1108 #[test]
1109 fn default_cache_path_falls_back_to_home_dot_zshrs() {
1110 let _g = crate::test_util::global_state_lock();
1111 with_zshrs_home(None, || {
1112 let p = default_cache_path();
1113 let s = p.to_string_lossy();
1115 assert!(
1116 s.ends_with(".zshrs/plugins.db"),
1117 "expected .zshrs/plugins.db tail, got: {}",
1118 s
1119 );
1120 });
1121 }
1122
1123 #[test]
1124 fn default_cache_path_uses_distinct_dir_per_zshrs_home_change() {
1125 let _g = crate::test_util::global_state_lock();
1126 with_zshrs_home(Some("/tmp/zshrs-plugin-a"), || {
1127 let a = default_cache_path();
1128 with_zshrs_home(Some("/tmp/zshrs-plugin-b"), || {
1129 let b = default_cache_path();
1130 assert_ne!(a, b, "different ZSHRS_HOME must yield different paths");
1131 });
1132 });
1133 }
1134
1135 #[test]
1140 fn file_mtime_returns_some_for_existing_file() {
1141 let _g = crate::test_util::global_state_lock();
1142 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime.txt");
1143 std::fs::write(&tmp, b"x").unwrap();
1144 let mt = file_mtime(&tmp);
1145 assert!(mt.is_some(), "existing file should produce mtime");
1146 let (secs, _ns) = mt.unwrap();
1148 assert!(secs > 0, "mtime secs must be positive: {}", secs);
1149 let _ = std::fs::remove_file(&tmp);
1150 }
1151
1152 #[test]
1153 fn file_mtime_returns_none_for_missing_path() {
1154 let _g = crate::test_util::global_state_lock();
1155 assert!(file_mtime(Path::new("/nonexistent/zshrs/missing.bin")).is_none());
1156 }
1157
1158 #[test]
1159 fn file_mtime_secs_monotonic_after_rewrite() {
1160 let _g = crate::test_util::global_state_lock();
1161 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime_two.txt");
1162 std::fs::write(&tmp, b"a").unwrap();
1163 let first = file_mtime(&tmp).unwrap();
1164 std::thread::sleep(std::time::Duration::from_millis(1100));
1166 std::fs::write(&tmp, b"b").unwrap();
1167 let second = file_mtime(&tmp).unwrap();
1168 assert!(
1171 second >= first,
1172 "mtime regressed: first={:?} second={:?}",
1173 first,
1174 second
1175 );
1176 let _ = std::fs::remove_file(&tmp);
1177 }
1178
1179 #[test]
1180 fn file_mtime_path_with_special_chars_resolves() {
1181 let _g = crate::test_util::global_state_lock();
1182 let tmp = std::env::temp_dir().join("zshrs plugin cache (space).bin");
1183 std::fs::write(&tmp, b"x").unwrap();
1184 let mt = file_mtime(&tmp);
1185 assert!(mt.is_some(), "spaces in filename must not block resolution");
1186 let _ = std::fs::remove_file(&tmp);
1187 }
1188
1189 #[test]
1190 fn default_cache_path_relative_zshrs_home_taken_verbatim() {
1191 let _g = crate::test_util::global_state_lock();
1192 with_zshrs_home(Some("relative-dir"), || {
1193 assert_eq!(
1194 default_cache_path(),
1195 PathBuf::from("relative-dir/plugins.db")
1196 );
1197 });
1198 }
1199
1200 #[test]
1201 fn default_cache_path_empty_zshrs_home_is_empty_dir_plus_db() {
1202 let _g = crate::test_util::global_state_lock();
1203 with_zshrs_home(Some(""), || {
1204 assert_eq!(default_cache_path(), PathBuf::from("plugins.db"));
1207 });
1208 }
1209}
1210
1211#[cfg(test)]
1212mod classify_tests {
1213 use super::*;
1214 use std::path::Path;
1215
1216 fn classify(p: &str) -> (String, String, String) {
1217 let e = classify_plugin_path(Path::new(p));
1218 (e.manager, e.name, e.root.to_string_lossy().into_owned())
1219 }
1220
1221 #[test]
1222 fn zinit_legacy_dir_user_repo() {
1223 let (m, n, r) = classify(
1224 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions/zsh-autosuggestions.plugin.zsh",
1225 );
1226 assert_eq!(m, "zinit");
1227 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1228 assert_eq!(
1229 r,
1230 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions"
1231 );
1232 }
1233
1234 #[test]
1235 fn zinit_xdg_dir_user_repo() {
1236 let (m, n, _) = classify(
1237 "/home/u/.local/share/zinit/plugins/romkatv---powerlevel10k/p10k.zsh",
1238 );
1239 assert_eq!(m, "zinit");
1240 assert_eq!(n, "romkatv/powerlevel10k");
1241 }
1242
1243 #[test]
1244 fn oh_my_zsh_core_plugin() {
1245 let (m, n, r) = classify("/Users/wizard/.oh-my-zsh/plugins/git/git.plugin.zsh");
1246 assert_eq!(m, "oh-my-zsh");
1247 assert_eq!(n, "git");
1248 assert_eq!(r, "/Users/wizard/.oh-my-zsh/plugins/git");
1249 }
1250
1251 #[test]
1252 fn oh_my_zsh_custom_plugin() {
1253 let (m, n, _) = classify(
1254 "/Users/wizard/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh",
1255 );
1256 assert_eq!(m, "oh-my-zsh");
1257 assert_eq!(n, "zsh-syntax-highlighting");
1258 }
1259
1260 #[test]
1261 fn oh_my_zsh_theme_tagged_with_theme_suffix() {
1262 let (m, n, _) =
1263 classify("/Users/wizard/.oh-my-zsh/themes/agnoster.zsh-theme");
1264 assert_eq!(m, "oh-my-zsh");
1265 assert_eq!(n, "agnoster.zsh-theme.theme");
1266 }
1267
1268 #[test]
1269 fn prezto_module() {
1270 let (m, n, _) = classify("/Users/wizard/.zprezto/modules/git/init.zsh");
1271 assert_eq!(m, "prezto");
1272 assert_eq!(n, "git");
1273 }
1274
1275 #[test]
1276 fn antidote_repo() {
1277 let (m, n, _) = classify(
1278 "/Users/wizard/.cache/antidote/zsh-users/zsh-autosuggestions/zsh-autosuggestions.zsh",
1279 );
1280 assert_eq!(m, "antidote");
1281 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1282 }
1283
1284 #[test]
1285 fn antigen_bundle() {
1286 let (m, n, _) = classify(
1287 "/Users/wizard/.antigen/bundles/zsh-users/zsh-completions/zsh-completions.plugin.zsh",
1288 );
1289 assert_eq!(m, "antigen");
1290 assert_eq!(n, "zsh-users/zsh-completions");
1291 }
1292
1293 #[test]
1294 fn zplug_repo() {
1295 let (m, n, _) = classify(
1296 "/Users/wizard/.zplug/repos/zsh-users/zsh-history-substring-search/zsh-history-substring-search.zsh",
1297 );
1298 assert_eq!(m, "zplug");
1299 assert_eq!(n, "zsh-users/zsh-history-substring-search");
1300 }
1301
1302 #[test]
1303 fn zsh_more_completions_groups_into_one() {
1304 let (m, n, _) = classify(
1305 "/Users/wizard/forkedRepos/zsh-more-completions/src/_some_long_completion",
1306 );
1307 assert_eq!(m, "zsh-more-completions");
1308 assert_eq!(n, "zsh-more-completions");
1309 }
1310
1311 #[test]
1312 fn zpwr_root_recognized() {
1313 let (m, n, _) = classify("/Users/wizard/.zpwr/local/.aliases.sh");
1314 assert_eq!(m, "zpwr");
1315 assert_eq!(n, "zpwr");
1316 }
1317
1318 #[test]
1319 fn loose_plugin_uses_parent_dir_as_name() {
1320 let (m, n, r) = classify("/opt/local/share/zsh/something/init.zsh");
1321 assert_eq!(m, "loose");
1322 assert_eq!(n, "something");
1323 assert_eq!(r, "/opt/local/share/zsh/something");
1324 }
1325}
1326
1327impl crate::ported::vm_helper::ShellExecutor {
1335 pub(crate) fn snapshot_state(&self) -> PluginSnapshot {
1337 PluginSnapshot {
1338 functions: self.function_names().into_iter().collect(),
1339 aliases: self.alias_entries().into_iter().map(|(k, _)| k).collect(),
1340 global_aliases: self
1341 .global_alias_entries()
1342 .into_iter()
1343 .map(|(k, _)| k)
1344 .collect(),
1345 suffix_aliases: self
1346 .suffix_alias_entries()
1347 .into_iter()
1348 .map(|(k, _)| k)
1349 .collect(),
1350 variables: if let Ok(tab) = crate::ported::params::paramtab().read() {
1351 tab.iter()
1352 .filter(|(_, pm)| pm.u_arr.is_none())
1353 .map(|(k, pm)| (k.clone(), pm.u_str.clone().unwrap_or_default()))
1354 .collect()
1355 } else {
1356 std::collections::HashMap::new()
1357 },
1358 arrays: if let Ok(tab) = crate::ported::params::paramtab().read() {
1359 tab.iter()
1360 .filter(|(_, pm)| pm.u_arr.is_some())
1361 .map(|(k, _)| k.clone())
1362 .collect()
1363 } else {
1364 std::collections::HashSet::new()
1365 },
1366 assoc_arrays: if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1367 m.keys().cloned().collect()
1368 } else {
1369 std::collections::HashSet::new()
1370 },
1371 fpath: self.fpath.clone(),
1372 options: crate::ported::options::opt_state_snapshot(),
1373 hooks: {
1374 let names = [
1376 "chpwd",
1377 "precmd",
1378 "preexec",
1379 "periodic",
1380 "zshexit",
1381 "zshaddhistory",
1382 ];
1383 let mut m = std::collections::HashMap::new();
1384 for h in &names {
1385 let arr_name = format!("{}_functions", h);
1386 if let Some(arr) = self.array(&arr_name) {
1387 if !arr.is_empty() {
1388 m.insert(h.to_string(), arr);
1389 }
1390 }
1391 }
1392 m
1393 },
1394 autoloads: {
1395 crate::ported::hashtable::shfunctab_lock()
1398 .read()
1399 .ok()
1400 .map(|t| {
1401 t.iter()
1402 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1403 .map(|(name, _)| name.clone())
1404 .collect()
1405 })
1406 .unwrap_or_default()
1407 },
1408 }
1409 }
1410 pub(crate) fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
1412 let mut delta = PluginDelta::default();
1413
1414 let mut fn_keys: Vec<&String> = self.function_source.keys().collect();
1423 fn_keys.sort();
1424 for name in fn_keys {
1425 if !snap.functions.contains(name) {
1426 let source = self.function_source.get(name).unwrap();
1427 delta
1428 .functions
1429 .push((name.clone(), source.as_bytes().to_vec()));
1430 }
1431 }
1432
1433 let push_alias = |delta: &mut PluginDelta,
1434 entries: Vec<(String, String)>,
1435 snap_set: &std::collections::HashSet<String>,
1436 kind: AliasKind| {
1437 let mut entries = entries;
1438 entries.sort_by(|a, b| a.0.cmp(&b.0));
1439 for (name, value) in entries {
1440 if !snap_set.contains(&name) {
1441 delta.aliases.push((name, value, kind));
1442 }
1443 }
1444 };
1445 push_alias(
1446 &mut delta,
1447 self.alias_entries(),
1448 &snap.aliases,
1449 AliasKind::Regular,
1450 );
1451 push_alias(
1452 &mut delta,
1453 self.global_alias_entries(),
1454 &snap.global_aliases,
1455 AliasKind::Global,
1456 );
1457 push_alias(
1458 &mut delta,
1459 self.suffix_alias_entries(),
1460 &snap.suffix_aliases,
1461 AliasKind::Suffix,
1462 );
1463
1464 const NON_REPLAYABLE_VARS: &[&str] = &[
1479 "0",
1480 "_",
1481 "?",
1482 "$",
1483 "!",
1484 "PPID",
1485 "RANDOM",
1486 "SECONDS",
1487 "EPOCHSECONDS",
1488 "EPOCHREALTIME",
1489 "LINENO",
1490 "OLDPWD",
1491 "PWD",
1492 "STATUS",
1493 "OPTIND",
1494 "OPTARG",
1495 "IFS",
1496 "FUNCNAME",
1497 "BASHPID",
1498 "BASH_LINENO",
1499 "BASH_SOURCE",
1500 "ZSH_ARGZERO",
1501 "ZSH_EVAL_CONTEXT",
1502 "ZSH_SUBSHELL",
1503 "HISTCMD",
1504 "MATCH",
1505 "MBEGIN",
1506 "MEND",
1507 ];
1508 let mut var_keys: Vec<String> = if let Ok(tab) = crate::ported::params::paramtab().read() {
1509 tab.iter()
1510 .filter(|(_, pm)| pm.u_arr.is_none())
1511 .map(|(k, _)| k.clone())
1512 .collect()
1513 } else {
1514 Vec::new()
1515 };
1516 var_keys.sort();
1517 for name in &var_keys {
1518 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1519 continue;
1520 }
1521 let value = crate::ported::params::getsparam(name).unwrap_or_default();
1522 match snap.variables.get(name) {
1523 Some(old) if old == &value => {} _ => {
1525 if env::var(name).ok().as_ref() == Some(&value) {
1527 delta.exports.push((name.clone(), value.clone()));
1528 } else {
1529 delta.variables.push((name.clone(), value.clone()));
1530 }
1531 }
1532 }
1533 }
1534
1535 let arr_entries: Vec<(String, Vec<String>)> =
1537 if let Ok(tab) = crate::ported::params::paramtab().read() {
1538 let mut v: Vec<(String, Vec<String>)> = tab
1539 .iter()
1540 .filter_map(|(k, pm)| pm.u_arr.clone().map(|a| (k.clone(), a)))
1541 .collect();
1542 v.sort_by(|a, b| a.0.cmp(&b.0));
1543 v
1544 } else {
1545 Vec::new()
1546 };
1547 for (name, values) in arr_entries {
1548 if !snap.arrays.contains(&name) {
1549 delta.arrays.push((name, values));
1550 }
1551 }
1552
1553 let assoc_entries: Vec<(String, indexmap::IndexMap<String, String>)> =
1560 if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1561 let mut v: Vec<(String, indexmap::IndexMap<String, String>)> =
1562 m.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1563 v.sort_by(|a, b| a.0.cmp(&b.0));
1564 v
1565 } else {
1566 Vec::new()
1567 };
1568 for (name, map) in assoc_entries {
1569 if !snap.assoc_arrays.contains(&name) {
1570 let plain: HashMap<String, String> =
1576 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1577 delta.assoc_arrays.push((name, plain));
1578 }
1579 }
1580
1581 for p in &self.fpath {
1583 if !snap.fpath.contains(p) {
1584 delta.fpath_additions.push(p.to_string_lossy().to_string());
1585 }
1586 }
1587
1588 let current = crate::ported::options::opt_state_snapshot();
1590 let mut opt_keys: Vec<&String> = current.keys().collect();
1591 opt_keys.sort();
1592 for name in opt_keys {
1593 let value = current.get(name).unwrap();
1594 match snap.options.get(name) {
1595 Some(old) if old == value => {}
1596 _ => delta.options_changed.push((name.clone(), *value)),
1597 }
1598 }
1599
1600 let names = [
1602 "chpwd",
1603 "precmd",
1604 "preexec",
1605 "periodic",
1606 "zshexit",
1607 "zshaddhistory",
1608 ];
1609 let mut hook_names: Vec<&&str> = names.iter().collect();
1610 hook_names.sort();
1611 for &h in hook_names {
1612 let arr_name = format!("{}_functions", h);
1613 let funcs = self.array(&arr_name).unwrap_or_default();
1614 let old_funcs = snap.hooks.get(h);
1615 for f in &funcs {
1616 let is_new = old_funcs.is_none_or(|old| !old.contains(f));
1617 if is_new {
1618 delta.hooks.push((h.to_string(), f.clone()));
1619 }
1620 }
1621 }
1622
1623 let current_autoloads: Vec<String> = crate::ported::hashtable::shfunctab_lock()
1625 .read()
1626 .ok()
1627 .map(|t| {
1628 t.iter()
1629 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1630 .map(|(name, _)| name.clone())
1631 .collect()
1632 })
1633 .unwrap_or_default();
1634 let mut autoload_keys: Vec<&String> = current_autoloads.iter().collect();
1635 autoload_keys.sort();
1636 for name in autoload_keys {
1637 if !snap.autoloads.contains(name) {
1638 delta.autoloads.push((name.clone(), String::new()));
1641 }
1642 }
1643
1644 delta
1645 }
1646 pub(crate) fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
1648 for (name, value, kind) in &delta.aliases {
1650 match kind {
1651 AliasKind::Regular => {
1652 self.set_alias(name.clone(), value.clone());
1653 }
1654 AliasKind::Global => {
1655 self.set_global_alias(name.clone(), value.clone());
1656 }
1657 AliasKind::Suffix => {
1658 self.set_suffix_alias(name.clone(), value.clone());
1659 }
1660 }
1661 }
1662
1663 const NON_REPLAYABLE_VARS: &[&str] = &[
1670 "0",
1671 "_",
1672 "?",
1673 "$",
1674 "!",
1675 "PPID",
1676 "RANDOM",
1677 "SECONDS",
1678 "EPOCHSECONDS",
1679 "EPOCHREALTIME",
1680 "LINENO",
1681 "OLDPWD",
1682 "PWD",
1683 "STATUS",
1684 "OPTIND",
1685 "OPTARG",
1686 "IFS",
1687 "FUNCNAME",
1688 "BASHPID",
1689 "BASH_LINENO",
1690 "BASH_SOURCE",
1691 "ZSH_ARGZERO",
1692 "ZSH_EVAL_CONTEXT",
1693 "ZSH_SUBSHELL",
1694 "HISTCMD",
1695 "MATCH",
1696 "MBEGIN",
1697 "MEND",
1698 ];
1699 for (name, value) in &delta.variables {
1700 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1701 continue;
1702 }
1703 self.set_scalar(name.clone(), value.clone());
1704 }
1705
1706 for (name, value) in &delta.exports {
1708 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1709 continue;
1710 }
1711 self.set_scalar(name.clone(), value.clone());
1712 env::set_var(name, value);
1713 }
1714
1715 for (name, values) in &delta.arrays {
1717 self.set_array(name.clone(), values.clone());
1718 }
1719
1720 for (name, map) in &delta.assoc_arrays {
1725 let mut idx_map: indexmap::IndexMap<String, String> =
1730 indexmap::IndexMap::with_capacity(map.len());
1731 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1736 entries.sort_by(|a, b| a.0.cmp(b.0));
1737 for (k, v) in entries {
1738 idx_map.insert(k.clone(), v.clone());
1739 }
1740 self.set_assoc(name.clone(), idx_map);
1741 }
1742
1743 for p in &delta.fpath_additions {
1745 let pb = PathBuf::from(p);
1746 if !self.fpath.contains(&pb) {
1747 self.fpath.push(pb);
1748 }
1749 }
1750
1751 if !delta.completions.is_empty() {
1753 let mut comps = self.assoc("_comps").unwrap_or_default();
1754 for (cmd, func) in &delta.completions {
1755 comps.insert(cmd.clone(), func.clone());
1756 }
1757 self.set_assoc("_comps".to_string(), comps);
1758 }
1759
1760 for (name, enabled) in &delta.options_changed {
1762 crate::ported::options::opt_state_set(name, *enabled);
1763 }
1764
1765 for (hook, func) in &delta.hooks {
1771 let array_name = format!("{}_functions", hook);
1772 let mut arr = self.array(&array_name).unwrap_or_default();
1773 if !arr.iter().any(|f| f == func) {
1774 arr.push(func.clone());
1775 crate::ported::params::setaparam(&array_name, arr);
1776 }
1777 }
1778
1779 for (name, bytes) in &delta.functions {
1783 let Ok(source) = std::str::from_utf8(bytes) else {
1784 continue;
1785 };
1786 let saved_errflag = errflag.load(Ordering::Relaxed);
1788 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
1789 crate::ported::parse::parse_init(source);
1790 let program = crate::ported::parse::parse();
1791 let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
1792 errflag.store(saved_errflag, Ordering::Relaxed);
1793 if parse_failed || program.lists.is_empty() {
1794 continue;
1795 }
1796 let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
1797 self.functions_compiled.insert(name.clone(), chunk);
1798 self.function_source
1799 .insert(name.clone(), source.to_string());
1800 }
1801 }
1802}
1803