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_identity() -> Option<(i64, u64)> {
60 static BIN_ID: OnceLock<Option<(i64, u64)>> = OnceLock::new();
61 *BIN_ID.get_or_init(|| {
62 let exe = std::env::current_exe().ok()?;
63 let meta = std::fs::metadata(&exe).ok()?;
64 Some((meta.mtime(), meta.len()))
65 })
66}
67
68#[derive(Debug, Clone, Default)]
77pub struct PluginDelta {
78 pub functions: Vec<(String, Vec<u8>)>, pub aliases: Vec<(String, String, AliasKind)>, pub global_aliases: Vec<(String, String)>,
82 pub suffix_aliases: Vec<(String, String)>,
84 pub variables: Vec<(String, String)>,
86 pub exports: Vec<(String, String)>, pub arrays: Vec<(String, Vec<String>)>,
89 pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
91 pub completions: Vec<(String, String)>, pub fpath_additions: Vec<String>,
94 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)>, }
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum AliasKind {
103 Regular,
105 Global,
107 Suffix,
109}
110
111impl AliasKind {
112 fn as_i32(self) -> i32 {
113 match self {
114 AliasKind::Regular => 0,
115 AliasKind::Global => 1,
116 AliasKind::Suffix => 2,
117 }
118 }
119 fn from_i32(v: i32) -> Self {
120 match v {
121 1 => AliasKind::Global,
122 2 => AliasKind::Suffix,
123 _ => AliasKind::Regular,
124 }
125 }
126}
127
128pub struct PluginCache {
130 conn: Connection,
132}
133
134impl PluginCache {
135 pub fn open(path: &Path) -> rusqlite::Result<Self> {
137 let _lowfd = crate::lowfd::LowFdGuard::new();
148 let conn = Connection::open(path)?;
149 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
150 crate::lowfd::register_internal_fds(); let cache = Self { conn };
152 cache.init_schema()?;
153 Ok(cache)
154 }
155
156 fn init_schema(&self) -> rusqlite::Result<()> {
157 self.conn.execute_batch(
158 r#"
159 CREATE TABLE IF NOT EXISTS plugins (
160 id INTEGER PRIMARY KEY,
161 path TEXT NOT NULL UNIQUE,
162 mtime_secs INTEGER NOT NULL,
163 mtime_nsecs INTEGER NOT NULL,
164 source_time_ms INTEGER NOT NULL,
165 cached_at INTEGER NOT NULL,
166 binary_mtime INTEGER NOT NULL DEFAULT 0,
167 binary_len INTEGER NOT NULL DEFAULT 0
168 );
169
170 CREATE TABLE IF NOT EXISTS plugin_functions (
171 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
172 name TEXT NOT NULL,
173 body BLOB NOT NULL
174 );
175
176 CREATE TABLE IF NOT EXISTS plugin_aliases (
177 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
178 name TEXT NOT NULL,
179 value TEXT NOT NULL,
180 kind INTEGER NOT NULL DEFAULT 0
181 );
182
183 CREATE TABLE IF NOT EXISTS plugin_variables (
184 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
185 name TEXT NOT NULL,
186 value TEXT NOT NULL,
187 is_export INTEGER NOT NULL DEFAULT 0
188 );
189
190 CREATE TABLE IF NOT EXISTS plugin_arrays (
191 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
192 name TEXT NOT NULL,
193 value_json TEXT NOT NULL
194 );
195
196 -- Associative-array deltas (e.g. ZINIT[BIN_DIR]=...). Stored
197 -- as JSON {key: value} so insertion order isn't load-bearing
198 -- (matches HashMap semantics on the Rust side). Direct
199 -- analogue of plugin_arrays for assoc shape.
200 CREATE TABLE IF NOT EXISTS plugin_assoc_arrays (
201 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
202 name TEXT NOT NULL,
203 value_json TEXT NOT NULL
204 );
205
206 CREATE TABLE IF NOT EXISTS plugin_completions (
207 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
208 command TEXT NOT NULL,
209 function TEXT NOT NULL
210 );
211
212 CREATE TABLE IF NOT EXISTS plugin_fpath (
213 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
214 path TEXT NOT NULL
215 );
216
217 CREATE TABLE IF NOT EXISTS plugin_hooks (
218 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
219 hook TEXT NOT NULL,
220 function TEXT NOT NULL
221 );
222
223 CREATE TABLE IF NOT EXISTS plugin_bindkeys (
224 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
225 keyseq TEXT NOT NULL,
226 widget TEXT NOT NULL,
227 keymap TEXT NOT NULL DEFAULT 'main'
228 );
229
230 CREATE TABLE IF NOT EXISTS plugin_zstyles (
231 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
232 pattern TEXT NOT NULL,
233 style TEXT NOT NULL,
234 value TEXT NOT NULL
235 );
236
237 CREATE TABLE IF NOT EXISTS plugin_options (
238 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
239 name TEXT NOT NULL,
240 enabled INTEGER NOT NULL
241 );
242
243 CREATE TABLE IF NOT EXISTS plugin_autoloads (
244 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
245 function TEXT NOT NULL,
246 flags TEXT NOT NULL DEFAULT ''
247 );
248
249 -- compaudit cache: security audit results per fpath directory
250 CREATE TABLE IF NOT EXISTS compaudit_cache (
251 id INTEGER PRIMARY KEY,
252 path TEXT NOT NULL UNIQUE,
253 mtime_secs INTEGER NOT NULL,
254 mtime_nsecs INTEGER NOT NULL,
255 uid INTEGER NOT NULL,
256 mode INTEGER NOT NULL,
257 is_secure INTEGER NOT NULL,
258 checked_at INTEGER NOT NULL
259 );
260
261 CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
262 CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
263
264 -- Migration: legacy script_bytecode table (bytecode now lives in
265 -- the rkyv shard at ~/.zshrs/scripts.rkyv). Drop on open so
266 -- existing DBs reclaim the space and don't carry stale bytecode.
267 DROP INDEX IF EXISTS idx_script_bytecode_path;
268 DROP TABLE IF EXISTS script_bytecode;
269 "#,
270 )?;
271 let _ = self.conn.execute(
282 "ALTER TABLE plugins ADD COLUMN binary_mtime INTEGER NOT NULL DEFAULT 0",
283 [],
284 );
285 let _ = self.conn.execute(
291 "ALTER TABLE plugins ADD COLUMN binary_len INTEGER NOT NULL DEFAULT 0",
292 [],
293 );
294 Ok(())
295 }
296
297 pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
307 let row: Option<(i64, i64, i64)> = self
308 .conn
309 .query_row(
310 "SELECT id, binary_mtime, binary_len FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
311 params![path, mtime_secs, mtime_nsecs],
312 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
313 )
314 .ok();
315 let (id, cached_bin_mtime, cached_bin_len) = row?;
316 let (bin_mtime, bin_len) = current_binary_identity()?;
326 if cached_bin_mtime != bin_mtime || cached_bin_len != bin_len as i64 {
327 return None;
328 }
329 Some(id)
330 }
331
332 pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
334 let mut delta = PluginDelta::default();
335
336 let mut stmt = self
338 .conn
339 .prepare("SELECT name, body FROM plugin_functions WHERE plugin_id = ?1")?;
340 let rows = stmt.query_map(params![plugin_id], |row| {
341 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
342 })?;
343 for r in rows {
344 delta.functions.push(r?);
345 }
346
347 let mut stmt = self
349 .conn
350 .prepare("SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1")?;
351 let rows = stmt.query_map(params![plugin_id], |row| {
352 Ok((
353 row.get::<_, String>(0)?,
354 row.get::<_, String>(1)?,
355 AliasKind::from_i32(row.get::<_, i32>(2)?),
356 ))
357 })?;
358 for r in rows {
359 delta.aliases.push(r?);
360 }
361
362 let mut stmt = self
364 .conn
365 .prepare("SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1")?;
366 let rows = stmt.query_map(params![plugin_id], |row| {
367 Ok((
368 row.get::<_, String>(0)?,
369 row.get::<_, String>(1)?,
370 row.get::<_, bool>(2)?,
371 ))
372 })?;
373 for r in rows {
374 let (name, value, is_export) = r?;
375 if is_export {
376 delta.exports.push((name, value));
377 } else {
378 delta.variables.push((name, value));
379 }
380 }
381
382 let mut stmt = self
384 .conn
385 .prepare("SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1")?;
386 let rows = stmt.query_map(params![plugin_id], |row| {
387 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
388 })?;
389 for r in rows {
390 let (name, json) = r?;
391 let vals: Vec<String> = json
393 .trim_matches(|c| c == '[' || c == ']')
394 .split(',')
395 .map(|s| s.trim().trim_matches('"').to_string())
396 .filter(|s| !s.is_empty())
397 .collect();
398 delta.arrays.push((name, vals));
399 }
400
401 let mut stmt = self
405 .conn
406 .prepare("SELECT name, value_json FROM plugin_assoc_arrays WHERE plugin_id = ?1")?;
407 let rows = stmt.query_map(params![plugin_id], |row| {
408 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
409 })?;
410 for r in rows {
411 let (name, json) = r?;
412 let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or_default();
413 delta.assoc_arrays.push((name, map));
414 }
415
416 let mut stmt = self
418 .conn
419 .prepare("SELECT command, function FROM plugin_completions WHERE plugin_id = ?1")?;
420 let rows = stmt.query_map(params![plugin_id], |row| {
421 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
422 })?;
423 for r in rows {
424 delta.completions.push(r?);
425 }
426
427 let mut stmt = self
429 .conn
430 .prepare("SELECT path FROM plugin_fpath WHERE plugin_id = ?1")?;
431 let rows = stmt.query_map(params![plugin_id], |row| row.get::<_, String>(0))?;
432 for r in rows {
433 delta.fpath_additions.push(r?);
434 }
435
436 let mut stmt = self
438 .conn
439 .prepare("SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1")?;
440 let rows = stmt.query_map(params![plugin_id], |row| {
441 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
442 })?;
443 for r in rows {
444 delta.hooks.push(r?);
445 }
446
447 let mut stmt = self
449 .conn
450 .prepare("SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1")?;
451 let rows = stmt.query_map(params![plugin_id], |row| {
452 Ok((
453 row.get::<_, String>(0)?,
454 row.get::<_, String>(1)?,
455 row.get::<_, String>(2)?,
456 ))
457 })?;
458 for r in rows {
459 delta.bindkeys.push(r?);
460 }
461
462 let mut stmt = self
464 .conn
465 .prepare("SELECT pattern, style, value FROM plugin_zstyles WHERE plugin_id = ?1")?;
466 let rows = stmt.query_map(params![plugin_id], |row| {
467 Ok((
468 row.get::<_, String>(0)?,
469 row.get::<_, String>(1)?,
470 row.get::<_, String>(2)?,
471 ))
472 })?;
473 for r in rows {
474 delta.zstyles.push(r?);
475 }
476
477 let mut stmt = self
479 .conn
480 .prepare("SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1")?;
481 let rows = stmt.query_map(params![plugin_id], |row| {
482 Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
483 })?;
484 for r in rows {
485 delta.options_changed.push(r?);
486 }
487
488 let mut stmt = self
490 .conn
491 .prepare("SELECT function, flags FROM plugin_autoloads WHERE plugin_id = ?1")?;
492 let rows = stmt.query_map(params![plugin_id], |row| {
493 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
494 })?;
495 for r in rows {
496 delta.autoloads.push(r?);
497 }
498
499 Ok(delta)
500 }
501
502 pub fn store(
504 &self,
505 path: &str,
506 mtime_secs: i64,
507 mtime_nsecs: i64,
508 source_time_ms: u64,
509 delta: &PluginDelta,
510 ) -> rusqlite::Result<()> {
511 let now = std::time::SystemTime::now()
512 .duration_since(std::time::UNIX_EPOCH)
513 .map(|d| d.as_secs() as i64)
514 .unwrap_or(0);
515
516 self.conn
518 .execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
519
520 let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
521 self.conn.execute(
522 "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at, binary_mtime, binary_len) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
523 params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now, bin_mtime, bin_len as i64],
524 )?;
525 let plugin_id = self.conn.last_insert_rowid();
526
527 for (name, body) in &delta.functions {
529 self.conn.execute(
530 "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
531 params![plugin_id, name, body],
532 )?;
533 }
534
535 for (name, value, kind) in &delta.aliases {
537 self.conn.execute(
538 "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
539 params![plugin_id, name, value, kind.as_i32()],
540 )?;
541 }
542
543 for (name, value) in &delta.variables {
545 self.conn.execute(
546 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
547 params![plugin_id, name, value],
548 )?;
549 }
550 for (name, value) in &delta.exports {
551 self.conn.execute(
552 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
553 params![plugin_id, name, value],
554 )?;
555 }
556
557 for (name, vals) in &delta.arrays {
559 let json = format!(
560 "[{}]",
561 vals.iter()
562 .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
563 .collect::<Vec<_>>()
564 .join(",")
565 );
566 self.conn.execute(
567 "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
568 params![plugin_id, name, json],
569 )?;
570 }
571
572 for (name, map) in &delta.assoc_arrays {
578 let json = serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string());
579 self.conn.execute(
580 "INSERT INTO plugin_assoc_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
581 params![plugin_id, name, json],
582 )?;
583 }
584
585 for (cmd, func) in &delta.completions {
587 self.conn.execute(
588 "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
589 params![plugin_id, cmd, func],
590 )?;
591 }
592
593 for p in &delta.fpath_additions {
595 self.conn.execute(
596 "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
597 params![plugin_id, p],
598 )?;
599 }
600
601 for (hook, func) in &delta.hooks {
603 self.conn.execute(
604 "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
605 params![plugin_id, hook, func],
606 )?;
607 }
608
609 for (keyseq, widget, keymap) in &delta.bindkeys {
611 self.conn.execute(
612 "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
613 params![plugin_id, keyseq, widget, keymap],
614 )?;
615 }
616
617 for (pattern, style, value) in &delta.zstyles {
619 self.conn.execute(
620 "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
621 params![plugin_id, pattern, style, value],
622 )?;
623 }
624
625 for (name, enabled) in &delta.options_changed {
627 self.conn.execute(
628 "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
629 params![plugin_id, name, *enabled],
630 )?;
631 }
632
633 for (func, flags) in &delta.autoloads {
635 self.conn.execute(
636 "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
637 params![plugin_id, func, flags],
638 )?;
639 }
640
641 Ok(())
642 }
643
644 pub fn stats(&self) -> (i64, i64) {
646 let plugins: i64 = self
647 .conn
648 .query_row("SELECT COUNT(*) FROM plugins", [], |r| r.get(0))
649 .unwrap_or(0);
650 let functions: i64 = self
651 .conn
652 .query_row("SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0))
653 .unwrap_or(0);
654 (plugins, functions)
655 }
656
657 pub fn count_stale(&self) -> usize {
659 let mut stmt = match self
660 .conn
661 .prepare("SELECT path, mtime_secs, mtime_nsecs FROM plugins")
662 {
663 Ok(s) => s,
664 Err(_) => return 0,
665 };
666 let rows = match stmt.query_map([], |row| {
667 Ok((
668 row.get::<_, String>(0)?,
669 row.get::<_, i64>(1)?,
670 row.get::<_, i64>(2)?,
671 ))
672 }) {
673 Ok(r) => r,
674 Err(_) => return 0,
675 };
676 let mut count = 0;
677 for (path, cached_s, cached_ns) in rows.flatten() {
678 match file_mtime(std::path::Path::new(&path)) {
679 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
680 None => count += 1, _ => {}
682 }
683 }
684 count
685 }
686
687 pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
694 self.conn.query_row(
695 "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
696 params![dir, mtime_secs, mtime_nsecs],
697 |row| row.get::<_, bool>(0),
698 ).ok()
699 }
700
701 pub fn store_compaudit(
703 &self,
704 dir: &str,
705 mtime_secs: i64,
706 mtime_nsecs: i64,
707 uid: u32,
708 mode: u32,
709 is_secure: bool,
710 ) -> rusqlite::Result<()> {
711 let now = std::time::SystemTime::now()
712 .duration_since(std::time::UNIX_EPOCH)
713 .map(|d| d.as_secs() as i64)
714 .unwrap_or(0);
715
716 self.conn.execute(
717 "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)",
718 params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
719 )?;
720 Ok(())
721 }
722
723 pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
726 let euid = unsafe { libc::geteuid() };
727 let mut insecure = Vec::new();
728
729 for dir in fpath {
730 let dir_str = dir.to_string_lossy().to_string();
731 let meta = match std::fs::metadata(dir) {
732 Ok(m) => m,
733 Err(_) => continue, };
735 let mt_s = meta.mtime();
736 let mt_ns = meta.mtime_nsec();
737
738 if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
740 if !is_secure {
741 insecure.push(dir_str);
742 }
743 continue;
744 }
745
746 let mode = meta.mode();
748 let uid = meta.uid();
749 let is_secure = Self::check_dir_security(&meta, euid);
750
751 let parent_secure = dir
753 .parent()
754 .and_then(|p| std::fs::metadata(p).ok())
755 .map(|pm| Self::check_dir_security(&pm, euid))
756 .unwrap_or(true);
757
758 let secure = is_secure && parent_secure;
759
760 let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
762
763 if !secure {
764 insecure.push(dir_str);
765 }
766 }
767
768 if insecure.is_empty() {
769 tracing::debug!(
770 dirs = fpath.len(),
771 "compaudit: all directories secure (cached)"
772 );
773 } else {
774 tracing::warn!(
775 insecure_count = insecure.len(),
776 dirs = fpath.len(),
777 "compaudit: insecure directories found"
778 );
779 }
780
781 insecure
782 }
783
784 pub fn list_plugin_paths(&self) -> Vec<(String, i64)> {
789 let mut stmt = match self
790 .conn
791 .prepare("SELECT path, mtime_secs FROM plugins ORDER BY id")
792 {
793 Ok(s) => s,
794 Err(_) => return Vec::new(),
795 };
796 let rows = match stmt.query_map([], |row| {
797 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
798 }) {
799 Ok(r) => r,
800 Err(_) => return Vec::new(),
801 };
802 rows.flatten().collect()
803 }
804
805 fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
808 let mode = meta.mode();
809 let uid = meta.uid();
810
811 if uid == 0 || uid == euid {
813 return true;
814 }
815
816 let group_writable = mode & 0o020 != 0;
818 let world_writable = mode & 0o002 != 0;
819
820 !group_writable && !world_writable
821 }
822}
823
824pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
826 let meta = std::fs::metadata(path).ok()?;
827 Some((meta.mtime(), meta.mtime_nsec()))
828}
829
830#[derive(Debug, Clone)]
836pub struct PluginEntry {
837 pub manager: String,
838 pub name: String,
839 pub root: PathBuf,
840}
841
842fn classify_plugin_path(path: &Path) -> PluginEntry {
846 let s = path.to_string_lossy();
847
848 for marker in ["/.zinit/plugins/", "/zinit/plugins/"] {
851 if let Some(start) = s.find(marker) {
852 let after = &s[start + marker.len()..];
853 if let Some(end) = after.find('/') {
854 let dir = &after[..end];
855 let name = dir.replacen("---", "/", 1);
856 let root: PathBuf = s[..start + marker.len() + end].into();
857 return PluginEntry {
858 manager: "zinit".into(),
859 name,
860 root,
861 };
862 }
863 }
864 }
865
866 for (marker, kind) in [
868 ("/.oh-my-zsh/custom/plugins/", "plugin"),
869 ("/.oh-my-zsh/plugins/", "plugin"),
870 ("/.oh-my-zsh/custom/themes/", "theme"),
871 ("/.oh-my-zsh/themes/", "theme"),
872 ] {
873 if let Some(start) = s.find(marker) {
874 let after = &s[start + marker.len()..];
875 let end = after.find('/').unwrap_or(after.len());
876 let leaf = &after[..end];
877 let name = if kind == "theme" {
878 format!("{}.theme", leaf)
879 } else {
880 leaf.to_string()
881 };
882 let root: PathBuf = s[..start + marker.len() + end].into();
883 return PluginEntry {
884 manager: "oh-my-zsh".into(),
885 name,
886 root,
887 };
888 }
889 }
890
891 if let Some(start) = s.find("/.zprezto/modules/") {
893 let after = &s[start + "/.zprezto/modules/".len()..];
894 let end = after.find('/').unwrap_or(after.len());
895 let name = after[..end].to_string();
896 let root: PathBuf = s[..start + "/.zprezto/modules/".len() + end].into();
897 return PluginEntry {
898 manager: "prezto".into(),
899 name,
900 root,
901 };
902 }
903
904 for marker in ["/antidote/repos/", "/.cache/antidote/"] {
907 if let Some(start) = s.find(marker) {
908 let after = &s[start + marker.len()..];
909 let mut split = after.splitn(3, '/');
911 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
912 let name = format!("{}/{}", user, repo);
913 let root: PathBuf =
914 format!("{}{}/{}", &s[..start + marker.len()], user, repo).into();
915 return PluginEntry {
916 manager: "antidote".into(),
917 name,
918 root,
919 };
920 }
921 }
922 }
923
924 if let Some(start) = s.find("/.antigen/bundles/") {
926 let after = &s[start + "/.antigen/bundles/".len()..];
927 let mut split = after.splitn(3, '/');
928 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
929 let name = format!("{}/{}", user, repo);
930 let root: PathBuf = format!(
931 "{}/{}/{}",
932 &s[..start + "/.antigen/bundles".len()],
933 user,
934 repo
935 )
936 .into();
937 return PluginEntry {
938 manager: "antigen".into(),
939 name,
940 root,
941 };
942 }
943 }
944
945 if let Some(start) = s.find("/.zplug/repos/") {
947 let after = &s[start + "/.zplug/repos/".len()..];
948 let mut split = after.splitn(3, '/');
949 if let (Some(user), Some(repo), _) = (split.next(), split.next(), split.next()) {
950 let name = format!("{}/{}", user, repo);
951 let root: PathBuf =
952 format!("{}/{}/{}", &s[..start + "/.zplug/repos".len()], user, repo).into();
953 return PluginEntry {
954 manager: "zplug".into(),
955 name,
956 root,
957 };
958 }
959 }
960
961 if let Some(start) = s.find("/zsh-more-completions/") {
964 let root: PathBuf = s[..start + "/zsh-more-completions".len()].into();
965 return PluginEntry {
966 manager: "zsh-more-completions".into(),
967 name: "zsh-more-completions".into(),
968 root,
969 };
970 }
971
972 for marker in ["/.zpwr/", "/zpwr/"] {
974 if let Some(start) = s.find(marker) {
975 let root: PathBuf = s[..start + marker.len() - 1].into();
976 return PluginEntry {
977 manager: "zpwr".into(),
978 name: "zpwr".into(),
979 root,
980 };
981 }
982 }
983
984 let root = path
986 .parent()
987 .map(PathBuf::from)
988 .unwrap_or_else(|| path.into());
989 let name = root
990 .file_name()
991 .map(|n| n.to_string_lossy().into_owned())
992 .unwrap_or_else(|| "(loose)".into());
993 PluginEntry {
994 manager: "loose".into(),
995 name,
996 root,
997 }
998}
999
1000pub fn list_plugins(cache_path: &Path) -> Vec<PluginEntry> {
1004 let cache = match PluginCache::open(cache_path) {
1005 Ok(c) => c,
1006 Err(_) => return Vec::new(),
1007 };
1008 let mut seen: std::collections::BTreeMap<(String, String, PathBuf), PluginEntry> =
1009 std::collections::BTreeMap::new();
1010 for (path, _mtime) in cache.list_plugin_paths() {
1011 let entry = classify_plugin_path(Path::new(&path));
1012 seen.entry((
1013 entry.manager.clone(),
1014 entry.name.clone(),
1015 entry.root.clone(),
1016 ))
1017 .or_insert(entry);
1018 }
1019 seen.into_values().collect()
1020}
1021
1022pub fn dump_plugins_json() -> String {
1036 let entries = list_plugins(&default_cache_path());
1037 let mut s = String::from("{\"schema\":1,\"plugins\":[");
1038 for (i, e) in entries.iter().enumerate() {
1039 if i > 0 {
1040 s.push(',');
1041 }
1042 s.push_str(&format!(
1043 "{{\"manager\":{},\"name\":{},\"root\":{}}}",
1044 json_str(&e.manager),
1045 json_str(&e.name),
1046 json_str(&e.root.to_string_lossy())
1047 ));
1048 }
1049 s.push_str("]}");
1050 s
1051}
1052
1053fn json_str(s: &str) -> String {
1054 let mut out = String::with_capacity(s.len() + 2);
1055 out.push('"');
1056 for c in s.chars() {
1057 match c {
1058 '"' => out.push_str("\\\""),
1059 '\\' => out.push_str("\\\\"),
1060 '\n' => out.push_str("\\n"),
1061 '\r' => out.push_str("\\r"),
1062 '\t' => out.push_str("\\t"),
1063 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1064 c => out.push(c),
1065 }
1066 }
1067 out.push('"');
1068 out
1069}
1070
1071pub fn default_cache_path() -> PathBuf {
1074 if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
1075 return PathBuf::from(custom).join("plugins.db");
1076 }
1077 dirs::home_dir()
1078 .unwrap_or_else(|| PathBuf::from("/tmp"))
1079 .join(".zshrs/plugins.db")
1080}
1081
1082#[cfg(test)]
1083mod migration_tests {
1084 use super::*;
1085
1086 #[test]
1087 fn opening_an_existing_db_drops_legacy_script_bytecode_table() {
1088 let _g = crate::test_util::global_state_lock();
1089 let tmp = tempfile::tempdir().unwrap();
1094 let db_path = tmp.path().join("legacy.db");
1095
1096 let pre = Connection::open(&db_path).unwrap();
1098 pre.execute_batch(
1099 r#"
1100 CREATE TABLE script_bytecode (
1101 id INTEGER PRIMARY KEY,
1102 path TEXT NOT NULL UNIQUE,
1103 mtime_secs INTEGER NOT NULL,
1104 mtime_nsecs INTEGER NOT NULL,
1105 bytecode BLOB NOT NULL,
1106 cached_at INTEGER NOT NULL
1107 );
1108 CREATE INDEX idx_script_bytecode_path ON script_bytecode(path);
1109 INSERT INTO script_bytecode (id, path, mtime_secs, mtime_nsecs, bytecode, cached_at)
1110 VALUES (1, '/fake/legacy.zsh', 0, 0, x'00deadbeef', 0);
1111 "#,
1112 )
1113 .unwrap();
1114 drop(pre);
1115
1116 let _cache = PluginCache::open(&db_path).expect("open after migration");
1118
1119 let post = Connection::open(&db_path).unwrap();
1121 let exists: i64 = post
1122 .query_row(
1123 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='script_bytecode'",
1124 [],
1125 |row| row.get(0),
1126 )
1127 .unwrap();
1128 assert_eq!(exists, 0, "legacy script_bytecode must be dropped");
1129 }
1130
1131 fn with_zshrs_home<F: FnOnce()>(value: Option<&str>, f: F) {
1136 let prev = std::env::var_os("ZSHRS_HOME");
1137 match value {
1138 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1139 None => std::env::remove_var("ZSHRS_HOME"),
1140 }
1141 f();
1142 match prev {
1143 Some(v) => std::env::set_var("ZSHRS_HOME", v),
1144 None => std::env::remove_var("ZSHRS_HOME"),
1145 }
1146 }
1147
1148 #[test]
1149 fn default_cache_path_honors_zshrs_home() {
1150 let _g = crate::test_util::global_state_lock();
1151 with_zshrs_home(Some("/tmp/zshrs-plugin-cache-home"), || {
1152 assert_eq!(
1153 default_cache_path(),
1154 PathBuf::from("/tmp/zshrs-plugin-cache-home/plugins.db")
1155 );
1156 });
1157 }
1158
1159 #[test]
1160 fn default_cache_path_filename_is_plugins_db() {
1161 let _g = crate::test_util::global_state_lock();
1162 with_zshrs_home(Some("/tmp/zshrs-plugin-fname"), || {
1163 assert_eq!(
1164 default_cache_path().file_name().and_then(|s| s.to_str()),
1165 Some("plugins.db")
1166 );
1167 });
1168 }
1169
1170 #[test]
1171 fn default_cache_path_falls_back_to_home_dot_zshrs() {
1172 let _g = crate::test_util::global_state_lock();
1173 with_zshrs_home(None, || {
1174 let p = default_cache_path();
1175 let s = p.to_string_lossy();
1177 assert!(
1178 s.ends_with(".zshrs/plugins.db"),
1179 "expected .zshrs/plugins.db tail, got: {}",
1180 s
1181 );
1182 });
1183 }
1184
1185 #[test]
1186 fn default_cache_path_uses_distinct_dir_per_zshrs_home_change() {
1187 let _g = crate::test_util::global_state_lock();
1188 with_zshrs_home(Some("/tmp/zshrs-plugin-a"), || {
1189 let a = default_cache_path();
1190 with_zshrs_home(Some("/tmp/zshrs-plugin-b"), || {
1191 let b = default_cache_path();
1192 assert_ne!(a, b, "different ZSHRS_HOME must yield different paths");
1193 });
1194 });
1195 }
1196
1197 #[test]
1202 fn file_mtime_returns_some_for_existing_file() {
1203 let _g = crate::test_util::global_state_lock();
1204 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime.txt");
1205 std::fs::write(&tmp, b"x").unwrap();
1206 let mt = file_mtime(&tmp);
1207 assert!(mt.is_some(), "existing file should produce mtime");
1208 let (secs, _ns) = mt.unwrap();
1210 assert!(secs > 0, "mtime secs must be positive: {}", secs);
1211 let _ = std::fs::remove_file(&tmp);
1212 }
1213
1214 #[test]
1215 fn file_mtime_returns_none_for_missing_path() {
1216 let _g = crate::test_util::global_state_lock();
1217 assert!(file_mtime(Path::new("/nonexistent/zshrs/missing.bin")).is_none());
1218 }
1219
1220 #[test]
1221 fn file_mtime_secs_monotonic_after_rewrite() {
1222 let _g = crate::test_util::global_state_lock();
1223 let tmp = std::env::temp_dir().join("zshrs_plugin_cache_mtime_two.txt");
1224 std::fs::write(&tmp, b"a").unwrap();
1225 let first = file_mtime(&tmp).unwrap();
1226 std::thread::sleep(std::time::Duration::from_millis(1100));
1228 std::fs::write(&tmp, b"b").unwrap();
1229 let second = file_mtime(&tmp).unwrap();
1230 assert!(
1233 second >= first,
1234 "mtime regressed: first={:?} second={:?}",
1235 first,
1236 second
1237 );
1238 let _ = std::fs::remove_file(&tmp);
1239 }
1240
1241 #[test]
1242 fn file_mtime_path_with_special_chars_resolves() {
1243 let _g = crate::test_util::global_state_lock();
1244 let tmp = std::env::temp_dir().join("zshrs plugin cache (space).bin");
1245 std::fs::write(&tmp, b"x").unwrap();
1246 let mt = file_mtime(&tmp);
1247 assert!(mt.is_some(), "spaces in filename must not block resolution");
1248 let _ = std::fs::remove_file(&tmp);
1249 }
1250
1251 #[test]
1252 fn default_cache_path_relative_zshrs_home_taken_verbatim() {
1253 let _g = crate::test_util::global_state_lock();
1254 with_zshrs_home(Some("relative-dir"), || {
1255 assert_eq!(
1256 default_cache_path(),
1257 PathBuf::from("relative-dir/plugins.db")
1258 );
1259 });
1260 }
1261
1262 #[test]
1263 fn default_cache_path_empty_zshrs_home_is_empty_dir_plus_db() {
1264 let _g = crate::test_util::global_state_lock();
1265 with_zshrs_home(Some(""), || {
1266 assert_eq!(default_cache_path(), PathBuf::from("plugins.db"));
1269 });
1270 }
1271}
1272
1273#[cfg(test)]
1274mod classify_tests {
1275 use super::*;
1276 use std::path::Path;
1277
1278 fn classify(p: &str) -> (String, String, String) {
1279 let e = classify_plugin_path(Path::new(p));
1280 (e.manager, e.name, e.root.to_string_lossy().into_owned())
1281 }
1282
1283 #[test]
1284 fn zinit_legacy_dir_user_repo() {
1285 let (m, n, r) = classify(
1286 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions/zsh-autosuggestions.plugin.zsh",
1287 );
1288 assert_eq!(m, "zinit");
1289 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1290 assert_eq!(
1291 r,
1292 "/Users/wizard/.zinit/plugins/zsh-users---zsh-autosuggestions"
1293 );
1294 }
1295
1296 #[test]
1297 fn zinit_xdg_dir_user_repo() {
1298 let (m, n, _) =
1299 classify("/home/u/.local/share/zinit/plugins/romkatv---powerlevel10k/p10k.zsh");
1300 assert_eq!(m, "zinit");
1301 assert_eq!(n, "romkatv/powerlevel10k");
1302 }
1303
1304 #[test]
1305 fn oh_my_zsh_core_plugin() {
1306 let (m, n, r) = classify("/Users/wizard/.oh-my-zsh/plugins/git/git.plugin.zsh");
1307 assert_eq!(m, "oh-my-zsh");
1308 assert_eq!(n, "git");
1309 assert_eq!(r, "/Users/wizard/.oh-my-zsh/plugins/git");
1310 }
1311
1312 #[test]
1313 fn oh_my_zsh_custom_plugin() {
1314 let (m, n, _) = classify(
1315 "/Users/wizard/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh",
1316 );
1317 assert_eq!(m, "oh-my-zsh");
1318 assert_eq!(n, "zsh-syntax-highlighting");
1319 }
1320
1321 #[test]
1322 fn oh_my_zsh_theme_tagged_with_theme_suffix() {
1323 let (m, n, _) = classify("/Users/wizard/.oh-my-zsh/themes/agnoster.zsh-theme");
1324 assert_eq!(m, "oh-my-zsh");
1325 assert_eq!(n, "agnoster.zsh-theme.theme");
1326 }
1327
1328 #[test]
1329 fn prezto_module() {
1330 let (m, n, _) = classify("/Users/wizard/.zprezto/modules/git/init.zsh");
1331 assert_eq!(m, "prezto");
1332 assert_eq!(n, "git");
1333 }
1334
1335 #[test]
1336 fn antidote_repo() {
1337 let (m, n, _) = classify(
1338 "/Users/wizard/.cache/antidote/zsh-users/zsh-autosuggestions/zsh-autosuggestions.zsh",
1339 );
1340 assert_eq!(m, "antidote");
1341 assert_eq!(n, "zsh-users/zsh-autosuggestions");
1342 }
1343
1344 #[test]
1345 fn antigen_bundle() {
1346 let (m, n, _) = classify(
1347 "/Users/wizard/.antigen/bundles/zsh-users/zsh-completions/zsh-completions.plugin.zsh",
1348 );
1349 assert_eq!(m, "antigen");
1350 assert_eq!(n, "zsh-users/zsh-completions");
1351 }
1352
1353 #[test]
1354 fn zplug_repo() {
1355 let (m, n, _) = classify(
1356 "/Users/wizard/.zplug/repos/zsh-users/zsh-history-substring-search/zsh-history-substring-search.zsh",
1357 );
1358 assert_eq!(m, "zplug");
1359 assert_eq!(n, "zsh-users/zsh-history-substring-search");
1360 }
1361
1362 #[test]
1363 fn zsh_more_completions_groups_into_one() {
1364 let (m, n, _) =
1365 classify("/Users/wizard/forkedRepos/zsh-more-completions/src/_some_long_completion");
1366 assert_eq!(m, "zsh-more-completions");
1367 assert_eq!(n, "zsh-more-completions");
1368 }
1369
1370 #[test]
1371 fn zpwr_root_recognized() {
1372 let (m, n, _) = classify("/Users/wizard/.zpwr/local/.aliases.sh");
1373 assert_eq!(m, "zpwr");
1374 assert_eq!(n, "zpwr");
1375 }
1376
1377 #[test]
1378 fn loose_plugin_uses_parent_dir_as_name() {
1379 let (m, n, r) = classify("/opt/local/share/zsh/something/init.zsh");
1380 assert_eq!(m, "loose");
1381 assert_eq!(n, "something");
1382 assert_eq!(r, "/opt/local/share/zsh/something");
1383 }
1384}
1385
1386impl crate::ported::vm_helper::ShellExecutor {
1394 pub(crate) fn snapshot_state(&self) -> PluginSnapshot {
1396 PluginSnapshot {
1397 functions: self.function_names().into_iter().collect(),
1398 aliases: self.alias_entries().into_iter().map(|(k, _)| k).collect(),
1399 global_aliases: self
1400 .global_alias_entries()
1401 .into_iter()
1402 .map(|(k, _)| k)
1403 .collect(),
1404 suffix_aliases: self
1405 .suffix_alias_entries()
1406 .into_iter()
1407 .map(|(k, _)| k)
1408 .collect(),
1409 variables: if let Ok(tab) = crate::ported::params::paramtab().read() {
1410 tab.iter()
1411 .filter(|(_, pm)| pm.u_arr.is_none())
1412 .map(|(k, pm)| (k.clone(), pm.u_str.clone().unwrap_or_default()))
1413 .collect()
1414 } else {
1415 std::collections::HashMap::new()
1416 },
1417 arrays: if let Ok(tab) = crate::ported::params::paramtab().read() {
1418 tab.iter()
1419 .filter(|(_, pm)| pm.u_arr.is_some())
1420 .map(|(k, _)| k.clone())
1421 .collect()
1422 } else {
1423 std::collections::HashSet::new()
1424 },
1425 assoc_arrays: if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1426 m.keys().cloned().collect()
1427 } else {
1428 std::collections::HashSet::new()
1429 },
1430 fpath: self.fpath.clone(),
1431 options: crate::ported::options::opt_state_snapshot(),
1432 hooks: {
1433 let names = [
1435 "chpwd",
1436 "precmd",
1437 "preexec",
1438 "periodic",
1439 "zshexit",
1440 "zshaddhistory",
1441 ];
1442 let mut m = std::collections::HashMap::new();
1443 for h in &names {
1444 let arr_name = format!("{}_functions", h);
1445 if let Some(arr) = self.array(&arr_name) {
1446 if !arr.is_empty() {
1447 m.insert(h.to_string(), arr);
1448 }
1449 }
1450 }
1451 m
1452 },
1453 autoloads: {
1454 crate::ported::hashtable::shfunctab_lock()
1457 .read()
1458 .ok()
1459 .map(|t| {
1460 t.iter()
1461 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1462 .map(|(name, _)| name.clone())
1463 .collect()
1464 })
1465 .unwrap_or_default()
1466 },
1467 }
1468 }
1469 pub(crate) fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
1471 let mut delta = PluginDelta::default();
1472
1473 let mut fn_keys: Vec<&String> = self.function_source.keys().collect();
1482 fn_keys.sort();
1483 for name in fn_keys {
1484 if !snap.functions.contains(name) {
1485 let source = self.function_source.get(name).unwrap();
1486 delta
1487 .functions
1488 .push((name.clone(), source.as_bytes().to_vec()));
1489 }
1490 }
1491
1492 let push_alias = |delta: &mut PluginDelta,
1493 entries: Vec<(String, String)>,
1494 snap_set: &std::collections::HashSet<String>,
1495 kind: AliasKind| {
1496 let mut entries = entries;
1497 entries.sort_by(|a, b| a.0.cmp(&b.0));
1498 for (name, value) in entries {
1499 if !snap_set.contains(&name) {
1500 delta.aliases.push((name, value, kind));
1501 }
1502 }
1503 };
1504 push_alias(
1505 &mut delta,
1506 self.alias_entries(),
1507 &snap.aliases,
1508 AliasKind::Regular,
1509 );
1510 push_alias(
1511 &mut delta,
1512 self.global_alias_entries(),
1513 &snap.global_aliases,
1514 AliasKind::Global,
1515 );
1516 push_alias(
1517 &mut delta,
1518 self.suffix_alias_entries(),
1519 &snap.suffix_aliases,
1520 AliasKind::Suffix,
1521 );
1522
1523 const NON_REPLAYABLE_VARS: &[&str] = &[
1538 "0",
1539 "_",
1540 "?",
1541 "$",
1542 "!",
1543 "PPID",
1544 "RANDOM",
1545 "SECONDS",
1546 "EPOCHSECONDS",
1547 "EPOCHREALTIME",
1548 "LINENO",
1549 "OLDPWD",
1550 "PWD",
1551 "STATUS",
1552 "OPTIND",
1553 "OPTARG",
1554 "IFS",
1555 "FUNCNAME",
1556 "BASHPID",
1557 "BASH_LINENO",
1558 "BASH_SOURCE",
1559 "ZSH_ARGZERO",
1560 "ZSH_EVAL_CONTEXT",
1561 "ZSH_SUBSHELL",
1562 "HISTCMD",
1563 "MATCH",
1564 "MBEGIN",
1565 "MEND",
1566 ];
1567 let mut var_keys: Vec<String> = if let Ok(tab) = crate::ported::params::paramtab().read() {
1568 tab.iter()
1569 .filter(|(_, pm)| pm.u_arr.is_none())
1570 .map(|(k, _)| k.clone())
1571 .collect()
1572 } else {
1573 Vec::new()
1574 };
1575 var_keys.sort();
1576 for name in &var_keys {
1577 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1578 continue;
1579 }
1580 let value = crate::ported::params::getsparam(name).unwrap_or_default();
1581 match snap.variables.get(name) {
1582 Some(old) if old == &value => {} _ => {
1584 if env::var(name).ok().as_ref() == Some(&value) {
1586 delta.exports.push((name.clone(), value.clone()));
1587 } else {
1588 delta.variables.push((name.clone(), value.clone()));
1589 }
1590 }
1591 }
1592 }
1593
1594 let arr_entries: Vec<(String, Vec<String>)> =
1596 if let Ok(tab) = crate::ported::params::paramtab().read() {
1597 let mut v: Vec<(String, Vec<String>)> = tab
1598 .iter()
1599 .filter_map(|(k, pm)| pm.u_arr.clone().map(|a| (k.clone(), a)))
1600 .collect();
1601 v.sort_by(|a, b| a.0.cmp(&b.0));
1602 v
1603 } else {
1604 Vec::new()
1605 };
1606 for (name, values) in arr_entries {
1607 if !snap.arrays.contains(&name) {
1608 delta.arrays.push((name, values));
1609 }
1610 }
1611
1612 let assoc_entries: Vec<(String, indexmap::IndexMap<String, String>)> =
1619 if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1620 let mut v: Vec<(String, indexmap::IndexMap<String, String>)> =
1621 m.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1622 v.sort_by(|a, b| a.0.cmp(&b.0));
1623 v
1624 } else {
1625 Vec::new()
1626 };
1627 for (name, map) in assoc_entries {
1628 if !snap.assoc_arrays.contains(&name) {
1629 let plain: HashMap<String, String> =
1635 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1636 delta.assoc_arrays.push((name, plain));
1637 }
1638 }
1639
1640 for p in &self.fpath {
1642 if !snap.fpath.contains(p) {
1643 delta.fpath_additions.push(p.to_string_lossy().to_string());
1644 }
1645 }
1646
1647 let current = crate::ported::options::opt_state_snapshot();
1649 let mut opt_keys: Vec<&String> = current.keys().collect();
1650 opt_keys.sort();
1651 for name in opt_keys {
1652 let value = current.get(name).unwrap();
1653 match snap.options.get(name) {
1654 Some(old) if old == value => {}
1655 _ => delta.options_changed.push((name.clone(), *value)),
1656 }
1657 }
1658
1659 let names = [
1661 "chpwd",
1662 "precmd",
1663 "preexec",
1664 "periodic",
1665 "zshexit",
1666 "zshaddhistory",
1667 ];
1668 let mut hook_names: Vec<&&str> = names.iter().collect();
1669 hook_names.sort();
1670 for &h in hook_names {
1671 let arr_name = format!("{}_functions", h);
1672 let funcs = self.array(&arr_name).unwrap_or_default();
1673 let old_funcs = snap.hooks.get(h);
1674 for f in &funcs {
1675 let is_new = old_funcs.is_none_or(|old| !old.contains(f));
1676 if is_new {
1677 delta.hooks.push((h.to_string(), f.clone()));
1678 }
1679 }
1680 }
1681
1682 let current_autoloads: Vec<String> = crate::ported::hashtable::shfunctab_lock()
1684 .read()
1685 .ok()
1686 .map(|t| {
1687 t.iter()
1688 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1689 .map(|(name, _)| name.clone())
1690 .collect()
1691 })
1692 .unwrap_or_default();
1693 let mut autoload_keys: Vec<&String> = current_autoloads.iter().collect();
1694 autoload_keys.sort();
1695 for name in autoload_keys {
1696 if !snap.autoloads.contains(name) {
1697 delta.autoloads.push((name.clone(), String::new()));
1700 }
1701 }
1702
1703 delta
1704 }
1705 pub(crate) fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
1707 for (name, value, kind) in &delta.aliases {
1709 match kind {
1710 AliasKind::Regular => {
1711 self.set_alias(name.clone(), value.clone());
1712 }
1713 AliasKind::Global => {
1714 self.set_global_alias(name.clone(), value.clone());
1715 }
1716 AliasKind::Suffix => {
1717 self.set_suffix_alias(name.clone(), value.clone());
1718 }
1719 }
1720 }
1721
1722 const NON_REPLAYABLE_VARS: &[&str] = &[
1729 "0",
1730 "_",
1731 "?",
1732 "$",
1733 "!",
1734 "PPID",
1735 "RANDOM",
1736 "SECONDS",
1737 "EPOCHSECONDS",
1738 "EPOCHREALTIME",
1739 "LINENO",
1740 "OLDPWD",
1741 "PWD",
1742 "STATUS",
1743 "OPTIND",
1744 "OPTARG",
1745 "IFS",
1746 "FUNCNAME",
1747 "BASHPID",
1748 "BASH_LINENO",
1749 "BASH_SOURCE",
1750 "ZSH_ARGZERO",
1751 "ZSH_EVAL_CONTEXT",
1752 "ZSH_SUBSHELL",
1753 "HISTCMD",
1754 "MATCH",
1755 "MBEGIN",
1756 "MEND",
1757 ];
1758 for (name, value) in &delta.variables {
1759 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1760 continue;
1761 }
1762 self.set_scalar(name.clone(), value.clone());
1763 }
1764
1765 for (name, value) in &delta.exports {
1767 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1768 continue;
1769 }
1770 self.set_scalar(name.clone(), value.clone());
1771 env::set_var(name, value);
1772 }
1773
1774 for (name, values) in &delta.arrays {
1776 self.set_array(name.clone(), values.clone());
1777 }
1778
1779 for (name, map) in &delta.assoc_arrays {
1784 let mut idx_map: indexmap::IndexMap<String, String> =
1789 indexmap::IndexMap::with_capacity(map.len());
1790 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1795 entries.sort_by(|a, b| a.0.cmp(b.0));
1796 for (k, v) in entries {
1797 idx_map.insert(k.clone(), v.clone());
1798 }
1799 self.set_assoc(name.clone(), idx_map);
1800 }
1801
1802 for p in &delta.fpath_additions {
1804 let pb = PathBuf::from(p);
1805 if !self.fpath.contains(&pb) {
1806 self.fpath.push(pb);
1807 }
1808 }
1809
1810 if !delta.completions.is_empty() {
1812 let mut comps = self.assoc("_comps").unwrap_or_default();
1813 for (cmd, func) in &delta.completions {
1814 comps.insert(cmd.clone(), func.clone());
1815 }
1816 self.set_assoc("_comps".to_string(), comps);
1817 }
1818
1819 for (name, enabled) in &delta.options_changed {
1821 crate::ported::options::opt_state_set(name, *enabled);
1822 }
1823
1824 for (hook, func) in &delta.hooks {
1830 let array_name = format!("{}_functions", hook);
1831 let mut arr = self.array(&array_name).unwrap_or_default();
1832 if !arr.iter().any(|f| f == func) {
1833 arr.push(func.clone());
1834 crate::ported::params::setaparam(&array_name, arr);
1835 }
1836 }
1837
1838 for (name, bytes) in &delta.functions {
1842 let Ok(source) = std::str::from_utf8(bytes) else {
1843 continue;
1844 };
1845 let saved_errflag = errflag.load(Ordering::Relaxed);
1847 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
1848 crate::ported::parse::parse_init(source);
1849 let program = crate::ported::parse::parse();
1850 let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
1851 errflag.store(saved_errflag, Ordering::Relaxed);
1852 if parse_failed || program.lists.is_empty() {
1853 continue;
1854 }
1855 let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
1856 self.functions_compiled.insert(name.clone(), chunk);
1857 self.function_source
1858 .insert(name.clone(), source.to_string());
1859 }
1860 }
1861}
1862