1use rusqlite::{params, Connection, OptionalExtension};
22use std::collections::HashMap;
23use std::path::{Path, PathBuf};
24
25pub struct CompsysCache {
27 conn: Connection,
29}
30
31pub fn default_cache_path() -> PathBuf {
35 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
36 PathBuf::from(custom)
37 } else {
38 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
39 PathBuf::from(home).join(".zshrs")
40 };
41 root.join("compsys.db")
42}
43
44pub fn cache_lock_path() -> PathBuf {
51 let db = default_cache_path();
52 db.with_file_name(format!(
53 "{}.lock",
54 db.file_name()
55 .map(|s| s.to_string_lossy().into_owned())
56 .unwrap_or_else(|| "compsys.db".to_string())
57 ))
58}
59
60pub fn acquire_rebuild_lock() -> Option<nix::fcntl::Flock<std::fs::File>> {
72 let path = cache_lock_path();
73 if let Some(parent) = path.parent() {
74 let _ = std::fs::create_dir_all(parent);
75 }
76 let f = std::fs::File::options()
77 .read(true)
78 .write(true)
79 .create(true)
80 .truncate(false)
81 .open(&path)
82 .ok()?;
83 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
84}
85
86pub fn current_binary_identity() -> Option<(i64, i64, u64)> {
101 use std::os::unix::fs::MetadataExt;
102 static BIN_ID: std::sync::OnceLock<Option<(i64, i64, u64)>> = std::sync::OnceLock::new();
103 *BIN_ID.get_or_init(|| {
104 let exe = std::env::current_exe().ok()?;
105 let meta = std::fs::metadata(&exe).ok()?;
106 Some((meta.mtime(), meta.mtime_nsec(), meta.len()))
107 })
108}
109
110pub fn binary_identity_stamp() -> Option<String> {
115 current_binary_identity().map(|(secs, nsecs, len)| format!("{}.{}.{}", secs, nsecs, len))
116}
117
118impl CompsysCache {
119 pub fn conn(&self) -> &Connection {
121 &self.conn
122 }
123
124 pub fn count_table(&self, table: &str) -> rusqlite::Result<usize> {
126 let sql = format!("SELECT COUNT(*) FROM {}", table);
128 self.conn
129 .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
130 }
131
132 pub fn count_table_where(&self, table: &str, condition: &str) -> rusqlite::Result<usize> {
134 let sql = format!("SELECT COUNT(*) FROM {} WHERE {}", table, condition);
135 self.conn
136 .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
137 }
138
139 pub fn open(path: impl AsRef<Path>) -> rusqlite::Result<Self> {
141 let _lowfd = crate::lowfd::LowFdGuard::new();
145 let conn = Connection::open(path)?;
146 crate::lowfd::register_internal_fds(); let cache = Self { conn };
148 cache.configure_for_speed()?;
149 cache.init_schema()?;
150 Ok(cache)
151 }
152
153 pub fn memory() -> rusqlite::Result<Self> {
155 let conn = Connection::open_in_memory()?;
156 let cache = Self { conn };
157 cache.configure_for_speed()?;
158 cache.init_schema()?;
159 Ok(cache)
160 }
161
162 fn configure_for_speed(&self) -> rusqlite::Result<()> {
164 self.conn.execute_batch(
166 r#"
167 PRAGMA journal_mode = WAL;
168 PRAGMA synchronous = NORMAL;
169 PRAGMA cache_size = -64000;
170 PRAGMA mmap_size = 268435456;
171 PRAGMA temp_store = MEMORY;
172 "#,
173 )
174 }
175
176 fn init_schema(&self) -> rusqlite::Result<()> {
177 let has_legacy_bytecode_col = {
192 let exists: i64 = self.conn.query_row(
193 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='autoloads'",
194 [],
195 |row| row.get(0),
196 )?;
197 if exists == 0 {
198 false
199 } else {
200 let mut stmt = self.conn.prepare("PRAGMA table_info(autoloads)")?;
201 let cols: Vec<String> = stmt
202 .query_map([], |row| row.get::<_, String>(1))?
203 .collect::<rusqlite::Result<_>>()?;
204 cols.iter().any(|c| c == "bytecode")
205 }
206 };
207 if has_legacy_bytecode_col {
208 self.conn.execute_batch(
209 r#"
210 BEGIN;
211 CREATE TABLE autoloads_new (
212 name TEXT PRIMARY KEY,
213 source TEXT NOT NULL,
214 offset INTEGER NOT NULL,
215 size INTEGER NOT NULL,
216 body TEXT
217 ) WITHOUT ROWID;
218 INSERT OR IGNORE INTO autoloads_new (name, source, offset, size, body)
219 SELECT name, source, offset, size, body FROM autoloads;
220 DROP TABLE autoloads;
221 ALTER TABLE autoloads_new RENAME TO autoloads;
222 COMMIT;
223 "#,
224 )?;
225 }
226 self.conn.execute_batch(
227 r#"
228 -- Autoloads: flat table, PRIMARY KEY = clustered index
229 -- body stores actual function definition - NO filesystem access on autoload -Xz
230 -- compinit reads from .zwc or plain files ONCE, stores body here.
231 -- bytecode is held separately in the rkyv autoload-cache shard.
232 CREATE TABLE IF NOT EXISTS autoloads (
233 name TEXT PRIMARY KEY,
234 source TEXT NOT NULL,
235 offset INTEGER NOT NULL,
236 size INTEGER NOT NULL,
237 body TEXT
238 ) WITHOUT ROWID;
239
240 -- zstyle: flat lookup by pattern+style
241 CREATE TABLE IF NOT EXISTS zstyles (
242 pattern TEXT NOT NULL,
243 style TEXT NOT NULL,
244 value TEXT NOT NULL,
245 eval INTEGER DEFAULT 0,
246 PRIMARY KEY (pattern, style)
247 ) WITHOUT ROWID;
248
249 -- Completion mappings: direct key lookup
250 CREATE TABLE IF NOT EXISTS comps (
251 command TEXT PRIMARY KEY,
252 function TEXT NOT NULL
253 ) WITHOUT ROWID;
254
255 -- Pattern completions (`#compdef -p pat`, compinit sh:399 ->
256 -- `_patcomps`). Consulted BEFORE the `$_comps` name lookup.
257 CREATE TABLE IF NOT EXISTS patcomps (
258 pattern TEXT PRIMARY KEY,
259 function TEXT NOT NULL
260 ) WITHOUT ROWID;
261
262 -- Post-pattern completions (`#compdef -P pat`, compinit sh:404 ->
263 -- `_postpatcomps`). A SEPARATE table on purpose: `_dispatch`
264 -- walks `_patcomps` before the `$_comps` lookup and
265 -- `_postpatcomps` after it, and the post pass sets
266 -- `_compskip=default` first (_dispatch sh:72) so the sh:84
267 -- default fallback is suppressed. Storing both in `patcomps`
268 -- (the pre-fix behaviour) ran every `-P` completer in the PRE
269 -- phase without that flag, so `PATH=/usr/bin:<TAB>` ran
270 -- `_dir_list` AND then fell through to `_value`/`_default`,
271 -- listing every file instead of only directories.
272 CREATE TABLE IF NOT EXISTS postpatcomps (
273 pattern TEXT PRIMARY KEY,
274 function TEXT NOT NULL
275 ) WITHOUT ROWID;
276
277 -- Key completions
278 CREATE TABLE IF NOT EXISTS keycomps (
279 key TEXT PRIMARY KEY,
280 function TEXT NOT NULL
281 ) WITHOUT ROWID;
282
283 -- Services
284 CREATE TABLE IF NOT EXISTS services (
285 command TEXT PRIMARY KEY,
286 service TEXT NOT NULL
287 ) WITHOUT ROWID;
288
289 -- Result cache
290 CREATE TABLE IF NOT EXISTS cache (
291 context TEXT PRIMARY KEY,
292 data BLOB NOT NULL,
293 mtime INTEGER NOT NULL
294 ) WITHOUT ROWID;
295
296 -- PATH executables: flat, fast prefix via FTS5
297 CREATE TABLE IF NOT EXISTS executables (
298 name TEXT PRIMARY KEY,
299 path TEXT NOT NULL
300 ) WITHOUT ROWID;
301
302 -- Named directories
303 CREATE TABLE IF NOT EXISTS named_dirs (
304 name TEXT PRIMARY KEY,
305 path TEXT NOT NULL
306 ) WITHOUT ROWID;
307
308 -- Shell functions
309 CREATE TABLE IF NOT EXISTS shell_functions (
310 name TEXT PRIMARY KEY,
311 source TEXT NOT NULL
312 ) WITHOUT ROWID;
313
314 -- Metadata
315 CREATE TABLE IF NOT EXISTS metadata (
316 key TEXT PRIMARY KEY,
317 value TEXT NOT NULL
318 ) WITHOUT ROWID;
319
320 -- FTS5 for lightning-fast prefix search (standalone, not content-synced)
321 CREATE VIRTUAL TABLE IF NOT EXISTS fts_comps USING fts5(
322 command,
323 tokenize='unicode61'
324 );
325
326 CREATE VIRTUAL TABLE IF NOT EXISTS fts_executables USING fts5(
327 name,
328 tokenize='unicode61'
329 );
330
331 CREATE VIRTUAL TABLE IF NOT EXISTS fts_shell_functions USING fts5(
332 name,
333 tokenize='unicode61'
334 );
335
336 -- Covering index for comps prefix search (fallback if FTS unavailable)
337 CREATE INDEX IF NOT EXISTS idx_comps_cmd ON comps(command);
338 CREATE INDEX IF NOT EXISTS idx_comps_func ON comps(function);
339 CREATE INDEX IF NOT EXISTS idx_executables_name ON executables(name);
340 CREATE INDEX IF NOT EXISTS idx_shell_functions_name ON shell_functions(name);
341 CREATE INDEX IF NOT EXISTS idx_named_dirs_name ON named_dirs(name);
342 "#,
343 )?;
344 self.migrate()?;
345 Ok(())
346 }
347
348 fn migrate(&self) -> rusqlite::Result<()> {
355 let has_ast: bool = self
356 .conn
357 .prepare("SELECT ast FROM autoloads LIMIT 0")
358 .is_ok();
359 if has_ast {
360 self.conn.execute_batch(
362 r#"
363 BEGIN;
364 CREATE TABLE autoloads_no_ast (
365 name TEXT PRIMARY KEY,
366 source TEXT NOT NULL,
367 offset INTEGER NOT NULL,
368 size INTEGER NOT NULL,
369 body TEXT
370 ) WITHOUT ROWID;
371 INSERT OR IGNORE INTO autoloads_no_ast (name, source, offset, size, body)
372 SELECT name, source, offset, size, body FROM autoloads;
373 DROP TABLE autoloads;
374 ALTER TABLE autoloads_no_ast RENAME TO autoloads;
375 COMMIT;
376 "#,
377 )?;
378 }
379 self.migrate_completion_tables()?;
380 Ok(())
381 }
382
383 const COMPLETION_SCHEMA_GENERATION: &'static str = "2";
393
394 fn migrate_completion_tables(&self) -> rusqlite::Result<()> {
399 let current: Option<String> = self
400 .conn
401 .query_row(
402 "SELECT value FROM metadata WHERE key = 'completion_schema'",
403 [],
404 |row| row.get(0),
405 )
406 .ok();
407 if current.as_deref() == Some(Self::COMPLETION_SCHEMA_GENERATION) {
408 return Ok(());
409 }
410 let stale: i64 = self
411 .conn
412 .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
413 .unwrap_or(0);
414 if stale > 0 {
415 tracing::info!(
416 rows = stale,
417 from = current.as_deref().unwrap_or("1"),
418 to = Self::COMPLETION_SCHEMA_GENERATION,
419 "compsys cache: completion tables from an older generation, rebuilding"
420 );
421 }
422 self.conn.execute_batch(
423 r#"
424 DELETE FROM comps;
425 DELETE FROM services;
426 DELETE FROM patcomps;
427 DELETE FROM postpatcomps;
428 DELETE FROM fts_comps;
429 "#,
430 )?;
431 self.conn.execute(
432 "INSERT OR REPLACE INTO metadata (key, value) VALUES ('completion_schema', ?1)",
433 params![Self::COMPLETION_SCHEMA_GENERATION],
434 )?;
435 Ok(())
436 }
437
438 pub fn add_autoload(
444 &self,
445 name: &str,
446 source: &str,
447 offset: i64,
448 size: i64,
449 ) -> rusqlite::Result<()> {
450 self.conn.execute(
451 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)",
452 params![name, source, offset, size],
453 )?;
454 Ok(())
455 }
456
457 pub fn add_autoload_with_body(
459 &self,
460 name: &str,
461 source: &str,
462 body: &str,
463 ) -> rusqlite::Result<()> {
464 self.conn.execute(
465 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)",
466 params![name, source, body.len() as i64, body],
467 )?;
468 Ok(())
469 }
470
471 pub fn add_autoloads_bulk(
473 &mut self,
474 autoloads: &[(String, String, i64, i64)],
475 ) -> rusqlite::Result<()> {
476 let tx = self.conn.transaction()?;
477 {
478 let mut stmt = tx.prepare(
479 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)"
480 )?;
481 for (name, source, offset, size) in autoloads {
482 stmt.execute(params![name, source, offset, size])?;
483 }
484 }
485 tx.commit()?;
486 Ok(())
487 }
488
489 pub fn add_autoloads_with_bodies_bulk(
491 &mut self,
492 autoloads: &[(String, String, String)], ) -> rusqlite::Result<()> {
494 let tx = self.conn.transaction()?;
495 {
496 let mut stmt = tx.prepare(
497 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)"
498 )?;
499 for (name, source, body) in autoloads {
500 stmt.execute(params![name, source, body.len() as i64, body])?;
501 }
502 }
503 tx.commit()?;
504 Ok(())
505 }
506
507 pub fn get_autoload(&self, name: &str) -> rusqlite::Result<Option<AutoloadStub>> {
509 self.conn
510 .query_row(
511 "SELECT source, offset, size, body FROM autoloads WHERE name = ?1",
512 params![name],
513 |row| {
514 Ok(AutoloadStub {
515 name: name.to_string(),
516 source: row.get(0)?,
517 offset: row.get(1)?,
518 size: row.get(2)?,
519 body: row.get(3)?,
520 })
521 },
522 )
523 .optional()
524 }
525
526 pub fn get_autoload_body(&self, name: &str) -> rusqlite::Result<Option<String>> {
528 self.conn
529 .query_row(
530 "SELECT body FROM autoloads WHERE name = ?1",
531 params![name],
532 |row| row.get(0),
533 )
534 .optional()
535 }
536
537 pub fn count_autoloads_with_body(&self) -> rusqlite::Result<usize> {
542 self.conn.query_row(
543 "SELECT COUNT(*) FROM autoloads WHERE body IS NOT NULL",
544 [],
545 |row| row.get::<_, i64>(0).map(|n| n as usize),
546 )
547 }
548
549 pub fn get_autoload_bodies_excluding(
558 &self,
559 exclude: &std::collections::HashSet<String>,
560 limit: usize,
561 ) -> rusqlite::Result<Vec<(String, String)>> {
562 let mut stmt = self
563 .conn
564 .prepare("SELECT name, body FROM autoloads WHERE body IS NOT NULL ORDER BY name")?;
565 let rows = stmt.query_map([], |row| {
566 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
567 })?;
568 let mut out = Vec::with_capacity(limit.min(256));
569 for row in rows {
570 let (name, body) = row?;
571 if exclude.contains(&name) {
572 continue;
573 }
574 out.push((name, body));
575 if out.len() >= limit {
576 break;
577 }
578 }
579 Ok(out)
580 }
581
582 pub fn get_autoload_body_or_zwc(&self, name: &str) -> Option<String> {
587 let stub = self.get_autoload(name).ok()??;
588
589 if let Some(body) = stub.body {
591 return Some(body);
592 }
593
594 if stub.size > 0 && !stub.source.is_empty() {
596 return Self::read_function_from_zwc(&stub.source, stub.offset, stub.size);
597 }
598
599 None
600 }
601
602 fn read_function_from_zwc(zwc_path: &str, offset: i64, size: i64) -> Option<String> {
604 use std::io::{Read, Seek, SeekFrom};
605
606 let mut file = std::fs::File::open(zwc_path).ok()?;
607 file.seek(SeekFrom::Start(offset as u64)).ok()?;
608
609 let mut buf = vec![0u8; size as usize];
610 file.read_exact(&mut buf).ok()?;
611
612 match String::from_utf8(buf) {
616 Ok(s) => Some(s),
617 Err(e) => Some(String::from_utf8_lossy(e.as_bytes()).into_owned()),
618 }
619 }
620
621 pub fn autoload_count(&self) -> rusqlite::Result<i64> {
623 self.conn
624 .query_row("SELECT COUNT(*) FROM autoloads", [], |row| row.get(0))
625 }
626
627 pub fn list_autoloads(&self, limit: usize) -> rusqlite::Result<Vec<String>> {
629 let mut stmt = self.conn.prepare("SELECT name FROM autoloads LIMIT ?1")?;
630 let rows = stmt.query_map(params![limit as i64], |row| row.get(0))?;
631 rows.collect()
632 }
633
634 pub fn list_autoload_names(&self) -> rusqlite::Result<Vec<String>> {
636 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
637 let rows = stmt.query_map([], |row| row.get(0))?;
638 rows.collect()
639 }
640
641 pub fn set_zstyle(
647 &self,
648 pattern: &str,
649 style: &str,
650 values: &[String],
651 eval: bool,
652 ) -> rusqlite::Result<()> {
653 let value_json = serde_values_to_json(values);
654 self.conn.execute(
655 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)",
656 params![pattern, style, value_json, eval as i32],
657 )?;
658 Ok(())
659 }
660
661 pub fn set_zstyles_bulk(
663 &mut self,
664 styles: &[(String, String, Vec<String>, bool)],
665 ) -> rusqlite::Result<()> {
666 let tx = self.conn.transaction()?;
667 {
668 let mut stmt = tx.prepare(
669 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)"
670 )?;
671 for (pattern, style, values, eval) in styles {
672 let value_json = serde_values_to_json(values);
673 stmt.execute(params![pattern, style, value_json, *eval as i32])?;
674 }
675 }
676 tx.commit()?;
677 Ok(())
678 }
679
680 pub fn delete_zstyle(&self, pattern: &str, style: Option<&str>) -> rusqlite::Result<usize> {
682 if let Some(s) = style {
683 self.conn.execute(
684 "DELETE FROM zstyles WHERE pattern = ?1 AND style = ?2",
685 params![pattern, s],
686 )
687 } else {
688 self.conn
689 .execute("DELETE FROM zstyles WHERE pattern = ?1", params![pattern])
690 }
691 }
692
693 pub fn lookup_zstyle(
695 &self,
696 context: &str,
697 style: &str,
698 ) -> rusqlite::Result<Option<ZStyleEntry>> {
699 let mut stmt = self
700 .conn
701 .prepare("SELECT pattern, value, eval FROM zstyles WHERE style = ?1")?;
702
703 let entries: Vec<(String, String, bool)> = stmt
704 .query_map(params![style], |row| {
705 Ok((row.get(0)?, row.get(1)?, row.get::<_, i32>(2)? != 0))
706 })?
707 .filter_map(|r| r.ok())
708 .collect();
709
710 let mut best: Option<(i32, String, bool)> = None;
712 for (pattern, value, eval) in entries {
713 if pattern_matches_context(&pattern, context) {
714 let weight = calculate_pattern_weight(&pattern);
715 if best.is_none() || weight > best.as_ref().unwrap().0 {
716 best = Some((weight, value, eval));
717 }
718 }
719 }
720
721 Ok(best.map(|(_, value, eval)| ZStyleEntry {
722 values: serde_json_to_values(&value),
723 eval,
724 }))
725 }
726
727 #[allow(clippy::type_complexity)]
729 pub fn list_zstyles(&self) -> rusqlite::Result<Vec<(String, String, Vec<String>, bool)>> {
730 let mut stmt = self
731 .conn
732 .prepare("SELECT pattern, style, value, eval FROM zstyles ORDER BY pattern, style")?;
733 let rows = stmt.query_map([], |row| {
734 let pattern: String = row.get(0)?;
735 let style: String = row.get(1)?;
736 let value: String = row.get(2)?;
737 let eval: bool = row.get::<_, i32>(3)? != 0;
738 Ok((pattern, style, serde_json_to_values(&value), eval))
739 })?;
740 rows.collect()
741 }
742
743 pub fn zstyle_count(&self) -> rusqlite::Result<i64> {
745 self.conn
746 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
747 }
748
749 pub fn set_comp(&self, command: &str, function: &str) -> rusqlite::Result<()> {
755 self.conn.execute(
756 "INSERT OR REPLACE INTO comps (command, function) VALUES (?1, ?2)",
757 params![command, function],
758 )?;
759 Ok(())
760 }
761
762 pub fn set_comps_bulk(&mut self, comps: &[(String, String)]) -> rusqlite::Result<()> {
764 let tx = self.conn.transaction()?;
765 tx.execute("DELETE FROM comps", [])?;
767 tx.execute("DELETE FROM fts_comps", [])?;
768 {
769 let mut stmt = tx.prepare("INSERT INTO comps (command, function) VALUES (?1, ?2)")?;
770 let mut fts_stmt = tx.prepare("INSERT INTO fts_comps (command) VALUES (?1)")?;
771 for (command, function) in comps {
772 stmt.execute(params![command, function])?;
773 fts_stmt.execute(params![command])?;
774 }
775 }
776 tx.commit()
777 }
778
779 pub fn comps_prefix_fts(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
781 if prefix.is_empty() {
782 return self.comps_kv();
783 }
784 let pattern = format!("{}*", prefix);
786 let mut stmt = self.conn.prepare(
787 "SELECT c.command, c.function FROM fts_comps f, comps c WHERE f.command MATCH ?1 AND c.command = f.command"
788 )?;
789 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
790 rows.collect()
791 }
792
793 pub fn comps_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
795 if prefix.is_empty() {
796 return self.comps_kv();
797 }
798 let pattern = format!("{}%", prefix);
799 let mut stmt = self.conn.prepare(
800 "SELECT command, function FROM comps WHERE command LIKE ?1 ORDER BY command",
801 )?;
802 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
803 rows.collect()
804 }
805
806 pub fn get_comp(&self, command: &str) -> rusqlite::Result<Option<String>> {
808 self.conn
809 .query_row(
810 "SELECT function FROM comps WHERE command = ?1",
811 params![command],
812 |row| row.get(0),
813 )
814 .optional()
815 }
816
817 pub fn get_all_comps(&self) -> rusqlite::Result<HashMap<String, String>> {
819 let mut stmt = self.conn.prepare("SELECT command, function FROM comps")?;
820 let rows = stmt.query_map([], |row| {
821 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
822 })?;
823 let mut map = HashMap::new();
824 for row in rows {
825 let (k, v) = row?;
826 map.insert(k, v);
827 }
828 Ok(map)
829 }
830
831 pub fn comp_count(&self) -> rusqlite::Result<i64> {
833 self.conn
834 .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
835 }
836
837 pub fn delete_comp(&self, command: &str) -> rusqlite::Result<usize> {
839 self.conn
840 .execute("DELETE FROM comps WHERE command = ?1", params![command])
841 }
842
843 pub fn set_patcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
849 self.conn.execute(
850 "INSERT OR REPLACE INTO patcomps (pattern, function) VALUES (?1, ?2)",
851 params![pattern, function],
852 )?;
853 Ok(())
854 }
855
856 pub fn find_patcomp(&self, command: &str) -> rusqlite::Result<Option<String>> {
858 let mut stmt = self
859 .conn
860 .prepare("SELECT pattern, function FROM patcomps")?;
861 let rows = stmt.query_map([], |row| {
862 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
863 })?;
864
865 for row in rows {
866 let (pattern, function) = row?;
867 if glob_matches(&pattern, command) {
868 return Ok(Some(function));
869 }
870 }
871 Ok(None)
872 }
873
874 pub fn set_postpatcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
880 self.conn.execute(
881 "INSERT OR REPLACE INTO postpatcomps (pattern, function) VALUES (?1, ?2)",
882 params![pattern, function],
883 )?;
884 Ok(())
885 }
886
887 pub fn postpatcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
889 let mut stmt = self
890 .conn
891 .prepare("SELECT pattern, function FROM postpatcomps")?;
892 let rows = stmt.query_map([], |row| {
893 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
894 })?;
895 rows.collect()
896 }
897
898 pub fn services_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
900 let mut stmt = self.conn.prepare("SELECT command, service FROM services")?;
901 let rows = stmt.query_map([], |row| {
902 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
903 })?;
904 rows.collect()
905 }
906
907 pub fn postpatcomps_count(&self) -> rusqlite::Result<i64> {
909 self.conn
910 .query_row("SELECT COUNT(*) FROM postpatcomps", [], |row| row.get(0))
911 }
912
913 pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
919 self.conn.execute(
920 "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
921 params![key, function],
922 )?;
923 Ok(())
924 }
925
926 pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
928 self.conn
929 .query_row(
930 "SELECT function FROM keycomps WHERE key = ?1",
931 params![key],
932 |row| row.get(0),
933 )
934 .optional()
935 }
936
937 pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
943 self.conn.execute(
944 "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
945 params![context, data, mtime],
946 )?;
947 Ok(())
948 }
949
950 pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
952 let now = std::time::SystemTime::now()
953 .duration_since(std::time::UNIX_EPOCH)
954 .unwrap()
955 .as_secs() as i64;
956
957 self.conn
958 .query_row(
959 "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
960 params![context, now - max_age],
961 |row| row.get(0),
962 )
963 .optional()
964 }
965
966 pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
968 let now = std::time::SystemTime::now()
969 .duration_since(std::time::UNIX_EPOCH)
970 .unwrap()
971 .as_secs() as i64;
972
973 self.conn
974 .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
975 }
976
977 pub fn clear_cache(&self) -> rusqlite::Result<()> {
979 self.conn.execute("DELETE FROM cache", [])?;
980 Ok(())
981 }
982
983 pub fn vacuum(&self) -> rusqlite::Result<()> {
989 self.conn.execute("VACUUM", [])?;
990 Ok(())
991 }
992
993 pub fn stats(&self) -> rusqlite::Result<CacheStats> {
995 Ok(CacheStats {
996 autoloads: self.autoload_count()?,
997 zstyles: self.zstyle_count()?,
998 comps: self.comp_count()?,
999 patcomps: self
1000 .conn
1001 .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
1002 keycomps: self
1003 .conn
1004 .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
1005 services: self
1006 .conn
1007 .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
1008 cache_entries: self
1009 .conn
1010 .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
1011 })
1012 }
1013}
1014
1015#[derive(Debug, Clone)]
1017pub struct AutoloadStub {
1018 pub name: String,
1020 pub source: String,
1022 pub offset: i64,
1024 pub size: i64,
1026 pub body: Option<String>,
1028}
1029
1030#[derive(Debug, Clone)]
1032pub struct ZStyleEntry {
1033 pub values: Vec<String>,
1035 pub eval: bool,
1037}
1038
1039#[derive(Debug)]
1041pub struct CacheStats {
1042 pub autoloads: i64,
1044 pub zstyles: i64,
1046 pub comps: i64,
1048 pub patcomps: i64,
1050 pub keycomps: i64,
1052 pub services: i64,
1054 pub cache_entries: i64,
1056}
1057
1058fn serde_values_to_json(values: &[String]) -> String {
1060 let escaped: Vec<String> = values
1061 .iter()
1062 .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
1063 .collect();
1064 format!("[{}]", escaped.join(","))
1065}
1066
1067fn serde_json_to_values(json: &str) -> Vec<String> {
1069 let trimmed = json.trim();
1070 if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
1071 return vec![json.to_string()];
1072 }
1073
1074 let inner = &trimmed[1..trimmed.len() - 1];
1075 if inner.is_empty() {
1076 return vec![];
1077 }
1078
1079 let mut values = Vec::new();
1080 let mut current = String::new();
1081 let mut in_string = false;
1082 let mut escape = false;
1083
1084 for c in inner.chars() {
1085 if escape {
1086 current.push(c);
1087 escape = false;
1088 } else if c == '\\' {
1089 escape = true;
1090 } else if c == '"' {
1091 in_string = !in_string;
1092 } else if c == ',' && !in_string {
1093 values.push(current.trim().to_string());
1094 current = String::new();
1095 } else {
1096 current.push(c);
1097 }
1098 }
1099 if !current.is_empty() {
1100 values.push(current.trim().to_string());
1101 }
1102
1103 values
1104}
1105
1106fn pattern_matches_context(pattern: &str, context: &str) -> bool {
1108 let pat_parts: Vec<&str> = pattern.split(':').collect();
1109 let ctx_parts: Vec<&str> = context.split(':').collect();
1110
1111 if pat_parts.len() > ctx_parts.len() {
1112 return false;
1113 }
1114
1115 for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
1116 if *p != "*" && *p != *c {
1117 return false;
1118 }
1119 }
1120
1121 true
1122}
1123
1124fn calculate_pattern_weight(pattern: &str) -> i32 {
1126 let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
1127 let mut weight = parts.len() as i32 * 100;
1128
1129 for part in &parts {
1130 if *part != "*" {
1131 weight += 10;
1132 }
1133 }
1134
1135 weight
1136}
1137
1138fn glob_matches(pattern: &str, text: &str) -> bool {
1140 let mut pat_chars = pattern.chars().peekable();
1141 let mut txt_chars = text.chars().peekable();
1142
1143 while let Some(p) = pat_chars.next() {
1144 match p {
1145 '*' => {
1146 if pat_chars.peek().is_none() {
1147 return true;
1148 }
1149 while txt_chars.peek().is_some() {
1150 if glob_matches(
1151 &pat_chars.clone().collect::<String>(),
1152 &txt_chars.clone().collect::<String>(),
1153 ) {
1154 return true;
1155 }
1156 txt_chars.next();
1157 }
1158 return false;
1159 }
1160 '?' => {
1161 if txt_chars.next().is_none() {
1162 return false;
1163 }
1164 }
1165 c => {
1166 if txt_chars.next() != Some(c) {
1167 return false;
1168 }
1169 }
1170 }
1171 }
1172
1173 txt_chars.peek().is_none()
1174}
1175
1176impl CompsysCache {
1182 pub fn comps_count(&self) -> rusqlite::Result<i64> {
1184 self.comp_count()
1185 }
1186
1187 pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
1189 let mut stmt = self
1190 .conn
1191 .prepare("SELECT command FROM comps ORDER BY command")?;
1192 let rows = stmt.query_map([], |row| row.get(0))?;
1193 rows.collect()
1194 }
1195
1196 pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1198 let mut stmt = self
1199 .conn
1200 .prepare("SELECT function FROM comps ORDER BY command")?;
1201 let rows = stmt.query_map([], |row| row.get(0))?;
1202 rows.collect()
1203 }
1204
1205 pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1207 let mut stmt = self
1208 .conn
1209 .prepare("SELECT command, function FROM comps ORDER BY command")?;
1210 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1211 rows.collect()
1212 }
1213
1214 pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1218 self.conn
1219 .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1220 }
1221
1222 pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1224 let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1225 let rows = stmt.query_map([], |row| row.get(0))?;
1226 rows.collect()
1227 }
1228
1229 pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1231 let mut stmt = self
1232 .conn
1233 .prepare("SELECT pattern, function FROM patcomps")?;
1234 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1235 rows.collect()
1236 }
1237
1238 pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1242 self.conn.execute(
1243 "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1244 params![command, service],
1245 )?;
1246 Ok(())
1247 }
1248
1249 pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1251 self.conn
1252 .query_row(
1253 "SELECT service FROM services WHERE command = ?1",
1254 params![command],
1255 |row| row.get(0),
1256 )
1257 .optional()
1258 }
1259
1260 pub fn services_count(&self) -> rusqlite::Result<i64> {
1262 self.conn
1263 .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1264 }
1265
1266 pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1268 let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1269 let rows = stmt.query_map([], |row| row.get(0))?;
1270 rows.collect()
1271 }
1272
1273 pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1275 let tx = self.conn.transaction()?;
1276 {
1277 let mut stmt =
1278 tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1279 for (command, service) in services {
1280 stmt.execute(params![command, service])?;
1281 }
1282 }
1283 tx.commit()?;
1284 Ok(())
1285 }
1286
1287 pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1291 self.autoload_count()
1292 }
1293
1294 pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1296 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1297 let rows = stmt.query_map([], |row| row.get(0))?;
1298 rows.collect()
1299 }
1300
1301 pub fn has_executables(&self) -> rusqlite::Result<bool> {
1307 let count: i64 = self
1308 .conn
1309 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1310 Ok(count > 0)
1311 }
1312
1313 pub fn set_executables_bulk(
1315 &mut self,
1316 executables: &[(String, String)],
1317 ) -> rusqlite::Result<()> {
1318 let tx = self.conn.transaction()?;
1319 tx.execute("DELETE FROM executables", [])?;
1320 tx.execute("DELETE FROM fts_executables", [])?;
1321 {
1322 let mut stmt =
1323 tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1324 let mut fts_stmt =
1325 tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1326 for (name, path) in executables {
1327 stmt.execute(params![name, path])?;
1328 fts_stmt.execute(params![name])?;
1329 }
1330 }
1331 tx.commit()
1332 }
1333
1334 pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1336 let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1337 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1338 rows.collect::<Result<std::collections::HashSet<_>, _>>()
1339 }
1340
1341 pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1343 let exists: i64 = self.conn.query_row(
1345 "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1346 params![name],
1347 |row| row.get(0),
1348 )?;
1349 Ok(exists == 1)
1350 }
1351
1352 pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1354 self.conn
1355 .query_row(
1356 "SELECT path FROM executables WHERE name = ?1",
1357 params![name],
1358 |row| row.get(0),
1359 )
1360 .optional()
1361 }
1362
1363 pub fn get_executables_prefix_fts(
1365 &self,
1366 prefix: &str,
1367 ) -> rusqlite::Result<Vec<(String, String)>> {
1368 if prefix.is_empty() {
1369 let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1370 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1371 return rows.collect();
1372 }
1373 let pattern = format!("{}*", prefix);
1374 let mut stmt = self.conn.prepare(
1375 "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1376 )?;
1377 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1378 rows.collect()
1379 }
1380
1381 pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1383 if prefix.is_empty() {
1384 let mut stmt = self
1385 .conn
1386 .prepare("SELECT name, path FROM executables ORDER BY name")?;
1387 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1388 return rows.collect();
1389 }
1390 let pattern = format!("{}%", prefix);
1391 let mut stmt = self
1392 .conn
1393 .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1394 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1395 rows.collect()
1396 }
1397
1398 pub fn executables_count(&self) -> rusqlite::Result<i64> {
1400 self.conn
1401 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1402 }
1403
1404 pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1410 let count: i64 = self
1411 .conn
1412 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1413 Ok(count > 0)
1414 }
1415
1416 pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1418 let tx = self.conn.transaction()?;
1419 tx.execute("DELETE FROM named_dirs", [])?;
1420 {
1421 let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1422 for (name, path) in dirs {
1423 stmt.execute(params![name, path])?;
1424 }
1425 }
1426 tx.commit()
1427 }
1428
1429 pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1431 let mut stmt = self
1432 .conn
1433 .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1434 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1435 rows.collect()
1436 }
1437
1438 pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1440 if prefix.is_empty() {
1441 return self.get_named_dirs();
1442 }
1443 let pattern = format!("{}%", prefix);
1444 let mut stmt = self
1445 .conn
1446 .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1447 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1448 rows.collect()
1449 }
1450
1451 pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1453 self.conn
1454 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1455 }
1456
1457 pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1463 let count: i64 =
1464 self.conn
1465 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1466 Ok(count > 0)
1467 }
1468
1469 pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1471 let tx = self.conn.transaction()?;
1472 tx.execute("DELETE FROM shell_functions", [])?;
1473 tx.execute("DELETE FROM fts_shell_functions", [])?;
1474 {
1475 let mut stmt =
1476 tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1477 let mut fts_stmt =
1478 tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1479 for (name, source) in funcs {
1480 stmt.execute(params![name, source])?;
1481 fts_stmt.execute(params![name])?;
1482 }
1483 }
1484 tx.commit()
1485 }
1486
1487 pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1489 let mut stmt = self
1490 .conn
1491 .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1492 let rows = stmt.query_map([], |row| row.get(0))?;
1493 rows.collect()
1494 }
1495
1496 pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1498 let mut stmt = self
1499 .conn
1500 .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1501 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1502 rows.collect()
1503 }
1504
1505 pub fn get_shell_functions_prefix_fts(
1507 &self,
1508 prefix: &str,
1509 ) -> rusqlite::Result<Vec<(String, String)>> {
1510 if prefix.is_empty() {
1511 return self.get_shell_functions();
1512 }
1513 let pattern = format!("{}*", prefix);
1514 let mut stmt = self.conn.prepare(
1515 "SELECT s.name, s.source FROM fts_shell_functions f, shell_functions s WHERE f.name MATCH ?1 AND s.name = f.name ORDER BY s.name"
1516 )?;
1517 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1518 rows.collect()
1519 }
1520
1521 pub fn get_shell_functions_prefix(
1523 &self,
1524 prefix: &str,
1525 ) -> rusqlite::Result<Vec<(String, String)>> {
1526 if prefix.is_empty() {
1527 return self.get_shell_functions();
1528 }
1529 let pattern = format!("{}%", prefix);
1530 let mut stmt = self
1531 .conn
1532 .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1533 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1534 rows.collect()
1535 }
1536
1537 pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1539 self.conn
1540 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1541 }
1542
1543 pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1549 self.conn.execute(
1550 "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1551 params![key, value],
1552 )?;
1553 Ok(())
1554 }
1555
1556 pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1558 self.conn
1559 .query_row(
1560 "SELECT value FROM metadata WHERE key = ?1",
1561 params![key],
1562 |row| row.get(0),
1563 )
1564 .optional()
1565 }
1566
1567 pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1573 let count: i64 = self
1574 .conn
1575 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1576 Ok(count > 0)
1577 }
1578
1579 pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1581 self.conn
1582 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1583 }
1584
1585 pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1587 let mut stmt = self
1588 .conn
1589 .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1590 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1591 rows.collect()
1592 }
1593}
1594
1595#[cfg(test)]
1596mod tests {
1597 use super::*;
1598
1599 #[test]
1600 fn test_cache_basic() {
1601 let cache = CompsysCache::memory().unwrap();
1602
1603 cache
1604 .add_autoload("_git", "more_src.zwc", 1024, 5000)
1605 .unwrap();
1606 cache
1607 .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1608 .unwrap();
1609
1610 let stub = cache.get_autoload("_git").unwrap().unwrap();
1611 assert_eq!(stub.source, "more_src.zwc");
1612 assert_eq!(stub.offset, 1024);
1613
1614 assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1615 }
1616
1617 #[test]
1618 fn test_zstyle_cache() {
1619 let cache = CompsysCache::memory().unwrap();
1620
1621 cache
1622 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1623 .unwrap();
1624 cache
1625 .set_zstyle(
1626 ":completion:*:descriptions",
1627 "format",
1628 &["%d".to_string()],
1629 false,
1630 )
1631 .unwrap();
1632
1633 let entry = cache
1634 .lookup_zstyle(":completion:foo", "menu")
1635 .unwrap()
1636 .unwrap();
1637 assert_eq!(entry.values, vec!["select"]);
1638
1639 let entry = cache
1640 .lookup_zstyle(":completion:foo:descriptions", "format")
1641 .unwrap()
1642 .unwrap();
1643 assert_eq!(entry.values, vec!["%d"]);
1644 }
1645
1646 #[test]
1647 fn test_zstyle_specificity() {
1648 let cache = CompsysCache::memory().unwrap();
1649
1650 cache
1651 .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1652 .unwrap();
1653 cache
1654 .set_zstyle(
1655 ":completion:*:*:*:default",
1656 "menu",
1657 &["yes".to_string()],
1658 false,
1659 )
1660 .unwrap();
1661
1662 let entry = cache
1663 .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1664 .unwrap()
1665 .unwrap();
1666 assert_eq!(entry.values, vec!["yes"]);
1667 }
1668
1669 #[test]
1670 fn test_comps_cache() {
1671 let mut cache = CompsysCache::memory().unwrap();
1672
1673 let comps = vec![
1674 ("git".to_string(), "_git".to_string()),
1675 ("docker".to_string(), "_docker".to_string()),
1676 ("cargo".to_string(), "_cargo".to_string()),
1677 ];
1678 cache.set_comps_bulk(&comps).unwrap();
1679
1680 assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1681 assert_eq!(
1682 cache.get_comp("docker").unwrap(),
1683 Some("_docker".to_string())
1684 );
1685 assert!(cache.get_comp("nonexistent").unwrap().is_none());
1686
1687 assert_eq!(cache.comp_count().unwrap(), 3);
1688 }
1689
1690 #[test]
1691 fn test_bulk_autoloads() {
1692 let mut cache = CompsysCache::memory().unwrap();
1693
1694 let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1695 .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1696 .collect();
1697
1698 cache.add_autoloads_bulk(&autoloads).unwrap();
1699 assert_eq!(cache.autoload_count().unwrap(), 1000);
1700
1701 let stub = cache.get_autoload("_func500").unwrap().unwrap();
1702 assert_eq!(stub.offset, 50000);
1703 assert!(stub.body.is_none()); }
1705
1706 #[test]
1707 fn test_autoload_with_body() {
1708 let cache = CompsysCache::memory().unwrap();
1709
1710 let body = r#"
1711local -a opts
1712opts=(--help --version --verbose)
1713_arguments $opts
1714"#;
1715 cache
1716 .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1717 .unwrap();
1718
1719 let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1720 assert_eq!(stub.body.as_deref(), Some(body));
1721 assert_eq!(stub.size, body.len() as i64);
1722
1723 let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1725 assert_eq!(direct_body.as_deref(), Some(body));
1726 }
1727
1728 #[test]
1729 fn test_bulk_autoloads_with_bodies() {
1730 let mut cache = CompsysCache::memory().unwrap();
1731
1732 let autoloads: Vec<(String, String, String)> = (0..100)
1733 .map(|i| {
1734 (
1735 format!("_func{}", i),
1736 format!("/path/to/_func{}", i),
1737 format!("# Function {}\necho hello", i),
1738 )
1739 })
1740 .collect();
1741
1742 cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1743 assert_eq!(cache.autoload_count().unwrap(), 100);
1744
1745 let stub = cache.get_autoload("_func50").unwrap().unwrap();
1746 assert!(stub.body.is_some());
1747 assert!(stub.body.unwrap().contains("Function 50"));
1748 }
1749
1750 #[test]
1751 fn test_get_autoload_body_or_zwc_with_body() {
1752 let cache = CompsysCache::memory().unwrap();
1753
1754 let body = "echo from sqlite";
1755 cache
1756 .add_autoload_with_body("_cached", "/some/path", body)
1757 .unwrap();
1758
1759 let result = cache.get_autoload_body_or_zwc("_cached");
1761 assert_eq!(result, Some(body.to_string()));
1762 }
1763
1764 #[test]
1765 fn test_get_autoload_body_or_zwc_no_body() {
1766 let cache = CompsysCache::memory().unwrap();
1767
1768 cache
1770 .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1771 .unwrap();
1772
1773 let result = cache.get_autoload_body_or_zwc("_nocache");
1775 assert!(result.is_none());
1776 }
1777
1778 #[test]
1779 fn test_get_autoload_body_or_zwc_not_found() {
1780 let cache = CompsysCache::memory().unwrap();
1781
1782 let result = cache.get_autoload_body_or_zwc("_nonexistent");
1784 assert!(result.is_none());
1785 }
1786
1787 #[test]
1788 fn test_patcomp() {
1789 let cache = CompsysCache::memory().unwrap();
1790
1791 cache.set_patcomp("git-*", "_git").unwrap();
1792 cache.set_patcomp("docker-*", "_docker").unwrap();
1793
1794 assert_eq!(
1795 cache.find_patcomp("git-commit").unwrap(),
1796 Some("_git".to_string())
1797 );
1798 assert_eq!(
1799 cache.find_patcomp("docker-compose").unwrap(),
1800 Some("_docker".to_string())
1801 );
1802 assert!(cache.find_patcomp("cargo").unwrap().is_none());
1803 }
1804
1805 #[test]
1806 fn test_glob_matches() {
1807 assert!(glob_matches("git-*", "git-commit"));
1808 assert!(glob_matches("*-compose", "docker-compose"));
1809 assert!(!glob_matches("*.rs", "zle_main"));
1812 assert!(!glob_matches("git-*", "docker-compose"));
1813 assert!(glob_matches("???", "abc"));
1814 assert!(!glob_matches("???", "abcd"));
1815 }
1816
1817 #[test]
1818 fn test_json_serde() {
1819 let values = vec!["hello".to_string(), "world".to_string()];
1820 let json = serde_values_to_json(&values);
1821 let back = serde_json_to_values(&json);
1822 assert_eq!(back, values);
1823
1824 let values = vec!["with \"quotes\"".to_string()];
1825 let json = serde_values_to_json(&values);
1826 let back = serde_json_to_values(&json);
1827 assert_eq!(back, vec!["with \"quotes\""]);
1828 }
1829
1830 #[test]
1831 fn test_stats() {
1832 let mut cache = CompsysCache::memory().unwrap();
1833
1834 cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1835 cache
1836 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1837 .unwrap();
1838 cache.set_comp("git", "_git").unwrap();
1839
1840 let stats = cache.stats().unwrap();
1841 assert_eq!(stats.autoloads, 1);
1842 assert_eq!(stats.zstyles, 1);
1843 assert_eq!(stats.comps, 1);
1844 }
1845
1846 #[test]
1847 fn test_large_scale() {
1848 let mut cache = CompsysCache::memory().unwrap();
1849
1850 let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1852 .map(|i| {
1853 (
1854 format!("_func{}", i),
1855 format!("src{}.zwc", i % 10),
1856 i * 50,
1857 50,
1858 )
1859 })
1860 .collect();
1861
1862 cache.add_autoloads_bulk(&autoloads).unwrap();
1863
1864 let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1866 assert_eq!(stub.offset, 9999 * 50);
1867
1868 assert_eq!(cache.autoload_count().unwrap(), 10000);
1869 }
1870
1871 #[test]
1872 fn test_executables_cache() {
1873 let mut cache = CompsysCache::memory().unwrap();
1874
1875 let executables = vec![
1876 ("ls".to_string(), "/bin/ls".to_string()),
1877 ("cat".to_string(), "/bin/cat".to_string()),
1878 ("git".to_string(), "/usr/bin/git".to_string()),
1879 ];
1880 cache.set_executables_bulk(&executables).unwrap();
1881
1882 assert!(cache.has_executables().unwrap());
1883 assert!(cache.has_executable("ls").unwrap());
1884 assert!(cache.has_executable("git").unwrap());
1885 assert!(!cache.has_executable("nonexistent").unwrap());
1886
1887 assert_eq!(
1888 cache.get_executable_path("ls").unwrap(),
1889 Some("/bin/ls".to_string())
1890 );
1891 assert_eq!(cache.executables_count().unwrap(), 3);
1892 }
1893
1894 #[test]
1895 fn test_executables_prefix_search() {
1896 let mut cache = CompsysCache::memory().unwrap();
1897
1898 let executables = vec![
1899 ("git".to_string(), "/usr/bin/git".to_string()),
1900 ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1901 ("grep".to_string(), "/bin/grep".to_string()),
1902 ("gzip".to_string(), "/bin/gzip".to_string()),
1903 ];
1904 cache.set_executables_bulk(&executables).unwrap();
1905
1906 let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1908 assert_eq!(git_cmds.len(), 2);
1909 assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1910 assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1911
1912 let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1913 assert_eq!(g_cmds.len(), 4);
1914 }
1915
1916 #[test]
1917 fn test_named_dirs_cache() {
1918 let mut cache = CompsysCache::memory().unwrap();
1919
1920 let dirs = vec![
1921 ("proj".to_string(), "/home/user/projects".to_string()),
1922 ("docs".to_string(), "/home/user/documents".to_string()),
1923 ];
1924 cache.set_named_dirs_bulk(&dirs).unwrap();
1925
1926 assert!(cache.has_named_dirs().unwrap());
1927
1928 let all = cache.get_named_dirs().unwrap();
1929 assert_eq!(all.len(), 2);
1930
1931 let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1932 assert_eq!(p_dirs.len(), 1);
1933 assert_eq!(p_dirs[0].0, "proj");
1934 }
1935
1936 #[test]
1937 fn test_shell_functions_cache() {
1938 let mut cache = CompsysCache::memory().unwrap();
1939
1940 let functions = vec![
1941 ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1942 (
1943 "zpwrClearList".to_string(),
1944 "/home/user/.zpwr/autoload".to_string(),
1945 ),
1946 (
1947 "zpwrTop".to_string(),
1948 "/home/user/.zpwr/autoload".to_string(),
1949 ),
1950 ];
1951 cache.set_shell_functions_bulk(&functions).unwrap();
1952
1953 assert!(cache.has_shell_functions().unwrap());
1954 assert_eq!(cache.shell_functions_count().unwrap(), 3);
1955
1956 let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1957 assert_eq!(zpwr.len(), 2);
1958 assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1960 assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1961 }
1962
1963 #[test]
1964 fn test_metadata() {
1965 let cache = CompsysCache::memory().unwrap();
1966
1967 cache.set_metadata("version", "1.0.0").unwrap();
1968 cache.set_metadata("build_time", "2026-04-22").unwrap();
1969
1970 assert_eq!(
1971 cache.get_metadata("version").unwrap(),
1972 Some("1.0.0".to_string())
1973 );
1974 assert_eq!(
1975 cache.get_metadata("build_time").unwrap(),
1976 Some("2026-04-22".to_string())
1977 );
1978 assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1979 }
1980
1981 #[test]
1982 fn test_comps_keys() {
1983 let mut cache = CompsysCache::memory().unwrap();
1984
1985 let comps = vec![
1986 ("git".to_string(), "_git".to_string()),
1987 ("docker".to_string(), "_docker".to_string()),
1988 ];
1989 cache.set_comps_bulk(&comps).unwrap();
1990
1991 let keys = cache.comps_keys().unwrap();
1992 assert_eq!(keys.len(), 2);
1993 assert!(keys.contains(&"docker".to_string()));
1994 assert!(keys.contains(&"git".to_string()));
1995 }
1996
1997 #[test]
1998 fn test_comps_prefix() {
1999 let mut cache = CompsysCache::memory().unwrap();
2000
2001 let comps = vec![
2002 ("git".to_string(), "_git".to_string()),
2003 ("gitk".to_string(), "_gitk".to_string()),
2004 ("docker".to_string(), "_docker".to_string()),
2005 ];
2006 cache.set_comps_bulk(&comps).unwrap();
2007
2008 let git_comps = cache.comps_prefix("git").unwrap();
2009 assert_eq!(git_comps.len(), 2);
2010 }
2011
2012 #[test]
2013 fn test_zstyles_bulk() {
2014 let mut cache = CompsysCache::memory().unwrap();
2015
2016 let styles = vec![
2017 (
2018 ":completion:*".to_string(),
2019 "menu".to_string(),
2020 vec!["select".to_string()],
2021 false,
2022 ),
2023 (
2024 ":completion:*".to_string(),
2025 "verbose".to_string(),
2026 vec!["yes".to_string()],
2027 false,
2028 ),
2029 (
2030 ":completion:*:descriptions".to_string(),
2031 "format".to_string(),
2032 vec!["%d".to_string()],
2033 false,
2034 ),
2035 ];
2036 cache.set_zstyles_bulk(&styles).unwrap();
2037
2038 assert!(cache.has_zstyles().unwrap());
2039 assert_eq!(cache.zstyles_count().unwrap(), 3);
2040 }
2041
2042 #[test]
2043 fn test_services() {
2044 let cache = CompsysCache::memory().unwrap();
2045
2046 cache.set_service("git", "scm").unwrap();
2047 cache.set_service("hg", "scm").unwrap();
2048
2049 assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
2050 assert_eq!(cache.get_service("unknown").unwrap(), None);
2051 }
2052
2053 #[test]
2054 fn test_cache_overwrite() {
2055 let cache = CompsysCache::memory().unwrap();
2056
2057 cache.set_comp("git", "_git_old").unwrap();
2058 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
2059
2060 cache.set_comp("git", "_git_new").unwrap();
2061 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
2062 }
2063
2064 #[test]
2065 fn test_executable_names() {
2066 let mut cache = CompsysCache::memory().unwrap();
2067
2068 let executables = vec![
2069 ("alpha".to_string(), "/bin/alpha".to_string()),
2070 ("beta".to_string(), "/bin/beta".to_string()),
2071 ("gamma".to_string(), "/bin/gamma".to_string()),
2072 ];
2073 cache.set_executables_bulk(&executables).unwrap();
2074
2075 let names = cache.get_executable_names().unwrap();
2076 assert_eq!(names.len(), 3);
2077 assert!(names.contains("alpha"));
2079 assert!(names.contains("beta"));
2080 assert!(names.contains("gamma"));
2081 }
2082
2083 #[test]
2084 fn postpatcomps_do_not_land_in_patcomps() {
2085 let cache = CompsysCache::memory().unwrap();
2096 cache
2097 .set_patcomp("*/(init|rc[0-9S]#).d/*", "_init_d")
2098 .unwrap();
2099 cache
2100 .set_postpatcomp("-value-,*PATH,-default-", "_dir_list")
2101 .unwrap();
2102
2103 let pat = cache.patcomps_kv().unwrap();
2104 assert_eq!(
2105 pat,
2106 vec![("*/(init|rc[0-9S]#).d/*".to_string(), "_init_d".to_string())]
2107 );
2108
2109 let post = cache.postpatcomps_kv().unwrap();
2110 assert_eq!(
2111 post,
2112 vec![(
2113 "-value-,*PATH,-default-".to_string(),
2114 "_dir_list".to_string()
2115 )]
2116 );
2117 assert_eq!(cache.postpatcomps_count().unwrap(), 1);
2118 assert_eq!(cache.patcomps_count().unwrap(), 1);
2119 }
2120
2121 #[test]
2122 fn generation_one_completion_tables_are_rebuilt() {
2123 let mut cache = CompsysCache::memory().unwrap();
2128 cache
2129 .set_comps_bulk(&[("git".to_string(), "_git".to_string())])
2130 .unwrap();
2131 cache
2132 .set_patcomp("-value-,*PATH,-default-", "_dir_list")
2133 .unwrap();
2134 cache
2136 .conn
2137 .execute("DELETE FROM metadata WHERE key = 'completion_schema'", [])
2138 .unwrap();
2139
2140 cache.migrate_completion_tables().unwrap();
2141
2142 assert_eq!(
2143 cache.comp_count().unwrap(),
2144 0,
2145 "stale comps must be dropped"
2146 );
2147 assert_eq!(cache.patcomps_count().unwrap(), 0);
2148 assert_eq!(
2149 cache.get_metadata("completion_schema").unwrap().as_deref(),
2150 Some(CompsysCache::COMPLETION_SCHEMA_GENERATION)
2151 );
2152 }
2153}