1use crate::parser::ShellCommand;
10use rusqlite::{params, Connection};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Default)]
16pub struct PluginDelta {
17 pub functions: Vec<(String, Vec<u8>)>, pub aliases: Vec<(String, String, AliasKind)>, pub global_aliases: Vec<(String, String)>,
20 pub suffix_aliases: Vec<(String, String)>,
21 pub variables: Vec<(String, String)>,
22 pub exports: Vec<(String, String)>, pub arrays: Vec<(String, Vec<String>)>,
24 pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
25 pub completions: Vec<(String, String)>, pub fpath_additions: Vec<String>,
27 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)>, }
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum AliasKind {
36 Regular,
37 Global,
38 Suffix,
39}
40
41impl AliasKind {
42 fn as_i32(self) -> i32 {
43 match self {
44 AliasKind::Regular => 0,
45 AliasKind::Global => 1,
46 AliasKind::Suffix => 2,
47 }
48 }
49 fn from_i32(v: i32) -> Self {
50 match v {
51 1 => AliasKind::Global,
52 2 => AliasKind::Suffix,
53 _ => AliasKind::Regular,
54 }
55 }
56}
57
58pub struct PluginCache {
60 conn: Connection,
61}
62
63impl PluginCache {
64 pub fn open(path: &Path) -> rusqlite::Result<Self> {
65 let conn = Connection::open(path)?;
66 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
67 let cache = Self { conn };
68 cache.init_schema()?;
69 Ok(cache)
70 }
71
72 fn init_schema(&self) -> rusqlite::Result<()> {
73 self.conn.execute_batch(r#"
74 CREATE TABLE IF NOT EXISTS plugins (
75 id INTEGER PRIMARY KEY,
76 path TEXT NOT NULL UNIQUE,
77 mtime_secs INTEGER NOT NULL,
78 mtime_nsecs INTEGER NOT NULL,
79 source_time_ms INTEGER NOT NULL,
80 cached_at INTEGER NOT NULL
81 );
82
83 CREATE TABLE IF NOT EXISTS plugin_functions (
84 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
85 name TEXT NOT NULL,
86 body BLOB NOT NULL
87 );
88
89 CREATE TABLE IF NOT EXISTS plugin_aliases (
90 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
91 name TEXT NOT NULL,
92 value TEXT NOT NULL,
93 kind INTEGER NOT NULL DEFAULT 0
94 );
95
96 CREATE TABLE IF NOT EXISTS plugin_variables (
97 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
98 name TEXT NOT NULL,
99 value TEXT NOT NULL,
100 is_export INTEGER NOT NULL DEFAULT 0
101 );
102
103 CREATE TABLE IF NOT EXISTS plugin_arrays (
104 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
105 name TEXT NOT NULL,
106 value_json TEXT NOT NULL
107 );
108
109 CREATE TABLE IF NOT EXISTS plugin_completions (
110 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
111 command TEXT NOT NULL,
112 function TEXT NOT NULL
113 );
114
115 CREATE TABLE IF NOT EXISTS plugin_fpath (
116 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
117 path TEXT NOT NULL
118 );
119
120 CREATE TABLE IF NOT EXISTS plugin_hooks (
121 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
122 hook TEXT NOT NULL,
123 function TEXT NOT NULL
124 );
125
126 CREATE TABLE IF NOT EXISTS plugin_bindkeys (
127 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
128 keyseq TEXT NOT NULL,
129 widget TEXT NOT NULL,
130 keymap TEXT NOT NULL DEFAULT 'main'
131 );
132
133 CREATE TABLE IF NOT EXISTS plugin_zstyles (
134 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
135 pattern TEXT NOT NULL,
136 style TEXT NOT NULL,
137 value TEXT NOT NULL
138 );
139
140 CREATE TABLE IF NOT EXISTS plugin_options (
141 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
142 name TEXT NOT NULL,
143 enabled INTEGER NOT NULL
144 );
145
146 CREATE TABLE IF NOT EXISTS plugin_autoloads (
147 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
148 function TEXT NOT NULL,
149 flags TEXT NOT NULL DEFAULT ''
150 );
151
152 -- Full parsed AST cache: skip lex+parse entirely on cache hit
153 CREATE TABLE IF NOT EXISTS script_ast (
154 id INTEGER PRIMARY KEY,
155 path TEXT NOT NULL UNIQUE,
156 mtime_secs INTEGER NOT NULL,
157 mtime_nsecs INTEGER NOT NULL,
158 ast BLOB NOT NULL,
159 cached_at INTEGER NOT NULL
160 );
161
162 -- compaudit cache: security audit results per fpath directory
163 CREATE TABLE IF NOT EXISTS compaudit_cache (
164 id INTEGER PRIMARY KEY,
165 path TEXT NOT NULL UNIQUE,
166 mtime_secs INTEGER NOT NULL,
167 mtime_nsecs INTEGER NOT NULL,
168 uid INTEGER NOT NULL,
169 mode INTEGER NOT NULL,
170 is_secure INTEGER NOT NULL,
171 checked_at INTEGER NOT NULL
172 );
173
174 CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
175 CREATE INDEX IF NOT EXISTS idx_script_ast_path ON script_ast(path);
176 CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
177 "#)?;
178 Ok(())
179 }
180
181 pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
184 self.conn.query_row(
185 "SELECT id FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
186 params![path, mtime_secs, mtime_nsecs],
187 |row| row.get(0),
188 ).ok()
189 }
190
191 pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
193 let mut delta = PluginDelta::default();
194
195 let mut stmt = self.conn.prepare(
197 "SELECT name, body FROM plugin_functions WHERE plugin_id = ?1"
198 )?;
199 let rows = stmt.query_map(params![plugin_id], |row| {
200 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
201 })?;
202 for r in rows { delta.functions.push(r?); }
203
204 let mut stmt = self.conn.prepare(
206 "SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1"
207 )?;
208 let rows = stmt.query_map(params![plugin_id], |row| {
209 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, AliasKind::from_i32(row.get::<_, i32>(2)?)))
210 })?;
211 for r in rows { delta.aliases.push(r?); }
212
213 let mut stmt = self.conn.prepare(
215 "SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1"
216 )?;
217 let rows = stmt.query_map(params![plugin_id], |row| {
218 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, bool>(2)?))
219 })?;
220 for r in rows {
221 let (name, value, is_export) = r?;
222 if is_export {
223 delta.exports.push((name, value));
224 } else {
225 delta.variables.push((name, value));
226 }
227 }
228
229 let mut stmt = self.conn.prepare(
231 "SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1"
232 )?;
233 let rows = stmt.query_map(params![plugin_id], |row| {
234 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
235 })?;
236 for r in rows {
237 let (name, json) = r?;
238 let vals: Vec<String> = json.trim_matches(|c| c == '[' || c == ']')
240 .split(',')
241 .map(|s| s.trim().trim_matches('"').to_string())
242 .filter(|s| !s.is_empty())
243 .collect();
244 delta.arrays.push((name, vals));
245 }
246
247 let mut stmt = self.conn.prepare(
249 "SELECT command, function FROM plugin_completions WHERE plugin_id = ?1"
250 )?;
251 let rows = stmt.query_map(params![plugin_id], |row| {
252 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
253 })?;
254 for r in rows { delta.completions.push(r?); }
255
256 let mut stmt = self.conn.prepare(
258 "SELECT path FROM plugin_fpath WHERE plugin_id = ?1"
259 )?;
260 let rows = stmt.query_map(params![plugin_id], |row| {
261 row.get::<_, String>(0)
262 })?;
263 for r in rows { delta.fpath_additions.push(r?); }
264
265 let mut stmt = self.conn.prepare(
267 "SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1"
268 )?;
269 let rows = stmt.query_map(params![plugin_id], |row| {
270 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
271 })?;
272 for r in rows { delta.hooks.push(r?); }
273
274 let mut stmt = self.conn.prepare(
276 "SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1"
277 )?;
278 let rows = stmt.query_map(params![plugin_id], |row| {
279 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?))
280 })?;
281 for r in rows { delta.bindkeys.push(r?); }
282
283 let mut stmt = self.conn.prepare(
285 "SELECT pattern, style, value FROM plugin_zstyles WHERE plugin_id = ?1"
286 )?;
287 let rows = stmt.query_map(params![plugin_id], |row| {
288 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?))
289 })?;
290 for r in rows { delta.zstyles.push(r?); }
291
292 let mut stmt = self.conn.prepare(
294 "SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1"
295 )?;
296 let rows = stmt.query_map(params![plugin_id], |row| {
297 Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
298 })?;
299 for r in rows { delta.options_changed.push(r?); }
300
301 let mut stmt = self.conn.prepare(
303 "SELECT function, flags FROM plugin_autoloads WHERE plugin_id = ?1"
304 )?;
305 let rows = stmt.query_map(params![plugin_id], |row| {
306 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
307 })?;
308 for r in rows { delta.autoloads.push(r?); }
309
310 Ok(delta)
311 }
312
313 pub fn store(
315 &self,
316 path: &str,
317 mtime_secs: i64,
318 mtime_nsecs: i64,
319 source_time_ms: u64,
320 delta: &PluginDelta,
321 ) -> rusqlite::Result<()> {
322 let now = std::time::SystemTime::now()
323 .duration_since(std::time::UNIX_EPOCH)
324 .map(|d| d.as_secs() as i64)
325 .unwrap_or(0);
326
327 self.conn.execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
329
330 self.conn.execute(
331 "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at) VALUES (?1, ?2, ?3, ?4, ?5)",
332 params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now],
333 )?;
334 let plugin_id = self.conn.last_insert_rowid();
335
336 for (name, body) in &delta.functions {
338 self.conn.execute(
339 "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
340 params![plugin_id, name, body],
341 )?;
342 }
343
344 for (name, value, kind) in &delta.aliases {
346 self.conn.execute(
347 "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
348 params![plugin_id, name, value, kind.as_i32()],
349 )?;
350 }
351
352 for (name, value) in &delta.variables {
354 self.conn.execute(
355 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
356 params![plugin_id, name, value],
357 )?;
358 }
359 for (name, value) in &delta.exports {
360 self.conn.execute(
361 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
362 params![plugin_id, name, value],
363 )?;
364 }
365
366 for (name, vals) in &delta.arrays {
368 let json = format!("[{}]", vals.iter().map(|v| format!("\"{}\"", v.replace('"', "\\\""))).collect::<Vec<_>>().join(","));
369 self.conn.execute(
370 "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
371 params![plugin_id, name, json],
372 )?;
373 }
374
375 for (cmd, func) in &delta.completions {
377 self.conn.execute(
378 "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
379 params![plugin_id, cmd, func],
380 )?;
381 }
382
383 for p in &delta.fpath_additions {
385 self.conn.execute(
386 "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
387 params![plugin_id, p],
388 )?;
389 }
390
391 for (hook, func) in &delta.hooks {
393 self.conn.execute(
394 "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
395 params![plugin_id, hook, func],
396 )?;
397 }
398
399 for (keyseq, widget, keymap) in &delta.bindkeys {
401 self.conn.execute(
402 "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
403 params![plugin_id, keyseq, widget, keymap],
404 )?;
405 }
406
407 for (pattern, style, value) in &delta.zstyles {
409 self.conn.execute(
410 "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
411 params![plugin_id, pattern, style, value],
412 )?;
413 }
414
415 for (name, enabled) in &delta.options_changed {
417 self.conn.execute(
418 "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
419 params![plugin_id, name, *enabled],
420 )?;
421 }
422
423 for (func, flags) in &delta.autoloads {
425 self.conn.execute(
426 "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
427 params![plugin_id, func, flags],
428 )?;
429 }
430
431 Ok(())
432 }
433
434 pub fn stats(&self) -> (i64, i64) {
436 let plugins: i64 = self.conn.query_row(
437 "SELECT COUNT(*) FROM plugins", [], |r| r.get(0)
438 ).unwrap_or(0);
439 let functions: i64 = self.conn.query_row(
440 "SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0)
441 ).unwrap_or(0);
442 (plugins, functions)
443 }
444
445 pub fn count_stale(&self) -> usize {
447 let mut stmt = match self.conn.prepare(
448 "SELECT path, mtime_secs, mtime_nsecs FROM plugins"
449 ) {
450 Ok(s) => s,
451 Err(_) => return 0,
452 };
453 let rows = match stmt.query_map([], |row| {
454 Ok((
455 row.get::<_, String>(0)?,
456 row.get::<_, i64>(1)?,
457 row.get::<_, i64>(2)?,
458 ))
459 }) {
460 Ok(r) => r,
461 Err(_) => return 0,
462 };
463 let mut count = 0;
464 for row in rows {
465 if let Ok((path, cached_s, cached_ns)) = row {
466 match file_mtime(std::path::Path::new(&path)) {
467 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
468 None => count += 1, _ => {}
470 }
471 }
472 }
473 count
474 }
475
476 pub fn count_stale_ast(&self) -> usize {
478 let mut stmt = match self.conn.prepare(
479 "SELECT path, mtime_secs, mtime_nsecs FROM script_ast"
480 ) {
481 Ok(s) => s,
482 Err(_) => return 0,
483 };
484 let rows = match stmt.query_map([], |row| {
485 Ok((
486 row.get::<_, String>(0)?,
487 row.get::<_, i64>(1)?,
488 row.get::<_, i64>(2)?,
489 ))
490 }) {
491 Ok(r) => r,
492 Err(_) => return 0,
493 };
494 let mut count = 0;
495 for row in rows {
496 if let Ok((path, cached_s, cached_ns)) = row {
497 match file_mtime(std::path::Path::new(&path)) {
498 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
499 None => count += 1,
500 _ => {}
501 }
502 }
503 }
504 count
505 }
506
507 pub fn check_ast(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<Vec<u8>> {
513 self.conn.query_row(
514 "SELECT ast FROM script_ast WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
515 params![path, mtime_secs, mtime_nsecs],
516 |row| row.get::<_, Vec<u8>>(0),
517 ).ok()
518 }
519
520 pub fn store_ast(
522 &self,
523 path: &str,
524 mtime_secs: i64,
525 mtime_nsecs: i64,
526 ast_bytes: &[u8],
527 ) -> rusqlite::Result<()> {
528 let now = std::time::SystemTime::now()
529 .duration_since(std::time::UNIX_EPOCH)
530 .map(|d| d.as_secs() as i64)
531 .unwrap_or(0);
532
533 self.conn.execute("DELETE FROM script_ast WHERE path = ?1", params![path])?;
534 self.conn.execute(
535 "INSERT INTO script_ast (path, mtime_secs, mtime_nsecs, ast, cached_at) VALUES (?1, ?2, ?3, ?4, ?5)",
536 params![path, mtime_secs, mtime_nsecs, ast_bytes, now],
537 )?;
538 Ok(())
539 }
540
541 pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
548 self.conn.query_row(
549 "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
550 params![dir, mtime_secs, mtime_nsecs],
551 |row| row.get::<_, bool>(0),
552 ).ok()
553 }
554
555 pub fn store_compaudit(
557 &self,
558 dir: &str,
559 mtime_secs: i64,
560 mtime_nsecs: i64,
561 uid: u32,
562 mode: u32,
563 is_secure: bool,
564 ) -> rusqlite::Result<()> {
565 let now = std::time::SystemTime::now()
566 .duration_since(std::time::UNIX_EPOCH)
567 .map(|d| d.as_secs() as i64)
568 .unwrap_or(0);
569
570 self.conn.execute(
571 "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)",
572 params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
573 )?;
574 Ok(())
575 }
576
577 pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
580 use std::os::unix::fs::MetadataExt;
581
582 let euid = unsafe { libc::geteuid() };
583 let mut insecure = Vec::new();
584
585 for dir in fpath {
586 let dir_str = dir.to_string_lossy().to_string();
587 let meta = match std::fs::metadata(dir) {
588 Ok(m) => m,
589 Err(_) => continue, };
591 let mt_s = meta.mtime();
592 let mt_ns = meta.mtime_nsec();
593
594 if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
596 if !is_secure {
597 insecure.push(dir_str);
598 }
599 continue;
600 }
601
602 let mode = meta.mode();
604 let uid = meta.uid();
605 let is_secure = Self::check_dir_security(&meta, euid);
606
607 let parent_secure = dir.parent()
609 .and_then(|p| std::fs::metadata(p).ok())
610 .map(|pm| Self::check_dir_security(&pm, euid))
611 .unwrap_or(true);
612
613 let secure = is_secure && parent_secure;
614
615 let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
617
618 if !secure {
619 insecure.push(dir_str);
620 }
621 }
622
623 if insecure.is_empty() {
624 tracing::debug!(dirs = fpath.len(), "compaudit: all directories secure (cached)");
625 } else {
626 tracing::warn!(
627 insecure_count = insecure.len(),
628 dirs = fpath.len(),
629 "compaudit: insecure directories found"
630 );
631 }
632
633 insecure
634 }
635
636 fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
639 use std::os::unix::fs::MetadataExt;
640 let mode = meta.mode();
641 let uid = meta.uid();
642
643 if uid == 0 || uid == euid {
645 return true;
646 }
647
648 let group_writable = mode & 0o020 != 0;
650 let world_writable = mode & 0o002 != 0;
651
652 !group_writable && !world_writable
653 }
654}
655
656pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
658 use std::os::unix::fs::MetadataExt;
659 let meta = std::fs::metadata(path).ok()?;
660 Some((meta.mtime(), meta.mtime_nsec()))
661}
662
663pub fn default_cache_path() -> PathBuf {
665 dirs::home_dir()
666 .unwrap_or_else(|| PathBuf::from("/tmp"))
667 .join(".cache/zshrs/plugins.db")
668}