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 {
827 manager: "zinit".into(),
828 name,
829 root,
830 };
831 }
832 }
833 }
834
835 for (marker, kind) in [
837 ("/.oh-my-zsh/custom/plugins/", "plugin"),
838 ("/.oh-my-zsh/plugins/", "plugin"),
839 ("/.oh-my-zsh/custom/themes/", "theme"),
840 ("/.oh-my-zsh/themes/", "theme"),
841 ] {
842 if let Some(start) = s.find(marker) {
843 let after = &s[start + marker.len()..];
844 let end = after.find('/').unwrap_or(after.len());
845 let leaf = &after[..end];
846 let name = if kind == "theme" {
847 format!("{}.theme", leaf)
848 } else {
849 leaf.to_string()
850 };
851 let root: PathBuf = s[..start + marker.len() + end].into();
852 return PluginEntry {
853 manager: "oh-my-zsh".into(),
854 name,
855 root,
856 };
857 }
858 }
859
860 if let Some(start) = s.find("/.zprezto/modules/") {
862 let after = &s[start + "/.zprezto/modules/".len()..];
863 let end = after.find('/').unwrap_or(after.len());
864 let name = after[..end].to_string();
865 let root: PathBuf = s[..start + "/.zprezto/modules/".len() + end].into();
866 return PluginEntry {
867 manager: "prezto".into(),
868 name,
869 root,
870 };
871 }
872
873 for marker in ["/antidote/repos/", "/.cache/antidote/"] {
876 if let Some(start) = s.find(marker) {
877 let after = &s[start + marker.len()..];
878 let mut split = after.splitn(3, '/');
880 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
881 let name = format!("{}/{}", user, repo);
882 let root: PathBuf =
883 format!("{}{}/{}", &s[..start + marker.len()], user, repo).into();
884 return PluginEntry {
885 manager: "antidote".into(),
886 name,
887 root,
888 };
889 }
890 }
891 }
892
893 if let Some(start) = s.find("/.antigen/bundles/") {
895 let after = &s[start + "/.antigen/bundles/".len()..];
896 let mut split = after.splitn(3, '/');
897 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
898 let name = format!("{}/{}", user, repo);
899 let root: PathBuf = format!(
900 "{}/{}/{}",
901 &s[..start + "/.antigen/bundles".len()],
902 user,
903 repo
904 )
905 .into();
906 return PluginEntry {
907 manager: "antigen".into(),
908 name,
909 root,
910 };
911 }
912 }
913
914 if let Some(start) = s.find("/.zplug/repos/") {
916 let after = &s[start + "/.zplug/repos/".len()..];
917 let mut split = after.splitn(3, '/');
918 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
919 let name = format!("{}/{}", user, repo);
920 let root: PathBuf =
921 format!("{}/{}/{}", &s[..start + "/.zplug/repos".len()], user, repo).into();
922 return PluginEntry {
923 manager: "zplug".into(),
924 name,
925 root,
926 };
927 }
928 }
929
930 if let Some(start) = s.find("/zsh-more-completions/") {
933 let root: PathBuf = s[..start + "/zsh-more-completions".len()].into();
934 return PluginEntry {
935 manager: "zsh-more-completions".into(),
936 name: "zsh-more-completions".into(),
937 root,
938 };
939 }
940
941 for marker in ["/.zpwr/", "/zpwr/"] {
943 if let Some(start) = s.find(marker) {
944 let root: PathBuf = s[..start + marker.len() - 1].into();
945 return PluginEntry {
946 manager: "zpwr".into(),
947 name: "zpwr".into(),
948 root,
949 };
950 }
951 }
952
953 let root = path
955 .parent()
956 .map(PathBuf::from)
957 .unwrap_or_else(|| path.into());
958 let name = root
959 .file_name()
960 .map(|n| n.to_string_lossy().into_owned())
961 .unwrap_or_else(|| "(loose)".into());
962 PluginEntry {
963 manager: "loose".into(),
964 name,
965 root,
966 }
967}
968
969pub fn list_plugins(cache_path: &Path) -> Vec<PluginEntry> {
973 let cache = match PluginCache::open(cache_path) {
974 Ok(c) => c,
975 Err(_) => return Vec::new(),
976 };
977 let mut seen: std::collections::BTreeMap<(String, String, PathBuf), PluginEntry> =
978 std::collections::BTreeMap::new();
979 for (path, _mtime) in cache.list_plugin_paths() {
980 let entry = classify_plugin_path(Path::new(&path));
981 seen.entry((
982 entry.manager.clone(),
983 entry.name.clone(),
984 entry.root.clone(),
985 ))
986 .or_insert(entry);
987 }
988 seen.into_values().collect()
989}
990
991pub fn dump_plugins_json() -> String {
1005 let entries = list_plugins(&default_cache_path());
1006 let mut s = String::from("{\"schema\":1,\"plugins\":[");
1007 for (i, e) in entries.iter().enumerate() {
1008 if i > 0 {
1009 s.push(',');
1010 }
1011 s.push_str(&format!(
1012 "{{\"manager\":{},\"name\":{},\"root\":{}}}",
1013 json_str(&e.manager),
1014 json_str(&e.name),
1015 json_str(&e.root.to_string_lossy())
1016 ));
1017 }
1018 s.push_str("]}");
1019 s
1020}
1021
1022fn json_str(s: &str) -> String {
1023 let mut out = String::with_capacity(s.len() + 2);
1024 out.push('"');
1025 for c in s.chars() {
1026 match c {
1027 '"' => out.push_str("\\\""),
1028 '\\' => out.push_str("\\\\"),
1029 '\n' => out.push_str("\\n"),
1030 '\r' => out.push_str("\\r"),
1031 '\t' => out.push_str("\\t"),
1032 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1033 c => out.push(c),
1034 }
1035 }
1036 out.push('"');
1037 out
1038}
1039
1040pub fn default_cache_path() -> PathBuf {
1043 if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
1044 return PathBuf::from(custom).join("plugins.db");
1045 }
1046 dirs::home_dir()
1047 .unwrap_or_else(|| PathBuf::from("/tmp"))
1048 .join(".zshrs/plugins.db")
1049}
1050
1051#[cfg(test)]
1052mod migration_tests {
1053 use super::*;
1054
1055 #[test]
1056 fn opening_an_existing_db_drops_legacy_script_bytecode_table() {
1057 let _g = crate::test_util::global_state_lock();
1058 let tmp = tempfile::tempdir().unwrap();
1063 let db_path = tmp.path().join("legacy.db");
1064
1065 let pre = Connection::open(&db_path).unwrap();
1067 pre.execute_batch(
1068 r#"
1069 CREATE TABLE script_bytecode (
1070 id INTEGER PRIMARY KEY,
1071 path TEXT NOT NULL UNIQUE,
1072 mtime_secs INTEGER NOT NULL,
1073 mtime_nsecs INTEGER NOT NULL,
1074 bytecode BLOB NOT NULL,
1075 cached_at INTEGER NOT NULL
1076 );
1077 CREATE INDEX idx_script_bytecode_path ON script_bytecode(path);
1078 INSERT INTO script_bytecode (id, path, mtime_secs, mtime_nsecs, bytecode, cached_at)
1079 VALUES (1, '/fake/legacy.zsh', 0, 0, x'00deadbeef', 0);
1080 "#,
1081 )
1082 .unwrap();
1083 drop(pre);
1084
1085 let _cache = PluginCache::open(&db_path).expect("open after migration");
1087
1088 let post = Connection::open(&db_path).unwrap();
1090 let exists: i64 = post
1091 .query_row(
1092 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='script_bytecode'",
1093 [],
1094 |row| row.get(0),
1095 )
1096 .unwrap();
1097 assert_eq!(exists, 0, "legacy script_bytecode must be dropped");
1098 }
1099
1100 fn with_zshrs_home<F: FnOnce()>(value: Option<&str>, f: F) {
1105 let prev = std::env::var_os("ZSHRS_HOME");
1106 match value {
1107 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1108 None => std::env::remove_var("ZSHRS_HOME"),
1109 }
1110 f();
1111 match prev {
1112 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1113 None => std::env::remove_var("ZSHRS_HOME"),
1114 }
1115 }
1116
1117 #[test]
1118 fn default_cache_path_honors_zshrs_home() {
1119 let _g = crate::test_util::global_state_lock();
1120 with_zshrs_home(Some("/tmp/zshrs-plugin-cache-home"), || {
1121 assert_eq!(
1122 default_cache_path(),
1123 PathBuf::from("/tmp/zshrs-plugin-cache-home/plugins.db")
1124 );
1125 });
1126 }
1127
1128 #[test]
1129 fn default_cache_path_filename_is_plugins_db() {
1130 let _g = crate::test_util::global_state_lock();
1131 with_zshrs_home(Some("/tmp/zshrs-plugin-fname"), || {
1132 assert_eq!(
1133 default_cache_path().file_name().and_then(|s| s.to_str()),
1134 Some("plugins.db")
1135 );
1136 });
1137 }
1138
1139 #[test]
1140 fn default_cache_path_falls_back_to_home_dot_zshrs() {
1141 let _g = crate::test_util::global_state_lock();
1142 with_zshrs_home(None, || {
1143 let p = default_cache_path();
1144 let s = p.to_string_lossy();
1146 assert!(
1147 s.ends_with(".zshrs/plugins.db"),
1148 "expected .zshrs/plugins.db tail, got: {}",
1149 s
1150 );
1151 });
1152 }
1153
1154 #[test]
1155 fn default_cache_path_uses_distinct_dir_per_zshrs_home_change() {
1156 let _g = crate::test_util::global_state_lock();
1157 with_zshrs_home(Some("/tmp/zshrs-plugin-a"), || {
1158 let a = default_cache_path();
1159 with_zshrs_home(Some("/tmp/zshrs-plugin-b"), || {
1160 let b = default_cache_path();
1161 assert_ne!(a, b, "different ZSHRS_HOME must yield different paths");
1162 });
1163 });
1164 }
1165
1166 #[test]
1171 fn file_mtime_returns_some_for_existing_file() {
1172 let _g = crate::test_util::global_state_lock();
1173 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime.txt");
1174 std::fs::write(&tmp, b"x").unwrap();
1175 let mt = file_mtime(&tmp);
1176 assert!(mt.is_some(), "existing file should produce mtime");
1177 let (secs, _ns) = mt.unwrap();
1179 assert!(secs > 0, "mtime secs must be positive: {}", secs);
1180 let _ = std::fs::remove_file(&tmp);
1181 }
1182
1183 #[test]
1184 fn file_mtime_returns_none_for_missing_path() {
1185 let _g = crate::test_util::global_state_lock();
1186 assert!(file_mtime(Path::new("/nonexistent/zshrs/missing.bin")).is_none());
1187 }
1188
1189 #[test]
1190 fn file_mtime_secs_monotonic_after_rewrite() {
1191 let _g = crate::test_util::global_state_lock();
1192 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime_two.txt");
1193 std::fs::write(&tmp, b"a").unwrap();
1194 let first = file_mtime(&tmp).unwrap();
1195 std::thread::sleep(std::time::Duration::from_millis(1100));
1197 std::fs::write(&tmp, b"b").unwrap();
1198 let second = file_mtime(&tmp).unwrap();
1199 assert!(
1202 second >= first,
1203 "mtime regressed: first={:?} second={:?}",
1204 first,
1205 second
1206 );
1207 let _ = std::fs::remove_file(&tmp);
1208 }
1209
1210 #[test]
1211 fn file_mtime_path_with_special_chars_resolves() {
1212 let _g = crate::test_util::global_state_lock();
1213 let tmp = std::env::temp_dir().join("zshrs plugin cache (space).bin");
1214 std::fs::write(&tmp, b"x").unwrap();
1215 let mt = file_mtime(&tmp);
1216 assert!(mt.is_some(), "spaces in filename must not block resolution");
1217 let _ = std::fs::remove_file(&tmp);
1218 }
1219
1220 #[test]
1221 fn default_cache_path_relative_zshrs_home_taken_verbatim() {
1222 let _g = crate::test_util::global_state_lock();
1223 with_zshrs_home(Some("relative-dir"), || {
1224 assert_eq!(
1225 default_cache_path(),
1226 PathBuf::from("relative-dir/plugins.db")
1227 );
1228 });
1229 }
1230
1231 #[test]
1232 fn default_cache_path_empty_zshrs_home_is_empty_dir_plus_db() {
1233 let _g = crate::test_util::global_state_lock();
1234 with_zshrs_home(Some(""), || {
1235 assert_eq!(default_cache_path(), PathBuf::from("plugins.db"));
1238 });
1239 }
1240}
1241
1242#[cfg(test)]
1243mod classify_tests {
1244 use super::*;
1245 use std::path::Path;
1246
1247 fn classify(p: &str) -> (String, String, String) {
1248 let e = classify_plugin_path(Path::new(p));
1249 (e.manager, e.name, e.root.to_string_lossy().into_owned())
1250 }
1251
1252 #[test]
1253 fn zinit_legacy_dir_user_repo() {
1254 let (m, n, r) = classify(
1255 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions/zsh-autosuggestions.plugin.zsh",
1256 );
1257 assert_eq!(m, "zinit");
1258 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1259 assert_eq!(
1260 r,
1261 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions"
1262 );
1263 }
1264
1265 #[test]
1266 fn zinit_xdg_dir_user_repo() {
1267 let (m, n, _) =
1268 classify("/home/u/.local/share/zinit/plugins/romkatv---powerlevel10k/p10k.zsh");
1269 assert_eq!(m, "zinit");
1270 assert_eq!(n, "romkatv/powerlevel10k");
1271 }
1272
1273 #[test]
1274 fn oh_my_zsh_core_plugin() {
1275 let (m, n, r) = classify("/Users/wizard/.oh-my-zsh/plugins/git/git.plugin.zsh");
1276 assert_eq!(m, "oh-my-zsh");
1277 assert_eq!(n, "git");
1278 assert_eq!(r, "/Users/wizard/.oh-my-zsh/plugins/git");
1279 }
1280
1281 #[test]
1282 fn oh_my_zsh_custom_plugin() {
1283 let (m, n, _) = classify(
1284 "/Users/wizard/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh",
1285 );
1286 assert_eq!(m, "oh-my-zsh");
1287 assert_eq!(n, "zsh-syntax-highlighting");
1288 }
1289
1290 #[test]
1291 fn oh_my_zsh_theme_tagged_with_theme_suffix() {
1292 let (m, n, _) = classify("/Users/wizard/.oh-my-zsh/themes/agnoster.zsh-theme");
1293 assert_eq!(m, "oh-my-zsh");
1294 assert_eq!(n, "agnoster.zsh-theme.theme");
1295 }
1296
1297 #[test]
1298 fn prezto_module() {
1299 let (m, n, _) = classify("/Users/wizard/.zprezto/modules/git/init.zsh");
1300 assert_eq!(m, "prezto");
1301 assert_eq!(n, "git");
1302 }
1303
1304 #[test]
1305 fn antidote_repo() {
1306 let (m, n, _) = classify(
1307 "/Users/wizard/.cache/antidote/zsh-users/zsh-autosuggestions/zsh-autosuggestions.zsh",
1308 );
1309 assert_eq!(m, "antidote");
1310 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1311 }
1312
1313 #[test]
1314 fn antigen_bundle() {
1315 let (m, n, _) = classify(
1316 "/Users/wizard/.antigen/bundles/zsh-users/zsh-completions/zsh-completions.plugin.zsh",
1317 );
1318 assert_eq!(m, "antigen");
1319 assert_eq!(n, "zsh-users/zsh-completions");
1320 }
1321
1322 #[test]
1323 fn zplug_repo() {
1324 let (m, n, _) = classify(
1325 "/Users/wizard/.zplug/repos/zsh-users/zsh-history-substring-search/zsh-history-substring-search.zsh",
1326 );
1327 assert_eq!(m, "zplug");
1328 assert_eq!(n, "zsh-users/zsh-history-substring-search");
1329 }
1330
1331 #[test]
1332 fn zsh_more_completions_groups_into_one() {
1333 let (m, n, _) =
1334 classify("/Users/wizard/forkedRepos/zsh-more-completions/src/_some_long_completion");
1335 assert_eq!(m, "zsh-more-completions");
1336 assert_eq!(n, "zsh-more-completions");
1337 }
1338
1339 #[test]
1340 fn zpwr_root_recognized() {
1341 let (m, n, _) = classify("/Users/wizard/.zpwr/local/.aliases.sh");
1342 assert_eq!(m, "zpwr");
1343 assert_eq!(n, "zpwr");
1344 }
1345
1346 #[test]
1347 fn loose_plugin_uses_parent_dir_as_name() {
1348 let (m, n, r) = classify("/opt/local/share/zsh/something/init.zsh");
1349 assert_eq!(m, "loose");
1350 assert_eq!(n, "something");
1351 assert_eq!(r, "/opt/local/share/zsh/something");
1352 }
1353}
1354
1355impl crate::ported::vm_helper::ShellExecutor {
1363 pub(crate) fn snapshot_state(&self) -> PluginSnapshot {
1365 PluginSnapshot {
1366 functions: self.function_names().into_iter().collect(),
1367 aliases: self.alias_entries().into_iter().map(|(k, _)| k).collect(),
1368 global_aliases: self
1369 .global_alias_entries()
1370 .into_iter()
1371 .map(|(k, _)| k)
1372 .collect(),
1373 suffix_aliases: self
1374 .suffix_alias_entries()
1375 .into_iter()
1376 .map(|(k, _)| k)
1377 .collect(),
1378 variables: if let Ok(tab) = crate::ported::params::paramtab().read() {
1379 tab.iter()
1380 .filter(|(_, pm)| pm.u_arr.is_none())
1381 .map(|(k, pm)| (k.clone(), pm.u_str.clone().unwrap_or_default()))
1382 .collect()
1383 } else {
1384 std::collections::HashMap::new()
1385 },
1386 arrays: if let Ok(tab) = crate::ported::params::paramtab().read() {
1387 tab.iter()
1388 .filter(|(_, pm)| pm.u_arr.is_some())
1389 .map(|(k, _)| k.clone())
1390 .collect()
1391 } else {
1392 std::collections::HashSet::new()
1393 },
1394 assoc_arrays: if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1395 m.keys().cloned().collect()
1396 } else {
1397 std::collections::HashSet::new()
1398 },
1399 fpath: self.fpath.clone(),
1400 options: crate::ported::options::opt_state_snapshot(),
1401 hooks: {
1402 let names = [
1404 "chpwd",
1405 "precmd",
1406 "preexec",
1407 "periodic",
1408 "zshexit",
1409 "zshaddhistory",
1410 ];
1411 let mut m = std::collections::HashMap::new();
1412 for h in &names {
1413 let arr_name = format!("{}_functions", h);
1414 if let Some(arr) = self.array(&arr_name) {
1415 if !arr.is_empty() {
1416 m.insert(h.to_string(), arr);
1417 }
1418 }
1419 }
1420 m
1421 },
1422 autoloads: {
1423 crate::ported::hashtable::shfunctab_lock()
1426 .read()
1427 .ok()
1428 .map(|t| {
1429 t.iter()
1430 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1431 .map(|(name, _)| name.clone())
1432 .collect()
1433 })
1434 .unwrap_or_default()
1435 },
1436 }
1437 }
1438 pub(crate) fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
1440 let mut delta = PluginDelta::default();
1441
1442 let mut fn_keys: Vec<&String> = self.function_source.keys().collect();
1451 fn_keys.sort();
1452 for name in fn_keys {
1453 if !snap.functions.contains(name) {
1454 let source = self.function_source.get(name).unwrap();
1455 delta
1456 .functions
1457 .push((name.clone(), source.as_bytes().to_vec()));
1458 }
1459 }
1460
1461 let push_alias = |delta: &mut PluginDelta,
1462 entries: Vec<(String, String)>,
1463 snap_set: &std::collections::HashSet<String>,
1464 kind: AliasKind| {
1465 let mut entries = entries;
1466 entries.sort_by(|a, b| a.0.cmp(&b.0));
1467 for (name, value) in entries {
1468 if !snap_set.contains(&name) {
1469 delta.aliases.push((name, value, kind));
1470 }
1471 }
1472 };
1473 push_alias(
1474 &mut delta,
1475 self.alias_entries(),
1476 &snap.aliases,
1477 AliasKind::Regular,
1478 );
1479 push_alias(
1480 &mut delta,
1481 self.global_alias_entries(),
1482 &snap.global_aliases,
1483 AliasKind::Global,
1484 );
1485 push_alias(
1486 &mut delta,
1487 self.suffix_alias_entries(),
1488 &snap.suffix_aliases,
1489 AliasKind::Suffix,
1490 );
1491
1492 const NON_REPLAYABLE_VARS: &[&str] = &[
1507 "0",
1508 "_",
1509 "?",
1510 "$",
1511 "!",
1512 "PPID",
1513 "RANDOM",
1514 "SECONDS",
1515 "EPOCHSECONDS",
1516 "EPOCHREALTIME",
1517 "LINENO",
1518 "OLDPWD",
1519 "PWD",
1520 "STATUS",
1521 "OPTIND",
1522 "OPTARG",
1523 "IFS",
1524 "FUNCNAME",
1525 "BASHPID",
1526 "BASH_LINENO",
1527 "BASH_SOURCE",
1528 "ZSH_ARGZERO",
1529 "ZSH_EVAL_CONTEXT",
1530 "ZSH_SUBSHELL",
1531 "HISTCMD",
1532 "MATCH",
1533 "MBEGIN",
1534 "MEND",
1535 ];
1536 let mut var_keys: Vec<String> = if let Ok(tab) = crate::ported::params::paramtab().read() {
1537 tab.iter()
1538 .filter(|(_, pm)| pm.u_arr.is_none())
1539 .map(|(k, _)| k.clone())
1540 .collect()
1541 } else {
1542 Vec::new()
1543 };
1544 var_keys.sort();
1545 for name in &var_keys {
1546 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1547 continue;
1548 }
1549 let value = crate::ported::params::getsparam(name).unwrap_or_default();
1550 match snap.variables.get(name) {
1551 Some(old) if old == &value => {} _ => {
1553 if env::var(name).ok().as_ref() == Some(&value) {
1555 delta.exports.push((name.clone(), value.clone()));
1556 } else {
1557 delta.variables.push((name.clone(), value.clone()));
1558 }
1559 }
1560 }
1561 }
1562
1563 let arr_entries: Vec<(String, Vec<String>)> =
1565 if let Ok(tab) = crate::ported::params::paramtab().read() {
1566 let mut v: Vec<(String, Vec<String>)> = tab
1567 .iter()
1568 .filter_map(|(k, pm)| pm.u_arr.clone().map(|a| (k.clone(), a)))
1569 .collect();
1570 v.sort_by(|a, b| a.0.cmp(&b.0));
1571 v
1572 } else {
1573 Vec::new()
1574 };
1575 for (name, values) in arr_entries {
1576 if !snap.arrays.contains(&name) {
1577 delta.arrays.push((name, values));
1578 }
1579 }
1580
1581 let assoc_entries: Vec<(String, indexmap::IndexMap<String, String>)> =
1588 if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1589 let mut v: Vec<(String, indexmap::IndexMap<String, String>)> =
1590 m.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1591 v.sort_by(|a, b| a.0.cmp(&b.0));
1592 v
1593 } else {
1594 Vec::new()
1595 };
1596 for (name, map) in assoc_entries {
1597 if !snap.assoc_arrays.contains(&name) {
1598 let plain: HashMap<String, String> =
1604 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1605 delta.assoc_arrays.push((name, plain));
1606 }
1607 }
1608
1609 for p in &self.fpath {
1611 if !snap.fpath.contains(p) {
1612 delta.fpath_additions.push(p.to_string_lossy().to_string());
1613 }
1614 }
1615
1616 let current = crate::ported::options::opt_state_snapshot();
1618 let mut opt_keys: Vec<&String> = current.keys().collect();
1619 opt_keys.sort();
1620 for name in opt_keys {
1621 let value = current.get(name).unwrap();
1622 match snap.options.get(name) {
1623 Some(old) if old == value => {}
1624 _ => delta.options_changed.push((name.clone(), *value)),
1625 }
1626 }
1627
1628 let names = [
1630 "chpwd",
1631 "precmd",
1632 "preexec",
1633 "periodic",
1634 "zshexit",
1635 "zshaddhistory",
1636 ];
1637 let mut hook_names: Vec<&&str> = names.iter().collect();
1638 hook_names.sort();
1639 for &h in hook_names {
1640 let arr_name = format!("{}_functions", h);
1641 let funcs = self.array(&arr_name).unwrap_or_default();
1642 let old_funcs = snap.hooks.get(h);
1643 for f in &funcs {
1644 let is_new = old_funcs.is_none_or(|old| !old.contains(f));
1645 if is_new {
1646 delta.hooks.push((h.to_string(), f.clone()));
1647 }
1648 }
1649 }
1650
1651 let current_autoloads: Vec<String> = crate::ported::hashtable::shfunctab_lock()
1653 .read()
1654 .ok()
1655 .map(|t| {
1656 t.iter()
1657 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1658 .map(|(name, _)| name.clone())
1659 .collect()
1660 })
1661 .unwrap_or_default();
1662 let mut autoload_keys: Vec<&String> = current_autoloads.iter().collect();
1663 autoload_keys.sort();
1664 for name in autoload_keys {
1665 if !snap.autoloads.contains(name) {
1666 delta.autoloads.push((name.clone(), String::new()));
1669 }
1670 }
1671
1672 delta
1673 }
1674 pub(crate) fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
1676 for (name, value, kind) in &delta.aliases {
1678 match kind {
1679 AliasKind::Regular => {
1680 self.set_alias(name.clone(), value.clone());
1681 }
1682 AliasKind::Global => {
1683 self.set_global_alias(name.clone(), value.clone());
1684 }
1685 AliasKind::Suffix => {
1686 self.set_suffix_alias(name.clone(), value.clone());
1687 }
1688 }
1689 }
1690
1691 const NON_REPLAYABLE_VARS: &[&str] = &[
1698 "0",
1699 "_",
1700 "?",
1701 "$",
1702 "!",
1703 "PPID",
1704 "RANDOM",
1705 "SECONDS",
1706 "EPOCHSECONDS",
1707 "EPOCHREALTIME",
1708 "LINENO",
1709 "OLDPWD",
1710 "PWD",
1711 "STATUS",
1712 "OPTIND",
1713 "OPTARG",
1714 "IFS",
1715 "FUNCNAME",
1716 "BASHPID",
1717 "BASH_LINENO",
1718 "BASH_SOURCE",
1719 "ZSH_ARGZERO",
1720 "ZSH_EVAL_CONTEXT",
1721 "ZSH_SUBSHELL",
1722 "HISTCMD",
1723 "MATCH",
1724 "MBEGIN",
1725 "MEND",
1726 ];
1727 for (name, value) in &delta.variables {
1728 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1729 continue;
1730 }
1731 self.set_scalar(name.clone(), value.clone());
1732 }
1733
1734 for (name, value) in &delta.exports {
1736 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1737 continue;
1738 }
1739 self.set_scalar(name.clone(), value.clone());
1740 env::set_var(name, value);
1741 }
1742
1743 for (name, values) in &delta.arrays {
1745 self.set_array(name.clone(), values.clone());
1746 }
1747
1748 for (name, map) in &delta.assoc_arrays {
1753 let mut idx_map: indexmap::IndexMap<String, String> =
1758 indexmap::IndexMap::with_capacity(map.len());
1759 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1764 entries.sort_by(|a, b| a.0.cmp(b.0));
1765 for (k, v) in entries {
1766 idx_map.insert(k.clone(), v.clone());
1767 }
1768 self.set_assoc(name.clone(), idx_map);
1769 }
1770
1771 for p in &delta.fpath_additions {
1773 let pb = PathBuf::from(p);
1774 if !self.fpath.contains(&pb) {
1775 self.fpath.push(pb);
1776 }
1777 }
1778
1779 if !delta.completions.is_empty() {
1781 let mut comps = self.assoc("_comps").unwrap_or_default();
1782 for (cmd, func) in &delta.completions {
1783 comps.insert(cmd.clone(), func.clone());
1784 }
1785 self.set_assoc("_comps".to_string(), comps);
1786 }
1787
1788 for (name, enabled) in &delta.options_changed {
1790 crate::ported::options::opt_state_set(name, *enabled);
1791 }
1792
1793 for (hook, func) in &delta.hooks {
1799 let array_name = format!("{}_functions", hook);
1800 let mut arr = self.array(&array_name).unwrap_or_default();
1801 if !arr.iter().any(|f| f == func) {
1802 arr.push(func.clone());
1803 crate::ported::params::setaparam(&array_name, arr);
1804 }
1805 }
1806
1807 for (name, bytes) in &delta.functions {
1811 let Ok(source) = std::str::from_utf8(bytes) else {
1812 continue;
1813 };
1814 let saved_errflag = errflag.load(Ordering::Relaxed);
1816 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
1817 crate::ported::parse::parse_init(source);
1818 let program = crate::ported::parse::parse();
1819 let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
1820 errflag.store(saved_errflag, Ordering::Relaxed);
1821 if parse_failed || program.lists.is_empty() {
1822 continue;
1823 }
1824 let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
1825 self.functions_compiled.insert(name.clone(), chunk);
1826 self.function_source
1827 .insert(name.clone(), source.to_string());
1828 }
1829 }
1830}
1831