1use rusqlite::{params, Connection};
24use std::collections::HashMap;
25use std::env;
26use std::path::{Path, PathBuf};
27use std::sync::OnceLock;
28use std::os::unix::fs::MetadataExt;
29use crate::ported::zsh_h::PM_UNDEFINED;
30use crate::ported::utils::{errflag, ERRFLAG_ERROR};
31use std::sync::atomic::Ordering;
32#[allow(unused_imports)]
33use crate::ported::exec::ShellExecutor;
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)>,
79 pub suffix_aliases: Vec<(String, String)>,
80 pub variables: Vec<(String, String)>,
81 pub exports: Vec<(String, String)>, pub arrays: Vec<(String, Vec<String>)>,
83 pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
84 pub completions: Vec<(String, String)>, pub fpath_additions: Vec<String>,
86 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)>, }
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AliasKind {
95 Regular,
96 Global,
97 Suffix,
98}
99
100impl AliasKind {
101 fn as_i32(self) -> i32 {
102 match self {
103 AliasKind::Regular => 0,
104 AliasKind::Global => 1,
105 AliasKind::Suffix => 2,
106 }
107 }
108 fn from_i32(v: i32) -> Self {
109 match v {
110 1 => AliasKind::Global,
111 2 => AliasKind::Suffix,
112 _ => AliasKind::Regular,
113 }
114 }
115}
116
117pub struct PluginCache {
119 conn: Connection,
120}
121
122impl PluginCache {
123 pub fn open(path: &Path) -> rusqlite::Result<Self> {
124 let conn = Connection::open(path)?;
125 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
126 let cache = Self { conn };
127 cache.init_schema()?;
128 Ok(cache)
129 }
130
131 fn init_schema(&self) -> rusqlite::Result<()> {
132 self.conn.execute_batch(
133 r#"
134 CREATE TABLE IF NOT EXISTS plugins (
135 id INTEGER PRIMARY KEY,
136 path TEXT NOT NULL UNIQUE,
137 mtime_secs INTEGER NOT NULL,
138 mtime_nsecs INTEGER NOT NULL,
139 source_time_ms INTEGER NOT NULL,
140 cached_at INTEGER NOT NULL,
141 binary_mtime INTEGER NOT NULL DEFAULT 0
142 );
143
144 CREATE TABLE IF NOT EXISTS plugin_functions (
145 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
146 name TEXT NOT NULL,
147 body BLOB NOT NULL
148 );
149
150 CREATE TABLE IF NOT EXISTS plugin_aliases (
151 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
152 name TEXT NOT NULL,
153 value TEXT NOT NULL,
154 kind INTEGER NOT NULL DEFAULT 0
155 );
156
157 CREATE TABLE IF NOT EXISTS plugin_variables (
158 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
159 name TEXT NOT NULL,
160 value TEXT NOT NULL,
161 is_export INTEGER NOT NULL DEFAULT 0
162 );
163
164 CREATE TABLE IF NOT EXISTS plugin_arrays (
165 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
166 name TEXT NOT NULL,
167 value_json TEXT NOT NULL
168 );
169
170 -- Associative-array deltas (e.g. ZINIT[BIN_DIR]=...). Stored
171 -- as JSON {key: value} so insertion order isn't load-bearing
172 -- (matches HashMap semantics on the Rust side). Direct
173 -- analogue of plugin_arrays for assoc shape.
174 CREATE TABLE IF NOT EXISTS plugin_assoc_arrays (
175 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
176 name TEXT NOT NULL,
177 value_json TEXT NOT NULL
178 );
179
180 CREATE TABLE IF NOT EXISTS plugin_completions (
181 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
182 command TEXT NOT NULL,
183 function TEXT NOT NULL
184 );
185
186 CREATE TABLE IF NOT EXISTS plugin_fpath (
187 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
188 path TEXT NOT NULL
189 );
190
191 CREATE TABLE IF NOT EXISTS plugin_hooks (
192 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
193 hook TEXT NOT NULL,
194 function TEXT NOT NULL
195 );
196
197 CREATE TABLE IF NOT EXISTS plugin_bindkeys (
198 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
199 keyseq TEXT NOT NULL,
200 widget TEXT NOT NULL,
201 keymap TEXT NOT NULL DEFAULT 'main'
202 );
203
204 CREATE TABLE IF NOT EXISTS plugin_zstyles (
205 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
206 pattern TEXT NOT NULL,
207 style TEXT NOT NULL,
208 value TEXT NOT NULL
209 );
210
211 CREATE TABLE IF NOT EXISTS plugin_options (
212 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
213 name TEXT NOT NULL,
214 enabled INTEGER NOT NULL
215 );
216
217 CREATE TABLE IF NOT EXISTS plugin_autoloads (
218 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
219 function TEXT NOT NULL,
220 flags TEXT NOT NULL DEFAULT ''
221 );
222
223 -- compaudit cache: security audit results per fpath directory
224 CREATE TABLE IF NOT EXISTS compaudit_cache (
225 id INTEGER PRIMARY KEY,
226 path TEXT NOT NULL UNIQUE,
227 mtime_secs INTEGER NOT NULL,
228 mtime_nsecs INTEGER NOT NULL,
229 uid INTEGER NOT NULL,
230 mode INTEGER NOT NULL,
231 is_secure INTEGER NOT NULL,
232 checked_at INTEGER NOT NULL
233 );
234
235 CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
236 CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
237
238 -- Migration: legacy script_bytecode table (bytecode now lives in
239 -- the rkyv shard at ~/.zshrs/scripts.rkyv). Drop on open so
240 -- existing DBs reclaim the space and don't carry stale bytecode.
241 DROP INDEX IF EXISTS idx_script_bytecode_path;
242 DROP TABLE IF EXISTS script_bytecode;
243 "#,
244 )?;
245 let _ = self
256 .conn
257 .execute("ALTER TABLE plugins ADD COLUMN binary_mtime INTEGER NOT NULL DEFAULT 0", []);
258 Ok(())
259 }
260
261 pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
272 let row: Option<(i64, i64)> = self
273 .conn
274 .query_row(
275 "SELECT id, binary_mtime FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
276 params![path, mtime_secs, mtime_nsecs],
277 |row| Ok((row.get(0)?, row.get(1)?)),
278 )
279 .ok();
280 let (id, cached_bin_mtime) = row?;
281 if let Some(bin_mtime) = current_binary_mtime() {
282 if cached_bin_mtime < bin_mtime {
283 return None;
284 }
285 }
286 Some(id)
287 }
288
289 pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
291 let mut delta = PluginDelta::default();
292
293 let mut stmt = self
295 .conn
296 .prepare("SELECT name, body FROM plugin_functions WHERE plugin_id = ?1")?;
297 let rows = stmt.query_map(params![plugin_id], |row| {
298 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
299 })?;
300 for r in rows {
301 delta.functions.push(r?);
302 }
303
304 let mut stmt = self
306 .conn
307 .prepare("SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1")?;
308 let rows = stmt.query_map(params![plugin_id], |row| {
309 Ok((
310 row.get::<_, String>(0)?,
311 row.get::<_, String>(1)?,
312 AliasKind::from_i32(row.get::<_, i32>(2)?),
313 ))
314 })?;
315 for r in rows {
316 delta.aliases.push(r?);
317 }
318
319 let mut stmt = self
321 .conn
322 .prepare("SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1")?;
323 let rows = stmt.query_map(params![plugin_id], |row| {
324 Ok((
325 row.get::<_, String>(0)?,
326 row.get::<_, String>(1)?,
327 row.get::<_, bool>(2)?,
328 ))
329 })?;
330 for r in rows {
331 let (name, value, is_export) = r?;
332 if is_export {
333 delta.exports.push((name, value));
334 } else {
335 delta.variables.push((name, value));
336 }
337 }
338
339 let mut stmt = self
341 .conn
342 .prepare("SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1")?;
343 let rows = stmt.query_map(params![plugin_id], |row| {
344 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
345 })?;
346 for r in rows {
347 let (name, json) = r?;
348 let vals: Vec<String> = json
350 .trim_matches(|c| c == '[' || c == ']')
351 .split(',')
352 .map(|s| s.trim().trim_matches('"').to_string())
353 .filter(|s| !s.is_empty())
354 .collect();
355 delta.arrays.push((name, vals));
356 }
357
358 let mut stmt = self
362 .conn
363 .prepare("SELECT name, value_json FROM plugin_assoc_arrays WHERE plugin_id = ?1")?;
364 let rows = stmt.query_map(params![plugin_id], |row| {
365 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
366 })?;
367 for r in rows {
368 let (name, json) = r?;
369 let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or_default();
370 delta.assoc_arrays.push((name, map));
371 }
372
373 let mut stmt = self
375 .conn
376 .prepare("SELECT command, function FROM plugin_completions WHERE plugin_id = ?1")?;
377 let rows = stmt.query_map(params![plugin_id], |row| {
378 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
379 })?;
380 for r in rows {
381 delta.completions.push(r?);
382 }
383
384 let mut stmt = self
386 .conn
387 .prepare("SELECT path FROM plugin_fpath WHERE plugin_id = ?1")?;
388 let rows = stmt.query_map(params![plugin_id], |row| row.get::<_, String>(0))?;
389 for r in rows {
390 delta.fpath_additions.push(r?);
391 }
392
393 let mut stmt = self
395 .conn
396 .prepare("SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1")?;
397 let rows = stmt.query_map(params![plugin_id], |row| {
398 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
399 })?;
400 for r in rows {
401 delta.hooks.push(r?);
402 }
403
404 let mut stmt = self
406 .conn
407 .prepare("SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1")?;
408 let rows = stmt.query_map(params![plugin_id], |row| {
409 Ok((
410 row.get::<_, String>(0)?,
411 row.get::<_, String>(1)?,
412 row.get::<_, String>(2)?,
413 ))
414 })?;
415 for r in rows {
416 delta.bindkeys.push(r?);
417 }
418
419 let mut stmt = self
421 .conn
422 .prepare("SELECT pattern, style, value FROM plugin_zstyles WHERE plugin_id = ?1")?;
423 let rows = stmt.query_map(params![plugin_id], |row| {
424 Ok((
425 row.get::<_, String>(0)?,
426 row.get::<_, String>(1)?,
427 row.get::<_, String>(2)?,
428 ))
429 })?;
430 for r in rows {
431 delta.zstyles.push(r?);
432 }
433
434 let mut stmt = self
436 .conn
437 .prepare("SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1")?;
438 let rows = stmt.query_map(params![plugin_id], |row| {
439 Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
440 })?;
441 for r in rows {
442 delta.options_changed.push(r?);
443 }
444
445 let mut stmt = self
447 .conn
448 .prepare("SELECT function, flags FROM plugin_autoloads WHERE plugin_id = ?1")?;
449 let rows = stmt.query_map(params![plugin_id], |row| {
450 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
451 })?;
452 for r in rows {
453 delta.autoloads.push(r?);
454 }
455
456 Ok(delta)
457 }
458
459 pub fn store(
461 &self,
462 path: &str,
463 mtime_secs: i64,
464 mtime_nsecs: i64,
465 source_time_ms: u64,
466 delta: &PluginDelta,
467 ) -> rusqlite::Result<()> {
468 let now = std::time::SystemTime::now()
469 .duration_since(std::time::UNIX_EPOCH)
470 .map(|d| d.as_secs() as i64)
471 .unwrap_or(0);
472
473 self.conn
475 .execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
476
477 let bin_mtime = current_binary_mtime().unwrap_or(0);
478 self.conn.execute(
479 "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at, binary_mtime) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
480 params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now, bin_mtime],
481 )?;
482 let plugin_id = self.conn.last_insert_rowid();
483
484 for (name, body) in &delta.functions {
486 self.conn.execute(
487 "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
488 params![plugin_id, name, body],
489 )?;
490 }
491
492 for (name, value, kind) in &delta.aliases {
494 self.conn.execute(
495 "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
496 params![plugin_id, name, value, kind.as_i32()],
497 )?;
498 }
499
500 for (name, value) in &delta.variables {
502 self.conn.execute(
503 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
504 params![plugin_id, name, value],
505 )?;
506 }
507 for (name, value) in &delta.exports {
508 self.conn.execute(
509 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
510 params![plugin_id, name, value],
511 )?;
512 }
513
514 for (name, vals) in &delta.arrays {
516 let json = format!(
517 "[{}]",
518 vals.iter()
519 .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
520 .collect::<Vec<_>>()
521 .join(",")
522 );
523 self.conn.execute(
524 "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
525 params![plugin_id, name, json],
526 )?;
527 }
528
529 for (name, map) in &delta.assoc_arrays {
535 let json = serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string());
536 self.conn.execute(
537 "INSERT INTO plugin_assoc_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
538 params![plugin_id, name, json],
539 )?;
540 }
541
542 for (cmd, func) in &delta.completions {
544 self.conn.execute(
545 "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
546 params![plugin_id, cmd, func],
547 )?;
548 }
549
550 for p in &delta.fpath_additions {
552 self.conn.execute(
553 "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
554 params![plugin_id, p],
555 )?;
556 }
557
558 for (hook, func) in &delta.hooks {
560 self.conn.execute(
561 "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
562 params![plugin_id, hook, func],
563 )?;
564 }
565
566 for (keyseq, widget, keymap) in &delta.bindkeys {
568 self.conn.execute(
569 "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
570 params![plugin_id, keyseq, widget, keymap],
571 )?;
572 }
573
574 for (pattern, style, value) in &delta.zstyles {
576 self.conn.execute(
577 "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
578 params![plugin_id, pattern, style, value],
579 )?;
580 }
581
582 for (name, enabled) in &delta.options_changed {
584 self.conn.execute(
585 "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
586 params![plugin_id, name, *enabled],
587 )?;
588 }
589
590 for (func, flags) in &delta.autoloads {
592 self.conn.execute(
593 "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
594 params![plugin_id, func, flags],
595 )?;
596 }
597
598 Ok(())
599 }
600
601 pub fn stats(&self) -> (i64, i64) {
603 let plugins: i64 = self
604 .conn
605 .query_row("SELECT COUNT(*) FROM plugins", [], |r| r.get(0))
606 .unwrap_or(0);
607 let functions: i64 = self
608 .conn
609 .query_row("SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0))
610 .unwrap_or(0);
611 (plugins, functions)
612 }
613
614 pub fn count_stale(&self) -> usize {
616 let mut stmt = match self
617 .conn
618 .prepare("SELECT path, mtime_secs, mtime_nsecs FROM plugins")
619 {
620 Ok(s) => s,
621 Err(_) => return 0,
622 };
623 let rows = match stmt.query_map([], |row| {
624 Ok((
625 row.get::<_, String>(0)?,
626 row.get::<_, i64>(1)?,
627 row.get::<_, i64>(2)?,
628 ))
629 }) {
630 Ok(r) => r,
631 Err(_) => return 0,
632 };
633 let mut count = 0;
634 for (path, cached_s, cached_ns) in rows.flatten() {
635 match file_mtime(std::path::Path::new(&path)) {
636 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
637 None => count += 1, _ => {}
639 }
640 }
641 count
642 }
643
644 pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
651 self.conn.query_row(
652 "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
653 params![dir, mtime_secs, mtime_nsecs],
654 |row| row.get::<_, bool>(0),
655 ).ok()
656 }
657
658 pub fn store_compaudit(
660 &self,
661 dir: &str,
662 mtime_secs: i64,
663 mtime_nsecs: i64,
664 uid: u32,
665 mode: u32,
666 is_secure: bool,
667 ) -> rusqlite::Result<()> {
668 let now = std::time::SystemTime::now()
669 .duration_since(std::time::UNIX_EPOCH)
670 .map(|d| d.as_secs() as i64)
671 .unwrap_or(0);
672
673 self.conn.execute(
674 "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)",
675 params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
676 )?;
677 Ok(())
678 }
679
680 pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
683
684 let euid = unsafe { libc::geteuid() };
685 let mut insecure = Vec::new();
686
687 for dir in fpath {
688 let dir_str = dir.to_string_lossy().to_string();
689 let meta = match std::fs::metadata(dir) {
690 Ok(m) => m,
691 Err(_) => continue, };
693 let mt_s = meta.mtime();
694 let mt_ns = meta.mtime_nsec();
695
696 if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
698 if !is_secure {
699 insecure.push(dir_str);
700 }
701 continue;
702 }
703
704 let mode = meta.mode();
706 let uid = meta.uid();
707 let is_secure = Self::check_dir_security(&meta, euid);
708
709 let parent_secure = dir
711 .parent()
712 .and_then(|p| std::fs::metadata(p).ok())
713 .map(|pm| Self::check_dir_security(&pm, euid))
714 .unwrap_or(true);
715
716 let secure = is_secure && parent_secure;
717
718 let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
720
721 if !secure {
722 insecure.push(dir_str);
723 }
724 }
725
726 if insecure.is_empty() {
727 tracing::debug!(
728 dirs = fpath.len(),
729 "compaudit: all directories secure (cached)"
730 );
731 } else {
732 tracing::warn!(
733 insecure_count = insecure.len(),
734 dirs = fpath.len(),
735 "compaudit: insecure directories found"
736 );
737 }
738
739 insecure
740 }
741
742 fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
745 let mode = meta.mode();
746 let uid = meta.uid();
747
748 if uid == 0 || uid == euid {
750 return true;
751 }
752
753 let group_writable = mode & 0o020 != 0;
755 let world_writable = mode & 0o002 != 0;
756
757 !group_writable && !world_writable
758 }
759}
760
761pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
763 let meta = std::fs::metadata(path).ok()?;
764 Some((meta.mtime(), meta.mtime_nsec()))
765}
766
767pub fn default_cache_path() -> PathBuf {
770 if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
771 return PathBuf::from(custom).join("plugins.db");
772 }
773 dirs::home_dir()
774 .unwrap_or_else(|| PathBuf::from("/tmp"))
775 .join(".zshrs/plugins.db")
776}
777
778#[cfg(test)]
779mod migration_tests {
780 use super::*;
781
782 #[test]
783 fn opening_an_existing_db_drops_legacy_script_bytecode_table() {
784 let tmp = tempfile::tempdir().unwrap();
789 let db_path = tmp.path().join("legacy.db");
790
791 let pre = Connection::open(&db_path).unwrap();
793 pre.execute_batch(
794 r#"
795 CREATE TABLE script_bytecode (
796 id INTEGER PRIMARY KEY,
797 path TEXT NOT NULL UNIQUE,
798 mtime_secs INTEGER NOT NULL,
799 mtime_nsecs INTEGER NOT NULL,
800 bytecode BLOB NOT NULL,
801 cached_at INTEGER NOT NULL
802 );
803 CREATE INDEX idx_script_bytecode_path ON script_bytecode(path);
804 INSERT INTO script_bytecode (id, path, mtime_secs, mtime_nsecs, bytecode, cached_at)
805 VALUES (1, '/fake/legacy.zsh', 0, 0, x'00deadbeef', 0);
806 "#,
807 )
808 .unwrap();
809 drop(pre);
810
811 let _cache = PluginCache::open(&db_path).expect("open after migration");
813
814 let post = Connection::open(&db_path).unwrap();
816 let exists: i64 = post
817 .query_row(
818 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='script_bytecode'",
819 [],
820 |row| row.get(0),
821 )
822 .unwrap();
823 assert_eq!(exists, 0, "legacy script_bytecode must be dropped");
824 }
825}
826
827impl crate::ported::exec::ShellExecutor {
835 pub(crate) fn snapshot_state(&self) -> PluginSnapshot {
837 PluginSnapshot {
838 functions: self.function_names().into_iter().collect(),
839 aliases: self.alias_entries().into_iter().map(|(k, _)| k).collect(),
840 global_aliases: self.global_alias_entries().into_iter().map(|(k, _)| k).collect(),
841 suffix_aliases: self.suffix_alias_entries().into_iter().map(|(k, _)| k).collect(),
842 variables: if let Ok(tab) = crate::ported::params::paramtab().read() {
843 tab.iter()
844 .filter(|(_, pm)| pm.u_arr.is_none())
845 .map(|(k, pm)| (k.clone(), pm.u_str.clone().unwrap_or_default()))
846 .collect()
847 } else {
848 std::collections::HashMap::new()
849 },
850 arrays: if let Ok(tab) = crate::ported::params::paramtab().read() {
851 tab.iter()
852 .filter(|(_, pm)| pm.u_arr.is_some())
853 .map(|(k, _)| k.clone())
854 .collect()
855 } else {
856 std::collections::HashSet::new()
857 },
858 assoc_arrays: if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
859 m.keys().cloned().collect()
860 } else {
861 std::collections::HashSet::new()
862 },
863 fpath: self.fpath.clone(),
864 options: crate::ported::options::opt_state_snapshot(),
865 hooks: {
866 let names = ["chpwd", "precmd", "preexec", "periodic", "zshexit", "zshaddhistory"];
868 let mut m = std::collections::HashMap::new();
869 for h in &names {
870 let arr_name = format!("{}_functions", h);
871 if let Some(arr) = self.array(&arr_name) {
872 if !arr.is_empty() {
873 m.insert(h.to_string(), arr);
874 }
875 }
876 }
877 m
878 },
879 autoloads: {
880 crate::ported::hashtable::shfunctab_lock().read().ok()
883 .map(|t| t.iter()
884 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
885 .map(|(name, _)| name.clone())
886 .collect())
887 .unwrap_or_default()
888 },
889 }
890 }
891 pub(crate) fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
893 let mut delta = PluginDelta::default();
894
895 let mut fn_keys: Vec<&String> = self.function_source.keys().collect();
904 fn_keys.sort();
905 for name in fn_keys {
906 if !snap.functions.contains(name) {
907 let source = self.function_source.get(name).unwrap();
908 delta
909 .functions
910 .push((name.clone(), source.as_bytes().to_vec()));
911 }
912 }
913
914 let push_alias = |delta: &mut PluginDelta,
915 entries: Vec<(String, String)>,
916 snap_set: &std::collections::HashSet<String>,
917 kind: AliasKind| {
918 let mut entries = entries;
919 entries.sort_by(|a, b| a.0.cmp(&b.0));
920 for (name, value) in entries {
921 if !snap_set.contains(&name) {
922 delta.aliases.push((name, value, kind));
923 }
924 }
925 };
926 push_alias(&mut delta, self.alias_entries(), &snap.aliases, AliasKind::Regular);
927 push_alias(&mut delta, self.global_alias_entries(), &snap.global_aliases, AliasKind::Global);
928 push_alias(&mut delta, self.suffix_alias_entries(), &snap.suffix_aliases, AliasKind::Suffix);
929
930 const NON_REPLAYABLE_VARS: &[&str] = &[
945 "0", "_", "?", "$", "!", "PPID", "RANDOM", "SECONDS",
946 "EPOCHSECONDS", "EPOCHREALTIME", "LINENO", "OLDPWD", "PWD",
947 "STATUS", "OPTIND", "OPTARG", "IFS", "FUNCNAME",
948 "BASHPID", "BASH_LINENO", "BASH_SOURCE",
949 "ZSH_ARGZERO", "ZSH_EVAL_CONTEXT", "ZSH_SUBSHELL",
950 "HISTCMD", "MATCH", "MBEGIN", "MEND",
951 ];
952 let mut var_keys: Vec<String> =
953 if let Ok(tab) = crate::ported::params::paramtab().read() {
954 tab.iter()
955 .filter(|(_, pm)| pm.u_arr.is_none())
956 .map(|(k, _)| k.clone())
957 .collect()
958 } else {
959 Vec::new()
960 };
961 var_keys.sort();
962 for name in &var_keys {
963 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
964 continue;
965 }
966 let value = crate::ported::params::getsparam(name).unwrap_or_default();
967 match snap.variables.get(name) {
968 Some(old) if old == &value => {} _ => {
970 if env::var(name).ok().as_ref() == Some(&value) {
972 delta.exports.push((name.clone(), value.clone()));
973 } else {
974 delta.variables.push((name.clone(), value.clone()));
975 }
976 }
977 }
978 }
979
980 let arr_entries: Vec<(String, Vec<String>)> =
982 if let Ok(tab) = crate::ported::params::paramtab().read() {
983 let mut v: Vec<(String, Vec<String>)> = tab.iter()
984 .filter_map(|(k, pm)| pm.u_arr.clone().map(|a| (k.clone(), a)))
985 .collect();
986 v.sort_by(|a, b| a.0.cmp(&b.0));
987 v
988 } else {
989 Vec::new()
990 };
991 for (name, values) in arr_entries {
992 if !snap.arrays.contains(&name) {
993 delta.arrays.push((name, values));
994 }
995 }
996
997 let assoc_entries: Vec<(String, indexmap::IndexMap<String, String>)> =
1004 if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1005 let mut v: Vec<(String, indexmap::IndexMap<String, String>)> = m.iter()
1006 .map(|(k, v)| (k.clone(), v.clone()))
1007 .collect();
1008 v.sort_by(|a, b| a.0.cmp(&b.0));
1009 v
1010 } else {
1011 Vec::new()
1012 };
1013 for (name, map) in assoc_entries {
1014 if !snap.assoc_arrays.contains(&name) {
1015 let plain: HashMap<String, String> =
1021 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1022 delta.assoc_arrays.push((name, plain));
1023 }
1024 }
1025
1026 for p in &self.fpath {
1028 if !snap.fpath.contains(p) {
1029 delta.fpath_additions.push(p.to_string_lossy().to_string());
1030 }
1031 }
1032
1033 let current = crate::ported::options::opt_state_snapshot();
1035 let mut opt_keys: Vec<&String> = current.keys().collect();
1036 opt_keys.sort();
1037 for name in opt_keys {
1038 let value = current.get(name).unwrap();
1039 match snap.options.get(name) {
1040 Some(old) if old == value => {}
1041 _ => delta.options_changed.push((name.clone(), *value)),
1042 }
1043 }
1044
1045 let names = ["chpwd", "precmd", "preexec", "periodic", "zshexit", "zshaddhistory"];
1047 let mut hook_names: Vec<&&str> = names.iter().collect();
1048 hook_names.sort();
1049 for &h in hook_names {
1050 let arr_name = format!("{}_functions", h);
1051 let funcs = self.array(&arr_name).unwrap_or_default();
1052 let old_funcs = snap.hooks.get(h);
1053 for f in &funcs {
1054 let is_new = old_funcs.is_none_or(|old| !old.contains(f));
1055 if is_new {
1056 delta.hooks.push((h.to_string(), f.clone()));
1057 }
1058 }
1059 }
1060
1061 let current_autoloads: Vec<String> = crate::ported::hashtable::shfunctab_lock()
1063 .read().ok()
1064 .map(|t| t.iter()
1065 .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1066 .map(|(name, _)| name.clone())
1067 .collect())
1068 .unwrap_or_default();
1069 let mut autoload_keys: Vec<&String> = current_autoloads.iter().collect();
1070 autoload_keys.sort();
1071 for name in autoload_keys {
1072 if !snap.autoloads.contains(name) {
1073 delta.autoloads.push((name.clone(), String::new()));
1076 }
1077 }
1078
1079 delta
1080 }
1081 pub(crate) fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
1083
1084 for (name, value, kind) in &delta.aliases {
1086 match kind {
1087 AliasKind::Regular => {
1088 self.set_alias(name.clone(), value.clone());
1089 }
1090 AliasKind::Global => {
1091 self.set_global_alias(name.clone(), value.clone());
1092 }
1093 AliasKind::Suffix => {
1094 self.set_suffix_alias(name.clone(), value.clone());
1095 }
1096 }
1097 }
1098
1099 const NON_REPLAYABLE_VARS: &[&str] = &[
1106 "0", "_", "?", "$", "!", "PPID", "RANDOM", "SECONDS",
1107 "EPOCHSECONDS", "EPOCHREALTIME", "LINENO", "OLDPWD", "PWD",
1108 "STATUS", "OPTIND", "OPTARG", "IFS", "FUNCNAME",
1109 "BASHPID", "BASH_LINENO", "BASH_SOURCE",
1110 "ZSH_ARGZERO", "ZSH_EVAL_CONTEXT", "ZSH_SUBSHELL",
1111 "HISTCMD", "MATCH", "MBEGIN", "MEND",
1112 ];
1113 for (name, value) in &delta.variables {
1114 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1115 continue;
1116 }
1117 self.set_scalar(name.clone(), value.clone());
1118 }
1119
1120 for (name, value) in &delta.exports {
1122 if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1123 continue;
1124 }
1125 self.set_scalar(name.clone(), value.clone());
1126 env::set_var(name, value);
1127 }
1128
1129 for (name, values) in &delta.arrays {
1131 self.set_array(name.clone(), values.clone());
1132 }
1133
1134 for (name, map) in &delta.assoc_arrays {
1139 let mut idx_map: indexmap::IndexMap<String, String> =
1144 indexmap::IndexMap::with_capacity(map.len());
1145 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1150 entries.sort_by(|a, b| a.0.cmp(b.0));
1151 for (k, v) in entries {
1152 idx_map.insert(k.clone(), v.clone());
1153 }
1154 self.set_assoc(name.clone(), idx_map);
1155 }
1156
1157 for p in &delta.fpath_additions {
1159 let pb = PathBuf::from(p);
1160 if !self.fpath.contains(&pb) {
1161 self.fpath.push(pb);
1162 }
1163 }
1164
1165 if !delta.completions.is_empty() {
1167 let mut comps = self.assoc("_comps").unwrap_or_default();
1168 for (cmd, func) in &delta.completions {
1169 comps.insert(cmd.clone(), func.clone());
1170 }
1171 self.set_assoc("_comps".to_string(), comps);
1172 }
1173
1174 for (name, enabled) in &delta.options_changed {
1176 crate::ported::options::opt_state_set(name, *enabled);
1177 }
1178
1179 for (hook, func) in &delta.hooks {
1181 self.add_hook(hook, func);
1182 }
1183
1184 for (name, bytes) in &delta.functions {
1188 let Ok(source) = std::str::from_utf8(bytes) else {
1189 continue;
1190 };
1191 let saved_errflag = errflag.load(Ordering::Relaxed);
1193 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
1194 crate::ported::parse::parse_init(source);
1195 let program = crate::ported::parse::parse();
1196 let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
1197 errflag.store(saved_errflag, Ordering::Relaxed);
1198 if parse_failed || program.lists.is_empty() {
1199 continue;
1200 }
1201 let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
1202 self.functions_compiled.insert(name.clone(), chunk);
1203 self.function_source
1204 .insert(name.clone(), source.to_string());
1205 }
1206 }
1207}
1208