1use rusqlite::{params, Connection};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13pub const BYTECODE_VERSION: u8 = 1;
27
28#[inline]
32fn wrap_bytecode(bytes: &[u8]) -> Vec<u8> {
33 let mut out = Vec::with_capacity(bytes.len() + 1);
34 out.push(BYTECODE_VERSION);
35 out.extend_from_slice(bytes);
36 out
37}
38
39#[inline]
44fn unwrap_bytecode(bytes: &[u8]) -> Option<Vec<u8>> {
45 if bytes.is_empty() || bytes[0] != BYTECODE_VERSION {
46 return None;
47 }
48 Some(bytes[1..].to_vec())
49}
50
51#[derive(Debug, Clone, Default)]
53pub struct PluginDelta {
54 pub functions: Vec<(String, Vec<u8>)>, pub aliases: Vec<(String, String, AliasKind)>, pub global_aliases: Vec<(String, String)>,
57 pub suffix_aliases: Vec<(String, String)>,
58 pub variables: Vec<(String, String)>,
59 pub exports: Vec<(String, String)>, pub arrays: Vec<(String, Vec<String>)>,
61 pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
62 pub completions: Vec<(String, String)>, pub fpath_additions: Vec<String>,
64 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)>, }
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum AliasKind {
73 Regular,
74 Global,
75 Suffix,
76}
77
78impl AliasKind {
79 fn as_i32(self) -> i32 {
80 match self {
81 AliasKind::Regular => 0,
82 AliasKind::Global => 1,
83 AliasKind::Suffix => 2,
84 }
85 }
86 fn from_i32(v: i32) -> Self {
87 match v {
88 1 => AliasKind::Global,
89 2 => AliasKind::Suffix,
90 _ => AliasKind::Regular,
91 }
92 }
93}
94
95pub struct PluginCache {
97 conn: Connection,
98}
99
100impl PluginCache {
101 pub fn open(path: &Path) -> rusqlite::Result<Self> {
102 let conn = Connection::open(path)?;
103 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
104 let cache = Self { conn };
105 cache.init_schema()?;
106 Ok(cache)
107 }
108
109 fn init_schema(&self) -> rusqlite::Result<()> {
110 self.conn.execute_batch(
111 r#"
112 CREATE TABLE IF NOT EXISTS plugins (
113 id INTEGER PRIMARY KEY,
114 path TEXT NOT NULL UNIQUE,
115 mtime_secs INTEGER NOT NULL,
116 mtime_nsecs INTEGER NOT NULL,
117 source_time_ms INTEGER NOT NULL,
118 cached_at INTEGER NOT NULL
119 );
120
121 CREATE TABLE IF NOT EXISTS plugin_functions (
122 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
123 name TEXT NOT NULL,
124 body BLOB NOT NULL
125 );
126
127 CREATE TABLE IF NOT EXISTS plugin_aliases (
128 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
129 name TEXT NOT NULL,
130 value TEXT NOT NULL,
131 kind INTEGER NOT NULL DEFAULT 0
132 );
133
134 CREATE TABLE IF NOT EXISTS plugin_variables (
135 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
136 name TEXT NOT NULL,
137 value TEXT NOT NULL,
138 is_export INTEGER NOT NULL DEFAULT 0
139 );
140
141 CREATE TABLE IF NOT EXISTS plugin_arrays (
142 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
143 name TEXT NOT NULL,
144 value_json TEXT NOT NULL
145 );
146
147 CREATE TABLE IF NOT EXISTS plugin_completions (
148 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
149 command TEXT NOT NULL,
150 function TEXT NOT NULL
151 );
152
153 CREATE TABLE IF NOT EXISTS plugin_fpath (
154 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
155 path TEXT NOT NULL
156 );
157
158 CREATE TABLE IF NOT EXISTS plugin_hooks (
159 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
160 hook TEXT NOT NULL,
161 function TEXT NOT NULL
162 );
163
164 CREATE TABLE IF NOT EXISTS plugin_bindkeys (
165 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
166 keyseq TEXT NOT NULL,
167 widget TEXT NOT NULL,
168 keymap TEXT NOT NULL DEFAULT 'main'
169 );
170
171 CREATE TABLE IF NOT EXISTS plugin_zstyles (
172 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
173 pattern TEXT NOT NULL,
174 style TEXT NOT NULL,
175 value TEXT NOT NULL
176 );
177
178 CREATE TABLE IF NOT EXISTS plugin_options (
179 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
180 name TEXT NOT NULL,
181 enabled INTEGER NOT NULL
182 );
183
184 CREATE TABLE IF NOT EXISTS plugin_autoloads (
185 plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
186 function TEXT NOT NULL,
187 flags TEXT NOT NULL DEFAULT ''
188 );
189
190 -- Bytecode cache: skip lex+parse+compile entirely on cache hit
191 CREATE TABLE IF NOT EXISTS script_bytecode (
192 id INTEGER PRIMARY KEY,
193 path TEXT NOT NULL UNIQUE,
194 mtime_secs INTEGER NOT NULL,
195 mtime_nsecs INTEGER NOT NULL,
196 bytecode BLOB NOT NULL,
197 cached_at INTEGER NOT NULL
198 );
199
200 -- compaudit cache: security audit results per fpath directory
201 CREATE TABLE IF NOT EXISTS compaudit_cache (
202 id INTEGER PRIMARY KEY,
203 path TEXT NOT NULL UNIQUE,
204 mtime_secs INTEGER NOT NULL,
205 mtime_nsecs INTEGER NOT NULL,
206 uid INTEGER NOT NULL,
207 mode INTEGER NOT NULL,
208 is_secure INTEGER NOT NULL,
209 checked_at INTEGER NOT NULL
210 );
211
212 CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
213 CREATE INDEX IF NOT EXISTS idx_script_bytecode_path ON script_bytecode(path);
214 CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
215 "#,
216 )?;
217 Ok(())
218 }
219
220 pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
223 self.conn
224 .query_row(
225 "SELECT id FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
226 params![path, mtime_secs, mtime_nsecs],
227 |row| row.get(0),
228 )
229 .ok()
230 }
231
232 pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
234 let mut delta = PluginDelta::default();
235
236 let mut stmt = self
238 .conn
239 .prepare("SELECT name, body FROM plugin_functions WHERE plugin_id = ?1")?;
240 let rows = stmt.query_map(params![plugin_id], |row| {
241 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
242 })?;
243 for r in rows {
244 delta.functions.push(r?);
245 }
246
247 let mut stmt = self
249 .conn
250 .prepare("SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1")?;
251 let rows = stmt.query_map(params![plugin_id], |row| {
252 Ok((
253 row.get::<_, String>(0)?,
254 row.get::<_, String>(1)?,
255 AliasKind::from_i32(row.get::<_, i32>(2)?),
256 ))
257 })?;
258 for r in rows {
259 delta.aliases.push(r?);
260 }
261
262 let mut stmt = self
264 .conn
265 .prepare("SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1")?;
266 let rows = stmt.query_map(params![plugin_id], |row| {
267 Ok((
268 row.get::<_, String>(0)?,
269 row.get::<_, String>(1)?,
270 row.get::<_, bool>(2)?,
271 ))
272 })?;
273 for r in rows {
274 let (name, value, is_export) = r?;
275 if is_export {
276 delta.exports.push((name, value));
277 } else {
278 delta.variables.push((name, value));
279 }
280 }
281
282 let mut stmt = self
284 .conn
285 .prepare("SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1")?;
286 let rows = stmt.query_map(params![plugin_id], |row| {
287 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
288 })?;
289 for r in rows {
290 let (name, json) = r?;
291 let vals: Vec<String> = json
293 .trim_matches(|c| c == '[' || c == ']')
294 .split(',')
295 .map(|s| s.trim().trim_matches('"').to_string())
296 .filter(|s| !s.is_empty())
297 .collect();
298 delta.arrays.push((name, vals));
299 }
300
301 let mut stmt = self
303 .conn
304 .prepare("SELECT command, function FROM plugin_completions WHERE plugin_id = ?1")?;
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 {
309 delta.completions.push(r?);
310 }
311
312 let mut stmt = self
314 .conn
315 .prepare("SELECT path FROM plugin_fpath WHERE plugin_id = ?1")?;
316 let rows = stmt.query_map(params![plugin_id], |row| row.get::<_, String>(0))?;
317 for r in rows {
318 delta.fpath_additions.push(r?);
319 }
320
321 let mut stmt = self
323 .conn
324 .prepare("SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1")?;
325 let rows = stmt.query_map(params![plugin_id], |row| {
326 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
327 })?;
328 for r in rows {
329 delta.hooks.push(r?);
330 }
331
332 let mut stmt = self
334 .conn
335 .prepare("SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1")?;
336 let rows = stmt.query_map(params![plugin_id], |row| {
337 Ok((
338 row.get::<_, String>(0)?,
339 row.get::<_, String>(1)?,
340 row.get::<_, String>(2)?,
341 ))
342 })?;
343 for r in rows {
344 delta.bindkeys.push(r?);
345 }
346
347 let mut stmt = self
349 .conn
350 .prepare("SELECT pattern, style, value FROM plugin_zstyles 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 row.get::<_, String>(2)?,
356 ))
357 })?;
358 for r in rows {
359 delta.zstyles.push(r?);
360 }
361
362 let mut stmt = self
364 .conn
365 .prepare("SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1")?;
366 let rows = stmt.query_map(params![plugin_id], |row| {
367 Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
368 })?;
369 for r in rows {
370 delta.options_changed.push(r?);
371 }
372
373 let mut stmt = self
375 .conn
376 .prepare("SELECT function, flags FROM plugin_autoloads 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.autoloads.push(r?);
382 }
383
384 Ok(delta)
385 }
386
387 pub fn store(
389 &self,
390 path: &str,
391 mtime_secs: i64,
392 mtime_nsecs: i64,
393 source_time_ms: u64,
394 delta: &PluginDelta,
395 ) -> rusqlite::Result<()> {
396 let now = std::time::SystemTime::now()
397 .duration_since(std::time::UNIX_EPOCH)
398 .map(|d| d.as_secs() as i64)
399 .unwrap_or(0);
400
401 self.conn
403 .execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
404
405 self.conn.execute(
406 "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at) VALUES (?1, ?2, ?3, ?4, ?5)",
407 params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now],
408 )?;
409 let plugin_id = self.conn.last_insert_rowid();
410
411 for (name, body) in &delta.functions {
413 self.conn.execute(
414 "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
415 params![plugin_id, name, body],
416 )?;
417 }
418
419 for (name, value, kind) in &delta.aliases {
421 self.conn.execute(
422 "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
423 params![plugin_id, name, value, kind.as_i32()],
424 )?;
425 }
426
427 for (name, value) in &delta.variables {
429 self.conn.execute(
430 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
431 params![plugin_id, name, value],
432 )?;
433 }
434 for (name, value) in &delta.exports {
435 self.conn.execute(
436 "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
437 params![plugin_id, name, value],
438 )?;
439 }
440
441 for (name, vals) in &delta.arrays {
443 let json = format!(
444 "[{}]",
445 vals.iter()
446 .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
447 .collect::<Vec<_>>()
448 .join(",")
449 );
450 self.conn.execute(
451 "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
452 params![plugin_id, name, json],
453 )?;
454 }
455
456 for (cmd, func) in &delta.completions {
458 self.conn.execute(
459 "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
460 params![plugin_id, cmd, func],
461 )?;
462 }
463
464 for p in &delta.fpath_additions {
466 self.conn.execute(
467 "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
468 params![plugin_id, p],
469 )?;
470 }
471
472 for (hook, func) in &delta.hooks {
474 self.conn.execute(
475 "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
476 params![plugin_id, hook, func],
477 )?;
478 }
479
480 for (keyseq, widget, keymap) in &delta.bindkeys {
482 self.conn.execute(
483 "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
484 params![plugin_id, keyseq, widget, keymap],
485 )?;
486 }
487
488 for (pattern, style, value) in &delta.zstyles {
490 self.conn.execute(
491 "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
492 params![plugin_id, pattern, style, value],
493 )?;
494 }
495
496 for (name, enabled) in &delta.options_changed {
498 self.conn.execute(
499 "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
500 params![plugin_id, name, *enabled],
501 )?;
502 }
503
504 for (func, flags) in &delta.autoloads {
506 self.conn.execute(
507 "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
508 params![plugin_id, func, flags],
509 )?;
510 }
511
512 Ok(())
513 }
514
515 pub fn stats(&self) -> (i64, i64) {
517 let plugins: i64 = self
518 .conn
519 .query_row("SELECT COUNT(*) FROM plugins", [], |r| r.get(0))
520 .unwrap_or(0);
521 let functions: i64 = self
522 .conn
523 .query_row("SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0))
524 .unwrap_or(0);
525 (plugins, functions)
526 }
527
528 pub fn count_stale(&self) -> usize {
530 let mut stmt = match self
531 .conn
532 .prepare("SELECT path, mtime_secs, mtime_nsecs FROM plugins")
533 {
534 Ok(s) => s,
535 Err(_) => return 0,
536 };
537 let rows = match stmt.query_map([], |row| {
538 Ok((
539 row.get::<_, String>(0)?,
540 row.get::<_, i64>(1)?,
541 row.get::<_, i64>(2)?,
542 ))
543 }) {
544 Ok(r) => r,
545 Err(_) => return 0,
546 };
547 let mut count = 0;
548 for row in rows {
549 if let Ok((path, cached_s, cached_ns)) = row {
550 match file_mtime(std::path::Path::new(&path)) {
551 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
552 None => count += 1, _ => {}
554 }
555 }
556 }
557 count
558 }
559
560 pub fn count_stale_bytecode(&self) -> usize {
562 let mut stmt = match self
563 .conn
564 .prepare("SELECT path, mtime_secs, mtime_nsecs FROM script_bytecode")
565 {
566 Ok(s) => s,
567 Err(_) => return 0,
568 };
569 let rows = match stmt.query_map([], |row| {
570 Ok((
571 row.get::<_, String>(0)?,
572 row.get::<_, i64>(1)?,
573 row.get::<_, i64>(2)?,
574 ))
575 }) {
576 Ok(r) => r,
577 Err(_) => return 0,
578 };
579 let mut count = 0;
580 for (path, cached_s, cached_ns) in rows.flatten() {
581 match file_mtime(std::path::Path::new(&path)) {
582 Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
583 None => count += 1,
584 _ => {}
585 }
586 }
587 count
588 }
589
590 pub fn check_bytecode(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<Vec<u8>> {
599 let raw = self.conn.query_row(
600 "SELECT bytecode FROM script_bytecode WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
601 params![path, mtime_secs, mtime_nsecs],
602 |row| row.get::<_, Vec<u8>>(0),
603 ).ok()?;
604 unwrap_bytecode(&raw)
605 }
606
607 pub fn store_bytecode(
611 &self,
612 path: &str,
613 mtime_secs: i64,
614 mtime_nsecs: i64,
615 bytecode: &[u8],
616 ) -> rusqlite::Result<()> {
617 let now = std::time::SystemTime::now()
618 .duration_since(std::time::UNIX_EPOCH)
619 .map(|d| d.as_secs() as i64)
620 .unwrap_or(0);
621
622 let wrapped = wrap_bytecode(bytecode);
623 self.conn
624 .execute("DELETE FROM script_bytecode WHERE path = ?1", params![path])?;
625 self.conn.execute(
626 "INSERT INTO script_bytecode (path, mtime_secs, mtime_nsecs, bytecode, cached_at) VALUES (?1, ?2, ?3, ?4, ?5)",
627 params![path, mtime_secs, mtime_nsecs, wrapped, now],
628 )?;
629 Ok(())
630 }
631
632 pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
639 self.conn.query_row(
640 "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
641 params![dir, mtime_secs, mtime_nsecs],
642 |row| row.get::<_, bool>(0),
643 ).ok()
644 }
645
646 pub fn store_compaudit(
648 &self,
649 dir: &str,
650 mtime_secs: i64,
651 mtime_nsecs: i64,
652 uid: u32,
653 mode: u32,
654 is_secure: bool,
655 ) -> rusqlite::Result<()> {
656 let now = std::time::SystemTime::now()
657 .duration_since(std::time::UNIX_EPOCH)
658 .map(|d| d.as_secs() as i64)
659 .unwrap_or(0);
660
661 self.conn.execute(
662 "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)",
663 params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
664 )?;
665 Ok(())
666 }
667
668 pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
671 use std::os::unix::fs::MetadataExt;
672
673 let euid = unsafe { libc::geteuid() };
674 let mut insecure = Vec::new();
675
676 for dir in fpath {
677 let dir_str = dir.to_string_lossy().to_string();
678 let meta = match std::fs::metadata(dir) {
679 Ok(m) => m,
680 Err(_) => continue, };
682 let mt_s = meta.mtime();
683 let mt_ns = meta.mtime_nsec();
684
685 if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
687 if !is_secure {
688 insecure.push(dir_str);
689 }
690 continue;
691 }
692
693 let mode = meta.mode();
695 let uid = meta.uid();
696 let is_secure = Self::check_dir_security(&meta, euid);
697
698 let parent_secure = dir
700 .parent()
701 .and_then(|p| std::fs::metadata(p).ok())
702 .map(|pm| Self::check_dir_security(&pm, euid))
703 .unwrap_or(true);
704
705 let secure = is_secure && parent_secure;
706
707 let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
709
710 if !secure {
711 insecure.push(dir_str);
712 }
713 }
714
715 if insecure.is_empty() {
716 tracing::debug!(
717 dirs = fpath.len(),
718 "compaudit: all directories secure (cached)"
719 );
720 } else {
721 tracing::warn!(
722 insecure_count = insecure.len(),
723 dirs = fpath.len(),
724 "compaudit: insecure directories found"
725 );
726 }
727
728 insecure
729 }
730
731 fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
734 use std::os::unix::fs::MetadataExt;
735 let mode = meta.mode();
736 let uid = meta.uid();
737
738 if uid == 0 || uid == euid {
740 return true;
741 }
742
743 let group_writable = mode & 0o020 != 0;
745 let world_writable = mode & 0o002 != 0;
746
747 !group_writable && !world_writable
748 }
749}
750
751pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
753 use std::os::unix::fs::MetadataExt;
754 let meta = std::fs::metadata(path).ok()?;
755 Some((meta.mtime(), meta.mtime_nsec()))
756}
757
758pub fn default_cache_path() -> PathBuf {
760 dirs::home_dir()
761 .unwrap_or_else(|| PathBuf::from("/tmp"))
762 .join(".cache/zshrs/plugins.db")
763}
764
765#[cfg(test)]
766mod version_tests {
767 use super::*;
768
769 #[test]
770 fn wrap_unwrap_round_trip() {
771 let raw = b"some-bincode-blob".to_vec();
772 let wrapped = wrap_bytecode(&raw);
773 assert_eq!(wrapped[0], BYTECODE_VERSION);
774 let unwrapped = unwrap_bytecode(&wrapped).expect("matching version unwraps");
775 assert_eq!(unwrapped, raw);
776 }
777
778 #[test]
779 fn unwrap_rejects_old_version() {
780 let mut bogus = vec![0u8]; bogus.extend_from_slice(b"old-bincode-blob");
784 assert!(unwrap_bytecode(&bogus).is_none());
785
786 let mut future = vec![BYTECODE_VERSION.wrapping_add(1)];
787 future.extend_from_slice(b"future-bincode-blob");
788 assert!(unwrap_bytecode(&future).is_none());
789 }
790
791 #[test]
792 fn unwrap_rejects_empty_blob() {
793 assert!(unwrap_bytecode(&[]).is_none());
794 }
795
796 #[test]
797 fn store_then_check_round_trips_through_sqlite() {
798 let tmp = tempfile::tempdir().unwrap();
802 let db_path = tmp.path().join("test_cache.db");
803 let cache = PluginCache::open(&db_path).expect("open temp cache");
804
805 let path = "/fake/script.zsh";
806 let blob = b"bincode-bytes-here".to_vec();
807 cache
808 .store_bytecode(path, 12345, 6789, &blob)
809 .expect("store");
810 let got = cache.check_bytecode(path, 12345, 6789).expect("hit");
811 assert_eq!(got, blob);
812 }
813
814 #[test]
815 fn manually_inserted_old_version_invalidates() {
816 let tmp = tempfile::tempdir().unwrap();
820 let db_path = tmp.path().join("legacy_cache.db");
821 let cache = PluginCache::open(&db_path).expect("open temp cache");
822
823 let mut legacy = vec![0u8]; legacy.extend_from_slice(b"would-be-bincode");
825 cache
826 .conn
827 .execute(
828 "INSERT INTO script_bytecode (path, mtime_secs, mtime_nsecs, bytecode, cached_at) VALUES (?1, ?2, ?3, ?4, ?5)",
829 params!["/fake/legacy.zsh", 0i64, 0i64, legacy, 0i64],
830 )
831 .unwrap();
832
833 let result = cache.check_bytecode("/fake/legacy.zsh", 0, 0);
834 assert!(result.is_none(), "legacy bytecode must invalidate");
835 }
836}